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/callbacks.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
"""Python callables and C# source as the .NET delegates Numerics members take.
|
|
2
|
+
|
|
3
|
+
Numerics takes functions as delegates: ``Func<double, double>`` for integrands and root-finding
|
|
4
|
+
functions, ``Func<double[], double>`` for objectives, ``LogLikelihood`` for the MCMC samplers and
|
|
5
|
+
others. Every wrapper that takes one accepts any of:
|
|
6
|
+
|
|
7
|
+
- a Python callable, wrapped by :func:`to_delegate`: arrays reach it as NumPy float64 copies and
|
|
8
|
+
its return value is converted to the delegate's return type. Each evaluation crosses from
|
|
9
|
+
.NET into Python and holds the GIL. Measured on a desktop, the crossing of a raw pythonnet
|
|
10
|
+
delegate costs about 2 microseconds; the wrapper adds about 0.2 microseconds for scalar
|
|
11
|
+
arguments, about 0.4 microseconds for an array of up to :data:`BUFFERED_ARRAY_LENGTH` values,
|
|
12
|
+
and one copy at memory speed beyond that (about 30 microseconds for 100,000 values);
|
|
13
|
+
- a string of C# source, compiled by :func:`compile_csharp` into a delegate that runs entirely in
|
|
14
|
+
.NET, with no Python crossing per evaluation;
|
|
15
|
+
- a .NET delegate of the right type, passed through unchanged.
|
|
16
|
+
|
|
17
|
+
Parallel rule: a Python callback serializes on the GIL, so a Numerics object that would call it
|
|
18
|
+
from several threads runs serially instead. :func:`uses_python_callback` tells which case applies,
|
|
19
|
+
and :func:`apply_parallel_policy` turns the object's parallel switch off when any of its delegates
|
|
20
|
+
calls Python; compiled and .NET delegates leave it as it was.
|
|
21
|
+
|
|
22
|
+
:func:`compile_csharp` uses the Roslyn C# compiler (``Microsoft.CodeAnalysis.CSharp``) bundled in
|
|
23
|
+
the wheel's ``_bin`` directory, pinned by version and SHA-256 in ``pynumerics._version``. The
|
|
24
|
+
first compilation in a process takes one to two seconds while Roslyn starts; later ones take tens
|
|
25
|
+
of milliseconds, and a source compiled once is reused from a cache.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import threading
|
|
32
|
+
from collections.abc import Callable, Iterable
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from pynumerics import _version, runtime
|
|
39
|
+
from pynumerics._util import namespace
|
|
40
|
+
from pynumerics.errors import translate_public
|
|
41
|
+
from pynumerics.marshal import (
|
|
42
|
+
to_net_doubles,
|
|
43
|
+
to_net_matrix,
|
|
44
|
+
to_numpy_doubles,
|
|
45
|
+
to_numpy_ints,
|
|
46
|
+
to_numpy_matrix,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
ROSLYN_ASSEMBLIES: tuple[tuple[str, str], ...] = (
|
|
50
|
+
("Microsoft.CodeAnalysis.dll", _version.ROSLYN_CODEANALYSIS_SHA256),
|
|
51
|
+
("Microsoft.CodeAnalysis.CSharp.dll", _version.ROSLYN_CSHARP_SHA256),
|
|
52
|
+
)
|
|
53
|
+
"""The bundled Roslyn assemblies and their pinned SHA-256 digests, in load order."""
|
|
54
|
+
|
|
55
|
+
DEFAULT_USINGS: tuple[str, ...] = (
|
|
56
|
+
"System", "System.Collections.Generic", "System.Linq", "Numerics", "Numerics.Distributions",
|
|
57
|
+
"Numerics.Mathematics", "Numerics.Mathematics.LinearAlgebra", "Numerics.Mathematics.SpecialFunctions",
|
|
58
|
+
)
|
|
59
|
+
"""The namespaces C# callback source can use without qualification."""
|
|
60
|
+
|
|
61
|
+
PARALLEL_SWITCHES: tuple[str, ...] = ("ParallelizeChains", "UseParallel", "IsParallel", "Parallel")
|
|
62
|
+
"""The Numerics boolean properties that run work on several threads, in the order they are looked for."""
|
|
63
|
+
|
|
64
|
+
_LOCK = threading.RLock()
|
|
65
|
+
_DELEGATE_TYPES: dict[str, Any] = {}
|
|
66
|
+
_COMPILED: dict[tuple[str, str, tuple[str, ...]], Any] = {}
|
|
67
|
+
_REFERENCES: dict[str, Any] = {}
|
|
68
|
+
_ROSLYN: dict[str, Any] = {}
|
|
69
|
+
_SIGNATURES: dict[str, tuple[tuple[str, ...], str]] = {}
|
|
70
|
+
_STAGING: dict[str, tuple[Any, Any] | None] = {}
|
|
71
|
+
|
|
72
|
+
_RAW_DELEGATES = frozenset({"Numerics.Mathematics.Optimization.TransitionDistribution"})
|
|
73
|
+
"""Delegates whose array arguments are outputs the callback fills; they reach Python as the .NET arrays."""
|
|
74
|
+
|
|
75
|
+
_KEYWORDS = {
|
|
76
|
+
"System.Double": "double", "System.Single": "float", "System.Int32": "int", "System.Int64": "long",
|
|
77
|
+
"System.Boolean": "bool", "System.String": "string", "System.Object": "object", "System.Void": "void",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_IN_DOUBLE_LIST = "System.Collections.Generic.IList`1[[System.Double"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _delegate_table() -> dict[str, Any]:
|
|
84
|
+
"""Return the named delegate types, keyed by their C# spelling without spaces; built once."""
|
|
85
|
+
if not _DELEGATE_TYPES:
|
|
86
|
+
namespace("Numerics")
|
|
87
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
88
|
+
from System.Collections.Generic import IList # noqa: PLC0415
|
|
89
|
+
|
|
90
|
+
linear = namespace("Numerics.Mathematics.LinearAlgebra")
|
|
91
|
+
optimization = namespace("Numerics.Mathematics.Optimization")
|
|
92
|
+
mcmc = namespace("Numerics.Sampling.MCMC")
|
|
93
|
+
d, darr, i, b = System.Double, System.Array[System.Double], System.Int32, System.Boolean
|
|
94
|
+
dlist = IList[System.Double]
|
|
95
|
+
func, action = System.Func, System.Action
|
|
96
|
+
_DELEGATE_TYPES.update({
|
|
97
|
+
"Func<double,double>": func[d, d],
|
|
98
|
+
"Func<double,double,double>": func[d, d, d],
|
|
99
|
+
"Func<double,double,double,double>": func[d, d, d, d],
|
|
100
|
+
"Func<double[],double>": func[darr, d],
|
|
101
|
+
"Func<double[],double[]>": func[darr, darr],
|
|
102
|
+
"Func<double[],double,double>": func[darr, d, d],
|
|
103
|
+
"Func<double,double[],double>": func[d, darr, d],
|
|
104
|
+
"Func<double,double[],double[]>": func[d, darr, darr],
|
|
105
|
+
"Func<double[],double[],double>": func[darr, darr, d],
|
|
106
|
+
"Func<double[],bool>": func[darr, b],
|
|
107
|
+
"Func<IList<double>,double>": func[dlist, d],
|
|
108
|
+
"Func<IList<double>,double[]>": func[dlist, darr],
|
|
109
|
+
"Func<int,int,int,double>": func[i, i, i, d],
|
|
110
|
+
"Func<int,int,int,int>": func[i, i, i, i],
|
|
111
|
+
"Func<Vector,Vector>": func[linear.Vector, linear.Vector],
|
|
112
|
+
"Func<Vector,Matrix>": func[linear.Vector, linear.Matrix],
|
|
113
|
+
"Func<ParameterSet,double[]>": func[optimization.ParameterSet, darr],
|
|
114
|
+
"Action<double,double,double>": action[d, d, d],
|
|
115
|
+
"Action<double,double,double,double>": action[d, d, d, d],
|
|
116
|
+
"LogLikelihood": mcmc.LogLikelihood,
|
|
117
|
+
"HMC.Gradient": mcmc.HMC.Gradient,
|
|
118
|
+
"Gibbs.Proposal": mcmc.Gibbs.Proposal,
|
|
119
|
+
"MCMCSampler.ProgressChangedEventHandler": mcmc.MCMCSampler.ProgressChangedEventHandler,
|
|
120
|
+
"NUTS.LogLikelihoodAndGradient": mcmc.NUTS.LogLikelihoodAndGradient,
|
|
121
|
+
"TransitionDistribution": optimization.TransitionDistribution,
|
|
122
|
+
})
|
|
123
|
+
return _DELEGATE_TYPES
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def delegate_types() -> tuple[str, ...]:
|
|
127
|
+
"""Return the delegate type names :func:`delegate_type` resolves.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
tuple of str
|
|
132
|
+
C# spellings such as ``"Func<double,double>"`` and Numerics delegate names such as
|
|
133
|
+
``"LogLikelihood"`` and ``"HMC.Gradient"``.
|
|
134
|
+
"""
|
|
135
|
+
return tuple(_delegate_table())
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def delegate_type(spec: Any) -> Any:
|
|
139
|
+
"""Resolve a delegate type from its C# spelling, or return a .NET delegate type unchanged.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
spec : str or type
|
|
144
|
+
A name from :func:`delegate_types` (spaces are ignored, so ``"Func<double, double>"``
|
|
145
|
+
works), or a pythonnet delegate type such as ``System.Func[System.Double, System.Double]``.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
type
|
|
150
|
+
The pythonnet delegate type.
|
|
151
|
+
|
|
152
|
+
Raises
|
|
153
|
+
------
|
|
154
|
+
ValueError
|
|
155
|
+
If ``spec`` names no known delegate type.
|
|
156
|
+
TypeError
|
|
157
|
+
If ``spec`` is a type that is not a .NET delegate type.
|
|
158
|
+
"""
|
|
159
|
+
table = _delegate_table()
|
|
160
|
+
if isinstance(spec, str):
|
|
161
|
+
key = "".join(spec.split())
|
|
162
|
+
if key not in table:
|
|
163
|
+
raise ValueError(f"Unknown delegate type {spec!r}; known: {', '.join(table)}.")
|
|
164
|
+
return table[key]
|
|
165
|
+
import clr # noqa: PLC0415 - the runtime is running
|
|
166
|
+
import System # noqa: PLC0415
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
clr_type = clr.GetClrType(spec)
|
|
170
|
+
except Exception as error:
|
|
171
|
+
raise TypeError(f"{spec!r} is not a .NET delegate type.") from error
|
|
172
|
+
if not clr_type.IsSubclassOf(clr.GetClrType(System.MulticastDelegate)):
|
|
173
|
+
raise TypeError(f"{clr_type.FullName} is not a .NET delegate type.")
|
|
174
|
+
return spec
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
_resolve_delegate_type = delegate_type # compile_csharp's parameter of the same name shadows it
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _signature(dtype: Any) -> tuple[tuple[str, ...], str]:
|
|
181
|
+
"""Return the full names of a delegate's ``Invoke`` parameter types and return type, cached."""
|
|
182
|
+
import clr # noqa: PLC0415 - the runtime is running
|
|
183
|
+
|
|
184
|
+
clr_type = clr.GetClrType(dtype)
|
|
185
|
+
key = str(clr_type.AssemblyQualifiedName)
|
|
186
|
+
signature = _SIGNATURES.get(key)
|
|
187
|
+
if signature is None:
|
|
188
|
+
invoke = clr_type.GetMethod("Invoke")
|
|
189
|
+
signature = _SIGNATURES[key] = (tuple(str(p.ParameterType.FullName) for p in invoke.GetParameters()),
|
|
190
|
+
str(invoke.ReturnType.FullName))
|
|
191
|
+
return signature
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _argument(type_name: str) -> Callable[[Any], Any] | None:
|
|
195
|
+
"""Return the converter of a .NET argument for a Python callback, or None to pass it as it is."""
|
|
196
|
+
if type_name == "System.Double[]" or type_name.startswith(_IN_DOUBLE_LIST):
|
|
197
|
+
return to_numpy_doubles
|
|
198
|
+
if type_name == "System.Int32[]":
|
|
199
|
+
return to_numpy_ints
|
|
200
|
+
if type_name == "System.Double[,]":
|
|
201
|
+
return to_numpy_matrix
|
|
202
|
+
if type_name == "Numerics.Mathematics.LinearAlgebra.Vector":
|
|
203
|
+
return lambda vector: to_numpy_doubles(vector.Array)
|
|
204
|
+
if type_name == "Numerics.Mathematics.LinearAlgebra.Matrix":
|
|
205
|
+
return lambda matrix: to_numpy_matrix(matrix.ToArray())
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _result(type_name: str) -> Callable[[Any], Any] | None:
|
|
210
|
+
"""Return the converter of a Python callback's result to the .NET return type, or None to pass it."""
|
|
211
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
212
|
+
|
|
213
|
+
def net(convert: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
|
214
|
+
return lambda value: value if isinstance(value, System.Object) else convert(value)
|
|
215
|
+
|
|
216
|
+
if type_name == "System.Double":
|
|
217
|
+
return float
|
|
218
|
+
if type_name in ("System.Int32", "System.Int64"):
|
|
219
|
+
return int
|
|
220
|
+
if type_name == "System.Boolean":
|
|
221
|
+
return bool
|
|
222
|
+
if type_name == "System.Void":
|
|
223
|
+
return lambda value: None
|
|
224
|
+
if type_name == "System.Double[]":
|
|
225
|
+
return net(to_net_doubles)
|
|
226
|
+
if type_name == "System.Double[,]":
|
|
227
|
+
return net(to_net_matrix)
|
|
228
|
+
linear = namespace("Numerics.Mathematics.LinearAlgebra")
|
|
229
|
+
if type_name == "Numerics.Mathematics.LinearAlgebra.Vector":
|
|
230
|
+
return net(lambda value: linear.Vector(to_net_doubles(value)))
|
|
231
|
+
if type_name == "Numerics.Mathematics.LinearAlgebra.Matrix":
|
|
232
|
+
return net(lambda value: linear.Matrix(to_net_matrix(value)))
|
|
233
|
+
if type_name.startswith("System.ValueTuple`2[[System.Double") and "LinearAlgebra.Vector" in type_name:
|
|
234
|
+
pair = System.ValueTuple[System.Double, linear.Vector]
|
|
235
|
+
vector = net(lambda value: linear.Vector(to_net_doubles(value)))
|
|
236
|
+
return net(lambda value: pair(float(value[0]), vector(value[1])))
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def to_delegate(fn: Any, delegate_type_spec: Any) -> Any:
|
|
241
|
+
"""Return ``fn`` as a .NET delegate of the given type.
|
|
242
|
+
|
|
243
|
+
A Python callable is wrapped: array, ``IList<double>``, ``Vector`` and ``Matrix`` arguments
|
|
244
|
+
reach it as new NumPy arrays (``TransitionDistribution`` receives its output arrays as the .NET
|
|
245
|
+
arrays it fills), scalars as Python numbers and anything else as the .NET object; the result is
|
|
246
|
+
converted to the return type (a float for ``double``, a ``System.Double[]`` for ``double[]``, a
|
|
247
|
+
``Vector`` for ``Vector``, and so on). A delegate whose arguments and result are all ``double``
|
|
248
|
+
calls ``float(fn(...))`` with no other conversion. A delegate whose one argument is a
|
|
249
|
+
``double[]``, ``IList<double>`` or ``Vector`` of at most :data:`BUFFERED_ARRAY_LENGTH` values
|
|
250
|
+
stages it in a per-thread buffer, so an evaluation costs about 0.4 microseconds more than a
|
|
251
|
+
raw pythonnet delegate; longer arrays cross in one copy. The values are bit for bit the ones
|
|
252
|
+
.NET passed either way. A string is compiled by :func:`compile_csharp`. A .NET delegate of the
|
|
253
|
+
type is returned unchanged.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
fn : callable, str or System.Delegate
|
|
258
|
+
The function.
|
|
259
|
+
delegate_type_spec : str or type
|
|
260
|
+
The delegate type, as for :func:`delegate_type`.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
System.Delegate
|
|
265
|
+
A delegate of the requested type.
|
|
266
|
+
|
|
267
|
+
Raises
|
|
268
|
+
------
|
|
269
|
+
TypeError
|
|
270
|
+
If ``fn`` is neither callable, a string nor a delegate of the type.
|
|
271
|
+
ValueError
|
|
272
|
+
If the delegate type is unknown, or C# source does not compile.
|
|
273
|
+
"""
|
|
274
|
+
dtype = delegate_type(delegate_type_spec)
|
|
275
|
+
if isinstance(fn, str):
|
|
276
|
+
return compile_csharp(fn, dtype)
|
|
277
|
+
import clr # noqa: PLC0415 - the runtime is running
|
|
278
|
+
import System # noqa: PLC0415
|
|
279
|
+
|
|
280
|
+
if isinstance(fn, System.Delegate):
|
|
281
|
+
if not clr.GetClrType(dtype).IsInstanceOfType(fn):
|
|
282
|
+
raise TypeError(f"{fn.GetType().FullName} is not a {clr.GetClrType(dtype).FullName}.")
|
|
283
|
+
return fn
|
|
284
|
+
if not callable(fn):
|
|
285
|
+
raise TypeError(f"Expected a callable, C# source or a .NET delegate; got {type(fn).__name__}.")
|
|
286
|
+
arguments, result = _signature(dtype)
|
|
287
|
+
if result == "System.Double" and all(a == "System.Double" for a in arguments):
|
|
288
|
+
return dtype(_double_wrapper(fn, len(arguments)))
|
|
289
|
+
raw = str(clr.GetClrType(dtype).FullName) in _RAW_DELEGATES
|
|
290
|
+
converters = [None if raw else _argument(a) for a in arguments]
|
|
291
|
+
back = _result(result)
|
|
292
|
+
wrapper = dtype(_converting_wrapper(fn, converters, back))
|
|
293
|
+
if not raw and len(arguments) == 1 and result != "System.Void":
|
|
294
|
+
return _buffered_delegate(fn, dtype, arguments[0], back, wrapper)
|
|
295
|
+
return wrapper
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
BUFFERED_ARRAY_LENGTH = 1024
|
|
299
|
+
"""Longest array a one-argument Python callback receives through its per-thread staging buffer.
|
|
300
|
+
|
|
301
|
+
Below this length the .NET side copies the argument into a staging buffer that NumPy already
|
|
302
|
+
views, and Python copies it out with one slice, which avoids acquiring a buffer view of a new .NET
|
|
303
|
+
array on every evaluation. Longer arrays use the direct one-copy path, which is faster there.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _buffered_delegate(fn: Callable[..., Any], dtype: Any, argument: str, back: Callable[[Any], Any] | None,
|
|
308
|
+
direct: Any) -> Any:
|
|
309
|
+
"""Return a delegate that hands ``fn`` a ``double[]``, ``IList<double>`` or ``Vector`` argument cheaply.
|
|
310
|
+
|
|
311
|
+
The delegate, built by the factory of :func:`_staging_factory`, copies an argument of at most
|
|
312
|
+
:data:`BUFFERED_ARRAY_LENGTH` values into a staging ``double[]`` owned by the calling thread,
|
|
313
|
+
then calls Python with only the length; Python returns ``fn`` of a copy of the first ``length``
|
|
314
|
+
staged values, bit for bit the values .NET passed. A null or longer argument goes to ``direct``,
|
|
315
|
+
the plain converting delegate. Each thread has its own staging buffer, registered with Python
|
|
316
|
+
the first time that thread calls, so concurrent evaluations never share one; a nested call on
|
|
317
|
+
the same thread is safe because Python copies the staged values before calling ``fn``. Other
|
|
318
|
+
argument types return ``direct`` unchanged.
|
|
319
|
+
"""
|
|
320
|
+
factory = _staging_factory(dtype, argument)
|
|
321
|
+
if factory is None:
|
|
322
|
+
return direct
|
|
323
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
324
|
+
from System.Threading import ThreadLocal # noqa: PLC0415
|
|
325
|
+
|
|
326
|
+
views: dict[int, np.ndarray] = {}
|
|
327
|
+
ident = threading.get_ident
|
|
328
|
+
|
|
329
|
+
def register(buffer: Any) -> None:
|
|
330
|
+
views[ident()] = np.frombuffer(buffer, dtype=np.float64)
|
|
331
|
+
|
|
332
|
+
if back is None:
|
|
333
|
+
def staged(length: int) -> Any:
|
|
334
|
+
return fn(views[ident()][:length].copy())
|
|
335
|
+
else:
|
|
336
|
+
def staged(length: int) -> Any:
|
|
337
|
+
return back(fn(views[ident()][:length].copy()))
|
|
338
|
+
|
|
339
|
+
doubles = System.Array[System.Double]
|
|
340
|
+
python, _ = factory
|
|
341
|
+
return factory[1](direct, python(staged), System.Action[doubles](register), ThreadLocal[doubles]())
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _staging_factory(dtype: Any, argument: str) -> tuple[Any, Any] | None:
|
|
345
|
+
"""Return ``(python delegate type, factory)`` for the staging delegates of ``dtype``, compiled once.
|
|
346
|
+
|
|
347
|
+
The factory is a compiled ``System.Linq.Expressions`` lambda taking the direct delegate, the
|
|
348
|
+
Python ``Func<int, R>`` that reads the staged values, the ``Action<double[]>`` that registers a
|
|
349
|
+
new staging buffer, and the ``ThreadLocal<double[]>`` holding each thread's buffer; it returns
|
|
350
|
+
the staging delegate of ``dtype`` closed over them. Compiling once per delegate type keeps each
|
|
351
|
+
:func:`to_delegate` call in the microseconds. Returns None for an argument type it cannot stage.
|
|
352
|
+
"""
|
|
353
|
+
import clr # noqa: PLC0415 - the runtime is running
|
|
354
|
+
import System # noqa: PLC0415
|
|
355
|
+
|
|
356
|
+
clr_type = clr.GetClrType(dtype)
|
|
357
|
+
key = str(clr_type.AssemblyQualifiedName)
|
|
358
|
+
with _LOCK:
|
|
359
|
+
if key in _STAGING:
|
|
360
|
+
return _STAGING[key]
|
|
361
|
+
if argument == "System.Double[]":
|
|
362
|
+
source = "array"
|
|
363
|
+
elif argument.startswith(_IN_DOUBLE_LIST):
|
|
364
|
+
source = "list"
|
|
365
|
+
elif argument == "Numerics.Mathematics.LinearAlgebra.Vector":
|
|
366
|
+
source = "vector"
|
|
367
|
+
else:
|
|
368
|
+
_STAGING[key] = None
|
|
369
|
+
return None
|
|
370
|
+
from System.Collections.Generic import ICollection, List # noqa: PLC0415
|
|
371
|
+
from System.Linq.Expressions import Expression, ParameterExpression # noqa: PLC0415
|
|
372
|
+
from System.Threading import ThreadLocal # noqa: PLC0415
|
|
373
|
+
|
|
374
|
+
doubles = System.Array[System.Double]
|
|
375
|
+
double_array, integer = clr.GetClrType(doubles), clr.GetClrType(System.Int32)
|
|
376
|
+
invoke = clr_type.GetMethod("Invoke")
|
|
377
|
+
python_type = System.Func[System.Int32, invoke.ReturnType]
|
|
378
|
+
register_type, local_type = System.Action[doubles], ThreadLocal[doubles]
|
|
379
|
+
direct = Expression.Parameter(clr_type, "direct")
|
|
380
|
+
python = Expression.Parameter(clr.GetClrType(python_type), "python")
|
|
381
|
+
register = Expression.Parameter(clr.GetClrType(register_type), "register")
|
|
382
|
+
local = Expression.Parameter(clr.GetClrType(local_type), "local")
|
|
383
|
+
capacity = Expression.Constant(System.Int32(BUFFERED_ARRAY_LENGTH))
|
|
384
|
+
x = Expression.Parameter(invoke.GetParameters()[0].ParameterType, "x")
|
|
385
|
+
buffer = Expression.Variable(double_array, "buffer")
|
|
386
|
+
length = Expression.Variable(integer, "length")
|
|
387
|
+
array = Expression.Variable(double_array, "array")
|
|
388
|
+
no_array = Expression.Constant(None, double_array)
|
|
389
|
+
if source == "list":
|
|
390
|
+
collection = Expression.Convert(x, clr.GetClrType(ICollection[System.Double]))
|
|
391
|
+
measure = Expression.Assign(length, Expression.Property(collection, "Count"))
|
|
392
|
+
copy = Expression.Call(collection, "CopyTo", None, buffer, Expression.Constant(System.Int32(0)))
|
|
393
|
+
else:
|
|
394
|
+
values = x if source == "array" else array
|
|
395
|
+
measure = Expression.Assign(length, Expression.ArrayLength(values))
|
|
396
|
+
copy = Expression.Call(clr.GetClrType(System.Array), "Copy", None, values, buffer, length)
|
|
397
|
+
stored = Expression.Property(local, "Value")
|
|
398
|
+
fresh = Expression.Block(
|
|
399
|
+
Expression.Assign(buffer, Expression.NewArrayBounds(clr.GetClrType(System.Double), capacity)),
|
|
400
|
+
Expression.Assign(stored, buffer),
|
|
401
|
+
Expression.Invoke(register, buffer))
|
|
402
|
+
fast = Expression.Block(
|
|
403
|
+
Expression.Assign(buffer, stored),
|
|
404
|
+
Expression.IfThen(Expression.Equal(buffer, no_array), fresh),
|
|
405
|
+
copy,
|
|
406
|
+
Expression.Invoke(python, length))
|
|
407
|
+
slow = Expression.Invoke(direct, x)
|
|
408
|
+
steps = []
|
|
409
|
+
missing = Expression.Equal(x, Expression.Constant(None, x.Type))
|
|
410
|
+
if source == "vector":
|
|
411
|
+
vector_array = Expression.Condition(missing, no_array, Expression.Property(x, "Array"))
|
|
412
|
+
steps.append(Expression.Assign(array, vector_array))
|
|
413
|
+
missing = Expression.Equal(array, no_array)
|
|
414
|
+
steps.append(Expression.Condition(
|
|
415
|
+
missing, slow,
|
|
416
|
+
Expression.Block(measure, Expression.Condition(Expression.GreaterThan(length, capacity), slow, fast))))
|
|
417
|
+
variables = List[ParameterExpression]()
|
|
418
|
+
for variable in (buffer, length, array):
|
|
419
|
+
variables.Add(variable)
|
|
420
|
+
body = Expression.Block(invoke.ReturnType, variables, System.Array[Expression](steps))
|
|
421
|
+
inner = Expression.Lambda(clr_type, body, System.Array[ParameterExpression]([x]))
|
|
422
|
+
factory_type = System.Func[dtype, python_type, register_type, local_type, dtype]
|
|
423
|
+
outer = Expression.Lambda(clr.GetClrType(factory_type), inner,
|
|
424
|
+
System.Array[ParameterExpression]([direct, python, register, local]))
|
|
425
|
+
_STAGING[key] = (python_type, outer.Compile())
|
|
426
|
+
return _STAGING[key]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _converting_wrapper(fn: Callable[..., Any], converters: list[Callable[[Any], Any] | None],
|
|
430
|
+
back: Callable[[Any], Any] | None) -> Callable[..., Any]:
|
|
431
|
+
"""Return a wrapper that converts the arguments, calls ``fn`` and converts its result.
|
|
432
|
+
|
|
433
|
+
A one-argument delegate (objectives, log-likelihoods, gradients) gets a closure with its
|
|
434
|
+
converters bound directly, so an evaluation makes no per-argument loop or tuple.
|
|
435
|
+
"""
|
|
436
|
+
if len(converters) == 1:
|
|
437
|
+
(convert,) = converters
|
|
438
|
+
if convert is None:
|
|
439
|
+
return (lambda x: fn(x)) if back is None else (lambda x: back(fn(x)))
|
|
440
|
+
return (lambda x: fn(convert(x))) if back is None else (lambda x: back(fn(convert(x))))
|
|
441
|
+
|
|
442
|
+
def wrapper(*values: Any) -> Any:
|
|
443
|
+
values = tuple(v if c is None else c(v) for c, v in zip(converters, values, strict=True))
|
|
444
|
+
value = fn(*values)
|
|
445
|
+
return value if back is None else back(value)
|
|
446
|
+
|
|
447
|
+
return wrapper
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _double_wrapper(fn: Callable[..., Any], arity: int) -> Callable[..., float]:
|
|
451
|
+
"""Return a wrapper of an all-``double`` callback that only converts the result with ``float``."""
|
|
452
|
+
if arity == 1:
|
|
453
|
+
return lambda x: float(fn(float(x)))
|
|
454
|
+
if arity == 2:
|
|
455
|
+
return lambda x, y: float(fn(x, y))
|
|
456
|
+
return lambda *values: float(fn(*values))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def scalar_function(fn: Any) -> Any:
|
|
460
|
+
"""Return ``fn`` as a ``Func<double, double>`` (integrands, root-finding and 1-D optimization).
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
fn : callable, str or System.Delegate
|
|
465
|
+
A function of one float, C# source or a delegate, as for :func:`to_delegate`.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
System.Func[Double, Double]
|
|
470
|
+
The delegate.
|
|
471
|
+
"""
|
|
472
|
+
return to_delegate(fn, "Func<double,double>")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def vector_function(fn: Any) -> Any:
|
|
476
|
+
"""Return ``fn`` as a ``Func<double[], double>`` (objective functions and multidimensional integrands).
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
fn : callable, str or System.Delegate
|
|
481
|
+
A function of a float64 array returning a float, C# source or a delegate.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
System.Func[Double[], Double]
|
|
486
|
+
The delegate.
|
|
487
|
+
"""
|
|
488
|
+
return to_delegate(fn, "Func<double[],double>")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def bivariate_function(fn: Any) -> Any:
|
|
492
|
+
"""Return ``fn`` as a ``Func<double, double, double>`` (2-D integrands and ODE right-hand sides).
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
fn : callable, str or System.Delegate
|
|
497
|
+
A function of two floats, C# source or a delegate.
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
System.Func[Double, Double, Double]
|
|
502
|
+
The delegate.
|
|
503
|
+
"""
|
|
504
|
+
return to_delegate(fn, "Func<double,double,double>")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def log_likelihood(fn: Any) -> Any:
|
|
508
|
+
"""Return ``fn`` as a ``Numerics.Sampling.MCMC.LogLikelihood`` (the MCMC target density).
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
fn : callable, str or System.Delegate
|
|
513
|
+
A function of the parameter array returning the log-likelihood, C# source or a delegate.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
Numerics.Sampling.MCMC.LogLikelihood
|
|
518
|
+
The delegate.
|
|
519
|
+
"""
|
|
520
|
+
return to_delegate(fn, "LogLikelihood")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _is_python_delegate(value: Any) -> bool:
|
|
524
|
+
"""Return whether a .NET delegate calls into Python (pythonnet builds those in a generated assembly).
|
|
525
|
+
|
|
526
|
+
A compiled expression lambda, such as the staging delegate of :func:`to_delegate`, calls Python
|
|
527
|
+
when any delegate among its closure constants or captured variables does.
|
|
528
|
+
"""
|
|
529
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
530
|
+
|
|
531
|
+
for item in value.GetInvocationList():
|
|
532
|
+
target = item.Target
|
|
533
|
+
if target is None:
|
|
534
|
+
continue
|
|
535
|
+
target_type = target.GetType()
|
|
536
|
+
if str(target_type.Assembly.GetName().Name).startswith("__Python_Runtime"):
|
|
537
|
+
return True
|
|
538
|
+
if str(target_type.FullName) == "System.Runtime.CompilerServices.Closure":
|
|
539
|
+
captured = [*(target.Constants or ()), *(getattr(box, "Value", None) for box in (target.Locals or ()))]
|
|
540
|
+
if any(isinstance(c, System.Delegate) and _is_python_delegate(c) for c in captured):
|
|
541
|
+
return True
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def uses_python_callback(*functions: Any) -> bool:
|
|
546
|
+
"""Return whether any of the functions calls into Python when Numerics invokes it.
|
|
547
|
+
|
|
548
|
+
Parameters
|
|
549
|
+
----------
|
|
550
|
+
*functions : callable, str, System.Delegate or None
|
|
551
|
+
The functions a wrapper passes to Numerics. A Python callable, or a .NET delegate built
|
|
552
|
+
from one, calls Python; C# source and compiled or native .NET delegates do not. None is
|
|
553
|
+
ignored.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
bool
|
|
558
|
+
True when at least one function calls Python, so the Numerics object must not run it
|
|
559
|
+
from several threads.
|
|
560
|
+
"""
|
|
561
|
+
for fn in functions:
|
|
562
|
+
if fn is None or isinstance(fn, str):
|
|
563
|
+
continue
|
|
564
|
+
if runtime.is_initialized():
|
|
565
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
566
|
+
|
|
567
|
+
if isinstance(fn, System.Delegate):
|
|
568
|
+
if _is_python_delegate(fn):
|
|
569
|
+
return True
|
|
570
|
+
continue
|
|
571
|
+
if callable(fn):
|
|
572
|
+
return True
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def apply_parallel_policy(net: Any, *functions: Any) -> bool:
|
|
577
|
+
"""Turn off a Numerics object's parallel switch when any of its functions calls Python.
|
|
578
|
+
|
|
579
|
+
The first property of :data:`PARALLEL_SWITCHES` the object has is set to False when
|
|
580
|
+
:func:`uses_python_callback` is True, and left as it is otherwise.
|
|
581
|
+
|
|
582
|
+
Parameters
|
|
583
|
+
----------
|
|
584
|
+
net : object
|
|
585
|
+
The .NET object (for example an MCMC sampler or ``DifferentialEvolution``).
|
|
586
|
+
*functions : callable, str, System.Delegate or None
|
|
587
|
+
The functions given to the object.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
bool
|
|
592
|
+
True when the switch was turned off.
|
|
593
|
+
"""
|
|
594
|
+
if not uses_python_callback(*functions):
|
|
595
|
+
return False
|
|
596
|
+
for name in PARALLEL_SWITCHES:
|
|
597
|
+
if hasattr(net, name):
|
|
598
|
+
setattr(net, name, False)
|
|
599
|
+
return True
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _roslyn_directory() -> Path:
|
|
604
|
+
"""Return the directory holding the Roslyn assemblies: the assembly directory, else the packaged ``_bin``."""
|
|
605
|
+
first = ROSLYN_ASSEMBLIES[0][0]
|
|
606
|
+
for directory in (runtime.runtime_dir(), Path(runtime.__file__).resolve().parent / "_bin"):
|
|
607
|
+
if (directory / first).is_file():
|
|
608
|
+
return directory
|
|
609
|
+
raise runtime.RuntimeLoadError(
|
|
610
|
+
f"{first} is not beside Numerics.dll or in the packaged _bin directory; install the pynumerics wheel or "
|
|
611
|
+
"stage it with python/pynumerics/scripts/build_wheel.py --roslyn-only.")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def roslyn_available() -> bool:
|
|
615
|
+
"""Return whether the bundled Roslyn assemblies are present, so :func:`compile_csharp` can run.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
bool
|
|
620
|
+
True when both assemblies are found (their digests are checked when they load).
|
|
621
|
+
"""
|
|
622
|
+
try:
|
|
623
|
+
directory = _roslyn_directory()
|
|
624
|
+
except runtime.RuntimeLoadError:
|
|
625
|
+
return False
|
|
626
|
+
return all((directory / name).is_file() for name, _ in ROSLYN_ASSEMBLIES)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _load_roslyn() -> dict[str, Any]:
|
|
630
|
+
"""Verify and load the Roslyn assemblies once, and return the Roslyn types the compiler uses."""
|
|
631
|
+
if _ROSLYN:
|
|
632
|
+
return _ROSLYN
|
|
633
|
+
directory = _roslyn_directory()
|
|
634
|
+
for name, pinned in ROSLYN_ASSEMBLIES:
|
|
635
|
+
file = directory / name
|
|
636
|
+
digest = hashlib.sha256(file.read_bytes()).hexdigest()
|
|
637
|
+
if digest != pinned:
|
|
638
|
+
raise runtime.RuntimeLoadError(f"{file} has SHA-256 {digest}; pynumerics pins {pinned}.")
|
|
639
|
+
runtime.add_assembly(file)
|
|
640
|
+
code_analysis = namespace("Microsoft.CodeAnalysis")
|
|
641
|
+
csharp = namespace("Microsoft.CodeAnalysis.CSharp")
|
|
642
|
+
_ROSLYN.update({"ca": code_analysis, "cs": csharp})
|
|
643
|
+
return _ROSLYN
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _references() -> Any:
|
|
647
|
+
"""Return the compilation references: the .NET shared framework, Numerics and every added assembly."""
|
|
648
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
649
|
+
from System.Collections.Generic import List # noqa: PLC0415
|
|
650
|
+
|
|
651
|
+
roslyn = _load_roslyn()
|
|
652
|
+
added = tuple(sorted(str(p) for p in runtime.loaded_assemblies().values()))
|
|
653
|
+
key = "|".join(added)
|
|
654
|
+
references = _REFERENCES.get(key)
|
|
655
|
+
if references is None:
|
|
656
|
+
metadata = roslyn["ca"].MetadataReference
|
|
657
|
+
references = List[metadata]()
|
|
658
|
+
platform = str(System.AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).split(System.IO.Path.PathSeparator)
|
|
659
|
+
for path in [*platform, str(runtime.runtime_dir() / runtime.ASSEMBLY_FILE), *added]:
|
|
660
|
+
if path:
|
|
661
|
+
references.Add(metadata.CreateFromFile(path))
|
|
662
|
+
_REFERENCES.clear()
|
|
663
|
+
_REFERENCES[key] = references
|
|
664
|
+
return references
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _csharp_name(clr_type: Any) -> str:
|
|
668
|
+
"""Return the fully qualified C# spelling of a .NET type (``global::System.Func<double, double>``)."""
|
|
669
|
+
if clr_type.IsArray:
|
|
670
|
+
return f"{_csharp_name(clr_type.GetElementType())}[{',' * (clr_type.GetArrayRank() - 1)}]"
|
|
671
|
+
full = str(clr_type.FullName) if clr_type.FullName is not None else str(clr_type.Name)
|
|
672
|
+
if full in _KEYWORDS:
|
|
673
|
+
return _KEYWORDS[full]
|
|
674
|
+
name = str(clr_type.Name).split("`")[0]
|
|
675
|
+
if clr_type.IsGenericType:
|
|
676
|
+
arguments = [a for a in clr_type.GetGenericArguments()]
|
|
677
|
+
if clr_type.IsNested:
|
|
678
|
+
arguments = arguments[len(clr_type.DeclaringType.GetGenericArguments()):]
|
|
679
|
+
name += "<" + ", ".join(_csharp_name(a) for a in arguments) + ">"
|
|
680
|
+
if clr_type.IsNested:
|
|
681
|
+
return f"{_csharp_name(clr_type.DeclaringType)}.{name}"
|
|
682
|
+
return f"global::{clr_type.Namespace}.{name}" if clr_type.Namespace else f"global::{name}"
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
_CLASS = "PynumericsCallback"
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _emit(code: str, assembly_name: str) -> Any:
|
|
689
|
+
"""Compile C# code into an in-memory assembly and load it; raise ValueError with the errors."""
|
|
690
|
+
import System # noqa: PLC0415 - the runtime is running
|
|
691
|
+
|
|
692
|
+
roslyn = _load_roslyn()
|
|
693
|
+
ca, cs = roslyn["ca"], roslyn["cs"]
|
|
694
|
+
tree = cs.CSharpSyntaxTree.ParseText(code)
|
|
695
|
+
options = cs.CSharpCompilationOptions(ca.OutputKind.DynamicallyLinkedLibrary).WithOptimizationLevel(
|
|
696
|
+
ca.OptimizationLevel.Release)
|
|
697
|
+
compilation = cs.CSharpCompilation.Create(assembly_name, System.Array[ca.SyntaxTree]([tree]), _references(),
|
|
698
|
+
options)
|
|
699
|
+
stream = System.IO.MemoryStream()
|
|
700
|
+
emitted = compilation.Emit(stream)
|
|
701
|
+
if not emitted.Success:
|
|
702
|
+
messages = [str(d) for d in emitted.Diagnostics if d.Severity == ca.DiagnosticSeverity.Error]
|
|
703
|
+
raise ValueError("C# callback source does not compile:\n" + "\n".join(messages))
|
|
704
|
+
return System.Reflection.Assembly.Load(stream.ToArray())
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def compile_csharp(source: str, delegate_type: Any = "Func<double,double>", *,
|
|
708
|
+
usings: Iterable[str] = ()) -> Any:
|
|
709
|
+
"""Compile C# source into a .NET delegate that runs without calling back into Python.
|
|
710
|
+
|
|
711
|
+
``source`` is either an expression convertible to the delegate type, usually a lambda
|
|
712
|
+
(``"x => Math.Exp(-x * x)"``), or one or more static method declarations of which exactly one
|
|
713
|
+
matches the delegate's signature (``"static double F(double x) { return x * x; }"``). The code
|
|
714
|
+
is compiled with Roslyn against the .NET shared framework, ``Numerics.dll`` and every assembly
|
|
715
|
+
added with :func:`pynumerics.add_assembly`, so it can use Numerics types; the namespaces in
|
|
716
|
+
:data:`DEFAULT_USINGS` are imported. Compiled delegates are cached by source, delegate type and
|
|
717
|
+
usings, so compiling the same source again returns the same delegate.
|
|
718
|
+
|
|
719
|
+
Parameters
|
|
720
|
+
----------
|
|
721
|
+
source : str
|
|
722
|
+
The C# lambda, expression or static methods.
|
|
723
|
+
delegate_type : str or type, optional
|
|
724
|
+
The delegate type, as for :func:`delegate_type`; default ``Func<double,double>``.
|
|
725
|
+
usings : iterable of str, optional
|
|
726
|
+
More namespaces to import, for example ``("Numerics.Data.Statistics",)``.
|
|
727
|
+
|
|
728
|
+
Returns
|
|
729
|
+
-------
|
|
730
|
+
System.Delegate
|
|
731
|
+
The compiled delegate.
|
|
732
|
+
|
|
733
|
+
Raises
|
|
734
|
+
------
|
|
735
|
+
ValueError
|
|
736
|
+
If the source does not compile (the message lists Roslyn's errors with line and column
|
|
737
|
+
numbers counted in ``source``), if no or several static methods match the delegate, or if
|
|
738
|
+
the delegate type is unknown.
|
|
739
|
+
pynumerics.RuntimeLoadError
|
|
740
|
+
If the Roslyn assemblies are missing or do not match their pinned SHA-256.
|
|
741
|
+
|
|
742
|
+
Examples
|
|
743
|
+
--------
|
|
744
|
+
>>> from pynumerics.callbacks import compile_csharp
|
|
745
|
+
>>> f = compile_csharp("x => Math.Exp(-x * x)") # doctest: +SKIP
|
|
746
|
+
>>> f(0.0) # doctest: +SKIP
|
|
747
|
+
1.0
|
|
748
|
+
"""
|
|
749
|
+
dtype = _resolve_delegate_type(delegate_type)
|
|
750
|
+
import clr # noqa: PLC0415 - the runtime is running
|
|
751
|
+
import System # noqa: PLC0415
|
|
752
|
+
|
|
753
|
+
clr_type = clr.GetClrType(dtype)
|
|
754
|
+
extra = tuple(usings)
|
|
755
|
+
key = (source, str(clr_type.AssemblyQualifiedName), extra)
|
|
756
|
+
with _LOCK:
|
|
757
|
+
compiled = _COMPILED.get(key)
|
|
758
|
+
if compiled is not None:
|
|
759
|
+
return compiled
|
|
760
|
+
roslyn = _load_roslyn()
|
|
761
|
+
header = "".join(f"using {name};\n" for name in (*DEFAULT_USINGS, *extra))
|
|
762
|
+
assembly_name = f"{_CLASS}{len(_COMPILED) + 1}"
|
|
763
|
+
type_name = _csharp_name(clr_type)
|
|
764
|
+
expression = roslyn["cs"].SyntaxFactory.ParseExpression(source)
|
|
765
|
+
if not expression.ContainsDiagnostics:
|
|
766
|
+
code = (f"{header}public static class {_CLASS}\n{{\n public static {type_name} Create()\n {{\n"
|
|
767
|
+
f" return\n#line 1\n{source}\n#line default\n ;\n }}\n}}\n")
|
|
768
|
+
assembly = _emit(code, assembly_name)
|
|
769
|
+
compiled = assembly.GetType(_CLASS).GetMethod("Create").Invoke(None, None)
|
|
770
|
+
else:
|
|
771
|
+
code = f"{header}public static class {_CLASS}\n{{\n#line 1\n{source}\n#line default\n}}\n"
|
|
772
|
+
assembly = _emit(code, assembly_name)
|
|
773
|
+
flags = (System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
|
774
|
+
| System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.DeclaredOnly)
|
|
775
|
+
matches = [m for m in assembly.GetType(_CLASS).GetMethods(flags)
|
|
776
|
+
if System.Delegate.CreateDelegate(clr_type, m, False) is not None]
|
|
777
|
+
if len(matches) != 1:
|
|
778
|
+
raise ValueError(f"Expected exactly one static method matching {type_name}; found {len(matches)}.")
|
|
779
|
+
compiled = System.Delegate.CreateDelegate(clr_type, matches[0])
|
|
780
|
+
_COMPILED[key] = compiled
|
|
781
|
+
return compiled
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
__all__ = [
|
|
785
|
+
"BUFFERED_ARRAY_LENGTH", "DEFAULT_USINGS", "PARALLEL_SWITCHES", "ROSLYN_ASSEMBLIES", "apply_parallel_policy",
|
|
786
|
+
"bivariate_function",
|
|
787
|
+
"compile_csharp", "delegate_type", "delegate_types", "log_likelihood", "roslyn_available", "scalar_function",
|
|
788
|
+
"to_delegate", "uses_python_callback", "vector_function",
|
|
789
|
+
]
|
|
790
|
+
|
|
791
|
+
translate_public(globals())
|