diff --git a/.gitignore b/.gitignore index 12acc27..afaa372 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ __pycache__/ .docs-site/ -# Local session rules, kept on disk only +# Local agent instructions, kept on disk only CLAUDE.md .claude/ diff --git a/NOTICE b/NOTICE index 9b7ad33..71a0138 100644 --- a/NOTICE +++ b/NOTICE @@ -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. ================================================================================ diff --git a/Package.swift b/Package.swift index b38e83b..f7b7863 100644 --- a/Package.swift +++ b/Package.swift @@ -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), diff --git a/Sources/CuboCore/Calibration.swift b/Sources/CuboCore/Calibration.swift index 26de2dd..d439f8f 100644 --- a/Sources/CuboCore/Calibration.swift +++ b/Sources/CuboCore/Calibration.swift @@ -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). diff --git a/Sources/CuboCore/CubeEditing.swift b/Sources/CuboCore/CubeEditing.swift index 6c21e80..0513c7c 100644 --- a/Sources/CuboCore/CubeEditing.swift +++ b/Sources/CuboCore/CubeEditing.swift @@ -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 { diff --git a/Sources/CuboCore/CubeShape.swift b/Sources/CuboCore/CubeShape.swift index 9e33eb4..af4240d 100644 --- a/Sources/CuboCore/CubeShape.swift +++ b/Sources/CuboCore/CubeShape.swift @@ -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 diff --git a/Sources/CuboCore/CubeSource.swift b/Sources/CuboCore/CubeSource.swift index 13fbcd8..92a6f13 100644 --- a/Sources/CuboCore/CubeSource.swift +++ b/Sources/CuboCore/CubeSource.swift @@ -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. @@ -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. diff --git a/Sources/CuboCore/CuboCore.docc/Opening-a-cube.md b/Sources/CuboCore/CuboCore.docc/Opening-a-cube.md index 5aca44f..3538a2e 100644 --- a/Sources/CuboCore/CuboCore.docc/Opening-a-cube.md +++ b/Sources/CuboCore/CuboCore.docc/Opening-a-cube.md @@ -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. diff --git a/Sources/CuboCore/CuboError.swift b/Sources/CuboCore/CuboError.swift index 64c0a1f..a2dc656 100644 --- a/Sources/CuboCore/CuboError.swift +++ b/Sources/CuboCore/CuboError.swift @@ -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 @@ -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 diff --git a/Sources/CuboCore/DoubleFFT.swift b/Sources/CuboCore/DoubleFFT.swift index 09d74ba..54b3081 100644 --- a/Sources/CuboCore/DoubleFFT.swift +++ b/Sources/CuboCore/DoubleFFT.swift @@ -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 diff --git a/Sources/CuboCore/EllipseFit.swift b/Sources/CuboCore/EllipseFit.swift index db17a0b..21d2e31 100644 --- a/Sources/CuboCore/EllipseFit.swift +++ b/Sources/CuboCore/EllipseFit.swift @@ -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 diff --git a/Sources/CuboCore/MappedCube.swift b/Sources/CuboCore/MappedCube.swift index e4b3eb9..620ba5f 100644 --- a/Sources/CuboCore/MappedCube.swift +++ b/Sources/CuboCore/MappedCube.swift @@ -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 @@ -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) } diff --git a/Sources/CuboCore/MemoryCube.swift b/Sources/CuboCore/MemoryCube.swift index 73b9927..d93e11c 100644 --- a/Sources/CuboCore/MemoryCube.swift +++ b/Sources/CuboCore/MemoryCube.swift @@ -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]`. diff --git a/Sources/CuboCore/NPYFile.swift b/Sources/CuboCore/NPYFile.swift index 180f86f..10007db 100644 --- a/Sources/CuboCore/NPYFile.swift +++ b/Sources/CuboCore/NPYFile.swift @@ -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. // @@ -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) diff --git a/Sources/CuboCore/OriginFit.swift b/Sources/CuboCore/OriginFit.swift index edac7d6..9e3efe2 100644 --- a/Sources/CuboCore/OriginFit.swift +++ b/Sources/CuboCore/OriginFit.swift @@ -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. diff --git a/Sources/CuboParallax/ParallaxFourierShifter.swift b/Sources/CuboParallax/ParallaxFourierShifter.swift index 36b7119..09a5807 100644 --- a/Sources/CuboParallax/ParallaxFourierShifter.swift +++ b/Sources/CuboParallax/ParallaxFourierShifter.swift @@ -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 } diff --git a/Sources/CuboPhase/CenterOfMassReference.swift b/Sources/CuboPhase/CenterOfMassReference.swift index d06c19e..7f33774 100644 --- a/Sources/CuboPhase/CenterOfMassReference.swift +++ b/Sources/CuboPhase/CenterOfMassReference.swift @@ -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. // diff --git a/Sources/CuboPhase/DPC.swift b/Sources/CuboPhase/DPC.swift index 7919e42..8056509 100644 --- a/Sources/CuboPhase/DPC.swift +++ b/Sources/CuboPhase/DPC.swift @@ -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. diff --git a/Sources/CuboPhase/RotationSolver.swift b/Sources/CuboPhase/RotationSolver.swift index 426a879..af6b497 100644 --- a/Sources/CuboPhase/RotationSolver.swift +++ b/Sources/CuboPhase/RotationSolver.swift @@ -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. // diff --git a/Sources/CuboPhase/ScanVectorField.swift b/Sources/CuboPhase/ScanVectorField.swift index 45e2c1e..70945f3 100644 --- a/Sources/CuboPhase/ScanVectorField.swift +++ b/Sources/CuboPhase/ScanVectorField.swift @@ -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. // diff --git a/Sources/CuboPtycho/PtychographySingle.swift b/Sources/CuboPtycho/PtychographySingle.swift index 9a8234e..01a3374 100644 --- a/Sources/CuboPtycho/PtychographySingle.swift +++ b/Sources/CuboPtycho/PtychographySingle.swift @@ -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 diff --git a/Sources/CuboStrain/ConsensusNumerics.swift b/Sources/CuboStrain/ConsensusNumerics.swift index 074e779..0ec5aa6 100644 --- a/Sources/CuboStrain/ConsensusNumerics.swift +++ b/Sources/CuboStrain/ConsensusNumerics.swift @@ -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 } @@ -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 -- diff --git a/Sources/CuboVirtual/BeamstopMask.swift b/Sources/CuboVirtual/BeamstopMask.swift index ae6d633..cbfc7a5 100644 --- a/Sources/CuboVirtual/BeamstopMask.swift +++ b/Sources/CuboVirtual/BeamstopMask.swift @@ -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. // @@ -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 diff --git a/Sources/CuboVirtual/CPUReducers.swift b/Sources/CuboVirtual/CPUReducers.swift index aff7cf0..91cb448 100644 --- a/Sources/CuboVirtual/CPUReducers.swift +++ b/Sources/CuboVirtual/CPUReducers.swift @@ -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. // diff --git a/Sources/CuboVirtual/CenterOfMass.swift b/Sources/CuboVirtual/CenterOfMass.swift index 3389db3..9f65a07 100644 --- a/Sources/CuboVirtual/CenterOfMass.swift +++ b/Sources/CuboVirtual/CenterOfMass.swift @@ -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 diff --git a/Sources/CuboVirtual/DetectorGeometry.swift b/Sources/CuboVirtual/DetectorGeometry.swift index 3b145ab..21e8203 100644 --- a/Sources/CuboVirtual/DetectorGeometry.swift +++ b/Sources/CuboVirtual/DetectorGeometry.swift @@ -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 diff --git a/Sources/CuboVirtual/FriedelOrigin.swift b/Sources/CuboVirtual/FriedelOrigin.swift index d2f7cfe..77015ef 100644 --- a/Sources/CuboVirtual/FriedelOrigin.swift +++ b/Sources/CuboVirtual/FriedelOrigin.swift @@ -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. @@ -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 @@ -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 @@ -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 diff --git a/Sources/CuboVirtual/MetalReducers.swift b/Sources/CuboVirtual/MetalReducers.swift index ce11059..77974b2 100644 --- a/Sources/CuboVirtual/MetalReducers.swift +++ b/Sources/CuboVirtual/MetalReducers.swift @@ -1,14 +1,14 @@ // // MetalReducers.swift -// The accelerated path (registration 3.2, 3.3): compute kernels compiled at run time from the source below, in Metal's +// The accelerated path: compute kernels compiled at run time from the source below, in Metal's // safe math mode (no fast-math reassociation), compensated Float32 sums, Double combination on the host. -// Held to the CPU path within relative 1e-5 (registration 3.4). +// Held to the CPU path within relative 1e-5. // // Four kernels: one thread per scan position (a detector mask, the centre of mass, the origin) or per detector pixel over the scan // positions. Every sum is compensated // (TwoSum and the fma TwoProduct, Dot2) and each thread returns its sum and its correction, which the host adds in -// Double (the first version's plain two-level Float32 sums missed the registered 1e-5 on zero-mean data: Gate B, -// 2026-10-15); the tiles are combined in Double; the kernels are compiled from source so the +// Double (the first version's plain two-level Float32 sums missed relative 1e-5 on +// zero-mean data); the tiles are combined in Double; the kernels are compiled from source so the // kit needs no .metallib and `swift build` alone builds it. // @@ -73,7 +73,7 @@ final class MetalContext: @unchecked Sendable { // Error-free transformations (Knuth's TwoSum, the fma TwoProduct): in safe math mode nothing reassociates them, so // s + c holds the running sum with its rounding error carried exactly to the next step (Ogita, Rump and Oishi's - // Dot2). The host adds s and c in Double. Registration 3.2, 3.3, amendment of 2026-10-15 (Gate B). + // Dot2). The host adds s and c in Double. inline void two_sum(thread float &s, thread float &c, float x) { const float t = s + x; const float z = t - s; @@ -137,7 +137,7 @@ final class MetalContext: @unchecked Sendable { lo = e - (hi - s); } - // One thread per scan position: Σ wP, Σ (w i) P, Σ (w j) P (registration slice 2, 3.1; Gate B amendment). Each + // One thread per scan position: Σ wP, Σ (w i) P, Σ (w j) P. Each // coefficient arrives as a Float32 pair (hi, lo) with hi + lo its Double value; the product with hi is split exactly by // fma, the lo part is added rounded, and every term goes into a renormalised double-float accumulator returned as // (hi, lo) for the host to add in Double. diff --git a/Sources/CuboVirtual/OriginMeasurement.swift b/Sources/CuboVirtual/OriginMeasurement.swift index 73e4147..bcf8337 100644 --- a/Sources/CuboVirtual/OriginMeasurement.swift +++ b/Sources/CuboVirtual/OriginMeasurement.swift @@ -1,6 +1,6 @@ // // OriginMeasurement.swift -// Per-position origins (registration slice 3, 3.1): py4DSTEM's get_origin (process/calibration/origin.py 222 at f050d207) +// Per-position origins: py4DSTEM's get_origin (process/calibration/origin.py 222 at f050d207) // in Double: scipy's gaussian_filter (σ = r, truncate 4, mode nearest, rows then columns), the first argmax, and the centre // of mass of the pattern within r · rscale of it. // @@ -46,7 +46,7 @@ public enum OriginMeasurement { try await measure(of: source, radius: radius, radiusScale: radiusScale, scanMask: nil, tiling: tiling) } - /// get_origin with its scan `mask` and `fast_center` (registration slice 4, 3.2): origins only where `scanMask` is true + /// get_origin with its scan `mask` and `fast_center`: origins only where `scanMask` is true /// (row-major over the scan), NaN elsewhere; with `fastCenter` the filtered argmax itself, no centre of mass. The radius /// default is the probe size of the whole cube's maximum pattern, as py4DSTEM's. public static func measure(of source: some CubeSource, radius: Double? = nil, radiusScale: Double = 1.2, diff --git a/Sources/CuboVirtual/PolarCube.swift b/Sources/CuboVirtual/PolarCube.swift index f6232b0..753d173 100644 --- a/Sources/CuboVirtual/PolarCube.swift +++ b/Sources/CuboVirtual/PolarCube.swift @@ -1,6 +1,6 @@ // // PolarCube.swift -// The polar-elliptical transform of every diffraction pattern (task 17): py4DSTEM's `DataCube.polar`, a `PolarDatacube` +// The polar-elliptical transform of every diffraction pattern: py4DSTEM's `DataCube.polar`, a `PolarDatacube` // (process/polar/polar_datacube.py at f050d207), in Double on the CPU. // // A pattern is binned (not interpolated) onto `annularCount x radialCount` cells: each detector pixel spreads its value over diff --git a/Sources/CuboVirtual/ProbeSize.swift b/Sources/CuboVirtual/ProbeSize.swift index 86600bd..ec751a3 100644 --- a/Sources/CuboVirtual/ProbeSize.swift +++ b/Sources/CuboVirtual/ProbeSize.swift @@ -1,6 +1,6 @@ // // ProbeSize.swift -// The radius and centre of the bright central disk (registration slice 2, 3.5): a port of py4DSTEM's `get_probe_size` +// The radius and centre of the bright central disk: a port of py4DSTEM's `get_probe_size` // (process/calibration/probe.py 6 at f050d207), step for step in float64, with NumPy's linspace, gradient, median and // pairwise sums, so it reproduces py4DSTEM's numbers bit for bit on the same pattern. // diff --git a/Sources/CuboVirtual/RadialBackground.swift b/Sources/CuboVirtual/RadialBackground.swift index ab62be8..c0ddd71 100644 --- a/Sources/CuboVirtual/RadialBackground.swift +++ b/Sources/CuboVirtual/RadialBackground.swift @@ -1,6 +1,6 @@ // // RadialBackground.swift -// The radial background of a diffraction pattern from its polar transform (task 17): py4DSTEM's `DataCube.get_radial_bkgrnd`, +// The radial background of a diffraction pattern from its polar transform: py4DSTEM's `DataCube.get_radial_bkgrnd`, // `get_radial_bksb_dp` and `get_local_ave_dp(radial_bksb=True)` (datacube/datacube.py at f050d207), in Double on the CPU. // // The background is the median over the angle of the valid polar cells at each radius (zero where none is valid), smoothed @@ -12,7 +12,7 @@ // `sqrt(u^2 + v^2 / (b / a)^2)` with u, v along the axes, while ``PolarCube`` bins radii on the scale of the MINOR one // (`(b / a) * hypot(...)`). py4DSTEM does exactly this, so a ring of the ellipse sits at `a` in the lookup and at `b` in // the polar bins: for `a != b` the background is laid out stretched by `a / b` against the polar profile. It is ported as -// it is (D17); a circle is not affected. No ellipse in the calibration is the circle `(1, 1, 0)`, whatever the sampling +// it is; a circle is not affected. No ellipse in the calibration is the circle `(1, 1, 0)`, whatever the sampling // says; the sampling's `elliptical: false` switches the polar transform only, as in py4DSTEM. // - Patterns are Float32 in the cube; the subtraction runs in Double and zeroes negative values (NaN stays NaN). // - `localAverage` clips the 3 x 3 Gaussian kernel at the scan edge and renormalises, as py4DSTEM does; py4DSTEM's own edge diff --git a/Sources/CuboVirtual/Results.swift b/Sources/CuboVirtual/Results.swift index ec19def..82b2cea 100644 --- a/Sources/CuboVirtual/Results.swift +++ b/Sources/CuboVirtual/Results.swift @@ -1,6 +1,6 @@ // // Results.swift -// What CuboVirtual returns: Double images that record the path that made them (registration 1.2). +// What CuboVirtual returns: Double images that record the path that made them. // // Double on purpose: py4DSTEM's virtual image of an integer cube is an exact integer sum (NumPy sums uint16 × bool in // uint64); a Float32 result would round it above 2^24, and a kit whose numbers are the reference cannot round where the @@ -13,8 +13,8 @@ import Foundation public enum ComputePath: String, Sendable, Hashable, Codable, CaseIterable { /// Accelerate on the CPU, Double accumulation: the reference path. case cpu - /// Metal compute kernels, Float32 accumulation, Double combination across tiles; within the registered tolerance of - /// the CPU path (registration 3.4). + /// Metal compute kernels, Float32 accumulation, Double combination across tiles; within relative 1e-5 of + /// the CPU path. case metal } diff --git a/Sources/CuboVirtual/SelectedDiffraction.swift b/Sources/CuboVirtual/SelectedDiffraction.swift index c01b517..4b9f1e3 100644 --- a/Sources/CuboVirtual/SelectedDiffraction.swift +++ b/Sources/CuboVirtual/SelectedDiffraction.swift @@ -1,10 +1,10 @@ // // SelectedDiffraction.swift -// The median pattern and virtual diffraction over a real-space selection (registration slice 4, 3.1): py4DSTEM's +// The median pattern and virtual diffraction over a real-space selection: py4DSTEM's // get_dp_median and get_virtual_diffraction(method, mask, shift_center, subpixel) at f050d207, CPU, Double. // // DEVIATIONS: an empty selection, weights summing to zero (for the mean) and non-finite weights are refused where -// py4DSTEM returns NaN or ∞ or raises; the shifted maximum is seeded at −∞ (slice 3; py4DSTEM seeds it at 0). Complex +// py4DSTEM returns NaN or ∞ or raises; the shifted maximum is seeded at −∞ (as in ShiftedImaging; py4DSTEM seeds it at 0). Complex // masks are not ported. // @@ -34,7 +34,7 @@ extension VirtualDiffraction { } /// A virtual diffraction pattern over a selection of positions, optionally centre-shifted (mean and max only). - /// `.mean` and `.max` over `.all` without a shift are slice 1's `mean` and `max` on the CPU, bit for bit. + /// `.mean` and `.max` over `.all` without a shift are `VirtualImaging`'s `mean` and `max` on the CPU, bit for bit. /// `medianWorkingBytes` bounds the median's gathered block (256 MiB by default; it changes the passes, not the result). /// `path: .metal` runs the mean and the max over `.all`, `.mask` and `.weights` on the GPU (compensated Float32 sums /// combined in Double, held to the CPU path within relative 1e-5); the median and a centre shift are CPU only and @@ -221,7 +221,7 @@ extension VirtualDiffraction { return c % 2 == 1 ? v[c / 2] : (v[c / 2 - 1] + v[c / 2]) / 2 } - /// The centre-shifted mean and max over a selection (slice 3, 3.4, with py4DSTEM's mask): each selected pattern moved + /// The centre-shifted mean and max over a selection (py4DSTEM's shift_center with a mask): each selected pattern moved /// by the map's mean minus its origin, then Σ moved (· w) / count (or Σ w) and the max of moved (· w), seeded at −∞. private static func shifted(_ source: some CubeSource, _ statistic: DiffractionStatistic, _ selected: [Int], _ weights: [Double]?, _ shift: PatternShift, _ tiling: ScanTiling, diff --git a/Sources/CuboVirtual/ShiftedImaging.swift b/Sources/CuboVirtual/ShiftedImaging.swift index 6ad629f..3204fec 100644 --- a/Sources/CuboVirtual/ShiftedImaging.swift +++ b/Sources/CuboVirtual/ShiftedImaging.swift @@ -1,7 +1,7 @@ // // ShiftedImaging.swift -// Centred, calibrated and centre-shifted virtual images, and centre-shifted mean and maximum patterns (registration -// slice 3, 3.3, 3.4): py4DSTEM's get_virtual_image(centered, calibrated, shift_center, subpixel) and +// Centred, calibrated and centre-shifted virtual images, and centre-shifted mean and maximum patterns: +// py4DSTEM's get_virtual_image(centered, calibrated, shift_center, subpixel) and // get_virtual_diffraction(shift_center, subpixel) at f050d207, CPU, Double. // // DEVIATIONS: centred and calibrated together are refused (py4DSTEM divides the pixel origin by the pixel size); the diff --git a/Sources/CuboVirtual/VacuumProbe.swift b/Sources/CuboVirtual/VacuumProbe.swift index 51da7da..6cd48c4 100644 --- a/Sources/CuboVirtual/VacuumProbe.swift +++ b/Sources/CuboVirtual/VacuumProbe.swift @@ -1,8 +1,8 @@ // // VacuumProbe.swift -// The vacuum probe and the cross-correlation kernels Bragg disk detection uses (registration slice 4, 3.5, 3.6): +// The vacuum probe and the cross-correlation kernels Bragg disk detection uses: // py4DSTEM's DataCube.get_vacuum_probe (datacube/datacube.py 516 at f050d207), Probe.measure_disk (= get_probe_size, -// slice 2's ProbeSize.estimate), zero_vacuum, zero_vacuum_sinusoid, generate_synthetic_probe, get_probe_kernel_flat, +// the kit's ProbeSize.estimate), zero_vacuum, zero_vacuum_sinusoid, generate_synthetic_probe, get_probe_kernel_flat, // get_probe_kernel_edge_gaussian and get_probe_kernel_edge_sigmoid (braggvectors/probe.py), CPU, Double. // // A Float32 reference implementation builds kernels on wrapped coordinates with a cosine shoulder in place of diff --git a/Sources/CuboVirtual/VirtualImaging.swift b/Sources/CuboVirtual/VirtualImaging.swift index b38bdf8..7593553 100644 --- a/Sources/CuboVirtual/VirtualImaging.swift +++ b/Sources/CuboVirtual/VirtualImaging.swift @@ -1,6 +1,6 @@ // // VirtualImaging.swift -// The public API of CuboVirtual (registration 1.2): virtual images, and the mean and maximum pattern, on the path the +// The public API of CuboVirtual: virtual images, and the mean and maximum pattern, on the path the // caller chooses. // // py4DSTEM f050d207: DataCube.get_virtual_image (virtualimage.py 67) with centered=False, calibrated=False, diff --git a/Tests/CuboBraggTests/BraggTests.swift b/Tests/CuboBraggTests/BraggTests.swift index 9c8f95e..6614ffe 100644 --- a/Tests/CuboBraggTests/BraggTests.swift +++ b/Tests/CuboBraggTests/BraggTests.swift @@ -68,7 +68,7 @@ func gaps(_ a: [BraggPeak], _ b: [BraggPeak]) -> (position: Double, intensity: D let fixture: Fixture init() throws { fixture = try Fixture() } - /// Every py4DSTEM case: same peaks, same order, to the registered tolerance. + /// Every py4DSTEM case: same peaks, same order, to the stated tolerance. @Test func matchesPy4DSTEM() throws { var worstPosition = 0.0, worstIntensity = 0.0, checked = 0 for c in fixture.cases where (c["reference"] as! String) == "py4dstem" { diff --git a/Tests/CuboBraggTests/SinglePrecisionTests.swift b/Tests/CuboBraggTests/SinglePrecisionTests.swift index a973bcc..f559858 100644 --- a/Tests/CuboBraggTests/SinglePrecisionTests.swift +++ b/Tests/CuboBraggTests/SinglePrecisionTests.swift @@ -362,7 +362,7 @@ import Testing } } -/// The tolerances of the `.single` path, written per quantity (D12) from the maxima measured in this suite (docs/bragg.md). +/// The tolerances of the `.single` path, written per quantity from the maxima measured in this suite (docs/bragg.md). /// The Float32 transform has an absolute error of about 1e-6 of the correlation maximum, so a peak's accuracy falls with its /// height: peaks above 1e-3 of the brightest (every peak of the pins' default cascade) are held to 5e-5 px and 5e-6 (the app's /// own tolerance against py4DSTEM is 2e-4 px); fainter ones (the pins with the spacing filter off reach 4e-5 of the brightest) to 1e-3 px and 5e-4. Phase correlation diff --git a/Tests/CuboCoreTests/CalibrationTests.swift b/Tests/CuboCoreTests/CalibrationTests.swift index 6aefcc2..0e6390b 100644 --- a/Tests/CuboCoreTests/CalibrationTests.swift +++ b/Tests/CuboCoreTests/CalibrationTests.swift @@ -4,7 +4,7 @@ import CuboCore /// py4DSTEM has no test for these calibration numbers, so the tolerance is `assert_allclose`'s default for Float64, rtol 1e-7, with /// atol 1e-12 so that a value that is zero in one arithmetic and 1e-17 in the other agrees (0.8.1; the pins were 1e-12 to 1e-15 before, -/// stricter than the package, D17). Stored values and indices stay exact (`==`). +/// stricter than the package). Stored values and indices stay exact (`==`). private func agrees(_ a: Double, _ b: Double) -> Bool { abs(a - b) <= 1e-12 + 1e-7 * abs(b) } private struct Op: Decodable { diff --git a/Tests/CuboCoreTests/CuboCoreTests.swift b/Tests/CuboCoreTests/CuboCoreTests.swift index e784035..7636f31 100644 --- a/Tests/CuboCoreTests/CuboCoreTests.swift +++ b/Tests/CuboCoreTests/CuboCoreTests.swift @@ -1,6 +1,6 @@ // // CuboCoreTests.swift -// The cube model (registration 1.1, 3.5, 5.5): shapes, tilings, the eager and lazy cubes, the .npy container and the +// The cube model: shapes, tilings, the eager and lazy cubes, the .npy container and the // element conversions. // @@ -29,7 +29,7 @@ import CuboCore #expect(s.elementsPerScanRow == 105); #expect(s.scanIndex(row: 1, column: 2) == 5) #expect(throws: CuboError.invalidShape([2, 0, 5, 7])) { try CubeShape([2, 0, 5, 7]) } #expect(throws: CuboError.invalidShape([2, 3, 5])) { try CubeShape([2, 3, 5]) } - // Two negative extents multiply to a positive count: only the per-extent guard refuses them (mutant M20). + // Two negative extents multiply to a positive count: only the per-extent guard refuses them. #expect(throws: CuboError.invalidShape([-2, -3, 5, 7])) { try CubeShape([-2, -3, 5, 7]) } #expect(throws: CuboError.invalidShape([Int.max / 2, 4, 4, 4])) { try CubeShape([Int.max / 2, 4, 4, 4]) } let decoded = try JSONDecoder().decode(CubeShape.self, from: JSONEncoder().encode(s)) @@ -184,7 +184,7 @@ import CuboCore #expect(eager.values == [1, 2, 3, 40000, 5, 6]) // A version 2.0 header longer than 64 KiB (what version 2.0 exists for): its third length byte is not zero, so a - // reader that drops or swaps the high bytes reads the data from the wrong place (mutant M14b, 2026-10-15). + // reader that drops or swaps the high bytes reads the data from the wrong place. let long = scratch.appendingPathComponent("v2-long.npy") var longHeader = dict + String(repeating: " ", count: 70_000) longHeader += String(repeating: " ", count: (64 - (12 + longHeader.utf8.count + 1) % 64) % 64) + "\n" @@ -236,7 +236,7 @@ import CuboCore if case .cannotOpen? = absent {} else { Issue.record("expected cannotOpen, got \(String(describing: absent))") } } - /// Gate B F2 (2026-10-15): a header whose element or byte count overflows is refused, never trapped on. + /// A header whose element or byte count overflows is refused, never trapped on. @Test(arguments: ["(4294967296, 4294967296, 4294967296, 4294967296)", "(9223372036854775807, 2)", "(3000000000, 3000000000)", "(1152921504606846976,)"]) func testOverflowingShapesAreRefused(_ shape: String) throws { diff --git a/Tests/CuboCoreTests/EllipseFitTests.swift b/Tests/CuboCoreTests/EllipseFitTests.swift index 49536f3..91c7d49 100644 --- a/Tests/CuboCoreTests/EllipseFitTests.swift +++ b/Tests/CuboCoreTests/EllipseFitTests.swift @@ -1,6 +1,6 @@ // // EllipseFitTests.swift -// The ellipse fits (task 18) against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py) and against the planted ellipses of +// The ellipse fits against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py) and against the planted ellipses of // tools/polar-ellipse/planted.py. py4DSTEM has no test for these functions: the direct evaluations are held to // `assert_allclose`'s Float64 default (rtol 1e-7); an iterative fit is compared with py4DSTEM's result at the convergence // tolerance of the optimiser both run (`leastsq`: xtol = 1.49012e-8, relative to the parameter, floor 1), and with the planted diff --git a/Tests/CuboCoreTests/FourierResampleTests.swift b/Tests/CuboCoreTests/FourierResampleTests.swift index 98469ad..ec90e37 100644 --- a/Tests/CuboCoreTests/FourierResampleTests.swift +++ b/Tests/CuboCoreTests/FourierResampleTests.swift @@ -1,6 +1,6 @@ // // FourierResampleTests.swift -// `resample_Q(method="fourier")` and `fourier_resample` (task 18) against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py) and +// `resample_Q(method="fourier")` and `fourier_resample` against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py) and // against the exact band-limited truth of tools/polar-ellipse/planted.py. py4DSTEM has no test for them. Its transform is // single precision (NumPy 2 `fft2` of Float32 is complex64), so the pinned comparison holds the kit to single-precision // agreement, relative 1e-6 of the largest value of a case (the rule of the other resample rows); the exact truth, where the diff --git a/Tests/CuboCoreTests/HotPixelTests.swift b/Tests/CuboCoreTests/HotPixelTests.swift index 4e216d6..4231e73 100644 --- a/Tests/CuboCoreTests/HotPixelTests.swift +++ b/Tests/CuboCoreTests/HotPixelTests.swift @@ -17,7 +17,7 @@ private func hotPixelPins() throws -> [HotPixelPin] { let shape = try CubeShape(p.shape) let cube = try MemoryCube(shape:shape,values:p.values) let result = try HotPixelFilter.filter(cube,threshold:p.threshold,comparisonIndex:p.index) - // Pinned actual Float32 py4DSTEM calls: means, masks and all output samples, asserted exact today (D12: a tolerance against py4DSTEM is allowed for the samples). + // Pinned actual Float32 py4DSTEM calls: means, masks and all output samples, asserted exact today (a tolerance against py4DSTEM would be allowed for the samples). #expect(result.mean.map(\.bitPattern) == p.mean.map(\.bitPattern)) #expect(result.mask == p.mask) #expect(result.cube.values.map(\.bitPattern) == p.filtered.map(\.bitPattern)) diff --git a/Tests/CuboCoreTests/PolicyParameterTests.swift b/Tests/CuboCoreTests/PolicyParameterTests.swift index 3642b4d..842745a 100644 --- a/Tests/CuboCoreTests/PolicyParameterTests.swift +++ b/Tests/CuboCoreTests/PolicyParameterTests.swift @@ -1,7 +1,7 @@ // // PolicyParameterTests.swift // The generality pass (0.2.0): a policy the kit used to fix is a parameter whose default is the old value. The default -// path is pinned by the slice tests (D12); these check that the parameter takes effect and is validated (tier 2). +// path is pinned by the slice tests; these check that the parameter takes effect and is validated. // import Testing diff --git a/Tests/CuboKitParityTests/RealDataTests.swift b/Tests/CuboKitParityTests/RealDataTests.swift index d1f7b8d..d8a94c6 100644 --- a/Tests/CuboKitParityTests/RealDataTests.swift +++ b/Tests/CuboKitParityTests/RealDataTests.swift @@ -101,7 +101,7 @@ import CuboVirtual log.append(line) } - // Eager equals lazy, exactly, on the real cube (registration 3.4, gate 3). + // Eager equals lazy, exactly, on the real cube. let eager = try await MemoryCube(loading: cube) let e = try await VirtualDiffraction.meanAndMax(of: eager, path: .cpu) assertIdentical(e.mean.values, cpu.mean.values, "\(name) mean eager") diff --git a/Tests/CuboKitTestSupport/PhaseSupport.swift b/Tests/CuboKitTestSupport/PhaseSupport.swift index 6e70db4..88d4bc8 100644 --- a/Tests/CuboKitTestSupport/PhaseSupport.swift +++ b/Tests/CuboKitTestSupport/PhaseSupport.swift @@ -59,7 +59,7 @@ package func checkChain(measured: ScanVectorField, reference: CenterOfMassRefere if case .planeFit = reference { near(fitted.row, Pins2.doubles(ch["fitted_x"]), 1e-10, "\(name) fitted row") near(fitted.column, Pins2.doubles(ch["fitted_y"]), 1e-10, "\(name) fitted column") - // py4DSTEM's curve_fit plane is recorded, not asserted (amendment 2026-10-15: up to 8.3e-6 px from the exact + // py4DSTEM's curve_fit plane is recorded, not asserted (up to 8.3e-6 px from the exact // least-squares plane of the same input; its stopping tolerance, not the definition). print("slice2 \(name): py4DSTEM curve_fit plane vs least squares of its own input \(ch["fit_distance_py4dstem_vs_lstsq_px"] ?? "n/a") px") } diff --git a/Tests/CuboKitTestSupport/S3Support.swift b/Tests/CuboKitTestSupport/S3Support.swift index dc97507..d75dd70 100644 --- a/Tests/CuboKitTestSupport/S3Support.swift +++ b/Tests/CuboKitTestSupport/S3Support.swift @@ -26,13 +26,13 @@ package enum S3 { package static let fitModels: [String: OriginFitModel] = ["constant": .constant, "plane": .plane, "parabola": .parabola, "bezier_two": .bezierTwo] - /// Every claim of section 4 on one pinned cube; prints what it measured. + /// Every pinned claim on one cube; prints what it measured. package static func check(_ e: [String: Any], cube: MappedCube, integer: Bool) async throws { let name = e["cube"] as! String let shape = cube.shape let r = Double(e["radius"] as! String)! var log = ["slice3 \(name)"] - // origins: the default radius is the probe size of the max pattern, asserted exact today (D12); origins 1e-9 px + // origins: the default radius is the probe size of the max pattern, asserted exact today; origins 1e-9 px let (measured, radius) = try await OriginMeasurement.measure(of: cube) #expect(radius.bitPattern == r.bitPattern, "\(name): radius \(radius) vs \(r)") let orow = d(e["origin_row"]), ocol = d(e["origin_column"]) @@ -79,7 +79,7 @@ package enum S3 { let pm = pats[key]! if key == "rounded" { assertRelative(mean.values, d(pm["mean"]), 1e-10, "\(name) \(key) mean") - } else { // the Fourier shift's error is normwise (amendment): |Δ| ≤ 1e-12 · max|want| + } else { // the Fourier shift's error is normwise: |Δ| ≤ 1e-12 · max|want| let want = d(pm["mean"]), top = want.map(abs).max()! #expect(zip(mean.values, want).allSatisfy { abs($0 - $1) <= 1e-12 * top }, "\(name) fourier mean") } diff --git a/Tests/CuboKitTestSupport/S4Support.swift b/Tests/CuboKitTestSupport/S4Support.swift index d4ac8dd..26c5f1d 100644 --- a/Tests/CuboKitTestSupport/S4Support.swift +++ b/Tests/CuboKitTestSupport/S4Support.swift @@ -41,7 +41,7 @@ package enum S4 { return worst } - /// Every claim of section 4 on one pinned cube; prints what it measured. + /// Every pinned claim on one cube; prints what it measured. package static func check(_ e: [String: Any], cube: MappedCube) async throws { let name = e["cube"] as! String, integer = e["integer"] as! Bool let shape = cube.shape, qy = shape.detectorRows, qx = shape.detectorColumns @@ -57,7 +57,7 @@ package enum S4 { if stat == .mean { assertRelative(got, want, 1e-10, "\(name) \(key) mean") if !own.isEmpty, key == "weights" { assertRelative(got, own, 1e-10, "\(name) \(key) mean vs py4DSTEM") } - if integer, key == "mask" { // exact sums of integers, one division: exact today (D12) + if integer, key == "mask" { // exact sums of integers, one division: exact today assertIdentical(got, want, "\(name) \(key) mean (integer)") assertIdentical(got, own, "\(name) \(key) mean vs py4DSTEM (integer)") } diff --git a/Tests/CuboKitTestSupport/TestSupport.swift b/Tests/CuboKitTestSupport/TestSupport.swift index 50b032a..e75ecb3 100644 --- a/Tests/CuboKitTestSupport/TestSupport.swift +++ b/Tests/CuboKitTestSupport/TestSupport.swift @@ -1,8 +1,8 @@ // // TestSupport.swift // The fixtures directory (read by #filePath, not as resources), the pins written by tools/slice-1/pins.py before any -// Swift existed (registration section 4), the real-data directory (outside the repository), and the registered -// comparison: relative `t` with the floor 1e-6 · max|want| over the image (registration 3.4). +// Swift existed, the real-data directory (outside the repository), and the relative +// comparison: relative `t` with the floor 1e-6 · max|want| over the image. // import Foundation @@ -14,7 +14,7 @@ package enum Fixtures { package static let directory = FixtureDirs.fixtures(of: "CuboVirtualTests") package static func url(_ name: String) -> URL { directory.appendingPathComponent(name) } - /// `$CUBOKIT_REAL_DATA`, default `~/Library/Caches/CuboKit/real-data` (registration 4). + /// `$CUBOKIT_REAL_DATA`, default `~/Library/Caches/CuboKit/real-data`. package static var realData: URL { if let env = ProcessInfo.processInfo.environment["CUBOKIT_REAL_DATA"], !env.isEmpty { return URL(fileURLWithPath: env) @@ -110,7 +110,7 @@ package struct GeometryPin: Decodable, Sendable { } } -/// The generator's geometry dictionary (registration 5.2), in the kit's names. +/// The generator's geometry dictionary, in the kit's names. package struct GeometrySpec: Decodable, Sendable { package let kind: String package let centerRow: Double?, centerColumn: Double?, radius: Double?, inner: Double?, outer: Double? @@ -130,7 +130,7 @@ package struct GeometrySpec: Decodable, Sendable { } } -/// The registered comparison (3.4): |got − want| ≤ t · max(|want|, 1e-6 · max_k |want_k|). Returns the largest +/// The relative comparison: |got − want| ≤ t · max(|want|, 1e-6 · max_k |want_k|). Returns the largest /// distance in units of that denominator, so a test can print what it measured. @discardableResult package func assertRelative(_ got: [Double], _ want: [Double], _ t: Double, _ message: @autoclosure () -> String, @@ -151,7 +151,7 @@ package func assertRelative(_ got: [Double], _ want: [Double], _ t: Double, _ me } /// Exact comparison (bit pattern). Use for readers, I/O, indices, metadata and same-code equality; for numerics against -/// a Python reference D12 prefers a written tolerance. +/// a Python reference, use the tolerance the reference's own tests use. package func assertIdentical(_ got: [Double], _ want: [Double], _ message: @autoclosure () -> String, sourceLocation: SourceLocation = SourceLocation(fileID: #fileID, filePath: #filePath, line: #line, column: #column)) { @@ -162,7 +162,7 @@ package func assertIdentical(_ got: [Double], _ want: [Double], _ message: @auto sourceLocation: sourceLocation) } -/// Whether the real-data directory holds `name`'s export (registration 4); the real-data tests are enabled by it. +/// Whether the real-data directory holds `name`'s export; the real-data tests are enabled by it. package func realDataIsPresent(_ name: String) -> Bool { FileManager.default.fileExists(atPath: Fixtures.realData.appendingPathComponent("\(name)-pins.json").path) } diff --git a/Tests/CuboParallaxTests/DFTAndSingleShifterTests.swift b/Tests/CuboParallaxTests/DFTAndSingleShifterTests.swift index 0b56b0d..6d67210 100644 --- a/Tests/CuboParallaxTests/DFTAndSingleShifterTests.swift +++ b/Tests/CuboParallaxTests/DFTAndSingleShifterTests.swift @@ -3,7 +3,7 @@ import Testing import CuboParallax /// 0.7.0: the Double FFT runs on vDSP_DFT_zop where vDSP takes the length (checked here against a naive DFT, with a length it -/// does not take beside them), and the Float32 shifter agrees with the Double one (D12 Float32: 1e-5 of the image range, written +/// does not take beside them), and the Float32 shifter agrees with the Double one (the float32 tolerance: 1e-5 of the image range, written /// per test). @Suite struct DFTAndSingleShifterTests { private func image(_ rows: Int, _ columns: Int) -> [Double] { diff --git a/Tests/CuboParallaxTests/StreamedPreprocessingTests.swift b/Tests/CuboParallaxTests/StreamedPreprocessingTests.swift index 137b2e4..fd8e10c 100644 --- a/Tests/CuboParallaxTests/StreamedPreprocessingTests.swift +++ b/Tests/CuboParallaxTests/StreamedPreprocessingTests.swift @@ -3,7 +3,7 @@ import Testing import CuboCore import CuboParallax -/// Streamed preprocessing against the resident Double `prepare`: Float32 rounding only, relative 2e-5 (D12, Float32). +/// Streamed preprocessing against the resident Double `prepare`: Float32 rounding only, relative 2e-5 (the float32 tolerance). @Suite struct StreamedPreprocessingTests { static let tolerance = 2e-5 static let sampling = ParallaxShift(row: 0.05, column: 0.05) diff --git a/Tests/CuboPhaseTests/CuboPhaseTests.swift b/Tests/CuboPhaseTests/CuboPhaseTests.swift index b5383d6..faca230 100644 --- a/Tests/CuboPhaseTests/CuboPhaseTests.swift +++ b/Tests/CuboPhaseTests/CuboPhaseTests.swift @@ -1,8 +1,7 @@ // // CuboPhaseTests.swift -// The centre-of-mass reference, the R–Q rotation and DPC against py4DSTEM f050d207 and planted truth (registration -// slice 2, 3.2–3.4, 4, 5). The pins are CuboVirtualTests/Fixtures/slice-2-pins.json; the real data are slice 2's -// exports in $CUBOKIT_REAL_DATA. +// The centre-of-mass reference, the R–Q rotation and DPC against py4DSTEM f050d207 and planted truth. The pins are +// CuboVirtualTests/Fixtures/slice-2-pins.json; the real data are slice 2's exports in $CUBOKIT_REAL_DATA. // import Foundation @@ -39,7 +38,7 @@ import CuboVirtual } @Suite struct CuboPhaseTruthTests { - /// 5.1: the reconstruction of a known phase gradient correlates with the phase at ≥ 0.99. + /// The reconstruction of a known phase gradient correlates with the phase at ≥ 0.99. @Test func testKnownPhaseIsReconstructed() throws { for f in Pins2.pins["fields"] as! [[String: Any]] where f["theta"] as! Double == 0 { let shape = f["shape"] as! [Int] @@ -122,8 +121,8 @@ import CuboVirtual #expect(off.accepted.allSatisfy { $0 } && off.finalStepSize == 8) } - /// Gate B F3: degrees become radians as NumPy's deg2rad and Python's math.radians do, deg · (π/180), asserted exact today - /// (deg · π / 180 differs by an ulp for 38 of the 179 default angles); F4: a non-finite field is refused. + /// Degrees become radians as NumPy's deg2rad and Python's math.radians do, deg · (π/180), asserted exact today + /// (deg · π / 180 differs by an ulp for 38 of the 179 default angles); a non-finite field is refused. @Test func testRadiansAndNonFiniteFields() throws { let f = (Pins2.pins["fields"] as! [[String: Any]]).first { $0["theta"] as! Double == 23 }! let shape = f["shape"] as! [Int] diff --git a/Tests/CuboPhaseTests/PolicyParameterTests.swift b/Tests/CuboPhaseTests/PolicyParameterTests.swift index 8a5e299..ad05868 100644 --- a/Tests/CuboPhaseTests/PolicyParameterTests.swift +++ b/Tests/CuboPhaseTests/PolicyParameterTests.swift @@ -1,6 +1,6 @@ // // PolicyParameterTests.swift -// The generality pass (0.2.0): the plane fit's degeneracy tolerance is a parameter, 1e-12 by default (tier 2). +// The generality pass (0.2.0): the plane fit's degeneracy tolerance is a parameter, 1e-12 by default. // import Testing @@ -20,7 +20,7 @@ import CuboPhase #expect(throws: CuboPhaseError.planeFitDegenerate) { try CenterOfMassReference.normalize(field, reference: .planeFit, degeneracyTolerance: 1) } - // refuter 0.2.0, F1: a negative tolerance hid a degenerate fit behind nan_to_num; refused now + // found in review of 0.2.0: a negative tolerance hid a degenerate fit behind nan_to_num; refused now for bad in [-1.0, -.infinity, .nan] { #expect(throws: CuboPhaseError.self) { try CenterOfMassReference.normalize(field, reference: .planeFit, degeneracyTolerance: bad) diff --git a/Tests/CuboPtychoTests/SinglePrecisionTests.swift b/Tests/CuboPtychoTests/SinglePrecisionTests.swift index 7a0bbb7..76078df 100644 --- a/Tests/CuboPtychoTests/SinglePrecisionTests.swift +++ b/Tests/CuboPtychoTests/SinglePrecisionTests.swift @@ -4,7 +4,7 @@ import CuboPtycho /// 0.7.0: the Float32 single-slice engine against the Double engine, on amplitudes that come from a known object (a smooth /// phase object read through a Gaussian probe, naive DFT), so the data is consistent and the iteration is well conditioned. -/// Tolerances (D12 Float32, measured 2.9e-5 object / 1.5e-6 probe after 3 iterations on a unit-modulus object): object 1e-4 +/// Tolerances (float32, measured 2.9e-5 object / 1.5e-6 probe after 3 iterations on a unit-modulus object): object 1e-4 /// absolute, probe 1e-5 absolute, error history 1e-5 relative. @Suite struct SinglePrecisionTests { static let d = 32, objectSize = 80, count = 36 diff --git a/Tests/CuboStrainTests/LatticeStrainTests.swift b/Tests/CuboStrainTests/LatticeStrainTests.swift index fb7b508..aabe30c 100644 --- a/Tests/CuboStrainTests/LatticeStrainTests.swift +++ b/Tests/CuboStrainTests/LatticeStrainTests.swift @@ -82,7 +82,7 @@ import Testing #expect(try LatticeStrain.chooseBasisConsensus([]) == nil) } - // MARK: - Round 6: consensus diagnostics, cancellation, MAD inliers + // MARK: - Consensus diagnostics, cancellation, MAD inliers final class Counter: @unchecked Sendable { private let lock = NSLock(); private var n = 0 diff --git a/Tests/CuboStrainTests/StrainRobustTests.swift b/Tests/CuboStrainTests/StrainRobustTests.swift index 02360ee..78db99b 100644 --- a/Tests/CuboStrainTests/StrainRobustTests.swift +++ b/Tests/CuboStrainTests/StrainRobustTests.swift @@ -4,7 +4,7 @@ import CuboStrain /// The robust local fit, the basis helpers, basis-rounding indexing and the frame rotation that mac4DSTEM's strain map is /// built from, against hand transcriptions of the app's formulas on small synthetic lattices. -/// Tolerances (D12): the Double helpers (score, tolerance, condition, rounding, frame rotation) 1e-9 relative; the robust +/// Tolerances: the Double helpers (score, tolerance, condition, rounding, frame rotation) 1e-9 relative; the robust /// fit, whose arithmetic is Float32 like `fitIntensityOnce`, 1e-6 relative to the vector length (limit 1e-5); /// planted strain recovered through the Float32 fit to 1e-5 absolute. @Suite struct StrainRobustTests { diff --git a/Tests/CuboVirtualTests/PinsTests.swift b/Tests/CuboVirtualTests/PinsTests.swift index 1f7fff5..27133c7 100644 --- a/Tests/CuboVirtualTests/PinsTests.swift +++ b/Tests/CuboVirtualTests/PinsTests.swift @@ -1,6 +1,6 @@ // // PinsTests.swift -// Agreement with py4DSTEM f050d207 on the synthetic cubes (registration 3.4, 4, 5.1, 5.2), read from slice-1-pins.json. +// Agreement with py4DSTEM f050d207 on the synthetic cubes, read from slice-1-pins.json. // Agreement is agreement, not truth (TruthTests); a difference is a failure of the port, never a tolerance to widen. // @@ -49,7 +49,7 @@ import CuboVirtual } } - /// Registration 3.4: CPU virtual images. Integer cubes exact against py4DSTEM's own call (D12: tolerance allowed); every cube within + /// CPU virtual images. Integer cubes exact against py4DSTEM's own call (a tolerance would be allowed); every cube within /// relative 1e-10 of the float64 evaluation; py4DSTEM's own float32 distance printed beside the kit's. @Test func testVirtualImagesOnTheCPUAgreeWithPy4DSTEM() async throws { for cube in pins.cubes { @@ -84,9 +84,9 @@ import CuboVirtual } } - /// Registration 3.4: Metal within relative 1e-5 of the CPU path; max exact. + /// Metal within relative 1e-5 of the CPU path; max exact. @Test(.enabled(if: VirtualDiffraction.metalIsAvailable, "no Metal device on this machine")) - func testMetalAgreesWithTheCPUWithinTheRegisteredTolerance() async throws { + func testMetalAgreesWithTheCPUWithinTolerance() async throws { for cube in pins.cubes { let source = try cube.mapped() for tiling in [ScanTiling.automatic, .rows(1)] { diff --git a/Tests/CuboVirtualTests/PolarRadialTests.swift b/Tests/CuboVirtualTests/PolarRadialTests.swift index 097e257..78cc694 100644 --- a/Tests/CuboVirtualTests/PolarRadialTests.swift +++ b/Tests/CuboVirtualTests/PolarRadialTests.swift @@ -1,6 +1,6 @@ // // PolarRadialTests.swift -// The polar data cube and the radial background (task 17) against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py, run on +// The polar data cube and the radial background against py4DSTEM 0.14.19 (tools/polar-ellipse/pins.py, run on // planted cubes) and against the planted truth of tools/polar-ellipse/planted.py. No upstream test exists for these functions, so // the tolerance is `assert_allclose`'s Float64 default (rtol 1e-7) with atol 1e-12 (the rule of CalibrationTests); the port runs // the same operations in the same order and the measured difference is printed. @@ -346,8 +346,8 @@ private struct Planted { #expect(circleRange > 20 * rightRange) } - /// The whole chain on the planted tilted case: fit the ring of the pattern (task 18), store the fit in the calibration - /// (`Calibration.ellipse`), transform (task 17): the result is constant in phi, as with the true ellipse. + /// The whole chain on the planted tilted case: fit the ring of the pattern, store the fit in the calibration + /// (`Calibration.ellipse`), transform: the result is constant in phi, as with the true ellipse. @Test func testFittedEllipseFeedsThePolarCube() async throws { let p = Planted() let cube = try p.cube() diff --git a/Tests/CuboVirtualTests/PolicyParameterTests.swift b/Tests/CuboVirtualTests/PolicyParameterTests.swift index 9f55639..4561aa2 100644 --- a/Tests/CuboVirtualTests/PolicyParameterTests.swift +++ b/Tests/CuboVirtualTests/PolicyParameterTests.swift @@ -2,7 +2,7 @@ // PolicyParameterTests.swift // The generality pass (0.2.0): the Gaussian filter's radius guard, the Friedel upsampling cap and the median's working // memory are parameters whose defaults are the old values (10⁶ px, 1024, 256 MiB); the default path is pinned bit for -// bit by the slice tests (tier 2). +// bit by the slice tests. // import Foundation @@ -16,7 +16,7 @@ import CuboVirtual #expect(try GaussianFilter(sigma: 10).radius == 40) #expect(throws: CuboVirtualError.self) { try GaussianFilter(sigma: 10, maximumRadius: 40) } #expect(try GaussianFilter(sigma: 10, maximumRadius: 41).kernel == GaussianFilter(sigma: 10).kernel) - // refuter 0.2.0, F2: a lifted guard trapped (Int overflow) or exhausted memory; bounded at 10⁸ now + // found in review of 0.2.0: a lifted guard trapped (Int overflow) or exhausted memory; bounded at 10⁸ now #expect(throws: CuboVirtualError.self) { try GaussianFilter(sigma: 1e9, maximumRadius: .max) } #expect(throws: CuboVirtualError.self) { try GaussianFilter(sigma: 1, maximumRadius: 0) } } @@ -29,7 +29,7 @@ import CuboVirtual let raised = try FriedelOrigin.origin(of: image, upsampleFactor: 1100, maximumUpsampleFactor: 2048) #expect(abs(raised.row - 3.5) <= 1e-6 && abs(raised.column - 3.5) <= 1e-6, "\(raised)") #expect(throws: CuboVirtualError.self) { try FriedelOrigin.origin(of: image, upsampleFactor: 16, maximumUpsampleFactor: 8) } - // refuter 0.2.0, F2 and F3: Int.max trapped; a cap below 3 gave a garbled message + // found in review of 0.2.0: Int.max trapped; a cap below 3 gave a garbled message #expect(throws: CuboVirtualError.self) { try FriedelOrigin.origin(of: image, upsampleFactor: .max, maximumUpsampleFactor: .max) } diff --git a/Tests/CuboVirtualTests/RefuterRegressionTests.swift b/Tests/CuboVirtualTests/RefuterRegressionTests.swift index 51ad2c4..28d6287 100644 --- a/Tests/CuboVirtualTests/RefuterRegressionTests.swift +++ b/Tests/CuboVirtualTests/RefuterRegressionTests.swift @@ -1,7 +1,7 @@ // // RefuterRegressionTests.swift -// The Gate B refuter's cases on 85318cd (2026-10-15; registration section 9), kept so they gate. F1: the Metal path missed -// the registered relative 1e-5 (floor 1e-6 · max) by up to 8e-3 on zero-mean, dark-subtracted noise and on cancelling +// Cases an adversarial review found on 85318cd (2026-10-15), kept so they gate. F1: the Metal path missed +// relative 1e-5 (floor 1e-6 · max) by up to 8e-3 on zero-mean, dark-subtracted noise and on cancelling // data; fixed by compensated kernels. F3 and F4: the CPU mean turned ±∞ into NaN and dropped the sign of −0; Metal // dropped NaN from the maximum. // @@ -33,7 +33,7 @@ struct RefuterRegressionTests { static let noiseShapes: [[Int]] = [[32, 32, 64, 64], [128, 128, 32, 32], [20, 20, 128, 128]] /// F1: pure N(0, 500) noise, no signal; plain and randomly weighted detectors; Metal within 1e-5 of the CPU path - /// under the registered floor, the max exact. + /// under the 1e-6 · max floor, the max exact. @Test(arguments: noiseShapes) func testMetalMeetsTheToleranceOnZeroMeanNoise(_ extents: [Int]) async throws { let shape = try CubeShape(extents) diff --git a/Tests/CuboVirtualTests/SelectedAreaMetalTests.swift b/Tests/CuboVirtualTests/SelectedAreaMetalTests.swift index b61d2fe..0fc9b58 100644 --- a/Tests/CuboVirtualTests/SelectedAreaMetalTests.swift +++ b/Tests/CuboVirtualTests/SelectedAreaMetalTests.swift @@ -17,7 +17,7 @@ private func testCube(rows: Int, columns: Int, qr: Int, qc: Int, seed: UInt64) t return try MemoryCube(shape: shape, values: v) } -// Metal against CPU: relative 1e-5 (the registered Metal tolerance; py4DSTEM states none for its float32 paths). +// Metal against CPU: relative 1e-5 (the kit's Metal tolerance; py4DSTEM states none for its float32 paths). @Suite(.enabled(if: VirtualDiffraction.metalIsAvailable, "no Metal device on this machine")) struct SelectedAreaMetalTests { private func selection(_ n: Int) -> (mask: [Bool], weights: [Double]) { diff --git a/Tests/CuboVirtualTests/Slice2Tests.swift b/Tests/CuboVirtualTests/Slice2Tests.swift index a1095a5..d5ea181 100644 --- a/Tests/CuboVirtualTests/Slice2Tests.swift +++ b/Tests/CuboVirtualTests/Slice2Tests.swift @@ -1,6 +1,6 @@ // // Slice2Tests.swift -// Centre of mass and probe size against py4DSTEM f050d207 and planted truth (registration slice 2, 3.1, 3.5, 4, 5), +// Centre of mass and probe size against py4DSTEM f050d207 and planted truth, // read from slice-2-pins.json (tools/slice-2/pins.py, run before any Swift). // @@ -34,7 +34,7 @@ enum Slice2 { #expect(Slice2.com.count == 24 && Slice2.probe.count == 20) } - /// 3.1, 4: the sums within 1e-10 of the float64 definition (integer cubes exact today, D12), the centre of mass within + /// The sums within 1e-10 of the float64 definition (integer cubes exact today), the centre of mass within /// 1e-9 px where the denominator is conditioned; py4DSTEM's own float32 distance printed. @Test func testCenterOfMassAgreesWithTheFloat64Definition() async throws { for e in Slice2.com { @@ -52,7 +52,7 @@ enum Slice2 { assertRelative(com.momentRow.values, Slice2.doubles(e["moment_row"]), 1e-10, "\(name)/\(mask) moment row") assertRelative(com.momentColumn.values, Slice2.doubles(e["moment_column"]), 1e-10, "\(name)/\(mask) moment col") } - // conditioned (registration 4): |S| ≥ 1e-6 · Σ|w P|, from the cube itself + // conditioned: |S| ≥ 1e-6 · Σ|w P|, from the cube itself let eager = try await MemoryCube(loading: cube) let n = cube.shape.detectorCount let w = try DetectorMask.make(Slice2.mask(e) ?? .mask([Float](repeating: 1, count: n)), shape: cube.shape) @@ -85,7 +85,7 @@ enum Slice2 { } } - /// 3.5: py4DSTEM's get_probe_size is float64 on these patterns, so the test asserts it exactly today (D12: a tolerance is allowed); where it + /// Py4DSTEM's get_probe_size is float64 on these patterns, so the test asserts it exactly today (a tolerance would be allowed); where it /// returns NaN the kit refuses. @Test func testProbeSizeIsPy4DSTEMsBitForBit() async throws { for e in Slice2.probe { @@ -115,7 +115,7 @@ enum Slice2 { } @Suite struct Slice2TruthTests { - /// 5.1: two pixels a at (i0, j0) and b at (i0+1, j0+1): row = i0 + b/(a+b), column = j0 + b/(a+b). + /// Two pixels a at (i0, j0) and b at (i0+1, j0+1): row = i0 + b/(a+b), column = j0 + b/(a+b). @Test func testPlantedShiftIsRecovered() async throws { let cube = try MappedCube(npy: Slice2.cubeURL("shift")) let truth = Slice2.pins["shift_truth"] as! [String: Any] @@ -126,7 +126,7 @@ enum Slice2 { } } - /// 5.1: a uniform disk: radius sqrt(n/π), centre exactly the disk's, on a pixel and a half-pixel. + /// A uniform disk: radius sqrt(n/π), centre exactly the disk's, on a pixel and a half-pixel. @Test func testUniformDiskProbeTruth() throws { for e in Slice2.probe where (e["cube"] as! String).hasPrefix("disk_") { let shape = e["shape"] as! [Int] @@ -157,10 +157,10 @@ enum Slice2 { } } -/// The Gate B refuter's slice-2 cases on 84815c9 (registration slice 2, section 9), kept so they gate. +/// Slice-2 cases an adversarial review found on 84815c9, kept so they gate. @Suite(.enabled(if: VirtualDiffraction.metalIsAvailable, "no Metal device on this machine")) struct Slice2RefuterRegressionTests { - /// F1: on a 1024² detector the row moment of an integer cube passes 3e10; the first Metal kernel's Float32 correction + /// On a 1024² detector the row moment of an integer cube passes 3e10; the first Metal kernel's Float32 correction /// lost it (2e-4 px). Integer cubes are exact on Metal now (renormalised double-float sums, exact to 2^48). @Test func testMetalCenterOfMassIsExactOnLargeIntegerCubes() async throws { let shape = try CubeShape([2, 2, 1024, 1024]) @@ -178,8 +178,8 @@ struct Slice2RefuterRegressionTests { #expect(cpu.momentRow.values.max()! > 3e10) } - /// F1: dark-subtracted data (a blob minus a wider blob plus noise): Metal within 1e-9 px of the CPU where - /// |S| ≥ 1e-3 · Σ|w P| (the Metal condition of the amendment). + /// Dark-subtracted data (a blob minus a wider blob plus noise): Metal within 1e-9 px of the CPU where + /// |S| ≥ 1e-3 · Σ|w P| (the Metal path's conditioning bar). @Test func testMetalCenterOfMassOnDarkSubtractedData() async throws { let q = 128, shape = try CubeShape([3, 3, q, q]) var g = Normals(state: 31) diff --git a/Tests/CuboVirtualTests/Slice3Tests.swift b/Tests/CuboVirtualTests/Slice3Tests.swift index cdd84eb..7952010 100644 --- a/Tests/CuboVirtualTests/Slice3Tests.swift +++ b/Tests/CuboVirtualTests/Slice3Tests.swift @@ -1,6 +1,6 @@ // // Slice3Tests.swift -// Per-position origins, fits, centred, calibrated and shifted virtual imaging (registration slice 3) against py4DSTEM +// Per-position origins, fits, centred, calibrated and shifted virtual imaging against py4DSTEM // f050d207 and planted truth: slice-3-pins.json and, on this machine, the real-data exports (-s3-pins.json). // @@ -17,7 +17,7 @@ import CuboVirtual try await S3.check(e, cube: try MappedCube(npy: Fixtures.url("\(name).npy")), integer: name == "planted_u16" || name == "counts16") } - /// 5: the origins of the planted cubes are the planted centres; the rounded shifted mean is the unshifted disk. + /// The origins of the planted cubes are the planted centres; the rounded shifted mean is the unshifted disk. @Test(arguments: ["planted_f32", "planted_u16"]) func testPlantedBeamShiftIsRecovered(_ name: String) async throws { let e = (S3.pins["cubes"] as! [[String: Any]]).first { $0["cube"] as! String == name }! @@ -34,7 +34,7 @@ import CuboVirtual #expect(mean.values == disk) } - /// 5: exact surfaces recovered by their own model; one outlier removed by the robust fit. + /// Exact surfaces recovered by their own model; one outlier removed by the robust fit. @Test func testFitTruth() throws { for (key, t) in S3.pins["fit_truth"] as! [String: [String: Any]] { let model = S3.fitModels[key]!, shape = t["shape"] as! [Int] @@ -70,7 +70,7 @@ import CuboVirtual #expect(abs(b.row.values[0] - m / s) <= 1e-12, "\(b.row.values[0]) vs \(m / s)") } - /// 3.3: shifts of exactly ±0.5 round half to even (to 0): the rounded shifted image is the unshifted one. + /// Shifts of exactly ±0.5 round half to even (to 0): the rounded shifted image is the unshifted one. @Test func testHalfShiftsRoundToEven() async throws { let cube = try MappedCube(npy: Fixtures.url("gaussian.npy")) let rows = (0..<30).map { Double($0 % 2) } // mean 0.5: every shift is ±0.5 @@ -82,8 +82,8 @@ import CuboVirtual assertRelative(shifted.values, plain.values, 1e-12, "half shifts") } - /// Gate B (slice 3) F1: a mirror-symmetric pattern filters to exactly symmetric values in scipy's order, so the first - /// of the tied maxima wins: a disk centred at (11.5, 12.5) has its origin at (11, 12) — the refuter's case. + /// A mirror-symmetric pattern filters to exactly symmetric values in scipy's order, so the first + /// of the tied maxima wins: a disk centred at (11.5, 12.5) has its origin at (11, 12). @Test func testSymmetricTiesKeepTheFirstArgmax() async throws { let q = (24, 25) let values: [Float] = (0..<(q.0 * q.1)).map { k in @@ -110,7 +110,7 @@ import CuboVirtual @Test func testKernelCentringAndRefusals() async throws { let k = try GaussianFilter(sigma: 2.6).kernel #expect(abs(k.reduce(0, +) - 1) <= 1e-15 && k == k.reversed() && k.count == 2 * Int(4 * 2.6 + 0.5) + 1) - // scipy's radius int(4σ + 0.5): σ = 1.7 gives 7, where int(4σ) would give 6 (mutant G1) + // scipy's radius int(4σ + 0.5): σ = 1.7 gives 7, where int(4σ) would give 6 #expect(try GaussianFilter(sigma: 1.7).radius == 7) #expect(try GaussianFilter(sigma: 2.6).radius == 10) let flat = try GaussianFilter(sigma: 1.7).apply(Array(repeating: 3.25, count: 63), rows: 7, columns: 9) diff --git a/Tests/CuboVirtualTests/Slice4Tests.swift b/Tests/CuboVirtualTests/Slice4Tests.swift index 76ad363..6d446c1 100644 --- a/Tests/CuboVirtualTests/Slice4Tests.swift +++ b/Tests/CuboVirtualTests/Slice4Tests.swift @@ -1,7 +1,7 @@ // // Slice4Tests.swift // The median pattern, virtual diffraction over a selection, origins with a scan mask, the Friedel origin, the beamstop -// mask, the vacuum probe and its kernels (registration slice 4) against py4DSTEM f050d207 and planted truth: +// mask, the vacuum probe and its kernels against py4DSTEM f050d207 and planted truth: // slice-4-pins.json and, on this machine, the real-data exports (-s4-pins.json). // @@ -18,7 +18,7 @@ import CuboVirtual try await S4.check(e, cube: try MappedCube(npy: Fixtures.url("\(name).npy"))) } - /// 5: the median is the closed form; one outlier position moves it at most one rank. + /// The median is the closed form; one outlier position moves it at most one rank. @Test func testMedianTruth() async throws { for t in S4.pins["median_truth"] as! [[String: Any]] { let nm = t["cube"] as! String @@ -31,7 +31,7 @@ import CuboVirtual } } - /// 5: one selected position gives its pattern; weights all one give the mean; one-hot weights give the pattern and max(P, 0). + /// One selected position gives its pattern; weights all one give the mean; one-hot weights give the pattern and max(P, 0). @Test func testSelectionTruth() async throws { let cube = try MappedCube(npy: Fixtures.url("counts16.npy")) let shape = cube.shape, N = shape.scanCount @@ -54,7 +54,7 @@ import CuboVirtual #expect(try await VirtualDiffraction.pattern(of: cube, statistic: .max).values == mx.values) } - /// 3.1, 3.3, 3.4, 3.6: the refusals where py4DSTEM returns NaN or ∞ or raises. + /// The refusals where py4DSTEM returns NaN or ∞ or raises. @Test func testRefusals() async throws { let cube = try MappedCube(npy: Fixtures.url("counts16.npy")) let N = cube.shape.scanCount @@ -119,10 +119,10 @@ import CuboVirtual #expect(hole.indices.filter { hole[$0] } == [0]) } - /// Gate B on bc41094 (registration 9): the refusals for an all-false Friedel mask, an upsampling factor above 1024 and + /// Cases a review found on bc41094: the refusals for an all-false Friedel mask, an upsampling factor above 1024 and /// an empty pattern or probe (each trapped or was ill-posed); NumPy's keep-first rule for equal maxima (±0); the /// kernels' messages name the normalisation that failed. - @Test func testGateBRegressions() async throws { + @Test func testReviewRegressions() async throws { let cube = try MappedCube(npy: Fixtures.url("friedel_plain.npy")) await #expect(throws: CuboVirtualError.invalidInput("the mask keeps no pixel of the pattern")) { try await FriedelOrigin.measure(of: cube, mask: Array(repeating: false, count: 32 * 34)) @@ -148,7 +148,7 @@ import CuboVirtual #expect(mx.values[0].sign == .plus && mx.values[1].sign == .minus, "\(mx.values.map(\.sign))") } - /// 5: the fast centre of the planted disk cube is the planted centre; excluded positions are NaN, selected ones + /// The fast centre of the planted disk cube is the planted centre; excluded positions are NaN, selected ones /// equal the unmasked run exactly; remove_NaN fills with the mean of the rest. @Test func testOriginsTruth() async throws { let s3 = (S3.pins["cubes"] as! [[String: Any]]).first { $0["cube"] as! String == "planted_f32" }! @@ -171,7 +171,7 @@ import CuboVirtual #expect(filled.meanRow.isFinite && some.meanRow.isNaN) } - /// 5: point-symmetric patterns give their centres; behind a planted beamstop the masked form keeps the peak at twice + /// Point-symmetric patterns give their centres; behind a planted beamstop the masked form keeps the peak at twice /// the centre; Gaussians at fractional centres within 0.05 px with u = 16. @Test func testFriedelTruth() async throws { let t = S4.pins["friedel_truth"] as! [String: Any] @@ -200,7 +200,7 @@ import CuboVirtual #expect(one.row == masked.row.values[1] && one.column == masked.column.values[1]) } - /// 5: the planted beamstop mask by hand: the arm and the border dilated by 2, the hole filled, the speck removed. + /// The planted beamstop mask by hand: the arm and the border dilated by 2, the hole filled, the speck removed. @Test func testBeamstopTruth() throws { let t = S4.pins["beamstop_truth"] as! [String: Any] let shape = t["shape"] as! [Int] @@ -210,7 +210,7 @@ import CuboVirtual #expect(!got[11 * 24 + 6 + 12] && got[11 * 24 + 6], "the speck is under the mask, the region beside the arm is not") } - /// 5: copies of one disk rolled by known shifts: the aligned average is the disk at p₀; p₀'s hot pixel is absent; + /// Copies of one disk rolled by known shifts: the aligned average is the disk at p₀; p₀'s hot pixel is absent; /// without alignment the mean of p₁ … p_{L−1}; the damping and zeroing as pinned. @Test func testVacuumTruth() async throws { let t = S4.pins["vacuum_truth"] as! [String: Any] @@ -237,7 +237,7 @@ import CuboVirtual print(log.joined(separator: "\n ")) } - /// 5: synthetic probes as py4DSTEM's (1/2 on the radius); the flat kernel sums to 1, the others to 0; the kernels pinned. + /// Synthetic probes as py4DSTEM's (1/2 on the radius); the flat kernel sums to 1, the others to 0; the kernels pinned. @Test func testKernelTruth() throws { let sps = S4.pins["synthetic_probes"] as! [[String: Any]] for sp in sps { diff --git a/Tests/CuboVirtualTests/TruthTests.swift b/Tests/CuboVirtualTests/TruthTests.swift index 133bbc0..8349d08 100644 --- a/Tests/CuboVirtualTests/TruthTests.swift +++ b/Tests/CuboVirtualTests/TruthTests.swift @@ -1,6 +1,6 @@ // // TruthTests.swift -// Planted truth independent of py4DSTEM (registration 5.5) and the refusals of 3.1. +// Planted truth independent of py4DSTEM, and the virtual detectors' refusals. // import Foundation @@ -122,7 +122,7 @@ import CuboVirtual } } - /// Registration 3.1 and the DEVIATION in the refusing direction. + /// The detector refusals, and the DEVIATION in the refusing direction. @Test func testRefusals() { func refuses(_ g: DetectorGeometry, _ want: CuboVirtualError, sourceLocation: SourceLocation = SourceLocation(fileID: #fileID, filePath: #filePath, diff --git a/Tests/Fixtures/README.md b/Tests/Fixtures/README.md index ecb9aa2..e045592 100644 --- a/Tests/Fixtures/README.md +++ b/Tests/Fixtures/README.md @@ -3,11 +3,11 @@ One row per committed fixture file under `Tests/*/Fixtures/`: size, the script under `tools/` that names it (the generator or its pin script; a few scripts write a whole directory; a script found only by file-name prefix, tool-folder name or test module is marked so), and a note where the origin is not the kit's own pinned reference. 152 files. -Licence: every fixture is generated from this kit's own code and the pinned Python reference (named in the README) and ships under the kit's GPL-3.0-or-later licence, except the rows marked *third party*. Nothing here is the owner's data: the committed text fixtures carry no owner path or name (checked by search), and real datasets are read from `$MACEM_DATA` and never copied into the repo. +Licence: every fixture is generated from this kit's own code and the pinned Python reference (named in the README) and ships under the kit's GPL-3.0-or-later licence, except the rows marked *third party*. Nothing here is private data: the committed text fixtures carry no local path or name (checked by search), and real datasets are read from `$CUBOKIT_REAL_DATA` and never copied into the repo. ## Needs an origin line -5 fixtures that no script under `tools/` names. They may be written by a directory-level script, by hand, or copied from an earlier kit; a Mac session with the pin scripts fills in the origin and removes the file from this list. +5 fixtures that no script under `tools/` names. They may be written by a directory-level script, by hand, or copied from an earlier kit; whoever runs the pin scripts and finds the origin fills it in and removes the file from this list. - `CuboParallaxTests/Fixtures/legacy050.json` - `CuboVirtualTests/Fixtures/median_3x5.npy` diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 9c8f442..5df16dc 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -9,8 +9,8 @@ tile to the GPU on every call; the `_resident` rows hold the cube in a `Resident Sizes (scan rows x columns x detector rows x columns): small 32x32x32x32 (1.0 M values), mid 64x64x32x32 (4.2 M), typical 100x100x64x64 (41.9 M, 168 MB). The large size (330x330x64x64, 1.8 GB) and the Bragg, strain, DPC, parallax and ptychography -functions are the nightly review's; this first table is what a Mac can measure while five other sessions use it. -The machine was shared during the run (load average 5 to 11 from other sessions' builds), so the spreads are wider than on a quiet Mac. +functions are not in this table. +The machine was shared during the run (load average 5 to 11 from other builds), so the spreads are wider than on a quiet Mac. Machine: Apple M5 Pro, macOS 27.0.1; Swift 6.4; py4DSTEM 0.14.19, NumPy 2.5.4, Python 3.12.14. Float32 cubes, best of five after one warm-up; cells are best / median (spread) in ms. @@ -95,7 +95,7 @@ Ratios are Python's time over the kit's (above 1 the kit is faster). `dp_mean` s and were no faster (the register variant was 1.5x slower: the strided reads between positions lose to row-contiguous ones). Numbers did not move. The hash of every result's bits is the same before and after for all six cases (`SpeedBench` prints it), so the -results are bit identical to the earlier scalar loops, which are the CPU Float64/Double reference (D16). Against NumPy on the same +results are bit identical to the earlier scalar loops, which are the CPU Float64/Double reference. Against NumPy on the same cube, mask and weights (`bench.py compare`; the largest difference relative to the largest value): | result | kit vs float64 truth | py4DSTEM/NumPy vs float64 truth | kit vs py4DSTEM | @@ -109,7 +109,7 @@ cube, mask and weights (`bench.py compare`; the largest difference relative to t So the kit's means differ from NumPy by NumPy's own Float32 accumulation error, as before, and are closer to the exact value than NumPy is. -## Backend decision per function (D16) +## Backend decision per function The break-even is read off the three sizes above; between two measured sizes it is interpolated, and says so. The Mac was loaded (load average 10 to 11) during the final run, so a crossing near a tie moves with load. diff --git a/docs/bragg.md b/docs/bragg.md index 74d93f0..76970a6 100644 --- a/docs/bragg.md +++ b/docs/bragg.md @@ -96,7 +96,7 @@ edge 10, spacing 12, sigma 2). Wall ms per pattern. | `.single` | **0.47** | **0.140** | 35.3 | 9.3 | | mac4DSTEM's Float path (open-items, 2026-10-20, its own scene) | 2.05 | 0.24 to 0.27 | | about 21 | -The app was not re-run here (its working tree is read-only from this repository), so its row is the figure its lane measured +The app was not re-run here (its working tree is read-only from this repository), so its row is the figure the app measured (4096 positions, 18 workers), not a same-run comparison; `.single` is ahead of it on `.parabolic` by a wide margin on this machine class. With `.multicorr` the time is the matrix-DFT refinement in Double (`CuboParallax`, about 4 ms per peak; a Float matrix DFT there is the next speed-up), not the correlation, which is why the two precisions are close there. @@ -146,8 +146,8 @@ Measured gaps (peak counts and order equal in every case): py4DSTEM, position 1. gained an `Edge` mode (`reflect` is scipy's default and what `find_Bragg_disks` uses; `nearest` stays the default of the origin measurement), pinned to scipy in `GaussianEdgeTests`. -Regenerate from a suite worktree: +Regenerate from the package root, with the reference environment one level up (see the README): ```sh -../../References/python/py4dstem/bin/python -I -B tools/bragg/pins.py +../References/python/py4dstem/bin/python -I -B tools/bragg/pins.py ``` diff --git a/docs/hot-pixels.md b/docs/hot-pixels.md index d544369..a6520c8 100644 --- a/docs/hot-pixels.md +++ b/docs/hot-pixels.md @@ -34,15 +34,14 @@ and adjacent hot pixels, sequential replacement, even clipped medians, wraparoun comparison indices 0/1/8/20, a one-row detector and noninteger Float32 mean sums. A 256-position 1×1 detector cancellation case pins NumPy's pairwise mean 0 where sequential summation gives 0.00390625. Mean samples, masks and every filtered pixel are asserted exact today (tolerance zero; -D12 allows a written tolerance against py4DSTEM for the samples). There is no comparison against a transcribed replacement implementation. +a tolerance against py4DSTEM would be allowed for the samples). There is no comparison against a transcribed replacement implementation. Measured gap: 0 for means, masks and filtered pixels, tested via Float32 bit patterns. Fixture size: 30,403 bytes. Three new Swift Testing tests failed against identity/ zero placeholders (38 issues), then passed. The intermediate sequential-only mean implementation failed specifically on the 1×1 detector mean. The final pairwise path makes that fixture pass without changing its expected value or tolerance. -The inherited Codex reviewer identified that case independently; Sonnet is not -available in this session. No owner approval is claimed. +An independent review identified that case as well. Two further actual-oracle fixtures pin negative-zero input medians on 1×1 and 2×2 frames with a negative threshold so all pixels are selected. NumPy takes a diff --git a/docs/idpc.md b/docs/idpc.md index 1714e89..e2e0788 100644 --- a/docs/idpc.md +++ b/docs/idpc.md @@ -30,12 +30,11 @@ Fixtures cover 7×10 periodic with anisotropic sampling, 8×8 at 2× padding and regularization 1e-4, and 7×10 at 3× padding with calibrated gradient scale, anisotropic sampling and regularization 0.03. The odd rectangular padded grid checks centering, frequency ordering and crop/gauge. All input and output samples -are committed; no owner data is included. Double precision throughout, absolute +are committed; no private data is included. Double precision throughout, absolute phase tolerance 1e-12 radians. No bit-exact claim. No microscope validation. Measured maximum absolute gaps: periodic 7.772e-16 rad, 2× padded 2.221e-16 rad, 3× physically calibrated padded 3.331e-16 rad. Fixture size 16,353 bytes. Two new Swift Testing tests cover all pins, periodic planted phase and invalid calibration/input/overflow cases. Both fail with the zero-output placeholder -(10 issues), then pass after implementation. Independent pre-commit review uses -the available Codex model; Sonnet is not exposed in this session. +(10 issues), then pass after implementation. diff --git a/docs/parallax-alignment.md b/docs/parallax-alignment.md index a315663..682ce0a 100644 --- a/docs/parallax-alignment.md +++ b/docs/parallax-alignment.md @@ -45,7 +45,7 @@ Python 3.12.4, NumPy 2.5.0, SciPy 1.18.0, py4DSTEM 0.14.19. Regenerate from the Four small three-frame stacks cover integer/wrapped shifts (8 × 10), fractional shifts on odd axes (9 × 11), noisy fractional shifts with a running reference (10 × 12), and a public DM4 diffraction crop (12 × 14). The latter is `small_datacube.dm4[0,0,250:262,249:263]`; its source SHA-256 is in the fixture. -Only that public test-data crop is committed; no owner's data is used. Seed: 20261016. +Only that public test-data crop is committed; no private data is used. Seed: 20261016. Additional shift pins cover factors 3, 7, 16 and 64 for the first frame of each stack. Shifts, shifted pixels, aligned means and matrix-DFT patches are compared against direct Python outputs @@ -136,7 +136,7 @@ lose their Nyquist component in the first fractional shift (the effect noted abo `endToEndEqualsChainedSteps` checks that `process` equals the chained single steps exactly (stack, shifts, image, errors, limits, upsampled image and probe offsets, with and without position correction and without the subpixel stage). -### The application's asks (round 6, for 0.6.0) +### The application's asks (for 0.6.0) mac4DSTEM's switch to CuboParallax 0.4.0 left six asks; each is an addition whose defaults keep 0.5.0 behaviour. `DefaultUnchangedTests` holds the 0.5.0 outputs (fixture `legacy050.json`, written by a throwaway generator run on diff --git a/docs/parity.md b/docs/parity.md index a9805f2..96ea764 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -1,7 +1,7 @@ # Parity with py4DSTEM A checklist against py4DSTEM's documented API in the areas CuboKit holds or plans, at the pinned commit `f050d207` -(0.14.19), so "complete" is a measured claim (suite ROADMAP, "Kits", phase 4). Updated at every tag. The areas are the +(0.14.19), so "complete" is a measured claim. Updated at every tag. The areas are the `DataCube` (`datacube/`), the calibration of origins and probes (`process/calibration/`, `data/calibration.py`), the `Probe` class (`braggvectors/probe.py`), DPC and the phase base class's preprocessing (`process/phase/`), and the headline lists for strain, Bragg vectors and disk detection, parallax and ptychography. Crystallography (`Crystal`, orientation, @@ -37,10 +37,10 @@ States: **present** (pinned against py4DSTEM or planted truth; numerics held to | `get_virtual_image(mode, geometry)`: `point`, `circle`, `annulus`, `rectangle` | `VirtualImaging.image(of:geometry:)` | present (CPU and Metal) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | | `get_virtual_image(mode="mask", geometry=array)` | `VirtualImaging.image(of:geometry:)` with a mask geometry | present (CPU and Metal) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | | `get_virtual_image(return_mask=True)`, `make_detector` | `DetectorMask.make` | present | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | -| `get_virtual_image(centered=True)` | `VirtualImaging.image(of:geometry:centeredOn:)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | -| `get_virtual_image(calibrated=True)` | `VirtualImaging.image(of:geometry:pixelSize:)` | present (slice 3; centred and calibrated together refused) | study | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | -| `get_virtual_image(shift_center=True, subpixel=False)` | `shift: .rounded` | present (slice 3; CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | -| `get_virtual_image(shift_center=True, subpixel=True)` | `shift: .bilinear` | present (slice 3; CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | +| `get_virtual_image(centered=True)` | `VirtualImaging.image(of:geometry:centeredOn:)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | +| `get_virtual_image(calibrated=True)` | `VirtualImaging.image(of:geometry:pixelSize:)` | present (centred and calibrated together refused) | study | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | +| `get_virtual_image(shift_center=True, subpixel=False)` | `shift: .rounded` | present (CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | +| `get_virtual_image(shift_center=True, subpixel=True)` | `shift: .bilinear` | present (CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | | `get_calibrated_detector_geometry` | applied inside `VirtualImaging.image` | partial: not exposed as a function | study | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | | `make_bragg_mask` | `BraggMasking.latticeMask(...)` | present (0.8.0; exact counts; `return_sum=False` is the loop over the lattice points) | study | no upstream test; np.allclose defaults on the Float64 reference; Metal against CPU 1e-5 | | `get_virtual_image(dask=True)`, `test_config` | — | not planned (Python-only; the lazy cube tiles instead) | not planned: Python-only; the lazy cube tiles instead | — | @@ -52,14 +52,14 @@ States: **present** (pinned against py4DSTEM or planted truth; numerics held to |---|---|---|---|---| | `get_dp_mean` | `VirtualDiffraction.mean` | present | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | | `get_dp_max` | `VirtualDiffraction.max` | present | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | -| `get_dp_median` | `VirtualDiffraction.median(of:)` | present (slice 4; CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | +| `get_dp_median` | `VirtualDiffraction.median(of:)` | present (CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | | `get_virtual_diffraction(method="mean")`, `"max"` | `VirtualDiffraction.mean`, `max`, `meanAndMax` | present | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | -| `get_virtual_diffraction(method="median")` | `VirtualDiffraction.pattern(of:statistic: .median, scan:)` | present (slice 4; CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | +| `get_virtual_diffraction(method="median")` | `VirtualDiffraction.pattern(of:statistic: .median, scan:)` | present (CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | | `get_virtual_diffraction(mask=)`, boolean or float real-space mask | `VirtualDiffraction.pattern(of:statistic:scan: .mask / .weights, path:)` | present (CPU compensated Double; Metal `cubo_weighted_pattern` for mean and max, 0.8.0, held to the CPU at 1e-5; `ResidentCube` reads in place) | study | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | | `get_virtual_diffraction(mask=)`, complex mask | — | missing (not ported) | not planned: absent (not ported) | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | -| `get_virtual_diffraction(shift_center=True, subpixel=False)` (mean, max) | `VirtualDiffraction.meanAndMax(of:shift: .rounded)` | present (slice 3; CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | -| `get_virtual_diffraction(shift_center=True, subpixel=True)` (mean, max) | `shift: .fourier` | present (slice 3; CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | -| `get_virtual_diffraction(shift_center=True, mask=)` (mean, max) | `pattern(of:statistic:scan:shift:)` | present (slice 4; CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | +| `get_virtual_diffraction(shift_center=True, subpixel=False)` (mean, max) | `VirtualDiffraction.meanAndMax(of:shift: .rounded)` | present (CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | +| `get_virtual_diffraction(shift_center=True, subpixel=True)` (mean, max) | `shift: .fourier` | present (CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | +| `get_virtual_diffraction(shift_center=True, mask=)` (mean, max) | `pattern(of:statistic:scan:shift:)` | present (CPU) | useful | no upstream test; np.allclose defaults; sums Float64-compensated, NumPy Float32 gap < 1e-6 relative; Metal against CPU 1e-5 | ## `DataCube`: calibration-bound properties and coordinates (datacube/datacube.py) @@ -86,19 +86,19 @@ States: **present** (pinned against py4DSTEM or planted truth; numerics held to | `thin_R` | `CubeEditing.thinScan` | present (0.8.0; exact) | study | index moves exact; sums 1e-6 relative of max (NumPy Float32); resample 1e-6 (scipy Float32); no upstream test | | `pad_Q` | `CubeEditing.padDetector` | present (0.8.0; exact; factor and output size) | study | index moves exact; sums 1e-6 relative of max (NumPy Float32); resample 1e-6 (scipy Float32); no upstream test | | `resample_Q` (bilinear, `conserve_array_sums`, `method="fourier"`) | `CubeEditing.resampleDetector`, `CubeEditing.resampleDetectorFourier` | present (0.9.0; method="fourier": scalar scale or output size, the Q pixel size per `resample_data_diffraction`; the kit returns Float32 where py4DSTEM returns float64 unless the sums are conserved) | study | bilinear unchanged; fourier 1e-6 of the largest value (py4DSTEM is complex64); measured 2.1e-7 | -| `filter_hot_pixels` (`thresh`, `ind_compare`, `return_mask`) | `HotPixelFilter.filter`, `.mask`, `.replacing` | present: eager Float32 cube, means/masks/pixels pinned exact today (D12); wrapped classification and clipped sequential replacement | useful | index moves exact; sums 1e-6 relative of max (NumPy Float32); resample 1e-6 (scipy Float32); no upstream test | +| `filter_hot_pixels` (`thresh`, `ind_compare`, `return_mask`) | `HotPixelFilter.filter`, `.mask`, `.replacing` | present: eager Float32 cube, means/masks/pixels pinned exact today; wrapped classification and clipped sequential replacement | useful | index moves exact; sums 1e-6 relative of max (NumPy Float32); resample 1e-6 (scipy Float32); no upstream test | | `median_filter_masked_pixels` | `CubeEditing.medianFilterMaskedPixels` | present (0.8.0; exact for odd kernels, in place and row-major as upstream) | study | index moves exact; sums 1e-6 relative of max (NumPy Float32); resample 1e-6 (scipy Float32); no upstream test | ## `DataCube`: probe, beam stop and background methods (datacube/datacube.py) | py4DSTEM | CuboKit | State | Mark | Tolerance (source) | |---|---|---|---|---| -| `get_probe_size` (`thresh_lower`, `thresh_upper`, `N`) | `ProbeSize.estimate` | present (slice 2; exact today, D12) | useful | no upstream test; np.allclose defaults | -| `get_vacuum_probe(ROI=boolean array)` | `VacuumProbe.measure(of:scanMask:)` | present (slice 4) | useful | no upstream test; np.allclose defaults | +| `get_probe_size` (`thresh_lower`, `thresh_upper`, `N`) | `ProbeSize.estimate` | present (exact today) | useful | no upstream test; np.allclose defaults | +| `get_vacuum_probe(ROI=boolean array)` | `VacuumProbe.measure(of:scanMask:)` | present | useful | no upstream test; np.allclose defaults | | `get_vacuum_probe(ROI=4-tuple)` | — | missing (not ported: pass the equivalent boolean ROI) | not planned: absent (not ported: pass the equivalent boolean ROI) | no upstream test; np.allclose defaults | -| `get_vacuum_probe(align, zero_vacuum, mask)` | `VacuumProbe.measure(of:scanMask:align:zeroVacuum:detectorMask:)` | present (slice 4) | useful | no upstream test; np.allclose defaults | -| `get_beamstop_mask` (`threshold`, `distance_edge`, `include_edges`, `sigma`, `scale_radial`) | `BeamstopMask.make(from:threshold:distanceEdge:includeEdges:sigma:scaleRadial:)` | present (slice 4) | useful | no upstream test; np.allclose defaults | -| `get_beamstop_mask(use_max_dp=True)` | the caller passes the max pattern | present (slice 4; via the caller's pattern) | useful | no upstream test; np.allclose defaults | +| `get_vacuum_probe(align, zero_vacuum, mask)` | `VacuumProbe.measure(of:scanMask:align:zeroVacuum:detectorMask:)` | present | useful | no upstream test; np.allclose defaults | +| `get_beamstop_mask` (`threshold`, `distance_edge`, `include_edges`, `sigma`, `scale_radial`) | `BeamstopMask.make(from:threshold:distanceEdge:includeEdges:sigma:scaleRadial:)` | present | useful | no upstream test; np.allclose defaults | +| `get_beamstop_mask(use_max_dp=True)` | the caller passes the max pattern | present (via the caller's pattern) | useful | no upstream test; np.allclose defaults | | `get_radial_bkgrnd` | `RadialBackground.background` | present (0.9.0; the lookup radius is on the major-axis scale while the polar bins are on the minor-axis scale: py4DSTEM's own, so a ring is stretched by a/b for a != b; a circle is exact) | study | no upstream test; np.allclose defaults; measured 4e-15 | | `get_radial_bksb_dp` | `RadialBackground.subtracted` | present (0.9.0) | study | no upstream test; np.allclose defaults; measured 2e-13 | | `get_local_ave_dp` (`radial_bksb`, `sigma`, `braggmask`) | `BraggMasking.localAverage`, `RadialBackground.localAverage` | present (0.9.0): `RadialBackground.localAverage` is `radial_bksb=True`, and with `keeping:` (a per-position mask, e.g. from `BraggMasking.mask`) the `radial_bksb` + `braggmask` branch (hand-composed, not pinned); plain and Bragg-masked without radial background as before | study | no upstream test; np.allclose defaults; measured 4e-13 | @@ -139,22 +139,22 @@ CuboKit has no stateful `Calibration` object yet; its measurements return plain | py4DSTEM | CuboKit | State | Mark | Tolerance (source) | |---|---|---|---|---| | `get_origin_single_dp` | the per-pattern step of `OriginMeasurement.measure` | partial: no single-pattern entry point | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin(r, rscale)` | `OriginMeasurement.measure(radius:radiusScale:)` | present (slice 3; CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin(r, rscale)` | `OriginMeasurement.measure(radius:radiusScale:)` | present (CPU) | useful | no upstream test; np.allclose defaults on the Float64 reference | | `get_origin(dp_max=)` | the max pattern is computed internally | partial: no argument to pass it | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin(mask=)` | `OriginMeasurement.measure(of:scanMask:)` | present (slice 4; NaN outside the mask) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin(fast_center=True)` | `OriginMeasurement.measure(of:scanMask:fastCenter:)` | present (slice 4) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin(remove_NaN=True)` | `OriginMap.replacingNaNWithMean()` | present (slice 4) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin_friedel(mask, upsample_factor)` | `FriedelOrigin.measure(of:mask:upsampleFactor:)`, `.origin(of:)` | present (slice 4; CPU only) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_origin_friedel(device="gpu")` | — | missing (planned: a Metal path, later lane) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `fit_origin(fitfunction="constant")` | `OriginFit.fit(model: .constant)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `fit_origin(fitfunction="plane")` | `OriginFit.fit(model: .plane)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `fit_origin(fitfunction="parabola")` | `OriginFit.fit(model: .parabola)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `fit_origin(fitfunction="bezier_two")` | `OriginFit.fit(model: .bezierTwo)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `fit_origin(robust, robust_steps, robust_thresh)` | `OriginFit.fit(robust:robustSteps:robustThreshold:)` | present (slice 3) | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin(mask=)` | `OriginMeasurement.measure(of:scanMask:)` | present (NaN outside the mask) | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin(fast_center=True)` | `OriginMeasurement.measure(of:scanMask:fastCenter:)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin(remove_NaN=True)` | `OriginMap.replacingNaNWithMean()` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin_friedel(mask, upsample_factor)` | `FriedelOrigin.measure(of:mask:upsampleFactor:)`, `.origin(of:)` | present (CPU only) | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `get_origin_friedel(device="gpu")` | — | missing (planned: a Metal path, later) | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `fit_origin(fitfunction="constant")` | `OriginFit.fit(model: .constant)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `fit_origin(fitfunction="plane")` | `OriginFit.fit(model: .plane)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `fit_origin(fitfunction="parabola")` | `OriginFit.fit(model: .parabola)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `fit_origin(fitfunction="bezier_two")` | `OriginFit.fit(model: .bezierTwo)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | +| `fit_origin(robust, robust_steps, robust_thresh)` | `OriginFit.fit(robust:robustSteps:robustThreshold:)` | present | useful | no upstream test; np.allclose defaults on the Float64 reference | | `fit_origin(mask=)` | `OriginFit.fit` | partial: NaN entries are left out of the fit; no boolean mask argument | useful | no upstream test; np.allclose defaults on the Float64 reference | | mac4DSTEM trimmed fit (`fitOriginTrimmed`, MAD cut, up to three rounds) | `OriginFit.fitTrimmed` | named option beside `fit(robust:)`; mask and surfaces pinned to a Float32 transcription of the app source, the final fit also to `fit_origin(mask=kept)` (not counted twice) | useful | no upstream test; np.allclose defaults on the Float64 reference | | `fit_origin(returnfitp=True)` | — | missing (the fit coefficients are not returned) | useful | no upstream test; np.allclose defaults on the Float64 reference | -| `get_probe_size` (`process/calibration/probe.py`) | `ProbeSize.estimate` | present (slice 2; exact today, D12) | study | no upstream test; np.allclose defaults on the Float64 reference | +| `get_probe_size` (`process/calibration/probe.py`) | `ProbeSize.estimate` | present (exact today) | study | no upstream test; np.allclose defaults on the Float64 reference | | `fit_ellipse_1D` | `EllipseFit.fit1D` | present (0.9.0; default `fitRadii` repaired, upstream raises `TypeError`; `ignoring:` is upstream's `mask` with the same polarity) | useful | no upstream test; np.allclose defaults on the Float64 reference; the fit is compared at 1e-6 (the optimiser's own result moves by 1e-7 to 7e-4 for one ulp of start): measured 7e-10 relative | | `fit_ellipse_amorphous_ring` | `EllipseFit.fitAmorphousRing` | present (0.9.0; `keeping:` is upstream's `mask`, `true` = use, the opposite of `fit_ellipse_1D`; `initial:` is `p0`) | useful | as `fit_ellipse_1D`; measured 3e-8 relative, cost 4e-9 | | `ellipse_err`, `double_sided_gaussian`, `double_sided_gaussian_fiterr` | `EllipseFit.ellipseError`, `.doubleSidedGaussian`, `.doubleSidedGaussianFitError` | present (0.9.0) | study | np.allclose defaults on the Float64 reference; measured below 1e-7 | @@ -168,48 +168,48 @@ CuboKit has no stateful `Calibration` object yet; its measurements return plain | py4DSTEM | CuboKit | State | Mark | Tolerance (source) | |---|---|---|---|---| -| `Probe(data)` with `probe`, `kernel`, `alpha`, `origin` | functions on a detector image | partial (slice 4): no stateful `Probe` object | study | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe(data)` with `probe`, `kernel`, `alpha`, `origin` | functions on a detector image | partial: no stateful `Probe` object | study | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | | `Probe.from_vacuum_data` (`mask`, `threshold`, `expansion`, `opening`) | — | missing (planned: the Bragg-disk module) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.generate_synthetic_probe(radius, width, Qshape)` | `SyntheticProbe.make(radius:width:detectorRows:detectorColumns:)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.measure_disk` (`thresh_lower`, `thresh_upper`, `N`, `zero_vacuum`, `alpha_max`) | `ProbeSize.estimate` (the same algorithm), then `VacuumProbe.zeroVacuum` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.generate_synthetic_probe(radius, width, Qshape)` | `SyntheticProbe.make(radius:width:detectorRows:detectorColumns:)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.measure_disk` (`thresh_lower`, `thresh_upper`, `N`, `zero_vacuum`, `alpha_max`) | `ProbeSize.estimate` (the same algorithm), then `VacuumProbe.zeroVacuum` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | | `Probe.measure_disk(plot=True)` | — | not planned (plotting) | not planned: plotting | — | -| `Probe.zero_vacuum` | `VacuumProbe.zeroVacuum(_:center:alpha:alphaMax:)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.zero_vacuum_sinusoid` (`s`, `w`, `p`) | `VacuumProbe.dampVacuum(_:center:alpha:start:width:power:)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_kernel(mode="flat")` | `ProbeKernel.flat` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_kernel(mode="gaussian", sigma)` | `ProbeKernel.gaussian` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_kernel(mode="sigmoid", radii)` | `ProbeKernel.sigmoid(profile: .sineSquared)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_kernel(mode="sigmoid_log", radii)` | `ProbeKernel.sigmoid(profile: .sineSquared)` (py4DSTEM dispatches this mode to the sine² trench; `.logistic` is `type="logistic"`) | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_probe_kernel_flat(origin, bilinear)` | `ProbeKernel.flat(_:origin:bilinear:)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_probe_kernel_edge_gaussian(sigma, origin, bilinear)` | `ProbeKernel.gaussian(_:sigma:origin:bilinear:)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_probe_kernel_edge_sigmoid(type="sine_squared")` | `ProbeKernel.sigmoid(_:innerRadius:outerRadius:profile: .sineSquared)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | -| `Probe.get_probe_kernel_edge_sigmoid(type="logistic")` | `ProbeKernel.sigmoid(..., profile: .logistic)` | present (slice 4) | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.zero_vacuum` | `VacuumProbe.zeroVacuum(_:center:alpha:alphaMax:)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.zero_vacuum_sinusoid` (`s`, `w`, `p`) | `VacuumProbe.dampVacuum(_:center:alpha:start:width:power:)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_kernel(mode="flat")` | `ProbeKernel.flat` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_kernel(mode="gaussian", sigma)` | `ProbeKernel.gaussian` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_kernel(mode="sigmoid", radii)` | `ProbeKernel.sigmoid(profile: .sineSquared)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_kernel(mode="sigmoid_log", radii)` | `ProbeKernel.sigmoid(profile: .sineSquared)` (py4DSTEM dispatches this mode to the sine² trench; `.logistic` is `type="logistic"`) | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_probe_kernel_flat(origin, bilinear)` | `ProbeKernel.flat(_:origin:bilinear:)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_probe_kernel_edge_gaussian(sigma, origin, bilinear)` | `ProbeKernel.gaussian(_:sigma:origin:bilinear:)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_probe_kernel_edge_sigmoid(type="sine_squared")` | `ProbeKernel.sigmoid(_:innerRadius:outerRadius:profile: .sineSquared)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | +| `Probe.get_probe_kernel_edge_sigmoid(type="logistic")` | `ProbeKernel.sigmoid(..., profile: .logistic)` | present | useful | no upstream test (`test_probe.py` checks types and shapes); np.allclose defaults | ## DPC and the phase base class's preprocessing (process/phase/dpc.py, phase_base_class.py) | py4DSTEM | CuboKit | State | Mark | Tolerance (source) | |---|---|---|---|---| | `DPC(datacube, energy, ...)` | `CenterOfMass`, `CenterOfMassReference`, `RotationSolver`, `DPC` | partial: functions, no stateful reconstruction object | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `_calculate_intensities_center_of_mass` | `CenterOfMass.measure` | present (slice 2; CPU and Metal) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `_calculate_intensities_center_of_mass` | `CenterOfMass.measure` | present (CPU and Metal) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `_extract_intensities_and_calibrations_from_datacube` (reciprocal sampling, mask) | `CenterOfMass.measure(mask:)`, `reciprocalSampling:` | partial: the calibration is passed in, not read from a cube | study | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(dp_mask)` | `CenterOfMass.measure(mask:)` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(fit_function="plane")`, `"constant"` | `CenterOfMassReference.planeFit`, `.constant` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(dp_mask)` | `CenterOfMass.measure(mask:)` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(fit_function="plane")`, `"constant"` | `CenterOfMassReference.planeFit`, `.constant` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `DPC.preprocess(fit_function="parabola")`, `"bezier_two"` | `OriginFit` has them | partial: not wired into `CenterOfMassReference` | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(force_com_shifts)` | `CenterOfMassReference.given` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(force_com_measured)` | the measured `ScanVectorField` is an input | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(rotation_angles_deg)` | `RotationSolver.solve(anglesDegrees:)` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(maximize_divergence=False)`: curl | `RotationCriterion.minimizeCurl` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(maximize_divergence=True)`: divergence | `RotationCriterion.maximizeDivergence` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(force_com_rotation, force_com_transpose)` | `forceRotationDegrees:`, `forceTranspose:` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.preprocess(padding_factor)` | `DPC.reconstruct(paddingFactor:)` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| the rotation search curves (`plot_rotation` data) | `RotationSolution.curve`, `curveTransposed` | present (slice 2; the plot itself n/a) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(force_com_shifts)` | `CenterOfMassReference.given` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(force_com_measured)` | the measured `ScanVectorField` is an input | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(rotation_angles_deg)` | `RotationSolver.solve(anglesDegrees:)` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(maximize_divergence=False)`: curl | `RotationCriterion.minimizeCurl` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(maximize_divergence=True)`: divergence | `RotationCriterion.maximizeDivergence` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(force_com_rotation, force_com_transpose)` | `forceRotationDegrees:`, `forceTranspose:` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.preprocess(padding_factor)` | `DPC.reconstruct(paddingFactor:)` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| the rotation search curves (`plot_rotation` data) | `RotationSolution.curve`, `curveTransposed` | present (the plot itself n/a) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `DPC.preprocess(vectorized_com_calculation, plot_*, device, clear_fft_cache)` | — | not planned (Python memory strategy, plotting, CuPy) | not planned: Python memory strategy, plotting, CuPy | — | -| `DPC.reconstruct(max_iter, step_size, stopping_criterion, backtrack)` | `DPC.reconstruct(maxIterations:stepSize:stoppingCriterion:backtrack:)` | present (slice 2; CPU, Double) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.reconstruct(gaussian_filter_sigma, gaussian_filter)` | — | missing (planned: DPC filters, later lane) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.reconstruct(q_lowpass, q_highpass, butterworth_order, butterworth_filter)` | — | missing (planned: DPC filters, later lane) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.reconstruct(max_iter, step_size, stopping_criterion, backtrack)` | `DPC.reconstruct(maxIterations:stepSize:stoppingCriterion:backtrack:)` | present (CPU, Double) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.reconstruct(gaussian_filter_sigma, gaussian_filter)` | — | missing (planned: DPC filters, later) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.reconstruct(q_lowpass, q_highpass, butterworth_order, butterworth_filter)` | — | missing (planned: DPC filters, later) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `DPC.reconstruct(store_iterations=True)` | `DPCReconstruction.errors`, `accepted` | partial: the error history is kept; the per-iteration objects are not | useful | none upstream (`test/` holds no phase test); np.allclose defaults | -| `DPC.reconstruct(device="gpu")` | — | missing (planned: a Metal DPC, later lane) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| `DPC.reconstruct(device="gpu")` | — | missing (planned: a Metal DPC, later) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `DPC.reconstruct(reset, progress_bar)` | — | not planned (Python state and progress bar) | not planned: Python state and progress bar | — | -| result: `object_phase`, `error`, `sampling`, rotation and transpose | `DPCReconstruction.phase`, `errors`, `RotationSolution` | present (slice 2) | useful | none upstream (`test/` holds no phase test); np.allclose defaults | +| result: `object_phase`, `error`, `sampling`, rotation and transpose | `DPCReconstruction.phase`, `errors`, `RotationSolution` | present | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `DPC.visualize`, `show_complex_CoM`, `to_h5` | — | not planned (plotting; FormatoKit) | not planned: plotting; FormatoKit | — | | complex CoM magnitude and angle (`show_complex_CoM` data) | `DPCViews.magnitude`, `.angle` | not counted (derived views): py4DSTEM's `row + i column` direction or the image-frame `atan2(row, column)`, wrapped to [0, 2 pi); NumPy-pinned (`tools/dpcviews/pins.py`), `RotationConvention` states the sign flip between conventions | useful | none upstream (`test/` holds no phase test); np.allclose defaults | | `set_device`, `set_storage`, `clear_device_mem`, `attach_datacube`, `reinitialize_parameters`, `set_save_defaults` | — | not planned (Python object plumbing) | not planned: Python object plumbing | — | @@ -244,7 +244,7 @@ CuboKit has no stateful `Calibration` object yet; its measurements return plain | `find_Bragg_disks(radial_bksb)` | — | missing (planned: with `get_radial_bksb_dp`) | useful | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | | `find_Bragg_disks(subpixel="none"/"poly"/"multicorr", upsample_factor)` | `BraggDiskOptions.subpixel` (`.pixel`, `.parabolic`, `.multicorr`), `upsampleFactor` | present: pinned, factors 3, 8, 16; a factor of 2 or less for multicorr is refused (py4DSTEM asserts) | useful | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | | `find_Bragg_disks(minAbsoluteIntensity, minRelativeIntensity, relativeToPeak, minPeakSpacing, edgeBoundary, maxNumPeaks)` | the same six options | present: pinned; `BraggMaxima.find` is `get_maxima_2D` | useful | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | -| `find_Bragg_disks(CUDA, CUDA_batched)` | — | missing (planned: a Metal path, later lane) | useful | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | +| `find_Bragg_disks(CUDA, CUDA_batched)` | — | missing (planned: a Metal path, later) | useful | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | | `find_Bragg_disks(distributed, ML, ml_*)`, the `diskdetection_parallel`, `_aiml`, `_cuda` variants | — | not planned (Python parallel tooling, TensorFlow) | not planned: Python parallel tooling, TensorFlow | — | | `BraggVectors` (`raw`, `cal`, `setcal`, `calibrate`, `get_vectors`, `shape`, `qx`, `qy`, `I`) | `BraggVectors` (peaks per scan position) | partial: the raw value container only; no calibration state | study | `test_braggvectors.py`, `test_workflow/test_disk_detection_*.py` (counts and shapes); positions np.allclose defaults | | `BraggVectors.get_virtual_image` | `BraggVectors.virtualImage(detector:)` | present (0.9.0): `.allPeaks`, `.circle`, `.annulus` over vectors already in detector coordinates; the four calibration flags are the caller's | useful | `test_braggvectors.py` (counts and shapes); np.allclose defaults, measured 0 | @@ -328,7 +328,7 @@ Source: an AST walk of the public functions and methods of `datacube`, `preproce | `process.phase.utils.fourier_ring_correlation`, `return_1D_profile`, `fourier_rotate_real_volume`, `fft_shift`, `fourier_translation_operator`, `spatial_frequencies`, `generate_batches`, `subdivide_into_batches`, `array_slice` | `FFT2D`, `ParallaxFourierShifter` cover `fft_shift`; the rest missing | partial | useful | none upstream | | `process.phase.utils` periodic gradients, divergence and the 3-D Poisson/Nesterov helpers (`compute_gradient_periodic`, `compute_divergence_periodic`, `preconditioned_poisson_solver_periodic_3D`, `nesterov_*`) | `IDPC` (2-D Poisson only) | missing | not planned: 3-D tomography is outside the kit until multislice ptychography is registered | — | | `process.phase.parameter_optimize.PtychographyOptimizer` (`grid_search`, `optimize`) | — | missing | useful | none upstream | -| `braggvectors.find_Bragg_disks_aiml*`, `_CUDA`, `_ipp`, `_dask`, `beta_parallel_disk_detection` | the learned detector stays in the app (D13, ADR 071); `BraggDisks.detectAll` tiles in process | not planned | not planned: ML, CUDA and cluster backends | — | +| `braggvectors.find_Bragg_disks_aiml*`, `_CUDA`, `_ipp`, `_dask`, `beta_parallel_disk_detection` | the learned detector stays in the app; `BraggDisks.detectAll` tiles in process | not planned | not planned: ML, CUDA and cluster backends | — | | `BraggVectors.set_raw_vectors`, `BraggVectorMap.pixelsize/pixelunits` | `BraggScanResult`, no map object | partial | useful | exact | | `BraggVectorClassification.__init__` (`braggpeaks`, `Qx`, `Qy`, `X_is_boolean`, `max_dist`), `get_braggpeak_labels_by_scan_position` | `BraggVectorClassification.init(vectors:referenceRows:referenceColumns:booleanData:maximumDistance:)`, `.peakLabels(...)` | present (0.9.0): labels and X exact (rows are `qx`) | useful | none upstream; labels exact, X exact | | `get_initial_classes`, `get_initial_classes_by_cooccurrence`, `get_initial_classes_from_images` | `.initialClasses(...)`, `.setInitialClassesByCooccurrence(...)`, `.setInitialClasses(images:)` | present (0.9.0): class sets exact, W exact, H by minimum-norm least squares (Jacobi SVD against `lstsq`) | useful | none upstream; np.allclose defaults, measured 3.5e-8 of the bound | @@ -374,7 +374,7 @@ Study rows: 53, of which 47 present, 5 partial, 1 missing. - `RotationSolver` gains `curve(_:anglesDegrees:criterion:transpose:isCancelled:)` (py4DSTEM computes the same curve inside `DPC.preprocess`) and runs the grid in parallel; the CPU centre of mass runs positions in parallel; both bit identical. -- The Metal centre of mass agrees with the CPU path where |S| ≥ 1e-3 · Σ|wP| (slice 2): Float32's absolute noise floor on +- The Metal centre of mass agrees with the CPU path where |S| ≥ 1e-3 · Σ|wP|: Float32's absolute noise floor on a Metal reduction is about 3e-10 of the total intensity, so a pattern whose weighted sum is far below its total is outside the claim. py4DSTEM has no GPU centre of mass to compare with. - The counts above are unchanged by 0.2.0 (no new py4DSTEM method). @@ -396,7 +396,7 @@ iterative `DPC`. Absolute phase tolerance `1e-12` rad; details in `docs/idpc.md` This refines the preprocessing row above (not counted twice). Generator `tools/hot-pixels/pins.py` calls the actual pinned method. Eight fixtures (30,403 bytes) -pin means, masks and every cleaned sample exactly today (measured gap 0; D12 allows a written tolerance). +pin means, masks and every cleaned sample exactly today (measured gap 0; a tolerance against py4DSTEM would be allowed). Supported input and numerical details: `docs/hot-pixels.md`. ## CPU ptychography details (refines the headline above; not counted twice) diff --git a/docs/ptycho.md b/docs/ptycho.md index 494db0c..8cd91b3 100644 --- a/docs/ptycho.md +++ b/docs/ptycho.md @@ -45,8 +45,7 @@ The fixture therefore starts with a fixed weak random phase (documented in the generator). No numerical correction to py4DSTEM's operator is implemented. The red baseline returned a zero probe/unchanged object; both regression tests failed. -Independent review used the available inherited Codex model; Sonnet is not available -in this session. Review requested the extra constraint and rectangular pins, now added. +An independent review requested the extra constraint and rectangular pins, now added. ## Probe constraints (centre of mass, amplitude) diff --git a/docs/strain.md b/docs/strain.md index 781d8b8..0931220 100644 --- a/docs/strain.md +++ b/docs/strain.md @@ -43,7 +43,7 @@ The three compatibility options are independent: observation support. Pair ties prefer lower residual, then shorter vectors. Minimum noncentral radius defaults to 2. Empty/no-consensus inputs return nil. -Result-returning variants (round 6, from the app's strain switch; the plain calls are unchanged and now delegate to them): +Result-returning variants (0.6.0, from the app's strain switch; the plain calls are unchanged and now delegate to them): - `chooseBasisConsensusResult(_:origin:minimumRadius:isCancelled:)` returns `LatticeStrain.ConsensusBasis`: the `basis` (identical to `chooseBasisConsensus`), `observationCount` and `supportCount` (exact integers), and `residual` (RMS @@ -63,10 +63,10 @@ printing is removed. The generator independently compiles the **original** helpe text from that commit, removing only access/isolation qualifiers and supplying minimal value shims. It never imports or executes CuboStrain to generate pins. -Regenerate from a suite worktree: +Regenerate from the package root, with the reference environment one level up (see the README): ```sh -../../References/python/py4dstem/bin/python -I -B tools/strain/pins.py +../References/python/py4dstem/bin/python -I -B tools/strain/pins.py ``` The generator checks Python 3.12.4, NumPy 2.5.0, py4DSTEM 0.14.19, the reference diff --git a/tools/benchmarks/bench.py b/tools/benchmarks/bench.py index 5d507bc..0a3491e 100644 --- a/tools/benchmarks/bench.py +++ b/tools/benchmarks/bench.py @@ -1,5 +1,5 @@ """py4DSTEM side of the CuboKit benchmark, and the merge into docs/benchmarks.md. -Run from the suite folder (one benchmark at a time machine-wide, under a memory cap): +Run from the folder that holds this package and References/python/py4dstem (the pinned venv; see the README); one benchmark at a time: References/python/py4dstem/bin/python -I -B CuboKit/tools/benchmarks/bench.py make # writes /{small,mid,typical}.npy References/python/py4dstem/bin/python -I -B CuboKit/tools/benchmarks/bench.py python # writes /python.json CUBOKIT_BENCH_DIR= swift test -c release --filter BenchmarkHarness # writes /swift.json diff --git a/tools/bragg/pins.py b/tools/bragg/pins.py index 3fed909..f0a5896 100644 --- a/tools/bragg/pins.py +++ b/tools/bragg/pins.py @@ -1,4 +1,4 @@ -"""Generate CuboBragg pins. Run under a memory cap with the pinned venv, from the package root: +"""Generate CuboBragg pins. Run with the pinned venv (see the README), from the package root: ../../References/python/py4dstem/bin/python -I -B tools/bragg/pins.py Two independent oracles, neither is Swift written for the kit: @@ -6,7 +6,7 @@ 2. The mac4DSTEM source at APP (Core/Analysis/DiskDetection.swift, Core/Compute/MatrixDFTCorrelation.swift, Core/Analysis/ProbeRingHint.swift, with ImagoKit 0.1.1's Float32 FFT2D) is compiled UNCHANGED apart from access qualifiers and four minimal shims (ProbeKernel's fields, a probe-size stub fed from py4DSTEM) and run. -Scenes are synthetic (logistic-edge disks at fractional positions, seeded noise); no owner data is used. +Scenes are synthetic (logistic-edge disks at fractional positions, seeded noise); no private data is used. """ import hashlib, json, platform, subprocess, tempfile from pathlib import Path diff --git a/tools/braggmask/pins.py b/tools/braggmask/pins.py index f618638..47bf1fe 100644 --- a/tools/braggmask/pins.py +++ b/tools/braggmask/pins.py @@ -1,4 +1,4 @@ -"""Pinned py4DSTEM DataCube.get_braggmask and get_local_ave_dp (braggmask and plain). Run from the suite folder: +"""Pinned py4DSTEM DataCube.get_braggmask and get_local_ave_dp (braggmask and plain). Run from the folder that holds this package and References/python/py4dstem (the pinned venv; see the README): References/python/py4dstem/bin/python -I -B CuboKit/tools/braggmask/pins.py Output: CuboKit/Tests/CuboBraggTests/Fixtures/braggmask.json """ diff --git a/tools/calibration/pins.py b/tools/calibration/pins.py index cbfca12..ae1a84a 100644 --- a/tools/calibration/pins.py +++ b/tools/calibration/pins.py @@ -1,7 +1,7 @@ """Pinned py4DSTEM calibration model and cube reshaping: DataCube.calibrate (dim vectors, meshgrids, qxxs), Calibration setters, and preprocess.set_scan_shape, swap_RQ, swap_Rxy, swap_Qxy, bin_data_real, thin_data_real, pad_data_diffraction, resample_data_diffraction (bilinear), median_filter_masked_pixels on a Float32 DataCube. -Run from the suite folder (References/python/py4dstem, a pinned venv): +Run from the folder that holds this package and References/python/py4dstem (the pinned venv; see the README): References/python/py4dstem/bin/python -I -B CuboKit/tools/calibration/pins.py Output: CuboKit/Tests/CuboCoreTests/Fixtures/calibration.json """ diff --git a/tools/classification/pins.py b/tools/classification/pins.py index 892c2b4..6f7bda8 100644 --- a/tools/classification/pins.py +++ b/tools/classification/pins.py @@ -1,7 +1,7 @@ """Pins for the classification slice: py4DSTEM's Featurization (PCA, NMF, GMM, class patterns), the NMF and Gaussian mixture of scikit-learn they call, NumPy's RandomState draws, BraggVectorClassification and BraggVectors.get_virtual_image. Everything is -produced by py4DSTEM itself on small planted inputs; no CuboKit Swift is used. Run from the CuboKit folder with the suite's -reference environment under the memory cap: +produced by py4DSTEM itself on small planted inputs; no CuboKit Swift is used. Run from the CuboKit folder with the +pinned reference environment (see the README): ../References/python/py4dstem/bin/python -I -B tools/classification/pins.py [featurization|classification|virtualimage|all] diff --git a/tools/embedding/pins.py b/tools/embedding/pins.py index 4924f7d..379b401 100644 --- a/tools/embedding/pins.py +++ b/tools/embedding/pins.py @@ -1,4 +1,4 @@ -"""Generate CuboEmbedding pins. Run under a memory cap with the OLD pinned venv (scikit-learn 1.4.2; References/python/py4dstem has 1.9.1, +"""Generate CuboEmbedding pins. Run with the OLD pinned venv (scikit-learn 1.4.2; References/python/py4dstem has 1.9.1, whose PCA fixtures differ, so a regeneration there is a fixture move of its own): ../../References/CuboKit/py4dstem-venv/bin/python -I -B tools/embedding/pins.py diff --git a/tools/hot-pixels/pins.py b/tools/hot-pixels/pins.py index 312e261..c157644 100644 --- a/tools/hot-pixels/pins.py +++ b/tools/hot-pixels/pins.py @@ -1,5 +1,5 @@ """Actual pinned py4DSTEM DataCube.filter_hot_pixels; Float32 cube semantics. -Run under a memory cap using the pinned isolated Python venv. +Run with the pinned isolated Python venv (see the README). """ from pathlib import Path import json, subprocess, platform diff --git a/tools/idpc/pins.py b/tools/idpc/pins.py index fb9a15b..6b2f5e0 100644 --- a/tools/idpc/pins.py +++ b/tools/idpc/pins.py @@ -1,7 +1,7 @@ """NumPy float64 transcription of the app's Fourier-integration iDPC. This is NOT py4DSTEM iterative DPC. Periodic/centered zero-padded boundary, mean subtraction before padding and after cropping, regularized inverse gradient. -Run with the pinned venv under a memory cap, from this worktree. +Run with the pinned venv (see the README), from the package root. """ from pathlib import Path import json, platform, hashlib, subprocess diff --git a/tools/parallax/pins.py b/tools/parallax/pins.py index 489145f..7a343c8 100644 --- a/tools/parallax/pins.py +++ b/tools/parallax/pins.py @@ -1,7 +1,7 @@ """Regenerate with the pinned venv, from the package root: ../References/python/py4dstem/bin/python -I -B tools/parallax/pins.py -Only public py4DSTEM test data is cropped; no owner's dataset is copied. +Only public py4DSTEM test data is cropped; no private dataset is copied. """ import hashlib import json diff --git a/tools/parallax/preprocess_pins.py b/tools/parallax/preprocess_pins.py index daea160..2353b7f 100644 --- a/tools/parallax/preprocess_pins.py +++ b/tools/parallax/preprocess_pins.py @@ -1,4 +1,4 @@ -"""Pinned Parallax.preprocess oracle; run under a memory cap and pinned Python -I -B.""" +"""Pinned Parallax.preprocess oracle; run with the pinned Python, -I -B.""" import json, platform, subprocess, hashlib from pathlib import Path import numpy as np diff --git a/tools/parallax/subpixel_pins.py b/tools/parallax/subpixel_pins.py index 5bfc36b..d57c33d 100644 --- a/tools/parallax/subpixel_pins.py +++ b/tools/parallax/subpixel_pins.py @@ -1,5 +1,5 @@ """Pinned subpixel_alignment KDE oracle (bilinear and Lanczos deposition, Gaussian blur, sinc low-pass, default factor -heuristic). Position correction is not pinned here. Run with the pinned venv, python -I -B, under a memory cap.""" +heuristic). Position correction is not pinned here. Run with the pinned venv, python -I -B.""" import json, warnings from pathlib import Path src=Path(__file__).with_name('aberration_pins.py').read_text().split("cases=[]")[0] diff --git a/tools/polar-ellipse/README.md b/tools/polar-ellipse/README.md index 6b0a8da..0331f1b 100644 --- a/tools/polar-ellipse/README.md +++ b/tools/polar-ellipse/README.md @@ -1,27 +1,27 @@ -# Polar cube, ellipse fit, Fourier resample: planted truth (CuboKit tasks 17 and 18) +# Polar cube, ellipse fit, Fourier resample: planted truth `planted.py` writes synthetic elliptical-ring cubes with an analytic radial background, exact and noisy ring points for an ellipse fit, and band-limited images for Fourier resampling, each with its truth in `truth.json`. numpy only. It does not -run py4DSTEM and claims no upstream number: the Mac pin scripts (kit audit step 0) add those. +run py4DSTEM and claims no upstream number: `pins.py` beside it adds those. ```sh python3 -I tools/polar-ellipse/planted.py --out "$TMPDIR/cubo-planted" # about 1 s, ~10 MB ``` -Do not commit the output (generated files do not live in a repo). If a small case is wanted as a fixture, shrink `DET`/`SCAN` +Do not commit the output. If a small case is wanted as a fixture, shrink `DET`/`SCAN` in the script and commit the `.npy` in its own commit with the reason. -## What the Mac session does with it -- **Polar cube (17):** transform `cube_.npy` with the case's true (r0, c0, a, b, theta) to the polar grid in `truth.json` +## How the tests use it +- **Polar cube:** transform `cube_.npy` with the case's true (r0, c0, a, b, theta) to the polar grid in `truth.json` (`n_q`, `n_phi`, `q_k`, `phi_j`). Compare with `polar_truth_.npy`: ring positions to 0.25 px, background to rtol 1e-2 off the rings. With the right ellipse the result is constant in phi; with a circle assumption on `tilted` it is not (a useful failing-before test). -- **Radial background (17):** `truth.json.background` is `amp * exp(-q/length) + const`. A fit or subtraction on the phi mean +- **Radial background:** `truth.json.background` is `amp * exp(-q/length) + const`. A fit or subtraction on the phi mean of `polar_truth` must return those three numbers; the rings are separated from the background in q (rho 1 and 1.6). -- **Ellipse fit (18):** feed `ring_points_.npy` (exact) and `_noisy.npy` (sigma 0.05 px, seed 1) to the fit. Exact: atol +- **Ellipse fit:** feed `ring_points_.npy` (exact) and `_noisy.npy` (sigma 0.05 px, seed 1) to the fit. Exact: atol 1e-9 px and 1e-12 rad; noisy: the tolerances in `truth.json.ellipse_fit`. `circle` has no theta; `near_circle` (a-b = 0.5 px) has a loose theta by construction. -- **Fourier resample (18):** `resample_*_input.npy` to the shape of `resample_*_truth.npy`, atol 1e-10 (exact, all modes are +- **Fourier resample:** `resample_*_input.npy` to the shape of `resample_*_truth.npy`, atol 1e-10 (exact, all modes are below Nyquist). Even sizes only. Conventions are in the module docstring (rows and columns; theta of the major axis from +column toward +row; ring k at diff --git a/tools/polar-ellipse/pins.py b/tools/polar-ellipse/pins.py index 478ef6e..2e6b967 100644 --- a/tools/polar-ellipse/pins.py +++ b/tools/polar-ellipse/pins.py @@ -1,9 +1,9 @@ -"""Pinned py4DSTEM polar data cube, radial background, ellipse fits and Fourier resampling (CuboKit tasks 17 and 18), run on +"""Pinned py4DSTEM polar data cube, radial background, ellipse fits and Fourier resampling (CuboKit 0.9.0), run on small cases built from the planted truth of tools/polar-ellipse/planted.py (its `pattern`, `ring_points`, `band_limited`). Calls py4DSTEM itself: PolarDatacube, DataCube.get_radial_bkgrnd / get_radial_bksb_dp / get_local_ave_dp(radial_bksb=True), fit_ellipse_1D, fit_ellipse_amorphous_ring, constrain_degenerate_ellipse, ellipse_err, double_sided_gaussian(_fiterr), convert_ellipse_params(_r), fourier_resample and resample_data_diffraction(method="fourier"). -Run from the suite folder (References/python/py4dstem, a pinned venv): +Run from the folder that holds this package and References/python/py4dstem (the pinned venv; see the README): References/python/py4dstem/bin/python -I -B CuboKit/tools/polar-ellipse/pins.py Output: CuboKit/Tests/CuboVirtualTests/Fixtures/polar-radial-pins.json and CuboKit/Tests/CuboCoreTests/Fixtures/ellipse-resample-pins.json Conventions: py4DSTEM's x is the kit's ROW and y its COLUMN, so the (x0, y0, a, b, theta) here are the kit's (row, column, a, b, theta) diff --git a/tools/polar-ellipse/planted.py b/tools/polar-ellipse/planted.py index 9e29d6e..f78a34c 100644 --- a/tools/polar-ellipse/planted.py +++ b/tools/polar-ellipse/planted.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Planted-truth inputs for CuboKit tasks 17 and 18: polar cube, radial background, ellipse fit, Fourier resample. +"""Planted-truth inputs for CuboKit's polar cube, radial background, ellipse fit, Fourier resample. numpy only. Writes `.npy` files and one `truth.json` into --out (default: ./out beside this script; do not commit the output). diff --git a/tools/slice-1/pins.py b/tools/slice-1/pins.py index 0873be7..7297edd 100644 --- a/tools/slice-1/pins.py +++ b/tools/slice-1/pins.py @@ -1,4 +1,4 @@ -"""CuboKit slice 1: the pin generator (registration docs/registration-slice-1.md, sections 2, 3, 4, 5). +"""CuboKit slice 1: the pin generator. Run with the pinned oracle, from any directory: @@ -6,19 +6,19 @@ ../References/python/py4dstem/bin/python -I -B tools/slice-1/pins.py real [name ...] `synthetic` writes Tests/CuboVirtualTests/Fixtures/.npy and slice-1-pins.json (committed) BEFORE any Swift exists. -`real` exports the owner's cubes (section 5.4) and py4DSTEM's test datacube (5.3) as .npy into the real-data +`real` exports your own cubes and py4DSTEM's test datacube as .npy into the real-data directory, $CUBOKIT_REAL_DATA (default ~/Library/Caches/CuboKit/real-data), outside the repository, and writes beside each -pins.json plus its float64 arrays as -.npy. py4DSTEM f050d207 (0.14.19) is the oracle: DataCube.get_virtual_image, make_detector, get_dp_mean, get_dp_max, called as a user calls them (verbose=False). Beside each py4DSTEM number the generator records the float64 evaluation of the -same definition (section 2); the kit is held to the float64 number and the distance between the two is recorded. +same definition; the kit is held to the float64 number and the distance between the two is recorded. File:line pins into References/py4DSTEM-f050d207/py4DSTEM/datacube/: virtualimage.py get_virtual_image 67 (np.sum(self.data[rx, ry] * mask)), make_detector 588 virtualdiffraction.py get_virtual_diffraction 63 (np.mean / np.max over axes (0, 1)), get_dp_max 308, get_dp_mean 337 datacube.py DataCube.__init__ 33 (dims Rx, Ry, Qx, Qy), R_Nx/Q_Nx 172-194 -Index map (registration 1.1): py4DSTEM's qx (first detector axis) is the kit's ROW, qy its COLUMN; geometry tuples +Index map: py4DSTEM's qx (first detector axis) is the kit's ROW, qy its COLUMN; geometry tuples pass through unchanged: circle ((row, column), r), annulus ((row, column), (r_in, r_out)), rectangle (rowMin, rowMax, columnMin, columnMax), point (row, column). """ @@ -54,7 +54,7 @@ def check(name, ok, detail=""): def g17(x): - """Every number as a float64 string with 17 significant digits (registration 4); parsed by Double(String).""" + """Every number as a float64 string with 17 significant digits; parsed by Double(String).""" return "%.17g" % float(x) @@ -78,7 +78,7 @@ def versions(): # ---------------------------------------------------------------------------------------------------------------- -# The detector (registration 3.1), written from the text, standard library only, to cross-check make_detector +# The detector, written from the text, standard library only, to cross-check make_detector # ---------------------------------------------------------------------------------------------------------------- def mask_by_definition(shape, g): qy, qx = shape @@ -279,7 +279,7 @@ def run_synthetic(): for gname, g in geos: mask_py, _, _ = py4dstem_mask(shape, g) mask_def = mask_by_definition(shape, g) - check(f"{name}/{gname}: make_detector = registration 3.1", + check(f"{name}/{gname}: make_detector = definition", np.array_equal(np.asarray(mask_py, dtype=np.float64), mask_def)) own32, f64, _ = virtual_image_both(cube32, g) own_native, _, _ = virtual_image_both(cube, g) @@ -396,7 +396,7 @@ def run_real(names): own_native, _, _ = virtual_image_both(data, g) else: own_native = own32 - check(f"{name}/{gname}: make_detector = registration 3.1", + check(f"{name}/{gname}: make_detector = definition", np.array_equal(np.asarray(mask, dtype=np.float64), mask_by_definition(mask.shape, g))) arrays[f"{gname}-image_f64"] = f64 arrays[f"{gname}-image_py4dstem_float32_cube"] = own32 diff --git a/tools/slice-2/pins.py b/tools/slice-2/pins.py index 723f91e..646aa64 100644 --- a/tools/slice-2/pins.py +++ b/tools/slice-2/pins.py @@ -1,4 +1,4 @@ -"""CuboKit slice 2: the pin generator (docs/registration-slice-2.md, sections 2 to 5). +"""CuboKit slice 2: the pin generator. ../References/python/py4dstem/bin/python -I -B tools/slice-2/pins.py synthetic ../References/python/py4dstem/bin/python -I -B tools/slice-2/pins.py real [name ...] @@ -7,7 +7,7 @@ `real` reads slice 1's exports in $CUBOKIT_REAL_DATA and writes -s2-pins.json and -s2-.npy beside them. For every quantity: py4DSTEM's own result (f050d207, float32 where py4DSTEM computes in float32) and the float64 -evaluation of the same definition written here from the registration's text (section 3), which the kit is held to. +evaluation of the same definition written here from py4DSTEM's documented behaviour, which the kit is held to. """ import datetime import hashlib @@ -53,7 +53,7 @@ def versions(): # ------------------------------------------------------------------------------------------------------------------ -# The float64 definitions (registration section 3) +# The float64 definitions # ------------------------------------------------------------------------------------------------------------------ def com_f64(cube, weights=None): """3.1: per position S, the row and column moments, and the quotients, in float64.""" @@ -338,9 +338,9 @@ def run_synthetic(): check(f"disk {cr},{cc}: py4DSTEM centre exact", abs(float(e["center_row"]) - cr) < 1e-12 and abs(float(e["center_column"]) - cc) < 1e-12) out["probe"].append(e) - # mutant-driven cases (amendment 2026-10-15): a ring exactly at a trusted threshold (so `>` and `>=` differ where the + # boundary cases: a ring exactly at a trusted threshold (so `>` and `>=` differ where the # radius is averaged), and a ring exactly at the last threshold with thresholds whose computed last linspace value is - # not the stop. Each is checked to change get_probe_size's answer under its mutation before it is pinned. + # not the stop. Each is checked to change get_probe_size's answer if the comparison is changed, before it is pinned. def probe_variant(dp, lo, hi, n, ge=False, inexact_last=False): t = np.arange(n) * ((hi - lo) / (n - 1)) + lo if not inexact_last: diff --git a/tools/slice-3/pins.py b/tools/slice-3/pins.py index 5c100ed..078771e 100644 --- a/tools/slice-3/pins.py +++ b/tools/slice-3/pins.py @@ -1,11 +1,11 @@ -"""CuboKit slice 3: the pin generator (docs/registration-slice-3.md). +"""CuboKit slice 3: the pin generator. ../References/python/py4dstem/bin/python -I -B tools/slice-3/pins.py synthetic ../References/python/py4dstem/bin/python -I -B tools/slice-3/pins.py real [name ...] synthetic -> Tests/CuboVirtualTests/Fixtures/slice-3-pins.json (+ planted cubes as .npy), before any Swift; real -> $CUBOKIT_REAL_DATA/-s3-pins.json and -s3-.npy beside slice 1's exports. -py4DSTEM's own results beside the float64 evaluation of each definition (section 3), which the kit is held to. +py4DSTEM's own results beside the float64 evaluation of each definition, which the kit is held to. """ import datetime, json, math, os, platform, sys, warnings warnings.filterwarnings("ignore") diff --git a/tools/slice-4/pins.py b/tools/slice-4/pins.py index 7df7f3b..a54416a 100644 --- a/tools/slice-4/pins.py +++ b/tools/slice-4/pins.py @@ -1,22 +1,21 @@ -"""CuboKit slice 4: the pin generator (docs/registration-slice-4.md). +"""CuboKit slice 4: the pin generator. ../References/python/py4dstem/bin/python -I -B tools/slice-4/pins.py synthetic ../References/python/py4dstem/bin/python -I -B tools/slice-4/pins.py export-bullseye ../References/python/py4dstem/bin/python -I -B tools/slice-4/pins.py real [name ...] -An external memory cap (a watchdog on the process tree, 8 GB by default) is the enforcer; the watchdog thread below is a -second guard. One worker at a time; almgsi and auref need --gb 16 (and CUBOKIT_PINS_MEM_GB=16). +The watchdog thread below caps memory (8 GB by default). One worker at a time; almgsi and auref need --gb 16 (and CUBOKIT_PINS_MEM_GB=16). synthetic -> Tests/CuboVirtualTests/Fixtures/slice-4-pins.json (+ planted cubes as .npy), before any Swift; -export-bullseye -> $CUBOKIT_REAL_DATA/bullseye.npy (the owner's 20 vacuum-probe images as a 1 x 20 x 250 x 250 cube); +export-bullseye -> $CUBOKIT_REAL_DATA/bullseye.npy (20 vacuum-probe images of your own as a 1 x 20 x 250 x 250 cube); real -> $CUBOKIT_REAL_DATA/-s4-pins.json beside slice 1's exports. -py4DSTEM's own results beside the float64 evaluation of each definition (section 3), which the kit is held to. +py4DSTEM's own results beside the float64 evaluation of each definition, which the kit is held to. """ import datetime, importlib.util, json, os, resource, sys, warnings -# The suite's memory cap (owner, 2026-10-15 12:30): 8 GB by default, set before anything else is loaded. This macOS +# The memory cap: 8 GB by default, set before anything else is loaded. This macOS # refuses RLIMIT_AS and RLIMIT_DATA outright (setrlimit: "current limit exceeds maximum limit"), so the cap is a watchdog: # a thread that ends the process with exit code 3 once its peak resident size passes the cap. The two big cubes (almgsi, -# auref) need about 12 GB for their float64 copies: CUBOKIT_PINS_MEM_GB=16, one worker at a time (registration 9). +# auref) need about 12 GB for their float64 copies: CUBOKIT_PINS_MEM_GB=16, one worker at a time. import threading, time _cap = int(float(os.environ.get("CUBOKIT_PINS_MEM_GB", "8")) * (1 << 30)) @@ -156,8 +155,8 @@ def parabola(c0, cp, cm): def friedel_one(P, mask=None, u=1): - """Section 3.3 in float64, written from the text. Returns (row, column, peak, gap) where gap is the distance of the - second-largest cc value below the peak over max|cc| (the near-tie measure of section 4).""" + """get_origin_friedel in float64, written from its definition. Returns (row, column, peak, gap) where gap is the distance of the + second-largest cc value below the peak over max|cc| (the near-tie measure).""" Qr, Qc = P.shape im = np.pad(np.asarray(P, np.float64), ((0, Qr), (0, Qc))) if mask is None: diff --git a/tools/strain/pins.py b/tools/strain/pins.py index a498d9a..c059a22 100644 --- a/tools/strain/pins.py +++ b/tools/strain/pins.py @@ -1,4 +1,4 @@ -"""Generate reference and original-app pins. Run under a memory cap with the pinned venv. +"""Generate reference and original-app pins. Run with the pinned venv (see the README). Paths resolve from either the primary checkout or a worktree. No new Swift implementation is used. """ import hashlib, json, platform, subprocess, tempfile