Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Sources/CuboBragg/CuboBragg.docc/CuboBragg.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ is the median fill of non-finite pixels, public.

## Topics

### Essentials

- <doc:Finding-Bragg-disks>

### Detection

- ``BraggDisks``
Expand All @@ -32,12 +36,16 @@ is the median fill of non-finite pixels, public.
- ``BraggFunnel``
- ``BraggVectors``
- ``BraggPeak``
- ``BraggScanResult``
- ``BraggFailurePolicy``
- ``BraggMasking``

### Options

- ``BraggDiskOptions``
- ``BraggSubpixel``
- ``BraggPeakOrder``
- ``BraggTieOrder``
- ``BraggNonFinitePixels``
- ``BraggPrecision``

Expand Down
44 changes: 44 additions & 0 deletions Sources/CuboBragg/CuboBragg.docc/Finding-Bragg-disks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Finding Bragg disks

Bragg disk detection correlates each diffraction pattern with a kernel built from the probe, keeps the maxima of the
correlation, and refines their positions to a fraction of a pixel. It is py4DSTEM's `find_Bragg_disks`. The kit splits it
into three steps, so a caller can stop after any of them.

1. **Build a kernel.** `ProbeKernel` in CuboVirtual turns a vacuum probe (a ``DetectorImage`` you measured, or a mean
pattern) into a corner-centred kernel. `ProbeKernel.flat` is py4DSTEM's default; `ProbeKernel.gaussian` and
`ProbeKernel.sigmoid` subtract an edge trench.
2. **Detect.** ``BraggDisks/detect(_:kernel:options:)`` returns the peaks of one pattern, brightest first. Each
``BraggPeak`` has a `row`, a `column` and an `intensity`, in detector pixels. To correlate many patterns against one kernel,
make a ``BraggDetector`` once and call ``BraggDetector/detect(_:options:)`` on each.
3. **Scan the cube.** ``BraggDisks/detectAll(in:kernel:options:tiling:)`` reads a cube a scan tile at a time and returns
``BraggVectors``: one peak list per scan position, indexed by `vectors[scanRow, scanColumn]`.

```swift
import CuboBragg
import CuboCore
import CuboVirtual
import Foundation

let cube = try MappedCube(npy: URL(fileURLWithPath: "cube.npy"))
let mean = try await VirtualDiffraction.mean(of: cube)
let kernel = try ProbeKernel.flat(mean) // corner-centred, sums to 1
let first = try BraggDisks.detect(mean, kernel: kernel) // [BraggPeak] of one pattern
let vectors = try await BraggDisks.detectAll(in: cube, kernel: kernel) // BraggVectors for the whole scan
let peaks = vectors[0, 0] // the peaks at scan position (0, 0)
```

``BraggDiskOptions`` defaults are py4DSTEM's. Choose the subpixel method with ``BraggSubpixel`` and the Float32 path with
`options.precision = .single` (``BraggPrecision``). Float32 needs a detector size vDSP supports; ask first with
``BraggDetector/supportsSinglePrecision(detectorRows:detectorColumns:)``.

## When a result is short or empty

``BraggDetector/detectWithFunnel(_:options:)`` returns the peaks and a ``BraggFunnel``, the count of maxima left after
each stage of the selection. The stage where the count drops to zero names the option to change.

For a whole cube, ``BraggDisks/detectAll(in:kernel:options:tiling:failurePolicy:isCancelled:progress:)`` can record a pattern
that fails (``BraggFailurePolicy/recordEmpty``) instead of stopping, and returns a ``BraggScanResult`` that lists the
failed positions.

The detected peaks are in detector pixels from the corner. To measure strain, centre them on the lattice origin with
`StrainMapping.centredPeaks` in CuboStrain.
35 changes: 35 additions & 0 deletions Sources/CuboEmbedding/CuboEmbedding.docc/Clustering-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Clustering patterns

