solprop.backends.numpy_ref.general_modal#

The general modal guide’s nonlinear source, on the reference backend.

This is a second execution path beside solprop.backends.numpy_ref.passes, not an extension of it: the legacy pipeline synthesises with one real static matrix in the time domain, and a general guide’s synthesis is a complex, frequency-dependent map of three Cartesian components, so it happens in the frequency domain instead.

The pipeline of one stage, for a chunk of quadrature nodes:

  1. Scale. Multiply the coarse channel spectra by the pad scale n_to / n_t, which keeps the two time-domain signals equal at coincident samples (conventions.md section 3), and by the analytic-signal doubling. Both are applied here and not folded into S, because S is the declared physical profile in V/(m sqrt(W)) that the guide’s provider declares.

  2. Synthesise. E(q, c, w) = sum_m S(q, c, m, w) a(m, w) at the chunk’s nodes, on the coarse grid the state lives on, placed into the fine spectrum at grids.crop_index; then one complex inverse transform per node and component of that doubled one-sided spectrum, which is the analytic signal E_a of conventions.md section 4 exactly: E(t) = Re E_a(t) and |E_a|^2 is the instantaneous power. A complex profile can only act on an analytic signal. Synthesising before the padding rather than after it is the same arithmetic – every fine bin outside crop_index multiplies a zero amplitude – over a map n_omega_fine / n_omega times smaller.

  3. Respond. P_i = eps0 sum_jkl A(q)_ijkl E_j E_k conj(E_l) per node, with A(q) the sum of chi3 T over the responses of that node’s material region (solprop.translate.responses.isotropic_kerr_tensor()). A node whose region names no response has no polarisation. Which field E is here is the record’s own declaration, read back from its model_id: the real field Re E_a for the real cubic P = eps0 chi3 (E.E) E, which is the legacy field route’s model and radiates the third harmonic, or the analytic signal itself for P = eps0 chi3 (2 |E|^2 E + (E.E) E*) / 4, which carries the fundamental band alone. The tensor carries the normalisation that belongs to each. The analytic form is contracted densely, over the pair history E_k conj(E_l) and then j. The real cubic is symmetric in (j, k, l), so it is evaluated as the symmetric contraction (symmetric_cubic()): ten monomials (E_j E_k) E_l with j <= k <= l per sample, against coefficients that sum each node’s tensor over each monomial’s orderings (symmetric_kerr_coefficients()), formed once per node and evaluation. That is exact for any tensor, and it is the reduction order every backend runs.

  4. Project. The physical polarisation is Re P_a, so its one-sided spectrum – the same real forward transform every other route takes – is what drives the one-sided state. Contract it with Q on the coarse grid and multiply by the record’s source_frequency_factor, -i omega.

The crop scale n_t / n_to stays in responses.prefactor, where it is on every route, and the spectral collar and the interaction picture are applied by the shared solprop.backends.numpy_ref.passes.stage_output().

The delayed response. A material region may declare the silica tensor Raman model beside its Kerr response (solprop.nonlinear.raman.SilicaTensorResponse), and step 3 then adds its polarisation: six real symmetric pair histories Q_kl = E_k E_l per node, convolved on a doubled axis with that response’s trace and rank-two kernels, and contracted with chi3 D[term] and the local field. The instantaneous tensor already carries the 1 - f_R that leaves, so the two shares add to the region’s one chi3.

What this route does not do. The analytic-signal form radiates no third harmonic, which is why it is not the route’s default: it remains selectable, and is the tested oracle for the fundamental band that the real cubic is measured against.

Memory in the real form. The chunk buffers of step 3 are float64 rather than complex128 when the field is real, and the symmetric contraction holds six pair products, a monomial and two component-wide sums per node and sample in place of the pair history and the contracted tensor (the pair history too when a delayed response needs its drive): at most 22 doubles against the 36 that general_memory()’s general_node_pair and general_node_weighted rows count for three components, so those rows are an upper bound in that form. The bound is the one a budget needs and the ledger is not split by form. The symmetric coefficients are formed per chunk from the tensor, n_components * n_monomials doubles a node, a transient like the others the ledger leaves out.

