pyextal.superspace

Incommensurate crystallography in (3+d)-dimensional superspace.

A modulated crystal has no three-dimensional lattice. Its diffraction pattern is still perfectly sharp, but the reflections do not sit on a 3D lattice, so three integers cannot label them. The superspace description of de Wolff, Janner, Janssen and van Smaalen restores a lattice by adding dimensions: every reflection is indexed by (h, k, l, m1, ..., md) and sits at

S(H) = h a* + k b* + l c* + m1 q1 + … + md qd

This subpackage implements that indexing, the modulated structure factor, and a kinematic zone-axis pattern, with 1T-TaS2 as the worked example.

Quick start

from pyextal.superspace import tas2, saed_pattern, plot_pattern

lat, atoms = tas2.structure(d=2)          # (3+2)D, incommensurate
pattern = saed_pattern(lat, atoms, zone=(0, 0, 1))
plot_pattern(pattern, annotate=12)

What is and is not here

  • d = 1, 2, 3 are all supported. The q-vectors have to be independent modulo the main lattice, and SuperspaceLattice warns when they are not – 1T-TaS2’s three CDW arms satisfy q1 + q2 + q3 = c*, so they span a rank-2 module and the correct description is (3+2)D, not (3+3)D.

  • Displacive and occupational modulation, with arbitrarily many harmonics. Modulated ADPs are not implemented.

  • Structure factors are computed by quadrature over internal space, checked against the Bessel-function closed form to machine precision.

  • Symmetry: superspace operations, the epsilon solver, and reflection transformation. Expanding modulation functions through an operation is implemented only for the well-behaved case; see symmetry.

  • The dynamical side goes through the commensurate approximant (approximant), which writes an ordinary .dat file for the unmodified Bloch-wave engine. Nothing in this subpackage changes the Fortran, pyextal.acom, or jaxEngine.

The 1T-TaS2 modulation amplitudes in tas2 are a labelled stand-in, not the published refinement. See that module.

Submodules

Classes

ReflectionSet

Superspace reflections with their derived 3D quantities.

SuperspaceLattice

A 3D lattice plus d modulation wave vectors.

DisplaciveModulation

Positional modulation: a displacement field over internal space.

Harmonic

One term of a Fourier series in the internal coordinate.

ModulatedAtom

One atom of the basic structure, with its modulation functions.

OccupationalModulation

Occupancy as a function of internal coordinate.

DiffractionPattern

A computed zone-axis pattern.

SuperspaceOperation

One symmetry operation of a (3+d) superspace group.

Functions

assign_superspace_index(lat, hkl_super[, M, ...])

Label supercell reflections with their (3+d) superspace indices.

dynamical_pattern(lat, atoms[, thickness, zone, ...])

A dynamical zone-axis pattern of the approximant, via the Bloch engine.

supercell_atoms(lat, atoms[, M, wrap])

Freeze the modulation into an ordinary list of 3D atomic positions.

supercell_cell(lat, M)

Lattice parameters of the supercell.

supercell_saed(lat, atoms, H[, thickness, zone, ...])

Dynamical pattern of the exact supercell, on the same beams as H.

write_dat(path, lat, atoms[, zone, xaxis, hv, conv, ...])

Write the approximant as a .dat file the Bloch-wave engine can parse.

enumerate_reflections(lat, gmax[, max_index, ...])

All superspace reflections inside a resolution sphere.

format_index(H)

Render one superspace index as (h k l | m1 m2).

coincidence_coset(lat[, dh_max, max_order, tol])

The satellite offsets that leave a reflection where it is.

distinct_beams(lat, H[, who, warn])

Reduce a beam list to one entry per physical reflection.

merge_coincident(lat, H[, tol])

Group superspace indices that are the same physical reflection.

satellite_indices(lat, max_order[, tol])

All (m_1..m_d) whose modulation vector is within max_order q-lengths.

satellite_length(lat, H)

|sum_j m_j q_j| for each reflection, in inverse Angstrom.

satellite_metric(lat)

Metric on satellite indices: Q_jk = q_j . q_k in inverse Angstrom squared.

cell_to_metric(cell)

Direct-space metric tensor from the six lattice parameters.

marker_sizes(intensity[, size, gamma, floor])

Marker areas for a diffraction pattern, saturating the direct beam.

plot_image(pattern[, ax, shape, extent, sigma, gamma, ...])

Draw a rendered pattern as an image.

plot_pattern(pattern[, ax, size, rel_min, ...])

Draw a pattern, distinguishing main reflections from satellite orders.

project_pattern(lat, H[, zone, xaxis])

In-plane coordinates of superspace reflections at a zone axis.

pseudo_voigt_kernel(radius, sigma, gamma)

The pseudo-Voigt point spread the JAX refinement fits, on a pixel grid.

render_image(pattern[, shape, extent, sigma, gamma, ...])

Render a pattern as a 2D intensity image, the way a plate records it.

saed_pattern(lat, atoms[, zone, gmax, max_index, ...])

Compute a kinematic zone-axis pattern.

saturation_level(pattern[, shape, extent, sigma, ...])

The intensity plot_image() clips at when it chooses for itself.

electron_form_factor(element, s[, s_floor])

Electron scattering factor by the Mott-Bethe relation.

xray_form_factor(element, s)

X-ray atomic form factor.

auto_n_internal(atoms, hkl, max_order[, safety, lo, hi])

Choose a grid fine enough that the quadrature does not alias.

bessel_reference(lat, H, amplitude[, element, ...])

Closed-form structure factor for one atom with a single harmonic.

internal_grid(d, n)

Uniform sampling of the internal unit cell [0, 1)^d.

structure_factor(lat, atoms, H[, kind, n_internal, ...])

Structure factors of a modulated structure.

epsilon_for(R, q[, max_epsilon, tol])

Solve R^T q_j = sum_k epsilon_jk q_k + G_j for integers.

expand_atoms(atoms, ops, lat[, tol])

Generate the symmetry-equivalent atoms and their modulation functions.

transform_reflection(op, H)

Apply a superspace operation to reflection indices.

Package Contents

pyextal.superspace.assign_superspace_index(lat, hkl_super, M=None, max_order=6, tol=1e-06)

Label supercell reflections with their (3+d) superspace indices.

This is what makes the dynamical calculation comparable with the kinematic one. The engine works in the supercell and knows nothing about modulation, so it reports a reflection as (1, 0, 0); in the parent’s terms that same reflection is the first-order satellite (0 0 0 | 1 0). Going back is a matter of undoing h' = M h and then splitting the result into an integral main index plus a satellite part.

The split is not unique for a commensurate structure – that is the same degeneracy SuperspaceLattice.supercell_matrix() exists because of – so the assignment of lowest satellite order is chosen, which is the physically meaningful one: a reflection is called a first-order satellite rather than a fifth-order one that happens to land in the same place.