An embedding turns many patterns into a few numbers each and groups similar ones. The kit does the two numerical steps on
row-major `[Double]` matrices, one row for each pattern. Reading patterns from a cube and turning each into a feature
vector is the caller's.

``PCA/fit(_:rows:columns:components:variant:)`` returns a ``PCAResult``: the `mean`, the `components` (one row each, in
descending order of variance), `explainedVariance`, `explainedVarianceRatio` and the `scores`, which are the data projected
on the components. ``PCA/transform(_:rows:result:)`` projects further rows on the same result.

``KMeans/fit(_:rows:columns:initialCentroids:maxIterations:tolerance:isCancelled:)`` runs Lloyd's algorithm from centroids
you give it, and returns a ``KMeansResult`` with a `labels` entry for every row, the `centroids`, the `inertia`, and whether
it `converged`.

```swift
import CuboEmbedding

// Six rows with three features: two groups, one near (0, 0, 0) and one near (5, 5, 5).
let data: [Double] = [0, 0.1, 0, 0.1, 0, 0.1, 0, 0, 0.2,
5, 5.1, 5, 5.1, 5, 5.1, 5, 5, 5.2]
let pca = try PCA.fit(data, rows: 6, columns: 3, components: 2)
let result = try KMeans.fit(pca.scores, rows: 6, columns: 2,
initialCentroids: Array(pca.scores[0..<2]) + Array(pca.scores[6..<8]))
print(result.labels) // [0, 0, 0, 1, 1, 1]
```

The default variant, ``PCA/Variant/scikitLearn``, follows scikit-learn 1.4.2, including its component signs.
``PCA/Variant/diffractionEmbeddingApp`` follows mac4DSTEM's own Float32 numerics. ``KMeans/fitSeeded(_:rows:columns:groups:seed:maxIterations:isCancelled:)``
chooses the first centroids from a seed, as the app does. scikit-learn's random seeding is not provided.

## When the patterns do not fit in memory

``PCACovarianceAccumulator`` adds the patterns block by block, so only one block is held. ``PCA/fit(_:components:)`` fits
from the accumulated covariance. The components then have the sign of the eigensolver, not scikit-learn's: pass a second
pass over the data through ``PCASignResolver`` and call `resolved()` for the scikit-learn signs.
13 changes: 13 additions & 0 deletions Sources/CuboEmbedding/CuboEmbedding.docc/CuboEmbedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ Accelerate implementation, and ``KMeans`` takes an `isCancelled` closure.

## Topics

### Essentials

- <doc:Clustering-patterns>
- <doc:Scope>

### Principal components and k-means

- ``PCA``
- ``PCAResult``
- ``KMeans``
- ``KMeansResult``
- ``EmbeddingError``

### Streamed PCA

- ``PCACovarianceAccumulator``
Expand Down
31 changes: 31 additions & 0 deletions Sources/CuboParallax/CuboParallax.docc/Aligning-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Aligning images

Parallax reconstruction starts from a stack of bright-field images, one for each detector pixel inside the bright-field
disk, and shifts each onto a reference. ``ParallaxAlignment`` is the first step, and works on any two images of the same
size, so you can use it before running the whole pipeline.

Images are row-major `[Double]` arrays. ``ParallaxAlignment/shift(reference:moving:rows:columns:upsampleFactor:)``
estimates the translation that relates `moving` to `reference`: a cross-correlation peak found on the pixel grid, then
refined by a matrix Fourier transform to `1 / upsampleFactor` of a pixel (factor 8 by default, as in py4DSTEM 0.14.19).
It returns a ``ParallaxShift`` with a `row` and a `column`. Positive values move content toward increasing row and column
indices. A constant image has no correlation peak, so the call throws.

``ParallaxAlignment/shifted(_:rows:columns:by:)`` applies a shift in the Fourier domain.