Module Attributes

DELAYED_TERMS

Delayed channels the silica tensor Raman model carries: trace and rank two.

PAIR_HISTORIES

Independent entries of the real symmetric pair history of three components.

BUFFERS_PER_GENERAL_NODE

Chunk buffers the general source holds per node, each n_components wide.

PAIR_BUFFERS_PER_GENERAL_NODE

Chunk buffers the general source holds per node, each n_components squared.

Functions

delayed_polarisation(field, pair, maps, owner)

Return one chunk's delayed polarisation, in C/m^2, from its pair history.

dispersive_polarisation(field, terms, ...)

Return one chunk's dispersive polarisation, in C/m^2, region by region.

expand_pair_history(stored)

Return the full symmetric pair history from its six independent entries.

general_dispersive_buffers(leaf)

Return the buffers per node and component SEP 6's response adds to a chunk.

general_memory(maps, tables, n_to)

Return the bytes the general source holds, one row per named buffer.

general_node_chunk(n_components, n_to, ...)

Return the nodes the general source may hold at once within a byte budget.

general_source(state, maps, crop_index, *, ...)

Evaluate one stage's modal source through the general guide's maps.

node_delayed_maps(tables, regional)

Derive the delayed response's per-node ownership, tensors and kernel spectra.

node_kerr_form(regional)

Return which cubic a compiled per-region record declares.

node_kerr_tensor(tables, regional)

Sum each node's material responses into one susceptibility tensor.

refuse_regional_responses(tables, regional, *)

Refuse a per-region response this route does not execute, before it allocates.

symmetric_cubic(coefficients, field)

Return the real cubic sum_jkl A_ijkl E_j E_k E_l by its symmetric form.

symmetric_kerr_coefficients(tensor)

Sum a per-node tensor over the orderings of each monomial of the real cubic.

symmetric_monomials(n_components)

Return the real cubic's monomials (j, k, l), j <= k <= l, in order.

Classes

GeneralMaps(synthesis, projection, tensor, ...)

The arrays one general modal problem's source pass reads, at one position.

solprop.backends.numpy_ref.general_modal.BUFFERS_PER_GENERAL_NODE = 4#

Chunk buffers the general source holds per node, each n_components wide.

The synthesised fine spectrum, the analytic time field, the polarisation in time and its one-sided spectrum.

solprop.backends.numpy_ref.general_modal.DELAYED_TERMS = 2#

Delayed channels the silica tensor Raman model carries: trace and rank two.

class solprop.backends.numpy_ref.general_modal.GeneralMaps(synthesis, projection, tensor, frequency_factor, node_chunk, form='analytic', node_response=None, delayed_tensor=None, delayed_spectrum=None, synthesis_terms=None, projection_terms=None, dispersive=None, node_region=None)[source]#

Bases: object

The arrays one general modal problem’s source pass reads, at one position.

Derived once at construction from the problem’s leaves. The route propagates constant geometry, so the maps have one interval of one Chebyshev degree along z and there is nothing to evaluate per stage.

Attributes:
synthesisnumpy.ndarray

S on the coarse grid, complex128, shape (n_nodes, n_components, n_modes, n_omega), in V/(m sqrt(W)).

projectionnumpy.ndarray

Q on the coarse grid, complex128, shape (n_modes, n_nodes, n_components, n_omega), in V m/sqrt(W).

tensornumpy.ndarray

The summed chi3 T of each node’s material region, float64, shape (n_nodes, n_components, n_components, n_components, n_components), in m²/V². Zero at a node whose region does not respond.

frequency_factornumpy.ndarray

-i omega, complex128, shape (n_omega,), in rad/s.

node_chunkint

Nodes the pass handles at a time, at least one.

node_responsenumpy.ndarray

Which response owns each node, int32, shape (n_nodes,), -1 where a node’s region declares none. It is what lets the delayed convolution run once per response over that response’s own nodes, with its own kernel.

