solprop.translate.expansion#
A z-dependent quantity as a Chebyshev expansion in the scalar paths it depends on.
A mode whose whole z dependence runs through named scalar paths
(solprop.modes.protocol.ParameterisedMode) makes every quantity built from it a
function of those parameters rather than of the position:
D(omega, z) = D(omega; rho(z), a(z)). That distinction is worth a representation of
its own, because the two dependences are completely different in character. D is
smooth and analytic in the density and the core radius; rho(z) is what carries the
square-root cusp of a near-vacuum entrance, the stage boundaries of a fill and the
corners of a taper. Tabulating D along z pays for that structure once per state
element:
with \(I_{jk}(z) = \int_0^z T_j(\hat\rho(z'))T_k(\hat a(z'))\,\mathrm{d}z'\) a
scalar table along z. The expensive array-valued object is resolved where it is easy
– one global Chebyshev box per parameter – and the hard structure stays in the scalar
tables, which cost 8 bytes per term per coefficient instead of 16 bytes per state
element. The memory of the kernel contract’s section 3 falls from
n_intervals x n_deg x state to n_terms x state, and evaluation
falls from two Clenshaw passes over the state to one over n_terms scalars followed
by one linear combination.
The same class serves the cumulative operator, whose weights are the integrals
\(I_{jk}\), and the prefactor, whose weights are the products
\(T_j(\hat\rho(z))T_k(\hat a(z))\) themselves; integrate is the only difference
between the two builds.
Where this does not apply. The expansion is one global box per parameter, so it is
exact only to the truncation of an analytic function. A quantity that kinks in a
parameter – the loss clamp of solprop.linear.LossPolicy crossing its limit,
the evanescent index floor of a capillary binding partway along the range – has no such
truncation, the degree search reaches its ceiling, and build_expansion() returns
None. The caller then falls back to the z table, which is the general representation
and splits at the kink as it always did. Nothing about the seam changes either way:
a backend evaluates through delta_phi, unitary_phase and prefactor_at.
The narrative is docs/guide/numerics/linear-operator.md, “The expansion in the
parameters”.
Module Attributes
Most varying parameters an expansion carries before the caller must fall back. |
|
Positions sampled per declared segment when bracketing a path's range. |
|
Overshoot of the mapped parameter tolerated before the composition is refused. |
Functions
|
Append endpoint differences over a cumulative expansion's parameter box. |
|
Expand a quantity in its scalar parameters, with weights along z. |
|
Measure a sampler's relative round-off on a parameter box, in the tail norm. |
|
Reuse coefficients for an unchanged function along covered parameter paths. |
Classes
|
A quantity as a Chebyshev expansion in its scalar paths, with weights along z. |
- solprop.translate.expansion.MAX_PARAMETERS: Final = 2#
Most varying parameters an expansion carries before the caller must fall back.
Two is what the physics layer declares – a core radius and a number density – and the tensor grid costs the product of the per-parameter node counts, so a third dimension would be a decision rather than a detail (
docs/dev/kernel-contract.mdsection 3).
- class solprop.translate.expansion.ParameterExpansion(parameters, ranges, degrees, terms, weights, endpoint_degrees=None)[source]#
Bases:
objectA quantity as a Chebyshev expansion in its scalar paths, with weights along z.
The value at a position is
sum_j terms[j] * weights(z)[j]: one Clenshaw pass overn_termsscalars and one linear combination over the value. Each shape annotation carries its own axis name because__post_init__is what enforces their agreement, and that check must stay reachable in production where no runtime type checker runs.- Attributes:
- parameterstuple of str
The path names the expansion runs over, in the order of the term multi-index, for example
("core_radius", "density").- rangesnumpy.ndarray
(lo, hi)per parameter in that parameter’s own units, float64, shape(n_params, 2), strictly increasing across each row. These are the values the paths reach over the tabulated range.- degreesnumpy.ndarray
Chebyshev degree per parameter,
numpy.intp, shape(n_params,). The terms are the tensor product, flattened in C order, so this block contributesprod(degrees + 1)of the n_terms terms and endpoint_degrees, when present, contributes the rest.- termsnumpy.ndarray
The expansion coefficients, shape
(n_terms, *value_shape). For the kernel contract’s section 3 the value shape is(n_batch_or_1, n_modes, n_pol, n_omega)and the array is complex128.- weightssolprop.translate.chebyshev.PiecewiseChebyshev
The scalar tables along z, value shape
(n_terms,), float64: the productsT_j(p_hat(z))for a plain quantity and their integrals from the start of the range for a cumulative one.- endpoint_degreesnumpy.ndarray or None, optional
Degrees of an appended tensor block with weights equal to the plain basis products minus their entrance values. The first block retains integral weights; both blocks use the same parameter box.
- Raises:
- TranslationError
If the parameter count, the ranges, the degrees, the term count and the weights’ value shape do not agree, or a range is not increasing.
Examples
>>> import numpy as np >>> from solprop.profiles import Linear >>> from solprop.translate.expansion import build_expansion >>> paths = {"density": Linear(1.0, 3.0, 1.0)} >>> expansion = build_expansion( ... lambda values: np.array([values["density"] ** 2]), paths, 0.0, 1.0 ... ) >>> expansion.parameters, expansion.n_terms (('density',), 3) >>> round(float(expansion(0.5).item()), 12) 4.0
- property budget_exceeded: bool[source]#
Return whether the weights’ adaptive split ran out of intervals.
- Returns:
- bool
The flag of the scalar tables; the parameter search itself reports failure by returning
Nonefrombuild_expansion()instead.
- degrees: Int[ndarray, 'n_degrees']#
- delta(z1, z2)[source]#
Return the difference of the expansion between two positions.
One subtraction of the scalar weights and one linear combination, rather than two of each: for a cumulative quantity this is the increment over the step.
- Parameters:
- z1, z2float
Positions in m, both inside
z_span.
- Returns:
- numpy.ndarray
value(z2) - value(z1), of shapevalue_shape.
- Raises:
- TranslationError
If a position lies outside the weights’ span.
- property n_terms: int[source]#
Return the number of expansion terms.
- Returns:
- int
The length of the flattened multi-index:
prod(degrees + 1), plusprod(endpoint_degrees + 1)for the appended endpoint block when one is present.
Examples
>>> import numpy as np >>> from solprop.profiles import Linear >>> from solprop.translate.expansion import build_expansion >>> build_expansion( ... lambda values: np.array([values["density"]]), ... {"density": Linear(1.0, 2.0, 1.0)}, ... 0.0, ... 1.0, ... ).n_terms 2
- property nbytes: int[source]#
Return the memory the expansion occupies.
- Returns:
- int
Bytes of the terms and of the scalar tables together.
Examples
>>> import numpy as np >>> from solprop.profiles import Linear >>> from solprop.translate.expansion import build_expansion >>> expansion = build_expansion( ... lambda values: np.array([values["density"]]), ... {"density": Linear(1.0, 2.0, 1.0)}, ... 0.0, ... 1.0, ... ) >>> expansion.nbytes > 0 True
- ranges: Float[ndarray, 'n_params two']#
- terms: Inexact[ndarray, 'n_terms *value']#
- property value_shape: tuple[int, ...][source]#
Return the shape of one evaluated value.
- Returns:
- tuple of int
The trailing axes of terms.
- weights: PiecewiseChebyshev#
- solprop.translate.expansion.RANGE_SAMPLES: Final = 4097#
Positions sampled per declared segment when bracketing a path’s range.
Every profile in
solprop.profilesis monotone between its own breakpoints, so the extremes are at the segment ends and the sweep is a guard rather than the method. It is a guard worth having: a measured profile handed in throughFromCallableneed not be monotone, and a range that misses the values the path actually reaches would make the composition an extrapolation.
- solprop.translate.expansion.RANGE_SLACK: Final = 1e-06#
Overshoot of the mapped parameter tolerated before the composition is refused.
The range is bracketed by sampling, so a path may reach a hair beyond it. Relative to the range,
1e-6past an end changesT_jby aboutcosh(j sqrt(2e-6)) - 1, which is below the tail tolerance at every degree the search reaches, and the value is clipped to the end. Anything larger means the bracket is wrong and is an error, because a Chebyshev series must never be extrapolated.
- solprop.translate.expansion.append_endpoint_expansion(integral, value_at, paths, *, tol)[source]#
Append endpoint differences over a cumulative expansion’s parameter box.
- Parameters:
- integralParameterExpansion
Cumulative expansion with integral weights and no endpoint block.
- value_atcallable
Endpoint quantity at one point of the existing parameter box.
- pathsMapping of str to ZProfile
Scalar paths in the same physical units as the original expansion.
- tolfloat
Positive relative resolution tolerance for the endpoint quantity.
- Returns:
- ParameterExpansion or None
Concatenated terms and scalar weights, or None if the endpoint quantity cannot be resolved on the parameter box.
- solprop.translate.expansion.build_expansion(value_at, paths, z0, z1, *, tol=1e-12, kinks=(), n_max=64, max_intervals=512, integrate=False)[source]#
Expand a quantity in its scalar parameters, with weights along z.
The parameter search is the one-dimensional search of
solprop.translate.chebyshevapplied to each parameter axis in turn, on one global box per parameter: sample the tensor grid of Chebyshev-Lobatto points, accept an axis when its coefficient tail is negligible per element, otherwise double that axis, reusing the samples already taken, up to n_max. There is no splitting in a parameter – a quantity that needs one is not analytic there, and the z table is the representation for it.- Parameters:
- value_atcallable
values -> array, taking one value per parameter by name and returning the quantity at those values. Its result must have the same shape and dtype at every point.- pathscollections.abc.Mapping
The varying paths, by name. A constant path contributes no dimension and must not be passed; its value stays inside value_at.
- z0, z1float
Ends of the range in m, with
z1 > z0. The weights cover it and the parameter ranges are what the paths reach over it.- tolfloat, optional
Target bound on the uniform error relative to the value scale, for both the parameter search and the scalar tables.
- kinksarray_like, optional
Positions in z that must be interval edges of the scalar tables: the paths’ declared breakpoints. Those outside the range are dropped.
- n_maxint, optional
Highest Chebyshev degree tried on a parameter axis before the build gives up.
- max_intervalsint, optional
Interval budget for the scalar tables.
- integratebool, optional
Whether the weights are the integrals of the products from z0, which makes the expansion a cumulative quantity. Default
False.
- Returns:
- ParameterExpansion or None
The expansion, or
Nonewhen a parameter axis is still unresolved at n_max or there are more thanMAX_PARAMETERSof them.Noneis the signal to fall back to the z table, not an error.
- Raises:
- TranslationError
If no paths are given, a path does not vary over the range, the range is empty, or value_at returns a non-finite or inconsistently shaped value.
Examples
>>> import numpy as np >>> from solprop.profiles import Linear >>> from solprop.translate.expansion import build_expansion >>> expansion = build_expansion( ... lambda values: np.array([1.0, values["density"]]), ... {"density": Linear(0.0, 2.0, 1.0)}, ... 0.0, ... 1.0, ... integrate=True, ... ) >>> [round(value, 12) for value in expansion.delta(0.0, 1.0).tolist()] [1.0, 1.0]
- solprop.translate.expansion.parameter_roundoff_floor(value_at, names, ranges)[source]#
Measure a sampler’s relative round-off on a parameter box, in the tail norm.
The parameter search accepts an axis when the coefficient tail of every element is small against that element’s own envelope along the axis, the samples on the other axes being separate elements (
_resolve_parameters). No degree can take that tail below the sampler’s own noise, so a tolerance under the floor measured here is unreachable. A floor measured along z does not bound it: the box has corners the path never visits, where an element can be orders smaller than anywhere on the path – a capillary’sln Re n_effat the low-density, wide-bore corner of a tapered gradient, near the long-wavelength crossing ofRe n_eff = 1– while its absolute rounding is the same.The probes are the tensor grid of each range’s two ends and midpoint. At each, a second difference of step
_FLOOR_STEP_FRACTION(1e-9) of the range, taken inward at an end, isolates the noise; it is divided by the element’s largest magnitude over the three probes of its line along the axis, floored atENVELOPE_FLOORof the global envelope as the tail test floors it.- Parameters:
- value_atcallable
values -> array, the sampler ofbuild_expansion(), evaluated at points inside the box only.- namestuple of str
The parameter names, in axis order.
- rangesnumpy.ndarray
(lo, hi)per parameter, shape(n_params, 2), withhi > lo.
- Returns:
- float
The largest relative noise found, dimensionless; zero for a sampler that vanishes at every probe.
Examples
A logarithm a thousand times smaller on the box’s low-density line has a floor about a thousand times larger there, measured against that line’s own values:
>>> import numpy as np >>> from solprop.translate.expansion import parameter_roundoff_floor >>> ranges = np.array([[1e-3, 1.0], [0.0, 1.0]]) >>> names = ("density", "radius") >>> def weight(values): ... scaled = values["density"] * (1 + values["radius"]) ... return np.array([np.log(1 + 1e-4 * scaled)]) >>> def uniform(values): ... return np.array([np.log(1 + 1e-4 * (1 + values["radius"]))]) >>> ratio = parameter_roundoff_floor(weight, names, ranges) / ( ... parameter_roundoff_floor(uniform, names, ranges) ... ) >>> f"{ratio:.0f}" '1000'
- solprop.translate.expansion.reweight_expansion(expansion, paths, *, integrate=False, tol=1e-12, kinks=())[source]#
Reuse coefficients for an unchanged function along covered parameter paths.
The caller must establish that the sampled function is unchanged, including parameters held constant during its original construction. This function checks path coverage and rebuilds scalar weights; it never extrapolates coefficients.
- Parameters:
- expansionParameterExpansion
Coefficients and original sampled parameter ranges.
- pathsMapping of str to ZProfile
New paths in each parameter’s original units, covering the same z span.
- integratebool, optional
Integrate weights from the start of the span, for a cumulative operator.
- tolfloat, optional
Scalar interpolation tolerance, positive and dimensionless.
- kinkssequence of float or numpy.ndarray, optional
Mandatory scalar-table breakpoints in m.
- Returns:
- ParameterExpansion or None
New weights sharing the exact terms array, or None when a parameter is missing, newly varying, or outside its original range. The reason is logged.