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.
Files changed (77) hide show
  1. ohagi/__init__.py +65 -0
  2. ohagi/_projection_aliases.py +198 -0
  3. ohagi/_utils/__init__.py +8 -0
  4. ohagi/_utils/serialization.py +68 -0
  5. ohagi/_utils/tree.py +23 -0
  6. ohagi/compat/__init__.py +5 -0
  7. ohagi/compat/aliases.py +87 -0
  8. ohagi/compat/paths.py +9 -0
  9. ohagi/compat/plugins.py +94 -0
  10. ohagi/data/__init__.py +4 -0
  11. ohagi/data/batching.py +70 -0
  12. ohagi/data/sampling.py +259 -0
  13. ohagi/execution/__init__.py +7 -0
  14. ohagi/execution/inference.py +17 -0
  15. ohagi/export/__init__.py +48 -0
  16. ohagi/export/artifacts.py +38 -0
  17. ohagi/export/manifest.py +15 -0
  18. ohagi/export/weights.py +86 -0
  19. ohagi/inference.py +355 -0
  20. ohagi/io/__init__.py +8 -0
  21. ohagi/io/_orbax_compat.py +40 -0
  22. ohagi/io/checkpoints.py +83 -0
  23. ohagi/io/orbax.py +124 -0
  24. ohagi/losses/__init__.py +40 -0
  25. ohagi/losses/config.py +103 -0
  26. ohagi/losses/core.py +204 -0
  27. ohagi/losses/lagrangian.py +73 -0
  28. ohagi/losses/residual.py +12 -0
  29. ohagi/metrics/__init__.py +26 -0
  30. ohagi/model_types.py +34 -0
  31. ohagi/models/__init__.py +3 -0
  32. ohagi/models/composition.py +27 -0
  33. ohagi/models/projected.py +116 -0
  34. ohagi/nn/__init__.py +68 -0
  35. ohagi/nn/adapters/__init__.py +11 -0
  36. ohagi/nn/adapters/flax.py +34 -0
  37. ohagi/nn/adapters/flax_functional.py +100 -0
  38. ohagi/nn/adapters/linen.py +51 -0
  39. ohagi/nn/adapters/projected_mlp.py +24 -0
  40. ohagi/nn/backbone.py +66 -0
  41. ohagi/nn/config.py +143 -0
  42. ohagi/nn/layers/__init__.py +58 -0
  43. ohagi/nn/layers/activations.py +50 -0
  44. ohagi/nn/layers/combinators.py +100 -0
  45. ohagi/nn/layers/core.py +94 -0
  46. ohagi/nn/layers/linear.py +62 -0
  47. ohagi/nn/mlp.py +113 -0
  48. ohagi/nn/outputs.py +81 -0
  49. ohagi/optim/__init__.py +4 -0
  50. ohagi/reporting/__init__.py +3 -0
  51. ohagi/reporting/history.py +33 -0
  52. ohagi/serialization/__init__.py +5 -0
  53. ohagi/serialization/_jax.py +55 -0
  54. ohagi/serialization/bundle.py +47 -0
  55. ohagi/serialization/metadata.py +20 -0
  56. ohagi/serialization/predictor.py +345 -0
  57. ohagi/serialization/problem.py +98 -0
  58. ohagi/training/__init__.py +69 -0
  59. ohagi/training/_batch.py +120 -0
  60. ohagi/training/_engine.py +95 -0
  61. ohagi/training/config.py +65 -0
  62. ohagi/training/engine.py +204 -0
  63. ohagi/training/logging.py +155 -0
  64. ohagi/training/projected.py +2438 -0
  65. ohagi/training/runner.py +24 -0
  66. ohagi/training/schedules.py +33 -0
  67. ohagi/training/state.py +90 -0
  68. ohagi/training/stateful.py +121 -0
  69. ohagi/training/steps.py +229 -0
  70. ohagi/transforms/__init__.py +6 -0
  71. ohagi/transforms/scaling.py +60 -0
  72. ohagi-1.0.0.dist-info/METADATA +233 -0
  73. ohagi-1.0.0.dist-info/RECORD +77 -0
  74. ohagi-1.0.0.dist-info/WHEEL +5 -0
  75. ohagi-1.0.0.dist-info/licenses/LICENSE +201 -0
  76. ohagi-1.0.0.dist-info/licenses/NOTICE +12 -0
  77. 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
+ }
@@ -0,0 +1,8 @@
1
+ """Internal scalar validation helpers."""
2
+
3
+ from .validation import (
4
+ nonnegative_float as nonnegative_float,
5
+ open_unit_interval as open_unit_interval,
6
+ positive_float as positive_float,
7
+ positive_integer as positive_integer,
8
+ )
@@ -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
+ )
@@ -0,0 +1,5 @@
1
+ """Identity-preserving legacy module aliases for the three frontend packages."""
2
+
3
+ from .aliases import install_aliases, reexport_module, registered_aliases
4
+
5
+ __all__ = ["install_aliases", "reexport_module", "registered_aliases"]
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ """Deterministic dataset selection, splitting and fixed-shape batching."""
2
+
3
+ from .batching import fixed_batches as fixed_batches
4
+ from ohagi.transforms.scaling import Standardizer as Standardizer
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]))