scirex 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.
- scirex/__init__.py +69 -0
- scirex/_internal/__init__.py +8 -0
- scirex/_internal/aliases.py +144 -0
- scirex/_internal/deprecation.py +92 -0
- scirex/_internal/einsum_utils.py +108 -0
- scirex/_internal/extras.py +71 -0
- scirex/_internal/lazy.py +109 -0
- scirex/_internal/neighbor_logging.py +227 -0
- scirex/_internal/paths.py +15 -0
- scirex/_internal/scaling.py +97 -0
- scirex/_internal/segment_csr.py +63 -0
- scirex/_internal/serialization.py +44 -0
- scirex/_internal/tensorly_backend.py +23 -0
- scirex/_internal/torch_setup.py +118 -0
- scirex/_internal/version.py +56 -0
- scirex/_internal/wandb_utils.py +47 -0
- scirex/_internal/web_utils.py +211 -0
- scirex/_internal/wno_train_utils.py +172 -0
- scirex/config/__init__.py +71 -0
- scirex/config/gino_carcfd_config.py +50 -0
- scirex/config/models.py +539 -0
- scirex/config/opt.py +91 -0
- scirex/config/transolver_pipe_config.py +67 -0
- scirex/data/__init__.py +65 -0
- scirex/data/containers/__init__.py +29 -0
- scirex/data/containers/dict_dataset.py +45 -0
- scirex/data/containers/pt_dataset.py +397 -0
- scirex/data/containers/tensor_dataset.py +118 -0
- scirex/data/datasets/__init__.py +49 -0
- scirex/data/datasets/airfrans.py +377 -0
- scirex/data/datasets/car_cfd.py +133 -0
- scirex/data/datasets/cavity_kepsilon.py +379 -0
- scirex/data/datasets/chipcooling.py +377 -0
- scirex/data/datasets/darcy.py +356 -0
- scirex/data/datasets/mesh_datamodule.py +423 -0
- scirex/data/datasets/navier_stokes.py +242 -0
- scirex/data/datasets/shapenet_car.py +428 -0
- scirex/data/datasets/shapenet_car_boundary.py +257 -0
- scirex/data/datasets/shapenet_car_drag.py +351 -0
- scirex/data/transforms/__init__.py +29 -0
- scirex/data/transforms/base_transforms.py +147 -0
- scirex/data/transforms/data_processors.py +372 -0
- scirex/data/transforms/normalizers.py +382 -0
- scirex/experimental/__init__.py +2 -0
- scirex/experimental/analysis.py +138 -0
- scirex/experimental/distributed/__init__.py +4 -0
- scirex/experimental/distributed/comm.py +216 -0
- scirex/experimental/distributed/config.py +28 -0
- scirex/experimental/distributed/helpers.py +154 -0
- scirex/experimental/distributed/mappings.py +123 -0
- scirex/experimental/geometry/__init__.py +12 -0
- scirex/experimental/geometry/gmsh_mesh.py +249 -0
- scirex/experimental/geometry/mesh_io.py +220 -0
- scirex/experimental/giwo.py +271 -0
- scirex/experimental/losses/__init__.py +103 -0
- scirex/experimental/losses/ad_differentiation.py +237 -0
- scirex/experimental/losses/ad_pde_terms.py +410 -0
- scirex/experimental/losses/darcy_hybrid_loss.py +186 -0
- scirex/experimental/losses/domain_membership.py +309 -0
- scirex/experimental/losses/exact_ad_differentiation.py +267 -0
- scirex/experimental/losses/galerkin_t6.py +395 -0
- scirex/experimental/losses/patch_quadrature_ad.py +549 -0
- scirex/experimental/losses/quadrature.py +229 -0
- scirex/experimental/patching.py +626 -0
- scirex/experimental/training/__init__.py +49 -0
- scirex/experimental/training/trainer.py +887 -0
- scirex/experimental/training/training_state.py +220 -0
- scirex/experimental/tuning/__init__.py +50 -0
- scirex/experimental/tuning/tuner.py +449 -0
- scirex/info.py +66 -0
- scirex/layers/__init__.py +74 -0
- scirex/layers/blocks/__init__.py +66 -0
- scirex/layers/blocks/base_spectral_conv.py +33 -0
- scirex/layers/blocks/fno_block.py +542 -0
- scirex/layers/blocks/gno_block.py +270 -0
- scirex/layers/blocks/gno_weighting_functions.py +112 -0
- scirex/layers/blocks/integral_transform.py +227 -0
- scirex/layers/blocks/neighbor_search.py +665 -0
- scirex/layers/blocks/physics_attention.py +386 -0
- scirex/layers/blocks/spectral_convolution.py +573 -0
- scirex/layers/blocks/spectral_convolution_classic.py +148 -0
- scirex/layers/blocks/transolver_block.py +120 -0
- scirex/layers/blocks/wavelet_conv.py +441 -0
- scirex/layers/blocks/wno_block.py +57 -0
- scirex/layers/common/__init__.py +47 -0
- scirex/layers/common/channel_mlp.py +209 -0
- scirex/layers/common/complex.py +92 -0
- scirex/layers/common/embeddings.py +431 -0
- scirex/layers/common/mlp.py +45 -0
- scirex/layers/common/normalization.py +202 -0
- scirex/layers/common/padding.py +179 -0
- scirex/layers/common/resample.py +190 -0
- scirex/layers/common/skip_connections.py +184 -0
- scirex/losses/__init__.py +73 -0
- scirex/losses/darcy_piecewise_coefficient.py +666 -0
- scirex/losses/data_losses.py +708 -0
- scirex/losses/differentiation.py +342 -0
- scirex/losses/equation_losses.py +925 -0
- scirex/losses/galerkin.py +417 -0
- scirex/losses/galerkin_q4.py +364 -0
- scirex/losses/pde_terms.py +564 -0
- scirex/models/__init__.py +57 -0
- scirex/models/_legacy_refs.py +40 -0
- scirex/models/base.py +472 -0
- scirex/models/fno.py +470 -0
- scirex/models/fno_classic.py +209 -0
- scirex/models/gino.py +683 -0
- scirex/models/transolver.py +330 -0
- scirex/models/wno.py +250 -0
- scirex/py.typed +0 -0
- scirex/training/__init__.py +88 -0
- scirex/training/optim/__init__.py +33 -0
- scirex/training/optim/adamw.py +438 -0
- scirex/training/optim/tensor_galore_projector.py +188 -0
- scirex/utils.py +61 -0
- scirex-1.0.0.dist-info/METADATA +452 -0
- scirex-1.0.0.dist-info/RECORD +120 -0
- scirex-1.0.0.dist-info/WHEEL +4 -0
- scirex-1.0.0.dist-info/licenses/COPYING +674 -0
- scirex-1.0.0.dist-info/licenses/LICENSE +165 -0
scirex/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
ScireX neural operators: a JAX/Flax library for learning solution
|
|
6
|
+
operators of partial differential equations.
|
|
7
|
+
|
|
8
|
+
Exposes the top-level model factory `get_model_jax` together with the FNO,
|
|
9
|
+
WNO, GINO and Transolver architectures. Everything except the version string is
|
|
10
|
+
imported lazily (PEP 562), so `import scirex` does not import JAX; the first
|
|
11
|
+
access of `scirex.FNO` or `scirex.models` does.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from scirex._internal.aliases import install as _install_aliases
|
|
20
|
+
from scirex._internal.lazy import resolve_lazy_attr
|
|
21
|
+
|
|
22
|
+
# Old import paths of modules that moved keep working (with a DeprecationWarning);
|
|
23
|
+
# the table is scirex._internal.aliases.MOVED.
|
|
24
|
+
_install_aliases()
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
__version__: str # read from the package metadata on first access, see __getattr__
|
|
28
|
+
from .models import FNO, GINO, WNO, Transolver, get_model_jax
|
|
29
|
+
|
|
30
|
+
__all__ = ["FNO", "GINO", "WNO", "Transolver", "get_model_jax"]
|
|
31
|
+
|
|
32
|
+
_LAZY_EXPORTS: dict[str, str] = {
|
|
33
|
+
"FNO": "scirex.models",
|
|
34
|
+
"GINO": "scirex.models",
|
|
35
|
+
"Transolver": "scirex.models",
|
|
36
|
+
"WNO": "scirex.models",
|
|
37
|
+
"get_model_jax": "scirex.models",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Subpackages reachable as attributes of `scirex` without an explicit import.
|
|
41
|
+
_SUBMODULES = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"config",
|
|
44
|
+
"data",
|
|
45
|
+
"experimental",
|
|
46
|
+
"layers",
|
|
47
|
+
"losses",
|
|
48
|
+
"models",
|
|
49
|
+
"training",
|
|
50
|
+
"utils",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __getattr__(name: str) -> Any:
|
|
56
|
+
if name == "__version__":
|
|
57
|
+
# defined once, in pyproject.toml; looked up on first use so that
|
|
58
|
+
# `import scirex` does not scan the installed distributions
|
|
59
|
+
from scirex._internal.version import package_version
|
|
60
|
+
|
|
61
|
+
globals()["__version__"] = package_version()
|
|
62
|
+
return globals()["__version__"]
|
|
63
|
+
if name in _SUBMODULES:
|
|
64
|
+
return importlib.import_module(f"{__name__}.{name}")
|
|
65
|
+
return resolve_lazy_attr(__name__, name, _LAZY_EXPORTS, globals())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __dir__() -> list[str]:
|
|
69
|
+
return sorted({*globals(), *__all__, *_SUBMODULES, "__version__"})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Private implementation helpers of ScireX. No stability guarantees.
|
|
5
|
+
|
|
6
|
+
Nothing in this package is public API: modules, names and signatures may
|
|
7
|
+
change or disappear in any release. Do not import from it in user code.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Old import paths of modules that moved, kept working.
|
|
5
|
+
|
|
6
|
+
When a module moves, its old dotted path stays importable: importing it gives the *same
|
|
7
|
+
module object* as the new path (so `isinstance` checks, monkeypatching and private
|
|
8
|
+
names behave identically) and emits a `DeprecationWarning` that names the new path and
|
|
9
|
+
the release that removes the old one. Submodules follow their package, so
|
|
10
|
+
`scirex.distributed.comm` resolves to `scirex.experimental.distributed.comm`.
|
|
11
|
+
|
|
12
|
+
The table `MOVED` is the single list of moves. It also feeds
|
|
13
|
+
`scirex.models._legacy_refs`, which translates the same paths when a checkpoint is
|
|
14
|
+
loaded. Add a line here in the same change that moves a module.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib
|
|
20
|
+
import importlib.abc
|
|
21
|
+
import importlib.util
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Sequence
|
|
24
|
+
from importlib.machinery import ModuleSpec
|
|
25
|
+
from types import ModuleType
|
|
26
|
+
|
|
27
|
+
from scirex._internal.deprecation import deprecation_message, warn_deprecated
|
|
28
|
+
|
|
29
|
+
#: release in which the old paths were deprecated, and the earliest that may drop them
|
|
30
|
+
SINCE = "1.0.0"
|
|
31
|
+
REMOVAL = "1.2.0"
|
|
32
|
+
|
|
33
|
+
#: old dotted module path -> where the module lives now
|
|
34
|
+
MOVED: dict[str, str] = {
|
|
35
|
+
# not yet covered by the stability promise
|
|
36
|
+
"scirex.distributed": "scirex.experimental.distributed",
|
|
37
|
+
"scirex.geometry": "scirex.experimental.geometry",
|
|
38
|
+
"scirex.models.giwo": "scirex.experimental.giwo",
|
|
39
|
+
"scirex.training.patching": "scirex.experimental.patching",
|
|
40
|
+
"scirex.tuning": "scirex.experimental.tuning",
|
|
41
|
+
"scirex.losses.ad_differentiation": "scirex.experimental.losses.ad_differentiation",
|
|
42
|
+
"scirex.losses.ad_pde_terms": "scirex.experimental.losses.ad_pde_terms",
|
|
43
|
+
"scirex.losses.exact_ad_differentiation": "scirex.experimental.losses.exact_ad_differentiation",
|
|
44
|
+
"scirex.losses.patch_quadrature_ad": "scirex.experimental.losses.patch_quadrature_ad",
|
|
45
|
+
"scirex.losses.quadrature": "scirex.experimental.losses.quadrature",
|
|
46
|
+
"scirex.losses.domain_membership": "scirex.experimental.losses.domain_membership",
|
|
47
|
+
"scirex.losses.galerkin_t6": "scirex.experimental.losses.galerkin_t6",
|
|
48
|
+
"scirex.losses.darcy_hybrid_loss": "scirex.experimental.losses.darcy_hybrid_loss",
|
|
49
|
+
"scirex.config.distributed": "scirex.experimental.distributed.config",
|
|
50
|
+
# private helpers
|
|
51
|
+
"scirex.data.web_utils": "scirex._internal.web_utils",
|
|
52
|
+
"scirex.layers.common.einsum_utils": "scirex._internal.einsum_utils",
|
|
53
|
+
"scirex.layers.common.segment_csr": "scirex._internal.segment_csr",
|
|
54
|
+
"scirex.training.trainer": "scirex.experimental.training.trainer",
|
|
55
|
+
"scirex.training.training_state": "scirex.experimental.training.training_state",
|
|
56
|
+
"scirex.training.torch_setup": "scirex._internal.torch_setup",
|
|
57
|
+
"scirex.training.wno_train_utils": "scirex._internal.wno_train_utils",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def moved_message(old: str, new: str) -> str:
|
|
62
|
+
"""The text of the `DeprecationWarning` for the import of `old`."""
|
|
63
|
+
return deprecation_message(old, since=SINCE, removal=REMOVAL, alternative=new)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _AliasLoader(importlib.abc.Loader):
|
|
67
|
+
"""Hands out the module that lives at the new path instead of loading a copy."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, old: str, new: str) -> None:
|
|
70
|
+
self.old, self.new = old, new
|
|
71
|
+
self._real_spec: ModuleSpec | None = None
|
|
72
|
+
|
|
73
|
+
def create_module(self, spec: ModuleSpec) -> ModuleType:
|
|
74
|
+
module = importlib.import_module(self.new)
|
|
75
|
+
self._real_spec = module.__spec__
|
|
76
|
+
# stacklevel 3: this frame, warn_deprecated's, then the importing line (the
|
|
77
|
+
# import machinery's own frames are skipped)
|
|
78
|
+
warn_deprecated(
|
|
79
|
+
self.old,
|
|
80
|
+
since=SINCE,
|
|
81
|
+
removal=REMOVAL,
|
|
82
|
+
alternative=self.new,
|
|
83
|
+
stacklevel=3,
|
|
84
|
+
)
|
|
85
|
+
return module
|
|
86
|
+
|
|
87
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
88
|
+
# the import system stamped the alias spec on the shared module; put the real
|
|
89
|
+
# one back so `module.__spec__.name` still agrees with `module.__name__`
|
|
90
|
+
if self._real_spec is not None:
|
|
91
|
+
module.__spec__ = self._real_spec
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AliasFinder(importlib.abc.MetaPathFinder):
|
|
95
|
+
"""Meta-path finder for the keys of a move table (and their submodules)."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, table: dict[str, str] | None = None) -> None:
|
|
98
|
+
self.table = MOVED if table is None else table
|
|
99
|
+
|
|
100
|
+
def target(self, fullname: str) -> str | None:
|
|
101
|
+
"""The new dotted path for `fullname`, or `None` if it was not moved."""
|
|
102
|
+
for old, new in self.table.items():
|
|
103
|
+
if fullname == old:
|
|
104
|
+
return new
|
|
105
|
+
if fullname.startswith(old + "."):
|
|
106
|
+
return new + fullname[len(old) :]
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def find_spec(
|
|
110
|
+
self,
|
|
111
|
+
fullname: str,
|
|
112
|
+
path: Sequence[str] | None = None,
|
|
113
|
+
target: ModuleType | None = None,
|
|
114
|
+
) -> ModuleSpec | None:
|
|
115
|
+
new = self.target(fullname)
|
|
116
|
+
if new is None:
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
real = importlib.util.find_spec(new)
|
|
120
|
+
except (ImportError, ValueError):
|
|
121
|
+
return None
|
|
122
|
+
if real is None:
|
|
123
|
+
return None
|
|
124
|
+
return importlib.util.spec_from_loader(
|
|
125
|
+
fullname,
|
|
126
|
+
_AliasLoader(fullname, new),
|
|
127
|
+
is_package=real.submodule_search_locations is not None,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def install(table: dict[str, str] | None = None) -> AliasFinder:
|
|
132
|
+
"""Put an `AliasFinder` first on `sys.meta_path` (once) and return it.
|
|
133
|
+
|
|
134
|
+
It must come first: the ordinary path finder would otherwise find the submodules of
|
|
135
|
+
a moved package inside its new directory and load them a second time under the old
|
|
136
|
+
name.
|
|
137
|
+
"""
|
|
138
|
+
wanted = MOVED if table is None else table
|
|
139
|
+
for finder in sys.meta_path:
|
|
140
|
+
if isinstance(finder, AliasFinder) and finder.table is wanted:
|
|
141
|
+
return finder
|
|
142
|
+
finder = AliasFinder(wanted)
|
|
143
|
+
sys.meta_path.insert(0, finder)
|
|
144
|
+
return finder
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Deprecation helpers implementing the policy in `doc/dev_guide/api_policy.md`.
|
|
5
|
+
|
|
6
|
+
A deprecated public name keeps working for at least two minor releases and emits
|
|
7
|
+
a `DeprecationWarning` that says when it was deprecated, when it can be removed
|
|
8
|
+
and what to use instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any, TypeVar
|
|
17
|
+
|
|
18
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def deprecation_message(
|
|
22
|
+
name: str, *, since: str, removal: str, alternative: str | None = None
|
|
23
|
+
) -> str:
|
|
24
|
+
"""The standard text of the `DeprecationWarning` for `name`."""
|
|
25
|
+
message = f"{name} is deprecated since ScireX {since} and will be removed in {removal} or later."
|
|
26
|
+
if alternative:
|
|
27
|
+
message += f" Use {alternative} instead."
|
|
28
|
+
return message
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def warn_deprecated(
|
|
32
|
+
name: str,
|
|
33
|
+
*,
|
|
34
|
+
since: str,
|
|
35
|
+
removal: str,
|
|
36
|
+
alternative: str | None = None,
|
|
37
|
+
stacklevel: int = 2,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Emit the standard `DeprecationWarning` for `name`.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
name: Dotted name of the deprecated object, e.g. `"scirex.training.resolve_dtype"`.
|
|
43
|
+
since: Release in which the deprecation started.
|
|
44
|
+
removal: Earliest release in which the name may be removed.
|
|
45
|
+
alternative: What users should use instead, if anything.
|
|
46
|
+
stacklevel: Passed to `warnings.warn`; `2` points at the caller of
|
|
47
|
+
the function that calls this helper.
|
|
48
|
+
"""
|
|
49
|
+
message = deprecation_message(
|
|
50
|
+
name, since=since, removal=removal, alternative=alternative
|
|
51
|
+
)
|
|
52
|
+
warnings.warn(message, DeprecationWarning, stacklevel=stacklevel)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def deprecated(
|
|
56
|
+
*, since: str, removal: str, alternative: str | None = None
|
|
57
|
+
) -> Callable[[F], F]:
|
|
58
|
+
"""Decorate a function or class so that using it emits a `DeprecationWarning`."""
|
|
59
|
+
|
|
60
|
+
def decorator(obj: F) -> F:
|
|
61
|
+
name = f"{obj.__module__}.{obj.__qualname__}"
|
|
62
|
+
if isinstance(obj, type):
|
|
63
|
+
original_init = obj.__init__ # type: ignore[misc]
|
|
64
|
+
|
|
65
|
+
@functools.wraps(original_init)
|
|
66
|
+
def init(self: Any, *args: Any, **kwargs: Any) -> None:
|
|
67
|
+
warn_deprecated(
|
|
68
|
+
name,
|
|
69
|
+
since=since,
|
|
70
|
+
removal=removal,
|
|
71
|
+
alternative=alternative,
|
|
72
|
+
stacklevel=3,
|
|
73
|
+
)
|
|
74
|
+
original_init(self, *args, **kwargs)
|
|
75
|
+
|
|
76
|
+
obj.__init__ = init # type: ignore[assignment, misc]
|
|
77
|
+
return obj
|
|
78
|
+
|
|
79
|
+
@functools.wraps(obj)
|
|
80
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
81
|
+
warn_deprecated(
|
|
82
|
+
name,
|
|
83
|
+
since=since,
|
|
84
|
+
removal=removal,
|
|
85
|
+
alternative=alternative,
|
|
86
|
+
stacklevel=3,
|
|
87
|
+
)
|
|
88
|
+
return obj(*args, **kwargs)
|
|
89
|
+
|
|
90
|
+
return wrapper # type: ignore[return-value]
|
|
91
|
+
|
|
92
|
+
return decorator
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Einsum utilities for half-precision complex tensors
|
|
6
|
+
(JAX analogues of torch `view_as_real` / `view_as_complex`).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp
|
|
10
|
+
import opt_einsum
|
|
11
|
+
import tensorly as tl
|
|
12
|
+
from tensorly.plugins import use_opt_einsum
|
|
13
|
+
|
|
14
|
+
from .tensorly_backend import use_jax_backend
|
|
15
|
+
|
|
16
|
+
use_jax_backend()
|
|
17
|
+
use_opt_einsum("optimal")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _view_as_real(a: jnp.ndarray) -> jnp.ndarray:
|
|
21
|
+
"""JAX equivalent of torch.view_as_real: complex (...) -> real (..., 2)"""
|
|
22
|
+
return jnp.stack([a.real, a.imag], axis=-1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _view_as_complex(a: jnp.ndarray) -> jnp.ndarray:
|
|
26
|
+
"""JAX equivalent of torch.view_as_complex: real (..., 2) -> complex (...)"""
|
|
27
|
+
return a[..., 0] + 1j * a[..., 1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def einsum_complexhalf_two_input(
|
|
31
|
+
eq: str, a: jnp.ndarray, b: jnp.ndarray
|
|
32
|
+
) -> jnp.ndarray:
|
|
33
|
+
"""
|
|
34
|
+
Compute (two-input) einsum for complexhalf tensors.
|
|
35
|
+
Because jnp.einsum currently does not support complex32 (complexhalf) types.
|
|
36
|
+
The inputs and outputs are the same as in jnp.einsum
|
|
37
|
+
"""
|
|
38
|
+
assert len(eq.split(",")) == 2, "Equation must have two inputs."
|
|
39
|
+
|
|
40
|
+
# cast both tensors to "view as real" form, and half precision
|
|
41
|
+
a = _view_as_real(a)
|
|
42
|
+
b = _view_as_real(b)
|
|
43
|
+
a = a.astype(jnp.float16)
|
|
44
|
+
b = b.astype(jnp.float16)
|
|
45
|
+
|
|
46
|
+
# create a new einsum equation that takes into account "view as real" form
|
|
47
|
+
input_output = eq.split("->")
|
|
48
|
+
new_output = "xy" + input_output[1]
|
|
49
|
+
input_terms = input_output[0].split(",")
|
|
50
|
+
new_inputs = [input_terms[0] + "x", input_terms[1] + "y"]
|
|
51
|
+
new_eqn = new_inputs[0] + "," + new_inputs[1] + "->" + new_output
|
|
52
|
+
|
|
53
|
+
# convert back to complex form
|
|
54
|
+
tmp = tl.einsum(new_eqn, a, b)
|
|
55
|
+
res = jnp.stack(
|
|
56
|
+
[tmp[0, 0, ...] - tmp[1, 1, ...], tmp[1, 0, ...] + tmp[0, 1, ...]], axis=-1
|
|
57
|
+
)
|
|
58
|
+
return _view_as_complex(res)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def einsum_complexhalf(eq: str, *args: jnp.ndarray) -> jnp.ndarray:
|
|
62
|
+
"""
|
|
63
|
+
Compute einsum for complexhalf tensors.
|
|
64
|
+
Because jnp.einsum currently does not support complex32 (complexhalf) types.
|
|
65
|
+
The inputs and outputs are the same as in jnp.einsum
|
|
66
|
+
"""
|
|
67
|
+
if len(args) == 2:
|
|
68
|
+
# if there are two inputs, it is faster to call this method
|
|
69
|
+
return einsum_complexhalf_two_input(eq, *args)
|
|
70
|
+
|
|
71
|
+
# find the optimal path
|
|
72
|
+
_, path_info = opt_einsum.contract_path(eq, *args)
|
|
73
|
+
partial_eqns = [
|
|
74
|
+
contraction_info[2] for contraction_info in path_info.contraction_list
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# create a dict of the input tensors by their label in the einsum equation
|
|
78
|
+
input_labels = eq.split("->")[0].split(",")
|
|
79
|
+
output_label = eq.split("->")[1]
|
|
80
|
+
tensors = dict(zip(input_labels, args))
|
|
81
|
+
|
|
82
|
+
# convert all tensors to half precision and "view as real" form
|
|
83
|
+
for key, tensor in tensors.items():
|
|
84
|
+
tensor = _view_as_real(tensor)
|
|
85
|
+
tensor = tensor.astype(jnp.float16)
|
|
86
|
+
tensors[key] = tensor
|
|
87
|
+
|
|
88
|
+
for partial_eq in partial_eqns:
|
|
89
|
+
# get the input tensors to partial_eq
|
|
90
|
+
in_labels, out_label = partial_eq.split("->")
|
|
91
|
+
in_labels = in_labels.split(",")
|
|
92
|
+
in_tensors = [tensors[label] for label in in_labels]
|
|
93
|
+
|
|
94
|
+
# create new einsum equation that takes into account "view as real" form
|
|
95
|
+
input_output = partial_eq.split("->")
|
|
96
|
+
new_output = "xy" + input_output[1]
|
|
97
|
+
input_terms = input_output[0].split(",")
|
|
98
|
+
new_inputs = [input_terms[0] + "x", input_terms[1] + "y"]
|
|
99
|
+
new_eqn = new_inputs[0] + "," + new_inputs[1] + "->" + new_output
|
|
100
|
+
|
|
101
|
+
# perform the einsum, and convert to "view as real" form
|
|
102
|
+
tmp = tl.einsum(new_eqn, *in_tensors)
|
|
103
|
+
result = jnp.stack(
|
|
104
|
+
[tmp[0, 0, ...] - tmp[1, 1, ...], tmp[1, 0, ...] + tmp[0, 1, ...]], axis=-1
|
|
105
|
+
)
|
|
106
|
+
tensors[out_label] = result
|
|
107
|
+
|
|
108
|
+
return _view_as_complex(tensors[output_label])
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Install hints for third-party packages that live in optional extras.
|
|
5
|
+
|
|
6
|
+
`pyproject.toml` groups packages that only some features need (`vtk` for the
|
|
7
|
+
benchmark loaders, `mlflow` and `optuna` for tuning) into extras. The helpers
|
|
8
|
+
here turn the bare `ModuleNotFoundError` for such a package into an
|
|
9
|
+
`ImportError` that names the feature and the extra to install.
|
|
10
|
+
|
|
11
|
+
This module must stay free of heavy imports.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import functools
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import Any, TypeVar
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def missing_extra_error(
|
|
24
|
+
err: ModuleNotFoundError, *, packages: tuple[str, ...], extra: str, feature: str
|
|
25
|
+
) -> ImportError | None:
|
|
26
|
+
"""Build an `ImportError` that names the extra to install.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
err: The `ModuleNotFoundError` that was raised.
|
|
30
|
+
packages: Top-level import names that the extra provides.
|
|
31
|
+
extra: Name of the extra in `pyproject.toml`, e.g. `"hpo"`.
|
|
32
|
+
feature: Human-readable name of what needs the package.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
An `ImportError` with the install command, or `None` if the missing
|
|
36
|
+
module is not one of `packages` (so the original error should propagate).
|
|
37
|
+
"""
|
|
38
|
+
missing = (err.name or "").split(".")[0]
|
|
39
|
+
if missing not in packages:
|
|
40
|
+
return None
|
|
41
|
+
return ImportError(
|
|
42
|
+
f"{feature} requires {missing!r}, which is not installed. "
|
|
43
|
+
f'Install the {extra!r} extra with `pip install "scirex[{extra}]"`.'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def requires_extra(*packages: str, extra: str) -> Callable[[F], F]:
|
|
48
|
+
"""Decorate a function so that a missing `packages` import names the extra.
|
|
49
|
+
|
|
50
|
+
The check happens when the function runs, not when it is defined, so code paths
|
|
51
|
+
that do not need the package (for example a cache hit) keep working without it.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def decorator(func: F) -> F:
|
|
55
|
+
feature = f"{func.__module__}.{func.__qualname__}"
|
|
56
|
+
|
|
57
|
+
@functools.wraps(func)
|
|
58
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
59
|
+
try:
|
|
60
|
+
return func(*args, **kwargs)
|
|
61
|
+
except ModuleNotFoundError as err:
|
|
62
|
+
explained = missing_extra_error(
|
|
63
|
+
err, packages=packages, extra=extra, feature=feature
|
|
64
|
+
)
|
|
65
|
+
if explained is None:
|
|
66
|
+
raise
|
|
67
|
+
raise explained from err
|
|
68
|
+
|
|
69
|
+
return wrapper # type: ignore[return-value]
|
|
70
|
+
|
|
71
|
+
return decorator
|
scirex/_internal/lazy.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 ZenteiQ AiTech Innovations Pvt. Ltd.
|
|
2
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
"""Helpers for package-level lazy exports (PEP 562).
|
|
5
|
+
|
|
6
|
+
Public packages such as `scirex.models` export their names through a module
|
|
7
|
+
level `__getattr__` so that `import scirex.models` does not import JAX, Flax or
|
|
8
|
+
any optional dependency. The first access of a name imports the module that defines
|
|
9
|
+
it. If that import fails because a third-party dependency is missing, the failure
|
|
10
|
+
is reported as an `ImportError` that names the dependency and the feature
|
|
11
|
+
that needs it.
|
|
12
|
+
|
|
13
|
+
This module must stay free of heavy imports.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
from collections.abc import Mapping, MutableMapping
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# Import name -> name on PyPI, for the packages whose absence we explain.
|
|
23
|
+
_PIP_NAMES: dict[str, str] = {
|
|
24
|
+
"einops": "einops",
|
|
25
|
+
"jaxwt": "jaxwt",
|
|
26
|
+
"open3d": "open3d",
|
|
27
|
+
"scipy": "scipy",
|
|
28
|
+
"tensorly": "tensorly",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def missing_dependency_error(
|
|
33
|
+
err: ModuleNotFoundError, feature: str
|
|
34
|
+
) -> ImportError | None:
|
|
35
|
+
"""Build an explanatory `ImportError` for a missing third-party package.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
err: The `ModuleNotFoundError` raised while importing `feature`.
|
|
39
|
+
feature: Human-readable name of what was being imported, e.g. `"WNO"`.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
An `ImportError` naming the missing package, or `None` if the missing
|
|
43
|
+
module is part of ScireX itself (a real bug that must not be disguised).
|
|
44
|
+
"""
|
|
45
|
+
missing = (err.name or "").split(".")[0]
|
|
46
|
+
if not missing or missing == "scirex":
|
|
47
|
+
return None
|
|
48
|
+
pip_name = _PIP_NAMES.get(missing, missing)
|
|
49
|
+
return ImportError(
|
|
50
|
+
f"{feature} requires the third-party package {missing!r}, "
|
|
51
|
+
"which is not installed. "
|
|
52
|
+
f"Install it with `pip install {pip_name}`."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def import_feature(module_name: str, feature: str) -> Any:
|
|
57
|
+
"""Import `module_name`, explaining a missing third-party dependency.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
module_name: Absolute dotted module name.
|
|
61
|
+
feature: Human-readable name of what needs the module.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The imported module.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ImportError: If a third-party dependency of the module is not installed.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
return importlib.import_module(module_name)
|
|
71
|
+
except ModuleNotFoundError as err:
|
|
72
|
+
explained = missing_dependency_error(err, feature)
|
|
73
|
+
if explained is None:
|
|
74
|
+
raise
|
|
75
|
+
raise explained from err
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_lazy_attr(
|
|
79
|
+
package: str,
|
|
80
|
+
name: str,
|
|
81
|
+
table: Mapping[str, str],
|
|
82
|
+
namespace: MutableMapping[str, Any],
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Resolve `name` of `package` through `table` and cache it in `namespace`.
|
|
85
|
+
|
|
86
|
+
Meant to be called from a module level `__getattr__`.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
package: `__name__` of the package whose attribute is requested.
|
|
90
|
+
name: The requested attribute.
|
|
91
|
+
table: Maps each lazily exported name to the absolute name of the module
|
|
92
|
+
that provides it.
|
|
93
|
+
namespace: The package's `globals()`; the resolved value is stored there
|
|
94
|
+
so that later lookups bypass `__getattr__`.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The attribute.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
AttributeError: If `name` is not in `table`.
|
|
101
|
+
ImportError: If a third-party dependency of the defining module is missing.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
module_name = table[name]
|
|
105
|
+
except KeyError:
|
|
106
|
+
raise AttributeError(f"module {package!r} has no attribute {name!r}") from None
|
|
107
|
+
value = getattr(import_feature(module_name, name), name)
|
|
108
|
+
namespace[name] = value
|
|
109
|
+
return value
|