solprop 0.1.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 (233) hide show
  1. solprop/__init__.py +121 -0
  2. solprop/_display.py +361 -0
  3. solprop/_helpers.py +223 -0
  4. solprop/_host.py +264 -0
  5. solprop/_interrupts.py +210 -0
  6. solprop/_numba_cache.py +557 -0
  7. solprop/_process.py +574 -0
  8. solprop/adiabatic.py +820 -0
  9. solprop/backends/__init__.py +35 -0
  10. solprop/backends/adams_core.py +557 -0
  11. solprop/backends/batch.py +596 -0
  12. solprop/backends/bodies.py +2123 -0
  13. solprop/backends/driver.py +2555 -0
  14. solprop/backends/exponential.py +714 -0
  15. solprop/backends/low_storage.py +727 -0
  16. solprop/backends/memory.py +2255 -0
  17. solprop/backends/mlx_gpu/__init__.py +21 -0
  18. solprop/backends/mlx_gpu/arithmetic.py +710 -0
  19. solprop/backends/mlx_gpu/backend.py +1598 -0
  20. solprop/backends/mlx_gpu/batch.py +78 -0
  21. solprop/backends/mlx_gpu/envelope.py +374 -0
  22. solprop/backends/mlx_gpu/passes.py +720 -0
  23. solprop/backends/multistep.py +4082 -0
  24. solprop/backends/multistep_general.py +455 -0
  25. solprop/backends/numba_cpu/__init__.py +20 -0
  26. solprop/backends/numba_cpu/backend.py +5238 -0
  27. solprop/backends/numba_cpu/batch.py +185 -0
  28. solprop/backends/numba_cpu/coupled_flow.py +1086 -0
  29. solprop/backends/numba_cpu/dispersive.py +1644 -0
  30. solprop/backends/numba_cpu/dispersive_general.py +451 -0
  31. solprop/backends/numba_cpu/dispersive_spatial.py +539 -0
  32. solprop/backends/numba_cpu/envelope_plasma.py +471 -0
  33. solprop/backends/numba_cpu/fft.py +1465 -0
  34. solprop/backends/numba_cpu/field_plasma.py +375 -0
  35. solprop/backends/numba_cpu/general_modal.py +981 -0
  36. solprop/backends/numba_cpu/kernels.py +2036 -0
  37. solprop/backends/numba_cpu/low_storage.py +1305 -0
  38. solprop/backends/numba_cpu/multistep.py +1937 -0
  39. solprop/backends/numba_cpu/openmp.py +349 -0
  40. solprop/backends/numba_cpu/pass_timers.py +162 -0
  41. solprop/backends/numba_cpu/planar.py +420 -0
  42. solprop/backends/numba_cpu/spatial.py +1202 -0
  43. solprop/backends/numba_cpu/tables.py +108 -0
  44. solprop/backends/numpy_ref/__init__.py +65 -0
  45. solprop/backends/numpy_ref/admission.py +717 -0
  46. solprop/backends/numpy_ref/backend.py +3825 -0
  47. solprop/backends/numpy_ref/batch.py +213 -0
  48. solprop/backends/numpy_ref/envelope_plasma.py +180 -0
  49. solprop/backends/numpy_ref/general_modal.py +1151 -0
  50. solprop/backends/numpy_ref/passes.py +2230 -0
  51. solprop/backends/numpy_ref/single.py +1288 -0
  52. solprop/backends/numpy_ref/tdse.py +1026 -0
  53. solprop/backends/numpy_ref/transforms.py +208 -0
  54. solprop/backends/populations.py +295 -0
  55. solprop/backends/protocol.py +787 -0
  56. solprop/backends/registry.py +653 -0
  57. solprop/backends/spatial.py +1152 -0
  58. solprop/boundaries.py +1204 -0
  59. solprop/constants.py +124 -0
  60. solprop/coupling/__init__.py +135 -0
  61. solprop/coupling/coupler.py +2906 -0
  62. solprop/coupling/elements.py +3244 -0
  63. solprop/coupling/envelopes.py +273 -0
  64. solprop/coupling/models.py +941 -0
  65. solprop/coupling/outcouple.py +226 -0
  66. solprop/coupling/plane.py +1847 -0
  67. solprop/coupling/port.py +412 -0
  68. solprop/coupling/remap.py +555 -0
  69. solprop/coupling/transport.py +517 -0
  70. solprop/driver/__init__.py +60 -0
  71. solprop/driver/_scan_batch.py +449 -0
  72. solprop/driver/_scan_context.py +24 -0
  73. solprop/driver/_scan_model.py +327 -0
  74. solprop/driver/_scan_run.py +899 -0
  75. solprop/driver/batch.py +423 -0
  76. solprop/driver/executors/__init__.py +38 -0
  77. solprop/driver/executors/_queue_state.py +208 -0
  78. solprop/driver/executors/_request.py +36 -0
  79. solprop/driver/executors/batch.py +73 -0
  80. solprop/driver/executors/local.py +33 -0
  81. solprop/driver/executors/pool.py +604 -0
  82. solprop/driver/executors/queue.py +706 -0
  83. solprop/driver/executors/slurm.py +365 -0
  84. solprop/driver/interface.py +1797 -0
  85. solprop/driver/material_output.py +986 -0
  86. solprop/driver/memory.py +822 -0
  87. solprop/driver/mixtures.py +382 -0
  88. solprop/driver/run.py +1489 -0
  89. solprop/driver/scaffold.py +259 -0
  90. solprop/driver/scan.py +91 -0
  91. solprop/driver/simulation.py +11064 -0
  92. solprop/driver/summary.py +847 -0
  93. solprop/driver/tdse.py +3172 -0
  94. solprop/driver/tdse_consistency.py +1219 -0
  95. solprop/exceptions.py +349 -0
  96. solprop/fourier.py +1025 -0
  97. solprop/gasflow.py +2087 -0
  98. solprop/grid.py +2341 -0
  99. solprop/hankel.py +198 -0
  100. solprop/linear.py +612 -0
  101. solprop/log.py +1541 -0
  102. solprop/modes/__init__.py +195 -0
  103. solprop/modes/_stepindex_vector.py +394 -0
  104. solprop/modes/antiresonant.py +1144 -0
  105. solprop/modes/base.py +475 -0
  106. solprop/modes/capillary.py +3806 -0
  107. solprop/modes/collection.py +1048 -0
  108. solprop/modes/connection.py +989 -0
  109. solprop/modes/fill.py +733 -0
  110. solprop/modes/graded_index.py +1272 -0
  111. solprop/modes/guide.py +2795 -0
  112. solprop/modes/material.py +806 -0
  113. solprop/modes/protocol.py +568 -0
  114. solprop/modes/quadrature.py +1732 -0
  115. solprop/modes/rectangular.py +887 -0
  116. solprop/modes/slab.py +1306 -0
  117. solprop/modes/stepindex.py +1630 -0
  118. solprop/modes/tabulated.py +1032 -0
  119. solprop/modes/vector.py +431 -0
  120. solprop/monitor.py +1018 -0
  121. solprop/noise.py +515 -0
  122. solprop/nonlinear/__init__.py +114 -0
  123. solprop/nonlinear/envelope_plasma.py +814 -0
  124. solprop/nonlinear/gnlse.py +2699 -0
  125. solprop/nonlinear/ionisation.py +795 -0
  126. solprop/nonlinear/mixtures.py +730 -0
  127. solprop/nonlinear/normalisation.py +462 -0
  128. solprop/nonlinear/optmat_raman.py +1686 -0
  129. solprop/nonlinear/optmat_rates.py +1312 -0
  130. solprop/nonlinear/raman.py +2705 -0
  131. solprop/nonlinear/responses.py +1611 -0
  132. solprop/nonlinear/tdse.py +890 -0
  133. solprop/optics.py +2078 -0
  134. solprop/output/__init__.py +110 -0
  135. solprop/output/_file_lock.py +97 -0
  136. solprop/output/_scan_lock.py +122 -0
  137. solprop/output/_writer_lock.py +79 -0
  138. solprop/output/capability.py +335 -0
  139. solprop/output/hdf5.py +2134 -0
  140. solprop/output/memory.py +668 -0
  141. solprop/output/scan.py +867 -0
  142. solprop/output/scan_artifacts.py +136 -0
  143. solprop/output/schema.py +4465 -0
  144. solprop/output/snapshots.py +968 -0
  145. solprop/output/tdse_archive.py +2307 -0
  146. solprop/physdata/__init__.py +190 -0
  147. solprop/physdata/_optmat.py +833 -0
  148. solprop/physdata/data/mirrors/HD120.csv +2408 -0
  149. solprop/physdata/data/mirrors/HD59_GDD.dat +2485 -0
  150. solprop/physdata/data/mirrors/NOTICE +26 -0
  151. solprop/physdata/data/mirrors/PC147.txt +2409 -0
  152. solprop/physdata/data/mirrors/PC1611.txt +2409 -0
  153. solprop/physdata/data/mirrors/PC1821.txt +2408 -0
  154. solprop/physdata/data/mirrors/PC70_GDD.csv +2408 -0
  155. solprop/physdata/data/mirrors/PC70_R.csv +2795 -0
  156. solprop/physdata/data/mirrors/UCxx-15FS_GD.csv +233 -0
  157. solprop/physdata/data/mirrors/UCxx-15FS_R.csv +1380 -0
  158. solprop/physdata/equation_of_state.py +255 -0
  159. solprop/physdata/gases.py +118 -0
  160. solprop/physdata/glasses.py +1771 -0
  161. solprop/physdata/metals.py +70 -0
  162. solprop/physdata/mixtures.py +1535 -0
  163. solprop/physdata/nonlinear_data.py +1209 -0
  164. solprop/physdata/raman_types.py +664 -0
  165. solprop/physdata/selection.py +1106 -0
  166. solprop/physdata/sellmeier.py +1279 -0
  167. solprop/physdata/species.py +1082 -0
  168. solprop/physdata/transport.py +395 -0
  169. solprop/plotting/__init__.py +75 -0
  170. solprop/plotting/propagation.py +724 -0
  171. solprop/plotting/spectrograms.py +229 -0
  172. solprop/plotting/statistics.py +247 -0
  173. solprop/plotting/style.py +625 -0
  174. solprop/polarisation.py +351 -0
  175. solprop/processing/__init__.py +172 -0
  176. solprop/processing/_examples.py +467 -0
  177. solprop/processing/axes.py +658 -0
  178. solprop/processing/coherence.py +275 -0
  179. solprop/processing/compression.py +1737 -0
  180. solprop/processing/fields.py +769 -0
  181. solprop/processing/mixtures.py +423 -0
  182. solprop/processing/modal.py +1248 -0
  183. solprop/processing/planes.py +570 -0
  184. solprop/processing/scans.py +962 -0
  185. solprop/processing/spectra.py +373 -0
  186. solprop/processing/time.py +934 -0
  187. solprop/profiles.py +3988 -0
  188. solprop/pulses.py +2445 -0
  189. solprop/py.typed +0 -0
  190. solprop/scan.py +189 -0
  191. solprop/sol.py +527 -0
  192. solprop/spatial.py +3096 -0
  193. solprop/stats.py +2966 -0
  194. solprop/stepper/__init__.py +74 -0
  195. solprop/stepper/controller.py +780 -0
  196. solprop/stepper/low_storage.py +155 -0
  197. solprop/stepper/multistep.py +963 -0
  198. solprop/stepper/runge_kutta.py +70 -0
  199. solprop/stepper/schedule.py +1070 -0
  200. solprop/stepper/tableau.py +797 -0
  201. solprop/tools.py +2860 -0
  202. solprop/translate/__init__.py +152 -0
  203. solprop/translate/chebyshev.py +2108 -0
  204. solprop/translate/coupled.py +3249 -0
  205. solprop/translate/coupling.py +384 -0
  206. solprop/translate/envelope_plasma.py +713 -0
  207. solprop/translate/expansion.py +1213 -0
  208. solprop/translate/flux.py +408 -0
  209. solprop/translate/free.py +269 -0
  210. solprop/translate/general.py +2302 -0
  211. solprop/translate/ionisation.py +729 -0
  212. solprop/translate/linop.py +3124 -0
  213. solprop/translate/masks.py +484 -0
  214. solprop/translate/modal.py +881 -0
  215. solprop/translate/modal_path.py +1149 -0
  216. solprop/translate/modal_tables.py +2365 -0
  217. solprop/translate/planar.py +442 -0
  218. solprop/translate/plasma_integration.py +369 -0
  219. solprop/translate/plasma_source.py +128 -0
  220. solprop/translate/precision.py +733 -0
  221. solprop/translate/problem.py +6728 -0
  222. solprop/translate/radial.py +717 -0
  223. solprop/translate/raman.py +749 -0
  224. solprop/translate/responses.py +3803 -0
  225. solprop/translate/spatial.py +1052 -0
  226. solprop/translate/stage.py +955 -0
  227. solprop/translate/tdse.py +1570 -0
  228. solprop/translate/tdse_source.py +313 -0
  229. solprop-0.1.0.dist-info/METADATA +201 -0
  230. solprop-0.1.0.dist-info/RECORD +233 -0
  231. solprop-0.1.0.dist-info/WHEEL +4 -0
  232. solprop-0.1.0.dist-info/licenses/LICENSE +674 -0
  233. solprop-0.1.0.dist-info/licenses/src/solprop/physdata/data/mirrors/NOTICE +26 -0