delayed_tensornumpy.ndarray

chi3_r D[term] of each response and channel, float64, shape (n_responses, n_terms, n_components, n_components, n_components, n_components), in m²/V². Empty when no region declares a delayed response.

delayed_spectrumnumpy.ndarray

The real transform of each kernel on the doubled fine axis, times the temporal weight, complex128, shape (n_responses, n_terms, n_to + 1), in s/s. Empty with delayed_tensor.

form{‘analytic’, ‘real’}

Which field the tensor is contracted against: the analytic signal, whose polarisation carries the fundamental band alone, or the real three-component field, whose cubic carries the third harmonic. Read from the record’s model_id by node_kerr_form(); tensor already carries the normalisation that goes with it.

dispersivesolprop.translate.responses.DispersiveKerrTerms or None

The gas regions’ dispersive channels (SEP 6), each acting at the nodes of its own region, or None.

node_regionnumpy.ndarray or None

Each node’s material region, int32, shape (n_nodes,): where each dispersive channel acts. Present with dispersive.

at(weights)[source]#

Return the maps a stage evaluates at, from its transform_weights.

A constant guide’s maps are returned as they are, whatever weights says, which keeps its arithmetic bit for bit what it was. A moving guide’s are the weighted sums of the stored coefficients, exactly as dPhi is the weighted sum of its terms (docs/dev/kernel-contract.md section 1).

Parameters:
weightsnumpy.ndarray or None

The stage bundle’s transform_weights, float64, one per term; None for maps that do not move.

Returns:
GeneralMaps

The maps at the stage’s position.

Raises:
BackendError

If moving maps are asked for with no weights, or constant ones with some.

delayed_spectrum: Complex[ndarray, 'n_responses_spectrum n_terms_spectrum n_rfft_spectrum'] | None = None#
delayed_tensor: Float[ndarray, 'n_responses_delayed n_terms_delayed n_components_delayed_i n_components_delayed_j n_components_delayed_k n_components_delayed_l'] | None = None#
dispersive: DispersiveKerrTerms | None = None#
form: KerrForm = 'analytic'#
frequency_factor: Complex[ndarray, 'n_omega_factor']#
property moving: bool[source]#

Return whether the maps move along z.

Returns:
bool

True when the synthesis and projection are Chebyshev terms a stage combines over its transform_weights.

property n_modes: int[source]#

Return how many channels the guide propagates.

Returns:
int

The channel axis of synthesis.

property n_nodes: int[source]#

Return how many quadrature nodes the guide’s rule carries.

Returns:
int

The node axis of synthesis.

property n_terms: int[source]#

Return how many delayed channels the maps carry.

Returns:
int

Zero when no region declares a delayed response.

node_chunk: int#
node_region: Int32[ndarray, 'n_nodes_region'] | None = None#
node_response: Int32[ndarray, 'n_nodes_response'] | None = None#
projection: Complex[ndarray, 'n_modes_q n_nodes_q n_components_q n_omega_q']#
projection_terms: Complex[ndarray, 'n_map_terms_q n_modes_mq n_nodes_mq n_components_mq n_omega_mq'] | None = None#
synthesis: Complex[ndarray, 'n_nodes_s n_components_s n_modes_s n_omega_s']#
synthesis_terms: Complex[ndarray, 'n_map_terms_s n_nodes_ms n_components_ms n_modes_ms n_omega_ms'] | None = None#
tensor: Float[ndarray, 'n_nodes_t n_components_t_i n_components_t_j n_components_t_k n_components_t_l']#
solprop.backends.numpy_ref.general_modal.PAIR_BUFFERS_PER_GENERAL_NODE = 2#

Chunk buffers the general source holds per node, each n_components squared.

The tensor contraction is written in two steps rather than as one four-operand einsum, so that every intermediate it allocates is a buffer this module names: the pair history E_k conj(E_l) and the tensor contracted against it. A single einsum with optimize=True allocates the same intermediates and names none of them, which a memory ledger cannot report.