Parameters:
  • lat (SuperspaceLattice) – The commensurate parent lattice.

  • hkl_super (array-like) – (n, 3) integer supercell reflection indices.

  • M (array-like, optional) – Supercell matrix; derived when None.

  • max_order (int) – Largest satellite order to consider.

  • tol (float) – Integrality tolerance on the main part.

Returns:

(H, ok) with H of shape (n, 3+d) and ok a boolean mask. Where ok is False no assignment within max_order was found and that row of H is zero.

Return type:

tuple[np.ndarray, np.ndarray]

pyextal.superspace.dynamical_pattern(lat, atoms, thickness=200.0, zone=(0, 0, 1), xaxis=(1, 0, 0), hv=200.0, gmax=0.8, path=None, max_order=6, keep_engine=False, backend='numpy', **dat_kw)

A dynamical zone-axis pattern of the approximant, via the Bloch engine.

Writes the supercell as a .dat, runs one incident orientation through the unmodified engine, and labels the output beams with their superspace indices so the result lines up with saed_pattern().

The engine keeps one crystal in Fortran module-global state, so this reparses and resets it. It calls terminate() at the end unless keep_engine is set – and note that terminate() ends the engine for the life of the process, so anything else needing it must run first. Leaving it out instead orphans the MPI workers at 100% CPU.

Parameters:
  • lat (SuperspaceLattice) – Must be commensurate.

  • atoms (list[ModulatedAtom]) – The basic structure, listed in P1.

  • thickness (float) – Specimen thickness in Angstrom.

  • zone (tuple) – Zone axis in supercell indices.

  • xaxis (tuple) – In-plane reference reflection, supercell indices.

  • hv (float) – Accelerating voltage in kV.

  • gmax (float) – Beam-selection radius, inverse Angstrom. The supercell reciprocal lattice is dense; raising this is expensive.

  • path (path-like, optional) – Where to write the .dat; a temporary file when None.

  • max_order (int) – Largest satellite order when labelling reflections.

  • keep_engine (bool) – Skip terminate(), leaving the engine live.

  • backend (str) – Eigensolver, 'numpy' (default; any build) or 'fortran' (MPI build only).

  • **dat_kw – Passed through to write_dat().

Returns:

With keys H (n, 3+d superspace indices), hkl_super (n, 3), intensity (n,), order (n,), labelled (n,) bool, M, cell, nbeams and path.

Return type:

dict

pyextal.superspace.supercell_atoms(lat, atoms, M=None, wrap=True)

Freeze the modulation into an ordinary list of 3D atomic positions.

Each basic-cell atom is replicated onto every lattice coset of the supercell. Its internal coordinate follows from where it sits, xbar_j = q_j . (t + r), so each copy gets a different displacement – which is exactly what makes the supercell a superstructure rather than |det M| identical copies.

Parameters:
Returns:

(M, cell, sites) with sites a list of (label, element, position, B); position is (3,) in supercell fractional coordinates.

Return type:

tuple

Raises:

ValueError – If the structure is incommensurate, so no supercell exists.

pyextal.superspace.supercell_cell(lat, M)

Lattice parameters of the supercell.

Parameters:
  • lat (SuperspaceLattice) – Supplies the basic metric.

  • M (array-like) – (3, 3) integer supercell matrix; rows are the supercell direct lattice vectors in terms of the basic ones.

Returns:

(a, b, c, alpha, beta, gamma), Angstrom and degrees.

Return type:

tuple

pyextal.superspace.supercell_saed(lat, atoms, H, thickness=200.0, zone=(0, 0, 1), xaxis=(1, 0, 0), hv=200.0, M=None, path=None, keep_engine=False, backend='numpy', **dat_kw)

Dynamical pattern of the exact supercell, on the same beams as H.

The commensurate structure is an ordinary 3D crystal, so this needs nothing from the superspace machinery: the modulation is frozen into |det M| times as many atomic coordinates and the unmodified engine solves it. What it does need is the same beam set as the superspace calculation, or the two dynamical results would not be comparable – different beams means a different scattering problem. Each superspace reflection maps to the supercell index M @ S(H), which is integral exactly because the structure is commensurate, and that list is written out explicitly.

Agreement between this and dynamical_saed() on the same structure is a strong check on the superspace engine: one route freezes the modulation into 507 atoms and never mentions a satellite, the other keeps 3 atoms and integrates over internal space.

Parameters:
  • lat (SuperspaceLattice) – Must be commensurate.

  • atoms (list[ModulatedAtom]) – The basic structure in P1.

  • H (array-like) – (n, 3+d) superspace beam list.

  • thickness (float) – Specimen thickness, Angstrom.

  • zone (tuple) – Zone axis in supercell indices.

  • xaxis (tuple) – In-plane reference reflection, supercell indices.

  • hv (float) – Accelerating voltage, kV.

  • M (array-like, optional) – Supercell matrix; derived when None.

  • path (path-like, optional) – Where to write the .dat.

  • keep_engine (bool) – Skip terminate().

  • backend (str) – Eigensolver, 'numpy' (default; any build) or 'fortran' (MPI build only).

  • **dat_kw – Passed to write_dat().

Returns:

H (the beams, unchanged and in order), hkl_super, intensity normalised to the strongest diffracted beam, pattern (a DiffractionPattern, so the result renders through plot_image() like any other), M, cell, natoms and nbeams.

Return type:

dict

Raises:

ValueError – If the structure is incommensurate, or a beam does not map to an integral supercell index.

pyextal.superspace.write_dat(path, lat, atoms, zone=(0, 0, 1), xaxis=(1, 0, 0), hv=200.0, conv=0.0, samp=20, M=None, gmax=0.9, sgmax=3.0, sgmin=0.3, omeg=30.0, bmin=0.1, aper=None, abs_pot=0.0, name='approximant', norm=None, kt=(0, 0, 0), header=None, hkl=None)

Write the approximant as a .dat file the Bloch-wave engine can parse.

The file uses spg 1 1 – P1 with every atom listed – because the supercell’s symmetry is lower than the basic structure’s, and letting the engine expand a higher group would put atoms in the wrong places.

The .dat format is parsed sequentially and is order-sensitive, so the keywords below are emitted in the order the parser expects; see docs/source/configuration.md. sele chooses the beams geometrically, which is convenient but means the beam count moves with the voltage and the cell – and the supercell reciprocal lattice is |det M| times denser than the basic one, so a gmax that is unremarkable for the parent cell can select thousands of beams here. Start small and check what you got.

