daehn 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. daehn-2.0.0/.github/scripts/api_contract.py +95 -0
  2. daehn-2.0.0/.github/scripts/coverage_report.py +53 -0
  3. daehn-2.0.0/.github/scripts/documentation.py +59 -0
  4. daehn-2.0.0/.github/scripts/documentation_examples.py +37 -0
  5. daehn-2.0.0/.github/scripts/environment.py +46 -0
  6. daehn-2.0.0/.github/scripts/numerical_regression.py +190 -0
  7. daehn-2.0.0/.github/scripts/package_install.py +105 -0
  8. daehn-2.0.0/.github/scripts/package_smoke.py +68 -0
  9. daehn-2.0.0/.github/scripts/performance.py +109 -0
  10. daehn-2.0.0/.github/scripts/quality_metrics.py +203 -0
  11. daehn-2.0.0/.github/scripts/testpypi_build.py +49 -0
  12. daehn-2.0.0/.github/validation/README.md +72 -0
  13. daehn-2.0.0/.github/validation/api.json +324 -0
  14. daehn-2.0.0/.github/validation/numerical.json +20 -0
  15. daehn-2.0.0/.github/validation/quality.json +111 -0
  16. daehn-2.0.0/.github/validation/requirements.txt +4 -0
  17. daehn-2.0.0/AUTHORS +4 -0
  18. daehn-2.0.0/CHANGELOG.md +49 -0
  19. daehn-2.0.0/CITATION.bib +7 -0
  20. daehn-2.0.0/CITATION.cff +11 -0
  21. daehn-2.0.0/CONTRIBUTING.md +58 -0
  22. daehn-2.0.0/LICENSE +201 -0
  23. daehn-2.0.0/MANIFEST.in +8 -0
  24. daehn-2.0.0/NOTICE +12 -0
  25. daehn-2.0.0/PKG-INFO +253 -0
  26. daehn-2.0.0/README.md +205 -0
  27. daehn-2.0.0/docs/BUILD.md +40 -0
  28. daehn-2.0.0/docs/COMPATIBILITY.md +34 -0
  29. daehn-2.0.0/docs/CONFIGURATION.md +35 -0
  30. daehn-2.0.0/docs/ECOSYSTEM.md +18 -0
  31. daehn-2.0.0/docs/INFERENCE.md +56 -0
  32. daehn-2.0.0/docs/INSTALL.md +28 -0
  33. daehn-2.0.0/docs/MIGRATION.md +24 -0
  34. daehn-2.0.0/docs/PROBLEM.md +56 -0
  35. daehn-2.0.0/docs/PUBLISHING.md +114 -0
  36. daehn-2.0.0/docs/VERSION.md +6 -0
  37. daehn-2.0.0/docs/_static/architecture.png +0 -0
  38. daehn-2.0.0/docs/_static/custom.css +7 -0
  39. daehn-2.0.0/docs/api/conditions.rst +17 -0
  40. daehn-2.0.0/docs/api/discretization.rst +18 -0
  41. daehn-2.0.0/docs/api/index.rst +14 -0
  42. daehn-2.0.0/docs/api/inference.rst +8 -0
  43. daehn-2.0.0/docs/api/system.rst +19 -0
  44. daehn-2.0.0/docs/api/training.rst +18 -0
  45. daehn-2.0.0/docs/changelog.rst +7 -0
  46. daehn-2.0.0/docs/citations.rst +8 -0
  47. daehn-2.0.0/docs/conf.py +55 -0
  48. daehn-2.0.0/docs/contributing.rst +7 -0
  49. daehn-2.0.0/docs/examples/case-studies.md +12 -0
  50. daehn-2.0.0/docs/examples/index.rst +8 -0
  51. daehn-2.0.0/docs/examples/quickstart.py +93 -0
  52. daehn-2.0.0/docs/examples/quickstart.rst +81 -0
  53. daehn-2.0.0/docs/index.rst +55 -0
  54. daehn-2.0.0/docs/license.rst +11 -0
  55. daehn-2.0.0/docs/requirements.txt +9 -0
  56. daehn-2.0.0/docs/user-guide/index.rst +11 -0
  57. daehn-2.0.0/pyproject.toml +72 -0
  58. daehn-2.0.0/setup.cfg +4 -0
  59. daehn-2.0.0/src/daehn/__init__.py +28 -0
  60. daehn-2.0.0/src/daehn/api/__init__.py +49 -0
  61. daehn-2.0.0/src/daehn/api/configuration.py +20 -0
  62. daehn-2.0.0/src/daehn/api/data.py +83 -0
  63. daehn-2.0.0/src/daehn/api/differential/__init__.py +71 -0
  64. daehn-2.0.0/src/daehn/api/differential/compiler.py +561 -0
  65. daehn-2.0.0/src/daehn/api/differential/conditions.py +78 -0
  66. daehn-2.0.0/src/daehn/api/differential/derivatives.py +71 -0
  67. daehn-2.0.0/src/daehn/api/differential/discretization.py +334 -0
  68. daehn-2.0.0/src/daehn/api/differential/equations.py +38 -0
  69. daehn-2.0.0/src/daehn/api/differential/reconstruction.py +252 -0
  70. daehn-2.0.0/src/daehn/api/differential/scaling.py +50 -0
  71. daehn-2.0.0/src/daehn/api/differential/symbols.py +76 -0
  72. daehn-2.0.0/src/daehn/api/differential/system.py +63 -0
  73. daehn-2.0.0/src/daehn/api/fit.py +840 -0
  74. daehn-2.0.0/src/daehn/api/logging.py +117 -0
  75. daehn-2.0.0/src/daehn/api/model.py +122 -0
  76. daehn-2.0.0/src/daehn/api/network.py +22 -0
  77. daehn-2.0.0/src/daehn/api/objectives.py +105 -0
  78. daehn-2.0.0/src/daehn/api/problem.py +20 -0
  79. daehn-2.0.0/src/daehn/api/projection.py +60 -0
  80. daehn-2.0.0/src/daehn/api/root.py +79 -0
  81. daehn-2.0.0/src/daehn/serialization/__init__.py +61 -0
  82. daehn-2.0.0/src/daehn/serialization/artifacts.py +8 -0
  83. daehn-2.0.0/src/daehn.egg-info/PKG-INFO +253 -0
  84. daehn-2.0.0/src/daehn.egg-info/SOURCES.txt +91 -0
  85. daehn-2.0.0/src/daehn.egg-info/dependency_links.txt +1 -0
  86. daehn-2.0.0/src/daehn.egg-info/requires.txt +25 -0
  87. daehn-2.0.0/src/daehn.egg-info/top_level.txt +1 -0
  88. daehn-2.0.0/tests/test_contracts.py +32 -0
  89. daehn-2.0.0/tests/test_data_reporting_training.py +130 -0
  90. daehn-2.0.0/tests/test_differential.py +131 -0
  91. daehn-2.0.0/tests/test_extended_differential.py +199 -0
  92. daehn-2.0.0/tests/test_integration.py +8 -0
  93. daehn-2.0.0/tests/test_optional.py +78 -0
