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.
- pynumerics/__init__.py +73 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.LICENSE.txt +27 -0
- pynumerics/_bin/Microsoft.CodeAnalysis.dll +0 -0
- pynumerics/_bin/Numerics.dll +0 -0
- pynumerics/_bin/pynumerics.runtimeconfig.json +15 -0
- pynumerics/_coverage.py +3825 -0
- pynumerics/_util.py +237 -0
- pynumerics/_version.py +40 -0
- pynumerics/bootstrap.py +593 -0
- pynumerics/callbacks.py +791 -0
- pynumerics/copulas.py +910 -0
- pynumerics/data.py +2233 -0
- pynumerics/differentiation.py +348 -0
- pynumerics/distributions.py +4043 -0
- pynumerics/download.py +623 -0
- pynumerics/enums.py +119 -0
- pynumerics/errors.py +245 -0
- pynumerics/functions.py +1142 -0
- pynumerics/goodness_of_fit.py +699 -0
- pynumerics/graphs.py +556 -0
- pynumerics/hypothesis_tests.py +445 -0
- pynumerics/identity.py +144 -0
- pynumerics/integration.py +677 -0
- pynumerics/linalg.py +1408 -0
- pynumerics/machine_learning.py +1335 -0
- pynumerics/marshal.py +733 -0
- pynumerics/mathematics.py +1053 -0
- pynumerics/mcmc.py +1406 -0
- pynumerics/multivariate.py +1539 -0
- pynumerics/ode.py +156 -0
- pynumerics/optimization.py +1736 -0
- pynumerics/probability.py +966 -0
- pynumerics/py.typed +0 -0
- pynumerics/regional.py +522 -0
- pynumerics/roots.py +274 -0
- pynumerics/runtime.py +384 -0
- pynumerics/sampling.py +1758 -0
- pynumerics/special.py +1079 -0
- pynumerics/statistics.py +1958 -0
- pynumerics/timeseries.py +1326 -0
- pynumerics-2.3.0.dist-info/METADATA +142 -0
- pynumerics-2.3.0.dist-info/RECORD +46 -0
- pynumerics-2.3.0.dist-info/WHEEL +5 -0
- pynumerics-2.3.0.dist-info/licenses/LICENSE +10 -0
- 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.")
|