The static maps S and Q, the per-node tensor, the dense E/H reference, its reciprocal partners and the coefficient dual are whole-problem allocations, counted row by row by general_memory(); they do not shrink with the chunk, which is why the counts here and solprop.translate.modal.BUFFERS_PER_NODE are different and neither reuses the other.

solprop.backends.numpy_ref.general_modal.PAIR_HISTORIES = 6#

Independent entries of the real symmetric pair history of three components.

Q_kl = E_k E_l is symmetric, so xx, yy, zz, xy, xz, yz carry all of it and the convolution – the expensive part of a delayed response – runs six times per node and not nine.

solprop.backends.numpy_ref.general_modal.delayed_polarisation(field, pair, maps, owner)[source]#

Return one chunk’s delayed polarisation, in C/m^2, from its pair history.

Six real symmetric histories per node, convolved with each response’s own kernel on the doubled fine axis – so the tail of a hundred-femtosecond response cannot wrap onto the head of the drive – and contracted with that response’s chi3 D[term] and the local field.

The loop is over responses rather than nodes because a convolution is not linear in its kernel’s owner: two material regions may declare different delayed models, and each one’s nodes are convolved with its own kernel.

Parameters:
fieldnumpy.ndarray

The local field at the chunk’s nodes, shape (n_nodes, 3, n_to), V/m: the real field for the real cubic and the analytic signal otherwise.

pairnumpy.ndarray

The real drive Q_kl, shape (n_nodes, 3, 3, n_to), V²/m².

mapsGeneralMaps

The problem’s maps, carrying the delayed tensors and kernel spectra.

ownernumpy.ndarray

Which response owns each node of the chunk, -1 for none.

Returns:
numpy.ndarray

The delayed polarisation, shape (n_nodes, 3, n_to), C/m².

solprop.backends.numpy_ref.general_modal.dispersive_polarisation(field, terms, node_region, densities)[source]#

Return one chunk’s dispersive polarisation, in C/m^2, region by region.

Each channel of SEP 6’s dispersive cubic – one species of one region’s gas fill – acts at the nodes of its own region, on the real three-component field: P = coefficient rho cubic(E) for the polynomial, and for the symmetric evaluator one cubic per region, under its gas fill’s mixture filter (evaluations()), by solprop.nonlinear.responses.dispersive_cubic(), the arithmetic of the reference evaluate(), whose isotropic Kleinman contraction is the real cubic’s tensor (SEP 6, decision Q3). The sector is the leaf’s: the full cubic beside kerr_form="real", the number-conserving one beside "analytic", and both are real polarisations, which the projection takes as it takes Re P_a.

Parameters:
fieldnumpy.ndarray

The real field at the chunk’s nodes, Re E_a, shape (n_nodes, n_components, n_to), in V/m.

termssolprop.translate.responses.DispersiveKerrTerms

The compiled channels, carrying their node regions.

node_regionnumpy.ndarray

Each node’s material region.

densitiessequence of float

Each channel’s number density, m^-3.

Returns:
numpy.ndarray

The polarisation, shape (n_nodes, n_components, n_to), C/m^2; zero at a node whose region has no channel.

solprop.backends.numpy_ref.general_modal.expand_pair_history(stored)[source]#

Return the full symmetric pair history from its six independent entries.

Parameters:
storednumpy.ndarray

The six entries xx, yy, zz, xy, xz, yz, shape (n_nodes, 6, n_to), in V²/m².

Returns:
numpy.ndarray

The symmetric (n_nodes, 3, 3, n_to) history, in V²/m².

solprop.backends.numpy_ref.general_modal.general_dispersive_buffers(leaf)[source]#

Return the buffers per node and component SEP 6’s response adds to a chunk.

The compiled general workspace holds, for each node of a chunk and each of its components, every filtered field’s spectrum and time series and every product and its spectrum: two complex-equivalent buffers per field and two per product. The symmetric evaluator has one field and one product per region with a gas fill (a mixture’s under its mixture filter); the polynomial two fields and two products per group of equal leg bound, and one cube per region (groups()).

