-
Notifications
You must be signed in to change notification settings - Fork 21
Implementation guide
This page describes how the backend-neutral EDRIXS interface fits together and what needs to be implemented in the PETSc backend. It is intended as a practical companion to the restructuring plan.
The main principle is that physical problem definition is backend independent, while construction and numerical solution of many-body operators are delegated to a backend.
For a detailed description of the reference SciPy implementation, see the SciPy backend implementation note.
The relevant parts of the repository are
edrixs/
models.py
solvers.py
_solvers_helpers.py
fock_basis.py
krylov.py
scipy_backend/
__init__.py
scipy_backend.py
petsc_backend/
__init__.py
petsc_backend.py
tests/
test_solvers_dispatch.py
test_scipy_operator_construction.py
solver_consistency_tests/
benchmark_checks/
example_reference_data.zip
run_and_compare_scipy_examples.py
run_lanio3_2v1c_scipy.py
run_lanio3_thin_scipy.py
run_nio_aim_xas_scipy.py
run_pu_o45_scipy.py
run_u_l3_scipy.py
run_uru2si2_scipy.py
The PETSc work should primarily be confined to edrixs/petsc_backend/petsc_backend.py, together with PETSc-specific tests and benchmark scripts as they are added.
The backend-independent API should not need to know the internal PETSc/SLEPc algorithm being used.
A typical calculation proceeds as follows.
For example,
from edrixs.models import model_1v1c
problem = model_1v1c(...)The model functions return backend-independent orbital-space data:
emat_i, umat_i, basis_i,
emat_n, umat_n, basis_n,
trans_mat
emat_i and emat_n contain two-fermion terms, umat_i and umat_n contain four-fermion terms, and the FockBasis objects describe the many-body basis.
trans_mat contains the orbital-space components of the photon transition operator. There are normally three components for a dipole transition or five for a quadrupole transition.
The model layer should remain independent of PETSc.
The public call is
from edrixs.solvers import get_ops
hmat_i, hmat_n, trans_ops = get_ops(
*problem,
backend="petsc",
backend_kws={...},
)get_ops calls the public build_op function for the initial Hamiltonian, intermediate Hamiltonian, and each component of the transition operator.
The backend entry point is therefore
build_op_petsc(emat, umat, lb, rb=None, *, backend_kws=None)This is a general Fock-space operator constructor, not specifically a Hamiltonian constructor.
It must construct an operator of the form
two-fermion contribution from emat
+
four-fermion contribution from umat
where either contribution may be absent (None).
lb is the output/left Fock basis and rb is the input/right Fock basis. If rb is None, it defaults to lb.
For Hamiltonians, normally lb == rb.
For photon transition operators,
lb = basis_n
rb = basis_i
umat = None
so the resulting operator can be rectangular and maps the initial-state Fock space into the intermediate-state Fock space.
The PETSc implementation must therefore not assume that every operator is square.
The public call is
eval_i, evec_i = ed(
hmat_i,
num_evals=num_evals,
backend="petsc",
backend_kws={...},
)The PETSc entry point is
ed_petsc(hmat_i, num_evals=1, *, backend_kws=None)It should return
eval_i : shape (num_evals,)
evec_i : shape (dimension, num_evals)
with eigenvectors stored by column.
The public API deliberately passes backend-specific solver controls in the dictionary backend_kws. PETSc/SLEPc-specific options therefore belong in this dictionary rather than in the public ed() signature.
For example, PETSc-specific choices such as eigensolver type, tolerances, iteration limits, subspace dimensions, or GPU-related options can be interpreted inside ed_petsc.
The SciPy implementation is useful as a behavioral reference, but the PETSc implementation does not need to copy the SciPy numerical algorithm.
The public call is
xas(
eval_i,
evec_i,
hmat_n,
trans_ops,
ominc,
gamma_c=...,
pol_type=...,
backend="petsc",
backend_kws={...},
)The PETSc entry point is
xas_petsc(
eval_i,
evec_i,
hmat_n,
trans_op,
ominc,
*,
gamma_c=0.1,
thin=1.0,
phi=0.0,
pol_type=None,
temperature=1.0,
scatter_axis=None,
backend_kws=None,
)The returned spectrum should have shape
(len(ominc), len(pol_type))
after default polarization handling.
The public physical arguments should retain the same meaning for every backend. PETSc-specific numerical choices belong in backend_kws.
The SciPy implementation currently uses Lanczos continued fractions. PETSc may use an equivalent PETSc/SLEPc implementation as appropriate; numerical method details are backend-specific.
The public call is
rixs(
eval_i,
evec_i,
hmat_i,
hmat_n,
trans_ops,
ominc,
eloss,
gamma_c=...,
gamma_f=...,
pol_type=...,
skip_gs=False,
backend="petsc",
backend_kws={...},
)The PETSc entry point is
rixs_petsc(
eval_i,
evec_i,
hmat_i,
hmat_n,
trans_op,
ominc,
eloss,
*,
gamma_c=0.1,
gamma_f=0.01,
thin=1.0,
thout=1.0,
phi=0.0,
pol_type=None,
temperature=1.0,
scatter_axis=None,
skip_gs=False,
return_poles=False,
backend_kws=None,
)The spectrum should have shape
(len(ominc), len(eloss), len(pol_type))
after default polarization handling.
skip_gs is part of the released public behavior and must be retained.
When skip_gs=True, transitions back into the retained initial-state subspace should be removed from the final-state spectrum. In the present SciPy implementation this is done by projecting the final RIXS vector out of the subspace spanned by evec_i before the final-state Lanczos calculation.
The PETSc implementation should reproduce this behavior even if the underlying numerical implementation is different.
return_poles is also part of the current public/backend contract and should be implemented consistently with the public behavior of the SciPy backend.
The current PETSc file is deliberately a stub. The functions that need real implementations are
owns_operator_petsc(operator)
build_op_petsc(
emat, umat, lb, rb=None, *, backend_kws=None
)
ed_petsc(
hmat_i, num_evals=1, *, backend_kws=None
)
xas_petsc(
eval_i, evec_i, hmat_n, trans_op, ominc, *,
gamma_c=0.1, thin=1.0, phi=0.0, pol_type=None,
temperature=1.0, scatter_axis=None, backend_kws=None
)
rixs_petsc(
eval_i, evec_i, hmat_i, hmat_n, trans_op, ominc, eloss, *,
gamma_c=0.1, gamma_f=0.01, thin=1.0, thout=1.0, phi=0.0,
pol_type=None, temperature=1.0, scatter_axis=None,
skip_gs=False, return_poles=False, backend_kws=None
)These signatures should be kept synchronized with the backend-neutral wrappers in solvers.py.
solvers.ed, solvers.xas, and solvers.rixs allow the backend to be supplied explicitly:
backend="petsc"but they can also infer the backend from the supplied operators.
This is why owns_operator_petsc() is part of the backend contract.
It should return True for PETSc objects owned by this backend and False otherwise.
Once PETSc operators are correctly recognized, calls such as
ed(hmat_i)can resolve to ed_petsc without the user explicitly repeating backend="petsc".
The implementation should avoid importing petsc4py when the PETSc backend is not being used. The current stub deliberately imports PETSc lazily.
Backend-specific controls are deliberately passed as one dictionary.
For example,
eval_i, evec_i = ed(
hmat_i,
num_evals=10,
backend="petsc",
backend_kws={
# PETSc/SLEPc-specific options
},
)The backend implementation is responsible for interpreting and validating the contents of this dictionary.
The public solvers.py interface should not accumulate PETSc-specific keyword arguments.
Likewise, build_op, xas, and rixs each receive their own backend_kws dictionary.
This allows SciPy and PETSc to expose different numerical controls without changing the common physical API.
The following should normally not be reimplemented in the PETSc backend:
- physical model construction in
models.py; -
FockBasisand Fock-basis generation infock_basis.py; - the public
build_op,get_ops,ed,xas, andrixswrappers insolvers.py; - physical meanings of polarization, broadening, temperature, incident-energy and energy-loss arguments;
- public output shapes;
-
skip_gsbehavior.
Backend-independent helper code can be shared where useful. Backend-specific numerical data structures, operator assembly, eigensolvers, Krylov methods, linear solves, and PETSc/SLEPc configuration belong in the PETSc backend.
edrixs/scipy_backend/scipy_backend.py is the existing working implementation of the staged API.
It is useful for understanding
- operator shapes and left/right basis conventions;
- the meaning of
num_evals; - the XAS and RIXS output layouts;
- dipole and quadrupole transition handling;
-
skip_gs; - the expected relationship between public functions and backend entry points.
It should not be treated as a requirement to reproduce SciPy implementation details. PETSc/SLEPc should use the numerical methods and data structures appropriate to that backend.
The repository-level tests live under tests/.
tests/test_solvers_dispatch.py checks the backend-neutral dispatch layer. Equivalent PETSc coverage should establish that
- PETSc operators are recognized by backend inference;
-
backend_kwsreaches the PETSc implementation; - XAS and RIXS return arrays of the expected shape;
-
skip_gshas the expected effect.
The consistency tests live under
tests/solver_consistency_tests/
These are the natural place to compare SciPy and PETSc on the same small physical problems.
The consistency tests should be small enough for CI. Their purpose is correctness across implementations rather than performance benchmarking.
The larger legacy-reference checks live in
benchmark_checks/
They serve a different purpose from the unit and consistency tests.
The six intended benchmark calculations are
LaNiO3_2v1c
LaNiO3_thin
NiO_AIM_XAS
Pu_O45
U_L3
URu2Si2
The reference spectra and eigenvalues are stored in
benchmark_checks/example_reference_data.zip
From the repository root:
cd benchmark_checks
unzip example_reference_data.zipAfter extraction there should be a directory
benchmark_checks/example_reference_data/
containing one directory for each benchmark case.
Do not modify the reference data when implementing a new backend.
With the EDRIXS environment activated, run
python run_and_compare_scipy_examples.pyThe runner
- runs each benchmark script;
- writes fresh calculation output below
example_validation_output/; - compares calculated eigenvalues with the legacy
eigvals.dat; - checks XAS/RIXS energy grids;
- compares XAS/RIXS intensities using the normalized integrated spectral difference;
- prints
PASSorFAILfor each comparison.
The default comparison tolerances are
spectral normalized difference: 1e-6
eigenvalue absolute tolerance: 1e-7 eV
energy-grid absolute tolerance: 1e-10
The output should be read case by case. The runner exits with status 0 when all required comparisons pass and status 1 when one or more comparisons fail.
If the calculation outputs have already been generated, they can be checked again without rerunning the calculations:
python run_and_compare_scipy_examples.py --compare-onlyThe current benchmark scripts and runner are named for and explicitly select the SciPy backend. The reference data and comparison logic are nevertheless backend independent.
For PETSc validation, retain exactly the same
- physical model parameters;
- incident-energy and energy-loss grids;
- broadenings;
- polarization choices;
- number of retained states;
- output-file conventions;
- comparison tolerances.
Then either
- add corresponding
run_*_petsc.pybenchmark scripts, or - later parameterize the benchmark scripts/runner so that the backend can be selected without duplicating the physical problem definition.
The important requirement is that a PETSc benchmark solves the same physical problem and writes the same comparison files. The reference data should remain unchanged.
A PETSc benchmark run should ultimately provide the same type of report:
LaNiO3_2v1c
PASS/FAIL eigenvalues
PASS/FAIL xas.dat
PASS/FAIL rixs_pi.dat
...
Pu_O45
PASS/FAIL eigenvalues
PASS/FAIL xas.dat
PASS/FAIL rixs_pi.dat
and likewise for the other four cases.
The current backend interface does not expose threading or multiprocessing controls.
At a minimum, the PETSc backend is integrated when the following command chain works without PETSc-specific changes to the model layer or public solver API:
problem = model_1v1c(...)
hmat_i, hmat_n, trans_ops = get_ops(
*problem,
backend="petsc",
backend_kws={...},
)
eval_i, evec_i = ed(
hmat_i,
num_evals=...,
backend="petsc",
backend_kws={...},
)
Ixas = xas(
eval_i,
evec_i,
hmat_n,
trans_ops,
ominc,
backend="petsc",
backend_kws={...},
)
Irixs = rixs(
eval_i,
evec_i,
hmat_i,
hmat_n,
trans_ops,
ominc,
eloss,
backend="petsc",
backend_kws={...},
)The corresponding small consistency tests should agree with SciPy, and the larger benchmark checks should then be used to identify any remaining numerical differences on realistic examples.