Parameters:
  • path (path-like) – Destination file.

  • lat (SuperspaceLattice) – The commensurate lattice.

  • atoms (list[ModulatedAtom]) – Basic-structure atoms, listed in P1.

  • zone (tuple) – Zone axis in supercell direct indices. With the basis SuperspaceLattice.supercell_matrix() returns, the parent’s [001] is still (0, 0, 1).

  • xaxis (tuple) – In-plane reference reflection, supercell indices.

  • hv (float) – Accelerating voltage in kV.

  • conv (float) – Convergence semi-angle in units of |g_xaxis|/|K|. Zero is a parallel beam, which is what a SAED pattern is.

  • samp (int) – Sampling across the disc.

  • M (array-like, optional) – Supercell matrix; derived when None.

  • gmax (float) – Beam-selection radius, inverse Angstrom.

  • sgmax (float) – Largest excitation error for beam selection.

  • sgmin (float) – Excitation error below which a beam is fully diagonalised.

  • omeg (float) – Bethe-potential parameter; larger is more accurate.

  • bmin (float) – Bethe potential cutoff.

  • aper (float, optional) – Output aperture radius; gmax when None.

  • abs_pot (float) – Mean absorption potential in volts.

  • name (str) – Crystal name written into the file.

  • norm (tuple, optional) – Surface normal; zone when None.

  • kt (tuple) – Tangential incident wavevector.

  • header (str, optional) – Extra comment lines at the top.

  • hkl (array-like, optional) – Explicit beam list in supercell indices, written as an hkl block instead of a sele line. Use it to make a supercell calculation solve the same scattering problem as a superspace one: pass the images of the superspace beams under M, and the two share a beam set so their intensities can be compared reflection by reflection.

Returns:

(path, M, cell, sites) – the file written plus its contents.

Return type:

tuple

class pyextal.superspace.ReflectionSet

Superspace reflections with their derived 3D quantities.

Variables:
  • H (np.ndarray) – (n, 3+d) integer superspace indices.

  • hkl (np.ndarray) – (n, 3) effective, generally non-integral, 3D reciprocal indices.

  • gmag (np.ndarray) – (n,) |S(H)| in inverse Angstrom.

  • order (np.ndarray) – (n,) satellite order – the rank of this reflection’s modulation length among the distinct lengths present, so 0 for a main reflection, 1 for the shortest satellites, and so on. Symmetry-equivalent satellites always share an order; see the module docstring for why sum |m| will not do.

  • satlen (np.ndarray) – (n,) |sum_j m_j q_j| in inverse Angstrom, the quantity order ranks.

  • d (int) – Number of modulation dimensions.

H: numpy.ndarray
hkl: numpy.ndarray
gmag: numpy.ndarray
order: numpy.ndarray
satlen: numpy.ndarray
d: int
property index_sum

(n,) sum |m_j|.

The conventional label, kept because it is what the literature usually quotes. It is not symmetry-invariant – do not filter on it.

Type:

np.ndarray

__len__()
__repr__()
select(mask)

A new set holding only the reflections mask keeps.

Parameters:

mask (np.ndarray) – Boolean or integer index array.

Returns:

The subset.

Return type:

ReflectionSet

main()

ReflectionSet: Just the main reflections.

satellites(order=None)

Satellites, optionally of one exact order.

Parameters:

order (int, optional) – Keep only this order; all satellites if None.

Returns:

The subset.

Return type:

ReflectionSet

labels()

list[str]: One formatted index per reflection.

pyextal.superspace.enumerate_reflections(lat, gmax, max_index=6, max_order=4, include_origin=True, condition=None)

All superspace reflections inside a resolution sphere.

Parameters:
  • lat (SuperspaceLattice) – Supplies the metric and the q-vectors.

  • gmax (float) – Largest |S(H)| to keep, inverse Angstrom.

  • max_index (int) – Half-width of the (h, k, l) search box.

  • max_order (int) – Satellite cutoff, as a multiple of the shortest modulation wave vector: |sum_j m_j q_j| <= max_order * min|q_j|. This is symmetry-invariant, unlike sum |m| – see the module docstring. The published 1T-TaS2 data reached order 6.

  • include_origin (bool) – Keep (0, 0, 0 | 0 ...). Defaults to True, since a diffraction pattern has a direct beam.

  • condition (callable, optional) – Reflection condition – takes the (n, 3+d) index array and returns a boolean mask of the allowed reflections. Systematically absent reflections come out with a zero structure factor anyway, since the centred copies cancel, so this saves work and keeps a listing readable rather than correcting anything. See pyextal.superspace.tas2.reflection_condition().

Returns:

Sorted by |S(H)|, ascending.

Return type:

ReflectionSet

Raises:

ValueError – If nothing falls inside gmax.

pyextal.superspace.format_index(H)

Render one superspace index as (h k l | m1 m2).

The bar keeps the main indices visually separate from the satellite ones, which matters when reading a list where most entries differ only after it.

Parameters:

H (array-like) – (3+d,) integer index.

Returns:

Formatted label.

Return type:

str

pyextal.superspace.coincidence_coset(lat, dh_max=1.05, max_order=24, tol=1e-06)

The satellite offsets that leave a reflection where it is.

c belongs to the coset when c . q is an integer reciprocal-lattice vector, so that (h, m) and (h - c.q, m + c) have the same effective index S(H) and are therefore the same physical reflection. The set is a lattice – infinite – so it has to be truncated.

The truncation is on the length of the shift |c . q|, not on the order sum |c_j|, and that is not a detail. The coset is closed under the crystal’s point group, but sum |c_j| is not invariant under it: cutting there admits some members of a symmetry orbit and not others, which breaks the pattern’s symmetry rather than restoring it. Measured on 1T-TaS2’s commensurate phase against the exact supercell, an order cut made the agreement worse than no coset at all (174% at order 4, against 41% uncorrected) while shell cuts improve monotonically. |c . q| is a reciprocal-space length and so is invariant, and truncating there keeps whole shells.

merge_coincident() handles the same degeneracy at the level of a finished reflection list; this is the form the dynamical calculation needs, because there the coincident amplitudes have to be summed inside every U(g,h) rather than afterwards.

For an incommensurate lattice only the identity qualifies, and the returned arrays are the single zero row – the no-op the engine starts from.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • dh_max (float) – Largest |c . q| to include, in inverse Angstrom. The default takes the first shell of shifts.

  • max_order (int) – How far to search in c; raise it only if a shell needs offsets larger than this, which the returned members make obvious.

  • tol (float) – How close c . q must be to integral.

Returns:

(c, dh), each (n, d) and (n, 3) of ints, ordered by increasing |c . q| with the identity at row 0.

Return type:

tuple[np.ndarray, np.ndarray]

pyextal.superspace.distinct_beams(lat, H, who='this calculation', warn=True)

Reduce a beam list to one entry per physical reflection.

A dynamical calculation must be given distinct reflections. On a commensurate lattice an enumerated list is not: several superspace indices name one reciprocal-lattice point, and handing them over as separate beams breaks the engine twice over. They become identical rows in the structure matrix, which is then singular – the eigensolver does not report that, it just returns an arbitrary basis in the degenerate subspace. And the .dat written from such a list repeats indices in its hkl block, so bloch_parse fails to resolve some out entries against it and appends them past the array allocated from the declared nbeams. That one is memory corruption, and it presents from a long way off: an MKL complaint about a CGEBAL parameter, a SIGSEGV inside CGEEV, or the child simply being killed.

