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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
__pycache__/
.docs-site/

# Local session rules, kept on disk only
# Local agent instructions, kept on disk only
CLAUDE.md
.claude/
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.

No virtual image or diffraction pattern computed by this library is validated
on a microscope: agreement with py4DSTEM and recovery of planted cubes are what
is claimed (design note, archived in the suite's private folder).
is claimed.


================================================================================
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ let package = Package(
.target(name: "CuboKitTestSupport", dependencies: ["CuboCore", "CuboVirtual", "CuboPhase"],
path: "Tests/CuboKitTestSupport", swiftSettings: settings),
// The real-data comparisons against the pinned float64 / py4DSTEM exports in $CUBOKIT_REAL_DATA (skipped by name when absent):
// `swift test -c release --parallel --skip CuboKitParityTests` is the per-session gate, the plain run is the full suite.
// `swift test -c release --parallel --skip CuboKitParityTests` is the fast gate, the plain run is the full suite.
.testTarget(name: "CuboKitParityTests", dependencies: ["CuboCore", "CuboVirtual", "CuboPhase", "CuboKitTestSupport"],
swiftSettings: settings),
.testTarget(name: "CuboCoreTests", dependencies: ["CuboCore"], exclude: ["Fixtures"], swiftSettings: settings),
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboCore/Calibration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
// Q-R rotation and flip, and the probe numbers, with the coordinate vectors and grids built from them.
//
// Reference: py4DSTEM `data.calibration.Calibration` and `DataCube.calibrate` (qxx, qyy, rxx, ryy, the raw grids, qxxs and
// qyys). py4DSTEM's x is this kit's ROW and y its COLUMN. A value type, so the registration of targets that py4DSTEM
// re-calibrates on every setter (`register_target`, `attach`) has no counterpart: a caller derives the axes it needs from the
// qyys). py4DSTEM's x is this kit's ROW and y its COLUMN. A value type, so the targets that py4DSTEM
// re-calibrates on every setter (`register_target`, `attach`) have no counterpart: a caller derives the axes it needs from the
// value. Differences that are on purpose:
// - py4DSTEM's grids come from `np.meshgrid(dim_x, dim_y)` and are shaped (Ny, Nx), the transpose of the cube's detector
// plane (the four `*_raw` grids are `meshgrid(..., indexing="ij")`-like and are not transposed). The grids here are `rows x columns`, entry [row][column] (the transpose of py4DSTEM's array, values equal).
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboCore/CubeEditing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ private extension Float {
var asDouble: Double { Double(self) }
}

// MARK: Fourier resampling (task 18; a self-contained addition beside the bilinear `resampleDetector`)
// MARK: Fourier resampling (a self-contained addition beside the bilinear `resampleDetector`)

/// How ``CubeEditing/resampleDetectorFourier(of:_:options:calibration:isCancelled:progress:)`` is told its size.
public enum FourierResampleSpecification: Sendable, Equatable {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboCore/CubeShape.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CubeShape.swift
// The shape of a 4D-STEM cube and the one index map the kit uses (registration 1.1).
// The shape of a 4D-STEM cube and the one index map the kit uses.
//
// Storage order is C order [scanRow][scanColumn][detectorRow][detectorColumn], py4DSTEM's (R_Nx, R_Ny, Q_Nx, Q_Ny).
// py4DSTEM's x (Rx, Qx: the first axis of a pair) is this kit's ROW; its y (Ry, Qy) is this kit's COLUMN. The kit says
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboCore/CubeSource.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CubeSource.swift
// How a computation gets its pixels: one pattern, or contiguous scan rows (registration 1.1). Every read returns
// How a computation gets its pixels: one pattern, or contiguous scan rows. Every read returns
// Float32 whatever the stored element type.
//
// A source here is only a shape and reads: load views, calibration, probes and file attributes belong to the consumer.
Expand Down Expand Up @@ -81,7 +81,7 @@ extension CubeShape {
}
}

/// How a whole-cube pass walks the scan axis (registration 1.1).
/// How a whole-cube pass walks the scan axis.
public enum ScanTiling: Sendable, Hashable {
/// The largest row count whose Float32 tile is at most 64 MiB, at least 1: a function of the shape alone, never of
/// the machine, so a tiled result is the same on every Mac.
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboCore/CuboCore.docc/Opening-a-cube.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ rounded to the nearest Float32 when it is read.

Whole-cube passes walk the scan in tiles. ``ScanTiling/automatic`` takes the largest number of scan rows whose Float32
tile fits in 64 MiB, and at least one row. It depends on the shape alone, not on the machine. ``ScanTiling/rows(_:)``
fixes the count. On the CPU path a result does not depend on the tiling; the Metal path agrees with it within its
registered tolerance.
fixes the count. On the CPU path a result does not depend on the tiling; the Metal path agrees with it within relative
1e-5.

To build a cube in memory from an array, use ``MemoryCube/init(shape:values:)`` with the values in storage order.
4 changes: 2 additions & 2 deletions Sources/CuboCore/CuboError.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CuboError.swift
// Every refusal of CuboCore. A refusal is never a fallback: a read that cannot be honoured stops (registration 1.1).
// Every refusal of CuboCore. A refusal is never a fallback: a read that cannot be honoured stops.
//

import Foundation
Expand All @@ -22,7 +22,7 @@ public enum CuboError: Error, Equatable, Sendable, LocalizedError {
case notNPY(String)
/// An `.npy` element type outside the supported list.
case unsupportedElementType(String)
/// Big-endian data (`>f4` and the like); slice 2 at the earliest.
/// Big-endian data (`>f4` and the like); not supported.
case bigEndianNotSupported(String)
/// `fortran_order: True`.
case fortranOrderNotSupported
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboCore/DoubleFFT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// The exact N-point complex DFT in Double for any N: vDSP's vDSP_DFT_zop plans for the lengths it takes (2^n × {1, 3, 5, 15};
// 5-7× faster than vDSP_fft_zipD from 128² up), vDSP's radix-2 FFT (vDSP_fft_zipD) for the other powers of two, Bluestein's
// chirp-z through a radix-2 FFT of length M ≥ 2N − 1 otherwise. Forward is NumPy's fft (e^{−2πi kn/N}, unnormalized);
// inverse is NumPy's ifft (e^{+2πi kn/N}, divided by N). Double throughout, for the DPC iteration of slice 2
// and the Fourier shifts of slice 3 (moved to CuboCore, package, in slice 3).
// inverse is NumPy's ifft (e^{+2πi kn/N}, divided by N). Double throughout, for the DPC iteration
// and the Fourier shifts (package access in CuboCore).
//

import Accelerate
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboCore/EllipseFit.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// EllipseFit.swift
// Fitting the elliptical distortion of a diffraction pattern (task 18): py4DSTEM's `process/calibration/ellipse.py` at f050d207
// Fitting the elliptical distortion of a diffraction pattern: py4DSTEM's `process/calibration/ellipse.py` at f050d207
// (`fit_ellipse_1D`, `ellipse_err`, `fit_ellipse_amorphous_ring`, `double_sided_gaussian`, `double_sided_gaussian_fiterr`,
// `constrain_degenerate_ellipse`) and the two conversions of `process/utils/elliptical_coords.py` (`convert_ellipse_params`,
// `convert_ellipse_params_r`), in Double. The fits run MINPACK's Levenberg-Marquardt (``MINPACKLeastSquares``) from the same
Expand Down
6 changes: 3 additions & 3 deletions Sources/CuboCore/MappedCube.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//
// MappedCube.swift
// The lazy cube: a file region mapped read-only and private; a read converts only the requested elements to Float32
// (registration 1.1, 3.5). Nothing is read until a pattern or a tile is asked for.
// The lazy cube: a file region mapped read-only and private; a read converts only the requested elements to Float32.
// Nothing is read until a pattern or a tile is asked for.
//
// Conversions (all native little-endian): float32 is copied; float64 rounds to nearest even (vDSP_vdpsp, as NumPy's
// astype(np.float32)); the 8-, 16- and 32-bit integers convert with vDSP (exact up to 2^24); the 64-bit integers with a
Expand Down Expand Up @@ -129,7 +129,7 @@ public actor MappedCube: CubeSource {
self.dataOffset = dataOffset
}

/// The rank-4 array in a NumPy `.npy` file (registration 1.1): C order, little-endian, a supported element type.
/// The rank-4 array in a NumPy `.npy` file: C order, little-endian, a supported element type.
public init(npy url: URL) throws {
let header = try NPYFile.header(of: url)
guard header.shape.count == 4 else { throw CuboError.wrongRank(expected: 4, received: header.shape.count) }
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboCore/MemoryCube.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//
// MemoryCube.swift
// The eager cube: the whole array as one [Float] in storage order (registration 1.1). Registered equal to the lazy
// MappedCube of the same bytes, bit for bit (3.4).
// The eager cube: the whole array as one [Float] in storage order. Equal to the lazy
// MappedCube of the same bytes, bit for bit (tested in CuboCoreTests).
//

/// A cube held in memory as Float32, `[ry][rx][qy][qx]`.
Expand Down
6 changes: 3 additions & 3 deletions Sources/CuboCore/NPYFile.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//
// NPYFile.swift
// NumPy's .npy container, versions 1.0, 2.0 and 3.0 (registration 1.1): the magic "\x93NUMPY", the version, the
// NumPy's .npy container, versions 1.0, 2.0 and 3.0: the magic "\x93NUMPY", the version, the
// header length (little-endian UInt16 for 1.0, UInt32 for 2.0 and 3.0), then a Python dict literal with 'descr',
// 'fortran_order' and 'shape'. The kit's exchange format with py4DSTEM in slice 1 (np.save(path, datacube.data));
// 'fortran_order' and 'shape'. The kit's exchange format with py4DSTEM (np.save(path, datacube.data));
// HDF5, DM4 and the vendor formats are out of scope here.
//

Expand Down Expand Up @@ -61,7 +61,7 @@ public enum NPYFile {
guard !fortran else { throw CuboError.fortranOrderNotSupported }
let type = try elementType(descr)
guard shape.allSatisfy({ $0 >= 0 }) else { throw CuboError.notNPY("negative extent in 'shape'") }
// The element and byte counts must fit an Int (Gate B, 2026-10-15: an unchecked product trapped).
// The element and byte counts must fit an Int (an unchecked product trapped; CuboCoreTests holds the case).
var bytes = type.byteSize
for e in shape {
let (product, overflow) = bytes.multipliedReportingOverflow(by: e)
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboCore/OriginFit.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// OriginFit.swift
// py4DSTEM's `fit_origin` for a scan map (registration slice 3, 3.2): process/calibration/origin.py 76 and fit_2D
// py4DSTEM's `fit_origin` for a scan map: process/calibration/origin.py 76 and fit_2D
// (process/fit/fit.py 38) at f050d207. The four models are linear in their parameters, so the fit is the least-squares
// solution, here by Householder QR in Double on the design matrix with py4DSTEM's [0, 1] coordinates; py4DSTEM reaches
// it with curve_fit within that solver's convergence. The robust option follows fit_2D's loop exactly.
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboParallax/ParallaxFourierShifter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public struct ParallaxFourierShifter: Sendable {
public var supportsSingle: Bool { singleFFT != nil }

/// ``shifted(_:by:)`` in Float32 (named apart so integer-literal arrays keep resolving to the Double form) on vDSP's Float DFT, for pipelines that hold their images in Float: agrees with the Double
/// shifter to 1e-4 px-equivalent (about 1e-5 relative to the image's range; D12 Float32) at about half the cost. Throws
/// shifter to 1e-4 px-equivalent (about 1e-5 relative to the image's range; the upstream float32 tolerance) at about half the cost. Throws
/// ``ParallaxAlignmentError/invalidDimensions`` when ``supportsSingle`` is false.
public func shiftedSingle(_ image: [Float], by shift: ParallaxShift) throws -> [Float] {
guard let single = singleFFT else { throw ParallaxAlignmentError.invalidDimensions }
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboPhase/CenterOfMassReference.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CenterOfMassReference.swift
// The reference a measured centre of mass is taken against, and the normalized shift (registration slice 2, 3.2):
// The reference a measured centre of mass is taken against, and the normalized shift:
// py4DSTEM's `fit_origin` (process/calibration/origin.py 76) and the normalization at the end of
// `_calculate_intensities_center_of_mass` (process/phase/phase_base_class.py) at f050d207.
//
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboPhase/DPC.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DPC.swift
// Differential phase contrast by py4DSTEM's iterative reconstruction (registration slice 2, 3.4): `DPC.preprocess`'s
// Differential phase contrast by py4DSTEM's iterative reconstruction: `DPC.preprocess`'s
// operators and `DPC.reconstruct` with `_forward`, `_adjoint`, `_update` (process/phase/dpc.py at f050d207), literally,
// in Double, with its default constraints (no Gaussian and no Butterworth filter). py4DSTEM computes this in float32;
// the kit is held to the float64 evaluation of the same steps.
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboPhase/RotationSolver.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//
// RotationSolver.swift
// The relative rotation (and transpose) between the scan and the detector, by minimising the curl of the
// centre-of-mass field or maximising its divergence (registration slice 2, 3.3): a port of py4DSTEM's
// centre-of-mass field or maximising its divergence: a port of py4DSTEM's
// `_solve_for_center_of_mass_relative_rotation` (process/phase/phase_base_class.py 769 at f050d207), in Double.
//

Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboPhase/ScanVectorField.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// ScanVectorField.swift
// A 2-vector per scan position (registration slice 2, section 1): py4DSTEM's (x, y) component pairs, which the kit names
// A 2-vector per scan position: py4DSTEM's (x, y) component pairs, which the kit names
// by the detector axis they lie along: `row` is py4DSTEM's x (the first detector axis), `column` its y.
//

Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboPtycho/PtychographySingle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CuboCore
import Foundation

/// The arithmetic of a ptychographic engine: `double` is the kit's reference path; `single` runs the heavy loops in Float32
/// (vDSP's Float DFT, half the memory) and agrees with `double` to 1e-5 relative to the object's scale (D12 Float32).
/// (vDSP's Float DFT, half the memory) and agrees with `double` to 1e-5 relative to the object's scale (the upstream float32 tolerance).
public enum PtychographyPrecision: Sendable, Equatable {
case double
case single
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboStrain/ConsensusNumerics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ enum ConsensusNumerics {
// Only positions that HAVE a non-central peak: the sentinel is
// finite, so an `isFinite` test let every empty or central-only
// position add 3.4e38, and at half the scan the median exploded
// and no basis was found (register D023, Gate D 2026-09-30).
// and no basis was found.
if nearest < .greatestFiniteMagnitude { nearestRadii.append(nearest) }
}
guard let typicalRadius = median(nearestRadii) else { return nil }
Expand Down Expand Up @@ -155,7 +155,7 @@ enum ConsensusNumerics {
}
}

// DEVIATION (register: occupiedPositions, Gate D 2026-09-30): the 8 %
// DEVIATION (occupiedPositions): the 8 %
// support bar is mac4DSTEM's own automation (py4DSTEM picks maxima of
// the whole-scan vector histogram and has no per-position count), and
// it is a fraction of the positions a cluster CAN be drawn from --
Expand Down
4 changes: 2 additions & 2 deletions Sources/CuboVirtual/BeamstopMask.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// BeamstopMask.swift
// The beamstop mask of a pattern (registration slice 4, 3.4): py4DSTEM's DataCube.get_beamstop_mask
// The beamstop mask of a pattern: py4DSTEM's DataCube.get_beamstop_mask
// (datacube/datacube.py 1013 at f050d207) on a pattern the caller passes (the mean pattern, or the maximum for
// `use_max_dp`), in Double, with scipy's binary_fill_holes (4-connected) and the exact Euclidean distance transform.
//
Expand All @@ -15,7 +15,7 @@ import Foundation
public enum BeamstopMask {
/// `true` under the beamstop (and within `distanceEdge` pixels of it, and of the border with `includeEdges`), `false`
/// in the pattern, row-major. `threshold` is the fraction of the sorted intensities below the cut; `sigma > 0` smooths
/// first (slice 3's Gaussian filter); `scaleRadial` multiplies by 1 + s · (distance from the centre in half-widths).
/// first (`GaussianFilter`); `scaleRadial` multiplies by 1 + s · (distance from the centre in half-widths).
public static func make(from pattern: DetectorImage, threshold: Double = 0.25, distanceEdge: Double = 2.0,
includeEdges: Bool = true, sigma: Double = 0, scaleRadial: Double? = nil) throws -> [Bool] {
let qy = pattern.detectorRows, qx = pattern.detectorColumns, n = qy * qx
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboVirtual/CPUReducers.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CPUReducers.swift
// The reference path (registration 3.2, 3.3): Accelerate, Double accumulation, the same arithmetic whatever the
// The reference path: Accelerate, Double accumulation, the same arithmetic whatever the
// tiling, so the eager and lazy cubes and every tiling give the same bits.
//

Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboVirtual/CenterOfMass.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// CenterOfMass.swift
// The intensity-weighted centre of mass of every pattern (registration slice 2, 3.1): py4DSTEM's
// The intensity-weighted centre of mass of every pattern: py4DSTEM's
// `_calculate_intensities_center_of_mass` (process/phase/phase_base_class.py 613 at f050d207), in Double.
//
// DEVIATION from a common Float32 implementation, toward py4DSTEM: negative intensities are summed as they are (that
Expand Down
2 changes: 1 addition & 1 deletion Sources/CuboVirtual/DetectorGeometry.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// DetectorGeometry.swift
// py4DSTEM's virtual detectors in detector pixels, from the corner, and their masks (registration 1.2, 3.1).
// py4DSTEM's virtual detectors in detector pixels, from the corner, and their masks.
//
// Ported from py4DSTEM f050d207, py4DSTEM/datacube/virtualimage.py, DataCubeVirtualImager.make_detector (line 588):
// circle (qx - x0)**2 + (qy - y0)**2 < r**2
Expand Down
8 changes: 4 additions & 4 deletions Sources/CuboVirtual/FriedelOrigin.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// FriedelOrigin.swift
// Per-position origins from Friedel symmetry (registration slice 4, 3.3): py4DSTEM's get_origin_friedel
// Per-position origins from Friedel symmetry: py4DSTEM's get_origin_friedel
// (process/calibration/origin.py 332 at f050d207) and its DFT upsampling (process/utils/multicorr.py
// upsampled_correlation 22, dftUpsample 145), CPU, Double. A pattern is centrosymmetric about the unscattered beam, so
// the correlation of the zero-padded pattern with its point reflection, Re(IFFT2(FFT2(im)²)), peaks at twice the origin.
Expand Down Expand Up @@ -66,7 +66,7 @@ public enum FriedelOrigin {
if let mask, mask.count != qy * qx {
throw CuboVirtualError.maskLengthMismatch(expected: qy * qx, received: mask.count)
}
if let mask, !mask.contains(true) { // every masked correlation is then noise over noise (Gate B F1)
if let mask, !mask.contains(true) { // every masked correlation is then noise over noise
throw CuboVirtualError.invalidInput("the mask keeps no pixel of the pattern")
}
self.qy = qy; self.qx = qx; ny = 2 * qy; nx = 2 * qx; self.u = u
Expand All @@ -89,7 +89,7 @@ public enum FriedelOrigin {
for k in re.indices { let a = re[k], b = im[k]; re[k] = a * a - b * b; im[k] = 2 * a * b }
}

/// The definition of 3.3 for one pattern (qy × qx, row-major).
/// The Friedel origin of one pattern (qy × qx, row-major).
func origin(_ p: [Double]) -> (Double, Double) {
let total = ny * nx
var re = [Double](repeating: 0, count: total), im = re
Expand Down Expand Up @@ -124,7 +124,7 @@ public enum FriedelOrigin {
return (Self.remainder(sx / 2, Double(qy)), Self.remainder(sy / 2, Double(qx)))
}

/// py4DSTEM's upsampled_correlation(FFT2(cc), u, (sx, sy)) (3.3).
/// py4DSTEM's upsampled_correlation(FFT2(cc), u, (sx, sy)).
func upsampled(_ cc: [Double], _ x: Double, _ y: Double) -> (Double, Double) {
let ud = Double(u)
let sx = (x * ud).rounded(.toNearestOrEven) / ud, sy = (y * ud).rounded(.toNearestOrEven) / ud
Expand Down
Loading
Loading