@@ -0,0 +1,95 @@
1
+ """Freeze public Python signatures; additions are allowed, removals/changes need review."""
2
+
3
+ import argparse
4
+ import ast
5
+ import json
6
+ from pathlib import Path
7
+
8
+
9
+ def surface(root):
10
+ entries = {}
11
+ for path in sorted((root / "src").rglob("*.py")):
12
+ module = ".".join(path.relative_to(root / "src").with_suffix("").parts)
13
+ tree = ast.parse(path.read_text(encoding="utf-8"))
14
+
15
+ def collect(body, prefix=""):
16
+ for node in body:
17
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
18
+ if node.name.startswith("_") and node.name not in {
19
+ "__init__",
20
+ "__call__",
21
+ }:
22
+ continue
23
+ entries[f"{module}:{prefix}{node.name}"] = (
24
+ ast.unparse(node.args)
25
+ + " | decorators="
26
+ + ",".join(ast.unparse(d) for d in node.decorator_list)
27
+ )
28
+ elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
29
+ collect(node.body, prefix + node.name + ".")
30
+ for field in node.body:
31
+ if isinstance(field, ast.AnnAssign) and isinstance(
32
+ field.target, ast.Name
33
+ ):
34
+ entries[
35
+ f"{module}:{prefix}{node.name}.{field.target.id}"
36
+ ] = ast.unparse(field.annotation) + (
37
+ "=" + ast.unparse(field.value)
38
+ if field.value is not None
39
+ else ""
40
+ )
41
+ elif isinstance(node, ast.Assign):
42
+ names = [t.id for t in node.targets if isinstance(t, ast.Name)]
43
+ if "__all__" in names and isinstance(
44
+ node.value, (ast.List, ast.Tuple)
45
+ ):
46
+ for item in node.value.elts:
47
+ if isinstance(item, ast.Constant) and isinstance(
48
+ item.value, str
49
+ ):
50
+ entries[f"{module}:export:{item.value}"] = "declared"
51
+ if "_EXPORTS" in names and isinstance(node.value, ast.Dict):
52
+ for key, value in zip(node.value.keys, node.value.values):
53
+ if isinstance(key, ast.Constant):
54
+ entries[f"{module}:export:{key.value}"] = ast.unparse(
55
+ value
56
+ )
57
+
58
+ collect(tree.body)
59
+ return entries
60
+
61
+
62
+ def compare(current, baseline):
63
+ return {
64
+ name: {"expected": value, "actual": current.get(name)}
65
+ for name, value in baseline.items()
66
+ if current.get(name) != value
67
+ }
68
+
69
+
70
+ def main():
71
+ parser = argparse.ArgumentParser(description=__doc__)
72
+ parser.add_argument("--write-baseline", action="store_true")
73
+ args = parser.parse_args()
74
+ root = Path(__file__).resolve().parents[2]
75
+ path = root / ".github/validation/api.json"
76
+ current = surface(root)
77
+ if args.write_baseline:
78
+ path.parent.mkdir(parents=True, exist_ok=True)
79
+ path.write_text(
80
+ json.dumps(current, indent=2, sort_keys=True) + "\n", encoding="utf-8"
81
+ )
82
+ else:
83
+ changes = compare(current, json.loads(path.read_text()))
84
+ if changes:
85
+ raise SystemExit(
86
+ "Public API changed; review compatibility before updating the baseline:\n"
87
+ + json.dumps(changes, indent=2)
88
+ )
89
+ assert compare({"f": "x"}, {"f": "x"}) == {}
90
+ assert compare({}, {"f": "x"}) and compare({"f": "y"}, {"f": "x"})
91
+ print(f"Public API compatible: {len(current)} declarations checked.")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -0,0 +1,53 @@
1
+ """Publish a badge and a small report from coverage.py's measured totals."""
2
+
3
+ import html
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+
9
+
10
+ def main(source, destination):
11
+ report = json.loads(Path(source).read_text(encoding="utf-8"))
12
+ totals = report["totals"]
13
+ percent = float(totals["percent_covered"])
14
+ color = "4c1" if percent >= 90 else "97ca00" if percent >= 75 else "dfb317"
15
+ label = f"{percent:.1f}%"
16
+ svg = (
17
+ '<svg xmlns="http://www.w3.org/2000/svg" width="130" height="20" '
18
+ 'role="img" aria-label="Coverage: ' + label + '">'
19
+ "<title>Coverage: " + label + "</title>"
20
+ '<rect width="75" height="20" fill="#555"/>'
21
+ '<rect x="75" width="55" height="20" fill="#' + color + '"/>'
22
+ '<g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" '
23
+ 'font-size="11"><text x="37.5" y="14">coverage</text>'
24
+ '<text x="102.5" y="14">' + html.escape(label) + "</text></g></svg>\n"
25
+ )
26
+ destination = Path(destination)
27
+ destination.mkdir(parents=True, exist_ok=True)
28
+ (destination / "coverage.svg").write_text(svg, encoding="utf-8")
29
+ summary = {
30
+ "commit": os.environ.get("GITHUB_SHA"),
31
+ "measurement": "statement and branch coverage, entire package",
32
+ "totals": totals,
33
+ }
34
+ (destination / "coverage-summary.json").write_text(
35
+ json.dumps(summary, indent=2) + "\n", encoding="utf-8"
36
+ )
37
+ message = (
38
+ f"## Coverage: {label}\n\n"
39
+ "Measured over the entire package, including unexecuted modules. "
40
+ "The percentage combines statements and branches.\n\n"
41
+ f"- Covered statements: {totals['covered_lines']}/{totals['num_statements']}\n"
42
+ f"- Covered branches: {totals['covered_branches']}/{totals['num_branches']}\n"
43
+ f"- Source commit: {os.environ.get('GITHUB_SHA', 'local')}\n"
44
+ )
45
+ (destination / "README.md").write_text(message, encoding="utf-8")
46
+ if os.environ.get("GITHUB_STEP_SUMMARY"):
47
+ with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
48
+ handle.write(message)
49
+ print(message)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main(*sys.argv[1:])
@@ -0,0 +1,59 @@
1
+ """Build the real Sphinx site and validate citation metadata."""
2
+
3
+ import json
4
+ import re
5
+ from urllib.parse import unquote, urlsplit
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+ from pybtex.database import parse_file
10
+ import yaml
11
+
12
+
13
+ def main():
14
+ root = Path(__file__).resolve().parents[2]
15
+ for link in re.findall(
16
+ r"\]\(([^)]+)\)", (root / "README.md").read_text(encoding="utf-8")
17
+ ):
18
+ url = urlsplit(link)
19
+ if url.scheme or not url.path:
20
+ continue
21
+ target = (root / unquote(url.path)).resolve()
22
+ if not target.is_relative_to(root) or not target.exists():
23
+ raise ValueError("Broken README target: " + link)
24
+ citation = yaml.safe_load((root / "CITATION.cff").read_text(encoding="utf-8"))
25
+ assert (
26
+ citation["cff-version"] == "1.2.0" and citation["title"] and citation["authors"]
27
+ )
28
+ entries = parse_file(str(root / "CITATION.bib")).entries
29
+ assert entries and all(entry.fields.get("title") for entry in entries.values())
30
+ subprocess.run(
31
+ [
32
+ sys.executable,
33
+ "-m",
34
+ "sphinx",
35
+ "-b",
36
+ "html",
37
+ "-W",
38
+ "--keep-going",
39
+ str(root / "docs"),
40
+ str(root / "site"),
41
+ ],
42
+ check=True,
43
+ )
44
+ report = {
45
+ "source_pages": len(list((root / "docs").rglob("*.rst")))
46
+ + len(list((root / "docs").rglob("*.md"))),
47
+ "citation_entries": len(entries),
48
+ "html_build": "site/index.html",
49
+ "builder": "Sphinx / sphinx_rtd_theme",
50
+ "warnings_as_errors": True,
51
+ }
52
+ (root / "documentation-results.json").write_text(
53
+ json.dumps(report, indent=2), encoding="utf-8"
54
+ )
55
+ print(json.dumps(report, indent=2))
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,37 @@
1
+ """Run checked documentation examples in isolated working directories."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+
10
+ ROOT = Path(__file__).resolve().parents[2]
11
+
12
+
13
+ def main():
14
+ scripts = sorted((ROOT / "docs/examples").glob("*.py"))
15
+ if not scripts:
16
+ raise RuntimeError("No checked documentation examples")
17
+ environment = dict(
18
+ os.environ,
19
+ JAX_ENABLE_X64="true",
20
+ JAX_PLATFORMS="cpu",
21
+ MPLBACKEND="Agg",
22
+ PYTHONDONTWRITEBYTECODE="1",
23
+ )
24
+ for script in scripts:
25
+ with tempfile.TemporaryDirectory(prefix="hardnet-doc-example-") as temporary:
26
+ subprocess.run(
27
+ [sys.executable, str(script)],
28
+ cwd=temporary,
29
+ env=environment,
30
+ check=True,
31
+ timeout=240,
32
+ )
33
+ print(json.dumps({"examples_passed": len(scripts)}, indent=2))
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,46 @@
1
+ """Record only public runtime configuration for cross-platform CI artifacts."""
2
+
3
+ from importlib import metadata
4
+ import json
5
+ from pathlib import Path
6
+ import platform
7
+ import sys
8
+
9
+ import jax
10
+
11
+
12
+ def main():
13
+ versions = {}
14
+ for name in (
15
+ "daehn",
16
+ "jax",
17
+ "jaxlib",
18
+ "numpy",
19
+ "scipy",
20
+ "ohagi",
21
+ "purin",
22
+ "flax",
23
+ "optax",
24
+ "orbax-checkpoint",
25
+ ):
26
+ try:
27
+ versions[name] = metadata.version(name)
28
+ except metadata.PackageNotFoundError:
29
+ versions[name] = None
30
+ report = {
31
+ "python": sys.version,
32
+ "system": platform.system(),
33
+ "release": platform.release(),
34
+ "machine": platform.machine(),
35
+ "versions": versions,
36
+ "devices": [str(device) for device in jax.devices()],
37
+ "x64_enabled": bool(jax.config.jax_enable_x64),
38
+ }
39
+ Path("ci-environment.json").write_text(
40
+ json.dumps(report, indent=2) + "\n", encoding="utf-8"
41
+ )
42
+ print(json.dumps(report, indent=2))
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,190 @@
1
+ import argparse
2
+
3
+
4
+ import json
5
+
6
+ import os
7
+
8
+ from pathlib import Path
9
+
10
+ import subprocess
11
+
12
+ import sys
13
+
14
+ import tempfile
15
+
16
+ import jax
17
+
18
+ import jax.numpy as jnp
19
+
20
+ import numpy as np
21
+
22
+
23
+ def reload_check(path, inputs, expected, framework, full_metadata=None):
24
+ code = """
25
+ import importlib.abc, json, sys
26
+ import numpy as np
27
+ class Block(importlib.abc.MetaPathFinder):
28
+ def find_spec(self, fullname, path=None, target=None):
29
+ if fullname.split('.')[0] in {'kkthn','daehn','nlpoptnet','flax','orbax'}:
30
+ raise AssertionError('Deployment imported '+fullname)
31
+ sys.meta_path.insert(0,Block())
32
+ from ohagi.inference import load
33
+ predictor=load(sys.argv[1])
34
+ x=json.loads(sys.argv[2]); expected=json.loads(sys.argv[3])
35
+ for _ in range(25):
36
+ np.testing.assert_allclose(predictor.predict(x),expected,atol=1e-8,rtol=1e-7)
37
+ np.testing.assert_allclose(predictor.predict_jax(x),expected,atol=1e-8,rtol=1e-7)
38
+ """
39
+ subprocess.run(
40
+ [
41
+ sys.executable,
42
+ "-B",
43
+ "-c",
44
+ code,
45
+ str(path),
46
+ json.dumps(np.asarray(inputs).tolist()),
47
+ json.dumps(np.asarray(expected).tolist()),
48
+ ],
49
+ check=True,
50
+ timeout=180,
51
+ )
52
+ if framework == "kkthn":
53
+ from kkthn import KKTHardNet
54
+
55
+ facade = KKTHardNet().load(full_metadata or path)
56
+ elif framework == "daehn":
57
+ from daehn import DAEHardNet
58
+
59
+ facade = DAEHardNet().load(path)
60
+ else:
61
+ from nlpoptnet import NLPOptNet
62
+
63
+ facade = NLPOptNet().load(full_metadata or path)
64
+ np.testing.assert_allclose(
65
+ np.asarray(facade.predict(np.asarray(inputs)[None])).reshape(-1),
66
+ np.asarray(expected).reshape(-1),
67
+ atol=1e-8,
68
+ rtol=1e-7,
69
+ )
70
+
71
+
72
+ def daehn(root):
73
+ from daehn.dae import (
74
+ Coordinate,
75
+ DifferentialState,
76
+ DerivativeSpec,
77
+ EquationBlock,
78
+ DAESystem,
79
+ TaylorNeighborhood,
80
+ ForwardDifference,
81
+ CentralDifference,
82
+ compile_dae,
83
+ DAETrainer,
84
+ DAETrainingConfig,
85
+ )
86
+ from daehn import MLPBackbone
87
+ from ohagi.inference import export, load
88
+
89
+ records = {}
90
+ for name, strategy in [
91
+ ("taylor", TaylorNeighborhood(deltas=(0.01,), order=1)),
92
+ ("forward", ForwardDifference(deltas=(0.01,))),
93
+ ("central", CentralDifference(deltas=(0.01,))),
94
+ ]:
95
+ state = DifferentialState("y")
96
+ system = DAESystem(
97
+ coordinates=(Coordinate("t"),),
98
+ differential_states=(state,),
99
+ derivatives=(DerivativeSpec(state, (1,)),),
100
+ differential_equations=EquationBlock(
101
+ lambda c, s, d, p: jnp.array([d[0] + s[0]]),
102
+ size=1,
103
+ affine_in_projection_variables=True,
104
+ ),
105
+ algebraic_equalities=EquationBlock(affine_in_projection_variables=True),
106
+ algebraic_inequalities=EquationBlock(affine_in_projection_variables=True),
107
+ name="decay",
108
+ )
109
+ compiled = compile_dae(system, discretization=strategy)
110
+ trainer = DAETrainer(
111
+ compiled=compiled,
112
+ prepared_projection=compiled.prepare_projection(
113
+ batch_size=2, method="linear"
114
+ ),
115
+ backbone=MLPBackbone(hidden_sizes=(4,)),
116
+ config=DAETrainingConfig(
117
+ epochs=2,
118
+ pretrain_epochs=1,
119
+ batch_size=2,
120
+ hidden_sizes=(4,),
121
+ seed=7,
122
+ print_every=0,
123
+ ),
124
+ )
125
+ x = np.linspace(0, 1, 8)[:, None]
126
+ p = np.empty((8, 0))
127
+ y = np.exp(-x)
128
+ result = trainer.fit(x[:4], p[:4], y[:4], -y[:4], x[4:], p[4:], y[4:], -y[4:])
129
+ decoded, *_ = trainer.model.forward(
130
+ result.parameters, jnp.asarray(x[:2]), jnp.asarray(p[:2])
131
+ )
132
+ np.testing.assert_allclose(
133
+ decoded["states"] + decoded["derivatives"], 0, atol=1e-7
134
+ )
135
+
136
+ def graph(inputs):
137
+ decoded, *_ = trainer.model.forward(
138
+ result.parameters, jnp.repeat(inputs, 2, axis=0), jnp.empty((2, 0))
139
+ )
140
+ return decoded["states"][:1]
141
+
142
+ path = root / name / "metadata.json"
143
+ export(path, graph, input_size=1, output_size=1, framework="daehn")
144
+ expected = load(path).predict([0.7])
145
+ reload_check(path, [0.7], expected, "daehn")
146
+ records[name] = {"epochs": len(result.history), "prediction": expected.tolist()}
147
+ return records
148
+
149
+
150
+ def run_case(directory):
151
+ """Exercise the installed frontend through short training and fresh-process reload."""
152
+ jax.config.update("jax_enable_x64", True)
153
+ original = Path.cwd()
154
+ os.chdir(directory)
155
+ try:
156
+ return daehn(Path(directory))
157
+ finally:
158
+ os.chdir(original)
159
+
160
+
161
+ def main():
162
+ parser = argparse.ArgumentParser(description=__doc__)
163
+ parser.add_argument("--write-baseline", action="store_true")
164
+ args = parser.parse_args()
165
+ root = Path(__file__).resolve().parents[2]
166
+ with tempfile.TemporaryDirectory() as directory:
167
+ result = run_case(directory)
168
+ baseline = root / ".github/validation/numerical.json"
169
+ if args.write_baseline:
170
+ baseline.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
171
+ else:
172
+ expected = json.loads(baseline.read_text())
173
+
174
+ def check(actual, reference):
175
+ if isinstance(reference, dict):
176
+ assert actual.keys() == reference.keys()
177
+ for key in reference:
178
+ check(actual[key], reference[key])
179
+ else:
180
+ np.testing.assert_allclose(actual, reference, atol=2e-5, rtol=2e-5)
181
+
182
+ check(result, expected)
183
+ (root / "numerical-results.json").write_text(
184
+ json.dumps(result, indent=2) + "\n", encoding="utf-8"
185
+ )
186
+ print(json.dumps(result, indent=2))
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -0,0 +1,105 @@
1
+ """Install exact stack wheels from PyPI or TestPyPI and verify dependencies."""
2
+
3
+ import argparse
4
+ from email.parser import BytesParser
5
+ import json
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import zipfile
11
+
12
+ TARGET = "daehn"
13
+ DEPENDENCIES = {"purin": "1.0.0", "ohagi": "1.0.0"}
14
+
15
+
16
+ def wheel_identity(path):
17
+ with zipfile.ZipFile(path) as archive:
18
+ metadata = next(
19
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
20
+ )
21
+ fields = BytesParser().parsebytes(archive.read(metadata))
22
+ return fields["Name"], fields["Version"]
23
+
24
+
25
+ def main():
26
+ parser = argparse.ArgumentParser(description=__doc__)
27
+ parser.add_argument("--version", required=True)
28
+ parser.add_argument(
29
+ "--local-dir", type=Path, help="Use matching local wheels before downloading"
30
+ )
31
+ parser.add_argument(
32
+ "--testpypi", action="store_true", help="Use the isolated testing index"
33
+ )
34
+ args = parser.parse_args()
35
+ target = "kkthn" if args.testpypi and TARGET == "kkt-hardnet" else TARGET
36
+ index = (
37
+ "https://test.pypi.org/simple/" if args.testpypi else "https://pypi.org/simple/"
38
+ )
39
+ requirements = dict(DEPENDENCIES, **{target: args.version})
40
+ with tempfile.TemporaryDirectory(prefix="stack-wheels-") as directory:
41
+ downloads = Path(directory)
42
+ wheels = []
43
+ origins = {}
44
+ for name, version in requirements.items():
45
+ matches = (
46
+ []
47
+ if args.local_dir is None
48
+ else [
49
+ path
50
+ for path in args.local_dir.glob("*.whl")
51
+ if wheel_identity(path) == (name, version)
52
+ ]
53
+ )
54
+ if len(matches) > 1:
55
+ raise ValueError("Ambiguous local wheel: " + name)
56
+ if matches:
57
+ wheel = matches[0].resolve()
58
+ origins[name] = "local build"
59
+ else:
60
+ subprocess.run(
61
+ [
62
+ sys.executable,
63
+ "-m",
64
+ "pip",
65
+ "download",
66
+ "--no-deps",
67
+ "--only-binary=:all:",
68
+ "--index-url",
69
+ index,
70
+ "--dest",
71
+ str(downloads),
72
+ name + "==" + version,
73
+ ],
74
+ check=True,
75
+ )
76
+ wheel = next(
77
+ path
78
+ for path in downloads.glob("*.whl")
79
+ if wheel_identity(path) == (name, version)
80
+ )
81
+ origins[name] = "TestPyPI" if args.testpypi else "PyPI"
82
+ wheels.append(str(wheel))
83
+ subprocess.run(
84
+ [
85
+ sys.executable,
86
+ "-m",
87
+ "pip",
88
+ "install",
89
+ "--index-url",
90
+ "https://pypi.org/simple/",
91
+ "--only-binary=:all:",
92
+ "--report",
93
+ "package-install-results.json",
94
+ *wheels,
95
+ ],
96
+ check=True,
97
+ )
98
+ subprocess.run([sys.executable, "-m", "pip", "check"], check=True)
99
+ print(
100
+ json.dumps({"versions": requirements, "stack_wheel_origins": origins}, indent=2)
101
+ )
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,68 @@
1
+ """Run installed distributions outside the checkout, including numerical and export checks."""
2
+
3
+ import argparse
4
+ import importlib
5
+ from importlib.metadata import version
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+
14
+ PACKAGE = "daehn"
15
+ DISTRIBUTION = "daehn"
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument("--version", required=True)
21
+ parser.add_argument("--distribution", default=DISTRIBUTION)
22
+ args = parser.parse_args()
23
+ root = Path(__file__).resolve().parents[2]
24
+ module = importlib.import_module(PACKAGE)
25
+ installed = Path(module.__file__).resolve()
26
+ if root in installed.parents:
27
+ raise RuntimeError(
28
+ "Smoke test imported the checkout instead of an installed wheel"
29
+ )
30
+ assert version(args.distribution) == args.version
31
+ assert module.__version__ == args.version
32
+ environment = dict(
33
+ os.environ,
34
+ JAX_PLATFORMS="cpu",
35
+ JAX_ENABLE_X64="true",
36
+ PYTHONNOUSERSITE="1",
37
+ PYTHONDONTWRITEBYTECODE="1",
38
+ MPLBACKEND="Agg",
39
+ )
40
+ environment.pop("PYTHONPATH", None)
41
+ scripts = sorted((root / "docs/examples").glob("*.py"))
42
+ assert scripts, "No runnable release examples found"
43
+ with tempfile.TemporaryDirectory(prefix=PACKAGE + "-smoke-") as directory:
44
+ work = Path(directory)
45
+ for script in scripts:
46
+ target = work / script.name
47
+ shutil.copyfile(script, target)
48
+ subprocess.run(
49
+ [sys.executable, "-I", str(target)],
50
+ cwd=work,
51
+ env=environment,
52
+ check=True,
53
+ timeout=300,
54
+ )
55
+ report = {
56
+ "distribution": args.distribution,
57
+ "version": version(args.distribution),
58
+ "installed_module": str(installed),
59
+ "examples": [path.name for path in scripts],
60
+ }
61
+ Path("package-smoke-results.json").write_text(
62
+ json.dumps(report, indent=2), encoding="utf-8"
63
+ )
64
+ print(json.dumps(report, indent=2))
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()