diff --git a/Sources/CuboBragg/CuboBragg.docc/CuboBragg.md b/Sources/CuboBragg/CuboBragg.docc/CuboBragg.md index dc6d351..1e131f9 100644 --- a/Sources/CuboBragg/CuboBragg.docc/CuboBragg.md +++ b/Sources/CuboBragg/CuboBragg.docc/CuboBragg.md @@ -24,6 +24,10 @@ is the median fill of non-finite pixels, public. ## Topics +### Essentials + +- + ### Detection - ``BraggDisks`` @@ -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`` diff --git a/Sources/CuboBragg/CuboBragg.docc/Finding-Bragg-disks.md b/Sources/CuboBragg/CuboBragg.docc/Finding-Bragg-disks.md new file mode 100644 index 0000000..c3ba824 --- /dev/null +++ b/Sources/CuboBragg/CuboBragg.docc/Finding-Bragg-disks.md @@ -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. diff --git a/Sources/CuboEmbedding/CuboEmbedding.docc/Clustering-patterns.md b/Sources/CuboEmbedding/CuboEmbedding.docc/Clustering-patterns.md new file mode 100644 index 0000000..edd3608 --- /dev/null +++ b/Sources/CuboEmbedding/CuboEmbedding.docc/Clustering-patterns.md @@ -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. diff --git a/Sources/CuboEmbedding/CuboEmbedding.docc/CuboEmbedding.md b/Sources/CuboEmbedding/CuboEmbedding.docc/CuboEmbedding.md index f25d9c0..250e867 100644 --- a/Sources/CuboEmbedding/CuboEmbedding.docc/CuboEmbedding.md +++ b/Sources/CuboEmbedding/CuboEmbedding.docc/CuboEmbedding.md @@ -14,6 +14,19 @@ Accelerate implementation, and ``KMeans`` takes an `isCancelled` closure. ## Topics +### Essentials + +- +- + +### Principal components and k-means + +- ``PCA`` +- ``PCAResult`` +- ``KMeans`` +- ``KMeansResult`` +- ``EmbeddingError`` + ### Streamed PCA - ``PCACovarianceAccumulator`` diff --git a/Sources/CuboParallax/CuboParallax.docc/Aligning-images.md b/Sources/CuboParallax/CuboParallax.docc/Aligning-images.md new file mode 100644 index 0000000..f9118fa --- /dev/null +++ b/Sources/CuboParallax/CuboParallax.docc/Aligning-images.md @@ -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:)``. diff --git a/Sources/CuboParallax/CuboParallax.docc/CuboParallax.md b/Sources/CuboParallax/CuboParallax.docc/CuboParallax.md index ba9eda9..a157090 100644 --- a/Sources/CuboParallax/CuboParallax.docc/CuboParallax.md +++ b/Sources/CuboParallax/CuboParallax.docc/CuboParallax.md @@ -29,6 +29,10 @@ stack, shifts and probe angles as separate calls. Defaults are py4DSTEM's; the p ## Topics +### Essentials + +- + ### End to end - ``ParallaxReconstruction`` @@ -75,5 +79,4 @@ stack, shifts and probe angles as separate calls. Defaults are py4DSTEM's; the p ### Errors - ``ParallaxError`` - - ``ParallaxAlignmentError`` diff --git a/Sources/CuboPtycho/CuboPtycho.docc/CuboPtycho.md b/Sources/CuboPtycho/CuboPtycho.docc/CuboPtycho.md index b60891b..0876f32 100644 --- a/Sources/CuboPtycho/CuboPtycho.docc/CuboPtycho.md +++ b/Sources/CuboPtycho/CuboPtycho.docc/CuboPtycho.md @@ -4,6 +4,10 @@ CPU ptychography preprocessing, probe construction and single-slice, mixed-state ## Topics +### Essentials + +- + ### Processing - ``PtychographyPreparation`` diff --git a/Sources/CuboPtycho/CuboPtycho.docc/Preparing-a-reconstruction.md b/Sources/CuboPtycho/CuboPtycho.docc/Preparing-a-reconstruction.md new file mode 100644 index 0000000..57acfad --- /dev/null +++ b/Sources/CuboPtycho/CuboPtycho.docc/Preparing-a-reconstruction.md @@ -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 . diff --git a/Sources/CuboStrain/CuboStrain.docc/CuboStrain.md b/Sources/CuboStrain/CuboStrain.docc/CuboStrain.md index 9869977..a3fa2d0 100644 --- a/Sources/CuboStrain/CuboStrain.docc/CuboStrain.md +++ b/Sources/CuboStrain/CuboStrain.docc/CuboStrain.md @@ -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 + +- +- + +### 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`` diff --git a/Sources/CuboStrain/CuboStrain.docc/From-peaks-to-strain.md b/Sources/CuboStrain/CuboStrain.docc/From-peaks-to-strain.md new file mode 100644 index 0000000..6d7c16a --- /dev/null +++ b/Sources/CuboStrain/CuboStrain.docc/From-peaks-to-strain.md @@ -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.