pyextal.superspace.engine¶
Driving the Fortran Bloch-wave engine with a modulated structure.
This is the Python side of Stage B. The engine carries superspace indices and a modulated structure factor of its own:
blochwave.superspaceholds the modulation dimensiondmod, the wave vectorsqvec, the satellite indiceshklmparallel todifpar.hkl, and the per-atom harmonics.difpar.gvecholds each beam’s effective reciprocal index,hkl + sum_j m_j q_j, which is generally not integral and so cannot live indifpar.hkl. Every geometry calculation readsgvec, so an ordinary crystal and a modulated one follow one path.sfact_modcomputes the modulated structure factor by quadrature over internal space, andsetughcalls it instead ofsfactwhendmod > 0.
dmod = 0 is the default and switches all of it off: an ordinary crystal
takes exactly the path it always did, and the result is bit-for-bit unchanged
(test_superspace_engine.py pins this).
Three limits worth knowing before using it:
P1 only. Relating the modulation functions of symmetry-equivalent atoms needs a phase that depends on each atom’s own position; that is not implemented, and
load()refuses anything else rather than quietly putting the wrong modulation on every generated atom.The beam list must be explicit.
selepicks beams by enumerating integer indices, which cannot reach a satellite. Write thehklblock out.One calculation per process.
terminate()ends the engine for the life of the process, and skipping it does not help either.run_isolated()works around it.
Functions¶
bool: Whether a modulation is currently active in the engine. |
|
|
Push a modulated structure into the engine and rebuild U(g,h). |
|
Switch modulation off and restore the ordinary U(g,h) matrix. |
|
Modulated structure factors straight from the Fortran. |
The engine's current beam list as (3+d) superspace indices. |
|
|
Write a |
|
A dynamical zone-axis pattern of a modulated structure. |
|
Run one engine calculation in a fresh interpreter and return its result. |
Module Contents¶
- pyextal.superspace.engine.is_loaded()¶
bool: Whether a modulation is currently active in the engine.
- pyextal.superspace.engine.load(lat, atoms, H, n_internal=32, coset=None, coset_dh=1.05)¶
Push a modulated structure into the engine and rebuild U(g,h).
Call after
bloch_parse: the crystal, the atom list and the beam list all have to be in place, because this writes alongside them.H supplies the satellite indices for every beam. Its main indices must match the
hklblock of the.datrow for row – the engine keeps them indifpar.hkland this only adds the satellite part, so a mismatch would silently pair the wrongmwith the wrong(h k l). It is checked.- Parameters:
lat (SuperspaceLattice) – Supplies
dand the wave vectors.atoms (list[ModulatedAtom]) – The structure in P1, in the same order as the
atomlines of the.dat.H (array-like) – (nbeams, 3+d) superspace indices, ordered as the
.datbeam list.coset (bool, optional) – Sum coincident superspace indices into every U(g,h). Only a commensurate structure has any, and there it is required: several indices name one physical reflection, and the crystal’s Fourier coefficient is the coherent sum of their amplitudes. Without it the superspace engine reproduces the t-averaged structure rather than the actual crystal, which shows up as satellites off by tens of percent against the exact supercell and as a broken point group. None (the default) turns it on exactly when
SuperspaceLattice.is_commensurate()says so.coset_dh (float) – Largest coincidence shift
|c . q|to include, in inverse Angstrom. The set is a lattice and so infinite; seecoincidence_coset()for why the cut has to be on the shift and not on the satellite order.n_internal (int) – Internal-space quadrature points per dimension. The integrand’s bandwidth grows with satellite order and modulation amplitude; too few aliases high harmonics onto low ones silently.
- Returns:
nbeams,natoms,d,n_internalandharmonics(the count loaded per atom).- Return type:
dict
- Raises:
ValueError – If H disagrees with the engine’s beam list or atom count, or the modulation dimension is out of range.
RuntimeError – If the space group is not P1, or the engine rejects the setup.
- pyextal.superspace.engine.unload()¶
Switch modulation off and restore the ordinary U(g,h) matrix.
- pyextal.superspace.engine.structure_factor(H, kind=3)¶
Modulated structure factors straight from the Fortran.
The engine’s own
sfact_mod, for checking againstpyextal.superspace.structurefactor.structure_factor()or for reading off individual values. Returns U_g in the engine’s units, which is the Python result timesratio(3).The direct beam is not meaningful here:
sfactdivides bys^2, which is exactly zero for(0 0 0 | 0 ...), and the engine takes U_0 from the mean inner potential instead.- Parameters:
H (array-like) – (n, 3+d) superspace indices.
kind (int) – 1 x-ray, 2 electron in volts, 3 electron in Angstrom^-2.
- Returns:
(elastic, absorptive), each (n,) complex.- Return type:
tuple[np.ndarray, np.ndarray]
- Raises:
RuntimeError – If no modulation is loaded.
ValueError – If H has the wrong width.
- pyextal.superspace.engine.engine_reflections()¶
The engine’s current beam list as (3+d) superspace indices.
- Returns:
(nbeams, 3+d) integers, or (nbeams, 3) when no modulation is loaded.
- Return type:
np.ndarray
- pyextal.superspace.engine.write_dat(path, lat, atoms, H, zone=(0, 0, 1), xaxis=(1, 0, 0), hv=200.0, conv=0.0, samp=20, sgmax=3.0, sgmin=0.3, omeg=30.0, out=None, aper=None, abs_pot=0.0, name='modulated', norm=None, kt=(0, 0, 0), header=None)¶
Write a
.datfor a modulated structure: P1, explicit beam list.The file describes the basic cell and its atoms. The modulation itself is not in the file – it goes in through
load()after parsing, which keeps the order-sensitive.datgrammar untouched.Only the main indices of H are written, since that is all
difpar.hklholds; load supplies the satellite part in the same order.- Parameters:
path (path-like) – Destination.
lat (SuperspaceLattice) – Supplies the cell.
atoms (list[ModulatedAtom]) – The structure in P1.
H (array-like) – (nbeams, 3+d) superspace indices for the beam list.
zone (tuple) – Zone axis.
xaxis (tuple) – In-plane reference reflection.
hv (float) – Accelerating voltage, kV.
conv (float) – Convergence semi-angle in units of
|g_xaxis|/|K|.samp (int) – Sampling across the disc.
sgmax (float) – Largest excitation error for beam selection.
sgmin (float) – Excitation error below which a beam is fully diagonalised.
omeg (float) – Bethe-potential parameter.
out (array-like, optional) – Main indices of the output beams; the direct beam alone when None, and ignored when aper is given.
aper (float, optional) –
Output-aperture radius in inverse Angstrom. Preferred over out for a modulated structure, because out matches on the three main indices and two beams differing only in their satellite part share those.
Note it selects at parse time, on the main index: the modulation is loaded afterwards, so
gvecis stillreal(hkl)when the aperture is applied. Make it large enough to keep every beam and cut on the effective |g| in Python instead –dynamical_saed()does.abs_pot (float) – Mean absorption potential, volts.
name (str) – Crystal name.
norm (tuple, optional) – Surface normal; zone when None.
kt (tuple) – Tangential incident wavevector.
header (str, optional) – Extra comment lines.
- Returns:
The file written.
- Return type:
Path
- pyextal.superspace.engine.dynamical_saed(lat, atoms, H, thickness=200.0, zone=(0, 0, 1), xaxis=None, hv=200.0, aper=None, n_internal=32, coset=None, coset_dh=1.05, path=None, keep_engine=False, backend='numpy', **dat_kw)¶
A dynamical zone-axis pattern of a modulated structure.
The whole of Stage B in one call: write the basic cell out, parse it, push the modulation in, solve one incident orientation, and come back with a
DiffractionPatternwhose intensities are dynamical rather than|F|^2. It is directly comparable withsaed_pattern()on the same structure, and renders the same way throughrender_image().Output beams are chosen by aperture, not by an
outlist:outmatches on the three main indices, and two beams differing only in their satellite part share those.The engine holds one crystal in Fortran module-global state, so this reparses and resets it.
terminate()runs at the end unless keep_engine is set – and it ends the engine for the life of the process, so userun_isolated()when more than one calculation is needed.- Parameters:
lat (SuperspaceLattice) – The superspace lattice.
atoms (list[ModulatedAtom]) – The structure in P1.
H (array-like) – (nbeams, 3+d) beam list.
thickness (float) – Specimen thickness in Angstrom.
zone (tuple) – Zone axis.
xaxis (tuple, optional) – In-plane reference reflection; the shortest in-plane main reflection when None.
hv (float) – Accelerating voltage, kV.
aper (float, optional) – Keep only reflections with effective
|g| <= aper, applied in Python after the solve. All of H when None.n_internal (int) – Internal-space quadrature points per dimension.
path (path-like, optional) – Where to write the
.dat; a temporary file when None.keep_engine (bool) – Skip
terminate().backend (str) – Eigensolver,
'numpy'(default; any build) or'fortran'(MPI build only). Both use the effective beam positions and the modulated structure factors set byload().**dat_kw – Passed to
write_dat().
- Returns:
With intensity normalised to the strongest diffracted beam, and F holding the modulated structure factors.
- Return type:
- pyextal.superspace.engine.run_isolated(func, *args, retries=2, settle=2.0, **kwargs)¶
Run one engine calculation in a fresh interpreter and return its result.
The engine keeps a single crystal in Fortran module-global state, and there is no way back from that inside one process:
terminate()ends it for the life of the process, and skipping it does not help either – a secondbloch_parse+tilt_runhangs, because the parked MPI workers cannot reconcile a re-parsed crystal with a different beam count. One dynamical calculation per process, and that is that.So when a notebook needs to compare two, each goes in its own process:
from pyextal.superspace import engine, supercell_saed a = engine.run_isolated(engine.dynamical_saed, lat, atoms, H, ...) b = engine.run_isolated(supercell_saed, lat, atoms, H, ...)
The child is started with
os.posix_spawn(), notsubprocess. Importing anything frompyextalrunsMPI_Initin the parent, andsubprocessreachesfork(); forking a process with a live MPI runtime and then spawning MPI again in the descendant corrupts its heap. It shows up asFatal glibc error: malloc.c: assertion failedfrom a child that runs perfectly on its own, which points nowhere near the cause.Two engine runs in quick succession are flaky even in separate processes: the second child spawns its MPI workers while the first child’s are still tearing down, and it surfaces as
oneMKL ERROR: Parameter 3 was incorrect on entry to CGEBALor a glibc heap assertion, at roughly one failure in two on back-to-back calls. retries and settle are a mitigation for that race, not a fix for it – the calculation itself is deterministic, so a retry either works or the error was real.Arguments and the return value travel by pickle, so func has to be importable by name in the child – a module-level function, not a lambda or a closure – and the arguments have to be picklable. Everything in this subpackage is.
- Parameters:
func (callable) – A module-level function.
*args – Positional arguments, pickled.
retries (int) – Extra attempts after a failure, for the spawn race above. Zero disables it, which is what you want when debugging a genuine failure in func.
settle (float) – Seconds to wait before a retry, letting the previous run’s MPI workers exit.
**kwargs – Keyword arguments, pickled.
- Returns:
Whatever func returns.
- Raises:
RuntimeError – If every attempt fails; the last child’s output is included.