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
@@ -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())