solprop.boundaries#
Absorbing boundaries as absorption rates per unit distance.
Both axes of the grid are finite and periodic, so energy that reaches the edge of the
frequency or the time window does not leave the simulation – it wraps around, and the
answer is wrong. The collars of solprop.grid mark where the propagation is
absorbed instead of modelled; this module turns them into rates.
Multiplying the solution by the collar profile W once per accepted step would make
the total absorption W^n for however many steps the adaptive controller happened to
take. The answer would then depend on rtol: refining the tolerance sixteen-fold
erodes the spectral wings fifty-fold, and the limit it approaches is not the taper the
profile describes but a brick wall at the edge of the flat region. As a rate it is
exact instead, because traversing a distance attenuates by a factor that depends only on
the distance. The spectral collar becomes
the rate that reproduces one multiplication by the window over the reference length, and the temporal collar becomes the bounded profile
whose integral over the collar is what the capped logarithm gave and whose largest
value is a fifth of the cap (TEMPORAL_ALPHA_LENGTH).
alpha is a power coefficient, as everywhere else in solprop: power falls as
exp(-alpha z), the field as exp(-alpha z / 2), and the linear operator carries
-alpha/2 (docs/dev/conventions.md section 2). The factor of two in the spectral
definition is what makes exp(-alpha ell / 2) = W – one reference length applies the
historical profile to the field exactly once, which is how the profile was used. The
reference length is ell = length / boundary_N by default: apply the window
boundary_N times over the whole propagation.
The two rates are applied by different mechanisms, each chosen because it is exact
(see docs/dev/decisions.md):
alpha(omega)is diagonal in omega, so it is folded into the linear operator before the cumulative operator is integrated. The propagator then applies it exactly over whatever sub-interval the stepper takes, at no cost.alpha(t)is diagonal in t and cannot ride the propagator. It is applied as the term-alpha(t) E(t) / 2inside every stage evaluation (nonlinear_absorber_term()), the default because the alternative – the split-step factorexp(-alpha(t) h / 2)after each accepted step (split_step_factor()), exact for the absorber alone because the factors telescope – was measured to be a first-order splitting of the whole. Both are here.The hard band limit outside the active mask stays a 0/1 mask (
apply_band_mask()), which is idempotent and never had the problem.
Two lengths set the collars’ strengths (SEP 22 section 4.6). reference_length
sets the spectral rate and the ceiling every spectral rate is validated against;
collar_length, reference_length unless given, sets the in-stage temporal collar,
12.6 (1 - W(t)) / ell_c, and its ceiling; a free-space run’s transverse boundary is
designed by SpatialAbsorber (SEP 40). A custom spectral rate
(custom_absorber()) can then be as strong as
a measurement needs without moving the explicit in-stage collars or their stable step,
and a temporal collar sized from a run’s walk-off (walkoff of absorber())
leaves the spectral rate where it was.
The manual page is docs/guide/numerics/boundaries.md.
Module Attributes
Default absorber strength: applications of the collar profile over the propagation. |
|
Cap on the spectral |
|
Depth of the temporal absorber at the edge of the time window, as |
|
Grid samples a collar edge must span before it counts as a taper. |
|
Field attenuation, in nepers, a walking packet must accumulate crossing the collar. |
|
|
|
The share of the energy the split collar's crossing limit may set aside: the fastest-walking light holding at most this much does not bound the step ( |
|
Walk-off rate buckets of |
|
The fastest bucket's lower edge, s/m: a group index 30 from the frame's, beyond any propagating light. |
|
How far DOPRI5's stability region reaches along the negative real axis. |
Functions
|
Build the absorbing boundaries of a grid. |
|
Zero a spectrum outside the active band. |
|
Apply the temporal absorber to a time-domain field over one accepted step. |
|
Convert a collar profile into a power absorption coefficient by its logarithm. |
|
Add a user's spectral power rate to a collar, keeping its in-stage collars. |
|
Build boundaries with both soft collars off and the hard band mask kept. |
|
Return the temporal absorber as a term to add inside the nonlinear evaluation. |
|
Return each |
|
Return the longest step the split collar holds a walking pulse to, m. |
|
Return the |
|
Return the factor the temporal absorber multiplies the field by over one step. |
|
Return the longest step an explicit method can take with an in-stage collar. |
|
Convert the temporal collar into a bounded power absorption coefficient. |
|
Warn if the absorber is too weak for the walk-off the run can produce. |
Classes
|
The absorbing boundaries of one grid, as rates per unit distance. |
|
The split temporal collar's crossing limit, read from where the light is. |
- class solprop.boundaries.Absorber(spectral_rate, temporal_rate, temporal_rate_fine, band_mask, reference_length, collar_length=None, temporal=True, user_rate=None)[source]#
Bases:
objectThe absorbing boundaries of one grid, as rates per unit distance.
- Attributes:
- spectral_ratenumpy.ndarray
Total power absorption coefficient on the coarse frequency axis, 1/m, shape
(n_omega,): the collar’s and any user’s rate (user_rate), the one rate the linear operator folds in as-alpha/2. The collar’s part is exactly zero in the flat region and outside the active mask, where the linear operator is zero anyway.- temporal_ratenumpy.ndarray
Power absorption coefficient on the coarse time axis, 1/m, shape
(n_t,).- temporal_rate_finenumpy.ndarray
The same on the fine time axis, 1/m, shape
(n_to,), for a response that is evaluated there.- band_masknumpy.ndarray
The hard band limit, shape
(n_mask,)matchingspectral_rate: true where the physics lives. Applied as a 0/1 multiply, which is idempotent.- reference_lengthfloat
The length
ellin metres over which the collar profile is applied once to the field. It sets the total spectral rate’s ceiling,60 / ell.- collar_lengthfloat or None
The length
ell_cin metres that sets the in-stage temporal collar’s strength, whose largest rate is12.6 / ell_c; it also sets the temporal rates’ ceiling,60 / ell_c. A free-space run’s real-space collar is designed separately (SpatialAbsorber, SEP 40).None, the default, means reference_length, as for every absorber before SEP 22; a value equal to reference_length is stored asNone, so that the record carries it only where the two differ. Left out of the run record atNone.- temporalbool
Whether the collar has a temporal part.
Falsefor a spectral-only collar (absorber()withtemporal=False), whose temporal rates are zero arrays; it is recorded only whenFalse. Any collar whose temporal rates are all zero, however it was built, is translated to no temporal absorber (solprop.translate.masks.temporal_rates()).- user_ratenumpy.ndarray or None
The user’s share of spectral_rate, 1/m, shape
(n_omega,), with0 <= user_rate <= spectral_rate: whatcustom_absorber()added, whichabsorbed_powerreports apart from the collar. It moves no bit of the operator, which reads the total.None, the default, is no user’s rate, and is left out of the run record, so a run without one keeps its record and its hash; set, it is recorded, and two splits of one total hash apart.
- Raises:
- GridError
If a length is not positive and finite, the mask and the spectral rate disagree in shape, a rate is negative, not finite, or above its ceiling, a collar declared spectral-only carries a temporal rate, or the user’s share is not finite, misshaped, negative or above the total.
Examples
>>> from solprop.boundaries import absorber >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> boundary = absorber(grid) >>> boundary.reference_length 0.075 >>> float(boundary.spectral_rate[grid.inband_mask].max()) 0.0
- band_mask: Bool[ndarray, 'n_mask']#
- property in_stage_length: float[source]#
Return the length that sets the in-stage collars’ strength.
- Returns:
- float
collar_length, or reference_length when it is
None, m.
Examples
>>> from solprop.boundaries import absorber >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> absorber(grid).in_stage_length 0.075
- spectral_rate: Float[ndarray, 'n_omega']#
- temporal_rate: Float[ndarray, 'n_t']#
- temporal_rate_fine: Float[ndarray, 'n_to']#
- solprop.boundaries.DEFAULT_BOUNDARY_N: Final = 20#
Default absorber strength: applications of the collar profile over the propagation.
It sets the reference length
ell = length / boundary_N, so the strength is a physical distance rather than a number of steps.
- solprop.boundaries.MAX_ALPHA_LENGTH: Final = 60.0#
Cap on the spectral
alpha * ell, and the ceiling every rate is validated against.The spectral collar reaches exactly zero at the band edge and
-2 log 0is an infinity, which would make the operatorinf * 0somewhere and hence a NaN. At 60 the field is attenuated byexp(-30), about 1e-13, over one reference length – far below anything a propagation can represent, so the cap costs nothing and keeps every rate finite. The temporal rate is bounded by construction atTEMPORAL_ALPHA_LENGTH, well inside this ceiling: were it the same capped logarithm, then under the in-stage absorber term the cap would set the stability limit and the step count.
- solprop.boundaries.MIN_COLLAR_ATTENUATION: Final = 6.907755278982137#
Field attenuation, in nepers, a walking packet must accumulate crossing the collar.
log(1e3): the field falls by a thousand and the power by sixty decibels. Below thatwarn_walkoff()says the absorber is too weak for the walk-off the run can produce.
- solprop.boundaries.MIN_COLLAR_SAMPLES: Final = 4#
Grid samples a collar edge must span before it counts as a taper.
With fewer the discrete profile steps from one to zero rather than tapering, the rate jumps to its cap in a single sample, and the boundary is the brick wall the rate form exists to avoid. A temporal collar built naively is exactly this degenerate for a
trangethat lands on a power of two; solprop’s grids guarantee a minimum width, and this is the guard that says so.
- solprop.boundaries.SPLIT_CROSSING: Final = 0.5#
cof the split collar’s step limith <= c w / max |d tau / dz|: no step carries light more than half the temporal collar’s width (split_crossing_step()).The
"split_step"scheme damps the collar once per accepted step, at the step’s end, so light that walks through the collar within one step wraps round the periodic window into the interior undamped, and on a linear stretch the error estimate sees nothing. At half a width a crossing is damped at two step ends at least; the same factor as SEP 40’s adaptive transverse clamp (solprop.spatial_absorber.CROSSING_ADAPTIVE).
- solprop.boundaries.SPLIT_OCCUPIED_SHARE: Final = 1e-06#
The share of the energy the split collar’s crossing limit may set aside: the fastest-walking light holding at most this much does not bound the step (
SplitCrossing). Were all of it to wrap round the window undamped, the field error would be its square root, the boundary monitor’s default budget.
- solprop.boundaries.SPLIT_RATE_BUCKETS: Final = 128#
Walk-off rate buckets of
SplitCrossing, logarithmic fromSPLIT_RATE_TOPdown nine decades: each bucket’s rate is its upper edge, at most \(10^{9/128}\), 18 %, above the rates it holds, and the open first bucket’s is the fastest rate the bins reach, so the limit is never longer than the bins’ own would give.
- solprop.boundaries.SPLIT_RATE_TOP: Final = 1e-07#
The fastest bucket’s lower edge, s/m: a group index 30 from the frame’s, beyond any propagating light. The fastest bucket is open above it and is counted at the fastest rate a run’s bins reach (
SplitCrossing.fastest).
- solprop.boundaries.STABILITY_RADIUS: Final = 3.3#
How far DOPRI5’s stability region reaches along the negative real axis.
The textbook value for both order-5(4) pairs this package carries, and what
stable_step()turns into a step limit for the in-stage absorber. Measured on the 1 mm capillary of the driver tests, whose collar reached 1.2e6 m^-1 under the capped logarithm temporal profile: a fixed schedule diverges ath alpha / 2 = 3.75and completes at 3.13, which brackets it.
- class solprop.boundaries.SpatialAbsorber(kappa=None, angle=None, discard_above=None, attenuation=8.517193191416238, safety_fraction=0.1, real_space=True)#
Bases:
objectThe transverse boundary a free-space run asks for (SEP 40 section 5.2).
A specification, resolved at translation against the grid, the medium and the launch into a
SpatialAbsorberDesign. By default (real_space=True) it asks for a real-space collar in the room the grid has outside its requested size (collar_fraction), as well as the safety ramp;real_space=Falseasks for the ramp alone, which is how a run with nospatial_absorberis resolved.- Attributes:
- kappafloat or None
The protected band’s edge,
|kappa| <= kappa, rad/m.- anglefloat or None
The protected band as an angle in vacuum behind a planar exit face,
sin(theta) = c kappa / omega(SEP 23’s convention), rad, in (0, pi/2): a band that scales with frequency. Neither kappa nor angle takes the band from the launch,c_b sqrt(2) kappa_rmsper axis (LAUNCH_BAND_FACTOR).- discard_abovefloat or None
A wavenumber above which the user declares no physical light, rad/m: the safety ramp then starts there. Never lowered by default.
- attenuationfloat
Gamma_min, the collar’s returned attenuation at the band’s edge, field nepers. Defaultlog(5e3).- safety_fractionfloat
The ramp’s width as a fraction of each axis’s largest wavenumber, in (0, 1). Default 0.1.
- real_spacebool
Whether a real-space collar is asked for. Default
True.
- Raises:
- GridError
If both kappa and angle are given, a value is out of range, or a band is declared with no collar to protect it.
Examples
>>> from solprop.spatial_absorber import SpatialAbsorber >>> SpatialAbsorber(angle=25e-3).angle 0.025 >>> SpatialAbsorber(real_space=False, safety_fraction=0.2).real_space False
- class solprop.boundaries.SpatialAbsorberDesign(specification, ramp_rate, ramp_strength, kappa_s, collar_rate=None, alpha0=0.0, width=(), kappa_p=(), kappa_lo=0.0, crossing_step=inf, unguarded_above=inf, check=<factory>, notes=<factory>)#
Bases:
objectA transverse boundary resolved at translation (SEP 40 section 5.2).
Immutable, and part of the translated problem: derived runs share it (
solprop.driver.simulation.Simulation.with_input()), and the design check that belongs to a launch is rerun for a new one. Every derived value is recorded in the run’s output beside the specification, not hashed with it.- Attributes:
- specificationSpatialAbsorber
What the run asked for.
- ramp_ratenumpy.ndarray
The safety ramp’s power rate on the spectral columns, 1/m.
- ramp_strengthfloat
Its peak, 1/m, the larger of the axes’.
- kappa_stuple of float
Where the ramp starts, per axis (one radially), rad/m.
- collar_ratenumpy.ndarray or None
The real-space collar’s power rate on the nodes, 1/m;
Nonewithout one.- alpha0float
The collar’s peak rate, 1/m; zero without one.
- widthtuple of float
The collar’s width per face’s axis, m; empty without one.
- kappa_ptuple of float
The protected band’s edge per axis at the reddest requested frequency, rad/m; empty without a collar.
- kappa_lofloat
The reflection threshold of the strongest face, rad/m; zero without a collar.
- crossing_stepfloat
The adaptive crossing limit, m; infinite without a collar.
- unguarded_abovefloat
The wavenumber above which content could in principle cross within a clamped step,
(0.75 / c_x) kappa_p, rad/m; infinite without a collar.- checkdict
The design check’s record (SEP 40 section 4.4): its status, the launch’s return, the bare wall’s at the same size, the frequency nodes and its wall time; empty until it has run, and without a collar.
- notesdict
Further derived values for the record: the intermediate band’s report, the reflected QDHT columns, the launch’s share in the ramp’s band.
- property has_collar: bool#
Return whether the design carries a real-space collar.
- Returns:
- bool
True when
collar_rateis set.
- ramp_rate: Float[ndarray, 'n_columns']#
- record()#
Return the design’s derived values for the run’s record.
- Returns:
- dict
JSON-ready scalars and tuples: the ramp’s start per axis and strength, and with a collar its peak rate, widths, band edges, threshold, crossing step and unguarded band, then the design check’s record and the notes. The rates on the nodes and columns are in the operator’s own record.
- specification: SpatialAbsorber#
- class solprop.boundaries.SplitCrossing(buckets, width, launch_limit=inf, fastest=1e-07, factor=0.5, share=1e-06)[source]#
Bases:
objectThe split temporal collar’s crossing limit, read from where the light is.
Under the
"split_step"scheme a step whose walk-off exceeds the collar’s width can carry light through it and round the periodic window undamped. Each(mode, frequency)bin walks at|d(beta - beta_frame)/d omega|, held here as its bucket (split_rate_buckets()); a state’s occupancy is its energy per bucket, and the limitfactor * width / ratetakes the rate of the fastest bucket left once the fastest holding at mostshareof the energy are set aside. Translation reads it from the launch (launch_limit), which holdsdz_maxand a fixed schedule; the boundary monitor reads it again from the state at every check, and the steps after it are capped there.- Attributes:
- bucketsnumpy.ndarray
Each bin’s bucket,
uint8, shape(n_modes, n_omega): the fastest of its batch members, polarisations and positions along the guide; evanescent and inactive bins in the last.- widthfloat
The collar’s width at its narrower edge, s.
- launch_limitfloat
The limit the launch’s occupancy gives, m.
- fastestfloat
The fastest rate a bin reaches, s/m: the open first bucket’s rate when above
SPLIT_RATE_TOP, so that no bin is counted slower than it walks.- factorfloat
c,SPLIT_CROSSING.- sharefloat
Examples
>>> import numpy as np >>> from solprop.boundaries import SplitCrossing, split_rate_buckets >>> buckets = split_rate_buckets(np.array([[1e-12, 1e-10]])) >>> crossing = SplitCrossing(buckets, 1e-13, launch_limit=0.05) >>> occupancy = crossing.occupancy(np.array([[[[1.0, 1e-4]]]], dtype=complex)) >>> # The fast bin holds 1e-8 of the energy and is set aside: the slow bin's limit, >>> # 50 mm at 1e-12 s/m, a little shorter at its bucket's edge. >>> round(crossing.limit(occupancy) * 1e3, 1) 49.1
- buckets: UInt8[ndarray, 'n_modes n_omega']#
- limit(occupancy)[source]#
Return the crossing limit of an occupancy, m.
- Parameters:
- occupancynumpy.ndarray
Energy per bucket (
occupancy()).
- Returns:
- float
factor * width / rateat the fastest bucket left once the fastest holding at mostshareof the energy are set aside;inffor no light.
- occupancy(field_spectrum, columns=None)[source]#
Return a state’s energy per bucket, on the host: the reference reduction.
- Parameters:
- field_spectrumnumpy.ndarray
A state, canonical layout.
- columnsnumpy.ndarray or None, optional
The modes’ quadrature weights,
Nonefor unit weights.
- Returns:
- numpy.ndarray
Shape
(SPLIT_RATE_BUCKETS,), in the state’s units of energy.
- solprop.boundaries.TEMPORAL_ALPHA_LENGTH: Final = 12.6#
Depth of the temporal absorber at the edge of the time window, as
alpha * ell.The temporal rate is
TEMPORAL_ALPHA_LENGTH * (1 - W(t)) / ell, the complement of the collar window (temporal_collar_rate()). At 12.6 its integral over the collar is what-2 log Wcapped atMAX_ALPHA_LENGTHgave – 6.3 per collar width per reference length, both edges, to a few per cent on the grids solprop builds – so the walk-off protection and the meaning ofboundary_Nare unchanged, while the largest rate is a fifth of the capped logarithm’s: the stability limit of the in-stage absorber term (stable_step()) is 4.8 times longer, and the error estimate does not see a rate of60 / ellon the collar’s last samples, which would cost the reduced M2 case 40 % of its steps. The capped logarithm puts 73 % of each edge’s attenuation in its outer 14 %; this profile spreads it over the edge, so a case whose field reaches the collar is absorbed earlier and answers differently. At the very edge the field falls byexp(-6.3), 1.8e-3, per reference length.
- solprop.boundaries.absorber(grid, *, boundary_N=20, boundary_length=None, temporal=True, walkoff=None)[source]#
Build the absorbing boundaries of a grid.
- Parameters:
- gridTimeGrid
The grid, for its collars, its masks and its propagation length.
- boundary_Nfloat, optional
Absorber strength, as the number of times the collar profile is applied to the field over the whole propagation; the reference length is
length / boundary_N. Besidewalkoffit sets the spectral collar alone.- boundary_lengthfloat or None, optional
Reference length in metres, overriding
boundary_N. Besidewalkoffit sets the spectral collar alone.- temporalbool, optional
Falsebuilds a spectral-only collar (SEP 22): its temporal rates are zero arrays on both time axes, the band mask and the spectral rate are as before, and the record saystemporal=False. It is translated to no temporal absorber, so the kernels compile no temporal pass under either absorber scheme. The grid is unchanged.- walkofffloat or None, optional
The steady group-delay drift, in seconds and positive, that the temporal collar must stop: a packet drifting by this much over
grid.lengthlosesMIN_COLLAR_ATTENUATIONnepers of field crossing the collar, counted over both edges, which is the conditionwarn_walkoff()checks. It setscollar_lengthto6.3 w L / (Gamma walkoff)for edges of widthw, a lengthLand that attenuationGamma, and changes nothing else;None, the default, leaves the temporal strength toboundary_N.It is measured over this grid’s own length. The collar’s rate depends only on the speed
walkoff / grid.length, so a slice of a run gets its parent’s temporal collar when given the drift of that slice: a quarter of the length, a quarter of the walk-off. Handing a slice its parent’s whole drift asks for a collar four times stronger.It does not conflict with
boundary_Norboundary_length, so neither is refused beside it. They keepreference_length, the spectral rate and its ceiling, andwalkoffsets the temporal rates and their ceiling; a slice that keeps its parent’s strength in both passesboundary_length=parent.reference_lengthand its own drift.Refused beside
temporal=False, which has no temporal collar to size. The window does not bound it: the loss needs only the collar’s width, and a drift longer than the window is absorbed on its first crossing. A large value costs steps instead, since an explicit method’s step is limited to about0.48 w / v, bystable_step()under the in-stage scheme. A free-space run’s transverse collar is designed separately and does not read this length.
- Returns:
- Absorber
The spectral and temporal rates, the hard band mask and the reference length.
- Raises:
- GridError
If the reference length is not positive and finite, either collar is too narrow on its axis to be a taper, or
walkoffis not positive and finite or is given withtemporal=False.
Examples
>>> import numpy as np >>> from solprop.boundaries import absorber >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> boundary = absorber(grid, boundary_N=20) >>> boundary.reference_length 0.075 >>> field_left = np.exp(-boundary.spectral_rate * boundary.reference_length / 2) >>> active = grid.active_mask >>> bool(np.allclose(field_left[active], grid.omega_window[active])) True >>> spectral_only = absorber(grid, temporal=False) >>> float(spectral_only.temporal_rate.max()), spectral_only.temporal (0.0, False) >>> walking = absorber(grid, walkoff=1e-12) >>> bool(np.array_equal(walking.spectral_rate, boundary.spectral_rate)) True >>> round(walking.in_stage_length / boundary.in_stage_length, 3) 1.023
- solprop.boundaries.apply_band_mask(spectrum, band_mask)[source]#
Zero a spectrum outside the active band.
The hard part of the boundary, kept as a 0/1 multiply rather than made a rate: it is idempotent, so applying it once per accepted step never accumulates, and it is the definition of the band the equation is solved on rather than an absorber.
- Parameters:
- spectrumnumpy.ndarray
Spectral amplitudes, shape
(..., n_omega).- band_masknumpy.ndarray
True where the physics lives, shape
(n_omega,).
- Returns:
- numpy.ndarray
The masked spectrum, with the shape of
spectrum.
Examples
>>> import numpy as np >>> from solprop.boundaries import apply_band_mask >>> mask = np.array([True, False, True]) >>> apply_band_mask(np.array([1.0, 2.0, 3.0]), mask).tolist() [1.0, 0.0, 3.0]
- solprop.boundaries.apply_split_step(field, temporal_rate, step)[source]#
Apply the temporal absorber to a time-domain field over one accepted step.
The reference implementation of the split-step absorber scheme, the alternative to the default in-stage term of
nonlinear_absorber_term(), and what that term is measured against.- Parameters:
- fieldnumpy.ndarray
Time-domain field, shape
(..., n_t); real on a field grid, complex on an envelope grid.- temporal_ratenumpy.ndarray
Power absorption coefficient on the time axis, 1/m, shape
(n_t,).- stepfloat
Distance travelled in metres.
- Returns:
- numpy.ndarray
The attenuated field, with the shape of
field.
Examples
>>> import numpy as np >>> from solprop.boundaries import apply_split_step, collar_rate >>> alpha = collar_rate(np.array([1.0, 0.5]), 0.1) >>> np.round(apply_split_step(np.ones((1, 2)), alpha, 0.1), 6).tolist() [[1.0, 0.5]]
- solprop.boundaries.collar_rate(window, reference_length)[source]#
Convert a collar profile into a power absorption coefficient by its logarithm.
The spectral collar’s rate; the temporal collar takes the bounded profile of
temporal_collar_rate()instead.alpha = -2 log(W) / ell, so that propagating throughellattenuates the field by exactlyW: the collar profiles were multiplied into the field, and this is the rate that reproduces one such multiplication over the reference length. Exactly zero whereW == 1, and capped atMAX_ALPHA_LENGTH / ellwhere the profile reaches zero.- Parameters:
- windownumpy.ndarray
Collar profile in
[0, 1], one in the interior and falling to zero at the boundary, shape(n,).- reference_lengthfloat
The length in metres over which the profile is applied once; positive, finite.
- Returns:
- numpy.ndarray
Power absorption coefficient in 1/m, shape
(n,), non-negative and finite.
- Raises:
- GridError
If the reference length is not positive and finite, or the profile leaves
[0, 1].
Examples
>>> import numpy as np >>> from solprop.boundaries import collar_rate >>> alpha = collar_rate(np.array([1.0, 0.5, 0.0]), 0.1) >>> np.round(alpha, 6).tolist() [0.0, 13.862944, 600.0] >>> float(np.exp(-alpha[1] * 0.1 / 2)) 0.5
- solprop.boundaries.custom_absorber(grid, spectral_rate, *, collar=None)[source]#
Add a user’s spectral power rate to a collar, keeping its in-stage collars.
The spectral rate is diagonal in frequency and folded into the linear operator, where it is exact and free whatever its strength (ADR-007); the in-stage collars are explicit and their strength bounds a replayed step (SEP 22 section 4.6). The user’s rate is therefore added to the collar’s spectral rate, kept apart as
user_ratefor the statistic that reports it, and the collar’s temporal rates, band mask and in-stage strength are kept as they are: itscollar_lengthis the collar’s ownin_stage_length. Itsreference_lengthstays the collar’s when the summed rate fits under the collar’s ceiling60 / reference_length, and otherwise becomes60over the sum’s largest value, the longest length that admits it. The collar length is recorded only where it differs from the reference length, so a zero user rate returns a collar equal to the one given, field for field.- Parameters:
- gridTimeGrid
The grid the collar is for, for its frequency axis.
- spectral_ratefloat, numpy.ndarray or callable
The user’s power absorption coefficient, 1/m: a scalar, an array of shape
(n_omega,)on the grid’s coarse frequency axis, or a callable of the angular frequencyomegain rad/s (the grid’s own axis, an envelope grid’s in FFT order) returning that array. Finite and non-negative; it is zeroed outside the collar’s band mask, where the operator is zero.- collarAbsorber or None, optional
The collar it is added to, taken by value;
Noneisabsorber()of the grid with its defaults. Any collar is accepted as it is, a spectral-only one (absorber(grid, temporal=False)) among them.
- Returns:
- Absorber
The collar with the summed spectral rate, and the user’s share of it in
user_rate, added to any share collar already holds.
- Raises:
- GridError
If the rate is not finite and non-negative, or not of the frequency axis’s shape.
Examples
>>> import numpy as np >>> from solprop.boundaries import absorber, custom_absorber >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> collar = absorber(grid) >>> same = custom_absorber(grid, 0.0, collar=collar) >>> same.reference_length == collar.reference_length, same.collar_length (True, None) >>> strong = custom_absorber(grid, 1e5, collar=collar) >>> strong.reference_length < collar.reference_length True >>> strong.in_stage_length == collar.in_stage_length True >>> bool(np.array_equal(strong.temporal_rate, collar.temporal_rate)) True
- solprop.boundaries.no_absorber(grid, *, boundary_N=20, boundary_length=None)[source]#
Build boundaries with both soft collars off and the hard band mask kept.
What
absorber=Falseon the keyword interfaces means (SEP 21 section 5): the temporal and spectral rates are zero, so no light is absorbed at either edge of the window, and the hard band mask still keeps the field inside the representable band. The reference length is kept for the record; a free-space run’s transverse boundary is designed separately (SpatialAbsorber) and does not read it.- Parameters:
- gridTimeGrid
The grid, for its masks and its propagation length.
- boundary_Nfloat, optional
As for
absorber(); it sets only the reference length here.- boundary_lengthfloat or None, optional
Reference length in metres, overriding
boundary_N.
- Returns:
- Absorber
Zero spectral and temporal rates, the band mask and the reference length, recorded as
temporal=False.
Examples
>>> from solprop.boundaries import no_absorber >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> boundary = no_absorber(grid) >>> float(boundary.spectral_rate.max()), float(boundary.temporal_rate.max()) (0.0, 0.0) >>> bool(boundary.band_mask.any()) True
- solprop.boundaries.nonlinear_absorber_term(field, temporal_rate)[source]#
Return the temporal absorber as a term to add inside the nonlinear evaluation.
-alpha(t) E(t) / 2: the derivative form of the same rate, and the reference implementation of the default in-stage absorber scheme. Integrating it exactly over a step would giveapply_split_step(); a Runge-Kutta stage integrates it to the order of the method, so the two agree to first order in the step and differ at second, which is what the comparison of the two schemes measures.- Parameters:
- fieldnumpy.ndarray
Time-domain field, shape
(..., n_t).- temporal_ratenumpy.ndarray
Power absorption coefficient on the time axis, 1/m, shape
(n_t,).
- Returns:
- numpy.ndarray
The contribution to
dE/dzin the time domain, with the shape offield.
Examples
>>> import numpy as np >>> from solprop.boundaries import collar_rate, nonlinear_absorber_term >>> alpha = collar_rate(np.array([0.8, 0.5]), 0.1) >>> np.round(nonlinear_absorber_term(np.ones(2), alpha), 6).tolist() [-2.231436, -6.931472]
- solprop.boundaries.split_bucket_rates(fastest=1e-07)[source]#
Return each
SplitCrossingbucket’s rate, its upper edge, s/m.- Parameters:
- fastestfloat, optional
The fastest rate the bins reach, s/m: the open first bucket’s rate when it exceeds
SPLIT_RATE_TOP.
- Returns:
- numpy.ndarray
Shape
(SPLIT_RATE_BUCKETS,), the fastest first.
Examples
>>> from solprop.boundaries import split_bucket_rates >>> rates = split_bucket_rates() >>> float(rates[0]), len(rates) (1e-07, 128)
- solprop.boundaries.split_crossing_step(rate, width, factor=0.5)[source]#
Return the longest step the split collar holds a walking pulse to, m.
factor * width / rate: a component whose group delay walksrateseconds per metre against the frame movesfactorof the collar’swidthin one step at most, so it cannot cross the collar between two of the split’s applications. Translation holds an adaptive run’sdz_maxto it and refuses a fixed schedule past it, under the"split_step"scheme; the in-stage scheme absorbs inside every stage and needs no such limit.- Parameters:
- ratefloat
The fastest walk-off against the frame, s/m, nonnegative: the rate
SplitCrossing.limit()reads from an occupancy.- widthfloat
The temporal collar’s width at its narrower edge, s.
- factorfloat, optional
c, the share of the width one step may walk;SPLIT_CROSSING.
- Returns:
- float
The step, m;
infwhen nothing walks.
Examples
>>> from solprop.boundaries import split_crossing_step >>> round(split_crossing_step(1.7e-12, 212e-15), 4) 0.0624 >>> split_crossing_step(0.0, 212e-15) inf
- solprop.boundaries.split_rate_buckets(rates)[source]#
Return the
SplitCrossingbucket of each walk-off rate.Bucket
kholds the rates up toSPLIT_RATE_TOP * 10**(-9 k / 128), the fastest first; a rate of zero, or below the last edge, is in the last bucket, and one aboveSPLIT_RATE_TOPin the first, which is open-ended.- Parameters:
- ratesnumpy.ndarray
Walk-off rates against the frame, s/m, nonnegative.
- Returns:
- numpy.ndarray
The buckets,
uint8, the shape of rates.
Examples
>>> import numpy as np >>> from solprop.boundaries import split_rate_buckets >>> split_rate_buckets(np.array([1.0, 1e-7, 5e-8, 0.0])).tolist() [0, 0, 4, 127]
- solprop.boundaries.split_step_factor(temporal_rate, step)[source]#
Return the factor the temporal absorber multiplies the field by over one step.
exp(-alpha(t) h / 2): the field, not the power, because that is what the state holds, andalphais a power coefficient. Successive factors telescope exactly toexp(-alpha(t) L / 2)however the solver subdivides the propagation, which is the whole point of rate semantics – and the property the tests assert.The factor of one half is required:
exp(-alpha(t) h)could not be right alongside the-alpha(t) E(t) / 2of the alternative scheme, or alongsidealpha = -2 log W / ell: with it, one reference length would apply the collar profile to the field twice, and the two schemes would differ at first order in the step rather than agreeing there.- Parameters:
- temporal_ratenumpy.ndarray
Power absorption coefficient on the time axis, 1/m, shape
(n_t,).- stepfloat
Distance travelled in metres, non-negative.
- Returns:
- numpy.ndarray
Multiplicative factor in
(0, 1], shape(n_t,).
- Raises:
- GridError
If the step is negative or not finite.
Examples
>>> import numpy as np >>> from solprop.boundaries import collar_rate, split_step_factor >>> alpha = collar_rate(np.array([1.0, 0.5]), 0.1) >>> np.round(split_step_factor(alpha, 0.1), 6).tolist() [1.0, 0.5]
- solprop.boundaries.stable_step(temporal_rate)[source]#
Return the longest step an explicit method can take with an in-stage collar.
The
"nonlinear"absorber scheme adds-alpha(t) E(t) / 2to the derivative, so the collar’s largest rate becomes an eigenvalue of the linearised problem and an explicit Runge-Kutta method must keeph alpha / 2inside its stability region. The split factor ofsplit_step_factor()integrates the same term exactly and is unconditionally stable, which is the one thing it is better at.DOPRI5’s and Tsit5’s stability region reaches
STABILITY_RADIUSalong the negative real axis, so the limit is2 R / max(alpha). Measured on the 1 mm capillary of the driver tests, whose collar reached 1.2e6 m^-1 under the capped logarithm then in force: a fixed schedule diverges ath alpha / 2 = 3.75and completes at 3.13, which brackets the textbook 3.3. With the bounded profile the largest rate isTEMPORAL_ALPHA_LENGTH / ell, so the limit is0.52 ell.The rate reaches its ceiling only at the very edge of the window, where the field is by design negligible, so an adaptive run is not usually held at this limit – the error norm is scaled by the state and an enormous derivative on an empty bin scores nothing. What the limit binds is a replayed schedule, and a floor dz_min set above it.
- Parameters:
- temporal_ratenumpy.ndarray
The collar’s power absorption coefficient on the time axis, 1/m, shape
(n_t,).
- Returns:
- float
The longest stable step in m, or
infwhen the collar is identically zero.
Examples
>>> import numpy as np >>> from solprop.boundaries import stable_step >>> round(stable_step(np.array([0.0, 6.6, 3.3])), 6) 1.0 >>> stable_step(np.zeros(4)) inf
- solprop.boundaries.temporal_collar_rate(axis, collar, reference_length)[source]#
Convert the temporal collar into a bounded power absorption coefficient.
alpha(t) = TEMPORAL_ALPHA_LENGTH * (1 - W(t)) / ell: exactly zero over the flat region, rising with every derivative continuous as the Planck window falls, and reachingTEMPORAL_ALPHA_LENGTH / ellat the edge of the window. The logarithm ofcollar_rate()is the right rate for the spectral collar, which rides the linear operator for free; for the temporal collar it would be a stiff term in every stage of the in-stage absorber, its cap would set the stability limit and the step count, and it puts nearly three quarters of the attenuation in the outer seventh of each edge. This profile carries the same integrated attenuation with a largest rate a fifth of the cap.- Parameters:
- axisnumpy.ndarray
Time axis to evaluate the rate on, s, shape
(n,).- collarsolprop.grid.Collar
The temporal collar geometry.
- reference_lengthfloat
The length
ellin metres; positive, finite.
- Returns:
- numpy.ndarray
Power absorption coefficient in 1/m, shape
(n,), in[0, TEMPORAL_ALPHA_LENGTH / ell].
- Raises:
- GridError
If the reference length is not positive and finite.
Examples
>>> import numpy as np >>> from solprop.boundaries import TEMPORAL_ALPHA_LENGTH, temporal_collar_rate >>> from solprop.grid import Collar >>> collar = Collar(-2.0, -1.0, 1.0, 2.0) >>> rate = temporal_collar_rate(np.array([-2.0, 0.0, 2.0]), collar, 0.1) >>> rate.tolist() == [TEMPORAL_ALPHA_LENGTH / 0.1, 0.0, TEMPORAL_ALPHA_LENGTH / 0.1] True
- solprop.boundaries.warn_walkoff(grid, group_delay_span, *, boundary_N=20, boundary_length=None)[source]#
Warn if the absorber is too weak for the walk-off the run can produce.
A packet whose group delay drifts by
group_delay_spanover the whole propagation crosses the temporal collar atgroup_delay_span / lengthseconds per metre, so it spendsdt / vmetres at each sample and accumulates a field attenuation of(length / group_delay_span) * integral(alpha) / 2nepers on the way through. BelowMIN_COLLAR_ATTENUATIONthe packet reaches the edge of the window still alive, wraps around, and aliases onto the rest of the field.The span is the caller’s, deliberately. A prediction from the group delay across the whole simulation band would, on a grid running to four microns, be dominated by band edges carrying nothing but shot noise, and so would fire on almost every run. A caller who passes the span of the band it cares about does not have that problem (
docs/dev/stepper.mdsection 6).- Parameters:
- gridTimeGrid
The grid, for its temporal collar and its propagation length.
- group_delay_spanfloat
Spread of group delay accumulated over the propagation, in seconds. Zero or negative means nothing walks anywhere and nothing is checked.
- boundary_Nfloat, optional
Absorber strength, as in
absorber().- boundary_lengthfloat or None, optional
Reference length in metres, overriding
boundary_N.
- Raises:
- GridError
If the span is not finite, or the reference length is invalid.
- Warns:
- SolpropWarning
If a packet drifting by that span would survive the collar.
Examples
>>> import warnings >>> from solprop.boundaries import warn_walkoff >>> from solprop.grid import FieldGrid >>> grid = FieldGrid.from_limits(1.5, 800e-9, (90e-9, 4e-6), 400e-15) >>> with warnings.catch_warnings(): ... warnings.simplefilter("error") ... warn_walkoff(grid, 50e-15)