Reducing costs nothing physically. U(g,h) already sums the coincident amplitudes internally – see coincidence_coset() – so the distinct reflections carry the whole structure.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • H (array-like) – (n, 3+d) superspace indices, as enumerated.

  • who (str) – Name used in the warning.

  • warn (bool) – Warn when the list actually shrinks.

Returns:

(m, 3+d) representatives, m <= n, in the input order.

Return type:

np.ndarray

pyextal.superspace.merge_coincident(lat, H, tol=1e-08)

Group superspace indices that are the same physical reflection.

For an incommensurate structure this never does anything: the map from (h k l m1 .. md) to S(H) is injective, which is what makes the indexing unique. For a commensurate one it is not – distinct indices land on the same reciprocal-lattice point, because the Fourier module has collapsed onto a 3D lattice. In 1T-TaS2’s commensurate phase, 36 of 85 positions in a modest beam list carry two indices.

That matters because the intensity at such a point is |F_1 + F_2 + ...|^2, a coherent sum of amplitudes. Treating the indices as independent reflections splits the intensity between them arbitrarily and destroys the pattern’s symmetry – symmetry-equivalent spots end up with different shares. It is the same degeneracy SuperspaceLattice.is_commensurate() warns about, seen from the diffraction side.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • H (array-like) – (n, 3+d) superspace indices.

  • tol (float) – Distance in reciprocal lattice units below which two effective indices count as the same reflection.

Returns:

(representatives, group, ngroups) – representatives is (ngroups, 3+d), taking the lowest-order index of each group; group is (n,) mapping every input row to its group; and ngroups is how many distinct reflections there are.

Return type:

tuple[np.ndarray, np.ndarray, int]

pyextal.superspace.satellite_indices(lat, max_order, tol=1e-06)

All (m_1..m_d) whose modulation vector is within max_order q-lengths.

The cutoff is |sum_j m_j q_j| <= max_order * min_j |q_j| – a length, measured in units of the shortest modulation wave vector. Because it is built from the invariant metric it keeps whole symmetry orbits, never part of one. For d = 1 it reduces to |m| <= max_order.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • max_order (int) – Cutoff in units of the shortest q-vector.

  • tol (float) – Relative slack, so a satellite exactly on the boundary is kept rather than lost to rounding.

Returns:

(n, d) integer satellite indices, including all-zero.

Return type:

np.ndarray

pyextal.superspace.satellite_length(lat, H)

|sum_j m_j q_j| for each reflection, in inverse Angstrom.

How far the satellite sits from its main reflection – zero for a main reflection, and equal for every member of a symmetry orbit.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • H (array-like) – (n, 3+d) superspace indices.

Returns:

(n,) lengths.

Return type:

np.ndarray

pyextal.superspace.satellite_metric(lat)

Metric on satellite indices: Q_jk = q_j . q_k in inverse Angstrom squared.

A symmetry operation permutes the q-vectors, so it preserves this form. Any cutoff or grouping built from it is therefore symmetry-invariant, which sum |m| is not.

Parameters:

lat (SuperspaceLattice) – Supplies the q-vectors and reciprocal metric.

Returns:

(d, d) positive-definite matrix.

Return type:

np.ndarray

class pyextal.superspace.SuperspaceLattice(cell, q, check_rank=True, tol=1e-06)

A 3D lattice plus d modulation wave vectors.

Variables:
  • cell (tuple) – The six lattice parameters of the basic (average) structure.

  • q (np.ndarray) – (d, 3) modulation wave vectors, in reciprocal lattice units of the basic structure.

  • d (int) – Number of modulation dimensions, 1 to 3.

  • gmx (np.ndarray) – (3, 3) direct metric tensor.

  • gmxr (np.ndarray) – (3, 3) reciprocal metric tensor.

MAX_D = 3
cell
q
gmx
gmxr
tol
_warn_on_rank()

Warn about a rank-deficient q set, distinguishing the two causes.

Commensurate q-vectors are rank-deficient too, but that is a legitimate thing to build – it is the approximant – so it gets its own, quieter message pointing at the supercell rather than telling the user they made a mistake.

property d

Number of modulation dimensions.

Type:

int

property dim

Total superspace dimension, 3 + d.

Type:

int

__repr__()
effective_hkl(H)

The 3D reciprocal-space index a superspace reflection actually sits at.

This is the whole point of the superspace description: a satellite is not at an integral position, so it cannot be named by three integers, but it is at a perfectly well defined place.

Parameters:

H (array-like) – (n, 3+d) integer superspace indices, or a single (3+d,) index.

Returns:

(n, 3) non-integral reciprocal indices (h, k, l) + sum_j m_j q_j, in units of the basic reciprocal cell.

Return type:

np.ndarray

Raises:

ValueError – If the last axis is not 3+d long.

g_magnitude(H)

Length of the scattering vector, inverse Angstrom.

Parameters:

H (array-like) – (n, 3+d) superspace indices.

Returns:

(n,) values of |S(H)|.

Return type:

np.ndarray

d_spacing(H)

Interplanar spacing 1/|S(H)| in Angstrom.

Parameters:

H (array-like) – (n, 3+d) superspace indices.

Returns:

(n,) spacings; inf for the direct beam.

Return type:

np.ndarray

static satellite_order(H)

Satellite order sum |m_j|; zero for a main reflection.

Parameters:

H (array-like) – (n, 3+d) superspace indices.

Returns:

(n,) integer orders.

Return type:

np.ndarray

is_main(H)

Whether each reflection is a main reflection.

Parameters:

H (array-like) – (n, 3+d) superspace indices.

Returns:

(n,) booleans.

Return type:

np.ndarray

integer_relations(max_n=6, tol=None)

Integer combinations of the q-vectors that land on the main lattice.

Each one found means two nominally distinct satellites fall at the same place, so the description carries a dimension the data does not. Searches n in a box of half-width max_n, taking the first of each +/- pair and skipping multiples of a relation already found.

Parameters:
  • max_n (int) – Half-width of the integer search box.

  • tol (float, optional) – Distance from a reciprocal lattice point, in reciprocal lattice units. The constructor’s tolerance when None.

Returns:

(n, g) pairs, n the (d,) integer coefficients and g the (3,) integer reciprocal lattice vector hit. Empty when the q-vectors are properly independent.

Return type:

list[tuple]

rationalise(max_denominator=256, tol=None)

Exact integer form of the q-vectors, when they are all rational.

Returns q_j = p_j / N with a single common denominator. This is what makes commensurability decidable rather than guessed at: a brute-force search for integer relations would have to reach n = 13 to notice that q = (3/13, 1/13, 1/3) is rational, and to n = 39 in three dimensions that is half a million candidates.

Parameters:
  • max_denominator (int) – Largest denominator to accept.

  • tol (float, optional) – How close the rational has to be; the constructor’s tolerance when None.

