volumential.nearfield_potential_table#
- volumential.nearfield_potential_table.DUFFY_NO_FALLBACK_ENV_VAR = 'VOLUMENTIAL_DUFFY_NO_FALLBACK'#
Environment variable that turns the batched-to-scalar DuffyRadial fallback into a hard error. Campaign runs set it so that a table which quietly dropped to the (orders-of-magnitude slower, differently converged) scalar per-entry builder cannot be mistaken for a batched build. It is an environment switch rather than a
DuffyBuildConfigfield on purpose: the build config is hashed into the table-cache fingerprint, so adding a field there would invalidate every cached table and would make an operational strictness policy part of the numerical cache key.
- volumential.nearfield_potential_table.DUFFY_BUILD_ROUTINGS = ('batched', 'scalar', 'scalar-adaptive', 'scalar-fallback')#
Recognized values of
table.build_routing.
- volumential.nearfield_potential_table.kernel_global_scaling_const_to_pymbolic(kernel)[source]#
kernel’s global scaling constant as a
pymbolicexpression.For every kernel Volumential tabulates the constant is a pure number – \(-1/(2\pi)\), \(1/(4\pi)\), \(i/4\) – so it is folded to a literal here. That keeps the generated
knl_scalingassignment a constant regardless of how the upstream mappers happen to shape the expression tree, which is whatNearFieldInteractionTable._rewrite_complex_exponentialsneeds in order to see (and split) an imaginary constant instead of an opaque node. Kernels whose scaling still carries a symbol go through the backend-aware mapper unchanged.
- class volumential.nearfield_potential_table.ComplexExponentialRewriter(unproven_arg_names=frozenset({}))[source]#
Bases:
CSECachingMapperMixin,IdentityMapperRewrite
exp(re + 1j*im)asexp(re) * (cos(im) + 1j*sin(im)).pyopencl’s
pyopencl-complex.himplementscdouble_exp(and the complex trigonometric functions) with the OpenCLsincos(x, &cosx)out-parameter builtin. On the PoCL 7.0 / LLVM 19.1.7 CPU driver that builtin costs roughly 200 ns per call against roughly 1.6 ns for a separatesin(x)pluscos(x)– a ~130x pathology, measured in isolation and not reproduced on a second CPU OpenCL runtime on the same processor. Because the Duffy table quadrature evaluates the kernel at every node, that single builtin made the 3D Helmholtz direct-table build about ten times slower than the otherwise identical real-valued Yukawa build at the same quadrature orders, and accounted for essentially the whole gap.Splitting the exponential keeps complex-valued kernels on real
exp/cos/sincalls, which the driver compiles normally. The rewrite isexp(a + b) = exp(a) exp(b)composed with Euler’s formula, both of which hold for complexaandb, applied to an exact structural split of the exponent (see_split_complex_expression), so it is valid for genuinely complex exponents too – the damped complex-frequency formexp((-a + 1j b) r)included – and not only for the purely imaginaryexp(1j k r)of the Helmholtz kernel. Exponents with no complex constant (Yukawa, Laplace) are left untouched and keep their plain realexp.The rewrite is value-exact but not conditioning-exact once the phase itself may be complex. For \(z = x + \mathrm{i}y\), both \(\cos z\) and \(\sin z\) grow like \(e^{|y|}/2\) while \(e^{\mathrm{i}z}\) decays like \(e^{-y}\), so Euler’s formula turns a decaying exponential into a cancelling difference of two large terms: severe relative error for moderate \(|y|\) and overflow to infinity or NaN beyond that. A complex exponent is not hypothetical –
HelmholtzKernel(dim, allow_evanescent=True)declares its wave numberkascomplex128– so a phase that is not provably real keeps itscdouble_exp, which evaluates the decaying result directly and stably. The test is_is_known_real, a positive node-by-node proof rather than a search for complex dependencies: the fused Duffy expressions are post-CSE, and_split_complex_expressionhands an opaqueCommonSubexpressionto the real part wholesale, so a phase can be complex without naming a complex argument anywhere the split can see. unproven_arg_names names the kernel arguments that are not provably real; see_kernel_arg_names_not_known_real.Mixes in the common-subexpression cache so a shared CSE node in the post-CSE expression DAG is visited once rather than once per reference.
CSECachingMapperMixinhas to come first in the bases: both it andIdentityMapperdefinemap_common_subexpression, so with the other order the MRO picks the uncached one and the cache – and thereforemap_common_subexpression_uncached()– is never reached.- map_common_subexpression_uncached(expr, /, *args, **kwargs)[source]#
Rewrite the body of a common subexpression that is not yet cached.
CSECachingMapperMixincalls this once per distinctCommonSubexpressionnode and caches the result; descending withIdentityMapperis what makes the rewrite reach inside the node instead of stopping at it.
- map_call(expr, /, *args, **kwargs)[source]#
Split an
expof a complex argument into realexp/cos/sin.Any other call, an
expwhose imaginary part is structurally zero, and anexpwhose halves are not provably double-precision real are all returned unchanged; see the class docstring for why, and_is_double_precision_realfor the last of those tests.
- class volumential.nearfield_potential_table.DuffyBuildConfig(radial_rule: str = 'tanh-sinh-fast', regular_quad_order: object = 20, radial_quad_order: object = 61, mp_dps: int = 50, auto_tune_orders: bool = False, auto_tune_samples: int = 5, auto_tune_floor_factor: float = 8.0, auto_tune_candidates: object = None)[source]#
Bases:
objectQuadrature settings for one Duffy near-field table build.
The single object that carries a build’s accuracy knobs from the caller to
NearFieldInteractionTable.build_table_via_duffy_radial(): the radial rule and its order, the regular (angular/tensor) order, the mpmath working precision, and the optional auto-tuning of the two orders. It is frozen, so it can be recorded verbatim in a table’s provenance.
- class volumential.nearfield_potential_table.SymmetryReductionDiagnostics(full_entry_count: int, representative_count: int, compression_ratio: float, orbit_size_histogram: tuple, sign_metadata_count: int, negative_scale_count: int, unreduced_payload_bytes: int, reconstructed_payload_bytes: int, metadata_payload_bytes: int, max_reconstruction_error: object = None, l2_reconstruction_error: object = None)[source]#
Bases:
objectWhat a table’s symmetry reduction bought, and what it cost.
Reported by
NearFieldInteractionTable.get_symmetry_reduction_diagnostics(): the entry counts before and after reduction and their ratio, the orbit-size histogram and sign bookkeeping behind it, the payload sizes of the three representations, and the error of reconstructing the full table from the representatives.The two error fields are
Noneunless there is finite reference data to reconstruct against. A table that has not been reduced yet supplies its owndatafor that, so the usual dense call fills them in; an already reduced table has no full array to compare with, and there the caller has to passreference_data.
- volumential.nearfield_potential_table.constant_one(x, y=None, z=None)[source]#
The constant function one, broadcast to the shape of x.
- volumential.nearfield_potential_table.get_laplace(dim)[source]#
The free-space Laplace kernel as a plain callable of its components.
2D only: \(-\log r / (2\pi)\), evaluated from the Cartesian components of the displacement so that it can be handed to the table builder as an ordinary Python function.
- volumential.nearfield_potential_table.get_cahn_hilliard(dim, b=0, c=0, approx_at_origin=False)[source]#
The 2D Cahn-Hilliard kernel as a plain callable of its components.
With \(\lambda_1^2\) and \(\lambda_2^2\) the two roots of \(\lambda^2 - b\lambda + c\), the returned function evaluates
\[-\frac{1}{2\pi(\lambda_1^2 - \lambda_2^2)} \bigl( K_0(\lambda_1 r) - K_0(\lambda_2 r) \bigr),\]the same expression that
volumential.table_manager.CahnHilliardKernelcarries symbolically. With approx_at_origin, each \(K_0\) is replaced by a small-argument series with the leading logarithmic term removed analytically.Distinct positive roots only, and all three inequalities are strict:
\[b > 0, \qquad c > 0, \qquad b^2 > 4c.\]The first two make both roots of \(\lambda^2 - b\lambda + c\) positive (they are its sum and its product) and the third makes them distinct, which is what the three steps here need: the real
numpy.sqrtof each root, a nonzero \(\lambda_1^2 - \lambda_2^2\) in the prefactor, and a nonzero denominator in the stabilized quadratic formula. Each boundary case breaks a different one – \(b^2 = 4c\) collapses the prefactor, \(c = 0\) reaches a \(0/0\), a negative root reaches \(\sqrt{\text{negative}}\) – and all of them produce a callable that returnsnanrather than raising, so check the coefficients before tabulating with it.volumential.table_manager.CahnHilliardKernelis not restricted this way: it takes the complex square root and rejects coincident roots outright.
- volumential.nearfield_potential_table.get_cahn_hilliard_laplacian(dim, b=0, c=0)[source]#
Not implemented; always raises
NotImplementedError.
- volumential.nearfield_potential_table.sumpy_kernel_to_lambda(sknl, fallback_dim=None, parameter_values=None)[source]#
Turn a
sumpykernel into a plain callable of its components.The table builders evaluate the kernel at quadrature nodes as an ordinary Python function, so the kernel’s symbolic expression – source and target post-processing applied, multiplied by its global scaling constant – is lambdified, with the Hankel and modified-Bessel calls routed to
scipy.special. parameter_values substitutes the kernel’s free symbols, such as a wave number, before lambdification; fallback_dim supplies the dimension for a kernel object that carries neitherdimnorambient_dim.
- class volumential.nearfield_potential_table.NearFieldInteractionTable(quad_order, method='gauss-legendre', dim=2, kernel_func=None, kernel_type=None, sumpy_kernel=None, build_method='DuffyRadial', source_box_extent=1, dtype=<class 'numpy.float64'>, derive_kernel_func=True, progress_bar=True, **kwargs)[source]#
Bases:
objectClass for a near-field interaction table.
A near-field interaction table stores precomputed singular integrals on template boxes and supports transforms to actual boxes on lookup. The query process is done through scaling the entries based on actual box sized.
Orientations are ordered counter-clockwise.
A template box is one of [0,1]^dim
- property data#
The table entries, allocated full of NaN on first access.
Assigning to it drops the orbit representative ids and marks the table as no longer symmetry-reduced, so the value is taken as the full entry array from then on. The setter only coerces the dtype; giving it anything but a one-dimensional array of
_full_entry_count()values is a caller error that later entry lookups will discover, not something it rejects.
- get_entry_index(source_mode_index, target_point_index, case_id)[source]#
Full table entry id of one (source mode, target, case) interaction.
On a symmetry-reduced table this is the id of the entry’s orbit representative, so an interaction and its images under the kernel’s symmetries resolve to one id.
decode_index()inverts the unreduced form of the same encoding.The id addresses the full entry space, which is an index into
dataonly while the table is stored densely. Compact reduced storage (set_reduced_table_data()) keeps the values in a shorter array addressed throughreduced_entry_ids, so read a value withget_entry_data()orget_entry_data_for_full_indices()rather than by indexingdatawith what this returns.
- unwrap_mode_index(mode_index)[source]#
Split a flat basis-mode index into one index per axis.
The flattening is row-major over
quad_ordernodes per axis and has to agree with the mesh generator’s node ordering.
- get_template_mode(mode_index)[source]#
The mode_index-th basis mode of the template box, as a callable.
Template modes are defined on an l_infty circle.
- get_mode_cheb_coeffs(mode_index, cheb_order)[source]#
Cheb coeffs of a mode. The projection process is performed on [0,1]^dim.
- get_symmetry_transform(source_mode_index)[source]#
Apply proper transforms to map source mode to a reduced region
Returns: - a transform that can be applied on the interaction case vectors connection box centers. - a transform that can be applied to the mode/point indices.
- find_target_point(target_point_index, case_index)[source]#
Apply proper transforms to find the target point’s coordinate.
Only translations and scalings are allowed in this step, avoiding the indices of quad points to be messed up.
- lookup_by_symmetry(entry_id)[source]#
Loop up table entry that is mapped to a region where: - k_i <= q/2 in all direction i - k_i’s are sorted in ascending order
Returns the mapped entry_id
- get_entry_data(entry_id)[source]#
Return the requested entry value from full or orbit-reduced storage.
- get_reduced_entry_ids()[source]#
Return full entry IDs stored by the current reduced representation.
- set_reduced_table_data(entry_ids, values)[source]#
Store reduced table values compactly by full entry ID.
- get_entry_data_for_full_indices(entry_ids)[source]#
Return stored values addressed by full table entry IDs.
- has_entry_data_for_full_indices(entry_ids)[source]#
Return whether all full entry IDs have stored finite values.
- compute_table_entry_duffy_radial(entry_id, radial_rule='tanh-sinh-fast', deg_theta=20, radial_quad_order=61, mp_dps=50)[source]#
- reconstruct_full_table_from_symmetry(canonical_data=None)[source]#
Reconstruct full table data from orbit-canonical representatives.
- get_symmetry_reduction_diagnostics(reference_data=None)[source]#
Return paper-facing symmetry-reduction counts and reconstruction errors.
- build_table_via_duffy_radial_batched(queue, radial_rule='tanh-sinh-fast', deg_theta=20, radial_quad_order=61, mp_dps=50, kernel_kwargs=None)[source]#
- build_table_via_duffy_radial_batched_2d(queue, radial_rule='tanh-sinh-fast', deg_theta=20, radial_quad_order=61, mp_dps=50, kernel_kwargs=None)[source]#
- build_normalizer_table(pool=None, pb=None)[source]#
Build normalizers, used for log-scaled kernels, currently only supported in 2D.
- build_table_via_duffy_radial(radial_rule='tanh-sinh-fast', regular_quad_order=20, radial_quad_order=61, mp_dps=50, queue=None, cl_ctx=None, build_config=None, auto_tune_orders=False, auto_tune_samples=5, auto_tune_floor_factor=8.0, auto_tune_candidates=None, **kwargs)[source]#
- build_table(cl_ctx=None, queue=None, build_config=None, **kwargs)[source]#
Fill in this table, by way of the Duffy radial builder.
The entry point the table manager calls. Which Duffy path actually runs – and, when the batched one gives way to the scalar one, why – is decided a level down in
build_table_via_duffy_radial()and recorded on the table asbuild_routingandbuild_fallback_reason.
- build_kernel_exterior_normalizer_table(cl_ctx, queue, pool=None, ncpus=None, mesh_order=5, quad_order=10, mesh_size=0.03, remove_tmp_files=True, **kwargs)[source]#
Build the kernel exterior normalizer table for fractional Laplacians.
An exterior normalizer for kernel \(G(r)\) and target \(x\) is defined as
\[\int_{B^c} G(\lVert x - y \rVert) dy\]where \(B\) is the source box \([0, source_box_extent]^dim\).