pynumerics 2.3.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 (46) hide show
  1. pynumerics/__init__.py +73 -0
  2. pynumerics/_bin/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  3. pynumerics/_bin/Microsoft.CodeAnalysis.LICENSE.txt +27 -0
  4. pynumerics/_bin/Microsoft.CodeAnalysis.dll +0 -0
  5. pynumerics/_bin/Numerics.dll +0 -0
  6. pynumerics/_bin/pynumerics.runtimeconfig.json +15 -0
  7. pynumerics/_coverage.py +3825 -0
  8. pynumerics/_util.py +237 -0
  9. pynumerics/_version.py +40 -0
  10. pynumerics/bootstrap.py +593 -0
  11. pynumerics/callbacks.py +791 -0
  12. pynumerics/copulas.py +910 -0
  13. pynumerics/data.py +2233 -0
  14. pynumerics/differentiation.py +348 -0
  15. pynumerics/distributions.py +4043 -0
  16. pynumerics/download.py +623 -0
  17. pynumerics/enums.py +119 -0
  18. pynumerics/errors.py +245 -0
  19. pynumerics/functions.py +1142 -0
  20. pynumerics/goodness_of_fit.py +699 -0
  21. pynumerics/graphs.py +556 -0
  22. pynumerics/hypothesis_tests.py +445 -0
  23. pynumerics/identity.py +144 -0
  24. pynumerics/integration.py +677 -0
  25. pynumerics/linalg.py +1408 -0
  26. pynumerics/machine_learning.py +1335 -0
  27. pynumerics/marshal.py +733 -0
  28. pynumerics/mathematics.py +1053 -0
  29. pynumerics/mcmc.py +1406 -0
  30. pynumerics/multivariate.py +1539 -0
  31. pynumerics/ode.py +156 -0
  32. pynumerics/optimization.py +1736 -0
  33. pynumerics/probability.py +966 -0
  34. pynumerics/py.typed +0 -0
  35. pynumerics/regional.py +522 -0
  36. pynumerics/roots.py +274 -0
  37. pynumerics/runtime.py +384 -0
  38. pynumerics/sampling.py +1758 -0
  39. pynumerics/special.py +1079 -0
  40. pynumerics/statistics.py +1958 -0
  41. pynumerics/timeseries.py +1326 -0
  42. pynumerics-2.3.0.dist-info/METADATA +142 -0
  43. pynumerics-2.3.0.dist-info/RECORD +46 -0
  44. pynumerics-2.3.0.dist-info/WHEEL +5 -0
  45. pynumerics-2.3.0.dist-info/licenses/LICENSE +10 -0
  46. pynumerics-2.3.0.dist-info/top_level.txt +1 -0