Returns:

(P, N) with P the (d, 3) integer numerators and N the common denominator, or None if any component is irrational to within tol.

Return type:

tuple or None

rank(tol=None)

Rank of the Fourier module beyond the three main dimensions.

Parameters:

tol (float, optional) – Relation tolerance; the constructor’s when None.

Returns:

The true number of independent modulation dimensions – d when the q-vectors are independent, 0 when they are all rational (a commensurate structure, which is a 3D supercell in disguise), and something between when only some directions are redundant.

Return type:

int

is_commensurate(tol=None)

Whether every q-vector is rational, so an exact 3D supercell exists.

Parameters:

tol (float, optional) – Rationality tolerance; the constructor’s when None.

Returns:

True when the modulation adds no incommensurate dimension.

Return type:

bool

supercell_matrix(max_denominator=64, tol=None)

The smallest 3D supercell in which every satellite is a main reflection.

For rational q-vectors the modulated structure is an ordinary periodic crystal with a larger cell, and that cell is the commensurate approximant – the one thing about a modulated structure the existing Bloch-wave engine can already handle, since it is plain 3D.

The rows of the returned matrix are the supercell’s direct lattice vectors in terms of the basic ones, so a reflection’s supercell index is M @ hkl and the volume ratio is |det M|. The rows are the three shortest independent integer vectors v with every v . q_j integral, which in a rank-3 lattice is guaranteed to be a basis.

Parameters:
  • max_denominator (int) – Largest denominator to consider when rationalising the q-vectors, and the half-width of the search.

  • tol (float, optional) – Rationality tolerance; the constructor’s when None.

Returns:

(3, 3) integer matrix, or None if the structure is genuinely incommensurate and no finite supercell exists.

Return type:

np.ndarray or None

_relation_text(n, g)

Human-readable form of one integer relation, for the warning.

pyextal.superspace.cell_to_metric(cell)

Direct-space metric tensor from the six lattice parameters.

Parameters:

cell (array-like) – (a, b, c, alpha, beta, gamma) with lengths in Angstrom and angles in degrees.

Returns:

(3, 3) direct metric tensor G, so that a vector with fractional coordinates x has length sqrt(x @ G @ x).

Return type:

np.ndarray

class pyextal.superspace.DisplaciveModulation(harmonics=(), d=1)

Bases: _FourierSeries

Positional modulation: a displacement field over internal space.

Variables:
  • harmonics (tuple[Harmonic]) – Terms with (3,) amplitudes, in fractional coordinates of the basic cell.

  • d (int) – Number of modulation dimensions.

evaluate(xbar)

Displacement at each internal coordinate.

Parameters:

xbar (np.ndarray) – (npts, d) internal coordinates.

Returns:

(npts, 3) displacement in fractional coordinates; all zeros when there are no harmonics.

Return type:

np.ndarray

max_amplitude()

float: Largest single-harmonic amplitude, for grid sizing.

class pyextal.superspace.Harmonic

One term of a Fourier series in the internal coordinate.

Variables:
  • n (tuple) – (d,) integer harmonic index.

  • sin (np.ndarray) – Amplitude of the sine term – (3,) for a displacive modulation, () for an occupational one.

  • cos (np.ndarray) – Amplitude of the cosine term, same shape as sin.

n: tuple
sin: numpy.ndarray
cos: numpy.ndarray
__post_init__()
property order

sum |n_j|, the harmonic order.

Type:

int

class pyextal.superspace.ModulatedAtom

One atom of the basic structure, with its modulation functions.

Variables:
  • label (str) – Free-form name, e.g. 'Ta1'.

  • element (str) – Symbol used to look up the scattering factor.

  • position (np.ndarray) – (3,) fractional coordinates in the basic cell.

  • occupancy (float) – Site occupancy of the basic structure; ignored when occupational is set, which carries its own average.

  • B (float) – Isotropic displacement parameter in Angstrom^2. Modulated ADPs are not implemented – see the module note in structurefactor.

  • displacive (DisplaciveModulation, optional) – Positional modulation.

  • occupational (OccupationalModulation, optional) – Occupancy modulation.

label: str
element: str
position: numpy.ndarray
occupancy: float = 1.0
B: float = 0.0
displacive: DisplaciveModulation = None
occupational: OccupationalModulation = None
__post_init__()
displacement(xbar)

Displacement field, zeros if the atom is not positionally modulated.

Parameters:

xbar (np.ndarray) – (npts, d) internal coordinates.

Returns:

(npts, 3) fractional displacements.

Return type:

np.ndarray

occupation(xbar)

Occupancy field, constant if the atom is not occupationally modulated.

Parameters:

xbar (np.ndarray) – (npts, d) internal coordinates.

Returns:

(npts,) occupancies.

Return type:

np.ndarray

class pyextal.superspace.OccupationalModulation(average=1.0, harmonics=(), d=1)

Bases: _FourierSeries

Occupancy as a function of internal coordinate.

Variables:
  • average (float) – The n = 0 term, i.e. the mean occupancy.

  • harmonics (tuple[Harmonic]) – Terms with scalar amplitudes.

  • d (int) – Number of modulation dimensions.

average
evaluate(xbar)

Occupancy at each internal coordinate.

Parameters:

xbar (np.ndarray) – (npts, d) internal coordinates.

Returns:

(npts,) occupancies.

Return type:

np.ndarray

class pyextal.superspace.DiffractionPattern(H, xy, intensity, order, sg, F, zone, xaxis, satlen=None)

A computed zone-axis pattern.

Variables:
  • H (np.ndarray) – (n, 3+d) superspace indices.

  • xy (np.ndarray) – (n, 2) in-plane coordinates, inverse Angstrom.

  • intensity (np.ndarray) – (n,) relative intensities, normalised so the strongest diffracted beam is 1. The direct beam is far above that and is meant to be clipped when plotted.

  • order (np.ndarray) – (n,) satellite order, ranked by modulation length so symmetry-equivalent satellites share it. See indexing.

  • satlen (np.ndarray) – (n,) |sum_j m_j q_j| in inverse Angstrom.

  • sg (np.ndarray) – (n,) excitation errors, inverse Angstrom.

  • F (np.ndarray) – (n,) complex structure factors.

  • zone (tuple) – The zone axis.

  • xaxis (tuple) – The in-plane reference reflection.

satlen
property index_sum

(n,) sum |m_j|; not symmetry-invariant, see order.

Type:

np.ndarray

__len__()
__repr__()
visible(rel=0.0001)

Reflections above a relative intensity threshold.

The threshold is measured against the strongest diffracted beam, not the direct one – intensities are already normalised that way, and the direct beam is orders of magnitude above it, so scaling to that would cut everything.

Parameters:

rel (float) – Fraction of the strongest diffracted intensity.

Returns:

Boolean mask.

Return type:

np.ndarray

brightest(n=20, exclude_origin=True)

