treams_core/lib.rs
1//! The numerical core of treams-rs: special functions, lattice sums, wave expansions,
2//! T-matrices and S-matrices, with analytic derivatives, in Rust and without Python.
3//!
4//! A pullback turns the gradient of a real loss with respect to a result into the
5//! gradients with respect to the inputs; the [glossary](#glossary) defines it with the
6//! related terms.
7//!
8//! `treams-core` is an internal crate. The Python package `treams_rs` calls it through
9//! the bindings crate `treams-py`, which Python imports as `treams_rs._native`; neither
10//! crate is published on crates.io. The user documentation, with examples and the
11//! design rationale, is at <https://yaugenst.github.io/treams-rs/latest/>.
12//!
13//! Upstream: [treams](https://github.com/tfp-photonics/treams) 0.4.7 at commit `1f5d0d6`.
14//! The kernels follow its formulas, conventions and mode order. Each module names the
15//! treams functions it mirrors, and the [module map](#module-map) pairs every module
16//! with its `treams_rs` and treams namespaces.
17//!
18//! # Layers
19//!
20//! The modules form six layers:
21//!
22//! | Layer | Modules | Contents |
23//! |---|---|---|
24//! | L0 | `numerics`, [`linalg`], [`fpenv`], [`threads`] | Numerical support |
25//! | L1 | [`special`] | Special functions |
26//! | L2 | [`lattice`] | Lattice sums and lattice geometry |
27//! | L3 | [`basis`], [`sw`], [`cw`], [`pw`], [`rotation`], [`channels`], [`vectorwaves`], [`fields`] | Wave families |
28//! | L4 | [`coeffs`], [`tmatrix`], [`ebcm`], [`cluster`] | Scattering by particles |
29//! | L5 | [`smatrix`] | Planar and periodic S-matrices |
30//!
31//! Production code should use its own layer and the layers below it, never a
32//! layer above.
33//! Modules of one layer may use each other: [`cw::to_sw()`] builds
34//! spherical modes, and [`tmatrix::sphere()`] takes the Mie coefficients of [`coeffs`].
35//! Any module may use the root items [`Complex`], [`Error`], [`Result`] and
36//! [`MAX_DEGREE`]. Tests may use any module.
37//!
38//! [`rotation`] and [`channels`] share one residual or gradient type across both
39//! multipole families ([`rotation::RotationResidual`], [`channels::ChannelGradient`]),
40//! so they stay at the top level instead of inside [`sw`] and [`cw`].
41//!
42//! A directory module keeps its files private and re-exports their items, so callers
43//! write `sw::expansion` and `smatrix::tr`. Three modules keep their own path:
44//! [`special::coordinates`], `numerics::broadcast` and `numerics::parallel`.
45//!
46//! # Conventions
47//!
48//! The contributor-facing [implementation rules](https://yaugenst.github.io/treams-rs/latest/development/architecture/#rules)
49//! are the common home for ownership, validation and evidence requirements.
50//! The conventions below define the core's terms.
51//!
52//! ## Numbers and labels
53//!
54//! - [`Complex`] is `num_complex::Complex64`, which `NumPy` calls `complex128`. Every
55//! kernel takes and returns double precision.
56//! - The degree is `l` and the order is `m`; `lmax` and `mmax` bound them. "Order" also
57//! names the order of a Bessel function, never a degree.
58//! - [`MAX_DEGREE`] = 128 bounds the degree `l` and the size of cylindrical orders `m`.
59//! Translations couple degrees up to `special::MAX_ORDER` = 256, and Wigner and Kambe
60//! labels reach [`special::MAX_LABEL`] = 260. treams has no such limits.
61//! - `pol: u8` is the polarization index, 0 or 1, and `helicity: bool` selects the
62//! convention that treams calls `poltype`. With `helicity` true, pol 1 is positive and
63//! pol 0 negative helicity; with `helicity` false, pol 1 is TM (the N multipoles) and
64//! pol 0 TE (the M multipoles). Bases list pol 1 before pol 0, as treams does.
65//! - [`special::Radial`] is the treams `modetype`: `Regular` is `'regular'`, and
66//! `Singular` is `'singular'`, the outgoing Hankel function of the first kind. The
67//! bindings pass it as `singular: bool`.
68//! - A basis entry `(pidx, mode)` holds the position index (treams `pidx`) and the mode
69//! label, [`sw::Mode`] or [`cw::Mode`]. `positions` are Cartesian expansion centres.
70//! - A matrix between two bases maps coefficients of the `source` basis to the
71//! `destination` basis. Translations take the displacement `destination - source`;
72//! lattice sums run over `source - destination`, and each lattice path says where it
73//! negates the displacement.
74//! - Lattice sums take the Bloch vector `kpar`, the Ewald split parameter `eta`, the
75//! lattice `vectors` (treams `a`) and the `shift` (treams `r`).
76//! - Planar inputs: `ks` holds the two helicity wavenumbers of each medium, `kzs` the
77//! normal wavenumbers, `zs` the impedances and `q` the transverse wavevectors.
78//! [`smatrix::chirality_density`] and [`smatrix::oriented_chirality`] use the
79//! argument names of their native functions: `normal` for the normal wavenumbers
80//! and, in [`smatrix::oriented_chirality`], `transverse` for the transverse
81//! wavevectors. The [`smatrix`] module docs give the order of the sides and of the
82//! four S-matrix blocks.
83//!
84//! ## Names
85//!
86//! - Items inside [`sw`], [`cw`] and [`pw`] carry no family prefix; other modules name
87//! them by path, as in `sw::Mode`, `cw::Basis` and `pw::expansion`. A module that
88//! serves both families prefixes the family: `rotation::sw_rotate`,
89//! `channels::cw_periodic_to_pw`.
90//! - A forward function carries the treams name or the name of its operation:
91//! `coeffs::mie`, `cluster::interaction`, `smatrix::tr`. Suffixes mark the variants:
92//! `_array` for an elementwise forward over arrays that returns a residual
93//! (`special::bessel_array`), `_matrix` for a matrix that changes the wave family
94//! (`cw::to_sw_matrix`), and `_value` for the forward that returns only the value of
95//! a forward with a residual (`smatrix::tr_value`). Matrices between two bases of one
96//! family are nouns: `sw::expansion`, `sw::lattice_expansion`, `rotation::sw_rotation`.
97//! - A forward `X` that supports gradients returns `(value, XResidual)` when its
98//! pullback does not read the value. When the pullback reads it, the forward returns
99//! only the residual, which lends the value through an accessor such as `value()`:
100//! [`coeffs::mie`], [`smatrix::tr`] and [`linalg::solve`] work this way.
101//! `XResidual::pullback(&self, cotangent)` returns an `XGradient` with one field per
102//! differentiable input, or an array of gradients in argument order. The residual
103//! name drops the suffixes `_array`, `_matrix` and `_value`:
104//! `sw::polar_translation_array` returns a [`sw::PolarTranslationResidual`], and
105//! `cw::to_sw_matrix` a [`cw::ToSwResidual`].
106//! - The `record` methods of [`cluster::InteractionFactor`] and
107//! [`cluster::IterativeSphereCluster`] return residuals named after their native
108//! context classes. [`cluster::IlluminateResidual`] matches `IlluminateContext`, which
109//! `diff.illuminate` also returns. [`cluster::IterativeResidual`] matches
110//! `IterativeContext`.
111//! - Family twins share one residual: [`rotation::sw_rotation`] and
112//! [`rotation::cw_rotation`] return [`rotation::RotationResidual`], and
113//! [`cluster::particle_cluster`] and [`cluster::cylindrical_particle_cluster`] return
114//! [`cluster::ParticleClusterResidual`]. [`linalg::solve_owned`], which reuses the
115//! operator's buffer, returns the [`linalg::SolveResidual`] of [`linalg::solve`].
116//! - The pullbacks of [`linalg::EigResidual`] and [`smatrix::BandsResidual`] take one
117//! cotangent per output and name each after its output: `values` and `vectors`, and
118//! `wavenumbers` and `vectors`.
119//!
120//! ## Gradients of complex values
121//!
122//! A loss `L` changes by `dL = Re Σ conj(g)·dx` when a value changes by `dx`, where `g`
123//! is the cotangent of that value. For a complex value `x = a + i b`, the cotangent is
124//! `∂L/∂a + i ∂L/∂b`, and real inputs get real gradients. A pullback through a
125//! holomorphic map `y = f(x)` therefore multiplies by the conjugate derivative:
126//! `g_x = conj(f'(x)) g_y`.
127//!
128//! ## Reusable derivatives and saved state
129//!
130//! A residual represents fixed primal inputs. Its `pushforward(&self, tangent)`
131//! maps input directions to output directions; `pullback(&self, cotangent)` maps
132//! an output cotangent to input gradients. Both borrow the residual, so repeated
133//! calls can reuse recorded values and factorizations. [`linalg::SolveResidual`]
134//! is an example. These are first-order actions; their presence does not imply
135//! support for differentiating the derivative implementation itself.
136//!
137//! Keep invariant-bearing residual fields private and retain only data needed by
138//! those actions. Returned gradient records may expose plain data. Prefer moving
139//! owned buffers when the caller has no further use for them, as
140//! [`linalg::solve_owned`] does, over cloning to satisfy a borrowed interface.
141//!
142//! [`saved::SavedState`] carries numerical state through array-only callbacks.
143//! Its bytes belong to one invocation of the current extension, not a persistent
144//! file format. Keep shape-derived sizes, tags, writes and restores synchronized;
145//! validate lengths, domains and allocation sizes before numerical work. Tests
146//! should produce state through the public operation, then check repeated round
147//! trips, both derivative directions, deferred errors and zero directions.
148//! A layout-preserving cleanup retains every byte; removing payload or padding is
149//! a separate compatibility decision.
150//!
151//! ## Errors
152//!
153//! Every fallible function returns [`Result`], whose [`Error`] says why it failed:
154//! [`InvalidInput`](Error::InvalidInput) for input outside the domain,
155//! [`SpecialFunction`](Error::SpecialFunction), [`NonFinite`](Error::NonFinite) and
156//! [`NotConverged`](Error::NotConverged) for numerical failures,
157//! [`Singular`](Error::Singular) for a singular linear system, and
158//! [`OutOfMemory`](Error::OutOfMemory) when the system refuses the memory of a large
159//! output or workspace. The bindings raise `OutOfMemory` as Python `MemoryError` and
160//! every other variant as `ValueError`, with the displayed message. treams returns NaN
161//! or emits `NumPy` warnings in the numerical cases.
162//!
163//! Reserve advertised large outputs and workspaces fallibly, through
164//! `numerics::zeros`, `numerics::filled`, `numerics::reserve` and the LU workspace of
165//! [`linalg`], so a refused request returns `OutOfMemory` instead of aborting the
166//! process. This is not a process-wide guarantee: input copies, matrix products,
167//! gradient buffers and other allocations can still abort when refused.
168//! Branch on typed error identity, not display text, and propagate resource and
169//! programming errors through numerical fallback.
170//!
171//! ## Parallelism and reproducibility
172//!
173//! Rayon runs independent items in parallel, on the pool of [`threads`], once their
174//! number reaches a threshold; [`numerics::parallel`](numerics/parallel/index.html)
175//! lists every threshold. The elementwise evaluations and the helpers there keep every
176//! output in index order. A pullback that adds over items splits them into chunks
177//! fixed by the item count, or into blocks fixed by the shape, and adds the partial
178//! sums in order, so the thread count sets how many workers run the chunks but never
179//! the order of the additions. The docs of these residuals, and of
180//! `numerics::broadcast` for the elementwise ones, say how they add; the thread count
181//! changes the result of none of them:
182//!
183//! - Outputs in index order, or sums in a fixed order: the elementwise residuals of
184//! [`special`] and [`lattice::SumResidual`], [`sw::PolarTranslationResidual`],
185//! [`sw::ExpansionResidual`], [`sw::LatticeExpansionFromTableResidual`],
186//! [`cw::PolarTranslationResidual`], [`cw::ExpansionResidual`],
187//! [`cw::LatticeExpansionResidual`], [`rotation::RotationResidual`],
188//! [`pw::PhasesResidual`], [`pw::PermutationResidual`],
189//! [`vectorwaves::VectorWaveResidual`], [`ebcm::QmatResidual`],
190//! [`coeffs::MieResidual`], [`coeffs::MieCylResidual`],
191//! [`tmatrix::SphereResidual`], [`tmatrix::CylinderResidual`],
192//! [`tmatrix::MetricResidual`], [`cluster::InteractionResidual`],
193//! [`cluster::IlluminateResidual`] and [`cluster::SphereClusterResidual`].
194//! - Partial sums over fixed chunks or blocks, added in order:
195//! [`sw::LatticeExpansionResidual`], [`sw::PeriodicToCwResidual`],
196//! [`cw::ToSwResidual`], [`pw::ExpansionResidual`], [`pw::FieldResidual`],
197//! [`channels::SphericalChannelsResidual`], [`channels::CylindricalChannelsResidual`],
198//! [`fields::FieldResidual`], [`fields::OperatorResidual`],
199//! [`cluster::IterativeResidual`] and [`smatrix::LayerStackResidual`].
200//!
201//! The intended contract is identical bits at every thread budget, including one
202//! thread ([`threads::set_num_threads`] or `TREAMS_RS_NUM_THREADS=1`).
203//!
204//! ## Floating-point environment
205//!
206//! The kernels assume IEEE 754 gradual underflow: subnormal numbers stay subnormal.
207//! JAX (through XLA) and `torch.set_flush_denormal(True)` make the calling thread flush
208//! them to zero, which breaks algorithms that scale through the subnormal range, such
209//! as faer's complex reciprocal of an LU pivot. [`fpenv::ieee`] runs a closure with
210//! subnormals on the calling thread and restores the caller's mode afterwards. Its name
211//! says what the closure gets, the IEEE 754 default arithmetic, and stays short because
212//! the bindings wrap the body of every Python entry point in it: `ieee(|| ...)`.
213//! `fpenv::flushing` does the opposite, for tests. The [`fpenv`] module docs explain
214//! the control registers and why the guard is sound.
215//!
216//! ## Lints
217//!
218//! The workspace keeps clippy's default lints, adds `pedantic` and `nursery`, requires
219//! docs on every public item, and the checks treat every warning as an error.
220//! `clippy::indexing_slicing` stays on, so new code indexes through iterators or checked
221//! access. A numerical module that validates its indices on entry opts out once, with
222//! `#![allow(clippy::indexing_slicing)] // <why the indices are in range>`; the reason
223//! comment is required. Tests may index freely (`clippy.toml`).
224//!
225//! # Glossary
226//!
227//! A record is a function that returns a value and a reusable context. The context stores what is needed to compute derivatives later: `context.pullback(g)` takes the gradient `g` of a real-valued loss with respect to the value and returns the gradients with respect to the inputs, one for each differentiable input, in the order of the arguments. Gradients follow the convention dL = Re Σ conj(g)·dx.
228//!
229//! In the Rust core, a function returns `(value, XResidual)`, and `XResidual::pullback(&self, cotangent)` returns the input gradients as `XGradient`.
230//!
231//! | Term | Meaning |
232//! |---|---|
233//! | forward | A function that computes a value. One that supports gradients returns a residual, beside the value or holding it. |
234//! | residual | What a forward saves for its pushforward and pullback: `XResidual` for the forward `X`. Not the `b - A x` of a linear system, which the GMRES solver of [`linalg`] calls the residual. |
235//! | tangent | An input or output direction at the recorded primal inputs. |
236//! | pushforward | `XResidual::pushforward`: maps input tangents to output tangents while borrowing the reusable residual. |
237//! | pullback | `XResidual::pullback`: turns the gradient with respect to the value into the gradients with respect to the inputs. It borrows the reusable residual. |
238//! | cotangent | The gradient of a real-valued loss with respect to one value: the `g` of the definition above. |
239//! | gradient | `XGradient`: the input gradients that a pullback returns, in the order of the forward's arguments. |
240//! | record | The Python name of a forward with a residual: a function of `treams_rs.diff` that returns `(value, context)`. |
241//! | context | The Python object that holds a residual, named after its record: `MieContext` for `diff.mie`. |
242//! | jet | A value carried through the arithmetic with its first derivatives along `N` directions (`numerics::Jet<N>`). |
243//! | [`RadialJet`](special::RadialJet) | A radial function with its first and second derivatives in its one complex argument. |
244//! | split (`eta`) | The Ewald split parameter, which divides a lattice sum into a real-space and a reciprocal-space part; `eta = 0` selects it automatically. |
245//! | Bloch vector (`kpar`) | The wavevector components along a lattice: the term of lattice point `R` carries the phase `exp(i kpar · R)`. |
246//! | sheet | The branch of the complex logarithm on which a reciprocal-space term of a lattice sum is evaluated. |
247//!
248//! The [`lattice`](lattice#glossary) module docs define the other terms of the lattice
249//! sums.
250//!
251//! # Tests
252//!
253//! The tests have two tiers:
254//!
255//! - A module's inline `tests` module checks how that module computes its results:
256//! fast paths against reference paths, tables and plans, dispatch thresholds, error
257//! paths, accuracy against reference tables, and private helpers.
258//! - `src/properties/<domain>.rs` checks what the public operations of one domain
259//! promise: physical laws, analytic identities, and pullbacks against finite
260//! differences and adjoint pairings. The domains are `special`, `linalg`, `lattice`,
261//! `waves`, `plane`, `smatrix` and `tmatrix`, as in the Python suite; `lattice` is
262//! the directory `src/properties/lattice/`.
263//!
264//! A test of one module's implementation goes inline; a physical, analytic or adjoint
265//! identity goes into its domain file. Both tiers use crate-private items, so the crate
266//! has no `tests/` directory. `cargo test -p treams-core` runs both.
267//! [Testing](https://yaugenst.github.io/treams-rs/latest/development/testing/) on the docs site
268//! gives the helpers and conventions of both tiers.
269//!
270//! # Module map
271//!
272//! Each module mirrors one treams namespace where one exists. A dash marks a module
273//! without a counterpart.
274//!
275//! <!-- crosswalk:start -->
276//! | Rust module | treams_rs namespace | treams | Contents |
277//! |---|---|---|---|
278//! | [`special`] | `treams_rs.special` | `treams.special` | Bessel, Legendre and Wigner functions, coordinate transforms, incomplete gamma and Kambe integrals |
279//! | [`lattice`] | `treams_rs.lattice`, `treams_rs.misc.firstbrillouin*` | `treams.lattice`, `treams.misc.firstbrillouin*` | Lattice sums and lattice geometry |
280//! | [`sw`] | `treams_rs.sw`, `treams_rs.special.tl_vsw_*` | `treams.sw`, `treams.special.tl_vsw_*` | Spherical modes, translation coefficients, expansion matrices and the conversion of chains to cylindrical waves |
281//! | [`cw`] | `treams_rs.cw`, `treams_rs.special.tl_vcw` | `treams.cw`, `treams.special.tl_vcw` | Cylindrical modes, translation coefficients, expansion matrices and the conversion to spherical waves |
282//! | [`pw`] | `treams_rs.pw`, `treams_rs.misc.wave_vec_z` | `treams.pw`, `treams.misc.wave_vec_z` | Plane-wave polarizations, fields and multipole expansions |
283//! | [`basis`] | `treams_rs.SphericalBasis`, `treams_rs.CylindricalBasis` | `treams.SphericalWaveBasis`, `treams.CylindricalWaveBasis` | Multipole bases and the expansion gradients that both families share |
284//! | [`rotation`] | `treams_rs.sw.rotate`, `treams_rs.cw.rotate`, `treams_rs.operators.Rotate` | `treams.sw.rotate`, `treams.cw.rotate`, `treams.Rotate` | Rotation coefficients and rotation matrices of multipole bases |
285//! | [`channels`] | `treams_rs.sw.periodic_to_pw`, `treams_rs.cw.periodic_to_pw` | `treams.sw.periodic_to_pw`, `treams.cw.periodic_to_pw` | Plane-wave channels of periodic arrays |
286//! | [`vectorwaves`] | `treams_rs.special.vsh_*`, `vsw_*`, `vcw_*`, `vpw_*`, `sph_harm` | `treams.special.vsh_*`, `vsw_*`, `vcw_*`, `vpw_*`, `sph_harm` | Vector spherical harmonics and vector spherical, cylindrical and plane waves |
287//! | [`fields`] | `treams_rs.operators.efield` | `treams.efield` | Electric fields of multipole expansions at sample points |
288//! | [`coeffs`] | `treams_rs.coeffs`, `treams_rs.Material`, `treams_rs.misc.refractive_index` | `treams.coeffs`, `treams.Material`, `treams.misc.refractive_index` | Mie coefficients of multilayer spheres and cylinders, and materials |
289//! | [`tmatrix`] | `treams_rs.TMatrix`, `treams_rs.CylindricalTMatrix` | `treams.TMatrix`, `treams.TMatrixC` | T-matrices of single spheres and cylinders, and the cd, db and chi metrics |
290//! | [`ebcm`] | `treams_rs.ebcm` | `treams.ebcm` | Q-matrices of the extended boundary condition method |
291//! | [`cluster`] | `treams_rs.Cluster`, `treams_rs.iterative` | `treams.TMatrix.cluster`, `treams.TMatrix.interaction` | Multiple scattering in particle clusters, dense and matrix-free |
292//! | [`smatrix`] | `treams_rs.SMatrix`, `treams_rs.coeffs.fresnel` | `treams.SMatrices`, `treams.coeffs.fresnel` | Planar S-matrices: interfaces, layer stacks, composition, illumination, transmittance and bands |
293//! | [`linalg`] | `treams_rs.diff.solve`, `svdvals`, `eig` | - | Dense LU, SVD and eigensystems with pullbacks, and restarted GMRES |
294//! | `numerics` | - | - | Forward-mode jets, broadcasting and parallel thresholds |
295//! | [`fpenv`] | - | - | A guard that keeps subnormal numbers when the caller flushes them to zero |
296//! | [`threads`] | `treams_rs.set_num_threads`, `threads`, `thread_info` | - | The thread budget and the fork-safe pool that runs every parallel region |
297//! <!-- crosswalk:end -->
298
299// The crosswalk table ends the crate docs: its header names treams_rs without backticks,
300// as the docs generator expects, and clippy's doc_markdown lint flags that header when
301// a paragraph follows the table.
302
303// Modules by layer; production code imports only from its own and lower layers.
304
305// L0: numerical support.
306pub mod fpenv;
307pub mod linalg;
308pub(crate) mod numerics;
309pub mod threads;
310
311// L1: special functions (treams.special).
312pub mod special;
313
314// L2: lattice sums and lattice geometry (treams.lattice).
315pub mod lattice;
316
317// L3: wave families (bases, translations, conversions, rotations, channels, fields).
318pub mod basis;
319pub mod channels;
320pub mod cw;
321pub mod fields;
322pub mod pw;
323pub mod rotation;
324pub mod sw;
325pub mod vectorwaves;
326
327// L4: scattering (Mie coefficients, T-matrices, EBCM, clusters).
328pub mod cluster;
329pub mod coeffs;
330pub mod ebcm;
331pub mod saved;
332pub mod tmatrix;
333
334// L5: planar and periodic S-matrices (treams SMatrices).
335pub mod smatrix;
336
337/// Property tests by domain. An inline `tests` module checks how one module computes;
338/// a file here checks a physical, analytic or adjoint identity of a domain's public
339/// operations.
340#[cfg(test)]
341mod properties;
342/// Case budgets, pairings, finite differences, strategies and fixtures that the tests
343/// share.
344#[cfg(test)]
345pub(crate) mod test_support;
346
347/// Complex double precision used throughout the numerical core.
348pub type Complex = num_complex::Complex64;
349
350/// The largest multipole degree `l`, and the largest `|m|` of a cylindrical mode.
351///
352/// Translations couple degrees up to `2 MAX_DEGREE` (`special::MAX_ORDER`): the
353/// log-factorial cache, the AMOS Bessel orders and the solid-harmonic tables cover that
354/// range. treams has no such limit.
355///
356/// Error messages write the numbers out, with a comment naming the constant: the checks
357/// run once per element, and a literal message keeps formatting code out of them.
358pub const MAX_DEGREE: i32 = 128;
359
360/// A stable reason why a requested derivative is unavailable.
361///
362/// Saved metric states encode these reasons independently of their display text.
363#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
364pub enum DerivativeError {
365 /// The norm of the helicity contrast is not differentiable at zero.
366 #[error("chirality is not differentiable at zero contrast")]
367 ZeroChiralityContrast,
368 /// Singular-value weights have the wrong length or contain non-finite values.
369 #[error("invalid singular-value cotangent")]
370 InvalidSingularValueCotangent,
371 /// A nonzero weight requests the derivative of an unresolved singular value.
372 #[error("nonzero singular-value weight at zero or below numerical resolution is not supported")]
373 UnresolvedSingularValue,
374 /// Unequal weights at a repeated singular value depend on singular vectors.
375 #[error("repeated singular values require equal cotangent weights")]
376 UnequalSingularValueWeights,
377}
378
379/// A numerical failure, invalid physical input or refused memory.
380///
381/// The Python bindings raise [`OutOfMemory`](Self::OutOfMemory) as `MemoryError` and
382/// every other variant as `ValueError`, each with the displayed message.
383#[derive(Clone, Debug, thiserror::Error)]
384pub enum Error {
385 /// The operation does not accept the input, for example a negative degree or a
386 /// non-finite wavenumber.
387 #[error("{0}")]
388 InvalidInput(String),
389 /// A derivative is unavailable at the supplied inputs or weights.
390 #[error(transparent)]
391 Derivative(DerivativeError),
392 /// An AMOS Bessel, Legendre, Ferrers, Wigner, incomplete gamma or Kambe evaluation
393 /// failed.
394 #[error("special-function evaluation failed: {0}")]
395 SpecialFunction(String),
396 /// A finite input produced a non-finite value, for example an overflowing phase.
397 #[error("{0}")]
398 NonFinite(String),
399 /// A sum, series, iteration or decomposition did not reach its tolerance, or
400 /// cancellation lost the accuracy.
401 #[error("{0}")]
402 NotConverged(String),
403 /// A linear system (an LU solve, a 2 x 2 Mie inverse or an S-matrix solve) is
404 /// singular at the supplied parameters.
405 #[error("singular linear system")]
406 Singular,
407 /// The system refused memory for an output or workspace. The bindings raise it as
408 /// `MemoryError`.
409 #[error("{0}")]
410 OutOfMemory(String),
411}
412
413impl Error {
414 /// [`OutOfMemory`](Self::OutOfMemory) for `count` elements of `element_bytes` bytes
415 /// each: "cannot allocate 4096 bytes", or "cannot allocate more than
416 /// 18446744073709551615 bytes" (`usize::MAX` on 64-bit targets) when the product
417 /// overflows.
418 #[must_use]
419 pub fn out_of_memory(count: usize, element_bytes: usize) -> Self {
420 Self::OutOfMemory(count.checked_mul(element_bytes).map_or_else(
421 || format!("cannot allocate more than {} bytes", usize::MAX),
422 |bytes| format!("cannot allocate {bytes} bytes"),
423 ))
424 }
425
426 /// Whether the evaluation failed numerically ([`SpecialFunction`], [`NonFinite`] or
427 /// [`NotConverged`]) rather than on its input, a singular system or memory.
428 ///
429 /// Another method may succeed where one fails numerically: the one-dimensional
430 /// spherical lattice sums then fall back to their spectral series.
431 ///
432 /// [`SpecialFunction`]: Self::SpecialFunction
433 /// [`NonFinite`]: Self::NonFinite
434 /// [`NotConverged`]: Self::NotConverged
435 pub(crate) const fn is_numerical(&self) -> bool {
436 matches!(
437 self,
438 Self::SpecialFunction(_) | Self::NonFinite(_) | Self::NotConverged(_)
439 )
440 }
441}
442
443/// Numerical result with a typed error.
444pub type Result<T> = std::result::Result<T, Error>;