Spectral quadrature and time signals¶
C++ library · Thermodynamic XXX spectra
scripts/spectral_time_transform.py converts sampled densities to time signals
and propagates their sampling covariance. It uses NumPy binary64 arithmetic;
it does not inherit the C++ calculation's arbitrary precision. No private data,
plotting package or compiled Bethe program is required for the analytic example.
From a source checkout:
python3 -m venv .venv-analysis
. .venv-analysis/bin/activate
python -m pip install -r scripts/requirements-analysis.txt
python scripts/example_spectral_time_transform.py
For your own program, put the checkout's scripts directory on PYTHONPATH,
or place your program beside the module. There is no installed Python package
yet. NumPy 1.24 or later, below 3, is required; CI checks the 1.x minimum and
a current 2.x environment.
A fresh finite-chain versus thermodynamic example¶
With the installed Bethe programs on PATH, run:
python scripts/example_xxx_two_spinon_time.py --output xxx-time-example
This small calculation compares the longitudinal two-spinon response at
q=pi/2, using a 16-site periodic chain and the infinite chain. It calculates
fresh spectra with bethe-xxx-structure-factor and
bethe-xxx-structure-factor-thermo; no downloaded data or plotting dependency
is needed. --finite-solver and --thermo-solver accept explicit executable
paths when the programs are not on PATH. Both calculations use fp64 and one
solver thread. The example's default grids require only seconds on an ordinary
CPU, not a production run.
time.csv contains real and imaginary signals on 0 <= Jt <= 20:
finite_all_*: every calculated finite-chain two-spinon line at this momentum.finite_window_*: only those lines within the sampled thermodynamic interval.thermo_window_*: the integrated thermodynamic two-spinon density on that interval.
The finite lines are summed directly without broadening. The thermodynamic
frequency grid is concentrated near the lower threshold using geometric
segment boundaries, with 129 uniformly spaced points in each segment
(769 unique frequencies by default). The integral uses the threshold-coordinate
interpolant described below. The first sampled point is omega_lower + 1e-6 J:
the divergent endpoint and its missing interval are not filled or estimated.
The upper boundary is included with its zero density. Both signals omit
higher-spinon sectors, and the finite spectrum is not a thermodynamic
extrapolation; its weights and line positions need not match the infinite chain.
report.json records the support, zeroth/first moments, full first-moment sum
rules and calculation provenance. The original solver CSV exports are retained,
including their conventions, build revisions, controls and convergence status.
An existing output directory is refused. A failed source calculation stops the
example, retaining any earlier exports without declaring a completed report.
The time table separately records changes from doubling phase-quadrature order and coarsening the frequency grid, plus propagation of the solver's pointwise kernel error estimates. These are diagnostics, not a combined error bar or a bound on the missing threshold interval. Change these controls separately:
python scripts/example_xxx_two_spinon_time.py --points 257 --output xxx-time-grid-refined
python scripts/example_xxx_two_spinon_time.py --points 257 \
--lower-offset 1e-7 --output xxx-time-refined
--sites accepts multiples of four, keeping the momentum exactly pi/2.
--time-max, --time-points and --phase-order control the transform. Long
times require separate phase and frequency-grid checks. No signal is normalized
to another signal or to a sum rule.
Integration and physical measure¶
For the density convention used by the thermodynamic tools,
weights returns a complex matrix with one row per time and one column per
supplied frequency:
import numpy as np
from spectral_time_transform import weights
omega = np.array([1.0, 1.1, 1.6, 2.0])
density = np.ones_like(omega) # Analytic example, not a spin-chain spectrum.
times = np.linspace(0, 20, 81)
W = weights(omega, times, order=32)
signal = W @ density
The default representation interpolates S(omega) linearly. An order-point
Gauss–Legendre rule per cell integrates the exponential against that
interpolant; the default is 24. Negative times are supported. Only the sampled
interval is integrated. window=(lo, hi) restricts it further and must lie
within the selected nodes. There is no endpoint filling, tail extrapolation,
broadening or intensity adjustment.
These weights apply to a density, not discrete Lehmann line weights. For
finite lines, compute sum(weight * exp(-1j * gap * time)) directly: their
delta-function measure already cancels the 2*pi.
Threshold coordinates and moments¶
For a square-root lower threshold, supply positive offsets evaluated separately from the rounded absolute frequencies:
delta = np.geomspace(1e-6, 1.0, 65)
omega = 2.0 + delta
density = 1 / (2*np.sqrt(delta)) # Another analytic example.
W = weights(omega, times, offsets=delta)
signal = W @ density
m0 = (weights(omega, [0.0], offsets=delta) @ density)[0].real
m1 = (weights(omega, [0.0], offsets=delta, energy_power=1) @ density)[0].real
This interpolates G(x)=2*x*S(omega_lower+x*x) linearly in x=sqrt(delta).
For this example G is constant. Supplied offsets are retained at exact grid
nodes rather than reconstructed by subtracting rounded frequencies. Frequencies
and transformed coordinates must still be distinguishable in binary64. A
singular node at offset zero is rejected; its missing interval is not filled.
energy_power=r multiplies the same original interpolant by omega**r
inside the quadrature. Applying ordinary weights to omega*density constructs
a different interpolant and generally gives a different first moment.
With consistent quadrature, C(0)=m0 and C'(0)=-i*m1.
Shared-sample covariance¶
Let each row of samples contain one independent group estimate of the entire
frequency grid. With at least two groups:
from spectral_time_transform import statistics
mean = samples.mean(axis=0)
covariance = np.cov(samples, rowvar=False, ddof=1) / len(samples)
result = statistics(W, mean, covariance)
The covariance is that of the estimated mean, including off-diagonal
frequency correlations. Returned lists are real, imag,
real_standard_error, imag_standard_error and real_imag_covariance.
The last is the covariance of the real and imaginary parts of each output row.
Input means/covariances are real; complex cross-spectral statistics would need
an explicit real/imaginary block representation first.
For two transforms of the same data, propagate W1-W2 to obtain the error of
their difference. A contrast between times similarly uses the difference of
rows. Separate error bars cannot be combined as though they were independent.
For two sectors sampled with shared groups, concatenate their sample columns and retain the full joint covariance:
joint = np.hstack([sector_a_samples, sector_b_samples])
mean = joint.mean(axis=0)
covariance = np.cov(joint, rowvar=False, ddof=1) / len(joint)
combined = statistics(np.hstack([Wa, Wb]), mean, covariance)
difference = statistics(np.hstack([Wa, -Wb]), mean, covariance)
Wa and Wb can use different frequency grids but must have matching output
times. Cross blocks vanish only for independent estimates. The function checks
dimensions, finiteness, symmetry and positive semidefiniteness, tolerating
roundoff. The covariance eigenvalue check costs O(nodes^3). Returned marginal
statistics avoid storing a dense times × times covariance; obtain any required
time contrast through its rows instead. Inputs are not modified.
Ordinary rows use NumPy matrix products. A tiny covariance entry alone does
not select slower arithmetic: an exponent check identifies potentially lost
underflow terms, then compares a conservative allowance for that loss with
each row's variances and real/imaginary cross covariance. Negligible tails
retain the ordinary path. Overflow risk, or underflow loss that cannot be
shown negligible (including active zeros from cancellation), selects exact
rational products of the supplied binary64 entries. The standard errors and
cross covariance are then rounded to binary64.
This slower path preserves representable standard errors even when their
squared variances underflow or overflow, and preserves tiny covariances that
large coefficients amplify. It uses Python's standard library, with no extra
dependency. It does not improve the precision of the input measurements or the
symmetry/positive-semidefinite check. Overflowing returned values still raise
OverflowError; values below the subnormal range can round to zero. Finite
means use the usual NumPy transform.
Separate accuracy controls¶
- Phase quadrature: increase
orderat fixed grid. Long times may need many more points per cell. There is no automatic error estimate or refinement. - Spectral discretization: refine the frequency grid or compare subsets.
coarser_indices(count, stride)retains both endpoints; pass its output asindices=. Unselected columns receive zero weights. Increasing phase order does not fix an inaccurate interpolant. - Sampling and omitted support: propagate full covariance and assess unsampled frequency/rapidity regions separately. Sampling error excludes interpolation bias, a missing threshold interval and omitted sectors.
Invalid inputs raise ValueError; nonfinite phase, quadrature or transformed
arithmetic raises OverflowError. Failed or absent source values must be
handled before constructing a density array, not zero-filled.
Run the independent analytic and covariance checks with:
python -m unittest discover -s scripts -p 'test_spectral_time_transform.py' -v