Indices of the strongest reflections, for a quick look at a list.

Parameters:
  • n (int) – How many to return.

  • exclude_origin (bool) – Skip the direct beam, which dominates.

Returns:

Indices into the pattern arrays.

Return type:

np.ndarray

pyextal.superspace.marker_sizes(intensity, size=260, gamma=0.35, floor=1.5)

Marker areas for a diffraction pattern, saturating the direct beam.

Two things make a linear area-to-intensity map useless here. The direct beam is normalised against the strongest diffracted beam and the Mott-Bethe factor diverges as s -> 0, so its “intensity” is of order 1e7 – scaled linearly it becomes a single marker larger than the axes, and the whole plot comes out a solid block of colour. And satellites are ~1e-3 of a main reflection, so anything that renders the mains sensibly renders the satellites as nothing.

Intensity is therefore clipped at 1 (the direct beam saturates, exactly as it does on a recorded plate) and raised to gamma, the power stretch normally used to display a diffraction pattern.

Parameters:
  • intensity (array-like) – Relative intensities, mains near 1.

  • size (float) – Marker area for a saturated reflection.

  • gamma (float) – Power stretch. 1.0 is linear; smaller lifts weak reflections into view.

  • floor (float) – Smallest marker area, so a very weak reflection is still a visible dot rather than nothing.

Returns:

Marker areas in points squared.

Return type:

np.ndarray

pyextal.superspace.plot_image(pattern, ax=None, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, stretch=0.4, cmap='inferno', include_direct=True, saturate=True, title=None, rel_min=0.0, colorbar=False, vmax=None)

Draw a rendered pattern as an image.

Two things have to be handled or the picture is a black square with one white dot. The direct beam is some seven orders of magnitude above the strongest diffracted beam, so the display is scaled to the diffracted maximum and the centre is allowed to saturate – which is what a recorded plate does anyway. And satellites are ~1e-3 of a main reflection, so a power stretch lifts them into view.

Parameters:
  • pattern (DiffractionPattern) – What to draw.

  • ax (matplotlib.axes.Axes, optional) – Target axes.

  • shape (tuple) – Image size in pixels.

  • extent (float, optional) – Half-width of the field, inverse Angstrom.

  • sigma (float) – Gaussian width of the point spread, pixels.

  • gamma (float) – Lorentzian width, pixels.

  • radius (int, optional) – Kernel half-width.

  • stretch (float) – Display power. 1.0 is linear; smaller lifts the weak reflections. 0.4 shows satellites without washing out the mains.

  • cmap (str) – Colormap.

  • include_direct (bool) – Render the direct beam at all.

  • saturate (bool) – Scale the display to the strongest diffracted beam and let the direct beam clip. Turning this off scales to the true maximum, which is the direct beam, and shows little else.

  • title (str, optional) – Axes title.

  • rel_min (float) – Intensity cutoff before rendering.

  • colorbar (bool) – Add a colorbar keyed to the saturation level.

  • vmax (float, optional) –

    Intensity that maps to full scale, in the units of the returned image; everything above it clips. Chosen automatically when None – the strongest diffracted beam, which is what saturate describes.

    Pass it to brighten (smaller) or dim (larger) a plot, and above all to put several patterns on one scale: the automatic choice is made per pattern, so side-by-side panels are otherwise each normalised to their own maximum and cannot be compared. saturation_level() computes the automatic value without drawing anything.

Returns:

(ax, image) with image the unscaled intensity array.

Return type:

tuple

Raises:

ValueError – If vmax is not positive.

pyextal.superspace.plot_pattern(pattern, ax=None, size=260, rel_min=0.0001, label_orders=True, colors=None, annotate=0, title=None, gamma=0.35)

Draw a pattern, distinguishing main reflections from satellite orders.

Marker area is a power-stretched, saturating function of intensity; see marker_sizes() for why a linear map does not work.

Parameters:
  • pattern (DiffractionPattern) – What to draw.

  • ax (matplotlib.axes.Axes, optional) – Target axes; a new figure when None.

  • size (float) – Marker area for a saturated reflection.

  • rel_min (float) – Relative intensity below which a reflection is dropped.

  • label_orders (bool) – Show a legend keyed by satellite order.

  • colors (dict, optional) – Satellite order -> colour.

  • annotate (int) – Label this many of the strongest reflections with their superspace index. Zero draws none.

  • title (str, optional) – Axes title.

  • gamma (float) – Power stretch on intensity; smaller lifts the satellites.

Returns:

The axes drawn on.

Return type:

matplotlib.axes.Axes

pyextal.superspace.project_pattern(lat, H, zone=(0, 0, 1), xaxis=None)

In-plane coordinates of superspace reflections at a zone axis.

Shared by every route that produces a pattern – kinematic, the superspace engine, the exact supercell – so their images can be laid side by side and compared pixel for pixel. Two routes computing the same reflection at slightly different coordinates would be a subtle and very annoying bug.

Parameters:
  • lat (SuperspaceLattice) – The lattice.

  • H (array-like) – (n, 3+d) superspace indices.

  • zone (tuple) – Zone axis, direct-space indices.

  • xaxis (tuple, optional) – In-plane reference reflection. The shortest in-plane main reflection when None, which keeps the axis in the basic structure’s terms so it survives a change of q.

Returns:

(xy, xaxis) with xy (n, 2) in inverse Angstrom.

Return type:

tuple[np.ndarray, tuple]

Raises:

ValueError – If the zone admits no in-plane main reflection.

pyextal.superspace.pseudo_voigt_kernel(radius, sigma, gamma)

The pseudo-Voigt point spread the JAX refinement fits, on a pixel grid.

Identical in form to jaxEngine.postProcess.create_pseudo_voigt_kernel, including the Thompson-Cox-Hastings width combination and the eta approximation, so a sigma and gamma refined against experimental data can be handed straight to render_image() and mean the same thing. Widths are in pixels.

The kernel is tapered to zero on a disc rather than left square. Its Lorentzian part decays only as 1/r^2, so at any affordable radius it is still a per cent or so of the peak at the edge – and truncating that squarely puts a visible square halo around every strong reflection in the convolved image. A raised-cosine taper over the outer quarter removes it and costs nothing, since the discarded intensity is renormalised back.

Parameters:
  • radius (int) – Kernel half-width; the kernel is (2r+1) square, with the corners tapered away.

  • sigma (float) – Gaussian width, pixels.

  • gamma (float) – Lorentzian width, pixels.

Returns:

(2r+1, 2r+1) kernel summing to 1.

Return type:

np.ndarray

pyextal.superspace.render_image(pattern, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, rel_min=0.0, include_direct=True)

Render a pattern as a 2D intensity image, the way a plate records it.

A scatter plot draws markers whose area someone chose; this puts intensity on a pixel grid, so the result is an image in the same sense as the engine’s own diffraction output – something you can threshold, integrate, line-scan or compare against a recorded pattern.