```swift
import CuboParallax

let rows = 64, columns = 64
var reference = [Double](repeating: 0, count: rows * columns)
for row in 28..<36 { for column in 28..<36 { reference[row * columns + column] = 1 } } // a square
let moving = try ParallaxAlignment.shifted(reference, rows: rows, columns: columns,
by: ParallaxShift(row: 2.5, column: -1))
let found = try ParallaxAlignment.shift(reference: reference, moving: moving, rows: rows, columns: columns)
print(found.row, found.column) // the size of the shift that was applied; the sign follows the convention above
```

To align a whole stack, ``ParallaxAlignment/align(_:reference:rows:columns:upsampleFactor:runningAverage:isCancelled:progress:)``
takes the images, a reference and the same factor. It accepts `isCancelled` and `progress` closures, and a cancelled call
throws `CancellationError`. To estimate many shifts of the same size, ``ParallaxFourierShifter`` builds the FFT plan once.

For the whole bright-field pipeline, see ``ParallaxReconstruction/process(_:shape:reciprocalSampling:scanSampling:energyEV:options:isCancelled:progress:)``.
5 changes: 4 additions & 1 deletion Sources/CuboParallax/CuboParallax.docc/CuboParallax.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ stack, shifts and probe angles as separate calls. Defaults are py4DSTEM's; the p

## Topics

### Essentials

- <doc:Aligning-images>

### End to end

- ``ParallaxReconstruction``
Expand Down Expand Up @@ -75,5 +79,4 @@ stack, shifts and probe angles as separate calls. Defaults are py4DSTEM's; the p
### Errors

- ``ParallaxError``

- ``ParallaxAlignmentError``
4 changes: 4 additions & 0 deletions Sources/CuboPtycho/CuboPtycho.docc/CuboPtycho.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ CPU ptychography preprocessing, probe construction and single-slice, mixed-state

## Topics

### Essentials

- <doc:Preparing-a-reconstruction>

### Processing

- ``PtychographyPreparation``
Expand Down
28 changes: 28 additions & 0 deletions Sources/CuboPtycho/CuboPtycho.docc/Preparing-a-reconstruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Preparing a reconstruction

Ptychography fits an object and a probe to the measured patterns. Before an engine runs, three inputs must exist: the
amplitudes of the patterns, the scan positions in the object's pixels, and a starting probe. ``PtychographyPreparation``
makes the first two from a cube, and ``PtychographyProbe`` the third.

Sampling is in ångström. ``PtychographyPreparation/objectSampling(wavelength:angularSampling:size:)`` gives the object's
pixel size from the electron wavelength, the angular sampling of the detector in milliradians per pixel, and the pattern
size. ``PtychographyProbe/build(rows:columns:rowSampling:columnSampling:wavelength:cutoff:rolloff:defocus:astigmatism:astigmatismAngle:sphericalAberration:higherOrder:)``
builds a unit-norm probe with an aperture of `cutoff` radians and its aberrations. Both are in Å, and the probe is
corner-centred. It is a ``PtychographyArray`` of split real and imaginary parts.

```swift
import CuboPtycho

let wavelength = 0.0197 // 300 keV, in Å
let sampling = PtychographyPreparation.objectSampling(wavelength: wavelength, angularSampling: 0.5, size: 128)
let probe = try PtychographyProbe.build(rows: 128, columns: 128, rowSampling: sampling, columnSampling: sampling,
wavelength: wavelength, cutoff: 0.02, defocus: 100)
print(probe.rows, probe.columns)
```

``PtychographyPreparation/prepare(source:origins:geometry:objectSamplingRow:objectSamplingColumn:wavelength:semiangle:options:)`` and its options (``PtychographyPreparationOptions``) do the
whole preparation of a cube in one call: amplitudes, positions, object shape and the starting probe. They return a
``PreparedPtychography``. The three engines then take the same inputs: ``SingleslicePtychography``,
``MixedstatePtychography`` and ``MultislicePtychography``.