solprop/__init__.py ADDED
@@ -0,0 +1,121 @@
1
+ # SPDX-FileCopyrightText: 2026 John C. Travers and contributors
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ """Ultrafast nonlinear pulse propagation.
6
+
7
+ solprop models ultrafast pulse propagation with the unidirectional pulse propagation
8
+ equation (UPPE) and the generalised nonlinear Schrödinger equation (GNLSE), for the full
9
+ electric field or a complex envelope, in one mode, in many modes of a waveguide and
10
+ in radial or Cartesian free space. The design is described under ``docs/dev/`` and the
11
+ manual under ``docs/``.
12
+
13
+ The package installs a :class:`logging.NullHandler`; call
14
+ :func:`solprop.log.configure_logging` from a script to see progress and diagnostics.
15
+
16
+ Every submodule is an attribute of the package -- ``solprop.grid``, ``solprop.driver``,
17
+ ``solprop.plotting`` -- loaded on first access (:pep:`562`), so ``dir(solprop)`` and tab
18
+ completion list them while ``import solprop`` stays within its import-time budget: the
19
+ modules that pull in h5py, matplotlib or a compiled backend are not imported until they
20
+ are used.
21
+ """
22
+
23
+ import importlib
24
+ import logging
25
+ from importlib.metadata import PackageNotFoundError, version
26
+ from types import ModuleType
27
+ from typing import Final
28
+
29
+ # The scheduler's CPU grant, read before any OpenMP runtime binds the main thread.
30
+ import solprop._process # noqa: F401 # pyright: ignore[reportUnusedImport]
31
+ from solprop.exceptions import SolpropError
32
+ from solprop.log import configure_logging
33
+
34
+ __all__ = ["SolpropError", "__version__", "configure_logging"]
35
+
36
+ _SUBMODULES: Final = (
37
+ "adiabatic",
38
+ "backends",
39
+ "boundaries",
40
+ "constants",
41
+ "coupling",
42
+ "driver",
43
+ "exceptions",
44
+ "fourier",
45
+ "gasflow",
46
+ "grid",
47
+ "hankel",
48
+ "linear",
49
+ "log",
50
+ "modes",
51
+ "monitor",
52
+ "noise",
53
+ "nonlinear",
54
+ "optics",
55
+ "output",
56
+ "physdata",
57
+ "plotting",
58
+ "polarisation",
59
+ "processing",
60
+ "profiles",
61
+ "pulses",
62
+ "scan",
63
+ "sol",
64
+ "spatial",
65
+ "stats",
66
+ "stepper",
67
+ "tools",
68
+ "translate",
69
+ )
70
+ """The submodules reachable as attributes; a test keeps it equal to what is on disk."""
71
+
72
+
73
+ def __getattr__(name: str) -> ModuleType:
74
+ """Import a submodule the first time it is asked for as an attribute.
75
+
76
+ Parameters
77
+ ----------
78
+ name : str
79
+ The attribute looked up on the package.
80
+
81
+ Returns
82
+ -------
83
+ types.ModuleType
84
+ The submodule, imported now and cached by the import system.
85
+
86
+ Raises
87
+ ------
88
+ AttributeError
89
+ If `name` is not a submodule of the package.
90
+ """
91
+ if name in _SUBMODULES:
92
+ return importlib.import_module(f"{__name__}.{name}")
93
+ msg = f"module {__name__!r} has no attribute {name!r}"
94
+ raise AttributeError(msg)
95
+
96
+
97
+ def __dir__() -> list[str]:
98
+ """List the package's public surface, the submodules included whether or not loaded.
99
+
100
+ The names in ``__all__``, the submodules and the dunders; not the imports this
101
+ module happens to make, which are implementation and not part of the package.
102
+
103
+ Returns
104
+ -------
105
+ list of str
106
+ The sorted names ``dir`` and tab completion show.
107
+ """
108
+ names = {*globals(), *_SUBMODULES}
109
+ return sorted(
110
+ name
111
+ for name in names
112
+ if name.startswith("__") or name in __all__ or name in _SUBMODULES
113
+ )
114
+
115
+
116
+ try:
117
+ __version__ = version("solprop")
118
+ except PackageNotFoundError: # pragma: no cover - only when run from a source checkout
119
+ __version__ = "0.0.0"
120
+
121
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
solprop/_display.py ADDED
@@ -0,0 +1,361 @@
1
+ # SPDX-FileCopyrightText: 2026 John C. Travers and contributors
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ """Format objects for a person reading them at a prompt.
6
+
7
+ The package's printing rule, in one place: ``repr(x)`` is one line and never holds an
8
+ array, so that it is safe in a log record or an error message; ``str(x)`` is a short
9
+ aligned block in the units people quote (nm, fs, uJ, um, bar); and IPython and Jupyter
10
+ show that block when the object is the last expression of a cell, through
11
+ :meth:`Displayed._repr_pretty_`.
12
+
13
+ Printing never evaluates physics, never warns and never touches a file it does not
14
+ already own: a block is built from numbers the object already holds.
15
+
16
+ This module imports nothing from the rest of the package, and NumPy only where an array
17
+ is actually summarised, so that any module may use it.
18
+ """
19
+
20
+ import math
21
+ import textwrap
22
+ from collections.abc import Iterable, Sequence
23
+ from typing import Protocol, runtime_checkable
24
+
25
+ __all__ = [
26
+ "Displayed",
27
+ "block",
28
+ "engineering",
29
+ "line",
30
+ "phase_text",
31
+ "summarise",
32
+ "tail",
33
+ ]
34
+
35
+ _PREFIXES = {
36
+ -18: "a",
37
+ -15: "f",
38
+ -12: "p",
39
+ -9: "n",
40
+ -6: "u",
41
+ -3: "m",
42
+ 0: "",
43
+ 3: "k",
44
+ 6: "M",
45
+ 9: "G",
46
+ 12: "T",
47
+ 15: "P",
48
+ 18: "E",
49
+ }
50
+ """SI prefixes by decimal exponent. ``u`` stands for micro: the package's text is ASCII
51
+ wherever a terminal, a log file or an HDF5 attribute may have to carry it."""
52
+
53
+ _WIDTH = 88
54
+ """Longest line of a block, the package's line length."""
55
+
56
+ _WRAPPED = 4
57
+ """Most lines a wrapped value takes in a block; a longer one ends with an ellipsis."""
58
+
59
+
60
+ @runtime_checkable
61
+ class _Printer(Protocol):
62
+ """The part of IPython's pretty printer that :class:`Displayed` uses."""
63
+
64
+ def text(self, value: str) -> None:
65
+ """Write text.
66
+
67
+ Parameters
68
+ ----------
69
+ value : str
70
+ The text.
71
+ """
72
+ ...
73
+
74
+
75
+ def engineering(value: float | None, unit: str = "", digits: int = 4) -> str:
76
+ """Return a number with an SI prefix, as a person would write it.
77
+
78
+ Parameters
79
+ ----------
80
+ value : float or None
81
+ The number, in the SI unit named by `unit`.
82
+ unit : str, optional
83
+ The unit's symbol, without a prefix: ``"m"``, ``"s"``, ``"J"``, ``"W"``.
84
+ With an empty unit no prefix is used and the number is written plainly.
85
+ digits : int, optional
86
+ Significant figures.
87
+
88
+ Returns
89
+ -------
90
+ str
91
+ For example ``"800 nm"``, ``"30 fs"``, ``"1.5 m"``; ``"none"`` for ``None``
92
+ and ``"nan"``, ``"inf"`` or ``"-inf"`` for a value that is not finite.
93
+
94
+ Examples
95
+ --------
96
+ >>> from solprop._display import engineering
97
+ >>> engineering(800e-9, "m"), engineering(1.5, "m"), engineering(275e-6, "J")
98
+ ('800 nm', '1.5 m', '275 uJ')
99
+ >>> engineering(float("nan"), "s"), engineering(None, "s"), engineering(0.0, "W")
100
+ ('nan', 'none', '0 W')
101
+ """
102
+ if value is None:
103
+ return "none"
104
+ number = float(value)
105
+ if not math.isfinite(number):
106
+ return str(number)
107
+ if not unit:
108
+ return f"{number:.{digits}g}"
109
+ if number == 0.0:
110
+ return f"0 {unit}"
111
+ exponent = 3 * math.floor(math.log10(abs(number)) / 3.0)
112
+ exponent = min(max(exponent, min(_PREFIXES)), max(_PREFIXES))
113
+ scaled = number / 10.0**exponent
114
+ # Rounding to the requested figures can carry 999.96 over to 1000.
115
+ if abs(float(f"{scaled:.{digits}g}")) >= 1000.0 and exponent < max(_PREFIXES):
116
+ exponent += 3
117
+ scaled = number / 10.0**exponent
118
+ return f"{scaled:.{digits}g} {_PREFIXES[exponent]}{unit}"
119
+
120
+
121
+ def summarise(values: object, unit: str = "") -> str:
122
+ """Return a one-phrase summary of an array, never its contents.
123
+
124
+ Parameters
125
+ ----------
126
+ values : array_like
127
+ The array.
128
+ unit : str, optional
129
+ SI unit of the values, for the range; empty to give the shape alone.
130
+
131
+ Returns
132
+ -------
133
+ str
134
+ For example ``"<201 values, 0 m to 1.5 m>"`` or ``"<2x1x8192 complex>"``.
135
+
136
+ Examples
137
+ --------
138
+ >>> import numpy as np
139
+ >>> from solprop._display import summarise
140
+ >>> summarise(np.linspace(0.0, 1.5, 201), "m")
141
+ '<201 values, 0 m to 1.5 m>'
142
+ >>> summarise(np.zeros((2, 1, 8), dtype=complex))
143
+ '<2x1x8 complex>'
144
+ """
145
+ import numpy as np # noqa: PLC0415 - kept out of the import of modules that only format scalars
146
+
147
+ array = np.asarray(values)
148
+ if array.ndim != 1 or not unit or array.size == 0 or np.iscomplexobj(array):
149
+ shape = "x".join(str(size) for size in array.shape) or "scalar"
150
+ kind = "complex" if np.iscomplexobj(array) else "values"
151
+ return f"<{shape} {kind}>"
152
+ low, high = float(np.min(array)), float(np.max(array))
153
+ return (
154
+ f"<{array.size} values, {engineering(low, unit)} to {engineering(high, unit)}>"
155
+ )
156
+
157
+
158
+ _PHASE_NAMES = ("CEP", "delay", "GDD", "TOD", "FOD")
159
+ """What the first Taylor coefficients of a spectral phase are called."""
160
+
161
+
162
+ def phase_text(coefficients: Iterable[float]) -> str:
163
+ """Return Taylor spectral-phase coefficients as people quote them.
164
+
165
+ Parameters
166
+ ----------
167
+ coefficients : iterable of float
168
+ ``c_0, c_1, ...`` in ``s**n``; a tuple or an array.
169
+
170
+ Returns
171
+ -------
172
+ str
173
+ For example ``"CEP 0.3 rad, GDD 250 fs^2"``: zero coefficients are left out,
174
+ order ``n`` is in ``fs^n``, and ``"none"`` stands for no phase at all.
175
+
176
+ Examples
177
+ --------
178
+ >>> from solprop._display import phase_text
179
+ >>> phase_text((0.3, 0.0, 250e-30, -4000e-45))
180
+ 'CEP 0.3 rad, GDD 250 fs^2, TOD -4000 fs^3'
181
+ >>> phase_text(())
182
+ 'none'
183
+ """
184
+ parts = []
185
+ for order, value in enumerate(coefficients):
186
+ if float(value) == 0.0:
187
+ continue
188
+ name = _PHASE_NAMES[order] if order < len(_PHASE_NAMES) else f"order {order}"
189
+ unit = "rad" if order == 0 else ("fs" if order == 1 else f"fs^{order}")
190
+ parts.append(f"{name} {float(value) * 1e15**order:.4g} {unit}")
191
+ return ", ".join(parts) or "none"
192
+
193
+
194
+ def tail(text: str, width: int = 68) -> str:
195
+ """Return the end of a long text, which for a path is the part that matters.
196
+
197
+ Parameters
198
+ ----------
199
+ text : str
200
+ The text, typically a file path.
201
+ width : int, optional
202
+ Longest result, in characters.
203
+
204
+ Returns
205
+ -------
206
+ str
207
+ The text itself, or an ellipsis and its last ``width - 3`` characters.
208
+
209
+ Examples
210
+ --------
211
+ >>> from solprop._display import tail
212
+ >>> tail("/a/very/long/path/to/run.h5", 12)
213
+ '...to/run.h5'
214
+ """
215
+ return text if len(text) <= width else "..." + text[-(width - 3) :]
216
+
217
+
218
+ def line(name: str, fields: Iterable[tuple[str, object]]) -> str:
219
+ """Return a one-line ``Name(key=value, ...)`` description.
220
+
221
+ Parameters
222
+ ----------
223
+ name : str
224
+ The class name.
225
+ fields : iterable of tuple
226
+ ``(key, value)`` pairs; a value is written with ``str``, and a pair whose key
227
+ is empty is written as the value alone.
228
+
229
+ Returns
230
+ -------
231
+ str
232
+ The line.
233
+
234
+ Examples
235
+ --------
236
+ >>> from solprop._display import line
237
+ >>> line("Grid", [("n_t", 8192), ("", "field")])
238
+ 'Grid(n_t=8192, field)'
239
+ """
240
+ parts = [f"{key}={value}" if key else str(value) for key, value in fields]
241
+ return f"{name}({', '.join(parts)})"
242
+
243
+
244
+ def block(title: str, rows: Sequence[tuple[str, object]]) -> str:
245
+ """Return an aligned multi-line description.
246
+
247
+ Parameters
248
+ ----------
249
+ title : str
250
+ The first line: the class and its headline.
251
+ rows : sequence of tuple
252
+ ``(key, value)`` pairs, one line each. A value holding newlines is indented
253
+ under its key; a line longer than the package's 88 columns is wrapped at a
254
+ space, its continuation indented under its value, over at most four lines,
255
+ and what is left over, or a single word too long for a line, is cut with an
256
+ ellipsis, so that a block is always safe to paste into a page.
257
+
258
+ Returns
259
+ -------
260
+ str
261
+ The block, without a trailing newline.
262
+
263
+ Examples
264
+ --------
265
+ >>> from solprop._display import block
266
+ >>> print(block("Pulse", [("energy", "275 uJ"), ("duration", "7.5 fs")]))
267
+ Pulse
268
+ energy: 275 uJ
269
+ duration: 7.5 fs
270
+ """
271
+ width = max((len(key) for key, _ in rows), default=0) + 1
272
+ under = " " * (width + 3)
273
+ # A long title continues deeper than the rows, so it cannot be read as one.
274
+ lines = _fitted("", title, " ")
275
+ for key, value in rows:
276
+ first, *rest = str(value).split("\n")
277
+ lines.extend(_fitted(f" {key + ':':<{width}} ", first, under))
278
+ for extra in rest:
279
+ deeper = extra[: len(extra) - len(extra.lstrip())]
280
+ lines.extend(_fitted(under + deeper, extra.lstrip(), under + deeper))
281
+ return "\n".join(lines)
282
+
283
+
284
+ def _fitted(lead: str, value: str, indent: str) -> list[str]:
285
+ """Return one line of a block within the package's width.
286
+
287
+ Parameters
288
+ ----------
289
+ lead : str
290
+ What precedes the value on its first line: the key's column, or the
291
+ indentation of a continuation.
292
+ value : str
293
+ The text, wrapped at its spaces when the line is too long.
294
+ indent : str
295
+ The indentation of each wrapped continuation, under the value.
296
+
297
+ Returns
298
+ -------
299
+ list of str
300
+ The line or its wrapped pieces, at most :data:`_WRAPPED` of them; the last
301
+ kept piece of a longer value, and a piece holding a single word too long for
302
+ the width, end with an ellipsis.
303
+ """
304
+ if len(lead) + len(value) <= _WIDTH:
305
+ return [lead + value]
306
+ pieces = textwrap.wrap(
307
+ value,
308
+ _WIDTH,
309
+ initial_indent=lead,
310
+ subsequent_indent=indent,
311
+ break_long_words=False,
312
+ break_on_hyphens=False,
313
+ ) or [lead]
314
+ if len(pieces) > _WRAPPED:
315
+ pieces = [*pieces[: _WRAPPED - 1], pieces[_WRAPPED - 1][: _WIDTH - 4] + " ..."]
316
+ return [
317
+ piece if len(piece) <= _WIDTH else piece[: _WIDTH - 3] + "..."
318
+ for piece in pieces
319
+ ]
320
+
321
+
322
+ class Displayed:
323
+ """Give a class the package's printing: a block for ``str`` and for IPython.
324
+
325
+ A class provides ``_describe()`` returning its title and its rows, and its own
326
+ one-line ``__repr__``; this mixin turns the first into ``__str__`` and into the
327
+ text IPython and Jupyter show for the last expression of a cell. The plain Python
328
+ prompt, log records and error messages keep the one-line ``repr``.
329
+ """
330
+
331
+ def _describe(self) -> tuple[str, Sequence[tuple[str, object]]]:
332
+ """Return the title and the rows of the block.
333
+
334
+ Returns
335
+ -------
336
+ tuple
337
+ The first line, and ``(key, value)`` pairs for the rest.
338
+ """
339
+ return repr(self), ()
340
+
341
+ def __str__(self) -> str:
342
+ """Return the aligned block.
343
+
344
+ Returns
345
+ -------
346
+ str
347
+ The title and one line per row.
348
+ """
349
+ return block(*self._describe())
350
+
351
+ def _repr_pretty_(self, printer: _Printer, cycle: bool) -> None:
352
+ """Write the block for IPython's pretty printer.
353
+
354
+ Parameters
355
+ ----------
356
+ printer : object
357
+ IPython's printer.
358
+ cycle : bool
359
+ Whether the object was met again while printing itself.
360
+ """
361
+ printer.text(repr(self) if cycle else str(self))
solprop/_helpers.py ADDED
@@ -0,0 +1,223 @@
1
+ # SPDX-FileCopyrightText: 2026 John C. Travers and contributors
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ """The evaluations a backend supplies to solprop's shared per-stage and per-step code.
6
+
7
+ Two small scalar computations run on every backend's path: a scalar table's Chebyshev
8
+ rows at a step's positions (behind every stage bundle and anchor, and an expansion's
9
+ rate rows; :meth:`solprop.translate.chebyshev.PiecewiseChebyshev.at_positions`) and
10
+ the Lagrange basis of an Adams formula on its actual nodes
11
+ (:func:`solprop.stepper.multistep.integrated_weights` and
12
+ :func:`~solprop.stepper.multistep.lagrange_coefficients`). The code around them is
13
+ shared, but **which implementation evaluates them belongs to the backend that runs the
14
+ stage** (SEP 29 section 6.6, R16), never to a process-wide switch or to what happens
15
+ to be installed: the NumPy reference must not run compiled code it exists to check,
16
+ and one simulation must not change another's arithmetic.
17
+
18
+ A backend names its record in ``StepDriver.helpers`` and the shared driver, march and
19
+ engines pass it down: ``numpy_ref`` takes :data:`PLAIN`, the uncompiled Python forms;
20
+ ``numba_cpu`` takes :data:`NUMBA`, the same functions compiled; the native backend
21
+ takes its own record of C++ bindings, defined in its package. All three perform the
22
+ same IEEE operations in the same order, strict (no contraction, no reassociation;
23
+ SEP 25 section 4.2), so they agree to the last bit wherever their arithmetic is IEEE.
24
+ A record may also form a whole exponential Adams trial's coefficient tables in one
25
+ call (:attr:`Helpers.tables`), the composition of
26
+ :func:`solprop.backends.multistep.trial_tables` over its own basis, which its
27
+ resident engine reads every trial; the others compose them from the basis.
28
+
29
+ Translation is not a backend's: it runs before any backend exists and builds the
30
+ problem every backend receives, so it keeps the shared reference compiled by Numba
31
+ (:func:`solprop.translate.chebyshev.clenshaw`, and a table called without a record),
32
+ and the shared functions' defaults are :data:`NUMBA` for callers outside a backend. A
33
+ z table's state-sized evaluation is not a stage scalar and stays on that shared pass.
34
+
35
+ Like ``_display`` and ``_numba_cache``, this module imports nothing from the rest of
36
+ the package at import time, so any layer may use it; each record imports its
37
+ functions from where they live at the first evaluation.
38
+ """
39
+
40
+ import functools
41
+ from collections.abc import Callable
42
+ from dataclasses import dataclass, field
43
+
44
+ import numpy as np
45
+
46
+ __all__ = [
47
+ "NUMBA",
48
+ "PLAIN",
49
+ "BasisFunction",
50
+ "Helpers",
51
+ "PointsFunction",
52
+ "TablesFunction",
53
+ ]
54
+
55
+ PointsFunction = Callable[
56
+ [np.ndarray, np.ndarray, float, float, np.ndarray, np.ndarray], int
57
+ ]
58
+ """``points(z, edges, lower, upper, c, out) -> int``: a table's rows at positions.
59
+
60
+ The contract of :func:`solprop.translate.chebyshev._clenshaw_points`: ``z`` float64
61
+ ``(n_points,)``, ``edges`` float64 ``(n_intervals + 1,)``, ``c`` float64 or complex128
62
+ ``(n_intervals, n_deg, n_values)`` and ``out`` of ``c``'s dtype ``(n_points,
63
+ n_values)``, all C-contiguous; the result is the first position outside ``[lower,
64
+ upper]`` or ``-1``.
65
+ """
66
+
67
+ BasisFunction = Callable[[np.ndarray, float, bool], tuple[np.ndarray, np.ndarray]]
68
+ """``basis(points, upper, integrate) -> (rows, weights)``: an Adams formula's basis.
69
+
70
+ The contract of :func:`solprop.stepper.multistep._basis_rows`: ``points`` float64
71
+ ``(n,)`` and C-contiguous; ``rows`` ``(n, n)`` (zero when integrating) and ``weights``
72
+ ``(n,)`` (unset when not).
73
+ """
74
+
75
+ TablesFunction = Callable[
76
+ [np.ndarray, int, list[int], bool],
77
+ tuple[
78
+ np.ndarray,
79
+ np.ndarray,
80
+ np.ndarray,
81
+ np.ndarray,
82
+ np.ndarray,
83
+ np.ndarray,
84
+ np.ndarray,
85
+ ],
86
+ ]
87
+ """``tables(nodes, order, orders, compensated) -> tables``: a trial's tables at once.
88
+
89
+ The contract of :func:`solprop.backends.multistep.trial_tables`, whose composition over
90
+ the record's own basis it performs in one call, with the same operations in the same
91
+ order: ``nodes`` float64 ``(n,)`` and C-contiguous, newest first; the result is that
92
+ function's seven arrays.
93
+ """
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class Helpers:
98
+ """The per-stage and per-step evaluations of one backend.
99
+
100
+ Each operation is loaded at its first use and kept: a record names its functions
101
+ without importing them, so constructing or importing one loads nothing (the Numba
102
+ record compiles at its first evaluation, exactly as the cached dispatchers it
103
+ holds always have).
104
+
105
+ Attributes
106
+ ----------
107
+ name : str
108
+ What the record is, for messages and tests.
109
+ load_points : collections.abc.Callable
110
+ Returns the :data:`PointsFunction`.
111
+ load_basis : collections.abc.Callable
112
+ Returns the :data:`BasisFunction`.
113
+ load_tables : collections.abc.Callable or None, optional
114
+ Returns the :data:`TablesFunction`, for a record that forms an exponential
115
+ Adams trial's tables in one call; ``None``, the default, composes them from
116
+ the basis.
117
+
118
+ Examples
119
+ --------
120
+ >>> import numpy as np
121
+ >>> from solprop._helpers import PLAIN
122
+ >>> rows, weights = PLAIN.basis(np.array([1.0, 0.0]), 1.0, True)
123
+ >>> weights.tolist()
124
+ [0.5, 0.5]
125
+ """
126
+
127
+ name: str
128
+ load_points: Callable[[], PointsFunction] = field(repr=False)
129
+ load_basis: Callable[[], BasisFunction] = field(repr=False)
130
+ load_tables: Callable[[], TablesFunction] | None = field(default=None, repr=False)
131
+
132
+ @functools.cached_property
133
+ def points(self) -> PointsFunction:
134
+ """Return the Chebyshev rows at several positions.
135
+
136
+ Returns
137
+ -------
138
+ collections.abc.Callable
139
+ The :data:`PointsFunction`.
140
+ """
141
+ return self.load_points()
142
+
143
+ @functools.cached_property
144
+ def basis(self) -> BasisFunction:
145
+ """Return the Lagrange basis on an Adams formula's nodes.
146
+
147
+ Returns
148
+ -------
149
+ collections.abc.Callable
150
+ The :data:`BasisFunction`.
151
+ """
152
+ return self.load_basis()
153
+
154
+ @functools.cached_property
155
+ def tables(self) -> TablesFunction | None:
156
+ """Return an exponential Adams trial's tables in one call, if the record has it.
157
+
158
+ Returns
159
+ -------
160
+ collections.abc.Callable or None
161
+ The :data:`TablesFunction`, or ``None``: the tables are then composed from
162
+ :attr:`basis`.
163
+ """
164
+ return None if self.load_tables is None else self.load_tables()
165
+
166
+
167
+ def _plain_points() -> PointsFunction:
168
+ """Return the Chebyshev rows' Python form.
169
+
170
+ Returns
171
+ -------
172
+ collections.abc.Callable
173
+ :func:`solprop.translate.chebyshev._clenshaw_points`.
174
+ """
175
+ from solprop.translate import chebyshev # noqa: PLC0415 - where it lives
176
+
177
+ return chebyshev._clenshaw_points
178
+
179
+
180
+ def _plain_basis() -> BasisFunction:
181
+ """Return the Adams basis' Python form.
182
+
183
+ Returns
184
+ -------
185
+ collections.abc.Callable
186
+ :func:`solprop.stepper.multistep._basis_rows`.
187
+ """
188
+ from solprop.stepper import multistep # noqa: PLC0415 - where it lives
189
+
190
+ return multistep._basis_rows
191
+
192
+
193
+ def _numba_points() -> PointsFunction:
194
+ """Return the Chebyshev rows compiled by Numba.
195
+
196
+ Returns
197
+ -------
198
+ collections.abc.Callable
199
+ The cached dispatcher of :func:`solprop.translate.chebyshev._clenshaw_batch`.
200
+ """
201
+ from solprop.translate import chebyshev # noqa: PLC0415 - numba on first use
202
+
203
+ return chebyshev._clenshaw_batch()
204
+
205
+
206
+ def _numba_basis() -> BasisFunction:
207
+ """Return the Adams basis compiled by Numba.
208
+
209
+ Returns
210
+ -------
211
+ collections.abc.Callable
212
+ The cached dispatcher of :func:`solprop.stepper.multistep._basis`.
213
+ """
214
+ from solprop.stepper import multistep # noqa: PLC0415 - numba on first use
215
+
216
+ return multistep._basis()
217
+
218
+
219
+ PLAIN = Helpers("plain", _plain_points, _plain_basis)
220
+ """The uncompiled Python forms: the NumPy reference backend's."""
221
+
222
+ NUMBA = Helpers("numba", _numba_points, _numba_basis)
223
+ """The same forms compiled by Numba: ``numba_cpu``'s, and the shared default."""