Each reflection’s intensity is spread bilinearly over the four pixels around its position, so a peak that lands between pixels keeps its centroid and its total counts rather than snapping to a grid point. The whole image is then convolved with the pseudo-Voigt of pseudo_voigt_kernel().

The direct beam is orders of magnitude above everything else, so plot the result on a log or power scale – plot_image() does.

Parameters:
  • pattern (DiffractionPattern) – What to render.

  • shape (tuple) – (rows, cols) in pixels.

  • extent (float, optional) – Half-width of the field in inverse Angstrom. Taken from the pattern, with a margin, when None.

  • sigma (float) – Gaussian width of the point spread, in pixels.

  • gamma (float) – Lorentzian width, in pixels.

  • radius (int, optional) – Kernel half-width. ceil(6*max(sigma, gamma)) when None – generous because the Lorentzian component has long tails; pseudo_voigt_kernel() tapers whatever is left.

  • rel_min (float) – Drop reflections below this fraction of the strongest diffracted beam before rendering.

  • include_direct (bool) – Render the direct beam. It saturates everything; turning it off is often the more readable picture.

Returns:

The (rows, cols) image, and the (left, right, bottom, top) extent in inverse Angstrom, ready for imshow(..., extent=...).

Return type:

tuple[np.ndarray, tuple]

pyextal.superspace.saed_pattern(lat, atoms, zone=(0, 0, 1), gmax=1.6, max_index=8, max_order=4, thickness=200.0, hv=200.0, xaxis=None, kind='electron', sg_max=None, n_internal=None, condition=None, merge=None)

Compute a kinematic zone-axis pattern.

Parameters:
  • lat (SuperspaceLattice) – The superspace lattice.

  • atoms (list[ModulatedAtom]) – Structure, listed in P1.

  • zone (tuple) – Zone axis, direct-space indices.

  • gmax (float) – Resolution limit, inverse Angstrom.

  • max_index (int) – Half-width of the (h, k, l) search box.

  • max_order (int) – Largest satellite order.

  • thickness (float) – Specimen thickness in Angstrom, setting the relrod length 1/t. Larger means a thinner slice of reciprocal space contributes, so out-of-zone satellites fade.

  • hv (float) – Accelerating voltage in kV, for the Ewald sphere radius.

  • xaxis (tuple, optional) – In-plane reference reflection; the shortest in-plane main reflection when None.

  • kind (str) – 'electron' or 'xray' scattering factors.

  • sg_max (float, optional) – Discard reflections with |s_g| above this. 5 / thickness when None, past which the relrod contributes essentially nothing.

  • n_internal (int, optional) – Internal quadrature grid; auto when None.

  • condition (callable, optional) – Reflection condition, passed to enumerate_reflections().

  • merge (bool, optional) – Add the structure factors of superspace indices that are the same physical reflection before squaring. Only commensurate lattices have any, and for them it is not optional – the intensity there is |F_1 + F_2|^2, and treating the indices separately splits it arbitrarily and destroys the pattern’s symmetry. Decided from SuperspaceLattice.is_commensurate() when None, which is the right answer either way.

Returns:

The pattern.

Return type:

DiffractionPattern

Raises:

ValueError – If the zone admits no in-plane reflection within gmax.

pyextal.superspace.saturation_level(pattern, shape=(512, 512), extent=None, sigma=2.0, gamma=1.0, radius=None, rel_min=0.0)

The intensity plot_image() clips at when it chooses for itself.

The peak of the strongest diffracted beam once rendered and convolved. Useful for putting two patterns on one scale: plot_image picks this separately for each, so two panels drawn side by side are each normalised to their own maximum and their brightnesses cannot be compared. Compute it once and pass it to both as vmax:

level = saturation_level(kinematic, shape=(650, 650), sigma=3.0)
plot_image(kinematic, vmax=level, shape=(650, 650), sigma=3.0)
plot_image(dynamical, vmax=level, shape=(650, 650), sigma=3.0)

The render arguments must match the ones plot_image is given, since the convolution spreads a fixed number of counts and the peak height therefore depends on the grid and the kernel.

Parameters:
  • pattern (DiffractionPattern) – The pattern.

  • shape (tuple) – Image size in pixels.

  • extent (float, optional) – Half-width of the field, inverse Angstrom.

  • sigma (float) – Gaussian width of the point spread, pixels.

  • gamma (float) – Lorentzian width, pixels.

  • radius (int, optional) – Kernel half-width.

  • rel_min (float) – Intensity cutoff before rendering.

Returns:

Peak image intensity excluding the direct beam.

Return type:

float

pyextal.superspace.electron_form_factor(element, s, s_floor=1e-05)

Electron scattering factor by the Mott-Bethe relation.

f_e = MOTT * (Z_eff - f_x) / s^2, which diverges as s -> 0; the engine clamps s to 1e-5 there ([sfsub.f90:27]) and so does this, so the direct beam gets a large finite value rather than an infinity.

Z_eff is the Cromer-Mann parameterisation’s own zero-angle value, sum(a) + c, not the integer nuclear charge – which is what the Fortran engine uses (zt comes out as 72.95366 for Ta, against Z = 73). The difference is the fit’s residual at s = 0, and dividing it by s^2 turns it into a spurious tail that grows without bound as s -> 0: 0.8% of f_e for Ta already at s = 0.125. Since satellites sit close to their main reflections, that is exactly where they live. Using the integer Z here made the Python disagree with the engine by ~1e-3 across the board.

Parameters:
  • element (str) – Element symbol.

  • s (array-like) – |g| / 2 in inverse Angstrom.

  • s_floor (float) – Lower clamp on s.

Returns:

f_e(s) in Angstrom, same shape as s.

Return type:

np.ndarray

pyextal.superspace.xray_form_factor(element, s)

X-ray atomic form factor.

Parameters:
  • element (str) – Element symbol.

  • s (array-like) – |g| / 2 in inverse Angstrom, i.e. sin(theta)/lambda.

Returns:

f_x(s) in electrons, same shape as s.

Return type:

np.ndarray

pyextal.superspace.auto_n_internal(atoms, hkl, max_order, safety=4, lo=16, hi=256)

Choose a grid fine enough that the quadrature does not alias.

The integrand carries exp(2 pi i m.xbar), contributing bandwidth max|m|, and exp(2 pi i S.u(xbar)), whose Jacobi-Anger expansion has significant terms out to roughly |z| + 4 in each harmonic present, where z = 2 pi max|S.u|. The rectangle rule is exact below n/2, so the grid is sized at safety times the estimated bandwidth.

Parameters:
  • atoms (list[ModulatedAtom]) – Supplies the modulation amplitudes.

  • hkl (np.ndarray) – (n, 3) effective reciprocal indices.

  • max_order (int) – Largest satellite order in the reflection set.

  • safety (int) – Multiplier on the estimated bandwidth.

  • lo (int) – Floor on the returned value.

  • hi (int) – Ceiling, to keep n**d affordable.

