Skip to content

Commit 04f2dc2

Browse files
Merge pull request #582 from dynamicslab/example-directory
Template for removing examples, including removing example 3
2 parents 54cb11e + 8ae0a0a commit 04f2dc2

File tree

9 files changed

+185
-1407
lines changed

9 files changed

+185
-1407
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
on:
2+
push:
3+
branches:
4+
- master
5+
6+
jobs:
7+
notify-notebook:
8+
runs-on: ubuntu-latest
9+
steps:
10+
# Add more like these when linking external example CI
11+
- name: Inform original paper
12+
uses: peter-evans/repository-dispatch@v3
13+
with:
14+
token: ${{ secrets.PYSINDY_EXAMPLE_PAT }}
15+
repository: dynamicslab/sindy-original-example
16+
event-type: pysindy-commit

README.rst

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -173,25 +173,24 @@ Community guidelines
173173

174174
Contributing examples
175175
^^^^^^^^^^^^^^^^^^^^^
176-
We love seeing examples of PySINDy being used to solve interesting problems! If you would like to contribute an example, reach out to us by creating an issue.
177-
178-
At a minimum, we need to be able to run the example notebooks in the normal mode as well as in a test mode that uses smaller data in order to run faster and simply verify that cells execute without error. In order to do that, your example should obey the following directory tree
179-
180-
.. code-block::
181-
182-
./<name_of_example>/
183-
\
184-
|-example.py # save your notebook as a python script
185-
|-example_data.py # has functions to create/load data
186-
|-mock_data.py # has functions with same name as in example_data.py which create/load smaller datasets
187-
|-example.ipynb # run python examples/publish_notebook/<name_of_example> to generate this. Needs packages in requirements-dev.txt
188-
|-utils.py (Any other names example.py needs to import. Any additional local modules imported by example.py need to be submodules of utils.py, e.g. utils.plotting)
189-
190-
You can optimize your notebook for testing by checking ``__name__``. When our tests run ``example.py`` they set the ``__name__`` global to ``"testing"``. For instance, your notebook should determine whether to import from ``mock_data`` or ``example_data`` using this method (another example: you could also use this method to set ``max_iter``). It's a bit arbitrary, but try to make your examples run in under ten seconds using the mock data. You can use our test to verify your example in testing mode:
191-
192-
.. code-block::
193-
194-
pytest -k test_external --external-notebook="path/to/<name_of_example>"
176+
We love seeing examples of PySINDy being used to solve interesting problems! If you would like to contribute an example to the documentation, reach out to us by creating an issue.
177+
178+
Examples are external repositories that
179+
`follow a structure <https://github.com/dynamicslab/pysindy-example>`_ that pysindy
180+
knows how to incorporate into its documentation build. They tend to be pinned to
181+
a set of dependencies and may not be kept up to date with breaking API changes.
182+
183+
The linked repository has information on how to set up your example. To PR the example
184+
into this repository, (a) edit examples/external.yml and examples/README.rst with your
185+
repository information and (b) verify your own build passes in your repository,
186+
including publishing on github pages. If you want to keep your example up to date with
187+
the pysindy main branch, (c) add your repository information to the ``notify-experiments``
188+
workflow so that pysindy will trigger your notebooks to be run in CI in your own repo.
189+
This will require adding a
190+
`fine-grained PAT <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens>`_
191+
with the permissions ``contents: read & write`` and ``metadata: read only`` to the
192+
pysindy GH secrets. Alternatively, you can just trigger your builds based on cron timing.
193+
See the pysindy experiments repo for more information.
195194

196195

197196
Contributing code

docs/conf.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import importlib
2+
import os
3+
import re
24
import shutil
35
from pathlib import Path
6+
from typing import TypeVar
7+
8+
import requests
9+
import yaml
10+
from docutils import nodes
11+
from docutils.statemachine import StringList
12+
from sphinx.application import Sphinx
13+
from sphinx.directives.other import TocTree
14+
from sphinx.util.docutils import SphinxDirective
415

516
author = "dynamicslab"
617
project = "pysindy" # package name
@@ -42,9 +53,6 @@
4253

4354
here = Path(__file__).parent.resolve()
4455

45-
if (here / "static/custom.css").exists():
46-
html_static_path = ["static"]
47-
4856
exclude_patterns = ["build", "_build", "Youtube"]
4957
# pygments_style = "sphinx"
5058

