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.
Files changed (120) hide show
  1. scirex/__init__.py +69 -0
  2. scirex/_internal/__init__.py +8 -0
  3. scirex/_internal/aliases.py +144 -0
  4. scirex/_internal/deprecation.py +92 -0
  5. scirex/_internal/einsum_utils.py +108 -0
  6. scirex/_internal/extras.py +71 -0
  7. scirex/_internal/lazy.py +109 -0
  8. scirex/_internal/neighbor_logging.py +227 -0
  9. scirex/_internal/paths.py +15 -0
  10. scirex/_internal/scaling.py +97 -0
  11. scirex/_internal/segment_csr.py +63 -0
  12. scirex/_internal/serialization.py +44 -0
  13. scirex/_internal/tensorly_backend.py +23 -0
  14. scirex/_internal/torch_setup.py +118 -0
  15. scirex/_internal/version.py +56 -0
  16. scirex/_internal/wandb_utils.py +47 -0
  17. scirex/_internal/web_utils.py +211 -0
  18. scirex/_internal/wno_train_utils.py +172 -0
  19. scirex/config/__init__.py +71 -0
  20. scirex/config/gino_carcfd_config.py +50 -0
  21. scirex/config/models.py +539 -0
  22. scirex/config/opt.py +91 -0
  23. scirex/config/transolver_pipe_config.py +67 -0
  24. scirex/data/__init__.py +65 -0
  25. scirex/data/containers/__init__.py +29 -0
  26. scirex/data/containers/dict_dataset.py +45 -0
  27. scirex/data/containers/pt_dataset.py +397 -0
  28. scirex/data/containers/tensor_dataset.py +118 -0
  29. scirex/data/datasets/__init__.py +49 -0
  30. scirex/data/datasets/airfrans.py +377 -0
  31. scirex/data/datasets/car_cfd.py +133 -0
  32. scirex/data/datasets/cavity_kepsilon.py +379 -0
  33. scirex/data/datasets/chipcooling.py +377 -0
  34. scirex/data/datasets/darcy.py +356 -0
  35. scirex/data/datasets/mesh_datamodule.py +423 -0
  36. scirex/data/datasets/navier_stokes.py +242 -0
  37. scirex/data/datasets/shapenet_car.py +428 -0
  38. scirex/data/datasets/shapenet_car_boundary.py +257 -0
  39. scirex/data/datasets/shapenet_car_drag.py +351 -0
  40. scirex/data/transforms/__init__.py +29 -0
  41. scirex/data/transforms/base_transforms.py +147 -0
  42. scirex/data/transforms/data_processors.py +372 -0
  43. scirex/data/transforms/normalizers.py +382 -0
  44. scirex/experimental/__init__.py +2 -0
  45. scirex/experimental/analysis.py +138 -0
  46. scirex/experimental/distributed/__init__.py +4 -0
  47. scirex/experimental/distributed/comm.py +216 -0
  48. scirex/experimental/distributed/config.py +28 -0
  49. scirex/experimental/distributed/helpers.py +154 -0
  50. scirex/experimental/distributed/mappings.py +123 -0
  51. scirex/experimental/geometry/__init__.py +12 -0
  52. scirex/experimental/geometry/gmsh_mesh.py +249 -0
  53. scirex/experimental/geometry/mesh_io.py +220 -0
  54. scirex/experimental/giwo.py +271 -0
  55. scirex/experimental/losses/__init__.py +103 -0
  56. scirex/experimental/losses/ad_differentiation.py +237 -0
  57. scirex/experimental/losses/ad_pde_terms.py +410 -0
  58. scirex/experimental/losses/darcy_hybrid_loss.py +186 -0
  59. scirex/experimental/losses/domain_membership.py +309 -0
  60. scirex/experimental/losses/exact_ad_differentiation.py +267 -0
  61. scirex/experimental/losses/galerkin_t6.py +395 -0
  62. scirex/experimental/losses/patch_quadrature_ad.py +549 -0
  63. scirex/experimental/losses/quadrature.py +229 -0
  64. scirex/experimental/patching.py +626 -0
  65. scirex/experimental/training/__init__.py +49 -0
  66. scirex/experimental/training/trainer.py +887 -0
  67. scirex/experimental/training/training_state.py +220 -0
  68. scirex/experimental/tuning/__init__.py +50 -0
  69. scirex/experimental/tuning/tuner.py +449 -0
  70. scirex/info.py +66 -0
  71. scirex/layers/__init__.py +74 -0
  72. scirex/layers/blocks/__init__.py +66 -0
  73. scirex/layers/blocks/base_spectral_conv.py +33 -0
  74. scirex/layers/blocks/fno_block.py +542 -0
  75. scirex/layers/blocks/gno_block.py +270 -0
  76. scirex/layers/blocks/gno_weighting_functions.py +112 -0
  77. scirex/layers/blocks/integral_transform.py +227 -0
  78. scirex/layers/blocks/neighbor_search.py +665 -0
  79. scirex/layers/blocks/physics_attention.py +386 -0
  80. scirex/layers/blocks/spectral_convolution.py +573 -0
  81. scirex/layers/blocks/spectral_convolution_classic.py +148 -0
  82. scirex/layers/blocks/transolver_block.py +120 -0
  83. scirex/layers/blocks/wavelet_conv.py +441 -0
  84. scirex/layers/blocks/wno_block.py +57 -0
  85. scirex/layers/common/__init__.py +47 -0
  86. scirex/layers/common/channel_mlp.py +209 -0
  87. scirex/layers/common/complex.py +92 -0
  88. scirex/layers/common/embeddings.py +431 -0
  89. scirex/layers/common/mlp.py +45 -0
  90. scirex/layers/common/normalization.py +202 -0
  91. scirex/layers/common/padding.py +179 -0
  92. scirex/layers/common/resample.py +190 -0
  93. scirex/layers/common/skip_connections.py +184 -0
  94. scirex/losses/__init__.py +73 -0
  95. scirex/losses/darcy_piecewise_coefficient.py +666 -0
  96. scirex/losses/data_losses.py +708 -0
  97. scirex/losses/differentiation.py +342 -0
  98. scirex/losses/equation_losses.py +925 -0
  99. scirex/losses/galerkin.py +417 -0
  100. scirex/losses/galerkin_q4.py +364 -0
  101. scirex/losses/pde_terms.py +564 -0
  102. scirex/models/__init__.py +57 -0
  103. scirex/models/_legacy_refs.py +40 -0
  104. scirex/models/base.py +472 -0
  105. scirex/models/fno.py +470 -0
  106. scirex/models/fno_classic.py +209 -0
  107. scirex/models/gino.py +683 -0
  108. scirex/models/transolver.py +330 -0
  109. scirex/models/wno.py +250 -0
  110. scirex/py.typed +0 -0
  111. scirex/training/__init__.py +88 -0
  112. scirex/training/optim/__init__.py +33 -0
  113. scirex/training/optim/adamw.py +438 -0
  114. scirex/training/optim/tensor_galore_projector.py +188 -0
  115. scirex/utils.py +61 -0
  116. scirex-1.0.0.dist-info/METADATA +452 -0
  117. scirex-1.0.0.dist-info/RECORD +120 -0
  118. scirex-1.0.0.dist-info/WHEEL +4 -0
  119. scirex-1.0.0.dist-info/licenses/COPYING +674 -0
  120. 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
@@ -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