Python API
Signatures and docstrings of the public functions and classes in qoraphase at docs/site (b77d3f1), read from the source without importing it. The guides on this site say how the pieces fit together; this page is for looking up an argument.
qoraphase.algorithms.er
Error Reduction.
ER
python
@dataclass(frozen=True)
class ER(Iterative):
...``x <- P_S P_M x``. Monotone in the Fourier error, and slow.
ER.step
python
def step(self, state, problem)qoraphase.algorithms.hio
Hybrid Input-Output.
HIO
python
@dataclass(frozen=True)
class HIO(Iterative):
beta: float = 0.9``x <- P_M x`` inside the support, ``x - beta P_M x`` outside.
HIO.step
python
def step(self, state, problem)qoraphase.algorithms.raar
Relaxed Averaged Alternating Reflections.
RAAR
python
@dataclass(frozen=True)
class RAAR(Iterative):
beta: float = 0.9``x <- (beta/2)(R_S R_M x + x) + (1 - beta) P_M x`` with ``R_M = 2 P_M - I`` and ``R_S = 2 P_S - I``. ``beta -> 1`` is pure reflection (fast, unstable), ``beta -> 0`` degenerates to modulus projection.
RAAR.step
python
def step(self, state, problem)qoraphase.api
Public entry point.
default_recipe
python
def default_recipe() -> RecipeRAAR, a detwin pass, then shrinkwrapped RAAR and ER. The detwin runs before shrinkwrap starts updating the support. At the shrinkwrap settings shipped here the order makes little difference either way; it matters a great deal at looser settings, where a support that is still bloated gets cut in half and shrinkwrap -- which can only shrink -- locks the fragment in. Doing the cut first is simply the order with fewer ways to go wrong.
run
python
def run(problem: CDI, recipe=None, *, starts=1, batch=None, seed=None)Execute *recipe* on *problem*. See :func:`reconstruct`.
reconstruct
python
def reconstruct(intensity, *, starts: Literal[1]=1, **kwargs) -> Reconstruction
def reconstruct(intensity, *, starts: int, **kwargs) -> EnsembleArray in, reconstruction out.
Args:
intensity: measured intensity, center-origin, 2D or 3D.
support: boolean, ``True`` inside the object. ``None`` derives an
autocorrelation support.
mask: boolean, ``True`` marks *unusable* pixels.
recipe: ``None`` uses :func:`default_recipe`.
starts: number of random starts; >1 returns an :class:`Ensemble`.
batch: starts run simultaneously under ``vmap``. ``None`` runs them all
at once, which is fastest but can exhaust GPU memory on large 3D
data -- lower it if you hit OOM.
seed: PRNG seed; ``None`` means 0, not "random".qoraphase.core.ensemble
Multi-start results.
Ensemble
python
@dataclass(frozen=True)
class Ensemble:
runs: tuple
problem: object = None
history: tuple = ()A sequence of reconstructions that behaves like a Python collection. Runs are *not* mutually aligned as they come out of the solver: each one is the object up to its own global phase, translation and twin. Compare them by error or by agreement (:attr:`consensus`), or call :meth:`align` before doing anything that combines them. ``problem`` is carried so that a combined result can be scored against the original measurement; it is absent for hand-built ensembles. ``history`` holds the earlier generations of a :func:`~qoraphase.evolve` run, oldest first.
Ensemble.ranking
python
@property
def ranking(self) -> tupleRun indices ordered best (lowest error) first.
Ensemble.best
python
@property
def best(self) -> ReconstructionThe run with the lowest Fourier error. Runs stuck in different basins can fit the data equally well: on BCDI benchmark data the phase error of single runs split into two groups (about 0.08 and 0.2 rad) whose Fourier errors differed by 1e-3, so the lowest error picked either. :attr:`consensus` does not have that problem.
Ensemble.spread
python
@cached_property
def spread(self) -> tupleMean distance of each run from the others, after aligning them onto it. ``||align_to(other, run) - run|| / ||run||``, averaged over the other runs. A run in the basin most starts reach is close to the rest.
Ensemble.consensus
python
@property
def consensus(self) -> ReconstructionThe run closest to all the others (smallest :attr:`spread`). Needs no ground truth, only agreement between starts. With fewer than three runs there is no majority, and this is :attr:`best`.
Ensemble.align
python
def align(self, upsample: int=1, reference: Reconstruction | None=None) -> 'Ensemble'Bring every run onto *reference* (default :attr:`best`), in phase, position and branch. The reference supplies the convention, nothing more: if it happens to sit in the conjugate-flipped branch, the whole aligned ensemble does too. That is fine for combining runs, which is what this is for, and it is not a statement about which branch is real. ``upsample`` is passed to :func:`~qoraphase.postprocess.align_to`; the default aligns to whole pixels.
Ensemble.average
python
def average(self, upsample: int=1) -> ReconstructionCoherent mean of the runs aligned onto :attr:`consensus`. Aligning first is not optional: averaging raw runs cancels them against each other, because they differ by exactly the symmetries the solver cannot see. The reference is the consensus run rather than the best one: a lowest-error run from a minority basin would set the branch and position, and the majority would then be averaged onto it.
qoraphase.core.problem
The CDI problem: fixed reconstruction inputs.
CDI
python
@dataclass(frozen=True, init=False)
class CDI:
amplitude: jnp.ndarray
mask: jnp.ndarray
support: jnp.ndarray
model: object
def __init__(
self,
intensity=None,
*,
support=None,
mask=None,
model=None,
support_threshold: float = 0.1,
amplitude=None,
)Immutable reconstruction inputs. Holds no iteration state. Constructed from *intensity*; the stored field is the amplitude ``sqrt(clip(intensity, 0))`` so the square root is not recomputed inside the iteration loop. Registered as a pytree so it can cross ``jit`` / ``scan`` / ``vmap`` boundaries. ``model`` is a meta field: it is static and hashable.
CDI.intensity
python
@property
def intensity(self) -> jnp.ndarrayCDI.shape
python
@property
def shape(self) -> tupleCDI.reconstruct
python
def reconstruct(self, recipe=None, *, starts=1, batch=None, seed=None)qoraphase.core.result
Single reconstruction result.
Reconstruction
python
@dataclass(frozen=True)
class Reconstruction:
object: jnp.ndarray
support: jnp.ndarray
metrics: jnp.ndarray
coherence: jnp.ndarray | None = NoneOne reconstructed object plus its error history. The solution is only defined up to a global phase, a translation, and a conjugate flip (twin image). ``phase`` is therefore *not* an absolute phase; remove the ramp before interpreting it physically.
Reconstruction.amplitude
python
@property
def amplitude(self) -> jnp.ndarrayReconstruction.phase
python
@property
def phase(self) -> jnp.ndarrayReconstruction.error
python
@property
def error(self) -> floatFinal Fourier error; the ranking key.
Reconstruction.plot
python
def plot(self, axes=None)Amplitude / phase panels. 3D shows the central slice of each axis.
qoraphase.core.state
Iteration state and initialisation.
ReconState
python
class ReconState(NamedTuple):
obj: jnp.ndarray
support: jnp.ndarray
iteration: jnp.ndarray
key: jax.Array
coherence: jnp.ndarray | None = None
lowpass: tuple | None = NoneEverything that changes during iteration. Metrics are deliberately absent: the carry must be fixed-size, and metrics grow per iteration. They come out of ``lax.scan`` as stacked outputs instead.
ReconState.sigma
python
@property
def sigma(self) -> jnp.ndarray | NoneBlur width of the data at the current iteration, or None.
random_init
python
def random_init(key: jax.Array, problem, support: jnp.ndarray) -> jnp.ndarrayMeasured amplitude with uniformly random phase, back-transformed. Masked pixels start at zero magnitude: what is stored under the mask is not a measurement and must not steer the start.
initial_state
python
def initial_state(key: jax.Array, problem, obj: jnp.ndarray | None=None) -> ReconStateStart from *obj* (cut to the support) if given, else from random phases.
qoraphase.core.validate
Host-side validation. Runs on concrete numpy arrays before anything is traced, so failures surface as plain Python errors with useful messages instead of XLA shape errors.
check_inputs
python
def check_inputs(intensity, support, mask) -> Nonecheck_run
python
def check_run(recipe, starts: int, batch: int | None) -> Noneqoraphase.diagnostics
Pre-flight checks on a measurement. Everything here takes arrays and returns numbers -- no file handling, no reconstruction. The point is to catch the mistakes that otherwise surface as a plausible-looking but wrong result: a pattern that is not centred, an object that is not oversampled enough to be unique, a background subtraction that went negative. None of these are fatal on their own. They are the questions worth answering before believing a reconstruction.
looks_inverted
python
def looks_inverted(intensity: np.ndarray, mask: np.ndarray) -> boolTrue when the mask covers most pixels *and* most of the counts. A real mask covers zeros (module gaps, padded frames) or a few saturated pixels; a mask with its polarity inverted covers the pattern itself.
robust_peak_offset
python
def robust_peak_offset(intensity: np.ndarray) -> tupleLike :func:`peak_offset`, after capping every pixel at its brightest face neighbour. An isolated hot pixel or zinger then drops to the level around it, so it cannot decide where the pattern sits; a real peak, whose neighbours are bright too, barely moves. Two adjacent hot pixels still count as a peak.
inverted_mask_message
python
def inverted_mask_message(masked_fraction: float) -> strflip_about_origin
python
def flip_about_origin(a: np.ndarray) -> np.ndarrayReflect ``r -> -r`` for a center-origin array.
peak_offset
python
def peak_offset(intensity: np.ndarray) -> tupleOffset of the brightest pixel from the array centre, in pixels.
friedel_offset
python
def friedel_offset(intensity: np.ndarray) -> tupleCentring error inferred from Friedel symmetry, in pixels. A correctly centred pattern satisfies ``I(q) == I(-q)``. If instead the pattern sits at ``c``, correlating it with its own reflection peaks at ``2c`` -- so this resolves a half-pixel error that the brightest pixel cannot, and it works on broad patterns with no single dominant peak. Returns half the correlation peak position, rounded, so an odd result means the true offset is a half pixel.
autocorrelation_fill
python
def autocorrelation_fill(intensity: np.ndarray, threshold: float=0.1) -> tupleAutocorrelation extent per axis, as a fraction of the array. This is the quantity actually measured, and the honest one to read: a well-oversampled measurement puts the autocorrelation well inside the array, and a value approaching 1 means it no longer fits. An estimate, not a measurement -- it inherits the arbitrariness of *threshold*, and a noisy autocorrelation floor inflates the extent.
oversampling
python
def oversampling(intensity: np.ndarray, threshold: float=0.1) -> tupleLinear oversampling ratio per axis, from :func:`autocorrelation_fill`. The autocorrelation spans twice the object, so ``ratio = 2 / fill``. Read this together with the fill, not on its own. The autocorrelation cannot extend past the array, so ``fill <= 1`` and this ratio is **always at least 2** -- including on data that is badly undersampled, where the autocorrelation has simply been clipped. A ratio of 2.0 is not a pass; it means the estimate ran out of room.
Diagnosis
python
@dataclass(frozen=True)
class Diagnosis:
shape: tuple
dtype: str
peak_offset: tuple
friedel_offset: tuple
autocorrelation_fill: tuple
oversampling: tuple
total_oversampling: float
negative_fraction: float
masked_fraction: float
support_fraction: float
dynamic_range: float
warnings: tuple = field(default_factory=tuple)What the checks found. ``print()`` it.
diagnose
python
def diagnose(intensity, mask=None, support=None, threshold: float=0.1) -> DiagnosisCheck a measurement against the assumptions QoraPhase makes about it.
qoraphase.genetic
Genetic algorithm over generations of reconstructions. Several random starts are run, the best one is kept as the reference, every other run is aligned to it and bred with it, and the next generation restarts from the children instead of from random phases. Breeding formulas and the idea follow cohere's ``ga_utils`` / ``dvc_utils.breed``.
breed
python
def breed(mode: str, alpha: jnp.ndarray, beta: jnp.ndarray) -> jnp.ndarrayChild of *alpha* (the best run) and *beta*, which must already be aligned. Alignment (position, twin branch, global phase) is what :meth:`Ensemble.align` does; breeding unaligned runs mixes unrelated phases. Both are first rotated by the same global phase so that the mean phase of *alpha* is zero, which keeps the phase averages away from the +-pi wrap. ``_pa`` modes keep the phase of *alpha* and mix only the amplitudes.
evolve
python
def evolve(
problem: CDI,
recipe=None,
*,
starts: int = 8,
generations: int = 3,
breed: str = 'sqrt_ab',
cull: int | Sequence[int] = 0,
batch: int | None = None,
seed: int | None = None,
upsample: int = 10,
) -> EnsembleRun *recipe* for several generations, breeding with the best run in between.
Generation 0 is ``starts`` random starts. After each generation the
``cull[g]`` worst runs (by final error) are dropped, the rest are aligned to
the best (:meth:`Ensemble.align`, to ``1 / upsample`` pixel) and bred with it,
and the next generation runs *recipe* again from the children. The best run
is carried over unchanged. The returned :class:`Ensemble` is the last
generation; earlier ones are in its ``history``.
Args:
recipe: one recipe used for every generation, or a sequence with one per
generation; ``None`` uses :func:`default_recipe`.
generations: total generations, including the first.
breed: a key of :data:`BREEDERS`.
cull: an int (dropped after every generation) or a sequence indexed by
generation; missing entries are 0. The population must stay at one
run or more, and ``batch`` must divide every generation's size.
batch, seed: as in :func:`~qoraphase.reconstruct`. The same seed gives
the same result.
Children restart on the problem's support, not on the shrinkwrapped one of
the previous generation, and a partial-coherence kernel starts afresh.qoraphase.metrics.errors
Error metrics.
fourier_error
python
def fourier_error(
computed_magnitude: jnp.ndarray,
amplitude: jnp.ndarray,
mask: jnp.ndarray,
) -> jnp.ndarrayNormalised Fourier-space error over unmasked pixels only.
E_F = sqrt( sum_valid (|F(obj)| - amplitude)^2 / sum_valid amplitude^2 )
Takes the already-computed magnitude rather than the object, so that
ModulusProjection can hand over the FFT it has already paid for
(computing this separately would double the FFT cost).qoraphase.models.fft
Forward models: object space <-> detector space.
FFT
python
@dataclass(frozen=True)
class FFT:
...Unitary centered FFT. All public arrays are center-origin: index ``n // 2`` is the origin. The ``ifftshift`` / ``fftshift`` sandwich is entirely internal, so users never call ``fftshift`` themselves. ``adjoint(forward(x)) == x`` because ``norm="ortho"`` makes the transform unitary.
FFT.forward
python
def forward(self, obj: jnp.ndarray) -> jnp.ndarrayFFT.adjoint
python
def adjoint(self, wave: jnp.ndarray) -> jnp.ndarrayqoraphase.operators.coherence
Partial coherence: the measured intensity is the coherent one blurred by a kernel. With a partially coherent beam the detector records ``|F(obj)|^2 * K``, the coherent intensity convolved with a coherence kernel ``K`` (Clark et al., Nat. Commun. 3, 993 (2012)). Fitting a coherent model to such data fills the missing contrast with spurious structure in the object. Here ``K`` is estimated during the run by Richardson-Lucy deconvolution, and the modulus constraint compares the measurement with the blurred model instead.
convolve
python
def convolve(intensity: jnp.ndarray, kernel: jnp.ndarray) -> jnp.ndarrayCircular convolution of center-origin arrays; *kernel* may be smaller.
lucy_update
python
def lucy_update(kernel, coherent, measured, mask, iterations: int)Richardson-Lucy steps for ``measured = coherent * kernel``. The usual normalising sum is a constant here and drops out when the kernel is rescaled to unit sum after each step. Masked pixels do not vote.
blurred_magnitude
python
def blurred_magnitude(wave: jnp.ndarray, coherence) -> jnp.ndarray``sqrt(|wave|^2 * K)``, or ``|wave|`` when there is no kernel.
UpdateCoherence
python
@dataclass(frozen=True)
class UpdateCoherence(Stage):
iterations: int = 20Re-estimate the coherence kernel from the current object. The model intensity is scaled to the measured total first, so the kernel carries only the shape of the blur. Produces no error metric.
UpdateCoherence.run
python
def run(self, state, problem)PartialCoherence
python
@dataclass(frozen=True)
class PartialCoherence(Stage):
body: Recipe
size: int | tupleRun *body* with a coherence kernel, starting from a flat one. The kernel stays in the state afterwards, so later stages keep using it and the result reports it as ``Reconstruction.coherence``.
PartialCoherence.run
python
def run(self, state, problem)partial_coherence
python
def partial_coherence(*stages, kernel=7, lucy: int=20) -> PartialCoherence``qp.partial_coherence(qp.repeat(8, qp.RAAR(50), qp.Shrinkwrap()), kernel=7)`` Every iterative algorithm inside *stages* is preceded by a kernel update (*lucy* Richardson-Lucy steps), so the update interval is the length of the algorithm stages. *kernel* is the kernel size in pixels, an odd int or one per axis; it must not exceed the array. Start it after a few hundred coherent iterations: the first estimate comes from the current object.
qoraphase.operators.lowpass
Low-pass resolution progression: fit blurred data first, the data itself last. Early iterations see a Gaussian-smoothed version of the measured amplitude, and the blur narrows to the raw data over the run. This is cohere's ``lowpass_filter`` (``features.LowPassFilter``): the filter is applied to the *data*, not to the computed wave, and the sigma ramps linearly with the iteration number. Why the data and not the wave: the modulus constraint is a projection onto "amplitude equals the target". Smoothing the target gives a different, smoother constraint set that every algorithm projects onto unchanged, and the iterate is never filtered, so nothing but the target differs between filtered and unfiltered iterations. Smoothing the wave instead would put a lossy operator in the middle of the iteration, and it would not be a projection. A Gaussian blur of the amplitude in the detector is a Gaussian envelope on the object in real space. Speckle contrast drops and the large-scale structure of the pattern survives, so the early iterations are asked to explain less. The blur is in detector pixels, so a good sigma depends on the fringe spacing, roughly the array size divided by the object size.
filtered_amplitude
python
def filtered_amplitude(problem, sigma) -> jnp.ndarrayThe measured amplitude blurred to *sigma* pixels, ignoring masked pixels. Normalised convolution: masked pixels (beamstop, hot pixels) are left out of the average instead of dragging their stored value into their neighbours. Where no unmasked pixel is in reach the measured value is kept.
LowPass
python
@dataclass(frozen=True)
class LowPass(Stage):
body: Recipe
sigma: tupleRun *body* with the data blurred by a sigma that ramps from ``sigma[0]`` to ``sigma[1]`` over the iterations of *body*, then go back to the data. The schedule lives in the iteration state, so the body can be any mix of algorithms, shrinkwrap, detwin, phase limits and partial coherence. Error metrics inside the body are against the blurred data.
LowPass.run
python
def run(self, state, problem)low_pass
python
def low_pass(*stages, sigma=(2.0, 0.5)) -> LowPass``qp.low_pass(qp.RAAR(100), qp.Shrinkwrap(), sigma=(1.5, 0.0))`` *sigma* is the blur width in detector pixels at the first and at the last iteration of *stages*, interpolated linearly in between; a single number is a constant blur. A last sigma of 0 ends on the unblurred data. Metrics from these iterations are against the blurred data. To use it in one generation of :func:`~qoraphase.evolve` only, give that generation its own recipe.
qoraphase.operators.modulus
Modulus (Fourier magnitude) projection.
modulus_project
python
def modulus_project(obj: jnp.ndarray, problem, coherence=None, sigma=None)Project onto the measured-magnitude set. Returns ``(projected_obj, fourier_error)``. The error comes for free from the forward transform this projection already computed; a separate metric operator would double the FFT cost. Masked pixels keep their *computed* magnitude -- they are unconstrained, not zero. With a *coherence* kernel the measurement is compared with the blurred model intensity, and the wave is rescaled by the same ratio. With a *sigma* the measured amplitude is first low-passed to that width (see :func:`~qoraphase.low_pass`), and the error is against the filtered data.
ModulusProjection
python
@dataclass(frozen=True)
class ModulusProjection:
...Public handle for :func:`modulus_project`.
qoraphase.operators.phase
Phase constraint: confine the object's phase to a range during a run.
PhaseLimited
python
@dataclass(frozen=True)
class PhaseLimited(Stage):
algorithm: Iterative
low: float
high: floatRun *algorithm* with its support narrowed, at every iteration, to the pixels whose current phase lies in ``[low, high]`` (radians). The narrowed support is used for that iteration only; the stored support is left alone, so shrinkwrap and later stages see the full one. Phase is measured from zero, so a range around zero also pins the global phase. This is cohere's ``phc`` and the "phase constraint" of the legacy MATLAB code; a weakly strained crystal starts close to real, and keeping it there early on discourages solutions with wild phase.
PhaseLimited.run
python
def run(self, state, problem)phase_limit
python
def phase_limit(low: float, high: float, *stages) -> Recipe``qp.phase_limit(-0.5, 0.5, qp.repeat(10, qp.HIO(50), qp.Shrinkwrap()))`` Every iterative algorithm inside *stages*, however deeply nested, runs under :class:`PhaseLimited`; everything else runs unchanged.
qoraphase.operators.shrinkwrap
Shrinkwrap support update.
gaussian_blur
python
def gaussian_blur(array: jnp.ndarray, sigma: float) -> jnp.ndarraySeparable Gaussian blur applied in Fourier space. Works for any ndim. ``sigma`` is in pixels; the transfer function is ``exp(-2 (pi sigma f)^2)`` with ``f`` in cycles/pixel, which is 1 at f=0 and therefore preserves the sum.
Shrinkwrap
python
@dataclass(frozen=True)
class Shrinkwrap(Stage):
threshold: float = 0.3
sigma: float = 1.0
method: str = 'max'Re-derive the support from the current object magnitude.
The blurred amplitude is thresholded at ``threshold`` times a reference
level, chosen by *method*:
``"max"``
the largest blurred amplitude. The classic Marchesini formulation, and
the reference drifts down as the reconstruction sharpens.
``"rms"``
the root-mean-square blurred amplitude inside the current support. A
bright outlier lifts this too -- it is inside the support after all --
but diluted across every pixel rather than setting the reference on its
own, so the support update is harder to destabilise.
The two thresholds are on different scales and do not transfer: PyNX uses
0.28 for ``"rms"``, and the classic Marchesini ``"max"`` value is 0.2.
The defaults here are 0.3 / 1.0, not 0.2 / 2.0. On a loose autocorrelation
support -- the case that actually needs shrinkwrap -- 0.2 / 2.0 never
converged in testing while 0.3 / 1.0 did, across every object tried. That
measurement is on synthetic data, so treat it as a starting point and not a
law; these two numbers are the first thing to re-tune on a real dataset.
Produces no error metric, so it contributes nothing to the metric history.Shrinkwrap.run
python
def run(self, state, problem)qoraphase.operators.support
Support projection and autocorrelation support estimate.
support_project
python
def support_project(obj: jnp.ndarray, support: jnp.ndarray) -> jnp.ndarrayZero everything outside the support.
SupportProjection
python
@dataclass(frozen=True)
class SupportProjection:
...Public handle for :func:`support_project`. v0.1 applies no real/positivity constraint -- the object is complex. A ``constraint=`` argument belongs here when that changes.
half_support
python
def half_support(support: jnp.ndarray, axis: int=0) -> jnp.ndarrayKeep the half of *support* below its centre of mass along *axis*. Used for detwinning: a support cut in half is no longer symmetric under ``r -> -r``, so it fits only one of the two conjugate solutions. Cutting at the centre of mass rather than the array centre keeps the cut on the object even when the object sits off-centre.
autocorrelation_support
python
def autocorrelation_support(intensity: jnp.ndarray, threshold: float=0.1) -> jnp.ndarrayDefault support when the user supplies none. The autocorrelation is about twice the object size, so this is a loose starting support and relies on shrinkwrap to tighten it. A measured support converges better; pass one when you have it.
qoraphase.postprocess
Resolving the CDI ambiguities after a reconstruction. A reconstruction is determined only up to a global phase, a translation, and a conjugate flip. Nothing during iteration can fix that -- all three are exact symmetries of the constraints -- so it is dealt with here, once, on concrete arrays. These run on the host and are not part of any traced loop. A linear phase across the object means its Fourier transform is centred off the array centre. :func:`remove_ramp` measures that offset from the data and cancels it; it does not know the diffraction geometry, so it removes whatever ramp is there, including one that carries physical information (strain, a displaced Bragg peak).
flip_about_origin
python
def flip_about_origin(x: jnp.ndarray) -> jnp.ndarrayReflect ``r -> -r`` for a center-origin array. Reversing the axes is not enough: with the origin at index ``n // 2`` the reflection maps ``i`` to ``(n - i) % n``, i.e. the reversal rolled by one.
twin
python
def twin(x: jnp.ndarray) -> jnp.ndarrayThe conjugate-flipped solution, indistinguishable from ``x`` in the data.
register
python
def register(obj, reference, upsample: int=1)Shift and peak value that best match *obj* onto *reference*. Returns ``(shift, value)``: ``obj`` is *reference* displaced by ``+shift`` (in pixels), and ``value`` is the complex correlation at that shift, whose angle is the global phase between the two. The whole-pixel peak comes from the FFT cross-correlation. With ``upsample > 1`` it is then refined in decades around the peak, evaluating the correlation only on a small grid by matrix-multiply DFT (Guizar-Sicairos et al., Opt. Lett. 33, 156, 2008), down to a step of ``1 / upsample`` pixel or finer.
align_to
python
def align_to(obj: jnp.ndarray, reference: jnp.ndarray, upsample: int=1) -> jnp.ndarrayTransform *obj* into the equivalent solution closest to *reference*. The translation and global phase come from the cross-correlation peak, and the branch is chosen by taking whichever of ``obj`` and ``twin(obj)`` correlates more strongly. This is a *relative* operation, and the twin part of it especially so: it adopts whatever branch *reference* is in. It does not, and cannot, tell you which branch is physically correct -- both fit the measurement exactly. What it does give you is a common representation, which is all that averaging an ensemble needs. With the default ``upsample=1`` the translation is to whole pixels. A larger value locates the peak to about ``1 / upsample`` pixel (see :func:`register`) and applies the remainder as a Fourier shift, which is exact only for an object that does not reach the array edge.
remove_ramp
python
def remove_ramp(
obj,
support=None,
*,
pad: int = 2,
iterations: int = 3,
zero_phase: bool = False,
)Remove the linear phase ramp by centring the object's Fourier peak. The ramp is read off as the centre of mass of ``|F|^2`` of the zero-padded object (``pad`` times larger per axis, which sets how finely the offset is resolved), and cancelled with a phase ramp that is zero at the array centre. The offset is a real number, so this is a sub-pixel shift of the spectrum. The centre of mass is unbiased when the ramp-free spectrum is centrosymmetric (a real object); for a general complex object it is an estimate. A spectrum displaced from the centre is cut off asymmetrically by the array edge, which pulls the centre of mass towards zero. Each pass removes most of the ramp and therefore most of that bias, so ``iterations`` passes are made. *support*, if given, restricts the measurement to the object (noise outside it would otherwise pull the peak); the returned array is not masked. With ``zero_phase`` the global phase is then set so that the mean phase inside the support, weighted by amplitude, is zero. Without a support the voxels above a tenth of the peak amplitude stand in for it. Traceable: usable under ``jit`` with ``pad``, ``iterations`` and ``zero_phase`` static.
qoraphase.prtf
Phase retrieval transfer function. Independent reconstructions agree on the Fourier components the data pins down and disagree elsewhere. Averaging their transforms, after removing the ambiguities, keeps the first and washes out the second, so the ratio of the averaged amplitude to the measured one falls from 1 towards 0 as the runs stop agreeing (Chapman et al., J. Opt. Soc. Am. A 23, 1179, 2006).
PRTF
python
@dataclass(frozen=True)
class PRTF:
frequency: jnp.ndarray
value: jnp.ndarray
crossing: floatRadially averaged PRTF. ``frequency`` holds the shell centres in cycles per object pixel and ``value`` the PRTF in each shell (nan for a shell with no usable pixel). ``crossing`` is the frequency at which ``value`` first falls below 1/e, linearly interpolated, or nan if it never does; ``resolution`` is its reciprocal, a full period in object pixels.
PRTF.resolution
python
@property
def resolution(self) -> floatprtf_curve
python
def prtf_curve(objects, intensity, mask=None)PRTF of already-aligned *objects* as ``(frequency, value, crossing)``. ``value`` is the ratio, per shell, of the summed amplitude of the mean transform to the summed measured amplitude, over unmasked pixels. Summing before dividing keeps near-empty pixels from dominating a shell. Pure ``jnp``: traceable under ``jit``.
prtf
python
def prtf(objects, intensity, mask=None, *, align: bool=True, upsample: int=100) -> PRTFPRTF of an :class:`~qoraphase.Ensemble` or a sequence of objects. With ``align=True`` every run is first brought onto the reference (the best run of an ensemble, the first of a sequence) in phase, position and twin branch, to ``1 / upsample`` pixel. Sub-pixel alignment matters here: a residual shift of a fraction of a pixel is a phase error that grows with frequency and would read as lost resolution. Pass ``align=False`` for objects that are already aligned. *intensity* and *mask* are the measurement (``mask=True`` marks an unusable pixel, which is left out). The curve is meaningful only for two or more runs; a single run has a PRTF of 1 by construction.
qoraphase.recipes.recipe
Stage protocol and declarative Recipe. Code order is execution order. No operator-overloading DSL.
Stage
python
class Stage:
...Anything a Recipe can run. ``run(state, problem) -> (new_state, metrics)`` where ``metrics`` is a 1-D float array with one entry per iteration that produced one. Stages that produce no error metric (Shrinkwrap) return an empty array.
Stage.run
python
def run(self, state, problem)Iterative
python
@dataclass(frozen=True)
class Iterative(Stage):
n: intA stage that repeats ``step`` ``n`` times under ``lax.scan``. ``n`` is the scan length and therefore static: changing it recompiles.
Iterative.step
python
def step(self, state, problem)Iterative.run
python
def run(self, state, problem)Recipe
python
@dataclass(frozen=True, init=False)
class Recipe(Stage):
stages: tuple
def __init__(self, *stages, **kwargs)Ordered sequence of stages.
Recipe.run
python
def run(self, state, problem)Repeat
python
@dataclass(frozen=True)
class Repeat(Stage):
n: int
body: RecipeRun ``body`` ``n`` times.
Repeat.run
python
def run(self, state, problem)Detwin
python
@dataclass(frozen=True)
class Detwin(Stage):
body: Recipe
axis: int = 0Run ``body`` against a support cut in half, then restore the support. The twin ``conj(obj(-r))`` fits the measurement exactly, so nothing in the data prefers one branch; a centrosymmetric support does not separate them either and the iteration stalls in a mixture. Halving the support removes that symmetry for a few cycles and biases the solution into one branch, after which the full support is restored. Which branch it lands in follows from the cut, not from the data.
Detwin.run
python
def run(self, state, problem)repeat
python
def repeat(n: int, *stages) -> Repeat``qp.repeat(10, qp.RAAR(50), qp.Shrinkwrap())``
detwin
python
def detwin(*stages, axis: int=0) -> Detwin``qp.detwin(qp.HIO(20))`` -- break the twin symmetry, then carry on.