Returns:

Points per internal dimension, rounded up to even.

Return type:

int

pyextal.superspace.bessel_reference(lat, H, amplitude, element='TA', position=(0.0, 0.0, 0.0), kind='electron', s_floor=1e-05)

Closed-form structure factor for one atom with a single harmonic.

For u(xbar) = A sin(2 pi xbar) in (3+1)D, Jacobi-Anger gives

Int_0^1 exp{2 pi i [m xbar + S.A sin(2 pi xbar)]} dxbar = J_-m(2 pi S.A)

exactly. This is the analytic check on the quadrature in structure_factor(); see test_superspace.py.

Parameters:
  • lat (SuperspaceLattice) – Must be (3+1)-dimensional.

  • H (array-like) – (n, 4) superspace indices.

  • amplitude (array-like) – (3,) sine amplitude in fractional coordinates.

  • element (str) – Element symbol for the scattering factor.

  • position (array-like) – (3,) fractional position of the single atom.

  • kind (str) – 'electron' or 'xray'.

  • s_floor (float) – Lower clamp on s.

Returns:

(n,) complex structure factors.

Return type:

np.ndarray

Raises:

ValueError – If lat is not (3+1)-dimensional.

pyextal.superspace.internal_grid(d, n)

Uniform sampling of the internal unit cell [0, 1)^d.

Parameters:
  • d (int) – Number of modulation dimensions.

  • n (int) – Points per dimension.

Returns:

(n**d, d) internal coordinates.

Return type:

np.ndarray

pyextal.superspace.structure_factor(lat, atoms, H, kind='electron', n_internal=None, max_points=4000000, s_floor=1e-05)

Structure factors of a modulated structure.

Parameters:
  • lat (SuperspaceLattice) – Supplies the metric and the q-vectors.

  • atoms (list[ModulatedAtom]) – The structure, listed in P1 – every atom of the basic cell, not just the asymmetric unit.

  • H (array-like) – (n, 3+d) integer superspace indices.

  • kind (str) – 'electron' for Mott-Bethe scattering factors, matching the Bloch-wave engine, or 'xray' for the Cromer-Mann values.

  • n_internal (int, optional) – Points per internal dimension. Sized by auto_n_internal() when None.

  • max_points (int) – Reflections are processed in chunks so that chunk * n_internal**d stays under this, bounding memory.

  • s_floor (float) – Lower clamp on s = |g|/2, as the engine uses.

Returns:

(n,) complex structure factors, in the units of the chosen scattering factor.

Return type:

np.ndarray

Raises:

ValueError – If kind is not recognised, or H has the wrong width.

class pyextal.superspace.SuperspaceOperation

One symmetry operation of a (3+d) superspace group.

Variables:
  • R (np.ndarray) – (3, 3) integer rotation on direct fractional coordinates.

  • tau (np.ndarray) – (3,) translation part, fractional.

  • epsilon (np.ndarray) – (d, d) integer action on internal coordinates.

  • tau_i (np.ndarray) – (d,) internal translation.

  • G (np.ndarray) – (d, 3) integer reciprocal lattice vectors from the q-transformation; all zero for the well-behaved settings.

  • name (str) – Optional label.

R: numpy.ndarray
tau: numpy.ndarray = None
epsilon: numpy.ndarray = None
tau_i: numpy.ndarray = None
G: numpy.ndarray = None
name: str = ''
__post_init__()
property d

Number of modulation dimensions.

Type:

int

resolve(lat, max_epsilon=3, tol=1e-06)

Fill in epsilon and G from the lattice, if not already given.

Parameters:
  • lat (SuperspaceLattice) – Supplies the q-vectors.

  • max_epsilon (int) – Half-width of the integer search for epsilon.

  • tol (float) – Tolerance on the residual being a lattice vector.

Returns:

A new operation with the internal part filled in.

Return type:

SuperspaceOperation

Raises:

ValueError – If R does not map the q-set onto itself.

pyextal.superspace.epsilon_for(R, q, max_epsilon=3, tol=1e-06)

Solve R^T q_j = sum_k epsilon_jk q_k + G_j for integers.

A 3D operation is only a symmetry of a modulated structure if it permutes the Fourier module, which is what this checks. The search is a small exhaustive one over integer epsilon rows – d <= 3 and the entries are tiny, so there are at most a few hundred candidates.

The decomposition is not unique when the q-vectors are dependent modulo the main lattice, which is exactly the 1T-TaS2 case: with three CDW arms satisfying q1 + q2 + q3 = c*, any solution can be shifted by a multiple of that relation. The smallest one is returned – minimum |epsilon| first, then minimum |G| – which picks out the plain permutation with G = 0 rather than some equivalent monster. Taking whichever candidate an unordered search happened to reach first would make the answer depend on iteration order.

Parameters:
  • R (array-like) – (3, 3) integer rotation on direct fractional coordinates.

  • q (array-like) – (d, 3) modulation wave vectors.

  • max_epsilon (int) – Half-width of the integer search per component.

  • tol (float) – How close the residual has to be to an integer vector.

Returns:

(epsilon, G), shapes (d, d) and (d, 3).

Return type:

tuple[np.ndarray, np.ndarray]

Raises:

ValueError – If some R^T q_j cannot be written this way, meaning R is not a symmetry of the modulated structure.

pyextal.superspace.expand_atoms(atoms, ops, lat, tol=1e-06)

Generate the symmetry-equivalent atoms and their modulation functions.

Under (R | tau) an atom moves to R r + tau and its displacement field becomes u'(xbar) = R u(epsilon^-1 (xbar - tau_i)). Written out in harmonics, the index maps as n -> epsilon^-T n and the amplitudes mix by the phase phi = 2 pi n' . tau_i:

A' = R (A cos phi + B sin phi)
B' = R (-A sin phi + B cos phi)
Parameters:
  • atoms (list[ModulatedAtom]) – The asymmetric unit.

  • ops (list[SuperspaceOperation]) – Operations, resolved or not.

  • lat (SuperspaceLattice) – Supplies the q-vectors for resolving.

  • tol (float) – Tolerance for recognising a duplicate position.

Returns:

The expanded set, duplicates removed.

Return type:

list[ModulatedAtom]

Raises:

NotImplementedError – If any operation has a nonzero G. See the module docstring – list the structure in P1 instead.

pyextal.superspace.transform_reflection(op, H)

Apply a superspace operation to reflection indices.

From R^T S(H) = (R^T h + G^T m) + sum_k (epsilon^T m)_k q_k, so the main part picks up the G contribution and the satellite part transforms by epsilon^T. Valid for any G.

Parameters:
  • op (SuperspaceOperation) – Operation with its internal part resolved.

  • H (array-like) – (n, 3+d) integer superspace indices.

Returns:

(n, 3+d) transformed indices.

Return type:

np.ndarray

Raises:

ValueError – If op has no internal part, or the widths disagree.