pynumerics/roots.py ADDED
@@ -0,0 +1,274 @@
1
+ """Root finding from ``Numerics.Mathematics.RootFinding``: bisection, Brent, Newton-Raphson and secant.
2
+
3
+ Each function calls the static Numerics solver with the function given as a Python callable,
4
+ C# source or a .NET delegate (see :mod:`pynumerics.callbacks`). The defaults are the Numerics
5
+ defaults.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+
14
+ from pynumerics import callbacks
15
+ from pynumerics._util import namespace
16
+ from pynumerics.errors import translate_public
17
+ from pynumerics.marshal import to_net_doubles, to_numpy_doubles
18
+
19
+
20
+ def _root_finding() -> Any:
21
+ """Return the ``Numerics.Mathematics.RootFinding`` namespace after starting the runtime."""
22
+ return namespace("Numerics.Mathematics.RootFinding")
23
+
24
+
25
+ def bisection_solve(f: Any, first_guess: float, lower: float, upper: float, tolerance: float = 1e-8,
26
+ max_iterations: int = 1000, report_failure: bool = True) -> float:
27
+ """Find a root of ``f`` in a sign-changing bracket by bisection (``Bisection.Solve``).
28
+
29
+ Parameters
30
+ ----------
31
+ f : callable, str or System.Delegate
32
+ Function of one float.
33
+ first_guess : float
34
+ A point inside the bracket; it is validated but does not steer the search.
35
+ lower, upper : float
36
+ Bracket with f(lower)·f(upper) ≤ 0.
37
+ tolerance : float, optional
38
+ Tolerance on the step between successive estimates; default 1e-8.
39
+ max_iterations : int, optional
40
+ Iteration limit; default 1000.
41
+ report_failure : bool, optional
42
+ Raise when the solver fails to converge; default True.
43
+
44
+ Returns
45
+ -------
46
+ float
47
+ The root.
48
+
49
+ Raises
50
+ ------
51
+ ValueError
52
+ If the bracket does not change sign or the guess lies outside it, or if the solver does not
53
+ converge and ``report_failure`` is True (raised by Numerics).
54
+ """
55
+ return float(_root_finding().Bisection.Solve(callbacks.scalar_function(f), float(first_guess), float(lower),
56
+ float(upper), float(tolerance), int(max_iterations),
57
+ bool(report_failure)))
58
+
59
+
60
+ BRENT_MINIMUM_RELATIVE_TOLERANCE = 8.881784197001252e-16
61
+ """``Brent.MinimumRelativeTolerance``: 4 * 2**-52, the default and smallest ``relative_tolerance`` of
62
+ :func:`brent_solve` (the default ``rtol`` of ``scipy.optimize.brentq``)."""
63
+
64
+
65
+ def brent_solve(f: Any, lower: float, upper: float, tolerance: float = 1e-8, max_iterations: int = 1000,
66
+ report_failure: bool = True, *,
67
+ relative_tolerance: float = BRENT_MINIMUM_RELATIVE_TOLERANCE) -> float:
68
+ """Find a root of ``f`` in a sign-changing bracket by Brent's method (``Brent.Solve``).
69
+
70
+ The solver is a literal port of SciPy's ``brentq.c``, so the root is bitwise equal to
71
+ ``scipy.optimize.brentq(f, lower, upper, xtol=tolerance, rtol=relative_tolerance, maxiter=max_iterations)``.
72
+
73
+ Parameters
74
+ ----------
75
+ f : callable, str or System.Delegate
76
+ Function of one float.
77
+ lower, upper : float
78
+ Bracket with f(lower)·f(upper) ≤ 0.
79
+ tolerance : float, optional
80
+ Absolute tolerance (``xtol``), non-negative; default 1e-8.
81
+ max_iterations : int, optional
82
+ Iteration limit; default 1000.
83
+ report_failure : bool, optional
84
+ Raise when the solver fails (the Numerics default); default True.
85
+ relative_tolerance : float, optional
86
+ Relative tolerance (``rtol``), at least :data:`BRENT_MINIMUM_RELATIVE_TOLERANCE`, the default.
87
+
88
+ Returns
89
+ -------
90
+ float
91
+ The root.
92
+
93
+ Raises
94
+ ------
95
+ ValueError
96
+ If the bracket does not change sign, or a tolerance is out of range (raised by Numerics).
97
+ """
98
+ return float(_root_finding().Brent.Solve(callbacks.scalar_function(f), float(lower), float(upper),
99
+ float(tolerance), float(relative_tolerance), int(max_iterations),
100
+ bool(report_failure)))
101
+
102
+
103
+ def brent_bracket(f: Any, lower: float, upper: float,
104
+ max_iterations: int = 10) -> tuple[bool, float, float, float, float]:
105
+ """Expand a range outward until it brackets a sign change (``Brent.Bracket``).
106
+
107
+ Parameters
108
+ ----------
109
+ f : callable, str or System.Delegate
110
+ Function of one float.
111
+ lower, upper : float
112
+ Starting range.
113
+ max_iterations : int, optional
114
+ Expansion limit; default 10.
115
+
116
+ Returns
117
+ -------
118
+ tuple
119
+ ``(found, lower, upper, f_lower, f_upper)``.
120
+ """
121
+ found, lo, hi, f1, f2 = _root_finding().Brent.Bracket(callbacks.scalar_function(f), float(lower), float(upper),
122
+ 0.0, 0.0, int(max_iterations))
123
+ return bool(found), float(lo), float(hi), float(f1), float(f2)
124
+
125
+
126
+ def secant_solve(f: Any, lower: float, upper: float, tolerance: float = 1e-8, max_iterations: int = 1000,
127
+ report_failure: bool = True) -> float:
128
+ """Find a root of ``f`` by the secant method started from two points (``Secant.Solve``).
129
+
130
+ Parameters
131
+ ----------
132
+ f : callable, str or System.Delegate
133
+ Function of one float.
134
+ lower, upper : float
135
+ The two starting points, which bracket the root.
136
+ tolerance : float, optional
137
+ Tolerance on the step between successive estimates; default 1e-8.
138
+ max_iterations : int, optional
139
+ Iteration limit; default 1000.
140
+ report_failure : bool, optional
141
+ Raise when the solver fails to converge; default True.
142
+
143
+ Returns
144
+ -------
145
+ float
146
+ The root.
147
+
148
+ Raises
149
+ ------
150
+ ValueError
151
+ If the solver does not converge and ``report_failure`` is True (raised by Numerics).
152
+ """
153
+ return float(_root_finding().Secant.Solve(callbacks.scalar_function(f), float(lower), float(upper),
154
+ float(tolerance), int(max_iterations), bool(report_failure)))
155
+
156
+
157
+ def newton_raphson_solve(f: Any, df: Any, first_guess: float, tolerance: float = 1e-8, max_iterations: int = 1000,
158
+ report_failure: bool = True) -> float:
159
+ """Find a root of ``f`` by Newton-Raphson iteration (``NewtonRaphson.Solve``).
160
+
161
+ The iteration is unbounded; :func:`newton_raphson_robust_solve` keeps it inside a bracket.
162
+
163
+ Parameters
164
+ ----------
165
+ f : callable, str or System.Delegate
166
+ Function of one float.
167
+ df : callable, str or System.Delegate
168
+ The derivative of ``f``.
169
+ first_guess : float
170
+ Starting point.
171
+ tolerance : float, optional
172
+ Tolerance on the step between successive estimates; default 1e-8.
173
+ max_iterations : int, optional
174
+ Iteration limit; default 1000.
175
+ report_failure : bool, optional
176
+ Raise when the solver fails to converge; default True.
177
+
178
+ Returns
179
+ -------
180
+ float
181
+ The root.
182
+
183
+ Raises
184
+ ------
185
+ ValueError
186
+ If the solver does not converge and ``report_failure`` is True (raised by Numerics).
187
+ """
188
+ return float(_root_finding().NewtonRaphson.Solve(callbacks.scalar_function(f), callbacks.scalar_function(df),
189
+ float(first_guess), float(tolerance), int(max_iterations),
190
+ bool(report_failure)))
191
+
192
+
193
+ def newton_raphson_robust_solve(f: Any, df: Any, first_guess: float, lower: float, upper: float,
194
+ tolerance: float = 1e-8, max_iterations: int = 1000,
195
+ report_failure: bool = True) -> float:
196
+ """Find a root of ``f`` by Newton-Raphson with bisection fallback in a bracket (``NewtonRaphson.RobustSolve``).
197
+
198
+ Parameters
199
+ ----------
200
+ f : callable, str or System.Delegate
201
+ Function of one float.
202
+ df : callable, str or System.Delegate
203
+ The derivative of ``f``.
204
+ first_guess : float
205
+ Starting point inside the bracket.
206
+ lower, upper : float
207
+ Bracket with f(lower)·f(upper) ≤ 0.
208
+ tolerance : float, optional
209
+ Tolerance on the step between successive estimates; default 1e-8.
210
+ max_iterations : int, optional
211
+ Iteration limit; default 1000.
212
+ report_failure : bool, optional
213
+ Raise when the solver fails to converge; default True.
214
+
215
+ Returns
216
+ -------
217
+ float
218
+ The root.
219
+
220
+ Raises
221
+ ------
222
+ ValueError
223
+ If the bracket does not change sign, or if the solver does not converge and ``report_failure``
224
+ is True (raised by Numerics).
225
+ """
226
+ return float(_root_finding().NewtonRaphson.RobustSolve(
227
+ callbacks.scalar_function(f), callbacks.scalar_function(df), float(first_guess), float(lower), float(upper),
228
+ float(tolerance), int(max_iterations), bool(report_failure)))
229
+
230
+
231
+ def newton_raphson_solve_system(f: Any, jacobian: Any, first_guess: Any, tolerance: float = 1e-8,
232
+ max_iterations: int = 1000, report_failure: bool = True) -> np.ndarray:
233
+ """Solve the system f(x) = 0 by multivariate Newton-Raphson (``NewtonRaphson.Solve(Func<Vector, Vector>, ...)``).
234
+
235
+ Parameters
236
+ ----------
237
+ f : callable, str or System.Delegate
238
+ Function of a float64 array returning an array of the same length (``Func<Vector, Vector>``).
239
+ jacobian : callable, str or System.Delegate
240
+ Function of a float64 array returning the square Jacobian matrix (``Func<Vector, Matrix>``).
241
+ first_guess : array_like
242
+ Starting point.
243
+ tolerance : float, optional
244
+ Tolerance on the step norm; default 1e-8.
245
+ max_iterations : int, optional
246
+ Iteration limit; default 1000.
247
+ report_failure : bool, optional
248
+ Raise when the solver fails to converge; default True.
249
+
250
+ Returns
251
+ -------
252
+ numpy.ndarray
253
+ The solution.
254
+
255
+ Raises
256
+ ------
257
+ ValueError
258
+ If the Jacobian is singular, or if the solver does not converge and ``report_failure`` is True
259
+ (raised by Numerics).
260
+ """
261
+ linear_algebra = namespace("Numerics.Mathematics.LinearAlgebra")
262
+ solution = _root_finding().NewtonRaphson.Solve(
263
+ callbacks.to_delegate(f, "Func<Vector,Vector>"), callbacks.to_delegate(jacobian, "Func<Vector,Matrix>"),
264
+ linear_algebra.Vector(to_net_doubles(first_guess)), float(tolerance), int(max_iterations),
265
+ bool(report_failure))
266
+ return to_numpy_doubles(solution.Array)
267
+
268
+
269
+ __all__ = [
270
+ "BRENT_MINIMUM_RELATIVE_TOLERANCE", "bisection_solve", "brent_bracket", "brent_solve",
271
+ "newton_raphson_robust_solve", "newton_raphson_solve", "newton_raphson_solve_system", "secant_solve",
272
+ ]
273
+
274
+ translate_public(globals())
pynumerics/runtime.py ADDED
@@ -0,0 +1,384 @@
1
+ """Host the .NET runtime that runs RMC.Numerics inside Python.
2
+
3
+ This module is the single owner of the .NET runtime in a Python process that uses
4
+ RMC.Numerics. It loads CoreCLR through pythonnet, references ``Numerics.dll`` once,
5
+ and lets dependent packages add their own assemblies (for example ``Hydrologics.dll``
6
+ or ``RMC.BestFit.dll``) to the same runtime afterwards, so one process holds one
7
+ CoreCLR and one ``Numerics`` assembly.
8
+
9
+ ``pythonnet.load("coreclr", ...)`` must run before any ``import clr``: on Windows a
10
+ bare ``import clr`` starts the legacy .NET Framework, that choice cannot be undone for
11
+ the life of the process, and the net10.0 assemblies cannot load there. The loader
12
+ refuses to continue when it detects that state.
13
+
14
+ The assembly directory is resolved once per process, and the first match wins:
15
+
16
+ 1. the ``dll_dir`` argument of :func:`initialize`;
17
+ 2. the ``PYNUMERICS_DLL_DIR`` environment variable;
18
+ 3. the ``_bin`` directory packaged beside this module.
19
+
20
+ A ``dll_dir`` or ``PYNUMERICS_DLL_DIR`` that is not a directory is an error rather
21
+ than a reason to keep searching. The runtime configuration is always the packaged
22
+ ``_bin/pynumerics.runtimeconfig.json`` unless the chosen directory holds its own.
23
+
24
+ The packaged configuration turns off quick JIT for methods with loops
25
+ (``System.Runtime.TieredCompilation.QuickJitForLoops``) and profile-guided tiering
26
+ (``System.Runtime.TieredPGO``). Most Numerics work from Python is one call that loops over an
27
+ array, and a process that keeps compiling new methods delays the promotion of unoptimized
28
+ first-tier code for seconds, so the array members ran up to about three times slower than their
29
+ optimized code. Compiling those methods optimized at first use removes that, at a start-up
30
+ cost of about twenty milliseconds. The settings change only when machine code is optimized, never the
31
+ floating-point operations it performs, so every value is unchanged; the ``DOTNET_TC_QuickJitForLoops``
32
+ and ``DOTNET_TieredPGO`` environment variables override them.
33
+
34
+ The .NET installation is ``DOTNET_ROOT`` when that variable is set, otherwise the one
35
+ clr_loader discovers (the default installation directory on Windows and macOS, or the
36
+ parent of the ``dotnet`` executable on ``PATH``).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import os
42
+ import sys
43
+ import threading
44
+ from pathlib import Path
45
+ from typing import Any
46
+
47
+ ENV_DLL_DIR = "PYNUMERICS_DLL_DIR"
48
+ """Environment variable naming a directory that holds ``Numerics.dll``."""
49
+
50
+ ASSEMBLY_FILE = "Numerics.dll"
51
+ RUNTIME_CONFIG_FILE = "pynumerics.runtimeconfig.json"
52
+
53
+ _PACKAGED_BIN = Path(__file__).resolve().parent / "_bin"
54
+
55
+ _runtime_dir: Path | None = None
56
+ _extra_assemblies: dict[str, Path] = {}
57
+ _LOCK = threading.RLock() # pythonnet.load is not safe to race; concurrent first calls start it once
58
+
59
+
60
+ class RuntimeLoadError(RuntimeError):
61
+ """Error raised when the .NET runtime or an assembly cannot be loaded.
62
+
63
+ :func:`initialize` raises it when a runtime other than CoreCLR, such as the legacy
64
+ .NET Framework, already occupies the process; when ``dll_dir`` or
65
+ ``PYNUMERICS_DLL_DIR`` is not a directory; when no directory holding
66
+ ``Numerics.dll`` is found; when no runtime configuration is available; and when a
67
+ ``Numerics`` assembly from another file is already loaded. :func:`add_assembly`
68
+ raises it when the file does not exist, would load a second ``Numerics`` assembly,
69
+ or names an assembly already loaded from another file. A runtime that already occupies the process cannot be
70
+ replaced, so the first case requires a new Python process.
71
+ """
72
+
73
+
74
+ def initialize(dll_dir: str | os.PathLike[str] | None = None) -> Path:
75
+ """Start the .NET runtime and load ``Numerics.dll``.
76
+
77
+ Loads CoreCLR with the runtime configuration; when pythonnet already hosts CoreCLR
78
+ in the process, uses that runtime instead of loading another. The call appends the
79
+ assembly directory to ``sys.path`` and references ``Numerics.dll`` there by full path,
80
+ so a copy elsewhere on ``sys.path`` is never loaded instead, after which namespaces
81
+ such as ``Numerics.Distributions`` can be imported. After a successful call, later calls return immediately and
82
+ ignore their arguments. Every wrapper in this package calls ``initialize`` itself;
83
+ call it directly to choose the assembly directory or to surface loading problems
84
+ early.
85
+
86
+ Parameters
87
+ ----------
88
+ dll_dir : str or os.PathLike, optional
89
+ Directory holding ``Numerics.dll``. None, the default, tries the
90
+ ``PYNUMERICS_DLL_DIR`` environment variable and then the packaged ``_bin``
91
+ directory.
92
+
93
+ Returns
94
+ -------
95
+ pathlib.Path
96
+ The directory ``Numerics.dll`` was loaded from.
97
+
98
+ Raises
99
+ ------
100
+ pynumerics.RuntimeLoadError
101
+ If a runtime other than CoreCLR already occupies the process, for example after
102
+ a bare ``import clr`` on Windows; if ``dll_dir`` or ``PYNUMERICS_DLL_DIR`` is
103
+ not a directory; if no directory holding ``Numerics.dll`` is found; if no
104
+ runtime configuration is available; if no .NET installation is found; or if a
105
+ ``Numerics`` assembly from another file is already loaded in the process.
106
+
107
+ Notes
108
+ -----
109
+ Errors raised by pythonnet while starting the runtime, for example when no
110
+ compatible .NET runtime is installed, propagate unchanged. The call is
111
+ thread-safe: threads that call it together start the runtime once.
112
+
113
+ Examples
114
+ --------
115
+ >>> import pynumerics
116
+ >>> pynumerics.initialize() # doctest: +SKIP
117
+ >>> pynumerics.numerics_identity().version # doctest: +SKIP
118
+ '2.3.0.0'
119
+ """
120
+ global _runtime_dir
121
+ if _runtime_dir is not None:
122
+ return _runtime_dir
123
+ with _LOCK:
124
+ if _runtime_dir is not None:
125
+ return _runtime_dir
126
+
127
+ _guard_against_foreign_runtime()
128
+ directory = _resolve_dll_dir(dll_dir)
129
+ runtime_config = _resolve_runtime_config(directory)
130
+
131
+ import pythonnet
132
+
133
+ if pythonnet.get_runtime_info() is None:
134
+ pythonnet.load("coreclr", runtime_config=str(runtime_config), dotnet_root=str(_resolve_dotnet_root()))
135
+ _assert_coreclr(pythonnet.get_runtime_info())
136
+
137
+ import clr # noqa: PLC0415 - legal only after pythonnet.load above
138
+
139
+ if str(directory) not in sys.path:
140
+ sys.path.append(str(directory))
141
+ _reference(clr, directory / ASSEMBLY_FILE)
142
+ _runtime_dir = directory
143
+ return directory
144
+
145
+
146
+ def add_assembly(path: str | os.PathLike[str]) -> Any:
147
+ """Load another assembly into the runtime that hosts RMC.Numerics.
148
+
149
+ Starts the runtime first if it is not running. The assembly's directory is
150
+ appended to ``sys.path`` and the assembly is referenced by its full path, so its namespaces
151
+ can be imported afterwards. Adding the same file again is a no-op. Dependent
152
+ packages, such as the Hydrologics bindings, load their engine through this
153
+ function so the process keeps one ``Numerics`` assembly: references from the added
154
+ assembly to ``Numerics`` bind to the one already loaded.
155
+
156
+ Parameters
157
+ ----------
158
+ path : str or os.PathLike
159
+ Path to the assembly file, for example ``.../Hydrologics.dll``.
160
+
161
+ Returns
162
+ -------
163
+ System.Reflection.Assembly
164
+ The loaded assembly.
165
+
166
+ Raises
167
+ ------
168
+ pynumerics.RuntimeLoadError
169
+ If the file does not exist, if it is a ``Numerics`` assembly (only the one this
170
+ package loads is allowed), if an assembly of the same name from another file is
171
+ already loaded, or if the runtime cannot start.
172
+ """
173
+ file = Path(path).resolve()
174
+ if not file.is_file():
175
+ raise RuntimeLoadError(f"The assembly '{file}' does not exist.")
176
+ if file.stem.lower() == "numerics":
177
+ raise RuntimeLoadError(
178
+ f"Refusing to load '{file}': one process loads one Numerics assembly, and "
179
+ "pynumerics already owns it."
180
+ )
181
+ initialize()
182
+ with _LOCK:
183
+ import clr # noqa: PLC0415 - the runtime is started above
184
+
185
+ directory = str(file.parent)
186
+ if directory not in sys.path:
187
+ sys.path.append(directory)
188
+ assembly = _reference(clr, file)
189
+ _extra_assemblies[file.stem] = file
190
+ return assembly
191
+
192
+
193
+ def loaded_assemblies() -> dict[str, Path]:
194
+ """Return the assemblies added through :func:`add_assembly`, by name.
195
+
196
+ Returns
197
+ -------
198
+ dict of str to pathlib.Path
199
+ Assembly name to the file it was requested from.
200
+ """
201
+ with _LOCK:
202
+ return dict(_extra_assemblies)
203
+
204
+
205
+ def runtime_dir() -> Path:
206
+ """Return the directory ``Numerics.dll`` was loaded from.
207
+
208
+ Starts the .NET runtime first if it is not already running.
209
+
210
+ Returns
211
+ -------
212
+ pathlib.Path
213
+ The assembly directory resolved by :func:`initialize`.
214
+
215
+ Raises
216
+ ------
217
+ pynumerics.RuntimeLoadError
218
+ If the runtime is not running yet and cannot be started.
219
+ """
220
+ return initialize()
221
+
222
+
223
+ def is_initialized() -> bool:
224
+ """Return whether :func:`initialize` has completed in this process.
225
+
226
+ Returns
227
+ -------
228
+ bool
229
+ True after a successful :func:`initialize`.
230
+ """
231
+ return _runtime_dir is not None
232
+
233
+
234
+ def get_runtime_info() -> Any:
235
+ """Return pythonnet's description of the .NET runtime hosting RMC.Numerics.
236
+
237
+ Starts the .NET runtime first if it is not already running.
238
+
239
+ Returns
240
+ -------
241
+ clr_loader.types.RuntimeInfo
242
+ The runtime description, with ``kind``, ``version``, ``initialized``,
243
+ ``shutdown``, and ``properties`` fields; ``kind`` is ``"CoreCLR"``.
244
+
245
+ Raises
246
+ ------
247
+ pynumerics.RuntimeLoadError
248
+ If the runtime is not running yet and cannot be started.
249
+ """
250
+ initialize()
251
+ import pythonnet
252
+
253
+ return pythonnet.get_runtime_info()
254
+
255
+
256
+ def _reference(clr: Any, file: Path) -> Any:
257
+ """Reference an assembly by its full path and confirm the runtime bound that file.
258
+
259
+ A reference by simple name loads the first file of that name on ``sys.path``, so a copy of the
260
+ assembly in an earlier ``sys.path`` entry (the working directory, another package's binaries)
261
+ would be loaded in place of the resolved one. The full path binds the resolved file; references
262
+ by name made afterwards bind to the assembly already loaded.
263
+
264
+ Raises
265
+ ------
266
+ pynumerics.RuntimeLoadError
267
+ If an assembly with the same name from another file is already loaded in the process.
268
+ """
269
+ import System # noqa: PLC0415 - the runtime is running
270
+
271
+ try:
272
+ assembly = clr.AddReference(str(file))
273
+ except System.IO.FileLoadException as error:
274
+ raise RuntimeLoadError(
275
+ f"Cannot load '{file}': an assembly named '{file.stem}' from another file is already loaded in this "
276
+ "process. Restart the process and call pynumerics.initialize() before any other CLR use."
277
+ ) from error
278
+ location = Path(str(assembly.Location))
279
+ if os.path.normcase(str(location.resolve())) != os.path.normcase(str(file.resolve())):
280
+ raise RuntimeLoadError(
281
+ f"Cannot load '{file}': an assembly named '{file.stem}' is already loaded in this process from "
282
+ f"'{location}'. Restart the process and call pynumerics.initialize() before any other CLR use."
283
+ )
284
+ return assembly
285
+
286
+
287
+ def _guard_against_foreign_runtime() -> None:
288
+ """Fail loudly when ``clr`` was imported before this loader configured CoreCLR.
289
+
290
+ A pre-existing ``clr`` module means other code initialized a CLR first; on Windows a
291
+ bare ``import clr`` selects the legacy .NET Framework irreversibly, and the net10.0
292
+ assemblies cannot load there.
293
+ """
294
+ if "clr" not in sys.modules:
295
+ return
296
+
297
+ import pythonnet
298
+
299
+ info = pythonnet.get_runtime_info()
300
+ if info is None or not _is_coreclr(info):
301
+ raise RuntimeLoadError(
302
+ "The 'clr' module was imported before pynumerics.initialize() and the "
303
+ "process is not hosting CoreCLR. On Windows a bare 'import clr' initializes "
304
+ "the legacy .NET Framework irreversibly; restart the process and call "
305
+ "pynumerics.initialize() before any other CLR use."
306
+ )
307
+
308
+
309
+ def _assert_coreclr(info: Any) -> None:
310
+ """Raise when the loaded runtime is not CoreCLR (for example .NET Framework or Mono)."""
311
+ if info is None or not _is_coreclr(info):
312
+ kind = getattr(info, "kind", None)
313
+ raise RuntimeLoadError(
314
+ f"pythonnet is hosting '{kind}' but the net10.0 Numerics assembly requires "
315
+ "CoreCLR. Restart the process so pynumerics can load CoreCLR before any "
316
+ "other CLR use."
317
+ )
318
+
319
+
320
+ def _is_coreclr(info: Any) -> bool:
321
+ """Return True when a pythonnet RuntimeInfo describes CoreCLR."""
322
+ return str(getattr(info, "kind", "")).lower().replace(" ", "") == "coreclr"
323
+
324
+
325
+ def _resolve_dll_dir(dll_dir: str | os.PathLike[str] | None) -> Path:
326
+ """Resolve the assembly directory through the documented discovery order."""
327
+ candidates: list[tuple[str, Path, bool]] = []
328
+ if dll_dir is not None:
329
+ candidates.append(("explicit dll_dir", Path(dll_dir), True))
330
+ env_dir = os.environ.get(ENV_DLL_DIR)
331
+ if env_dir:
332
+ candidates.append((f"{ENV_DLL_DIR} environment variable", Path(env_dir), True))
333
+ candidates.append(("packaged _bin", _PACKAGED_BIN, False))
334
+
335
+ for label, candidate, must_exist in candidates:
336
+ if candidate.is_dir() and (candidate / ASSEMBLY_FILE).is_file():
337
+ return candidate.resolve()
338
+ if must_exist:
339
+ if not candidate.is_dir():
340
+ raise RuntimeLoadError(f"The {label} '{candidate}' is not a directory.")
341
+ raise RuntimeLoadError(f"The {label} '{candidate}' does not hold {ASSEMBLY_FILE}.")
342
+
343
+ raise RuntimeLoadError(
344
+ f"No directory holding {ASSEMBLY_FILE} found: pass dll_dir, set {ENV_DLL_DIR}, "
345
+ "install the pynumerics wheel, or stage the assembly with "
346
+ "python/pynumerics/scripts/build_wheel.py --stage-only."
347
+ )
348
+
349
+
350
+ def _resolve_dotnet_root() -> Path:
351
+ """Return the .NET installation root: ``DOTNET_ROOT`` when set, else clr_loader's discovery.
352
+
353
+ clr_loader probes the default installation directory on Windows and macOS and the
354
+ real parent of the ``dotnet`` executable on ``PATH`` elsewhere.
355
+ """
356
+ env_root = os.environ.get("DOTNET_ROOT")
357
+ if env_root:
358
+ root = Path(env_root)
359
+ if not root.is_dir():
360
+ raise RuntimeLoadError(f"DOTNET_ROOT '{root}' is not a directory.")
361
+ return root
362
+ from clr_loader.util.find import find_dotnet_root # noqa: PLC0415 - pythonnet dependency
363
+
364
+ try:
365
+ root = Path(find_dotnet_root())
366
+ except RuntimeError as error:
367
+ raise RuntimeLoadError(
368
+ "No .NET installation found. Install the .NET 10 runtime and put 'dotnet' on PATH, "
369
+ "or set DOTNET_ROOT to the installation directory."
370
+ ) from error
371
+ if not root.is_dir():
372
+ raise RuntimeLoadError(
373
+ f"The .NET installation directory '{root}' does not exist. Install the .NET 10 runtime "
374
+ "or set DOTNET_ROOT to the installation directory."
375
+ )
376
+ return root
377
+
378
+
379
+ def _resolve_runtime_config(directory: Path) -> Path:
380
+ """Return the runtime configuration: the directory's own, else the packaged one."""
381
+ for candidate in (directory / RUNTIME_CONFIG_FILE, _PACKAGED_BIN / RUNTIME_CONFIG_FILE):
382
+ if candidate.is_file():
383
+ return candidate
384
+ raise RuntimeLoadError(f"No {RUNTIME_CONFIG_FILE} beside {ASSEMBLY_FILE} or in the packaged _bin directory.")