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.
- daehn-2.0.0/.github/scripts/api_contract.py +95 -0
- daehn-2.0.0/.github/scripts/coverage_report.py +53 -0
- daehn-2.0.0/.github/scripts/documentation.py +59 -0
- daehn-2.0.0/.github/scripts/documentation_examples.py +37 -0
- daehn-2.0.0/.github/scripts/environment.py +46 -0
- daehn-2.0.0/.github/scripts/numerical_regression.py +190 -0
- daehn-2.0.0/.github/scripts/package_install.py +105 -0
- daehn-2.0.0/.github/scripts/package_smoke.py +68 -0
- daehn-2.0.0/.github/scripts/performance.py +109 -0
- daehn-2.0.0/.github/scripts/quality_metrics.py +203 -0
- daehn-2.0.0/.github/scripts/testpypi_build.py +49 -0
- daehn-2.0.0/.github/validation/README.md +72 -0
- daehn-2.0.0/.github/validation/api.json +324 -0
- daehn-2.0.0/.github/validation/numerical.json +20 -0
- daehn-2.0.0/.github/validation/quality.json +111 -0
- daehn-2.0.0/.github/validation/requirements.txt +4 -0
- daehn-2.0.0/AUTHORS +4 -0
- daehn-2.0.0/CHANGELOG.md +49 -0
- daehn-2.0.0/CITATION.bib +7 -0
- daehn-2.0.0/CITATION.cff +11 -0
- daehn-2.0.0/CONTRIBUTING.md +58 -0
- daehn-2.0.0/LICENSE +201 -0
- daehn-2.0.0/MANIFEST.in +8 -0
- daehn-2.0.0/NOTICE +12 -0
- daehn-2.0.0/PKG-INFO +253 -0
- daehn-2.0.0/README.md +205 -0
- daehn-2.0.0/docs/BUILD.md +40 -0
- daehn-2.0.0/docs/COMPATIBILITY.md +34 -0
- daehn-2.0.0/docs/CONFIGURATION.md +35 -0
- daehn-2.0.0/docs/ECOSYSTEM.md +18 -0
- daehn-2.0.0/docs/INFERENCE.md +56 -0
- daehn-2.0.0/docs/INSTALL.md +28 -0
- daehn-2.0.0/docs/MIGRATION.md +24 -0
- daehn-2.0.0/docs/PROBLEM.md +56 -0
- daehn-2.0.0/docs/PUBLISHING.md +114 -0
- daehn-2.0.0/docs/VERSION.md +6 -0
- daehn-2.0.0/docs/_static/architecture.png +0 -0
- daehn-2.0.0/docs/_static/custom.css +7 -0
- daehn-2.0.0/docs/api/conditions.rst +17 -0
- daehn-2.0.0/docs/api/discretization.rst +18 -0
- daehn-2.0.0/docs/api/index.rst +14 -0
- daehn-2.0.0/docs/api/inference.rst +8 -0
- daehn-2.0.0/docs/api/system.rst +19 -0
- daehn-2.0.0/docs/api/training.rst +18 -0
- daehn-2.0.0/docs/changelog.rst +7 -0
- daehn-2.0.0/docs/citations.rst +8 -0
- daehn-2.0.0/docs/conf.py +55 -0
- daehn-2.0.0/docs/contributing.rst +7 -0
- daehn-2.0.0/docs/examples/case-studies.md +12 -0
- daehn-2.0.0/docs/examples/index.rst +8 -0
- daehn-2.0.0/docs/examples/quickstart.py +93 -0
- daehn-2.0.0/docs/examples/quickstart.rst +81 -0
- daehn-2.0.0/docs/index.rst +55 -0
- daehn-2.0.0/docs/license.rst +11 -0
- daehn-2.0.0/docs/requirements.txt +9 -0
- daehn-2.0.0/docs/user-guide/index.rst +11 -0
- daehn-2.0.0/pyproject.toml +72 -0
- daehn-2.0.0/setup.cfg +4 -0
- daehn-2.0.0/src/daehn/__init__.py +28 -0
- daehn-2.0.0/src/daehn/api/__init__.py +49 -0
- daehn-2.0.0/src/daehn/api/configuration.py +20 -0
- daehn-2.0.0/src/daehn/api/data.py +83 -0
- daehn-2.0.0/src/daehn/api/differential/__init__.py +71 -0
- daehn-2.0.0/src/daehn/api/differential/compiler.py +561 -0
- daehn-2.0.0/src/daehn/api/differential/conditions.py +78 -0
- daehn-2.0.0/src/daehn/api/differential/derivatives.py +71 -0
- daehn-2.0.0/src/daehn/api/differential/discretization.py +334 -0
- daehn-2.0.0/src/daehn/api/differential/equations.py +38 -0
- daehn-2.0.0/src/daehn/api/differential/reconstruction.py +252 -0
- daehn-2.0.0/src/daehn/api/differential/scaling.py +50 -0
- daehn-2.0.0/src/daehn/api/differential/symbols.py +76 -0
- daehn-2.0.0/src/daehn/api/differential/system.py +63 -0
- daehn-2.0.0/src/daehn/api/fit.py +840 -0
- daehn-2.0.0/src/daehn/api/logging.py +117 -0
- daehn-2.0.0/src/daehn/api/model.py +122 -0
- daehn-2.0.0/src/daehn/api/network.py +22 -0
- daehn-2.0.0/src/daehn/api/objectives.py +105 -0
- daehn-2.0.0/src/daehn/api/problem.py +20 -0
- daehn-2.0.0/src/daehn/api/projection.py +60 -0
- daehn-2.0.0/src/daehn/api/root.py +79 -0
- daehn-2.0.0/src/daehn/serialization/__init__.py +61 -0
- daehn-2.0.0/src/daehn/serialization/artifacts.py +8 -0
- daehn-2.0.0/src/daehn.egg-info/PKG-INFO +253 -0
- daehn-2.0.0/src/daehn.egg-info/SOURCES.txt +91 -0
- daehn-2.0.0/src/daehn.egg-info/dependency_links.txt +1 -0
- daehn-2.0.0/src/daehn.egg-info/requires.txt +25 -0
- daehn-2.0.0/src/daehn.egg-info/top_level.txt +1 -0
- daehn-2.0.0/tests/test_contracts.py +32 -0
- daehn-2.0.0/tests/test_data_reporting_training.py +130 -0
- daehn-2.0.0/tests/test_differential.py +131 -0
- daehn-2.0.0/tests/test_extended_differential.py +199 -0
- daehn-2.0.0/tests/test_integration.py +8 -0
- 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()
|