Parameters:
leafsolprop.translate.responses.DispersiveKerrTerms or None

The route’s dispersive channels, or None.

Returns:
int

Complex-equivalent buffers per node and component; zero without the leaf.

solprop.backends.numpy_ref.general_modal.general_memory(maps, tables, n_to)[source]#

Return the bytes the general source holds, one row per named buffer.

Three groups. The execution maps S and Q and the per-node tensor are derived once. The reference the record carries – the dense E and H, their reciprocal partners and the coefficient dual – is resident for the whole run because the problem owns it, and is counted here although this pass reads none of it: it is on the far side of the seam, it is the largest group on a guide with many channels, and a budget that did not see it would be wrong by that much. The chunk buffers are the pass’s own, and shrink with node_chunk.

Per-stage transients are not counted: the doubling weights, the inverse transform’s output beside the zero-filled buffer it replaces, the conjugated field, the contiguous copy rfft makes of a strided real view and its fine one-sided output before the crop. Measured with tracemalloc on a 288-node strand, they add 6.2 % to the rows below at their peak (2.52 MiB over 40.9 MiB); each is freed inside the pass.

Parameters:
mapsGeneralMaps

The derived maps of one problem.

tablessolprop.translate.modal_tables.ModalTransformTables

The record the maps came from, whose stored reference is resident.

n_toint

Fine time samples of the run’s grid, positive.

Returns:
dict

Byte counts keyed by buffer name, each counted once: general_synthesis, general_projection – every coefficient of a moving guide’s maps – general_stage_maps, the two maps a moving guide’s stage evaluates (zero otherwise), and general_tensor; the resident reference general_electric, general_magnetic, general_backward_electric, general_backward_magnetic, general_reciprocal_gram and general_coordinate_dual, zero where the record leaves one empty; and the chunk buffers general_scaled_state, general_node_spectrum, general_node_field, general_node_pair, general_node_weighted, general_node_polarisation and general_node_source.

solprop.backends.numpy_ref.general_modal.general_node_chunk(n_components, n_to, n_omega_fine, budget, *, dispersive=0)[source]#

Return the nodes the general source may hold at once within a byte budget.

The chunk bounds the space-domain work alone: BUFFERS_PER_GENERAL_NODE complex buffers per node and component, PAIR_BUFFERS_PER_GENERAL_NODE per node and component squared, and SEP 6’s dispersive response’s own (general_dispersive_buffers()), over the longer of the fine time axis and the fine spectrum. The maps, the reference and the dual are whole-problem allocations that no chunk shrinks (general_memory()).

Parameters:
n_componentsint

Cartesian components of the local field, positive.

n_toint

Fine time samples, positive.

n_omega_fineint

Fine spectral bins, positive.

budgetint

Bytes the chunk buffers may occupy in total, positive.

dispersiveint, optional

The dispersive response’s complex buffers per node and component; zero, the default, without one.

Returns:
int

The chunk size, at least one.

Raises:
BackendError

If the budget is not positive.

Examples

>>> from solprop.backends.numpy_ref.general_modal import general_node_chunk
>>> general_node_chunk(3, 1024, 513, 64 * 2**20)
136
solprop.backends.numpy_ref.general_modal.general_source(state, maps, crop_index, *, n_to, pad_scale, transform_weights=None, dispersive_densities=())[source]#

Evaluate one stage’s modal source through the general guide’s maps.

The four steps of the module docstring, in the cubic maps.form declares, with the gas regions’ dispersive channels added at step 3 (dispersive_polarisation()). The result is the projected polarisation times -i omega: Q is in V m/sqrt(W) and P in C/m^2, so the product carries C V m^-1 W^(-1/2) s^-1, and C V is a joule – the unit is sqrt(W)/m, the amplitude rate da/dz itself. The crop scale n_t / n_to that responses.prefactor carries is a dimensionless grid convention and changes the number but not the unit.

Parameters:
statenumpy.ndarray

The lab-frame channel spectra, complex128, shape (n_modes, n_pol, n_omega), in the square root of watts, with n_pol one.

