ohagi 1.0.0__py3-none-any.whl
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.
- ohagi/__init__.py +65 -0
- ohagi/_projection_aliases.py +198 -0
- ohagi/_utils/__init__.py +8 -0
- ohagi/_utils/serialization.py +68 -0
- ohagi/_utils/tree.py +23 -0
- ohagi/compat/__init__.py +5 -0
- ohagi/compat/aliases.py +87 -0
- ohagi/compat/paths.py +9 -0
- ohagi/compat/plugins.py +94 -0
- ohagi/data/__init__.py +4 -0
- ohagi/data/batching.py +70 -0
- ohagi/data/sampling.py +259 -0
- ohagi/execution/__init__.py +7 -0
- ohagi/execution/inference.py +17 -0
- ohagi/export/__init__.py +48 -0
- ohagi/export/artifacts.py +38 -0
- ohagi/export/manifest.py +15 -0
- ohagi/export/weights.py +86 -0
- ohagi/inference.py +355 -0
- ohagi/io/__init__.py +8 -0
- ohagi/io/_orbax_compat.py +40 -0
- ohagi/io/checkpoints.py +83 -0
- ohagi/io/orbax.py +124 -0
- ohagi/losses/__init__.py +40 -0
- ohagi/losses/config.py +103 -0
- ohagi/losses/core.py +204 -0
- ohagi/losses/lagrangian.py +73 -0
- ohagi/losses/residual.py +12 -0
- ohagi/metrics/__init__.py +26 -0
- ohagi/model_types.py +34 -0
- ohagi/models/__init__.py +3 -0
- ohagi/models/composition.py +27 -0
- ohagi/models/projected.py +116 -0
- ohagi/nn/__init__.py +68 -0
- ohagi/nn/adapters/__init__.py +11 -0
- ohagi/nn/adapters/flax.py +34 -0
- ohagi/nn/adapters/flax_functional.py +100 -0
- ohagi/nn/adapters/linen.py +51 -0
- ohagi/nn/adapters/projected_mlp.py +24 -0
- ohagi/nn/backbone.py +66 -0
- ohagi/nn/config.py +143 -0
- ohagi/nn/layers/__init__.py +58 -0
- ohagi/nn/layers/activations.py +50 -0
- ohagi/nn/layers/combinators.py +100 -0
- ohagi/nn/layers/core.py +94 -0
- ohagi/nn/layers/linear.py +62 -0
- ohagi/nn/mlp.py +113 -0
- ohagi/nn/outputs.py +81 -0
- ohagi/optim/__init__.py +4 -0
- ohagi/reporting/__init__.py +3 -0
- ohagi/reporting/history.py +33 -0
- ohagi/serialization/__init__.py +5 -0
- ohagi/serialization/_jax.py +55 -0
- ohagi/serialization/bundle.py +47 -0
- ohagi/serialization/metadata.py +20 -0
- ohagi/serialization/predictor.py +345 -0
- ohagi/serialization/problem.py +98 -0
- ohagi/training/__init__.py +69 -0
- ohagi/training/_batch.py +120 -0
- ohagi/training/_engine.py +95 -0
- ohagi/training/config.py +65 -0
- ohagi/training/engine.py +204 -0
- ohagi/training/logging.py +155 -0
- ohagi/training/projected.py +2438 -0
- ohagi/training/runner.py +24 -0
- ohagi/training/schedules.py +33 -0
- ohagi/training/state.py +90 -0
- ohagi/training/stateful.py +121 -0
- ohagi/training/steps.py +229 -0
- ohagi/transforms/__init__.py +6 -0
- ohagi/transforms/scaling.py +60 -0
- ohagi-1.0.0.dist-info/METADATA +233 -0
- ohagi-1.0.0.dist-info/RECORD +77 -0
- ohagi-1.0.0.dist-info/WHEEL +5 -0
- ohagi-1.0.0.dist-info/licenses/LICENSE +201 -0
- ohagi-1.0.0.dist-info/licenses/NOTICE +12 -0
- ohagi-1.0.0.dist-info/top_level.txt +1 -0
ohagi/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""OHAGI: composable constrained learning in JAX.
|
|
2
|
+
|
|
3
|
+
Precision is controlled by the application. Legacy frontends retain their
|
|
4
|
+
historical precision settings; importing the common package is lightweight.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from ohagi.compat import install_aliases
|
|
9
|
+
from ohagi.compat import plugins as _plugins
|
|
10
|
+
from ohagi._projection_aliases import ALIASES as _PROJECTION_ALIASES
|
|
11
|
+
|
|
12
|
+
install_aliases(_PROJECTION_ALIASES)
|
|
13
|
+
__version__ = "1.0.0"
|
|
14
|
+
_EXPORTS = {
|
|
15
|
+
"Problem": ("problems", "Problem"),
|
|
16
|
+
"Projection": ("problems", "Projection"),
|
|
17
|
+
"FunctionalProblem": ("problems", "FunctionalProblem"),
|
|
18
|
+
"SymbolicProblem": ("problems", "SymbolicProblem"),
|
|
19
|
+
"MatrixProblem": ("problems", "MatrixProblem"),
|
|
20
|
+
"ParametricQP": ("problems", "ParametricQP"),
|
|
21
|
+
"ParametricLP": ("problems", "ParametricLP"),
|
|
22
|
+
"MLP": ("nn", "MLP"),
|
|
23
|
+
"MLPBackbone": ("nn", "MLPBackbone"),
|
|
24
|
+
"Loss": ("losses", "Loss"),
|
|
25
|
+
"prepare_projection": ("solvers", "prepare_projection"),
|
|
26
|
+
"ChambollePockConfig": ("solvers", "ChambollePockConfig"),
|
|
27
|
+
"ADMMConfig": ("solvers", "ADMMConfig"),
|
|
28
|
+
"FischerBurmeisterConfig": ("solvers", "FischerBurmeisterConfig"),
|
|
29
|
+
"ProjectionConfig": ("solvers", "ProjectionConfig"),
|
|
30
|
+
"LinearEqualityConfig": ("solvers", "LinearEqualityConfig"),
|
|
31
|
+
"LinearSolverConfig": ("solvers", "LinearSolverConfig"),
|
|
32
|
+
"ImplicitGradientConfig": ("solvers", "ImplicitGradientConfig"),
|
|
33
|
+
"SolveStatus": ("solvers", "SolveStatus"),
|
|
34
|
+
"Distance": ("projections", "Distance"),
|
|
35
|
+
"WeightedDistance": ("projections", "WeightedDistance"),
|
|
36
|
+
"LocalQuadratic": ("projections", "LocalQuadratic"),
|
|
37
|
+
"Sequential": ("projections", "Sequential"),
|
|
38
|
+
"ProjectedModel": ("models", "ProjectedModel"),
|
|
39
|
+
"Trainer": ("training.engine", "Trainer"),
|
|
40
|
+
"Adam": ("optim", "Adam"),
|
|
41
|
+
"save_predictor": ("export", "save_predictor"),
|
|
42
|
+
"load_predictor": ("export", "load_predictor"),
|
|
43
|
+
"load_inference": ("inference", "load"),
|
|
44
|
+
"export_inference": ("inference", "export"),
|
|
45
|
+
}
|
|
46
|
+
__all__ = list(dict.fromkeys([*_EXPORTS, *_plugins.public_symbols(__name__)]))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __getattr__(name):
|
|
50
|
+
if name in _EXPORTS:
|
|
51
|
+
module, symbol = _EXPORTS[name]
|
|
52
|
+
value = getattr(import_module("ohagi." + module), symbol)
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
return _plugins.public_symbol(__name__, name)
|
|
56
|
+
except AttributeError:
|
|
57
|
+
pass
|
|
58
|
+
try:
|
|
59
|
+
value = import_module("ohagi." + name)
|
|
60
|
+
except ModuleNotFoundError as error:
|
|
61
|
+
if error.name != "ohagi." + name:
|
|
62
|
+
raise
|
|
63
|
+
raise AttributeError(name) from error
|
|
64
|
+
globals()[name] = value
|
|
65
|
+
return value
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Historical core projection imports point to PURIN."""
|
|
2
|
+
|
|
3
|
+
ALIASES = {
|
|
4
|
+
"ohagi._utils.validation": ("purin._utils.validation", False),
|
|
5
|
+
"ohagi.autodiff": ("purin.autodiff", True),
|
|
6
|
+
"ohagi.autodiff.complementarity": ("purin.autodiff.complementarity", False),
|
|
7
|
+
"ohagi.autodiff.fixed_point": ("purin.autodiff.fixed_point", False),
|
|
8
|
+
"ohagi.compilation": ("purin.compilation", True),
|
|
9
|
+
"ohagi.differentiation": ("purin.differentiation", True),
|
|
10
|
+
"ohagi.differentiation.policies": ("purin.differentiation.policies", False),
|
|
11
|
+
"ohagi.execution.runtime": ("purin.execution.runtime", False),
|
|
12
|
+
"ohagi.linalg": ("purin.linalg", True),
|
|
13
|
+
"ohagi.linalg.equilibration": ("purin.linalg.equilibration", False),
|
|
14
|
+
"ohagi.linalg.operators": ("purin.linalg.operators", False),
|
|
15
|
+
"ohagi.linalg.sparse": ("purin.linalg.sparse", True),
|
|
16
|
+
"ohagi.linalg.sparse.assembly": ("purin.linalg.sparse.assembly", False),
|
|
17
|
+
"ohagi.linalg.sparse.conversion": ("purin.linalg.sparse.conversion", False),
|
|
18
|
+
"ohagi.linalg.sparse.minres": ("purin.linalg.sparse.minres", False),
|
|
19
|
+
"ohagi.linalg.sparse.operators": ("purin.linalg.sparse.operators", False),
|
|
20
|
+
"ohagi.linalg.sparse.types": ("purin.linalg.sparse.types", False),
|
|
21
|
+
"ohagi.problems": ("purin.problems", True),
|
|
22
|
+
"ohagi.problems.canonical": ("purin.problems.canonical", False),
|
|
23
|
+
"ohagi.problems.compile": ("purin.problems.compile", False),
|
|
24
|
+
"ohagi.problems.expressions": ("purin.problems.expressions", False),
|
|
25
|
+
"ohagi.problems.functional": ("purin.problems.functional", False),
|
|
26
|
+
"ohagi.problems.problem": ("purin.problems.problem", False),
|
|
27
|
+
"ohagi.problems.projection": ("purin.problems.projection", False),
|
|
28
|
+
"ohagi.problems.qp": ("purin.problems.qp", False),
|
|
29
|
+
"ohagi.problems.sparsity": ("purin.problems.sparsity", False),
|
|
30
|
+
"ohagi.problems.symbolic": ("purin.problems.symbolic", False),
|
|
31
|
+
"ohagi.problems.types": ("purin.problems.types", False),
|
|
32
|
+
"ohagi.projections": ("purin.projections", True),
|
|
33
|
+
"ohagi.projections.adapters": ("purin.projections.adapters", False),
|
|
34
|
+
"ohagi.projections.diagnostics": ("purin.projections.diagnostics", False),
|
|
35
|
+
"ohagi.projections.distance": ("purin.projections.distance", False),
|
|
36
|
+
"ohagi.projections.local_quadratic": ("purin.projections.local_quadratic", False),
|
|
37
|
+
"ohagi.projections.sequential": ("purin.projections.sequential", False),
|
|
38
|
+
"ohagi.solvers": ("purin.solvers", True),
|
|
39
|
+
"ohagi.solvers.admm": ("purin.solvers.admm", True),
|
|
40
|
+
"ohagi.solvers.admm.config": ("purin.solvers.admm.config", False),
|
|
41
|
+
"ohagi.solvers.admm.factor": ("purin.solvers.admm.factor", False),
|
|
42
|
+
"ohagi.solvers.admm.kernels": ("purin.solvers.admm.kernels", False),
|
|
43
|
+
"ohagi.solvers.admm.rows": ("purin.solvers.admm.rows", False),
|
|
44
|
+
"ohagi.solvers.admm.solver": ("purin.solvers.admm.solver", False),
|
|
45
|
+
"ohagi.solvers.api": ("purin.solvers.api", False),
|
|
46
|
+
"ohagi.solvers.capabilities": ("purin.solvers.capabilities", False),
|
|
47
|
+
"ohagi.solvers.chambolle_pock": ("purin.solvers.chambolle_pock", True),
|
|
48
|
+
"ohagi.solvers.chambolle_pock.bounds": (
|
|
49
|
+
"purin.solvers.chambolle_pock.bounds",
|
|
50
|
+
False,
|
|
51
|
+
),
|
|
52
|
+
"ohagi.solvers.chambolle_pock.config": (
|
|
53
|
+
"purin.solvers.chambolle_pock.config",
|
|
54
|
+
False,
|
|
55
|
+
),
|
|
56
|
+
"ohagi.solvers.chambolle_pock.dense": ("purin.solvers.chambolle_pock.dense", False),
|
|
57
|
+
"ohagi.solvers.chambolle_pock.dense_implicit": (
|
|
58
|
+
"purin.solvers.chambolle_pock.dense_implicit",
|
|
59
|
+
False,
|
|
60
|
+
),
|
|
61
|
+
"ohagi.solvers.chambolle_pock.dense_layer": (
|
|
62
|
+
"purin.solvers.chambolle_pock.dense_layer",
|
|
63
|
+
False,
|
|
64
|
+
),
|
|
65
|
+
"ohagi.solvers.chambolle_pock.kernels": (
|
|
66
|
+
"purin.solvers.chambolle_pock.kernels",
|
|
67
|
+
False,
|
|
68
|
+
),
|
|
69
|
+
"ohagi.solvers.chambolle_pock.solver": (
|
|
70
|
+
"purin.solvers.chambolle_pock.solver",
|
|
71
|
+
False,
|
|
72
|
+
),
|
|
73
|
+
"ohagi.solvers.config": ("purin.solvers.config", False),
|
|
74
|
+
"ohagi.solvers.diagonal_qp": ("purin.solvers.diagonal_qp", False),
|
|
75
|
+
"ohagi.solvers.fischer_burmeister": ("purin.solvers.fischer_burmeister", True),
|
|
76
|
+
"ohagi.solvers.fischer_burmeister.common": (
|
|
77
|
+
"purin.solvers.fischer_burmeister.common",
|
|
78
|
+
False,
|
|
79
|
+
),
|
|
80
|
+
"ohagi.solvers.fischer_burmeister.complementarity": (
|
|
81
|
+
"purin.solvers.fischer_burmeister.complementarity",
|
|
82
|
+
False,
|
|
83
|
+
),
|
|
84
|
+
"ohagi.solvers.fischer_burmeister.config": (
|
|
85
|
+
"purin.solvers.fischer_burmeister.config",
|
|
86
|
+
False,
|
|
87
|
+
),
|
|
88
|
+
"ohagi.solvers.fischer_burmeister.dense_semismooth": (
|
|
89
|
+
"purin.solvers.fischer_burmeister.dense_semismooth",
|
|
90
|
+
False,
|
|
91
|
+
),
|
|
92
|
+
"ohagi.solvers.fischer_burmeister.newton": (
|
|
93
|
+
"purin.solvers.fischer_burmeister.newton",
|
|
94
|
+
False,
|
|
95
|
+
),
|
|
96
|
+
"ohagi.solvers.fischer_burmeister.operators": (
|
|
97
|
+
"purin.solvers.fischer_burmeister.operators",
|
|
98
|
+
False,
|
|
99
|
+
),
|
|
100
|
+
"ohagi.solvers.fischer_burmeister.reduced": (
|
|
101
|
+
"purin.solvers.fischer_burmeister.reduced",
|
|
102
|
+
False,
|
|
103
|
+
),
|
|
104
|
+
"ohagi.solvers.fischer_burmeister.solver": (
|
|
105
|
+
"purin.solvers.fischer_burmeister.solver",
|
|
106
|
+
False,
|
|
107
|
+
),
|
|
108
|
+
"ohagi.solvers.implicit": ("purin.solvers.implicit", False),
|
|
109
|
+
"ohagi.solvers.linear_equality": ("purin.solvers.linear_equality", True),
|
|
110
|
+
"ohagi.solvers.linear_equality.config": (
|
|
111
|
+
"purin.solvers.linear_equality.config",
|
|
112
|
+
False,
|
|
113
|
+
),
|
|
114
|
+
"ohagi.solvers.linear_equality.solver": (
|
|
115
|
+
"purin.solvers.linear_equality.solver",
|
|
116
|
+
False,
|
|
117
|
+
),
|
|
118
|
+
"ohagi.solvers.metric": ("purin.solvers.metric", False),
|
|
119
|
+
"ohagi.solvers.primal_dual_interior_point": (
|
|
120
|
+
"purin.solvers.primal_dual_interior_point",
|
|
121
|
+
True,
|
|
122
|
+
),
|
|
123
|
+
"ohagi.solvers.primal_dual_interior_point.assembly": (
|
|
124
|
+
"purin.solvers.primal_dual_interior_point.assembly",
|
|
125
|
+
False,
|
|
126
|
+
),
|
|
127
|
+
"ohagi.solvers.primal_dual_interior_point.config": (
|
|
128
|
+
"purin.solvers.primal_dual_interior_point.config",
|
|
129
|
+
False,
|
|
130
|
+
),
|
|
131
|
+
"ohagi.solvers.primal_dual_interior_point.derivatives": (
|
|
132
|
+
"purin.solvers.primal_dual_interior_point.derivatives",
|
|
133
|
+
False,
|
|
134
|
+
),
|
|
135
|
+
"ohagi.solvers.primal_dual_interior_point.newton": (
|
|
136
|
+
"purin.solvers.primal_dual_interior_point.newton",
|
|
137
|
+
False,
|
|
138
|
+
),
|
|
139
|
+
"ohagi.solvers.primal_dual_interior_point.operators": (
|
|
140
|
+
"purin.solvers.primal_dual_interior_point.operators",
|
|
141
|
+
False,
|
|
142
|
+
),
|
|
143
|
+
"ohagi.solvers.primal_dual_interior_point.reference": (
|
|
144
|
+
"purin.solvers.primal_dual_interior_point.reference",
|
|
145
|
+
True,
|
|
146
|
+
),
|
|
147
|
+
"ohagi.solvers.primal_dual_interior_point.reference.bounds": (
|
|
148
|
+
"purin.solvers.primal_dual_interior_point.reference.bounds",
|
|
149
|
+
False,
|
|
150
|
+
),
|
|
151
|
+
"ohagi.solvers.primal_dual_interior_point.reference.filter": (
|
|
152
|
+
"purin.solvers.primal_dual_interior_point.reference.filter",
|
|
153
|
+
False,
|
|
154
|
+
),
|
|
155
|
+
"ohagi.solvers.primal_dual_interior_point.reference.iteration": (
|
|
156
|
+
"purin.solvers.primal_dual_interior_point.reference.iteration",
|
|
157
|
+
False,
|
|
158
|
+
),
|
|
159
|
+
"ohagi.solvers.primal_dual_interior_point.reference.line_search": (
|
|
160
|
+
"purin.solvers.primal_dual_interior_point.reference.line_search",
|
|
161
|
+
False,
|
|
162
|
+
),
|
|
163
|
+
"ohagi.solvers.primal_dual_interior_point.reference.newton": (
|
|
164
|
+
"purin.solvers.primal_dual_interior_point.reference.newton",
|
|
165
|
+
False,
|
|
166
|
+
),
|
|
167
|
+
"ohagi.solvers.primal_dual_interior_point.reference.residuals": (
|
|
168
|
+
"purin.solvers.primal_dual_interior_point.reference.residuals",
|
|
169
|
+
False,
|
|
170
|
+
),
|
|
171
|
+
"ohagi.solvers.primal_dual_interior_point.reference.state": (
|
|
172
|
+
"purin.solvers.primal_dual_interior_point.reference.state",
|
|
173
|
+
False,
|
|
174
|
+
),
|
|
175
|
+
"ohagi.solvers.primal_dual_interior_point.reference.step": (
|
|
176
|
+
"purin.solvers.primal_dual_interior_point.reference.step",
|
|
177
|
+
False,
|
|
178
|
+
),
|
|
179
|
+
"ohagi.solvers.primal_dual_interior_point.residuals": (
|
|
180
|
+
"purin.solvers.primal_dual_interior_point.residuals",
|
|
181
|
+
False,
|
|
182
|
+
),
|
|
183
|
+
"ohagi.solvers.primal_dual_interior_point.scalar": (
|
|
184
|
+
"purin.solvers.primal_dual_interior_point.scalar",
|
|
185
|
+
False,
|
|
186
|
+
),
|
|
187
|
+
"ohagi.solvers.primal_dual_interior_point.solver": (
|
|
188
|
+
"purin.solvers.primal_dual_interior_point.solver",
|
|
189
|
+
False,
|
|
190
|
+
),
|
|
191
|
+
"ohagi.solvers.primal_dual_interior_point.types": (
|
|
192
|
+
"purin.solvers.primal_dual_interior_point.types",
|
|
193
|
+
False,
|
|
194
|
+
),
|
|
195
|
+
"ohagi.solvers.registry": ("purin.solvers.registry", False),
|
|
196
|
+
"ohagi.solvers.semismooth": ("purin.solvers.semismooth", False),
|
|
197
|
+
"ohagi.solvers.types": ("purin.solvers.types", False),
|
|
198
|
+
}
|
ohagi/_utils/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Shared JSON conversion, checksums, and atomic artifact writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import jax
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def json_safe(value):
|
|
15
|
+
"""Convert nested numerical values into JSON-compatible Python values."""
|
|
16
|
+
if isinstance(value, np.generic):
|
|
17
|
+
return value.item()
|
|
18
|
+
if isinstance(value, (np.ndarray, jax.Array)):
|
|
19
|
+
return np.asarray(jax.device_get(value)).tolist()
|
|
20
|
+
if isinstance(value, Mapping):
|
|
21
|
+
return {str(key): json_safe(item) for key, item in value.items()}
|
|
22
|
+
if isinstance(value, (list, tuple)):
|
|
23
|
+
return [json_safe(item) for item in value]
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sha256(path):
|
|
28
|
+
"""Return the SHA-256 digest of a file without loading it all into memory."""
|
|
29
|
+
digest = hashlib.sha256()
|
|
30
|
+
with Path(path).open("rb") as handle:
|
|
31
|
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
32
|
+
digest.update(block)
|
|
33
|
+
return digest.hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def atomic_write(path, data):
|
|
37
|
+
"""Replace a file atomically using a temporary file in the same directory."""
|
|
38
|
+
path = Path(path)
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
temporary = None
|
|
41
|
+
try:
|
|
42
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as handle:
|
|
43
|
+
temporary = Path(handle.name)
|
|
44
|
+
handle.write(data)
|
|
45
|
+
handle.flush()
|
|
46
|
+
os.fsync(handle.fileno())
|
|
47
|
+
os.replace(temporary, path)
|
|
48
|
+
finally:
|
|
49
|
+
if temporary is not None:
|
|
50
|
+
temporary.unlink(missing_ok=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_json(path, value):
|
|
54
|
+
"""Atomically write a deterministic, human-readable JSON document."""
|
|
55
|
+
data = json.dumps(json_safe(value), indent=2, sort_keys=True) + "\n"
|
|
56
|
+
atomic_write(path, data.encode("utf-8"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def timestamp():
|
|
60
|
+
"""Compact filesystem-friendly run timestamp."""
|
|
61
|
+
import time
|
|
62
|
+
|
|
63
|
+
return time.strftime("%Y%m%d_%H%M%S")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_path(path):
|
|
67
|
+
"""Expand and resolve a filesystem path for artifact access."""
|
|
68
|
+
return Path(path).expanduser().resolve()
|
ohagi/_utils/tree.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Small PyTree predicates and selections used by training steps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import jax
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tree_all_finite(tree):
|
|
9
|
+
"""Return a scalar JAX predicate indicating whether every pytree leaf is finite."""
|
|
10
|
+
leaves = jax.tree_util.tree_leaves(tree)
|
|
11
|
+
result = jnp.asarray(True)
|
|
12
|
+
for leaf in leaves:
|
|
13
|
+
result = result & jnp.all(jnp.isfinite(leaf))
|
|
14
|
+
return result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tree_select(condition, true_tree, false_tree):
|
|
18
|
+
"""Select corresponding pytree leaves with a scalar or broadcastable predicate."""
|
|
19
|
+
return jax.tree_util.tree_map(
|
|
20
|
+
lambda true_value, false_value: jnp.where(condition, true_value, false_value),
|
|
21
|
+
true_tree,
|
|
22
|
+
false_tree,
|
|
23
|
+
)
|
ohagi/compat/__init__.py
ADDED
ohagi/compat/aliases.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Lazy aliases point to canonical OHAGI or PURIN modules.
|
|
2
|
+
|
|
3
|
+
Loading the same Python file under two names duplicates classes and registries.
|
|
4
|
+
This loader instead returns the already imported canonical module object. Exact
|
|
5
|
+
alias catalogs prevent unrelated packages from being intercepted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from importlib.abc import Loader, MetaPathFinder
|
|
11
|
+
from importlib.util import spec_from_loader
|
|
12
|
+
from importlib.machinery import PathFinder
|
|
13
|
+
from threading import RLock
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _AliasLoader(Loader):
|
|
17
|
+
def __init__(self, target):
|
|
18
|
+
self.target = target
|
|
19
|
+
|
|
20
|
+
def create_module(self, spec):
|
|
21
|
+
"""Create the compatibility namespace requested by the import machinery."""
|
|
22
|
+
return import_module(self.target)
|
|
23
|
+
|
|
24
|
+
def exec_module(self, module):
|
|
25
|
+
"""Populate an imported compatibility namespace with its registered resources."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _AliasFinder(MetaPathFinder):
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.aliases = {}
|
|
32
|
+
self.lock = RLock()
|
|
33
|
+
|
|
34
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
35
|
+
"""Resolve a registered compatibility namespace during module import."""
|
|
36
|
+
entry = self.aliases.get(fullname)
|
|
37
|
+
if entry is None:
|
|
38
|
+
if (
|
|
39
|
+
any(name.startswith(fullname + ".") for name in self.aliases)
|
|
40
|
+
and PathFinder.find_spec(fullname, path) is None
|
|
41
|
+
):
|
|
42
|
+
from .plugins import _Namespace
|
|
43
|
+
|
|
44
|
+
return spec_from_loader(fullname, _Namespace(), is_package=True)
|
|
45
|
+
return None
|
|
46
|
+
destination, is_package = entry
|
|
47
|
+
return spec_from_loader(
|
|
48
|
+
fullname, _AliasLoader(destination), is_package=is_package
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_FINDER = _AliasFinder()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def install_aliases(aliases):
|
|
56
|
+
"""Install exact aliases, rejecting mixed old/new modules in one process."""
|
|
57
|
+
with _FINDER.lock:
|
|
58
|
+
for name, entry in aliases.items():
|
|
59
|
+
target, is_package = entry
|
|
60
|
+
if name == target:
|
|
61
|
+
continue
|
|
62
|
+
previous = _FINDER.aliases.get(name)
|
|
63
|
+
if previous is not None and previous != (target, is_package):
|
|
64
|
+
raise ImportError(f"Conflicting alias for {name}")
|
|
65
|
+
module = sys.modules.get(name)
|
|
66
|
+
if module is not None and module.__name__ != target:
|
|
67
|
+
raise ImportError(
|
|
68
|
+
f"{name} is already loaded from another implementation; use a fresh Python process"
|
|
69
|
+
)
|
|
70
|
+
_FINDER.aliases.update({name: tuple(entry) for name, entry in aliases.items()})
|
|
71
|
+
if _FINDER not in sys.meta_path:
|
|
72
|
+
sys.meta_path.insert(0, _FINDER)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def reexport_module(namespace, target):
|
|
76
|
+
"""Preserve public and historical private attributes on a root facade."""
|
|
77
|
+
implementation = import_module(target)
|
|
78
|
+
for name, value in vars(implementation).items():
|
|
79
|
+
if not name.startswith("__") or name in {"__all__", "__version__", "__doc__"}:
|
|
80
|
+
namespace[name] = value
|
|
81
|
+
namespace["__getattr__"] = lambda name: getattr(implementation, name)
|
|
82
|
+
namespace["__dir__"] = lambda: sorted(set(namespace) | set(dir(implementation)))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def registered_aliases():
|
|
86
|
+
"""Return the currently registered compatibility module aliases."""
|
|
87
|
+
return dict(_FINDER.aliases)
|
ohagi/compat/paths.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Resolve module names stored before the OHAGI/PURIN package rename."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def canonical_module_name(name: str) -> str:
|
|
5
|
+
"""Translate historical factory namespaces without changing attributes."""
|
|
6
|
+
for previous, current in (("hax", "ohagi"), ("projx", "purin")):
|
|
7
|
+
if name == previous or name.startswith(previous + "."):
|
|
8
|
+
return current + name[len(previous) :]
|
|
9
|
+
return name
|
ohagi/compat/plugins.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Lazy plugin discovery; the core has no knowledge of frontend packages."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from importlib.abc import MetaPathFinder, Loader
|
|
7
|
+
from importlib.machinery import PathFinder
|
|
8
|
+
from importlib.util import spec_from_loader
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@lru_cache(maxsize=None)
|
|
13
|
+
def _entries(group):
|
|
14
|
+
return tuple(entry_points(group=group))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_symbols = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def public_symbols(module):
|
|
21
|
+
"""Return the public symbols registered for a frontend namespace."""
|
|
22
|
+
prefix = module + "."
|
|
23
|
+
names = set(_symbols)
|
|
24
|
+
names.update(ep.name for ep in _entries("ohagi.symbols"))
|
|
25
|
+
return sorted(
|
|
26
|
+
key[len(prefix) :]
|
|
27
|
+
for key in names
|
|
28
|
+
if key.startswith(prefix) and "." not in key[len(prefix) :]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def register_symbols(values):
|
|
33
|
+
"""Register frontend symbols for compatibility lookup."""
|
|
34
|
+
_symbols.update(values)
|
|
35
|
+
for key in values:
|
|
36
|
+
parent, name = key.rsplit(".", 1)
|
|
37
|
+
module = sys.modules.get(parent)
|
|
38
|
+
exports = getattr(module, "__all__", None) if module is not None else None
|
|
39
|
+
if isinstance(exports, list) and name not in exports:
|
|
40
|
+
exports.append(name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def public_symbol(module, name):
|
|
44
|
+
"""Resolve a named public symbol from the compatibility registry."""
|
|
45
|
+
key = module + "." + name
|
|
46
|
+
destination = _symbols.get(key)
|
|
47
|
+
if destination is None:
|
|
48
|
+
matches = tuple(ep for ep in _entries("ohagi.symbols") if ep.name == key)
|
|
49
|
+
if not matches:
|
|
50
|
+
raise AttributeError(name)
|
|
51
|
+
destination = matches[0].value
|
|
52
|
+
owner, symbol = destination.split(":", 1)
|
|
53
|
+
return getattr(import_module(owner), symbol)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _Namespace(Loader):
|
|
57
|
+
def create_module(self, spec):
|
|
58
|
+
"""Create the compatibility namespace requested by the import machinery."""
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def exec_module(self, module):
|
|
62
|
+
"""Populate an imported compatibility namespace with its registered resources."""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Plugins(MetaPathFinder):
|
|
67
|
+
busy = False
|
|
68
|
+
|
|
69
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
70
|
+
"""Resolve a registered compatibility namespace during module import."""
|
|
71
|
+
if self.busy or not fullname.startswith("ohagi."):
|
|
72
|
+
return None
|
|
73
|
+
if PathFinder.find_spec(fullname, path) is not None:
|
|
74
|
+
return None
|
|
75
|
+
matches = tuple(
|
|
76
|
+
ep
|
|
77
|
+
for ep in _entries("ohagi.compat")
|
|
78
|
+
if fullname == ep.name or fullname.startswith(ep.name + ".")
|
|
79
|
+
)
|
|
80
|
+
if matches:
|
|
81
|
+
self.busy = True
|
|
82
|
+
try:
|
|
83
|
+
from .aliases import install_aliases, _FINDER
|
|
84
|
+
|
|
85
|
+
install_aliases(matches[0].load())
|
|
86
|
+
return _FINDER.find_spec(fullname, path, target)
|
|
87
|
+
finally:
|
|
88
|
+
self.busy = False
|
|
89
|
+
if any(ep.name.startswith(fullname + ".") for ep in _entries("ohagi.compat")):
|
|
90
|
+
return spec_from_loader(fullname, _Namespace(), is_package=True)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
sys.meta_path.append(_Plugins())
|
ohagi/data/__init__.py
ADDED
ohagi/data/batching.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Fixed shapes and explicit masks for JIT-compiled steps."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def fixed_batches(data, batch_size, *, indices=None, drop_last=False):
|
|
7
|
+
"""Yield fixed-shape NumPy batches with explicit validity masks.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
data: Nonempty mapping of arrays sharing the same sample dimension.
|
|
11
|
+
The reserved key ``mask`` must not be supplied.
|
|
12
|
+
batch_size: Positive number of rows in each emitted batch.
|
|
13
|
+
indices: Optional one-dimensional integer selection/order of sample rows.
|
|
14
|
+
drop_last: Discard incomplete final batches instead of padding them.
|
|
15
|
+
|
|
16
|
+
Yields:
|
|
17
|
+
Mapping of data arrays with leading dimension ``batch_size`` plus a
|
|
18
|
+
boolean ``mask``. Padded rows repeat the final selected observation and
|
|
19
|
+
have mask value False, so reducers must honor the mask.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If sizes, sample dimensions or selected indices are invalid.
|
|
23
|
+
"""
|
|
24
|
+
if not isinstance(batch_size, int) or batch_size < 1:
|
|
25
|
+
raise ValueError("batch_size must be positive")
|
|
26
|
+
if not data:
|
|
27
|
+
raise ValueError("data must contain arrays")
|
|
28
|
+
arrays = {key: np.asarray(value) for key, value in data.items()}
|
|
29
|
+
if any(value.ndim == 0 for value in arrays.values()):
|
|
30
|
+
raise ValueError("data arrays require a sample dimension")
|
|
31
|
+
size = len(next(iter(arrays.values())))
|
|
32
|
+
if size < 1 or any(
|
|
33
|
+
value.ndim == 0 or len(value) != size for value in arrays.values()
|
|
34
|
+
):
|
|
35
|
+
raise ValueError("data arrays must share a nonempty sample dimension")
|
|
36
|
+
if "mask" in arrays:
|
|
37
|
+
raise ValueError("mask is generated by fixed_batches")
|
|
38
|
+
indices = np.arange(size) if indices is None else np.asarray(indices)
|
|
39
|
+
if (
|
|
40
|
+
indices.ndim != 1
|
|
41
|
+
or not np.issubdtype(indices.dtype, np.integer)
|
|
42
|
+
or np.any(indices < 0)
|
|
43
|
+
or np.any(indices >= size)
|
|
44
|
+
):
|
|
45
|
+
raise ValueError("invalid batch indices")
|
|
46
|
+
if drop_last:
|
|
47
|
+
indices = indices[: (len(indices) // batch_size) * batch_size]
|
|
48
|
+
for start in range(0, len(indices), batch_size):
|
|
49
|
+
selected = indices[start : start + batch_size]
|
|
50
|
+
count = len(selected)
|
|
51
|
+
padded = np.pad(selected, (0, batch_size - count), mode="edge")
|
|
52
|
+
yield {
|
|
53
|
+
**{key: value[padded] for key, value in arrays.items()},
|
|
54
|
+
"mask": np.arange(batch_size) < count,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def trimmed_batches(values, batch_size):
|
|
59
|
+
"""Reshape a feature matrix into complete fixed-size batches.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
values: Two-dimensional array of shape ``(samples, features)``.
|
|
63
|
+
batch_size: Number of observations in each complete batch.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Array of shape ``(samples // batch_size, batch_size, features)``.
|
|
67
|
+
Incomplete trailing rows are discarded; no padding or mask is added.
|
|
68
|
+
"""
|
|
69
|
+
trimmed = values[: (values.shape[0] // batch_size) * batch_size]
|
|
70
|
+
return trimmed.reshape((-1, batch_size, values.shape[1]))
|