What the module does and does not provide is listed in <doc:Scope>.
33 changes: 33 additions & 0 deletions Sources/CuboStrain/CuboStrain.docc/CuboStrain.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,36 @@ Detector row is the reference first coordinate; detector column is the second.
For a whole scan, ``StrainMapping`` follows py4DSTEM's `StrainMap`: build the Bragg vector map from centred peaks,
choose the basis from its maxima, fit every scan position, take the reference from a scan region and return flat
row-major exx, eyy, exy and theta maps (NaN where a position could not be fitted), with an optional coordinate rotation.

## Topics

### Essentials

- <doc:From-peaks-to-strain>
- <doc:Scope>

### Per-pattern lattice fitting

- ``LatticeStrain``
- ``LatticeBasis``
- ``LatticeVector``
- ``LatticePeak``
- ``LatticeFit``
- ``StrainTensor``
- ``LatticeStrainError``

### Scan-level strain map

- ``StrainMapping``
- ``StrainMapOptions``
- ``StrainMapResult``
- ``StrainPeak``
- ``StrainReference``
- ``StrainReferenceMethod``
- ``StrainFitMethod``
- ``StrainIndexing``
- ``StrainBasisOptions``
- ``StrainDirections``
- ``BraggVectorHistogram``
- ``StrainFrame``
- ``StrainMappingError``
38 changes: 38 additions & 0 deletions Sources/CuboStrain/CuboStrain.docc/From-peaks-to-strain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# From peaks to strain

Strain comes from how far a lattice has moved from a reference, measured in reciprocal space. A lattice is two vectors,
`g1` and `g2`, as a ``LatticeBasis`` in detector pixels from the lattice origin. A reference basis and a local basis give a
``StrainTensor``: `exx` along the first (row) axis, `eyy` along the second, the shear `exy`, and the rotation `theta` in
radians.

At the smallest scale, ``LatticeStrain/strain(local:reference:coordinateRotation:)`` compares two bases.

```swift
import CuboStrain

let reference = LatticeBasis(g1: LatticeVector(row: 20, column: 0), g2: LatticeVector(row: 0, column: 20))
let stretched = LatticeBasis(g1: LatticeVector(row: 19.8, column: 0), g2: LatticeVector(row: 0, column: 20))
let tensor = try LatticeStrain.strain(local: stretched, reference: reference)
print(tensor.exx, tensor.eyy, tensor.exy, tensor.theta)
```

For a pattern, choose the basis from the maxima of the Bragg vector map with
``LatticeStrain/chooseBasis(maxima:origin:)``, index the peaks with ``LatticeStrain/index(peaks:directions:maximumSpacing:)``
and fit them with ``LatticeStrain/fit(_:origin:minimumPeaks:)``. A fit returns a ``LatticeFit`` with the basis and a residual.
Take the reference from a set of fitted bases with ``LatticeStrain/reference(_:)``.

## A whole scan

``StrainMapping/compute(peaks:scanRows:scanColumns:detectorRows:detectorColumns:origin:directions:basisOptions:options:)``
runs those steps for every scan position, the way py4DSTEM's `StrainMap` does. It needs the peaks of every position as
``StrainPeak`` values centred on the lattice origin, which `StrainMapping.centredPeaks(_:origin:)` makes from the
``BraggVectors`` of CuboBragg. The ``StrainMapOptions`` take `maximumPeakSpacing`, the farthest a peak may be from a
direction and still be indexed, and the reference: ``StrainReference/fittedMedian``, a ``StrainReference/region(_:)`` of the
scan, or a ``StrainReference/supplied(_:)`` basis.

The ``StrainMapResult`` has flat row-major maps `exx`, `eyy`, `exy` and `theta` of `scanRows * scanColumns` values, NaN where
a position could not be fitted, and `valid`, `indexedPeakCount` and `residual` for each position. A value from a position
that is not `valid` is not a measurement.

Agreement with py4DSTEM shows that the port follows its definition. It does not show that a strain map is the specimen's
strain. That needs a calibrated detector and a specimen with a known lattice.
Loading