mapsGeneralMaps

The guide’s synthesis, projection, per-node tensor and frequency factor.

crop_indexnumpy.ndarray

Where each coarse bin sits in the fine spectrum, numpy.intp, shape (n_omega,).

n_toint

Fine time samples, positive and even.

pad_scalefloat

n_to / n_t, the oversampling scale of conventions.md section 3.

transform_weightsnumpy.ndarray or None, optional

The stage bundle’s weights of a moving guide’s maps (GeneralMaps.at()); None for maps that do not move.

dispersive_densitiessequence of float, optional

Each dispersive channel’s number density at the stage, m^-3, in the channels’ order: the stage bundle’s dispersive_kerr_densities.

Returns:
numpy.ndarray

The modal source on the coarse grid, complex128, shape (n_modes, n_pol, n_omega), in sqrt(W)/m before the crop scale.

Raises:
BackendError

If the densities do not match the dispersive channels.

solprop.backends.numpy_ref.general_modal.node_delayed_maps(tables, regional)[source]#

Derive the delayed response’s per-node ownership, tensors and kernel spectra.

A delayed response has memory, so unlike the instantaneous tensor it cannot be summed into one per-node array: two regions may declare different kernels and a convolution is not linear in the kernel’s owner. What is derived instead is the response each node belongs to, each response’s chi3_r D[term], and each kernel’s transform on the doubled fine axis, already scaled by the temporal quadrature weight so that the transform product is the causal integral.

Parameters:
tablessolprop.translate.modal_tables.ModalTransformTables

The guide’s reference, whose region_index owns each node’s region.

regionalsolprop.translate.responses.RegionalResponseTables or None

The compiled per-region responses, or None for a linear guide.

Returns:
tuple

The per-node response index (-1 where a node’s region declares none), the scaled delayed tensors and the kernel spectra, the last two None when no region declares a delayed response.

Raises:
BackendError

Naming the leaf and what about it is not executed.

solprop.backends.numpy_ref.general_modal.node_kerr_form(regional)[source]#

Return which cubic a compiled per-region record declares.

The form is a physical-model choice and travels in model_id, which is a representation tag inside the resume identity already; it is not a leaf field, because adding one would move a contract row and bump the kernel schema for a string the record can carry.

Parameters:
regionalsolprop.translate.responses.RegionalResponseTables or None

The compiled per-region responses, or None for a linear guide, whose zero tensor makes the form immaterial.

Returns:
{‘analytic’, ‘real’}

The declared form; "analytic" for a linear guide.

Raises:
BackendError

If the record declares a model identity this backend does not execute.

solprop.backends.numpy_ref.general_modal.node_kerr_tensor(tables, regional)[source]#

Sum each node’s material responses into one susceptibility tensor.

A response acts in one material region and a node belongs to exactly one, so the tensor a node sees is sum_r chi3_r T_r over the responses of its region. Two responses in one region add, which is what superposed instantaneous susceptibilities do; a region no response names contributes nothing, which is how a linear cladding is declared.

chi3 is stored as Chebyshev coefficients along z. This route propagates constant geometry, so one interval of degree zero is required and the single coefficient is the value.

Parameters:
tablessolprop.translate.modal_tables.ModalTransformTables

The guide’s reference, whose region_index owns each node’s region.

regionalsolprop.translate.responses.RegionalResponseTables or None

The compiled per-region responses, or None for a linear guide.

Returns:
numpy.ndarray

The per-node tensor, float64, shape (n_nodes, 3, 3, 3, 3), in m²/V².

Raises:
BackendError

If the responses do not resolve the reference’s components, the susceptibility varies along z, or a delayed term is declared.

solprop.backends.numpy_ref.general_modal.refuse_regional_responses(tables, regional, *, backend='numpy_ref')[source]#

Refuse a per-region response this route does not execute, before it allocates.

Called from the backend’s admission list, where it runs before the driver’s buffers exist, and again from node_kerr_tensor(), which is public and must not build a tensor from a record it cannot read. Neither call allocates.