@@ -105,7 +113,7 @@ def patched_parse(self):
105113
GoogleDocstring._parse = patched_parse
106114

107115

108-
def setup(app):
116+
def setup(app: Sphinx):
109117
"""Our sphinx extension for copying from examples/ to docs/examples
110118
111119
Since nbsphinx does not handle glob/regex paths, we need to
@@ -135,3 +143,113 @@ def setup(app):
135143
)
136144
if (here / "static/custom.css").exists():
137145
app.add_css_file("custom.css")
146+
147+
_grab_external_examples(example_source)
148+
app.add_directive("pysindy-example", PysindyExample)
149+
150+
151+
EXTERNAL_EXAMPLES: dict[str, list[tuple[str, str]]] = {}
152+
153+
154+
def _load_ext_config(example_source: Path) -> list[dict[str, str]]:
155+
ext_config = example_source / "external.yml"
156+
with open(ext_config) as f:
157+
ext_examples = yaml.safe_load(f)
158+
return ext_examples
159+
160+
161+
def _grab_external_examples(example_source: Path):
162+
ext_examples = _load_ext_config(example_source)
163+
for example in ext_examples:
164+
ex_name = example["name"]
165+
user = example["user"]
166+
repo = example["repo"]
167+
ref = example["ref"]
168+
dir = example["dir"]
169+
base = f"https://raw.githubusercontent.com/{user}/{repo}/{ref}/{dir}/"
170+
notebooks = fetch_notebook_list(base)
171+
base = f"https://raw.githubusercontent.com/{user}/{repo}/{ref}/"
172+
local_nbs = [(name, copy_nb(base, pth, repo)) for name, pth in notebooks]
173+
EXTERNAL_EXAMPLES[ex_name] = local_nbs
174+
175+
176+
class PysindyExample(SphinxDirective):
177+
required_arguments = 0
178+
optional_arguments = 0
179+
option_spec = {"key": str, "title": str}
180+
has_content = True
181+
182+
def run(self) -> list[nodes.Node]:
183+
key = self.options["key"]
184+
example_config = _load_ext_config((here / "../examples").resolve())
185+
try:
186+
this_example = [ex for ex in example_config if ex["name"] == key][0]
187+
except IndexError:
188+
RuntimeError("Unknown configuration key for external example")
189+
heading_text: str = self.options.get("title")
190+
base_repo = f"https://github.com/{this_example['user']}/{this_example['repo']}"
191+
repo_ref = nodes.reference(name="Source repo", refuri=base_repo)
192+
ref_text = nodes.Text("Source repo")
193+
repo_ref += ref_text
194+
repo_par = nodes.paragraph()
195+
repo_par += repo_ref
196+
normalized_text = re.sub(r"\s", "_", heading_text)
197+
tgt_node = nodes.target(refid=normalized_text)
198+
title_node = nodes.title()
199+
title_text = nodes.Text(heading_text)
200+
title_node += [title_text, tgt_node]
201+
content_nodes = self.parse_content_to_nodes()
202+
toc_items = []
203+
for name, relpath in EXTERNAL_EXAMPLES[key]:
204+
if name:
205+
toc_str = f"{name} <{relpath}>"
206+
if not name:
207+
toc_str = relpath
208+
toc_items.append(toc_str)
209+
toc_nodes = TocTree(
210+
name="PysindyExample",
211+
options={"maxdepth": 1},
212+
arguments=[],
213+
content=StringList(initlist=toc_items),
214+
lineno=self.lineno,
215+
block_text="",
216+
content_offset=self.content_offset,
217+
state=self.state,
218+
state_machine=self.state_machine,
219+
).run()
220+
section_node = nodes.section(ids=[heading_text], names=[heading_text])
221+
section_node += [title_node, *content_nodes, *toc_nodes, repo_par]
222+
return [section_node]
223+
224+
225+
def fetch_notebook_list(base: str) -> list[tuple[str, str]]:
226+
"""Gets the list of example notebooks from a repo's index.html
227+
228+
Each entry is a tuple of the title name of a link and the address
229+
"""
230+
index = requests.get(base + "index.rst")
231+
if index.status_code != 200:
232+
raise RuntimeError("Unable to locate external example directory")
233+
text = str(index.content, encoding="utf-8")
234+
link_line = r"^\s+(.*)[^\S\r\n]+(\S+.ipynb)"
235+
T = TypeVar("T")
236+
237+
def deduplicate(mylist: list[T]) -> list[T]:
238+
return list(set(mylist))
239+
240+
rellinks = deduplicate(re.findall(link_line, text, flags=re.MULTILINE))
241+
return rellinks
242+
243+
244+
def copy_nb(base: str, relpath: str, repo: str) -> str:
245+
"""Create a local copy of external file, modifying relative reference"""
246+
example_dir = Path(__file__).parent / "examples"
247+
repo_local_dir = example_dir / repo
248+
repo_local_dir.mkdir(exist_ok=True)
249+
page = requests.get(base + relpath)
250+
if page.status_code != 200:
251+
raise RuntimeError(f"Unable to locate external notebook at {base + relpath}")
252+
filename = repo_local_dir / relpath.rsplit("/", 1)[1]
253+
with open(filename, "wb") as f:
254+
f.write(page.content)
255+
return os.path.relpath(filename, start=example_dir)

examples/3_original_paper.ipynb

Lines changed: 0 additions & 1365 deletions
This file was deleted.

examples/README.rst

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
PySINDy Examples
22
================
33

4-
This directory showcases the following examples of PySINDy in action.
4+
This directory showcases examples of PySINDy in action. Not all examples are run on
5+
the current master branch. They serve to show what is possible with pysindy, but do
6+
not necessarily use the current API.
7+
Each is copied from another repository that contains dependency information and
8+
potentially a greater description.
59

6-
`Feature overview <./1_feature_overview/example.ipynb>`_
10+
Some notebooks require substantial computing resources.
11+
12+
Feature overview
713
-----------------------------------------------------------------------------------------------------------
814
This notebook gives an almost exhaustive overview of the different features available in PySINDy. It's a good reference for how to set various options and work with different types of datasets.
915

@@ -24,23 +30,20 @@ We recommend that people new to SINDy start here. We give a gentle introduction
2430

2531
./2_introduction_to_sindy/example
2632

27-
`Original paper <./3_original_paper/example.ipynb>`_
28-
-------------------------------------------------------------------------------------------------------
29-
This notebook uses PySINDy to reproduce the examples in the `original SINDy paper <https://www.pnas.org/content/pnas/113/15/3932.full.pdf>`_. Namely, it applies PySINDy to the following problems:
30-
31-
* Linear 2D ODE
32-
* Cubic 2D ODE
33-
* Linear 3D ODE
34-
* Lorenz system
35-
* Fluid wake behind a cylinder
36-
* Logistic map
37-
* Hopf system
3833

39-
.. toctree::
40-
:hidden:
41-
:maxdepth: 1
34+
.. pysindy-example::
35+
:key: original
36+
:title: Original Paper
4237

43-
./3_original_paper/example
38+
This repository recreates the results from the `original SINDy paper <https://www.pnas.org/content/pnas/113/15/3932.full.pdf>`_.
39+
It applies SINDy to the following problems:
40+
* Linear 2D ODE
41+
* Cubic 2D ODE
42+
* Linear 3D ODE
43+
* Lorenz system
44+
* Fluid wake behind a cylinder
45+
* Logistic map
46+
* Hopf system
4447

4548
`Scikit-learn compatibility <./4_scikit_learn_compatibility/example.ipynb>`_
4649
-------------------------------------------------------------------------------------------------------------------------------

examples/data/PODcoefficients.mat

-1.1 MB
Binary file not shown.
-403 KB
Binary file not shown.

examples/external.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- name: "original"
2+
user: "dynamicslab"
3+
repo: "sindy-original-example"
4+
ref: "e68efeb"
5+
dir: "examples"

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ dev = [
5151
]
5252
docs = [
5353
"ipython",
54+
"nbsphinx",
5455
"pandoc",
56+
"requests",
5557
"sphinx-rtd-theme",
56-
"sphinx==7.1.2",
58+
"sphinx==7.4.7",
59+
"pyyaml",
5760
"sphinxcontrib-apidoc",
58-
"nbsphinx"
5961
]
6062
miosr = [
6163
"gurobipy>=9.5.1,!=10.0.0"

0 commit comments

Comments
 (0)