A component count that disagrees with the reference is the dangerous one: a one-component tensor broadcasts into the three-component contraction and propagates a meaningless polarisation, and a two-component one dies inside einsum with a shape message that names no leaf.

Parameters:
tablessolprop.translate.modal_tables.ModalTransformTables

The guide’s reference, which fixes the component count.

regionalsolprop.translate.responses.RegionalResponseTables or None

The compiled per-region responses, or None for a linear guide.

backendstr, optional

The registry name each refusal names: numba_cpu executes the same responses and runs this same list.

Raises:
BackendError

Naming the leaf and what about it is not executed.

solprop.backends.numpy_ref.general_modal.symmetric_cubic(coefficients, field)[source]#

Return the real cubic sum_jkl A_ijkl E_j E_k E_l by its symmetric form.

The reduction order of the real cubic on every backend: each sample forms the pair products E_j E_k (j <= k), each monomial as (E_j E_k) E_l, and each component as sum_m C_im monomial_m over the monomials in their order, sequential from zero. Against the dense sum_j (sum_kl A_ijkl E_k E_l) E_j that is 3 + 10 products a component in place of 81 + 3.

Rounding. With M_i = sum_jkl |A_ijkl| |E_j| |E_k| |E_l|: a coefficient is within gamma_5 of its exact sum, a monomial within gamma_2, and the ten-term sum of products within gamma_10, so the component is within gamma_17 M_i of exact arithmetic; the dense order is within gamma_14 (the pair, nine products, the field and three terms), so the two orders differ by at most gamma_31 M_i.

Parameters:
coefficientsnumpy.ndarray

symmetric_kerr_coefficients() of the nodes’ tensors, in m²/V².

fieldnumpy.ndarray

The real field at the nodes, float64, shape (n_nodes, n_components, n_to), in V/m.

Returns:
numpy.ndarray

The cubic before eps0, float64, shape (n_nodes, n_components, n_to), in V/m.

solprop.backends.numpy_ref.general_modal.symmetric_kerr_coefficients(tensor)[source]#

Sum a per-node tensor over the orderings of each monomial of the real cubic.

On a real field sum_jkl A_ijkl E_j E_k E_l = sum_m C_im (E_j E_k) E_l over the monomials of symmetric_monomials(), with C_im the sum of A_ijkl over every ordered triple that sorts to monomial m: one ordering for x x x, three for x x y, six for x y z. That is an identity for any tensor, symmetric or not. The orderings are added in lexicographic order of (j, k, l), each sum sequential from zero, which is the order every backend forms them in; a coefficient is within gamma_5 of the exact sum relative to the sum of its terms’ magnitudes.

Parameters:
tensornumpy.ndarray

The per-node chi3 T, float64, shape (n_nodes, n_components, n_components, n_components, n_components), in m²/V².

Returns:
numpy.ndarray

The coefficients, float64, shape (n_nodes, n_components, n_monomials), in m²/V².

Examples

>>> from solprop.backends.numpy_ref.general_modal import (
...     symmetric_kerr_coefficients,
... )
>>> from solprop.translate.responses import isotropic_kerr_tensor
>>> coefficients = symmetric_kerr_coefficients(isotropic_kerr_tensor("real")[None])
>>> coefficients[0, 0, :4]  # x: xxx, xxy, xxz, xyy
array([1., 0., 0., 1.])
solprop.backends.numpy_ref.general_modal.symmetric_monomials(n_components)[source]#

Return the real cubic’s monomials (j, k, l), j <= k <= l, in order.

The order is lexicographic, and it is the order symmetric_cubic() sums them in on every backend: ten for three components, four for two, one for one.

Parameters:
n_componentsint

Cartesian components of the local field, one to three.

Returns:
tuple

The component triples.

Examples

>>> from solprop.backends.numpy_ref.general_modal import symmetric_monomials
>>> symmetric_monomials(2)
((0, 0, 0), (0, 0, 1), (0, 1, 1), (1, 1, 1))