From d96baff1368b742a6ed41e8a7423a6d0a2a38847 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:40:36 +0000 Subject: [PATCH 01/17] docs(core): drop references to documents outside the repo Comments in DiffractoCore cited registration sections, review rounds and work sessions that are not part of this repository. They now say what the code does, or name the module that does it. Comment-only change. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- Sources/DiffractoCore/CIFImport.swift | 10 ++--- Sources/DiffractoCore/ConventionalCell.swift | 8 ++-- .../DiffractoCore/ConventionalSetting.swift | 10 ++--- Sources/DiffractoCore/CrystalStructure.swift | 15 ++++--- Sources/DiffractoCore/Element.swift | 2 +- Sources/DiffractoCore/LatticeReduction.swift | 6 +-- Sources/DiffractoCore/LaueClass.swift | 1 - Sources/DiffractoCore/MillerIndex.swift | 10 ++--- Sources/DiffractoCore/PointSymmetry.swift | 13 +++--- Sources/DiffractoCore/ReflectionList.swift | 19 +++++---- Sources/DiffractoCore/SymmetryDetection.swift | 40 +++++++++---------- Sources/DiffractoCore/SymmetryOperation.swift | 2 +- Sources/DiffractoCore/UnitCell.swift | 9 ++--- 13 files changed, 70 insertions(+), 75 deletions(-) diff --git a/Sources/DiffractoCore/CIFImport.swift b/Sources/DiffractoCore/CIFImport.swift index 149b689..b6ac915 100644 --- a/Sources/DiffractoCore/CIFImport.swift +++ b/Sources/DiffractoCore/CIFImport.swift @@ -1,11 +1,11 @@ // // CIFImport.swift // Role: the subset of CIF that defines a structure (cell, the atom-site loop, the symmetry-operation loop), read into -// a `CrystalStructure`. Ported from reference commit 7448401b `CIFImport.swift` (registration section 3.7): the +// a `CrystalStructure`. Ported from reference commit 7448401b `CIFImport.swift`: the // tokenizer, the `(3)` uncertainty suffix, the element symbol rule, the written-precision half-step, the // expansion that merges images within the propagated half-step AND 0.10 Å, and the refusals that protect the // structure. Generalised: every crystal system is admitted; a file without operations has its symmetry -// detected from the sites (3.4b); the two app-specific refusals (point group, scattering factor) are gone. +// detected from the sites; the two app-specific refusals (point group, scattering factor) are gone. // Errors are user-facing: every case names the offending tag, value or symbol so a scientist can find and fix // the problem in their own file. // @@ -93,7 +93,7 @@ public enum CIFImportError: Error, Equatable, Sendable, CustomStringConvertible } } -/// `localizedDescription` (what an app shows) is the same text as `description` (registration 0.0.4 section 5). +/// `localizedDescription` (what an app shows) is the same text as `description`. extension CIFImportError: LocalizedError { public var errorDescription: String? { description } } @@ -181,7 +181,7 @@ public enum CIFImport { let cell = UnitCell(a: parsed.a, b: parsed.b, c: parsed.c, alphaDeg: parsed.alpha, betaDeg: parsed.beta, gammaDeg: parsed.gamma) let name = displayName(dataBlockName: parsed.dataBlockName, fileBaseName: fileBaseName) - // The symmetry is detected from the (expanded) cell either way (registration 3.4, amended 2026-10-14): a + // The symmetry is detected from the (expanded) cell either way: a // file's loop may declare less than its atoms have. let structure = CrystalStructure.detectingSymmetry( cell: cell, sites: parsed.sites, fileOperations: parsed.operations, @@ -768,7 +768,7 @@ public enum CIFImport { propagate(op.rotation.rows[0], site.halfStep), propagate(op.rotation.rows[1], site.halfStep), propagate(op.rotation.rows[2], site.halfStep)) - // Only images of the SAME row merge (Gate B amendment, 2026-10-15): two rows on one position are two + // Only images of the SAME row merge: two rows on one position are two // species sharing a site (Fe2+ and Fe3+ in magnetite), each with its own occupancy; at full // occupancy they are refused by the close-contact rule, never silently merged. let isDuplicate = kept.contains { existing in diff --git a/Sources/DiffractoCore/ConventionalCell.swift b/Sources/DiffractoCore/ConventionalCell.swift index f16f03c..d83a9c7 100644 --- a/Sources/DiffractoCore/ConventionalCell.swift +++ b/Sources/DiffractoCore/ConventionalCell.swift @@ -1,11 +1,11 @@ // // ConventionalCell.swift // Role: the conventional (possibly centred) cell of a lattice, its Bravais lattice and the integer transformation to -// it (registration docs/registration-slice-1b.md section 1). The lattice is first Niggli-reduced +// it. The lattice is first Niggli-reduced // (`niggliReduced`). On the reduced basis every lattice symmetry is an integer matrix with entries in // {-1, 0, 1}, so the lattice's point group is found exactly by testing those 3^9 matrices against the metric // (RᵀGR = G within a stated relative tolerance); that group IS the lattice character the International Tables -// (A, section 9.2) tabulate as 44 reduced-form conditions: its order names the crystal family (2 aP, 4 m, 8 o, +// (A) tabulate as 44 reduced-form conditions: its order names the crystal family (2 aP, 4 m, 8 o, // 16 t, 12 hR, 24 hP, 48 c) and the 44 characters differ only in which centring the family's conventional // cell then needs. The conventional axes are the symmetry axes of that group (the shortest lattice vector along // each), and the centring is read off the lattice points inside the cell. No table is copied. @@ -17,10 +17,10 @@ // (`transformation == .identity`), but a cell that is a conventional cell of its lattice in ANOTHER setting is // re-labelled into this one: a monoclinic P cell (index 1) goes to unique axis b with a and c the shortest pair and // β ≥ 90°, so a c-unique or a-unique monoclinic cell, or one whose a and c are not that pair, comes back with a -// permuted or sign-changed transformation (Gate B on 4876b18, F2). Read `transformation` and `setting`, not the +// permuted or sign-changed transformation. Read `transformation` and `setting`, not the // family name, to know whether the cell moved. // -// Tolerances (Gate B on 4876b18, F3): the lattice symmetry here is decided by the relative `tolerance` alone +// Tolerances: the lattice symmetry here is decided by the relative `tolerance` alone // (|RᵀGR − G|ᵢⱼ ≤ tolerance · √(Gᵢᵢ Gⱼⱼ), default 1e-9). `detectingSymmetry(metricLengthTolerance:)` is passed on as // this tolerance; `metricAngleToleranceDeg` is not used by the conventional-cell route to find the cell or its // Bravais lattice. A primitive body-centred cubic cell written with the angle 109.4712° (the exact one is diff --git a/Sources/DiffractoCore/ConventionalSetting.swift b/Sources/DiffractoCore/ConventionalSetting.swift index 486367d..0f6090f 100644 --- a/Sources/DiffractoCore/ConventionalSetting.swift +++ b/Sources/DiffractoCore/ConventionalSetting.swift @@ -1,9 +1,9 @@ // // ConventionalSetting.swift -// Role: the symmetry of a structure in ANY cell setting (registration docs/registration-slice-1b.md section 1). The +// Role: the symmetry of a structure in ANY cell setting. The // sites are carried into the conventional cell of the lattice (x_conventional = P⁻¹ x_file, wrapped, the lattice // centring added as images and merged within the position tolerance), the operations are detected there by -// slice 1's rule (a conventional cell has its holohedry in the standard position), and the result is reported +// the conventional-cell rule (a conventional cell has its holohedry in the standard position), and the result is reported // in BOTH settings: `CrystalStructure` carries the file setting's operations (conjugated back, R' = P R P⁻¹, // t' = P t) and `ConventionalSetting` the conventional cell's own structure, operations, Laue class (so its // absences and ring names) and the transformation between the two. @@ -102,7 +102,7 @@ extension CrystalStructure { let conventional = cell.conventionalCell(tolerance: metricLengthTolerance) let p = conventional.transformation if p == .identity || sites.isEmpty { - // The file's cell is the conventional cell: nothing moves, slice 1's detection is the detection. + // The file's cell is the conventional cell: nothing moves, the plain detection is the detection. let detection = SymmetryDetection.detect( sites: sites, cell: cell, coarsestHalfStep: coarsestHalfStep, extraCandidates: fileOperations?.map(\.rotation) ?? [], preferredOperations: fileOperations ?? [], @@ -187,7 +187,7 @@ extension CrystalStructure { spaceGroupNumber: spaceGroupNumber, name: name) } - /// The identity first, then by rotation and translation: slice 1's order. + /// The identity first, then by rotation and translation: the order `SymmetryDetection` returns. static func operationOrder(_ lhs: SymmetryOperation, _ rhs: SymmetryOperation) -> Bool { if lhs.rotation.isIdentity != rhs.rotation.isIdentity { return lhs.rotation.isIdentity } let l = lhs.rotation.rows.flatMap { [$0.x, $0.y, $0.z] }, r = rhs.rotation.rows.flatMap { [$0.x, $0.y, $0.z] } @@ -198,7 +198,7 @@ extension CrystalStructure { /// The centring images of ONE site, without any that coincides with an earlier image of the same site (a centring /// translation listed twice). Rows are never merged with each other: two rows on one position (a mixed-occupancy - /// site written as two rows of one element) are two sites in the conventional cell too (Gate B F2). + /// site written as two rows of one element) are two sites in the conventional cell too. static func mergingCoincident(images: [AtomSite], matcher: PositionMatcher) -> [AtomSite] { var kept: [AtomSite] = [] for image in images where !kept.contains(where: { matcher.matches($0.fractional, image.fractional) }) { diff --git a/Sources/DiffractoCore/CrystalStructure.swift b/Sources/DiffractoCore/CrystalStructure.swift index 5431126..2719b22 100644 --- a/Sources/DiffractoCore/CrystalStructure.swift +++ b/Sources/DiffractoCore/CrystalStructure.swift @@ -30,15 +30,15 @@ public struct CrystalStructure: Sendable, Equatable { public enum SymmetrySource: Sendable, Equatable { /// Given to the initialiser as they are, not checked against the sites (a structure built in code). case operationsGiven - /// Found from the sites by the holohedry test (registration 3.4b). + /// Found from the sites by the holohedry test. case detected /// A file's loop expanded the asymmetric unit, then the operations were detected from the expanded cell - /// (3.4 as amended 2026-10-14); the loop is `fileOperations`. + ///; the loop is `fileOperations`. case detectedAfterFileExpansion } /// Which cell the numbers of this value (`cell`, `sites`, `operations`, `laueGroup`, and the reflections and rings - /// built from them) are in. A file's own setting is never relabelled silently (slice 1b). + /// built from them) are in. A file's own setting is never relabelled silently. public enum Setting: Sendable, Equatable { /// The cell the structure was given in. case file @@ -76,7 +76,7 @@ public struct CrystalStructure: Sendable, Equatable { /// A name for the structure (a CIF's data block, a formula); nil when none was given. public let name: String? /// Candidate rotations of the holohedry that failed the fixed position tolerance but would pass at the file's own - /// written precision (3.4b): reported, not taken. Empty when the operations were given. + /// written precision: reported, not taken. Empty when the operations were given. public let unresolvedRotations: [IntegerMatrix] /// The largest uncertainty of a detected translation that is neither exact (from the file, or on the k/24 grid): /// its least-squares residual plus twice the written half-step, at least 1e-9. Reported; the absence test does not @@ -108,7 +108,7 @@ public struct CrystalStructure: Sendable, Equatable { self.symmetryRefusal = nil } - /// A structure whose operations are detected from its sites (registration 3.4b): a P1 file, a cell built in + /// A structure whose operations are detected from its sites: a P1 file, a cell built in /// code, or a file's expanded cell with its loop as `fileOperations` (whose rotations join the candidates). /// `coarsestHalfStep` is the written precision of the coordinates when known, used only to report an unresolved /// candidate. @@ -317,10 +317,9 @@ public struct CrystalStructure: Sendable, Equatable { // MARK: Reflections /// Every reflection with 0 < |g| ≤ `kMax` (Å⁻¹), with its systematic-absence flag from this structure's - /// operations, and its families by this structure's Laue group (registration 3.8, 3.9). A reflection with |g| + /// operations, and its families by this structure's Laue group. A reflection with |g| /// exactly `kMax` is in or out by the last bit of |g|, which can differ by one ulp between two settings of the same - /// lattice, so the lists of the file's cell and of its conventional cell may differ by such a reflection (Gate B on - /// 4876b18). + /// lattice, so the lists of the file's cell and of its conventional cell may differ by such a reflection. public func reflections(kMax: Double) throws(ReflectionListError) -> ReflectionList { try reflections(kMax: kMax, policy: .default) } diff --git a/Sources/DiffractoCore/Element.swift b/Sources/DiffractoCore/Element.swift index 12eb84f..f39f08e 100644 --- a/Sources/DiffractoCore/Element.swift +++ b/Sources/DiffractoCore/Element.swift @@ -2,7 +2,7 @@ // Element.swift // Role: chemical symbols 1 ... 118 and the CIF symbol rule (reference commit 7448401b `CIFImport.elementSymbol(from:)`): // trailing oxidation-state and isotope markers stripped, case normalised. Which elements have a scattering factor -// is slice 2's table to say, not this file's. +// is DiffractoScattering's table to say, not this file's. // import Foundation diff --git a/Sources/DiffractoCore/LatticeReduction.swift b/Sources/DiffractoCore/LatticeReduction.swift index 18e8eb9..e09c050 100644 --- a/Sources/DiffractoCore/LatticeReduction.swift +++ b/Sources/DiffractoCore/LatticeReduction.swift @@ -1,7 +1,7 @@ // // LatticeReduction.swift -// Role: the Niggli-reduced cell of a lattice and the integer change of basis that reaches it (registration -// docs/registration-slice-1b.md section 1). The algorithm is Křivý & Gruber (1976), Acta Cryst. A32, 297, +// Role: the Niggli-reduced cell of a lattice and the integer change of basis that reaches it. +// The algorithm is Křivý & Gruber (1976), Acta Cryst. A32, 297, // steps A1 to A8, with Grosse-Kunstleve, Sauter & Adams (2004), Acta Cryst. A60, 1-6: every comparison carries // an epsilon (x < y becomes x < y - ε, x = y becomes |x - y| ≤ ε), so a cell within ε of a boundary between // two reduced forms reduces to one of them and never cycles; and the metric is recomputed from the exact input @@ -74,7 +74,7 @@ extension UnitCell { // A5, A6, A7: a shear that makes |ξ| ≤ B, |η| ≤ A, |ζ| ≤ A (with the boundary cases of the paper). The shear is // by an integer MULTIPLE when the excess is large (`shearMultiple`: 1 up to 50 lengths, the paper's unit // shear, so a cell that needs a few dozen shears reduces exactly as before), so a cell sheared by thousands of - // lattice units reduces in a few steps and not after thousands (Gate B on 4876b18, F1). + // lattice units reduces in a few steps and not after thousands. if abs(xi) > b + eps || (abs(xi - b) <= eps && 2 * eta < zeta - eps) || (abs(xi + b) <= eps && zeta < -eps) { let n: Int = UnitCell.shearMultiple(abs(xi), b, eps) if n < 0 { break } diff --git a/Sources/DiffractoCore/LaueClass.swift b/Sources/DiffractoCore/LaueClass.swift index 79ef0bb..6e2305e 100644 --- a/Sources/DiffractoCore/LaueClass.swift +++ b/Sources/DiffractoCore/LaueClass.swift @@ -2,7 +2,6 @@ // LaueClass.swift // Role: the seven crystal systems and the eleven Laue classes (the point groups a kinematic pattern can tell apart, // since Friedel's law adds the inversion), with the 32 → 11 table and the properties the orbit naming needs. -// Registration docs/registration-slice-1.md sections 3.5 and 3.6. // import Foundation diff --git a/Sources/DiffractoCore/MillerIndex.swift b/Sources/DiffractoCore/MillerIndex.swift index fca65ee..3d366b6 100644 --- a/Sources/DiffractoCore/MillerIndex.swift +++ b/Sources/DiffractoCore/MillerIndex.swift @@ -1,10 +1,10 @@ // // MillerIndex.swift // Role: (h, k, l), the integer triple that names a reflection or a lattice plane, and the integer 3 × 3 matrices the -// symmetry operations rotate it with. The covariant action is the one that matters here (registration section -// 3.6): a rotation R acting on fractional coordinates as p' = R p sends the reflection (h k l) to the row +// symmetry operations rotate it with. The covariant action is the one that matters here: +// a rotation R acting on fractional coordinates as p' = R p sends the reflection (h k l) to the row // vector (h k l) R, so |F| is preserved along that orbit and a reflection fixed by R is absent unless -// (h k l) · t is an integer (section 3.8). +// (h k l) · t is an integer. // import Foundation @@ -32,7 +32,7 @@ public struct MillerIndex: Sendable, Hashable, Comparable, CustomStringConvertib } /// The row vector (h k l) times `rotation`: where a symmetry operation with that rotation part sends this - /// reflection (registration section 3.6). + /// reflection. public func applying(_ rotation: IntegerMatrix) -> MillerIndex { MillerIndex( h * rotation.rows[0].x + k * rotation.rows[1].x + l * rotation.rows[2].x, @@ -48,7 +48,7 @@ public struct MillerIndex: Sendable, Hashable, Comparable, CustomStringConvertib Double(h) * v.x + Double(k) * v.y + Double(l) * v.z } - /// Of the orbit-canonical key (registration 3.6): how many indices are non-negative. + /// Of the orbit-canonical key: how many indices are non-negative. public var nonNegativeCount: Int { (h >= 0 ? 1 : 0) + (k >= 0 ? 1 : 0) + (l >= 0 ? 1 : 0) } diff --git a/Sources/DiffractoCore/PointSymmetry.swift b/Sources/DiffractoCore/PointSymmetry.swift index aded4c4..4205fca 100644 --- a/Sources/DiffractoCore/PointSymmetry.swift +++ b/Sources/DiffractoCore/PointSymmetry.swift @@ -1,10 +1,9 @@ // // PointSymmetry.swift // Role: a finite group of integer rotation matrices in the fractional basis: the holohedry of a metric, the Laue -// group of a structure, the orbit of a reflection and its canonical form and name. Registration -// docs/registration-slice-1.md sections 3.4 (the holohedry generators), 3.5 (the Laue class of a group) and 3.6 -// (orbits: (h k l) R; the canonical form is the orbit member greatest under (number of non-negative indices, -// lexicographic), the one rule that reproduces the reference implementations' cubic, hexagonal and monoclinic names). +// group of a structure, the orbit of a reflection and its canonical form and name. +// Orbits are (h k l) R; the canonical form is the orbit member greatest under (number of non-negative indices, +// lexicographic), the one rule that reproduces the reference implementations' cubic, hexagonal and monoclinic names. // import Foundation @@ -50,7 +49,7 @@ public struct PointSymmetry: Sendable, Equatable { PointSymmetry(rotations: rotations + rotations.map(\.negated)) } - /// The holohedry (the full Laue group of the lattice) of a metric family (section 3.4): the conventional setting, with + /// The holohedry (the full Laue group of the lattice) of a metric family: the conventional setting, with /// the 4-fold of a tetragonal metric about its odd axis and the unique axis of a monoclinic one where the metric puts it. public static func holohedry(of family: UnitCell.MetricFamily) -> PointSymmetry { let c2x = IntegerMatrix([1, 0, 0], [0, -1, 0], [0, 0, -1]) @@ -107,7 +106,7 @@ public struct PointSymmetry: Sendable, Equatable { /// The proper rotations (determinant +1). public var properRotations: [IntegerMatrix] { rotations.filter { $0.determinant == 1 } } - /// The Laue class by the order and the axis orders present (section 3.5); nil when the group is not closed, lacks + /// The Laue class by the order and the axis orders present; nil when the group is not closed, lacks /// the inversion, or has an order no Laue group has. public var laueClass: LaueClass? { guard containsInversion, isClosed else { return nil } @@ -126,7 +125,7 @@ public struct PointSymmetry: Sendable, Equatable { } } - // MARK: Orbits (section 3.6) + // MARK: Orbits /// {(h k l) R : R in the group}. public func orbit(of hkl: MillerIndex) -> Set { diff --git a/Sources/DiffractoCore/ReflectionList.swift b/Sources/DiffractoCore/ReflectionList.swift index 099ccbf..685d9d1 100644 --- a/Sources/DiffractoCore/ReflectionList.swift +++ b/Sources/DiffractoCore/ReflectionList.swift @@ -3,7 +3,7 @@ // Role: every (h, k, l) with 0 < |g| ≤ k_max, its d, whether the space-group operations allow it, and the rings // (families) the list falls into. The enumeration is reference commit 7448401b `Crystal.reflections` without the // structure factor (the per-axis index bound, DEVIATION of 2026-09-14, complete for oblique cells); the absence -// rule is registration 3.8; the family rule is reference commit a58c84c `DSpacingIndexing.families` (ADR 022), 3.9. +// rule is the general reflection condition; the family rule is reference commit a58c84c `DSpacingIndexing.families`. // import Foundation @@ -12,7 +12,7 @@ import simd public enum ReflectionListError: Error, Equatable, Sendable, CustomStringConvertible { /// k_max must be finite, positive and at most `ReflectionList.maximumKMax` Å⁻¹. (A reflection with |g| equal to k_max /// is in or out by the last bit of |g|, which can differ by one ulp between two settings of the same lattice: the - /// lists of a file's cell and of its conventional cell may differ by such a reflection; Gate B on 4876b18.) + /// lists of a file's cell and of its conventional cell may differ by such a reflection.) case invalidKMax(Double) /// The cell is not a valid lattice (`UnitCell.issues`), or so large that the list would exceed /// `ReflectionList.maximumReflections` (about (4/3)π k_max³ V reflections; 255 bytes each). @@ -33,7 +33,7 @@ public enum ReflectionListError: Error, Equatable, Sendable, CustomStringConvert } } -/// `localizedDescription` (what an app shows) is the same text as `description` (registration 0.0.4 section 5). +/// `localizedDescription` (what an app shows) is the same text as `description`. extension ReflectionListError: LocalizedError { public var errorDescription: String? { description } } @@ -135,7 +135,7 @@ public struct ReflectionList: Sendable, Equatable { /// above it the list is refused, never trapped. public static let maximumReflections = 10_000_000 /// The most (h, k, l) the enumeration box may hold: a needle-shaped cell has few reflections by volume but a long box - /// (Gate B on slice 2: a = 1e7, b = c = 1e-3 Å passed the volume estimate and enumerated 24 million). + /// (a = 1e7, b = c = 1e-3 Å passed the volume estimate and enumerated 24 million). public static let maximumEnumeration = 100_000_000 public let kMax: Double @@ -149,7 +149,7 @@ public struct ReflectionList: Sendable, Equatable { /// Whether orbit names print four Miller–Bravais indices: a hexagonal-axes Laue class (-3, -3m, 6/m, 6/mmm) in a /// cell whose metric is hexagonal (a = b, γ = 120°). A trigonal structure in rhombohedral axes keeps three - /// indices (Gate B amendment, 2026-10-15). + /// indices. public let usesMillerBravais: Bool /// The list for `cell` with the absences of `operations` and the orbits of `laueGroup`. @@ -211,11 +211,10 @@ public struct ReflectionList: Sendable, Equatable { } } - /// The general reflection condition (registration 3.8): absent when an operation (R, t) with (h k l) R = (h k l) + /// The general reflection condition: absent when an operation (R, t) with (h k l) R = (h k l) /// has (h k l) · t further than 1e-9 from an integer. For such an operation h · t does not depend on the origin, /// and a detected translation is the least-squares one, whose rounding error (ē − R ē, ē the sites' mean rounding) - /// has h · (ē − R ē) = 0 exactly when h R = h: so a shifted or coarsely written cell gives the exact answer - /// (Gate B amendment, 2026-10-15). + /// has h · (ē − R ē) = 0 exactly when h R = h: so a shifted or coarsely written cell gives the exact answer. public static func isAllowed(_ hkl: MillerIndex, by operations: [SymmetryOperation], tolerance: Double = 1e-9) -> Bool { let bound = tolerance.isFinite && tolerance >= 0 ? tolerance : 0 @@ -254,7 +253,7 @@ public struct ReflectionList: Sendable, Equatable { public var allowed: [Reflection] { reflections.filter(\.isAllowed) } - /// The rings (registration 3.9), on the allowed reflections by default; `allowedOnly: false` groups every + /// The rings, on the allowed reflections by default; `allowedOnly: false` groups every /// reflection of the lattice. public func families(allowedOnly: Bool = true) -> [ReflectionFamily] { ReflectionList.families(of: allowedOnly ? allowed : reflections, laueGroup: laueGroup, @@ -262,7 +261,7 @@ public struct ReflectionList: Sendable, Equatable { } /// The ring rule on any reflections in the list's order (increasing |g|): a reflection joins the current ring while - /// |g| ≤ g₀(1 + `familyTolerance`); orbits and names by `laueGroup` (slice 2 applies it after the |F| cut). + /// |g| ≤ g₀(1 + `familyTolerance`); orbits and names by `laueGroup` (DiffractoScattering applies it after the |F| cut). public static func families(of reflections: [Reflection], laueGroup: PointSymmetry, millerBravais: Bool, familyTolerance: Double = ReflectionList.familyTolerance) -> [ReflectionFamily] { diff --git a/Sources/DiffractoCore/SymmetryDetection.swift b/Sources/DiffractoCore/SymmetryDetection.swift index cb4c1ea..a1485e1 100644 --- a/Sources/DiffractoCore/SymmetryDetection.swift +++ b/Sources/DiffractoCore/SymmetryDetection.swift @@ -1,8 +1,8 @@ // // SymmetryDetection.swift // Role: the operations of a structure from its sites, found by testing every element of the metric's holohedry -// (and any rotation a file's loop names) on the sites (registration section 3.4b, amended 2026-10-14: always -// run, also after a file's expansion). The permutation test is reference commit 7448401b's `CIFImport.isSymmetry` +// (and any rotation a file's loop names) on the sites, always +// also after a file's expansion. The permutation test is reference commit 7448401b's `CIFImport.isSymmetry` // (candidate translations from site 0 onto every compatible site, targets distinct, positions within a FIXED // 5e-3 fractional, occupancies within 0.02), generalised from two generators of two families to the whole // holohedry of seven, and extended to keep every translation that works, so a centred cell yields its centring @@ -10,7 +10,7 @@ // // Each translation is REFINED by least squares over the matched sites (the mean of the residuals), then taken // from the file's loop when the loop has the same operation, else moved to the nearest k/24 only when it lies -// within three times its own uncertainty of it (Gate B amendment, 2026-10-15: a fixed 5e-3 snap moved genuine +// within three times its own uncertainty of it (a fixed 5e-3 snap moved genuine // off-grid translations of a shifted origin and lost or merged operations). // @@ -24,12 +24,12 @@ public enum SymmetryDetection { /// Occupancies are dimensionless and refined ones differ by thousandths between equivalent sites. public static let occupancyTolerance = 0.02 /// Through `CrystalStructure.detectingSymmetry(conventional: true)` (the default) only the LENGTH tolerance reaches the - /// lattice-symmetry test that finds the cell and its Bravais lattice; the angle tolerance has no effect there (Gate B - /// on 4876b18, F3), and spglib's symprec corresponds to a length tolerance of about 1e-5, not 1e-9: a bcc primitive + /// lattice-symmetry test that finds the cell and its Bravais lattice; the angle tolerance has no effect there, + /// and spglib's symprec corresponds to a length tolerance of about 1e-5, not 1e-9: a bcc primitive /// cell written with 109.4712° is hR at the default and Im-3m from 1e-5. - /// The metric family is proposed at the ring rule's tolerance (1e-9 relative on lengths, 1e-9° on angles; the - /// registration's 3.2 default): a rotation that is a symmetry then preserves every |g| to the 1e-9 the families are - /// grouped by, so an orbit never splits across two rings (Gate B amendment, 2026-10-15; the reference implementations' 1e-3 / 0.05° + /// The metric family is proposed at the ring rule's tolerance (1e-9 relative on lengths, 1e-9° on angles): + /// a rotation that is a symmetry then preserves every |g| to the 1e-9 the families are + /// grouped by, so an orbit never splits across two rings (the reference implementations' 1e-3 / 0.05° /// called a cell with c/a = 1.0005 cubic and split its rings). public static let defaultMetricLengthTolerance = 1e-9 public static let defaultMetricAngleToleranceDeg = 1e-9 @@ -71,20 +71,20 @@ public enum SymmetryDetection { } } - /// How two positions are called the same site (slice 1b). `.fractional(5e-3)` is slice 1's rule and the default: + /// How two positions are called the same site. `.fractional(5e-3)` is the default: /// the Euclidean distance in fractional coordinates, whatever the cell's size. `.cartesian(Å)` is spglib's sense: /// the minimum-image distance in Å, so a pseudo-symmetry of 0.013 Å reads as symmetry in a 50 Å cell at the - /// fractional default and not at `.cartesian(1e-3)`. The default stays fractional until a measured comparison is - /// registered. + /// fractional default and not at `.cartesian(1e-3)`. The default stays fractional until a measured comparison + /// supports a change. /// /// The Cartesian value is spglib's `symprec` in the sense that it is a distance in Å, but it is NOT the same - /// threshold: measured against spglib (Gate B on 4876b18, F4), an operation is kept here only while the displacement + /// threshold: measured against spglib, an operation is kept here only while the displacement /// that breaks it is below about HALF of `t`, and is lost at displacements of 0.5 to 0.9 `t`, where spglib with /// `symprec = t` still finds it up to 1.0 to 1.1 `t`. The set of operations is also closed under composition at this /// bound (`closedSubset`). Roughly, `.cartesian(2 * s)` here corresponds to spglib's symprec `s`, and the /// proposed `1e-3` to a symprec of about 5e-4 Å. The default is unchanged. /// - /// **The domain** (DiffractoKit 0.2.0, the Gate B findings F5 and F6). Whichever sense is used, the tolerance acts as a + /// **The domain** (DiffractoKit 0.2.0). Whichever sense is used, the tolerance acts as a /// window in fractional coordinates along each cell axis: `t` itself for `.fractional(t)`, `t` times the reciprocal axis /// length |bᵢ| for `.cartesian(t)`. Every window must lie in `minimumWindow` (1e-9) up to, not including, /// `maximumWindow` (0.5). The floor is the scale at which the cell wrap snaps a coordinate to the cell edge @@ -136,7 +136,7 @@ public enum SymmetryDetection { } /// How the permutation test finds the target of an image: a cell list (bins of the position tolerance, O(1) per - /// image) or slice 1's scan of every site (the reference path, O(N)). They return identical operations bit for bit. + /// image) or the plain scan of every site (the reference path, O(N)). They return identical operations bit for bit. public enum Search: Sendable, Equatable { case cellList case bruteForce @@ -253,7 +253,7 @@ public enum SymmetryDetection { /// The operations a space group can be made of: the set is closed under composition (mod lattice translations, /// within `positionTolerance`). Pseudo-symmetry within the fractional tolerance can pass single operations whose /// products do not pass (the reference implementations' split-atom gold: 40 operations, rotations of m-3m, not a group); an operation - /// that composes out of the set is dropped and the test repeated until the set is closed (2026-10-15). Sets above + /// that composes out of the set is dropped and the test repeated until the set is closed. Sets above /// `closureCheckLimit` operations are returned as found (a 500-atom symmetric supercell has 24000). public static let closureCheckLimit = 2048 @@ -348,7 +348,7 @@ public enum SymmetryDetection { let refined = wrapFractional(trial + mean) // The candidates for the final translation, best first: the file's exact one, the k/24 grid point, the // least-squares one, the trial. Each is RE-VERIFIED as a permutation before it is taken: moving a - // translation after the test is how a non-symmetry got in (found 2026-10-15 while fixing Gate B's F1). + // translation after the test is how a non-symmetry got in (found 2026-10-15). var options: [(SIMD3, Double)] = [] if let exact = preferred.first(where: { SymmetryDetection.translationsEqual($0.translation, refined, 3 * u + 1e-9) }) { options.append((exact.translationMod1, 0)) @@ -397,7 +397,7 @@ public enum SymmetryDetection { assignment.reserveCapacity(sites.count) for site in sites { let image = wrapFractional(rotation.apply(site.fractional) + translation) - // The same target slice 1 picks: the LOWEST index among the unclaimed sites that match. + // The same target the plain scan picks: the LOWEST index among the unclaimed sites that match. var target = Int.max index.forEachCandidate(near: image) { j in if j < target, !claimed[j], sites[j].z == site.z, @@ -411,7 +411,7 @@ public enum SymmetryDetection { return assignment } - /// Slice 1's scan of every site for every image (the CPU reference path of the cell list). + /// The plain scan of every site for every image (the CPU reference path of the cell list). static func permutationReference(_ rotation: IntegerMatrix, _ translation: SIMD3, of sites: [AtomSite], matcher: PositionMatcher, occupancyTolerance: Double = occupancyTolerance) -> [Int]? { @@ -444,7 +444,7 @@ public enum SymmetryDetection { } } -// MARK: - Position matching and the cell list (slice 1b) +// MARK: - Position matching and the cell list /// The rule that calls two fractional positions the same site. struct PositionMatcher: Sendable { @@ -479,7 +479,7 @@ struct PositionMatcher: Sendable { } /// Whether `a` and `b` (fractional, any image) are the same position within the tolerance. The argument order is - /// slice 1's: a site, then the image looked for. + /// the plain scan's: a site, then the image looked for. func matches(_ a: SIMD3, _ b: SIMD3) -> Bool { switch tolerance { case .fractional(let t): diff --git a/Sources/DiffractoCore/SymmetryOperation.swift b/Sources/DiffractoCore/SymmetryOperation.swift index 9e8b7f5..bfdb621 100644 --- a/Sources/DiffractoCore/SymmetryOperation.swift +++ b/Sources/DiffractoCore/SymmetryOperation.swift @@ -2,7 +2,7 @@ // SymmetryOperation.swift // Role: one affine operation on fractional coordinates, p' = R p + t, with R an integer matrix and t a translation; // parsed from and printed as the CIF `x,y,z` grammar. The parser is reference commit 7448401b's -// `CIFImport.parseSymmetryOperation` (registration section 3.3), with one addition: a rotation coefficient that +// `CIFImport.parseSymmetryOperation`, with one addition: a rotation coefficient that // is not an integer is refused, since no space-group operation has one in any setting. // diff --git a/Sources/DiffractoCore/UnitCell.swift b/Sources/DiffractoCore/UnitCell.swift index 51bbd03..8990fa8 100644 --- a/Sources/DiffractoCore/UnitCell.swift +++ b/Sources/DiffractoCore/UnitCell.swift @@ -3,8 +3,7 @@ // Role: a, b, c, α, β, γ and everything the metric alone determines: the real and reciprocal lattices, the metric // tensors, the volume, d(hkl) and g(hkl), the crystal family of the metric at a stated tolerance. Ported from // reference commit 7448401b `Crystal.swift` (the py4DSTEM / VESTA convention: a along x, b in the xy plane; reciprocal -// vectors without 2π, |g| in Å⁻¹, d = 1/|g| in Å) and generalised to the seven systems; registration -// docs/registration-slice-1.md sections 3.1 and 3.2. +// vectors without 2π, |g| in Å⁻¹, d = 1/|g| in Å) and generalised to the seven systems. // import Foundation @@ -146,14 +145,14 @@ public struct UnitCell: Sendable, Equatable { out.append(.invalidAngles) } // A cell flatter than V = 1e-6 · a b c is coplanar up to rounding (α = β = γ = 120° gives V ≈ 3e-6 ų for 5 Å - // edges, and structure factors of 1e6): refused (Gate B on slice 2, 2026-10-15). + // edges, and structure factors of 1e6): refused. if !volume.isFinite || volume <= 0 || volume < 1e-6 * a * b * c { out.append(.degenerateVolume) } return out } public var isValid: Bool { issues.isEmpty } - // MARK: The crystal family of the metric (registration 3.2) + // MARK: The crystal family of the metric public enum Axis: Int, Sendable, Equatable, CaseIterable { case a = 0, b = 1, c = 2 @@ -185,7 +184,7 @@ public struct UnitCell: Sendable, Equatable { let ra = angle(alphaDeg, 90), rb = angle(betaDeg, 90), rc = angle(gammaDeg, 90) if ab && ac && ra && rb && rc { return .cubic } if ab && ra && rb && angle(gammaDeg, 120) { return .hexagonal } - // Tetragonal about whichever axis is the odd one (registration 0.0.4 section 6): a = c ≠ b and b = c ≠ a were + // Tetragonal about whichever axis is the odd one: a = c ≠ b and b = c ≠ a were // read orthorhombic, so the 4-fold was never tested. The hexagonal 6-fold along a or b is not handled here. if ab && !ac && ra && rb && rc { return .tetragonal(uniqueAxis: .c) } if ac && !ab && ra && rb && rc { return .tetragonal(uniqueAxis: .b) } From 82d35c383bed1fe5d81716025b96e44e3db61e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:40:36 +0000 Subject: [PATCH 02/17] docs(scattering): drop references to documents outside the repo Comment-only change in DiffractoScattering: registration sections and review rounds removed, "slice 1" named as DiffractoCore. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .../ElectronScatteringFactors.swift | 4 +-- .../StructureFactors.swift | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/DiffractoScattering/ElectronScatteringFactors.swift b/Sources/DiffractoScattering/ElectronScatteringFactors.swift index 4c3280c..681bc9f 100644 --- a/Sources/DiffractoScattering/ElectronScatteringFactors.swift +++ b/Sources/DiffractoScattering/ElectronScatteringFactors.swift @@ -1,8 +1,8 @@ // // ElectronScatteringFactors.swift // Role: electron scattering factors f_e(Z, g²) by the Lobato & Van Dyck parameterisation (Acta Cryst. A70, 636–649, -// 2014), the table py4DSTEM ships (`py4DSTEM/process/utils/scattering_factors.txt` at f050d207, sha256 be845d1d…; -// registration docs/registration-slice-2.md §2, §3.1). The 103 rows are reference commit 7d6fdbd1's +// 2014), the table py4DSTEM ships (`py4DSTEM/process/utils/scattering_factors.txt` at f050d207, sha256 be845d1d…). +// The 103 rows are reference commit 7d6fdbd1's // `ScatteringFactors.swift` table, carried by a script, not by hand; a test holds every parameter to the fetched // file bit for bit. // diff --git a/Sources/DiffractoScattering/StructureFactors.swift b/Sources/DiffractoScattering/StructureFactors.swift index 7c98317..d8afcdd 100644 --- a/Sources/DiffractoScattering/StructureFactors.swift +++ b/Sources/DiffractoScattering/StructureFactors.swift @@ -1,10 +1,10 @@ // // StructureFactors.swift // Role: kinematic structure factors F(hkl) = (1/V) Σ_sites f(Z, |g|²) · occ · exp(2πi (h x + k y + l z)) in Å⁻² (f in Å -// over V in ų; py4DSTEM's and the reference implementations' "A" label names f's unit, not F's: Gate B, 2026-10-15), and the -// reflection list after the |F| cut with its rings (registration docs/registration-slice-2.md §1, §3.2, §3.3). +// over V in ų; py4DSTEM's and the reference implementations' "A" label names f's unit, not F's), and the +// reflection list after the |F| cut with its rings. // The loop is reference commit 7d6fdbd1's `Crystal.reflections(kMax:tolerance:)`, in the same order of operations, so -// the numbers can be bit identical to the reference implementations'; the enumeration and the ring rule are slice 1's. +// the numbers can be bit identical to the reference implementations'; the enumeration and the ring rule are DiffractoCore's. // import Foundation @@ -36,7 +36,7 @@ public struct StructureFactor: Sendable, Equatable { public enum StructureFactors { /// The reference implementations' |F| cut-off (Å⁻²): a reflection is kept when |F| > cut. A fixed number against a quantity that scales as - /// 1/V: a large cell keeps fewer reflections at the same cut (registration slice 2 §7). + /// 1/V: a large cell keeps fewer reflections at the same cut. public static let defaultCut = 1e-4 /// F(hkl) for `structure`, with the electron form factor unless another is given. nil when a site's element has @@ -70,7 +70,7 @@ public enum StructureFactors { public enum KinematicError: Error, Equatable, Sendable, CustomStringConvertible { /// Atomic numbers the form factor has no entry for; checked before any F is computed. case unsupportedElements([Int]) - /// Slice 1's refusal of the reflection list (k_max, or a cell too large or invalid). + /// DiffractoCore's refusal of the reflection list (k_max, or a cell too large or invalid). case reflectionList(ReflectionListError) /// The cut is not a finite number ≥ 0. case invalidCut(Double) @@ -95,18 +95,18 @@ public enum KinematicError: Error, Equatable, Sendable, CustomStringConvertible } } -/// `localizedDescription` (what an app shows) is the same text as `description` (registration 0.0.4 section 5). +/// `localizedDescription` (what an app shows) is the same text as `description`. extension KinematicError: LocalizedError { public var errorDescription: String? { description } } -/// A reflection of slice 1's list with its structure factor. +/// A reflection of DiffractoCore's list with its structure factor. public struct KinematicReflection: Sendable, Equatable { public let reflection: Reflection public let structureFactor: StructureFactor } -/// A ring after the cut: slice 1's family of the kept reflections, with Σ|F|² over its members. +/// A ring after the cut: DiffractoCore's family of the kept reflections, with Σ|F|² over its members. public struct KinematicFamily: Sendable, Equatable { public let family: ReflectionFamily /// Σ|F|² over the members (Å⁻⁴). @@ -117,23 +117,23 @@ public struct KinematicFamily: Sendable, Equatable { public struct KinematicReflectionList: Sendable, Equatable { public let kMax: Double public let cut: Double - /// Every reflection of slice 1's list (allowed or absent), in its order, with F. EMPTY when the list was built with + /// Every reflection of DiffractoCore's list (allowed or absent), in its order, with F. EMPTY when the list was built with /// `retainingAll: false` (`CrystalStructure.kinematicReflections`): `kept` and `families` are then the same values. public let all: [KinematicReflection] /// |F| > cut, in the same order. public let kept: [KinematicReflection] - /// Slice 1's ring rule on `kept`. + /// DiffractoCore's ring rule on `kept`. public let families: [KinematicFamily] /// Kept reflections that the detected space-group operations forbid: their |F| is non-zero only because the sites' /// written coordinates break the symmetry slightly (⅓ written as 0.333 gives |F| up to 3e-4 in magnesium). The reference implementations - /// keep them by |F| alone and so does the list; this names them so a caller can tell (Gate B F4, 2026-10-15). + /// keep them by |F| alone and so does the list; this names them so a caller can tell. public var keptButOperationAbsent: [KinematicReflection] { kept.filter { !$0.reflection.isAllowed } } } extension CrystalStructure { - /// The kinematic list (registration slice 2 §1): slice 1's reflections, F by `formFactor`, the cut, the rings. + /// The kinematic list: DiffractoCore's reflections, F by `formFactor`, the cut, the rings. /// /// **Memory (`retainingAll`).** By default the list keeps every reflection before the cut in `all` (and the Core list /// it came from is held while it is built): a 30 Å cell at k_max 2.5 Å⁻¹ peaks at some hundreds of megabytes, and the @@ -165,7 +165,7 @@ extension CrystalStructure { guard let f = StructureFactors.compute(r.hkl, g: r.g, sites: sites, volume: cell.volume, formFactor: formFactor) else { - // A form factor that has no value at this g² for some element: name the elements (Gate B F3). + // A form factor that has no value at this g² for some element: name the elements. let gSq = r.gLength * r.gLength throw .unsupportedElements(Array(Set(sites.map(\.z).filter { formFactor($0, gSq) == nil })).sorted()) } From b99189ec5eeb45c7a02fec387da4370c48118803 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:41:57 +0000 Subject: [PATCH 03/17] docs(geometry): drop references to documents outside the repo Comment-only change in DiffractoGeometry: registration sections, review rounds, finding numbers and work-session names removed; "slice 1/2/3b" named as DiffractoCore, DiffractoScattering or the file that does it; "the owner" reworded. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .../DiffractionPattern.swift | 3 +- .../ElectronWavelength.swift | 6 ++-- .../EllipseCalibration+Policy.swift | 6 ++-- .../EllipseCalibration.swift | 36 +++++++++---------- .../DiffractoGeometry/EllipseTransform.swift | 2 +- .../DiffractoGeometry/IndexingDisplay.swift | 2 +- .../RingCalibration+Fit.swift | 9 +++-- .../DiffractoGeometry/RingCalibration+Q.swift | 7 ++-- .../DiffractoGeometry/RingCalibration.swift | 35 +++++++++--------- Sources/DiffractoGeometry/RingEvidence.swift | 10 +++--- Sources/DiffractoGeometry/RingIndexing.swift | 14 ++++---- Sources/DiffractoGeometry/RingOverlay.swift | 4 +-- Sources/DiffractoGeometry/ZoneAxis.swift | 11 +++--- 13 files changed, 69 insertions(+), 76 deletions(-) diff --git a/Sources/DiffractoGeometry/DiffractionPattern.swift b/Sources/DiffractoGeometry/DiffractionPattern.swift index b3d052d..0f7ef08 100644 --- a/Sources/DiffractoGeometry/DiffractionPattern.swift +++ b/Sources/DiffractoGeometry/DiffractionPattern.swift @@ -1,7 +1,6 @@ // // DiffractionPattern.swift -// Role: the three-field pattern the ring and ellipse fit reads: width, height and row-major Float32 pixels. Slice 3b of -// DiffractoKit (docs/registration-slice-3.md section 2). +// Role: the three-field pattern the ring and ellipse fit reads: width, height and row-major Float32 pixels. // // SEED: reference commit 7d6fdbd1 `Core/Data/DiffractionPattern.swift`, only the 3-field struct (its display helpers // `FloatImage`, `RGBAImage`, the log contrast and the -1 masked-pixel sentinel are presentation and stay in the reference implementations). The diff --git a/Sources/DiffractoGeometry/ElectronWavelength.swift b/Sources/DiffractoGeometry/ElectronWavelength.swift index a9e1c72..beadef0 100644 --- a/Sources/DiffractoGeometry/ElectronWavelength.swift +++ b/Sources/DiffractoGeometry/ElectronWavelength.swift @@ -1,7 +1,7 @@ // // ElectronWavelength.swift // Role: the one electron wavelength of the kit, the relativistic de Broglie wavelength at an accelerating voltage. -// Slice 3a (docs/registration-slice-3.md section 1). Ported from reference commit 9482831 `RingCalibration.wavelengthAngstroms` +// Ported from reference commit 9482831 `RingCalibration.wavelengthAngstroms` // with the same arithmetic order, so the results are bit identical. // // λ = hc / √(eV (2 m₀c² + eV)), V in volts, λ in Å. @@ -37,8 +37,8 @@ public enum ElectronWavelength { /// λ in Å at an accelerating voltage in VOLTS (200 kV is 200_000). NaN unless volts is finite and positive. public static func angstroms(volts: Double) -> Double { guard volts.isFinite, volts > 0 else { return .nan } - // Above about 1.3e154 V the product overflows and λ would read 0, a plausible-looking number (Gate B on slice - // 3a, F1): NaN instead. The arithmetic below the guard is the reference implementation's, unchanged. + // Above about 1.3e154 V the product overflows and λ would read 0, a plausible-looking number: + // NaN instead. The arithmetic below the guard is the reference implementation's, unchanged. let product = volts * (2 * electronRestEnergyEV + volts) guard product.isFinite else { return .nan } return planckTimesLightEVAngstroms / product.squareRoot() diff --git a/Sources/DiffractoGeometry/EllipseCalibration+Policy.swift b/Sources/DiffractoGeometry/EllipseCalibration+Policy.swift index 4f4f2f3..7153e4d 100644 --- a/Sources/DiffractoGeometry/EllipseCalibration+Policy.swift +++ b/Sources/DiffractoGeometry/EllipseCalibration+Policy.swift @@ -1,6 +1,6 @@ // // EllipseCalibration+Policy.swift -// Role: the choices the ellipse fit makes that physics does not fix (DiffractoKit 0.2.0, docs/registration-0.2.0.md §1): +// Role: the choices the ellipse fit makes that physics does not fix (DiffractoKit 0.2.0): // the azimuthal coverage rule, the residual caps, the one-ring ratio, the robust-refit cut, the ring-width floors // and the thresholds of the pre-fit checks. Each is a field with its documented default; `Policy.default` is the // set every earlier release used, and the fits run bit for bit as before with it. @@ -10,8 +10,8 @@ import Foundation extension EllipseCalibration { - /// The policy of the ellipse fit: every number below was chosen by the suite (measured on its fixtures and on the - /// owner's data, or derived and rounded), none is a physical constant. `Policy.default` is what the fits used before + /// The policy of the ellipse fit: every number below was chosen by the suite (measured on its fixtures and on + /// real data, or derived and rounded), none is a physical constant. `Policy.default` is what the fits used before /// 0.2.0, and a fit given `.default` is bit identical to one given no policy. /// /// An invalid policy (see `isValid`) is refused by every fit that takes it with `FitError.invalidInput`, and gives diff --git a/Sources/DiffractoGeometry/EllipseCalibration.swift b/Sources/DiffractoGeometry/EllipseCalibration.swift index 4443e12..3673e85 100644 --- a/Sources/DiffractoGeometry/EllipseCalibration.swift +++ b/Sources/DiffractoGeometry/EllipseCalibration.swift @@ -2,8 +2,7 @@ // EllipseCalibration.swift // Role: detector-ellipse calibration of a ring pattern, in detector PIXELS. The conic path matches py4DSTEM // `fit_ellipse_1D`; the profile path ports `fit_ellipse_amorphous_ring`'s 11-parameter central Gaussian + -// asymmetric ("Janus") Gaussian ring model; `fitBestAvailable` chains them. Slice 3b of DiffractoKit -// (docs/registration-slice-3.md section 2). +// asymmetric ("Janus") Gaussian ring model; `fitBestAvailable` chains them. // // SEED: reference commit 7d6fdbd1 `Core/Analysis/EllipseCalibration.swift` (853 lines), ported verbatim in arithmetic // (every constant, every boundary, operation order): coverage 36 / 30 / 12 azimuthal bins, residual caps 0.2 / 0.4 / 0.22, @@ -67,8 +66,7 @@ public struct EllipseCalibrationFit: Sendable, Equatable { public let sparseCoverage: Bool /// What the data say about the ring at this fit (`EllipseCalibration.ringEvidence`), attached by /// `fitBestAvailable(..., requireRingEvidence: true)` (and so by `RingCalibration.fit` and `fitRing`); nil from every - /// other call, including `fitAmorphousRing`, whose flag only governs the one-sided floor. DiffractoKit 0.1.0 - /// (registration-0.0.4 §1.3, §1.4). + /// other call, including `fitAmorphousRing`, whose flag only governs the one-sided floor. DiffractoKit 0.1.0. public let ringEvidence: RingEvidence? // Explicit so the memberwise initializer is `public` (synthesized ones are internal). @@ -160,14 +158,14 @@ public enum EllipseCalibration { sparseCoverage: fit.sparseCoverage, ringEvidence: evidence) } - // MARK: Impulses, scale and cancellation (DiffractoKit 0.1.0, registration-0.0.4 §1.1, §1.2, §4) + // MARK: Impulses, scale and cancellation (DiffractoKit 0.1.0) // // DEVIATION from the reference implementations (7d6fdbd1), on inputs their code refused or that hold an isolated impulse only: // - an annulus pixel outside the range of its finite in-pattern 8-neighbours (at least 3) by more than twice the - // annulus's robust range (P99.5 − P0.5, nearest rank) is skipped like a non-finite pixel (slice 3b's F3: one hot + // annulus's robust range (P99.5 − P0.5, nearest rank) is skipped like a non-finite pixel (one hot // pixel refused a perfect ring as insufficientAngularCoverage(1)); // - the signal test is relative (range above |maximum| × 1e-6 and at least the least normal double; the reference implementations' floor of - // 1 made it absolute: slice 3b's F6), and a range below 0.5 is brought into [0.5, 1) by an exact power of two + // 1 made it absolute), and a range below 0.5 is brought into [0.5, 1) by an exact power of two // before the optimisers run, so their absolute floors act on the normalised problem (amended 2026-10-15 from // "below 1 into [1, 2)": a unit ring's Float maximum lies just below 1, and that rule rescaled the parity rings). // A pattern without impulses and with a range of at least 0.5 runs the reference implementations' arithmetic unchanged (the parity gate). @@ -205,12 +203,12 @@ public enum EllipseCalibration { dynamicRange < 0.5 ? Double(sign: .plus, exponent: -dynamicRange.exponent - 1, significand: 1) : 1 } - /// The relative signal test (registration-0.0.4 §1.2). + /// The relative signal test. static func hasSignal(dynamicRange: Double, maximum: Double, relative: Double = 1e-6) -> Bool { dynamicRange.isFinite && dynamicRange >= .leastNormalMagnitude && dynamicRange > abs(maximum) * relative } - /// Throws `CancellationError` when the task is cancelled or the caller's closure says so (registration-0.0.4 §4). + /// Throws `CancellationError` when the task is cancelled or the caller's closure says so. static func checkCancellation(_ isCancelled: (@Sendable () -> Bool)?) throws { if Task.isCancelled || isCancelled?() == true { throw CancellationError() } } @@ -228,7 +226,7 @@ public enum EllipseCalibration { } } - /// width × height equals the pixel count, without an Int overflow trap (Gate B on slice 3b, F5: a width of + /// width × height equals the pixel count, without an Int overflow trap (a width of /// Int.max/2 + 1 trapped in the product). DEVIATION from the reference implementations: they multiply unchecked. static func patternSizeIsConsistent(_ pattern: DiffractionPattern) -> Bool { let (count, overflow) = pattern.height.multipliedReportingOverflow(by: pattern.width) @@ -236,7 +234,7 @@ public enum EllipseCalibration { } /// The outer radius is finite and at most twice the pattern's diagonal: beyond that no pixel is added, and an infinite - /// or huge radius trapped in `Int(...)` or allocated gigabytes of bins (Gate B on slice 3b, F4). DEVIATION from the + /// or huge radius trapped in `Int(...)` or allocated gigabytes of bins. DEVIATION from the /// apps, which check only outer > inner; no annulus inside the pattern is affected. static func outerRadiusIsSane(_ outerRadius: Double, _ pattern: DiffractionPattern) -> Bool { let diagonal = (Double(pattern.width) * Double(pattern.width) + Double(pattern.height) * Double(pattern.height)).squareRoot() @@ -262,7 +260,7 @@ public enum EllipseCalibration { } /// The annulus pixels (inner < r ≤ outer when `includeOuter`, else inner < r < outer) about the start centre, row - /// order, finite and not an isolated impulse (registration-0.0.4 §1.1), with the impulse threshold used. + /// order, finite and not an isolated impulse, with the impulse threshold used. static func annulusPixels(_ pattern: DiffractionPattern, centerQX: Double, centerQY: Double, innerRadius: Double, outerRadius: Double, includeOuter: Bool, impulseFactor: Double = 2) @@ -321,7 +319,7 @@ public enum EllipseCalibration { guard hasSignal(dynamicRange: dynamicRange, maximum: maximum, relative: policy.minimumRelativeSignal) else { throw FitError.insufficientSignal } - // A range below 0.5 is brought into [0.5, 1) by an exact power of two (registration-0.0.4 §1.2): the conic's + // A range below 0.5 is brought into [0.5, 1) by an exact power of two: the conic's // geometry and residual are invariant, and its absolute floors then act on the normalised problem. let scale = normalisingScale(dynamicRange) if scale != 1 { @@ -344,7 +342,7 @@ public enum EllipseCalibration { // // WHY FIVE SIXTHS AND NOT A THIRD (2026-09-14). A third was written to // stop "a plausible conic from four peaks", and it does. It does not - // stop one from several GRAINS: on the demo cube the owner's fit + // stop one from several GRAINS: on the demo cube one fit // reported a = 43.68, b = 39.72 — 10 % distortion — on a detector that // is isotropic by construction, because grains at different radii in // different azimuthal sectors let one ellipse thread through them. @@ -367,7 +365,7 @@ public enum EllipseCalibration { // distortion measured from the arrangement of three grains can no // longer reach a strain map. // - // THE ANYWAY PATH (2026-09-15). The owner decided a caller may assert, + // THE ANYWAY PATH (2026-09-15). A caller may assert, // per annulus, "this holds exactly one ring" (`acceptSparseCoverage`) // and get the fit anyway, MARKED `sparseCoverage`. That re-admits the // legitimate sparse case the bound above costs, down to a restored @@ -581,7 +579,7 @@ public enum EllipseCalibration { acceptSparseCoverage: acceptSparseCoverage, requireRingEvidence: requireRingEvidence, span: span.sub(0, 0.97), isCancelled: isCancelled, policy: policy) guard requireRingEvidence else { span(1); return fit } - // DiffractoKit 0.1.0 (registration-0.0.4 §1.4): what the data say about the ring at the fit that is returned. + // DiffractoKit 0.1.0: what the data say about the ring at the fit that is returned. try checkCancellation(isCancelled) let evidence = ringEvidence(pattern: pattern, fit: fit, centerQX: centerQX, centerQY: centerQY, innerRadius: innerRadius, outerRadius: outerRadius, policy: policy.evidence, @@ -683,7 +681,7 @@ public enum EllipseCalibration { let annulus = annulusPixels(pattern, centerQX: centerQX, centerQY: centerQY, innerRadius: innerRadius, outerRadius: outerRadius, includeOuter: false, impulseFactor: policy.impulseRangeFactor) - // The global maximum skips isolated impulses too (registration-0.0.4 §1.1); a pixel is tested only when it would + // The global maximum skips isolated impulses too; a pixel is tested only when it would // raise the maximum, so a pattern without impulses gives the reference implementations' maximum. for (index, value) in pattern.pixels.enumerated() where value.isFinite { let v = Double(value) @@ -906,7 +904,7 @@ public enum EllipseCalibration { // A ring sigma within 0.05 px of its 0.25 px floor is a fit that drained the ring into // the background and the central Gaussian: a, b and the centre then // carry no ring information (`archive/v5/ring-fit-two-start-gateD-2026-10-14.md`). - // DiffractoKit 0.1.0 (registration-0.0.4 §1.5, slice 3b's F7): with `requireRingEvidence`, ONE sigma at the + // DiffractoKit 0.1.0: with `requireRingEvidence`, ONE sigma at the // floor is a sharp edge when the data hold the ring at this fit (its ring evidence passes); both at the // floor is still refused. let floorBound = policy.ringWidthFloor + policy.ringWidthFloorTolerance @@ -919,7 +917,7 @@ public enum EllipseCalibration { /// The fit from refined parameters. A fit with one ring sigma at the floor (only with `requireRingEvidence`) is /// kept when its ring evidence passes, after one more refinement with that floor lowered to 0.01 px, so a ring - /// edge sharper than 0.25 px does not bias the radius (registration-0.0.4 §1.5, amended). + /// edge sharper than 0.25 px does not bias the radius. func accept(_ refined: [Double], residual refinedResidual: Double, oneSidedFloor: Bool, progress: ProgressSpan) throws -> EllipseCalibrationFit { var parameters = refined, residual = refinedResidual diff --git a/Sources/DiffractoGeometry/EllipseTransform.swift b/Sources/DiffractoGeometry/EllipseTransform.swift index 3ae4c90..77c5e8d 100644 --- a/Sources/DiffractoGeometry/EllipseTransform.swift +++ b/Sources/DiffractoGeometry/EllipseTransform.swift @@ -1,7 +1,7 @@ // // EllipseTransform.swift // Role: the detector-distortion correction a fitted ellipse (a, b, theta) defines, applied to a detector offset, and its -// exact inverse. Slice 3b of DiffractoKit (docs/registration-slice-3.md section 2). +// exact inverse. // // SEED: reference commit 7d6fdbd1 `Core/Data/Calibration.swift`, `hasEllipse`, `acceptedEllipse`, `ellipseTransform`, // `ellipseCorrectedOffset` and `ellipseUncorrectedOffset` only (about 50 lines; none of that type's provenance machinery). diff --git a/Sources/DiffractoGeometry/IndexingDisplay.swift b/Sources/DiffractoGeometry/IndexingDisplay.swift index 790b93d..69a9314 100644 --- a/Sources/DiffractoGeometry/IndexingDisplay.swift +++ b/Sources/DiffractoGeometry/IndexingDisplay.swift @@ -1,7 +1,7 @@ // // IndexingDisplay.swift // Ported from macTEM TEMCore/Diffraction/IndexingDisplay.swift (the app's source is the oracle; pins in P23). -// Role: a ring's indexing AS THE ROOM DISPLAYS IT (slice 1a, R6): the kit's `RingIndexing` decision over a kinematic list, with +// Role: a ring's indexing AS THE ROOM DISPLAYS IT: the kit's `RingIndexing` decision over a kinematic list, with // the numbers a reader sees derived once, here, from the kit's own fields. The decision is the kit's, untouched; this file // adds the percentages, the odds, σ_q from the typed tolerance and the verdict's name. // diff --git a/Sources/DiffractoGeometry/RingCalibration+Fit.swift b/Sources/DiffractoGeometry/RingCalibration+Fit.swift index 995c9b9..fd81f14 100644 --- a/Sources/DiffractoGeometry/RingCalibration+Fit.swift +++ b/Sources/DiffractoGeometry/RingCalibration+Fit.swift @@ -1,13 +1,12 @@ // // RingCalibration+Fit.swift // Role: the fit-driven calibration: a ring of known d fitted in a pattern, the fit's numbers checked against every -// registered refusal, and Δq and the camera length computed from the fitted radius only when nothing refuses them. -// Slice 3b of DiffractoKit (docs/registration-slice-3.md section 2). +// refusal, and Δq and the camera length computed from the fitted radius only when nothing refuses them. // // SEED: reference commit 9482831 `Sources/TEMCore/Analysis/RingCalibration+Fit.swift`, logic unchanged. The one difference is the // fit it runs: the reference implementation's own `EllipseCalibration` copy there is the pre-ADR-069 file; this runs the kit's // `EllipseCalibration` (reference commit 7d6fdbd1, with ADR 069's two starts and the 0.30 px ring-width floor), so the numbers -// the reference implementation registered on its copy move when it imports the kit (stated in the consumers note). +// the reference implementation measured on its copy move when it imports the kit. // // THE PATH: `EllipseCalibration.fitBestAvailable` (the conic `fit1D` seeds py4DSTEM's 11-parameter asymmetric-Gaussian ring // and falls back to the conic when the profile does not fit), with the coverage bound held (`acceptSparseCoverage: @@ -22,10 +21,10 @@ // ring was fitted at all, and reaches the caller as thrown. An impossible d, V or pixel pitch is the caller's mistake // and throws `Refusal.invalidInput` before any fit runs. // -// THE SPLIT (DiffractoKit 0.1.0, registration-0.0.4 §2, the reference implementation's ask G1): `fitRing` fits the ring and decides the fit's +// THE SPLIT (DiffractoKit 0.1.0): `fitRing` fits the ring and decides the fit's // own refusals with no spacing, so a caller can measure a ring before it knows which family it is; `calibrating` adds d, V // and the pitch. `fit` is exactly `calibrating(fitRing(...), d:...)`. Since 0.1.0 the fit runs with -// `requireRingEvidence: true` (registration-0.0.4 §1.4): the report carries the ring evidence and the annulus's radii, so +// `requireRingEvidence: true`: the report carries the ring evidence and the annulus's radii, so // a ring outside its annulus, a profile that is not a peak, an inconsistent geometry and too few sectors holding the ring // are refused by name, and one ring sigma at the 0.30 px floor is kept when the evidence passes. // diff --git a/Sources/DiffractoGeometry/RingCalibration+Q.swift b/Sources/DiffractoGeometry/RingCalibration+Q.swift index 2934072..a6c55df 100644 --- a/Sources/DiffractoGeometry/RingCalibration+Q.swift +++ b/Sources/DiffractoGeometry/RingCalibration+Q.swift @@ -1,8 +1,7 @@ // // RingCalibration+Q.swift -// Role: the exact scattering vector q of a pixel radius on a flat detector, and its inverse. DiffractoKit 0.1.0 -// (docs/registration-0.0.4.md section 3, G2), added to the closed forms of `RingCalibration.swift` without touching -// them. +// Role: the exact scattering vector q of a pixel radius on a flat detector, and its inverse. DiffractoKit 0.1.0, +// added to the closed forms of `RingCalibration.swift` without touching them. // // THE FORMULAS (exact; nothing here is a small-angle form): // θ = ½ atan(R / L) the Bragg angle of a ring at radius R on a flat detector perpendicular to the beam @@ -26,7 +25,7 @@ extension RingCalibration { /// R may be as large as you like against L: 2θ stays below 180° and q below 2/λ, so there is no other limit. /// VALIDITY: against 60-digit arithmetic (tools/kit-0.0.4/q-pins.py, 100 cases, V 60 to 300 kV, L 500 px to 1e5 px, R 1 px /// to 2000 px) the worst relative error is 8.2e-16: atan2 and sin each round once and neither amplifies (the form has no - /// subtraction). The 1/d identity on slice 3a's 320 cases holds to 5.5e-16. The trig-free form + /// subtraction). The 1/d identity on the 320 ring-calibration pin cases holds to 5.5e-16. The trig-free form /// 2 sin(½ atan(R/L)) = (R/h) / √((1 + L/h)/2), h = hypot(R, L), was measured too and is no better (both 2.2e-16 in /// NumPy on the grid), so the atan2/sin form stays. public static func q(radius: Double, cameraLengthPixels: Double, wavelengthAngstroms: Double) -> Double { diff --git a/Sources/DiffractoGeometry/RingCalibration.swift b/Sources/DiffractoGeometry/RingCalibration.swift index db24ae4..983361e 100644 --- a/Sources/DiffractoGeometry/RingCalibration.swift +++ b/Sources/DiffractoGeometry/RingCalibration.swift @@ -1,9 +1,8 @@ // // RingCalibration.swift // Role: the calibration a diffraction ring of known d gives (Δq in 1/Å per pixel, and the camera length) as closed forms, -// and the decision of when a ring is NOT to be calibrated from. Slice 3a of DiffractoKit -// (docs/registration-slice-3.md); ported from reference commit 9482831 `RingCalibration.swift` with every formula, constant, -// NaN rule, refusal order and message kept. The fit that fills a `FitReport` is slice 3b, not here. +// and the decision of when a ring is NOT to be calibrated from. Ported from reference commit 9482831 `RingCalibration.swift` with every formula, constant, +// NaN rule, refusal order and message kept. The fit that fills a `FitReport` is in `RingCalibration+Fit.swift`. // // THE FORMULAS (exact; nothing here is a small-angle form): // λ = hc / √(eV (2 m₀c² + eV)) the relativistic electron wavelength, Å, V in volts (`ElectronWavelength`) @@ -19,7 +18,7 @@ // The closed forms are total: outside their domain they return NaN, never a plausible number. `calibrate` and // `refusals` check the domain first and say why, by name. // -// TWO REFUSALS beyond the six of the original registration (model, coverage, sigma, residual, ellipticity, high tension): +// TWO REFUSALS beyond the six of the reference implementation (model, coverage, sigma, residual, ellipticity, high tension): // `.braggLimit` refuses λ/2d ≥ 1/√2, i.e. 2θ ≥ 90° (2θ ≥ 90° makes tan 2θ negative, and a flat detector perpendicular // to the beam shows no ring beyond 90° anyway), and `.invalidInput` refuses a number that is not finite and positive // (a NaN passes every comparison). @@ -52,7 +51,7 @@ public struct RingCalibration: Sendable, Equatable { // MARK: Refusals - /// Why a ring is not calibrated from: the six registered refusals, and the two additions `.braggLimit` and + /// Why a ring is not calibrated from: the six refusals of the reference implementation, and the two additions `.braggLimit` and /// `.invalidInput` (see the header). Every case but `.noHighTension` and `.braggLimit` withholds Δq as well; those /// two withhold only L (Δq needs no wavelength). public enum Refusal: Error, Equatable, Sendable, LocalizedError { @@ -73,17 +72,17 @@ public struct RingCalibration: Sendable, Equatable { /// A number that is not finite and positive (or a count outside its range): the caller's mistake, not data. case invalidInput(String) /// The fitted radius (a + b)/2 lies outside the annulus the fit was given: the ring term fitted something other - /// than the ring the caller drew (DiffractoKit 0.1.0, registration-0.0.4 §1.4; slice 3b's F2, a central blob). + /// than the ring the caller drew (DiffractoKit 0.1.0; a central blob, say). case ringOutsideAnnulus(radius: Double, inner: Double, outer: Double) /// The data's profile about the fit does not fall by a quarter of the ring's amplitude on both sides: an edge or a - /// slope, not a ring (registration-0.0.4 §1.3; slice 3b's F2, a disk edge). nil where a side had no pixels. + /// slope, not a ring (a disk edge, say). nil where a side had no pixels. case notARingPeak(innerDrop: Double?, outerDrop: Double?) /// The sectors put the ring off the fitted centre or ellipse by more than 0.25 px and 4 standard errors: the fit - /// was pulled by where the intensity is, not by where the ring is (registration-0.0.4 §1.3; slice 3b's F1 gap - /// noise, F2 texture). + /// was pulled by where the intensity is, not by where the ring is (gap + /// noise, texture). case ringGeometryInconsistent(centreShift: Double, ellipseShift: Double) /// The annulus holds a second ring: the pooled profile rises again beyond the fitted ring by `height` of its - /// amplitude (registration-0.0.4 §8, after Gate B: a neighbouring ring pulled the fit onto itself or between the + /// amplitude (a neighbouring ring pulls the fit onto itself or between the /// two). Narrow the annulus to one ring. case secondRingInAnnulus(height: Double) @@ -153,7 +152,7 @@ public struct RingCalibration: Sendable, Equatable { public var errorDescription: String? { reason } } - /// The numbers a ring fit reports, as plain values (slice 3b fills them from a ring fit and the annulus + /// The numbers a ring fit reports, as plain values (`RingCalibration+Fit.swift` fills them from a ring fit and the annulus /// it was given; this file has no fit in it). public struct FitReport: Sendable, Equatable { public var model: EllipseCalibrationModel @@ -200,7 +199,7 @@ public struct RingCalibration: Sendable, Equatable { public var ellipticity: Double { max(semiAxisA, semiAxisB) / min(semiAxisA, semiAxisB) - 1 } } - // MARK: Registered bounds, now the fields of `Policy` (0.2.0) + // MARK: Bounds, now the fields of `Policy` (0.2.0) /// The calibration's refusal bounds: the suite's choices, with `Policy.default` the set every release before 0.2.0 /// used. `refusals` and `fitRefusals` under an invalid policy return `[.invalidInput]` alone. @@ -248,15 +247,15 @@ public struct RingCalibration: Sendable, Equatable { } /// The azimuthal bins the fit divides the annulus into, and the fewest that must hold signal (a gap above about - /// 60°). The same numbers as the ring fit's own (slice 3b holds them equal in a test). + /// 60°). The same numbers as the ring fit's own (a test holds them equal). public static let angularBinCount = 36 public static let minimumOccupiedAngularBins = 30 /// The profile fit's cap on the normalised residual. public static let maximumResidual = 0.22 /// Past a/b − 1 = 0.093, (a + b)/2 and √(ab) differ by more than 0.1 %. Derived, not measured: the root of - /// (2 + ε) / (2√(1 + ε)) − 1 = 1e-3 is ε = 0.0936 (1.0009e-3 there; 9.886e-4 at 0.093), registered rounded down. + /// (2 + ε) / (2√(1 + ε)) − 1 = 1e-3 is ε = 0.0936 (1.0009e-3 there; 9.886e-4 at 0.093), rounded down. public static let maximumEllipticity = 0.093 - /// sin 45° = 1/√2: λ/2d at or above it puts 2θ at or beyond 90° (an addition beyond the registration: see the header). + /// sin 45° = 1/√2: λ/2d at or above it puts 2θ at or beyond 90° (an addition beyond the reference implementation: see the header). public static let maximumLambdaOver2d = 0.5.squareRoot() // MARK: Constants (CODATA 2018, stated in the header) @@ -297,14 +296,14 @@ public struct RingCalibration: Sendable, Equatable { public static func cameraLengthPixels(radius: Double, twoTheta: Double) -> Double { guard radius.isFinite, radius > 0, twoTheta.isFinite, twoTheta > 0, twoTheta < .pi / 2 else { return .nan } let length = radius / tan(twoTheta) - return length.isFinite ? length : .nan // an overflow is NaN, never infinity (Gate B on slice 3a, F2) + return length.isFinite ? length : .nan // an overflow is NaN, never infinity } /// Δq = (1/d)/R, Å⁻¹ per pixel, for d in Å and R in pixels. NaN unless both are finite and positive. public static func qPerPixel(d: Double, radius: Double) -> Double { guard d.isFinite, d > 0, radius.isFinite, radius > 0 else { return .nan } let q = (1 / d) / radius - return q.isFinite ? q : .nan // a subnormal d or R overflows: NaN, never infinity (Gate B on slice 3a, F2) + return q.isFinite ? q : .nan // a subnormal d or R overflows: NaN, never infinity } /// L = p · L_px, millimetres, for a pixel pitch p in MILLIMETRES (a 14 µm pixel is 0.014). NaN unless p is finite @@ -349,7 +348,7 @@ public struct RingCalibration: Sendable, Equatable { lengthRefusal: nil) } - /// Every registered refusal that applies to a fit's numbers and the stated V (volts, nil when none), in the order + /// Every refusal that applies to a fit's numbers and the stated V (volts, nil when none), in the order /// model, coverage, sigma, residual, ellipticity, outside the annulus, not a peak, geometry (the last three only /// when the report carries the annulus's radii and the ring evidence, DiffractoKit 0.1.0), high tension; empty when /// none applies. A number that is not finite (or a count out of range) returns `[.invalidInput]` alone: a NaN would diff --git a/Sources/DiffractoGeometry/RingEvidence.swift b/Sources/DiffractoGeometry/RingEvidence.swift index bc07e0c..6cbfbf5 100644 --- a/Sources/DiffractoGeometry/RingEvidence.swift +++ b/Sources/DiffractoGeometry/RingEvidence.swift @@ -2,8 +2,8 @@ // RingEvidence.swift // Role: what the data say about a ring at a fitted ellipse, read sector by sector and without a model in the azimuth: // whether each of the 36 sectors holds the ring, whether the ring is a peak, and whether the sectors put it where -// the fit says. DiffractoKit 0.1.0 (docs/registration-0.0.4.md §1.3), the kit's answer to slice 3b's Gate B gaps F1 -// (noise in an angular gap counted as coverage) and F2 (a disk edge, a blob, a textured circle calibrated). +// the fit says. DiffractoKit 0.1.0, the kit's answer to two gaps of the fit: +// noise in an angular gap counted as coverage, and a disk edge, a blob, a textured circle calibrated. // // THE MEASUREMENT. Every finite, non-impulse annulus pixel (the annulus about the caller's start centre, as the fit read // it) gets its offset δ = ρR − R from the fitted ellipse (ρ the profile model's normalised elliptical radius, R = (a + b)/2) @@ -19,7 +19,7 @@ // - The geometry: over the sectors with evidence, t = c0 + c1 cos φ + s1 sin φ + c2 cos 2φ + s2 sin 2φ weighted by // 1/(SE(t)² + 0.01²), its covariance scaled by max(1, χ²/(n − 5)); the centre shift √(c1² + s1²) and the ellipse shift // √(c2² + s2²) each fail beyond 0.25 px AND 4 standard errors. -// - One ring (added after Gate B on the 0.1.0 candidate, F1: a neighbouring ring inside the annulus was fitted, or fitted between the +// - One ring (added before 0.1.0: a neighbouring ring inside the annulus was fitted, or fitted between the // two): the profile pooled over the WHOLE annulus in 0.5 px bins of δ, smoothed over 3 bins, must not hold a second // peak more than 1 px from the fitted radius: a bin that rises above the lowest bin between it and the ring AND above the lowest bin // between it and the annulus's edge, by 0.25 A or more on both sides. A tail rising to the edge (a central beam) is @@ -74,9 +74,9 @@ public struct RingEvidence: Sendable, Equatable { self.ellipseShiftError = ellipseShiftError } - // MARK: Registered bounds (registration-0.0.4 §1.3), now the fields of `Policy` (0.2.0) + // MARK: Bounds, now the fields of `Policy` (0.2.0) - /// The bounds the ring evidence is measured and judged by (registration-0.0.4 §1.3): the suite's choices, with + /// The bounds the ring evidence is measured and judged by: the suite's choices, with /// `Policy.default` the set every release before 0.2.0 used. An invalid policy (`isValid` false) gives empty evidence /// from `EllipseCalibration.ringEvidence` and `false` from every judgement below. public struct Policy: Sendable, Equatable { diff --git a/Sources/DiffractoGeometry/RingIndexing.swift b/Sources/DiffractoGeometry/RingIndexing.swift index 5742f7c..832dc84 100644 --- a/Sources/DiffractoGeometry/RingIndexing.swift +++ b/Sources/DiffractoGeometry/RingIndexing.swift @@ -1,9 +1,9 @@ // // RingIndexing.swift -// Role: the (hkl) family a measured |q| belongs to, against a ring list (slice 2's `KinematicFamily`, or slice 1's +// Role: the (hkl) family a measured |q| belongs to, against a ring list (DiffractoScattering's `KinematicFamily`, or DiffractoCore's // `ReflectionFamily`), and the decision of when it is NOT to be named. The decision is reference commit 9482831 -// `DSpacingIndexing.index` (ADR 022, S2c ADR 024), generalised: the list is slice 1's family rule over any -// crystal system (registration docs/registration-slice-3.md §1, "RingIndexing"), so the lattice, orbit and +// `DSpacingIndexing.index` (ADR 022, S2c ADR 024), generalised: the list is DiffractoCore's family rule over any +// crystal system ("RingIndexing"), so the lattice, orbit and // family-construction code of the seed is not ported and `unsupportedLattice` / `unsupportedElement` cannot occur // here (the list builder refuses an element without a form factor, and every system has a Laue group). // @@ -13,7 +13,7 @@ // gives a confident wrong index. k is a Gaussian coverage factor (default 3). `kMax` is the k_max the list was built to // (Å⁻¹): the list is complete only below it, which is what `beyondKMax` guards. // -// THE DEFINITION (registered, docs/registration-slice-3.md §1; tools/slice-3/index-pins.py is written from the same text): +// THE DEFINITION (tools/slice-3/index-pins.py is written from the same text): // z, δ z = |q − g| / σ_q; the signed deviation δ = (d_meas − d)/d = g/q − 1; |Δd|/d = |δ|. Candidates order by // (z, g). // margin between two candidates, their log-likelihood ratio (z₂² − z₁²)/2; accepting needs ≥ ln 19 (the next at @@ -142,10 +142,10 @@ public enum RingIndexing { public var errorDescription: String? { reason } } - /// The family `q` (Å⁻¹) belongs to among the kinematic rings (slice 2's list after the |F| cut), given its standard + /// The family `q` (Å⁻¹) belongs to among the kinematic rings (DiffractoScattering's list after the |F| cut), given its standard /// uncertainty `sigma` (Å⁻¹), the k_max the list was built to and the coverage factor k; or the first refusal that /// applies (the header's order). - /// The decision against a kinematic list, with the k_max it was built to (Gate B on slice 3a, F7: a free k_max larger + /// The decision against a kinematic list, with the k_max it was built to (a free k_max larger /// than the list's turns the beyondKMax guard off; this overload cannot be given one). public static func index(q: Double, sigma: Double, in list: KinematicReflectionList, coverage k: Double = defaultCoverage, @@ -159,7 +159,7 @@ public enum RingIndexing { try index(q: q, sigma: sigma, in: families.map(\.family), kMax: kMax, coverage: k, minimumMargin: margin) } - /// The same over slice 1's rings (any list of families). + /// The same over DiffractoCore's rings (any list of families). public static func index(q: Double, sigma: Double, in families: [ReflectionFamily], kMax: Double, coverage k: Double = defaultCoverage, minimumMargin margin: Double = minimumMargin) throws(Refusal) -> Assignment { diff --git a/Sources/DiffractoGeometry/RingOverlay.swift b/Sources/DiffractoGeometry/RingOverlay.swift index 01d0b7a..741a80d 100644 --- a/Sources/DiffractoGeometry/RingOverlay.swift +++ b/Sources/DiffractoGeometry/RingOverlay.swift @@ -1,6 +1,6 @@ // // RingOverlay.swift -// Ported from macTEM `TEMCore/Diffraction/RingOverlay.swift` (the app's source is the oracle; pins in P22). The ring geometry on the image (slice 1a, R1 and R4): a fitted ring as an ellipse in IMAGE +// Ported from macTEM `TEMCore/Diffraction/RingOverlay.swift` (the app's source is the oracle; pins in P22). The ring geometry on the image: a fitted ring as an ellipse in IMAGE // coordinates (x = column, y = row), the polyline the room draws, the predicted ring of another family scaled from the // calibrating ring, and the distance of a point to an ellipse (the nearest-ring hit, and the harness's own check). // @@ -57,7 +57,7 @@ public enum RingOverlay { } /// The ellipse of a ring of another family, predicted from the calibrating ring: the same centre, b/a and angle, both - /// semi-axes scaled by `radius / ring.radius` (R4). nil unless every number is finite and positive. + /// semi-axes scaled by `radius / ring.radius`. nil unless every number is finite and positive. public static func predicted(from ring: RingEllipse, radius: Double) -> RingEllipse? { guard radius.isFinite, radius > 0, ring.radius.isFinite, ring.radius > 0 else { return nil } let scale = radius / ring.radius diff --git a/Sources/DiffractoGeometry/ZoneAxis.swift b/Sources/DiffractoGeometry/ZoneAxis.swift index 58943d0..3614137 100644 --- a/Sources/DiffractoGeometry/ZoneAxis.swift +++ b/Sources/DiffractoGeometry/ZoneAxis.swift @@ -1,6 +1,6 @@ // // ZoneAxis.swift -// Role: zone-axis primitives (registration docs/registration-slice-3.md §1, "ZoneAxis"): the 49 low-index beam +// Role: zone-axis primitives ("ZoneAxis"): the 49 low-index beam // directions, a direction [u v w] as a Cartesian unit vector of a cell, the detector basis of a beam direction, // and the projection of a reflection list onto it with the excitation error. Seeds, read at reference commit 7d6fdbd1 // by `git show`: `PhaseReferenceLibrary.lowIndexZoneAxes` and `cartesianZoneAxis`, `ACOMOrientation.detectorBasis` @@ -53,8 +53,7 @@ public struct ZoneAxis: Sendable, Hashable, CustomStringConvertible { /// The detector basis of a unit beam direction `n`: columns e₁, e₂, n, orthonormal and right-handed. The reference is /// x unless |n.x| ≥ 0.9, then y; e₁ = normalize(n × reference), e₂ = n × e₁ (reference `ACOMOrientation.detectorBasis`). - /// `n` must be a unit vector (`cartesian(in:)` returns one); `isUnitDirection` says whether it is (Gate B on slice 3a, - /// F4: a non-unit n gave a non-orthonormal or NaN basis silently). + /// `n` must be a unit vector (`cartesian(in:)` returns one); `isUnitDirection` says whether it is (a non-unit n gave a non-orthonormal or NaN basis silently). public static func detectorBasis(_ n: SIMD3) -> simd_double3x3 { let reference: SIMD3 = abs(n.x) < 0.9 ? [1, 0, 0] : [0, 1, 0] let e1 = simd_normalize(simd_cross(n, reference)) @@ -89,12 +88,12 @@ public struct ZoneAxis: Sendable, Hashable, CustomStringConvertible { /// /// DEVIATION (overall sign) from py4DSTEM f050d207, `Crystal.excitation_errors`: it writes s_g = (2 g_z − λ|g|²) / /// (2 − 2λ g_z) with k₀ = [0, 0, −1/λ], so its lab z is −n and its s_g = −s_g(here) / (1 + λ g · n): the whole - /// expression has the opposite sign, both terms flip together, the zero locus is the same (Gate B on slice 3a, F6 - /// corrected the earlier wording, which said only the curvature term flips). The denominator is not applied: s_g here + /// expression has the opposite sign, both terms flip together, the zero locus is the same + /// (not only the curvature term). The denominator is not applied: s_g here /// is the first-order form (the factor differs from 1 by under 1e-3 near the sphere). /// /// A reflection is kept when |s_g| ≤ `sgMax` (a non-finite s_g is never kept). - /// Nothing is kept for a non-unit or non-finite `n`, or a wavelength that is not finite and ≥ 0 (Gate B F4, F5). + /// Nothing is kept for a non-unit or non-finite `n`, or a wavelength that is not finite and ≥ 0. public static func project(_ reflections: [KinematicReflection], onto n: SIMD3, wavelengthAngstroms: Double? = nil, sgMax: Double) -> [Projected] { guard isUnitDirection(n) else { return [] } From 67a7c3ef9adb961680684a7e6180c76317434e8e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:42:09 +0000 Subject: [PATCH 04/17] docs(orientation): drop references to documents outside the repo Comment-only change in DiffractoOrientation: registration sections, roadmap task numbers and the study step removed. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- Sources/DiffractoOrientation/DiffractoOrientation.swift | 4 ++-- Sources/DiffractoOrientation/Misorientation.swift | 2 +- Sources/DiffractoOrientation/OrientationRelationship.swift | 2 +- Sources/DiffractoOrientation/OrientationResult.swift | 6 +++--- Sources/DiffractoOrientation/PhaseReferenceLibrary.swift | 5 ++--- Sources/DiffractoOrientation/PhaseVectorMatching.swift | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Sources/DiffractoOrientation/DiffractoOrientation.swift b/Sources/DiffractoOrientation/DiffractoOrientation.swift index fa05755..6f45f0d 100644 --- a/Sources/DiffractoOrientation/DiffractoOrientation.swift +++ b/Sources/DiffractoOrientation/DiffractoOrientation.swift @@ -1,6 +1,6 @@ // // DiffractoOrientation.swift -// Role: the module overview. DiffractoOrientation holds (registration docs/registration-slice-4.md): +// Role: the module overview. DiffractoOrientation holds: // // - `PhaseReferenceLibrary` and its value types (`PhaseDefinition`, `PhaseReferenceSettings`, `PhaseOrientationReference`, // `ReferenceVector`, `LatticeVector`, `OrientationRelationship`): for each phase, zone axis and in-plane rotation, the @@ -14,7 +14,7 @@ // - `ACOMTemplateLibrary` and `ACOMMatcher` (+ `ACOMTemplateSettings`, `ACOMExcitationModel`, `ACOMReflection`, `ACOMPeak`, // `ACOMError`): the apps' simplified polar-correlation ACOM route (docs/parity.md P25); not py4DSTEM's SO(3) correlogram. // -// py4DSTEM's SO(3) correlogram matcher and pyxem's template matching are not ported (slice 5); the ACOM route above is the apps' simplified one. CrystalOrientationSymmetry implements eleven-class minimum-angle reduction, +// py4DSTEM's SO(3) correlogram matcher and pyxem's template matching are not ported; the ACOM route above is the apps' simplified one. CrystalOrientationSymmetry implements eleven-class minimum-angle reduction, // native direction sampling, disorientation, the py4DSTEM direction families and explicit IPFColorKey conventions (native, py4DSTEM, orix); PhaseModel (validation, orientation class, Materials Project models) (docs/orientation-symmetry.md). // Depends on DiffractoCore, DiffractoScattering and ImagoKit's ImagoFFT (FFT1D, for the ring FFTs). // diff --git a/Sources/DiffractoOrientation/Misorientation.swift b/Sources/DiffractoOrientation/Misorientation.swift index 6f1c39f..86e3f81 100644 --- a/Sources/DiffractoOrientation/Misorientation.swift +++ b/Sources/DiffractoOrientation/Misorientation.swift @@ -2,7 +2,7 @@ // Misorientation.swift // Role: orix 0.15.0's `Misorientation((Gl, Gr))` (`equivalent`, `reduce` = `map_into_symmetry_reduced_zone`) and `Orientation` // (`angle_with`, `in_euler_fundamental_region`) on the kit's rotation matrices and `CrystalOrientationSymmetry` groups -// (docs/parity.md, ROADMAP task 35). Pins: Tests/DiffractoOrientationTests/Fixtures/orix-misorientation-pins.json. +// (docs/parity.md). Pins: Tests/DiffractoOrientationTests/Fixtures/orix-misorientation-pins.json. // // CONVENTIONS. A `Misorientation` carries a rotation matrix R that maps vectors of the SOURCE crystal to vectors of the TARGET crystal // (v_target = R v_source). Its symmetrically equivalent forms are g_t R g_s, g_t a proper rotation of the target group, g_s of the source diff --git a/Sources/DiffractoOrientation/OrientationRelationship.swift b/Sources/DiffractoOrientation/OrientationRelationship.swift index f5351fa..d07271c 100644 --- a/Sources/DiffractoOrientation/OrientationRelationship.swift +++ b/Sources/DiffractoOrientation/OrientationRelationship.swift @@ -1,6 +1,6 @@ // // OrientationRelationship.swift -// Role: the variants of an orientation relationship between a precipitate and its matrix (ROADMAP task 35, study step 6), for ACOM and +// Role: the variants of an orientation relationship between a precipitate and its matrix, for ACOM and // phase-map orientations: the matrix group enumerates the crystallographically equivalent placements of the precipitate, the // precipitate group identifies those that differ only by one of its own symmetry operations. beta'' (Mg5Si6, C2/m) in Al has // 24 / 2 = 12 variants, four needle variants for each of the three <001> axes. diff --git a/Sources/DiffractoOrientation/OrientationResult.swift b/Sources/DiffractoOrientation/OrientationResult.swift index ccffec4..69da369 100644 --- a/Sources/DiffractoOrientation/OrientationResult.swift +++ b/Sources/DiffractoOrientation/OrientationResult.swift @@ -2,7 +2,7 @@ // OrientationResult.swift // Role: the value types of an orientation result: `EulerAngles` (the py4DSTEM / SciPy Bunge convention, both directions), // `ZoneAxisFrame` (the detector frame of a zone axis and the orientation matrix built from it) and `OrientationResult` -// (one position's result). Registration docs/registration-slice-4.md, sections 1 and 4. The map and its presentation +// (one position's result). The map and its presentation // stay with the caller; symmetry primitives are in CrystalOrientationSymmetry.swift. // // `ZoneAxisFrame` is the apps' application-specific frame type under a library name; the renaming is the only change. @@ -93,8 +93,8 @@ public struct EulerAngles: Sendable, Equatable, Hashable { /// The detector frame of a zone axis and the py4DSTEM orientation matrix built from it. /// Columns are detector x, detector y, and beam z expressed in crystal axes, -/// matching py4DSTEM's `Orientation.matrix` convention (the frame rotation of the -/// registration, section 4: e1 = normalise(n × r), e2 = n × e1, r = x̂ when |n_x| < 0.9 else ŷ). +/// matching py4DSTEM's `Orientation.matrix` convention (the frame rotation +/// e1 = normalise(n × r), e2 = n × e1, r = x̂ when |n_x| < 0.9 else ŷ). public enum ZoneAxisFrame { public static func detectorBasis(zoneAxis n: SIMD3) -> simd_double3x3 { let reference: SIMD3 = abs(n.x) < 0.9 ? [1, 0, 0] : [0, 1, 0] diff --git a/Sources/DiffractoOrientation/PhaseReferenceLibrary.swift b/Sources/DiffractoOrientation/PhaseReferenceLibrary.swift index d4258a3..53a40c8 100644 --- a/Sources/DiffractoOrientation/PhaseReferenceLibrary.swift +++ b/Sources/DiffractoOrientation/PhaseReferenceLibrary.swift @@ -1,8 +1,7 @@ // // PhaseReferenceLibrary.swift // Role: the LIBRARY half of vector-matched phase mapping: for each phase, each sampled beam direction and each in-plane -// rotation, the set of reciprocal-lattice vectors that would appear in the detector plane (registration -// docs/registration-slice-4.md, section 1). +// rotation, the set of reciprocal-lattice vectors that would appear in the detector plane. // // Method: Thronsen, Hjelen, Holmestad et al., "Studying GPB zones and precipitates in Al-Cu-Li with scanning precession // electron diffraction", Ultramicroscopy 255 (2024) 113861, CC BY 4.0 (attribution in NOTICE). The method is @@ -475,7 +474,7 @@ public struct PhaseReferenceLibrary: Sendable { /// of `structure` with 0 < |g| ≤ `kMaxInvAngstrom` and |F| above the default cut (`StructureFactors.defaultCut`), /// `g` Cartesian in Å⁻¹ without 2π and `structureFactor.intensity` = |F|². /// - /// THE ORDER (registration section 2.1). The density cap's sort and the final length sort break ties on input order, + /// THE ORDER. The density cap's sort and the final length sort break ties on input order, /// so the input order is part of the definition. The original reflection list was enumerated h, k, l ascending /// lexicographically, filtered, then sorted by increasing |g| with a stable sort: equal |g| (to the last bit) kept /// the enumeration order, so ties are ordered by (h, k, l) ASCENDING. The kit's own list orders ties within 1e-12 diff --git a/Sources/DiffractoOrientation/PhaseVectorMatching.swift b/Sources/DiffractoOrientation/PhaseVectorMatching.swift index e9fdbde..2ebdc2c 100644 --- a/Sources/DiffractoOrientation/PhaseVectorMatching.swift +++ b/Sources/DiffractoOrientation/PhaseVectorMatching.swift @@ -2,7 +2,7 @@ // PhaseVectorMatching.swift // Role: the MATCHER half of vector-matched phase mapping: for each scan position, remove the matrix's reflections, score // what survives against every candidate entry of a `PhaseReferenceLibrary`, and return a phase label, a distance, and -// an explicit "not indexed" (registration docs/registration-slice-4.md, section 1). +// an explicit "not indexed". // // Method: Thronsen et al., Ultramicroscopy 255 (2024) 113861, CC BY 4.0 (attribution in NOTICE). The decision rule of // `.knownVariants` and its two constants (direct-matrix count 1, residual cutoff 0.07 Å⁻¹) are as the paper describes them; @@ -24,7 +24,7 @@ // // NOT SCORED AGAINST THE PUBLISHED GROUND TRUTH. Scoring this against the published ground truth of Thronsen et al. needs // a ~7.4 GB download and has not been run; the verdict rules have no reference implementation (py4DSTEM's phase tool is -// a different method), so every number this file produces is an output of the definitions in the registration, not a +// a different method), so every number this file produces is an output of the definitions written in this file, not a // measurement against an independent oracle. // // THE INPUT. A scan is `[[SIMD2]]`, one array of peak positions per scan position, row-major (`scanWidth` × From e9f45de1dd456c8333421619094ffc31776e0a32 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:43:45 +0000 Subject: [PATCH 05/17] test(core): drop references to documents outside the repo Comment, print and test-name text in DiffractoCoreTests: registration sections, review rounds, tiers and work-session names removed; a missing mutation script no longer cited; "registered" values renamed expected or documented. The D17 tolerance rule is now described in words. Also two leftover registration notes in DiffractoGeometry and macTEM's ADR numbers named as macTEM's. No assertion changes. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .../EllipseCalibration.swift | 2 +- Sources/DiffractoGeometry/RingIndexing.swift | 2 +- Tests/DiffractoCoreTests/CIFImportTests.swift | 16 ++++++++-------- .../ConventionalCellTests.swift | 4 ++-- .../ConventionalSymmetryTests.swift | 14 +++++++------- .../CrystalPresetsTests.swift | 2 +- Tests/DiffractoCoreTests/ErrorTextTests.swift | 4 ++-- .../FastDetectionTests.swift | 19 +++++++++---------- .../MaterialsProjectImportTests.swift | 2 +- .../PointSymmetryTests.swift | 16 ++++++++-------- Tests/DiffractoCoreTests/PolicyTests.swift | 12 ++++++------ Tests/DiffractoCoreTests/ReductionTests.swift | 6 +++--- .../RefuterRegressionTests.swift | 2 +- .../ShearReductionTests.swift | 2 +- Tests/DiffractoCoreTests/Slice1bSupport.swift | 4 ++-- .../SymmetryDetectionTests.swift | 2 +- .../SymmetryOperationTests.swift | 2 +- .../TetragonalSettingTests.swift | 2 +- .../ToleranceFixesTests.swift | 3 +-- Tests/DiffractoCoreTests/UnitCellTests.swift | 8 ++++---- 20 files changed, 61 insertions(+), 63 deletions(-) diff --git a/Sources/DiffractoGeometry/EllipseCalibration.swift b/Sources/DiffractoGeometry/EllipseCalibration.swift index 3673e85..d729f45 100644 --- a/Sources/DiffractoGeometry/EllipseCalibration.swift +++ b/Sources/DiffractoGeometry/EllipseCalibration.swift @@ -699,7 +699,7 @@ public enum EllipseCalibration { hasSignal(dynamicRange: dynamicRange, maximum: maximum, relative: policy.minimumRelativeSignal) else { throw FitError.insufficientSignal } - // Registration-0.0.4 §1.2: a range below 0.5 runs normalised by an exact power of two; I0, I1 and the background are + // A range below 0.5 runs normalised by an exact power of two; I0, I1 and the background are // scaled back at the end, so the fit of amplitude 2^-k A is the fit of amplitude A, bit for bit. let scale = normalisingScale(dynamicRange) if scale != 1 { diff --git a/Sources/DiffractoGeometry/RingIndexing.swift b/Sources/DiffractoGeometry/RingIndexing.swift index 832dc84..0a6289d 100644 --- a/Sources/DiffractoGeometry/RingIndexing.swift +++ b/Sources/DiffractoGeometry/RingIndexing.swift @@ -2,7 +2,7 @@ // RingIndexing.swift // Role: the (hkl) family a measured |q| belongs to, against a ring list (DiffractoScattering's `KinematicFamily`, or DiffractoCore's // `ReflectionFamily`), and the decision of when it is NOT to be named. The decision is reference commit 9482831 -// `DSpacingIndexing.index` (ADR 022, S2c ADR 024), generalised: the list is DiffractoCore's family rule over any +// `DSpacingIndexing.index` (macTEM ADR 022 and ADR 024), generalised: the list is DiffractoCore's family rule over any // crystal system ("RingIndexing"), so the lattice, orbit and // family-construction code of the seed is not ported and `unsupportedLattice` / `unsupportedElement` cannot occur // here (the list builder refuses an element without a form factor, and every system has a Laue group). diff --git a/Tests/DiffractoCoreTests/CIFImportTests.swift b/Tests/DiffractoCoreTests/CIFImportTests.swift index a87c87e..4af1dbd 100644 --- a/Tests/DiffractoCoreTests/CIFImportTests.swift +++ b/Tests/DiffractoCoreTests/CIFImportTests.swift @@ -1,6 +1,6 @@ // // CIFImportTests.swift -// Planted truth for the importer (registration 5.3, 5.4): cell contents the files' formulas state, Laue classes by +// Planted truth for the importer: cell contents the files' formulas state, Laue classes by // the structure, absences by the operations, the reference implementations' family counts and names, and the refusals. β″ first. // @@ -43,8 +43,8 @@ struct CIFImportTests { for r in list.reflections { #expect(r.isAllowed == ((r.hkl.h + r.hkl.k) % 2 == 0), "\(r.hkl)") } } - /// The registered predictions: 1448 allowed reflections and 419 families at k_max 1.2 Å⁻¹ (the reference implementation's S2c record: - /// gemmi's 1448; the reference implementations' 1442 is after the |F| cut, slice 2's), and the first ten families by name, d and + /// The predictions written before the code: 1448 allowed reflections and 419 families at k_max 1.2 Å⁻¹ (the reference implementation's + /// record: gemmi's 1448; the reference implementations' 1442 is after DiffractoScattering's |F| cut), and the first ten families by name, d and /// multiplicity. Mutants: the family tolerance at 1e-4 or 0; the multiplicity as the orbit count. @Test func testBetaDoublePrimePredictedListAndFamilies() throws { let s = try CIFImport.read(Fixtures.betaDoublePrimeCIF).structure @@ -113,7 +113,7 @@ struct CIFImportTests { /// One atom and the four centring translations: a legal loop that declares less than the atoms have (Laue -1 by /// the loop, m-3m by the cell it builds). The structure's operations are detected from the expanded cell - /// (amendment 2); the loop is kept and is a subset of them. + ///; the loop is kept and is a subset of them. @Test func testGoldWithCentringOperationsExpandsToFourSitesAndDetectsFm3m() throws { let doc = try read("gold_centring_ops.cif") let s = doc.structure @@ -128,7 +128,7 @@ struct CIFImportTests { } /// Silicon: Fd-3m; {2 0 0}, {4 2 0}, {6 0 0} absent by the operations; {2 2 2} and {4 4 2} allowed by them (their - /// structure factor is zero by the basis, slice 2's), so 12 allowed families at 1.2 where the reference implementations list 10. + /// structure factor is zero by the basis, removed by DiffractoScattering's cut), so 12 allowed families at 1.2 where the reference implementations list 10. @Test func testSiliconAbsencesAndFamilies() throws { let s = try read("silicon_Fd-3m.cif").structure #expect(s.sites.count == 8) @@ -156,7 +156,7 @@ struct CIFImportTests { #expect(s.laueClass == .sixOverMmm) #expect(!ReflectionList.isAllowed(MillerIndex(0, 0, 1), by: s.operations), "the 6₃ screw: 000l with l odd") // 25 symmetry-allowed rings: the reference implementation's 24 plus {3 0 -3 1}, allowed by P6₃/mmc's operations and zero by the basis - // (amendment 5; slice 2's |F| cut removes it, as Si's {2 2 2}). + // (DiffractoScattering's |F| cut removes it, as Si's {2 2 2}). let families = try s.reflections(kMax: 1.2).families() #expect(families.count == 25) #expect(families.contains { $0.name == "{3 0 -3 1}" }) @@ -226,7 +226,7 @@ struct CIFImportTests { let hcp = try read("ideal_hcp_P1.cif").structure #expect(hcp.laueClass == .sixOverMmm) let hcpFamilies = try hcp.reflections(kMax: 1.2).families() - #expect(hcpFamilies.count == 24, "23 after the |F| cut (slice 2)") + #expect(hcpFamilies.count == 24, "23 after the |F| cut") let ring = try #require(hcpFamilies.first { $0.orbits.count == 2 }) #expect(ring.name == "{3 0 -3 2} + {0 0 0 6}") #expect(ring.multiplicity == 14) @@ -240,7 +240,7 @@ struct CIFImportTests { #expect(seededPyrite.laueClass == .mThreeBar) } - // MARK: Refusals and precision (5.4) + // MARK: Refusals and precision static let goldP1 = """ data_gold diff --git a/Tests/DiffractoCoreTests/ConventionalCellTests.swift b/Tests/DiffractoCoreTests/ConventionalCellTests.swift index 51218d5..bf5c107 100644 --- a/Tests/DiffractoCoreTests/ConventionalCellTests.swift +++ b/Tests/DiffractoCoreTests/ConventionalCellTests.swift @@ -1,6 +1,6 @@ // // ConventionalCellTests.swift -// Slice 1b, the conventional cell (registration docs/registration-slice-1b.md sections 1 to 3): the Bravais lattice +// The conventional cell: the Bravais lattice // name against spglib's lattice-only dataset and against the lattice each random cell was BUILT as, the cell's volume // and lengths, the transformation, and closed-form cells of the cubic lattices. // @@ -82,7 +82,7 @@ struct ConventionalCellTests { default: break } } - print("slice 1b conventional cell: \(cases.count) cells, worst volume \(worstVolume), length \(worstLength), metric \(worstMetric)") + print("conventional cell: \(cases.count) cells, worst volume \(worstVolume), length \(worstLength), metric \(worstMetric)") } /// Closed forms: the primitive cells of the cubic lattices have the conventional cell edge a, F and I with 4 and 2 diff --git a/Tests/DiffractoCoreTests/ConventionalSymmetryTests.swift b/Tests/DiffractoCoreTests/ConventionalSymmetryTests.swift index ffcf8ac..8b2c2b3 100644 --- a/Tests/DiffractoCoreTests/ConventionalSymmetryTests.swift +++ b/Tests/DiffractoCoreTests/ConventionalSymmetryTests.swift @@ -1,6 +1,6 @@ // // ConventionalSymmetryTests.swift -// Slice 1b, the symmetry of any cell setting (registration docs/registration-slice-1b.md sections 1 to 3): the +// The symmetry of any cell setting: the // operations, Laue class, Bravais lattice and rings of every pinned structure in the file's setting and in the // conventional setting, against spglib; and the planted truth (primitive fcc, bcc and β″, rutile with its 4-fold along // a, magnesium with γ = 60°, corundum in rhombohedral axes, a skewed hexagonal lattice). @@ -75,7 +75,7 @@ struct ConventionalSymmetryTests { } } } - print("slice 1b rings: \(rings) rings compared in g and multiplicity; names compared \(namesCompared), skipped \(namesSkipped) (conventional cell differs from spglib's in parameters: \(differentCells))") + print("conventional rings: \(rings) rings compared in g and multiplicity; names compared \(namesCompared), skipped \(namesSkipped) (conventional cell differs from spglib's in parameters: \(differentCells))") #expect(namesCompared >= 1200) } @@ -101,7 +101,7 @@ struct ConventionalSymmetryTests { } } - // MARK: Planted truth (registration section 3) + // MARK: Planted truth @Test func testPrimitiveCellsOfFccBccAndBetaDoublePrimeDetectTheirConventionalSymmetry() throws { let au = Self.named("au_fcc_primitive") @@ -128,7 +128,7 @@ struct ConventionalSymmetryTests { #expect(fe.operations.count == 48 && feConv.operations.count == 96 && feConv.laueClass == .mThreeBarM && feConv.bravais == .cI) #expect(abs(feConv.cell.a - 2.8665) <= 1e-9 && feConv.sites.count == 2) - // β″: C-centred primitive cell, 2/m, 8 operations in the conventional cell, the registered 1448 and 419 + // β″: C-centred primitive cell, 2/m, 8 operations in the conventional cell, the predicted 1448 and 419 let beta = Self.named("beta_double_prime_primitive") let betaConv = try #require(beta.conventionalSetting) #expect(beta.operations.count == 4 && betaConv.operations.count == 8 && betaConv.laueClass == .twoOverM && betaConv.bravais == .mC) @@ -172,7 +172,7 @@ struct ConventionalSymmetryTests { let cFirstNameWords: Int? = cRings.first.map { (f: ReflectionFamily) -> Int in f.name.split(separator: " ").count } let cFirstName: String? = cRings.first?.name #expect(cFirstNameWords == 4, "Miller-Bravais names in the hexagonal axes: \(String(describing: cFirstName))") - // in the rhombohedral file cell the names keep three indices (slice 1, Gate B F4) + // in the rhombohedral file cell the names keep three indices let fileRings = try corundum.reflections(kMax: 1.2).families() let fileFirstNameWords: Int? = fileRings.first.map { (f: ReflectionFamily) -> Int in f.name.split(separator: " ").count } #expect(fileFirstNameWords == 3) @@ -186,7 +186,7 @@ struct ConventionalSymmetryTests { } /// A file in its conventional setting keeps its numbers and its identity transformation; `conventional: false` is - /// slice 1's detection in the file's cell. + /// the plain detection in the file's cell. @Test func testConventionalFalseIsSliceOnesDetectionAndConventionalFilesAreUnchanged() throws { for (pin, s) in Self.detected where pin.source == "slice-1" { let plain = CrystalStructure.detectingSymmetry(cell: pin.cell.unitCell, sites: pin.atomSites, conventional: false) @@ -211,7 +211,7 @@ struct ConventionalSymmetryTests { extension ConventionalSymmetryTests { /// Two rows of one element on one position (Fe²⁺ and Fe³⁺ at 0.5 each) in a primitive fcc cell stay two sites each - /// in the conventional cell: 8 sites, not 4 (Gate B F2 carried into the conventional setting). + /// in the conventional cell: 8 sites, not 4 (carried into the conventional setting). @Test func testCoSitedRowsAreNotMergedByTheCentringImages() throws { let a = 4.0782 / 2.0.squareRoot() let cell = UnitCell.rhombohedral(a: a, alphaDeg: 60) diff --git a/Tests/DiffractoCoreTests/CrystalPresetsTests.swift b/Tests/DiffractoCoreTests/CrystalPresetsTests.swift index 91fa711..3083b3c 100644 --- a/Tests/DiffractoCoreTests/CrystalPresetsTests.swift +++ b/Tests/DiffractoCoreTests/CrystalPresetsTests.swift @@ -6,7 +6,7 @@ import DiffractoCore /// PresetPins: tools/presets/pins.py (pymatgen 2026.9.24, spglib 2.8.0). The pins build each structure independently, from the /// published cell and Wyckoff positions with `Structure.from_spacegroup`. Tolerances, written per quantity: lattice -/// parameters, volume and d(hkl) 1e-7 relative (D17; was 1e-12, closed forms in float64 on both sides); fractional site positions 1e-7 +/// parameters, volume and d(hkl) 1e-7 relative (the reference's tolerance; was 1e-12, closed forms in float64 on both sides); fractional site positions 1e-7 /// (the kit's hand lists carry 2/3 as a Double, pymatgen's orbit expansion computes it); counts, Z, space-group number /// and operation count exact. private struct PresetPins: Decodable { diff --git a/Tests/DiffractoCoreTests/ErrorTextTests.swift b/Tests/DiffractoCoreTests/ErrorTextTests.swift index ad6ef19..5d3f879 100644 --- a/Tests/DiffractoCoreTests/ErrorTextTests.swift +++ b/Tests/DiffractoCoreTests/ErrorTextTests.swift @@ -1,6 +1,6 @@ // // ErrorTextTests.swift -// Registration 0.0.4 section 5 (tier 2): the typed errors of DiffractoCore are `LocalizedError`s whose +// The typed errors of DiffractoCore are `LocalizedError`s whose // `localizedDescription` is their `description`, so an app that shows `error.localizedDescription` shows the kit's text // rather than Foundation's "The operation couldn't be completed. (DiffractoCore.CIFImportError error 0.)". // @@ -49,7 +49,7 @@ struct ErrorTextTests { #expect(!error.localizedDescription.contains("couldn’t be completed")) } - /// Registration 0.2.0, ask 2 (tier 3, a text test): a cell too large for the default limit says how many reflections it + /// A cell too large for the default limit says how many reflections it /// holds, what the default limit is, and which policy field sets another; an invalid cell says it is not a lattice. @Test func testTheTooLargeRefusalNamesItsNumbersAndTheField() throws { let text = ReflectionListError.cellTooLargeOrInvalid(estimatedReflections: 3e9).description diff --git a/Tests/DiffractoCoreTests/FastDetectionTests.swift b/Tests/DiffractoCoreTests/FastDetectionTests.swift index 84636f3..1277305 100644 --- a/Tests/DiffractoCoreTests/FastDetectionTests.swift +++ b/Tests/DiffractoCoreTests/FastDetectionTests.swift @@ -1,8 +1,7 @@ // // FastDetectionTests.swift -// Slice 1b, the Cartesian tolerance and the cell-list permutation test (registration -// docs/registration-slice-1b.md sections 1 and 3): the cell list returns slice 1's operations bit for bit on every -// slice-1 and slice-2 fixture, a 500-atom symmetric P1 cell is detected in under 2 s, and `.cartesian` tells apart a +// The Cartesian tolerance and the cell-list permutation test: the cell list returns the plain scan's operations bit for +// bit on every core and scattering fixture, a 500-atom symmetric P1 cell is detected in under 2 s, and `.cartesian` tells apart a // pseudo-symmetry that `.fractional` cannot in a large cell. // @@ -35,13 +34,13 @@ struct FastDetectionTests { } }() - /// Every fixture the kit has as (name, cell, sites): slice 1b's pins (slice-1 fixtures and the named cells), slice 2's - /// presets, and the CIFs of the fixtures directory. + /// Every fixture the kit has as (name, cell, sites): the conventional-setting pins (the core fixtures and the named cells), the + /// scattering presets, and the CIFs of the fixtures directory. private static var everyFixture: [(String, UnitCell, [AtomSite])] { var out = Pins1b.shared.structures.map { ($0.name, $0.cell.unitCell, $0.atomSites) } out += slice2Presets // near-coincident rows (1.3e-3 apart, both inside the tolerance) and their inversion about a general point: the - // target of an image is ambiguous, so the choice of slice 1 (the lowest unclaimed index) decides the translation + // target of an image is ambiguous, so the plain scan's choice (the lowest unclaimed index) decides the translation let u: [SIMD3] = [[0.10, 0.20, 0.30], [0.1013, 0.20, 0.30], [0.62, 0.31, 0.12], [0.62, 0.3113, 0.12]] let centre = SIMD3(0.35, 0.6, 0.8) let cluster = (u + u.map { centre - $0 }).map { AtomSite(z: 6, fractional: wrapFractional($0)) } @@ -143,7 +142,7 @@ struct FastDetectionTests { var detected: CrystalStructure? let elapsed = clock.measure { detected = CrystalStructure.detectingSymmetry(cell: cell, sites: sites) } let s = detected! - print("slice 1b: 500-atom symmetric P1 cell, \(s.operations.count) operations, \(elapsed)") + print("500-atom symmetric P1 cell, \(s.operations.count) operations, \(elapsed)") #expect(s.operations.count == 48 && s.laueClass == .mThreeBarM) #expect(elapsed < .seconds(2), "\(elapsed)") // the reference path returns the same operations @@ -160,15 +159,15 @@ struct FastDetectionTests { let clock = ContinuousClock() var detection: SymmetryDetection.Result? let elapsed = clock.measure { detection = SymmetryDetection.detect(sites: sites, cell: .cubic(a: 5 * 4.0782)) } - print("slice 1b: 500-atom 5×5×5 gold supercell, \(detection!.operations.count) operations, \(elapsed)") + print("500-atom 5×5×5 gold supercell, \(detection!.operations.count) operations, \(elapsed)") #expect(detection!.operations.count == 125 * 4 * 48) - #expect(elapsed < .seconds(30), "\(elapsed)") // slice 1: about 30 s for 500 atoms at N³ + #expect(elapsed < .seconds(30), "\(elapsed)") // the plain scan: about 30 s for 500 atoms at N³ } // MARK: The Cartesian tolerance /// A pseudo-centring of 0.013 Å in a 50 Å cell: a fractional 5e-3 is 0.25 Å and reads it as symmetry, a Cartesian 1e-3 Å - /// does not (Gate B F6). + /// does not. @Test func testCartesianToleranceSeparatesAPseudoCentringFractionalCannotSee() { let shift = 0.013 / 50 let sites = [AtomSite(z: 26, fractional: [0, 0, 0]), AtomSite(z: 26, fractional: [0.5, 0.5, 0.5 + shift])] diff --git a/Tests/DiffractoCoreTests/MaterialsProjectImportTests.swift b/Tests/DiffractoCoreTests/MaterialsProjectImportTests.swift index 59855e3..e04be76 100644 --- a/Tests/DiffractoCoreTests/MaterialsProjectImportTests.swift +++ b/Tests/DiffractoCoreTests/MaterialsProjectImportTests.swift @@ -2,7 +2,7 @@ // MaterialsProjectImportTests.swift // The Materials Project JSON models, the document-to-structure route and the expectation verdicts. Numerics against // pymatgen SpacegroupAnalyzer (fixtures/mp-import/pymatgen-pins.json, tools/mp-import/pins.py; pymatgen 2026.9.24, -// spglib 2.8.0): the conventional cell's volume and site count (volume 1e-7 relative (D17; was 1e-9): the MP inputs carry their cell to 3-4 +// spglib 2.8.0): the conventional cell's volume and site count (volume 1e-7 relative (the reference's tolerance; was 1e-9): the MP inputs carry their cell to 3-4 // digits and pymatgen's standard cell is rebuilt from lengths and angles), the Laue class and the metric family exact. // Verdicts, refusals and the formula parser are exact logic. Nothing touches the network. // diff --git a/Tests/DiffractoCoreTests/PointSymmetryTests.swift b/Tests/DiffractoCoreTests/PointSymmetryTests.swift index 56168c0..7dcc9bf 100644 --- a/Tests/DiffractoCoreTests/PointSymmetryTests.swift +++ b/Tests/DiffractoCoreTests/PointSymmetryTests.swift @@ -1,8 +1,8 @@ // // PointSymmetryTests.swift -// Planted truth for the groups (registration 5.2): the holohedry orders, closure, determinants and finite orders; +// Planted truth for the groups: the holohedry orders, closure, determinants and finite orders; // the Laue class of each group by the 3.5 rule; orbit sizes at general and special positions; the canonical forms -// and names the reference implementations use today (3.6). Group theory only; no reference code. +// and names the reference implementations use today. Group theory only; no reference code. // import DiffractoKitTestSupport @@ -25,9 +25,9 @@ struct PointSymmetryTests { (.triclinic, 2, .oneBar), ] - /// Every holohedry has its registered order, is closed, every element has determinant ±1 and finite order, and + /// Every holohedry has its expected order, is closed, every element has determinant ±1 and finite order, and /// its Laue class is the holohedry's. Mutant: a generator with a wrong entry (closure runs to 200 or the order moves). - @Test func testHolohedriesAreTheRegisteredGroups() { + @Test func testHolohedriesAreTheExpectedGroups() { for (family, order, laue) in Self.holohedries { let group = PointSymmetry.holohedry(of: family) #expect(group.order == order, "\(family)") @@ -75,7 +75,7 @@ struct PointSymmetryTests { #expect(PointSymmetry(rotations: [.identity, .inversion, Self.c4z]).laueClass == nil) } - /// General-position orbit sizes (5.2). Mutant: the orbit as hkl R⁻¹ (the 6-fold's orbit changes size for no hkl but + /// General-position orbit sizes. Mutant: the orbit as hkl R⁻¹ (the 6-fold's orbit changes size for no hkl but /// its members do: covered by the pins), the mirror dropped from 2/m (orbits halve). @Test func testGeneralOrbitSizes() { let general = MillerIndex(1, 2, 3) @@ -97,7 +97,7 @@ struct PointSymmetryTests { } } - /// Special positions (5.2): the reference implementations' multiplicities. Mutant: C₂ about a instead of b ({h0l} becomes 4). + /// Special positions: the reference implementations' multiplicities. Mutant: C₂ about a instead of b ({h0l} becomes 4). @Test func testSpecialOrbitSizes() { let mono = PointSymmetry.holohedry(of: .monoclinic(uniqueAxis: .b)) #expect(mono.orbit(of: MillerIndex(2, 0, 1)).count == 2) @@ -121,7 +121,7 @@ struct PointSymmetryTests { #expect(hex.orbit(of: MillerIndex(1, 1, 0)).contains(MillerIndex(-1, -1, 0)), "(1 1 0) and (-1 -1 0) are one orbit") // 4/m keeps {2 1 0} and {1 2 0} apart; 4/mmm joins them. hk0 is a special form (the mirror ⊥ c fixes it): - // 4 under 4/m and 8 under 4/mmm; the general hkl are 8 and 16 (registration 5.2, amended 2026-10-14). + // 4 under 4/m and 8 under 4/mmm; the general hkl are 8 and 16 (amended 2026-10-14). #expect(Self.fourOverM.orbit(of: MillerIndex(2, 1, 0)).count == 4) #expect(Self.fourOverM.orbit(of: MillerIndex(1, 2, 0)).count == 4) #expect(!(Self.fourOverM.orbit(of: MillerIndex(2, 1, 0)).contains(MillerIndex(1, 2, 0)))) @@ -130,7 +130,7 @@ struct PointSymmetryTests { #expect(PointSymmetry.holohedry(of: .tetragonal(uniqueAxis: .c)).orbit(of: MillerIndex(2, 1, 3)).count == 16) } - /// The one canonical rule reproduces the reference implementations' names (3.6). Mutant: the key without the non-negative count + /// The one canonical rule reproduces the reference implementations' names. Mutant: the key without the non-negative count /// (hexagonal {1 1 -2 0} becomes {2 -1 -1 0}); the sign of l not tied to h in 2/m. @Test func testCanonicalFormsAndNamesMatchTheApps() { let cubic = PointSymmetry.holohedry(of: .cubic) diff --git a/Tests/DiffractoCoreTests/PolicyTests.swift b/Tests/DiffractoCoreTests/PolicyTests.swift index f22af76..3a83b4b 100644 --- a/Tests/DiffractoCoreTests/PolicyTests.swift +++ b/Tests/DiffractoCoreTests/PolicyTests.swift @@ -1,9 +1,9 @@ // // PolicyTests.swift -// DiffractoKit 0.2.0 (docs/registration-0.2.0.md §1, §2): the policies of DiffractoCore are parameters with the suite's values -// as defaults. Tier 2: the default path is held by the existing suite; here a non-default value moves the outcome in the +// DiffractoKit 0.2.0: the policies of DiffractoCore are parameters with the suite's values +// as defaults. The default path is held by the existing suite; here a non-default value moves the outcome in the // stated direction and an invalid one is refused or taken as documented. Every test names under "RED UNDER" the production -// mutation that turns a default-path test red (tools/kit-0.2.0/mutants-policy.py). +// mutation that turns a default-path test red. // import DiffractoKitTestSupport @@ -131,7 +131,7 @@ struct ValidationPolicyTests { /// The documented defaults. /// RED UNDER: either default of `ValidationPolicy` changed. - @Test func theDefaultsAreTheRegisteredValues() { + @Test func theDefaultsAreTheDocumentedValues() { let p: Policy = .default #expect(p.closeContactLimit == 0.5) #expect(p.splitSiteOccupancyTolerance == 0.02) @@ -183,7 +183,7 @@ struct DetectionPolicyTests { /// The documented defaults. /// RED UNDER: any default of `SymmetryDetection.Policy` changed. - @Test func theDefaultsAreTheRegisteredValues() { + @Test func theDefaultsAreTheDocumentedValues() { let p: Policy = .default #expect(p.occupancyTolerance == 0.02) #expect(p.translationGrid == 1.0 / 24) @@ -334,7 +334,7 @@ struct CIFImportPolicyTests { /// The documented defaults. /// RED UNDER: any default of `CIFImport.Policy` changed. - @Test func theDefaultsAreTheRegisteredValues() { + @Test func theDefaultsAreTheDocumentedValues() { let p: Policy = .default #expect(p.maximumMergeDistance == 0.10) #expect(p.minimumMergeTolerance == 1e-6) diff --git a/Tests/DiffractoCoreTests/ReductionTests.swift b/Tests/DiffractoCoreTests/ReductionTests.swift index 9b42044..d98a73a 100644 --- a/Tests/DiffractoCoreTests/ReductionTests.swift +++ b/Tests/DiffractoCoreTests/ReductionTests.swift @@ -1,6 +1,6 @@ // // ReductionTests.swift -// Slice 1b, the lattice reduction (registration docs/registration-slice-1b.md sections 1 to 3): the Niggli-reduced +// The lattice reduction: the Niggli-reduced // cell against spglib and pymatgen on every pinned cell and 2800 random cells, and against the Niggli conditions // written out from the text; the transformation is unimodular and reproduces the reduced metric. // @@ -38,7 +38,7 @@ struct ReductionTests { } /// The reduced cell's six parameters equal spglib's and pymatgen's (two independent implementations of the same - /// algorithm) to 1e-5, the functions' own tol (D17; was 1e-10; relative on the lengths, degrees on the angles). + /// algorithm) to 1e-5, the functions' own tol (the reference's tolerance; was 1e-10; relative on the lengths, degrees on the angles). @Test func testNiggliReducedCellAgreesWithSpglibAndPymatgen() { var worstSpglib = 0.0, worstPymatgen = 0.0 for c in Self.cases { @@ -50,7 +50,7 @@ struct ReductionTests { #expect(ds <= 1e-5, "\(c.name) vs spglib: \(mine) \(c.spglib)") #expect(dp <= 1e-5, "\(c.name) vs pymatgen: \(mine) \(c.pymatgen)") } - print("slice 1b reduction: \(Self.cases.count) cells, worst deviation from spglib \(worstSpglib), from pymatgen \(worstPymatgen)") + print("reduction: \(Self.cases.count) cells, worst deviation from spglib \(worstSpglib), from pymatgen \(worstPymatgen)") } /// The Niggli conditions, checked directly on the reduced basis (not through the oracle); P is an integer matrix of diff --git a/Tests/DiffractoCoreTests/RefuterRegressionTests.swift b/Tests/DiffractoCoreTests/RefuterRegressionTests.swift index 33eedec..8940a54 100644 --- a/Tests/DiffractoCoreTests/RefuterRegressionTests.swift +++ b/Tests/DiffractoCoreTests/RefuterRegressionTests.swift @@ -1,6 +1,6 @@ // // RefuterRegressionTests.swift -// The Gate B refuter's findings on b4cfe17 (registration section 8, amendment of 2026-10-15), each as a regression +// An independent review's findings, each as a regression // test on the refuter's own CIF (Fixtures/refuter_*.cif). Truth: pymatgen-core 2026.9.23 / spglib 2.8.0 on the // UNSHIFTED structure (absences and families do not depend on the origin), computed independently of the refuter's // harness: Mg (c = 5.2112) P6₃/mmc 24 operations, 282 of 324 allowed at 1.2 Å⁻¹; one Fe atom in a 5 Å cube Pm-3m diff --git a/Tests/DiffractoCoreTests/ShearReductionTests.swift b/Tests/DiffractoCoreTests/ShearReductionTests.swift index 3943c92..0c9253e 100644 --- a/Tests/DiffractoCoreTests/ShearReductionTests.swift +++ b/Tests/DiffractoCoreTests/ShearReductionTests.swift @@ -1,6 +1,6 @@ // // ShearReductionTests.swift -// Slice 1b, Gate B finding F1 (registration docs/registration-slice-1b.md section 6, "Gate B on 4876b18"): a cell sheared +// A cell sheared // by hundreds or thousands of lattice units read aP / -1 with no warning, because the reduction shaved it back by unit // shears under a 2000-step cap. The reduction shears by integer multiples now, so any shear reduces in a few steps; when // the reduction still does not settle (rounding in the six parameters at a shear of 1e6) the result says so diff --git a/Tests/DiffractoCoreTests/Slice1bSupport.swift b/Tests/DiffractoCoreTests/Slice1bSupport.swift index 383cd02..68f01bf 100644 --- a/Tests/DiffractoCoreTests/Slice1bSupport.swift +++ b/Tests/DiffractoCoreTests/Slice1bSupport.swift @@ -3,7 +3,7 @@ // Role: the pins of tools/slice-1b/pins.py (spglib 2.8.0 and pymatgen as oracles for the reduced cell, the lattice's // Bravais name and conventional cell, the operation counts and the rings in the conventional setting), and the // Niggli conditions written out from the text, so the tests check the reduced cell directly and not only the -// oracle (registration docs/registration-slice-1b.md sections 2 and 3). +// oracle. // import DiffractoKitTestSupport @@ -104,7 +104,7 @@ func transformedVectors(_ cell: UnitCell, _ p: IntegerMatrix) -> [SIMD3] } } -/// The Niggli main conditions (Křivý & Gruber 1976; International Tables A, section 9.2) on the reduced basis, written +/// The Niggli main conditions (Křivý & Gruber 1976, International Tables A) on the reduced basis, written /// out from the text, each within `eps` (Ų). Returns the conditions that fail. func niggliViolations(_ v: [SIMD3], eps: Double) -> [String] { let a = dot(v[0], v[0]), b = dot(v[1], v[1]), c = dot(v[2], v[2]) diff --git a/Tests/DiffractoCoreTests/SymmetryDetectionTests.swift b/Tests/DiffractoCoreTests/SymmetryDetectionTests.swift index f26d180..0cffed8 100644 --- a/Tests/DiffractoCoreTests/SymmetryDetectionTests.swift +++ b/Tests/DiffractoCoreTests/SymmetryDetectionTests.swift @@ -1,6 +1,6 @@ // // SymmetryDetectionTests.swift -// The operations of a structure given as a site list (registration 3.4b), on cells whose content is known by +// The operations of a structure given as a site list, on cells whose content is known by // construction: β″ expanded by hand (C2/m: 8 operations), gold P1 (Fm-3m: 192), iron (Im-3m: 96), magnesium // (P6₃/mmc: 24), the split-atom gold that must not be cubic, and a tetragonal cell with a = c. // diff --git a/Tests/DiffractoCoreTests/SymmetryOperationTests.swift b/Tests/DiffractoCoreTests/SymmetryOperationTests.swift index 8acfde5..b8a9416 100644 --- a/Tests/DiffractoCoreTests/SymmetryOperationTests.swift +++ b/Tests/DiffractoCoreTests/SymmetryOperationTests.swift @@ -1,6 +1,6 @@ // // SymmetryOperationTests.swift -// The x,y,z grammar (registration 3.3): what parses, what it means, what is refused, and the round trip. +// The x,y,z grammar: what parses, what it means, what is refused, and the round trip. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoCoreTests/TetragonalSettingTests.swift b/Tests/DiffractoCoreTests/TetragonalSettingTests.swift index 308a2bc..66d42bc 100644 --- a/Tests/DiffractoCoreTests/TetragonalSettingTests.swift +++ b/Tests/DiffractoCoreTests/TetragonalSettingTests.swift @@ -1,6 +1,6 @@ // // TetragonalSettingTests.swift -// Registration 0.0.4 section 6 (tier 1): a tetragonal structure is tetragonal whichever axis carries its 4-fold. The +// A tetragonal structure is tetragonal whichever axis carries its 4-fold. The // pins are pymatgen 2026.9.24's SpacegroupAnalyzer (spglib 2.8.0) on rutile, anatase, beta-Sn, an orthorhombic control // with a = c by accident and a cubic one, each in three settings (tools/kit-0.0.4/tetragonal-pins.py, symprec 1e-4, // written before the fix). The truth beside them: the setting changes nothing, so the three settings give the same diff --git a/Tests/DiffractoCoreTests/ToleranceFixesTests.swift b/Tests/DiffractoCoreTests/ToleranceFixesTests.swift index c50513d..e0c2014 100644 --- a/Tests/DiffractoCoreTests/ToleranceFixesTests.swift +++ b/Tests/DiffractoCoreTests/ToleranceFixesTests.swift @@ -1,7 +1,6 @@ // // ToleranceFixesTests.swift -// DiffractoKit 0.2.0, the Gate B findings on tolerances (tier 1 for F5, F6 and F8, whose outcomes a scientist reads; -// tier 2 for F3): the position tolerance is refused where it cannot work and no structure is ever returned without the +// DiffractoKit 0.2.0, the review findings on tolerances: the position tolerance is refused where it cannot work and no structure is ever returned without the // identity operation; the metric tolerances read NaN, negative and infinite as 0; an absurd cell and an invalid Niggli // epsilon never trap and are never reported as reduced. Every test names under "RED UNDER" the production change that // turns it red. diff --git a/Tests/DiffractoCoreTests/UnitCellTests.swift b/Tests/DiffractoCoreTests/UnitCellTests.swift index be9baaa..de55b96 100644 --- a/Tests/DiffractoCoreTests/UnitCellTests.swift +++ b/Tests/DiffractoCoreTests/UnitCellTests.swift @@ -1,6 +1,6 @@ // // UnitCellTests.swift -// Planted truth for the cell (registration 5.1): the closed-form d of each system on rational cells, the volume by +// Planted truth for the cell: the closed-form d of each system on rational cells, the volume by // both formulas, d by the metric and by the vector, the reciprocal rows dual to the real ones, and the metric // family of each constructor. No reference code is read. // @@ -59,7 +59,7 @@ struct UnitCellTests { } } - /// The textbook formulas, one per system (5.1), to 1e-12 relative. Mutants: β read from α; the per-axis tile. + /// The textbook formulas, one per system, to 1e-12 relative. Mutants: β read from α; the per-axis tile. @Test func testClosedFormDPerSystem() { func check(_ cell: UnitCell, _ name: String, _ oneOverDSquared: (Double, Double, Double) -> Double) { for hkl in Self.indices { @@ -102,7 +102,7 @@ struct UnitCellTests { } } - /// the reference implementation's S2c hand anchors for β″ (d in Å). Mutant: β read from α (d(200) becomes a/2). + /// the reference implementation's hand anchors for β″ (d in Å). Mutant: β read from α (d(200) becomes a/2). @Test func testBetaDoublePrimeHandAnchors() { let cell = Fixtures.betaDoublePrimeCell let anchors: [(MillerIndex, Double)] = [ @@ -127,7 +127,7 @@ struct UnitCellTests { #expect(UnitCell.cubic(a: 4).metricFamily() == .cubic) #expect(UnitCell.hexagonal(a: 3, c: 5).metricFamily() == .hexagonal) #expect(UnitCell.tetragonal(a: 3, c: 5).metricFamily() == .tetragonal(uniqueAxis: .c)) - // The odd axis carries the 4-fold wherever it is (registration 0.0.4 section 6). + // The odd axis carries the 4-fold wherever it is. #expect(UnitCell.orthorhombic(a: 3, b: 5, c: 3).metricFamily() == .tetragonal(uniqueAxis: .b)) #expect(UnitCell.orthorhombic(a: 5, b: 3, c: 3).metricFamily() == .tetragonal(uniqueAxis: .a)) #expect(UnitCell.orthorhombic(a: 3, b: 4, c: 5).metricFamily() == .orthorhombic) From 5da34d864575a62a55cb70181d7a498e8c06dfd1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:45:34 +0000 Subject: [PATCH 06/17] test(geometry, scattering): drop references to documents outside the repo Comment, MARK and test-name text in DiffractoGeometryTests and DiffractoScatteringTests: registration sections, the old roadmap, review rounds and finding numbers, tiers and missing mutation scripts removed; the archived camera-length plan no longer cited; "registered" values renamed documented, quoted or predicted; "the owner's data" is the maintainer's. No assertion changes. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .../ToleranceFixesTests.swift | 6 +- .../EllipseCalibrationTests.swift | 12 ++-- .../EllipseNoisyRingTests.swift | 8 +-- .../EllipseStopRuleTests.swift | 2 +- .../EllipseTransformTests.swift | 2 +- .../GeometryRefuterTests.swift | 14 ++--- .../PolicyFixesTests.swift | 14 ++--- .../DiffractoGeometryTests/PolicyTests.swift | 12 ++-- .../RingCalibrationTests.swift | 60 +++++++++---------- .../RingFitAmendmentTests.swift | 54 ++++++++--------- .../RingFitCalibrationTests.swift | 8 +-- .../RingIndexingTests.swift | 24 ++++---- .../ZoneAxisTests.swift | 6 +- .../ConventionalRingsTests.swift | 2 +- .../PolicyTests.swift | 8 +-- .../ScatteringRefuterTests.swift | 16 ++--- .../ScatteringTruthTests.swift | 8 +-- 17 files changed, 128 insertions(+), 128 deletions(-) diff --git a/Tests/DiffractoCoreTests/ToleranceFixesTests.swift b/Tests/DiffractoCoreTests/ToleranceFixesTests.swift index e0c2014..6255bfc 100644 --- a/Tests/DiffractoCoreTests/ToleranceFixesTests.swift +++ b/Tests/DiffractoCoreTests/ToleranceFixesTests.swift @@ -33,7 +33,7 @@ struct ToleranceFixesTests { .cartesian(1e-300), .cartesian(5e-324), .cartesian(1e-10), .cartesian(1e300), .cartesian(5), .cartesian(1000), ] - // MARK: F5 and F6 + // MARK: The position tolerance /// `CIFImport.read` refuses every such position tolerance with `invalidStructure` and a message naming the tolerance; /// none traps and none imports a structure. @@ -129,7 +129,7 @@ struct ToleranceFixesTests { } } - // MARK: F3 + // MARK: The metric tolerances /// A metric tolerance that is NaN, negative or infinite is read as 0 (every length and angle exactly equal): the /// silicon cell is still read cubic at 0 because its lengths are written equal; a cell written with a 1e-12 difference @@ -161,7 +161,7 @@ struct ToleranceFixesTests { } } -// MARK: - F8: absurd cells and the Niggli epsilon +// MARK: - Absurd cells and the Niggli epsilon struct AbsurdCellTests { /// The fluorapatite file with its first cell length replaced. diff --git a/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift b/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift index ce01f81..6ded29b 100644 --- a/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift @@ -1,6 +1,6 @@ // // EllipseCalibrationTests.swift -// DiffractoKit slice 3b (docs/registration-slice-3.md section 3): `EllipseCalibration` on small clean patterns planted here, in +// `EllipseCalibration` on small clean patterns planted here, in // Swift, with their truth known: the constants, the input guards, the annulus boundaries, the coverage bound and the "fit // anyway" path, the one-ring check, the 0.30 px ring-width floor guard (ADR 069), the profile residual cap, a clean circle's // radius on both paths, and the row / column convention on a non-square pattern. The noisy plants of the reference implementations are @@ -9,7 +9,7 @@ // // Written for the kit (the seed has these checks only as tools/ellipse-calibration-test, a scientific harness); the numbers // marked "measured" were measured on reference commit 7d6fdbd1's own EllipseCalibration compiled -O (and equal the kit's, which -// EllipseAppsParityTests checks exactly, as a D12 quick check). +// EllipseAppsParityTests checks exactly). // // Every test names under "RED UNDER" the production mutation that turns it red. // @@ -69,7 +69,7 @@ struct EllipseCalibrationTests { /// 36 azimuthal bins, 30 needed by default (five sixths), 12 on the anyway path (one third); the calibration's own bounds are /// the same numbers (RingCalibrationTests holds them to the literals; this is the equality its header promised). /// RED UNDER: any of the three constants changed; the ring fit's bin count different from `RingCalibration`'s. - @Test func testTheBinConstantsAreTheRegisteredOnesAndEqualTheCalibrationsOwn() { + @Test func testTheBinConstantsAreTheDocumentedOnesAndEqualTheCalibrationsOwn() { #expect(EllipseCalibration.angularBinCount == 36) #expect(EllipseCalibration.degeneracyBoundBins == 30) #expect(EllipseCalibration.sparseFloorBins == 12) @@ -140,8 +140,8 @@ struct EllipseCalibrationTests { // MARK: The annulus boundaries - /// The conic's annulus is `inner < r <= outer` and the profile's `inner < r < outer` (kept for bit identity with the reference implementations, - /// registration section 2). On a lattice centred on a pixel the radii 10 and 20 are hit exactly by pixels such as (6, 8) and + /// The conic's annulus is `inner < r <= outer` and the profile's `inner < r < outer` (kept for bit identity with the reference implementations). + On a lattice centred on a pixel the radii 10 and 20 are hit exactly by pixels such as (6, 8) and /// (12, 16), so the counts differ by exactly the pixels on the boundaries; the expected counts are worked in integers. /// RED UNDER: `<=` for `<` or `<` for `<=` at either outer boundary; `>=` for `>` at the inner one. @Test func testTheAnnulusBoundariesAreInnerExcludedAndOuterIncludedForTheConicOnly() throws { @@ -307,7 +307,7 @@ struct EllipseCalibrationTests { // MARK: Truth: a clean circle /// A clean circular ring of width s about a known centre, on both paths: the conic's objective puts its radius at - /// R (1 + 1.75 (s/R)^2) (a property of the objective, registered in the header: +0.4375 % at R 30, s 3), the profile fit + /// R (1 + 1.75 (s/R)^2) (a property of the objective, in the header: +0.4375 % at R 30, s 3), the profile fit /// recovers R and its width s; a/b is 1 on both. Plant: the reference implementations' stop-rule ring 1 (96 px, R 30, s 3). /// RED UNDER: the profile model's radius taken from the conic; the profile's sigmas swapped with the central one. @Test func testACleanCircleGivesTheBiasedRadiusOnTheConicAndTheTrueRadiusOnTheProfile() throws { diff --git a/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift b/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift index 976e328..87436f1 100644 --- a/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift @@ -1,6 +1,6 @@ // // EllipseNoisyRingTests.swift -// DiffractoKit slice 3b (docs/registration-slice-3.md section 3): the profile ring fit on the three noisy plants of reference commit +// The profile ring fit on the three noisy plants of reference commit // 7d6fdbd1 `EllipseNoisyRingTests` (same names, same plants, same bands), with the plant renderer (SplitMix64 + Box-Muller, // Float pixels) ported into this file. The renderer is also the one tools/slice-3b/apps-parity/main.swift carries a copy of; // EllipseAppsParityTests proves the two render the same pixels (FNV-1a hash of every case) before it compares any fit. @@ -25,7 +25,7 @@ // Neither fit may be refused or fall back to the conic: the profile model finds the ring on all three, and the tests assert the // ring's mean radius, its b / a (what a caller applies to every Bragg vector) and its centre, against the plant. // -// RED UNDER (measured by the break-once run in tools/slice-3b/mutants.py): the second start dropped turns the dead ring and the +// RED UNDER (measured by breaking the code once per mutation): the second start dropped turns the dead ring and the // drifting ring red; the first start dropped turns the tail ring red. // @@ -86,11 +86,11 @@ struct EllipseNoisyRingTests { /// the fit accepts a ring-less profile fit there (I1 0, a ring sigma at the 0.25 px floor, b/a 0.947, where the conic gives /// 0.989 and the wider annulus 0.970); with it, the fit falls back to the conic and says why. /// - /// DISABLED, not skipped silently: the pattern is derived from the owner's data, which this kit never copies, and + /// DISABLED, not skipped silently: the pattern is derived from the maintainer's data, which this kit never copies, and /// `../data/README.md` holds no manifest line for it yet. When the line exists (the file under `$MACEM_DATA`, default /// `../data`, at the path below), remove the trait and set the path. The floor guard itself is held by /// EllipseCalibrationTests (`testARingFittedWithinPointThreePxOfTheFloorIsRefusedAndFallsBackToTheConic`). - @Test(.disabled("owner's data, manifest line pending")) + @Test(.disabled("maintainer's data, manifest line pending")) func testRinglessFitOnARealMeanPatternFallsBackToTheConic() throws { let root = ProcessInfo.processInfo.environment["MACEM_DATA"] ?? URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent() diff --git a/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift b/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift index 89cd1b7..569320b 100644 --- a/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift @@ -1,6 +1,6 @@ // // EllipseStopRuleTests.swift -// DiffractoKit slice 3b: `EllipseCalibration.fit1D`'s Levenberg-Marquardt loop (the reference implementation 2026-10-12) once tested convergence only +// `EllipseCalibration.fit1D`'s Levenberg-Marquardt loop (the reference implementation 2026-10-12) once tested convergence only // after an ACCEPTED step. At the optimum the cost change of a step is roundoff, every step was refused, damping grew until the // candidate equalled the point, and the 100-pass cap threw `didNotConverge` on about one clean ring in seven // (reference `archive/v5/ellipse-stop-rule-gateD-2026-10-12.md`). Ported from reference commit 7d6fdbd1 diff --git a/Tests/DiffractoGeometryTests/EllipseTransformTests.swift b/Tests/DiffractoGeometryTests/EllipseTransformTests.swift index bbfc9f3..7552838 100644 --- a/Tests/DiffractoGeometryTests/EllipseTransformTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseTransformTests.swift @@ -1,6 +1,6 @@ // // EllipseTransformTests.swift -// DiffractoKit slice 3b: `EllipseTransform` (reference commit 7d6fdbd1 `Calibration.ellipseTransform` / `ellipseCorrectedOffset` / +// `EllipseTransform` (reference commit 7d6fdbd1 `Calibration.ellipseTransform` / `ellipseCorrectedOffset` / // `ellipseUncorrectedOffset` / `acceptedEllipse`) against hand arithmetic, and the four `CalibrationEllipseRow29Tests` of the // seed re-expressed on the kit's type (same names, same numbers; the seed's `Calibration` becomes the failable `EllipseTransform` // and its static `correctedOffset`, which leaves the offset alone where the seed's `hasEllipse` was false). diff --git a/Tests/DiffractoGeometryTests/GeometryRefuterTests.swift b/Tests/DiffractoGeometryTests/GeometryRefuterTests.swift index 5a54f1e..38c2ebc 100644 --- a/Tests/DiffractoGeometryTests/GeometryRefuterTests.swift +++ b/Tests/DiffractoGeometryTests/GeometryRefuterTests.swift @@ -1,6 +1,6 @@ // // GeometryRefuterTests.swift -// The Gate B refuter's findings on slice 3a (a16cf84; registration slice 3 §7, amendment of 2026-10-15), each a test. +// An independent review's findings on the ring calibration's closed forms, each a test. // import DiffractoKitTestSupport @@ -13,7 +13,7 @@ import DiffractoGeometry struct GeometryRefuterTests { - /// F1: above about 1.3e154 V the product overflows; λ is NaN, never 0. Mutant: the overflow guard removed. + /// above about 1.3e154 V the product overflows; λ is NaN, never 0. Mutant: the overflow guard removed. @Test func testWavelengthOverflowIsNaN() { for v in [1.4e154, 1e155, 1e300, Double.greatestFiniteMagnitude] { #expect(ElectronWavelength.angstroms(volts: v).isNaN, "\(v)") @@ -21,7 +21,7 @@ struct GeometryRefuterTests { #expect(ElectronWavelength.angstroms(volts: 2e5) == 0.025079340450548003) } - /// F2: an overflowing camera length or Δq is NaN, never infinity. Mutant: the result guards removed. + /// an overflowing camera length or Δq is NaN, never infinity. Mutant: the result guards removed. @Test func testOverflowingLengthsAreNaN() { #expect(RingCalibration.cameraLengthPixels(radius: 1e300, twoTheta: 1e-300).isNaN) #expect(RingCalibration.qPerPixel(d: 5e-324, radius: 1).isNaN) @@ -29,7 +29,7 @@ struct GeometryRefuterTests { #expect(RingCalibration.cameraLengthPixels(radius: 150, twoTheta: 0.010651260937060768).isFinite) } - /// F4, F5: project keeps nothing for a non-unit, zero or non-finite n, or a negative or non-finite wavelength. + /// project keeps nothing for a non-unit, zero or non-finite n, or a negative or non-finite wavelength. @Test func testProjectRefusesBadDirectionsAndWavelengths() throws { let fcc: [SIMD3] = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]] let au = CrystalStructure.detectingSymmetry(cell: .cubic(a: 4.0782), sites: fcc.map { AtomSite(z: 79, fractional: $0) }) @@ -45,7 +45,7 @@ struct GeometryRefuterTests { #expect(ZoneAxis.isUnitDirection([0, 0, 1])) } - /// F6: with n the beam's travel direction, s_g = 0 is the Ewald condition |n/λ + g| = 1/λ (to first order exactly). + /// with n the beam's travel direction, s_g = 0 is the Ewald condition |n/λ + g| = 1/λ (to first order exactly). @Test func testCurvedExcitationErrorIsTheEwaldConditionForTheTravelDirection() throws { let fcc: [SIMD3] = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]] let au = CrystalStructure.detectingSymmetry(cell: .cubic(a: 4.0782), sites: fcc.map { AtomSite(z: 79, fractional: $0) }) @@ -58,7 +58,7 @@ struct GeometryRefuterTests { } } - /// F7: the list overload uses the list's own k_max (the free-k_max overload with a larger k_max accepts what the + /// the list overload uses the list's own k_max (the free-k_max overload with a larger k_max accepts what the /// list overload refuses as beyond k_max). @Test func testListOverloadUsesTheListsKMax() throws { let fcc: [SIMD3] = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]] @@ -71,7 +71,7 @@ struct GeometryRefuterTests { } } -/// The Gate B refuter's two traps on slice 3b (704e5b0): an unbounded outer radius and an overflowing pattern size are +/// The review's two traps in the ring and ellipse fit: an unbounded outer radius and an overflowing pattern size are /// refused as invalid input, never trapped. Mutants: either guard removed. struct EllipseRefuterTests { static func pattern() -> DiffractionPattern { diff --git a/Tests/DiffractoGeometryTests/PolicyFixesTests.swift b/Tests/DiffractoGeometryTests/PolicyFixesTests.swift index 0c12103..5d394c8 100644 --- a/Tests/DiffractoGeometryTests/PolicyFixesTests.swift +++ b/Tests/DiffractoGeometryTests/PolicyFixesTests.swift @@ -1,8 +1,8 @@ // // PolicyFixesTests.swift -// DiffractoKit 0.2.0, the Gate B findings on the ring-fit policies (tier 2): F4 (no trap for any bin count), F1 (the three -// bin counts and what `isValid` requires of them), F2 (the calibration policy's evidence replaces the ellipse policy's; -// loosening coverage through `fitRing` needs the three bounds), F3 (`RingIndexing.reach` with an invalid argument). +// DiffractoKit 0.2.0, the review findings on the ring-fit policies: no trap for any bin count; the three bin counts and +// what `isValid` requires of them; the calibration policy's evidence replaces the ellipse policy's (loosening coverage +// through `fitRing` needs the three bounds); `RingIndexing.reach` with an invalid argument. // Every test names under "RED UNDER" the production change that turns it red. // @@ -15,7 +15,7 @@ struct PolicyFixesTests { typealias T = RingFitAmendmentTests typealias Policy = EllipseCalibration.Policy - // MARK: F4 + // MARK: No trap for any bin count /// No Int traps `Policy(angularBinCount:)`: the derived bounds are computed without overflow, a count outside 6...3600 is /// not valid, and for every count that did not overflow before the derived values are the old expressions' (5/6 and 1/3, @@ -41,7 +41,7 @@ struct PolicyFixesTests { #expect(Policy.default.degeneracyBoundBins == 30 && Policy.default.sparseFloorBins == 12) } - // MARK: F1 + // MARK: The three bin counts /// The bounds are bin counts: setting `angularBinCount` alone on `.default` keeps 30 and 12 (the initialiser derives /// them, a mutation does not); `isValid` requires 1 <= sparse floor <= degeneracy bound <= bin count, so a violated @@ -66,7 +66,7 @@ struct PolicyFixesTests { #expect(Policy(angularBinCount: 36, degeneracyBoundBins: 1, sparseFloorBins: 1).isValid) } - // MARK: F2 + // MARK: The calibration policy's evidence static let scene = T.Scene() static func gapped() -> DiffractionPattern { @@ -132,7 +132,7 @@ struct PolicyFixesTests { } } - // MARK: F3 (RingIndexing.reach) + // MARK: RingIndexing.reach /// `reach` answers NaN for the coverage or margin that `index` refuses (a coverage that is not a positive number, a /// margin that is negative or not finite), and the closed form otherwise. diff --git a/Tests/DiffractoGeometryTests/PolicyTests.swift b/Tests/DiffractoGeometryTests/PolicyTests.swift index 61cba82..16001b4 100644 --- a/Tests/DiffractoGeometryTests/PolicyTests.swift +++ b/Tests/DiffractoGeometryTests/PolicyTests.swift @@ -1,9 +1,9 @@ // // PolicyTests.swift -// DiffractoKit 0.2.0 (docs/registration-0.2.0.md §1, §2): the policies of DiffractoGeometry are parameters with the suite's -// values as defaults. Tier 2 (the default path is held exact by the existing suite as a D12 quick check, EllipseAppsParityTests included): +// DiffractoKit 0.2.0: the policies of DiffractoGeometry are parameters with the suite's +// values as defaults. The default path is held exact by the existing suite, EllipseAppsParityTests included; // here a non-default value moves the outcome in the stated direction, and an invalid one is refused as documented. -// Every test names under "RED UNDER" the production mutation that turns it red (tools/kit-0.2.0/mutants-policy.py). +// Every test names under "RED UNDER" the production mutation that turns it red. // import DiffractoKitTestSupport @@ -40,7 +40,7 @@ struct EllipsePolicyTests { } /// The documented defaults are the constants the fit used before 0.2.0, and the derived bounds follow the bin count. - /// RED UNDER: any default of `Policy.init` changed (tools/kit-0.2.0/mutants-policy.py, ellipse-*). + /// RED UNDER: any default of `Policy.init` changed. @Test func theDefaultsAreTheDocumentedValues() { let p: Policy = .default #expect(p.angularBinCount == EllipseCalibration.angularBinCount) @@ -438,9 +438,9 @@ struct RingCalibrationPolicyTests { RingCalibration.refusals(report, highTensionVolts: 200_000, policy: policy).map(\.name) } - /// The documented defaults equal the registered constants. + /// The documented defaults equal the constants. /// RED UNDER: any default of `RingCalibration.Policy` or `RingEvidence.Policy` changed. - @Test func theDefaultsAreTheRegisteredBounds() { + @Test func theDefaultsAreTheDocumentedBounds() { let p: P = .default #expect(p.angularBinCount == 36) #expect(p.minimumOccupiedAngularBins == 30) diff --git a/Tests/DiffractoGeometryTests/RingCalibrationTests.swift b/Tests/DiffractoGeometryTests/RingCalibrationTests.swift index 9c55a33..ad24cfb 100644 --- a/Tests/DiffractoGeometryTests/RingCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/RingCalibrationTests.swift @@ -1,6 +1,6 @@ // // RingCalibrationTests.swift -// DiffractoKit slice 3a (docs/registration-slice-3.md): the closed forms (λ, 2θ, L_px, Δq, L_mm) against hand arithmetic, +// The closed forms (λ, 2θ, L_px, Δq, L_mm) against hand arithmetic, // and every refusal by its own name. Ported from reference commit 9482831 Tests/RingCalibrationTests.swift (30 tests, same // names, literals unchanged) to Swift Testing. No fit, no Python at run time: the expected numbers below were worked once, // by hand, in 60-digit decimal arithmetic from the CODATA 2018 constants stated in ElectronWavelength.swift (h and c and e @@ -8,8 +8,9 @@ // RingCalibrationPinsTests (tools/slice-3/ring-pins.py). // // Every test names under "RED UNDER" the production mutation that turns it red. -// One change from the seed: `testTheBoundsAreTheRegisteredOnesAndTheBinsTheFitsOwn` held the bin constants equal to the -// ring fit's (EllipseCalibration, slice 3b); here it holds them to the literals 36 and 30, and slice 3b adds the equality. +// One change from the seed: `testTheBoundsAreTheDocumentedOnesAndTheBinsTheFitsOwn` held the bin constants equal to the +// ring fit's (EllipseCalibration); here it holds them to the literals 36 and 30, and EllipseCalibrationTests adds the +// equality. // import DiffractoKitTestSupport @@ -38,7 +39,7 @@ struct RingCalibrationTests { private let lambda200 = 0.025079340450548003 private let lambda300 = 0.019687489006848796 - // Au {111}: d = 2.3546 Å (ROADMAP), at 200 kV, R = 150 px: + // Au {111}: d = 2.3546 Å, at 200 kV, R = 150 px: // λ/2d = 0.025079340450548003 / 4.7092 = 0.0053256052940091742; 2θ = 2 arcsin(.) = 0.010651260937060768 rad; // tan 2θ = 0.010651663748250814; L_px = 150 / tan 2θ = 14082.307097295722; // Δq = (1/d)/R = 0.4247005860868088 / 150 = 0.0028313372405787253 Å⁻¹ per pixel; @@ -50,7 +51,7 @@ struct RingCalibrationTests { private let deltaQAu111R150 = 0.0028313372405787253 private let lengthMMAu111At200P14 = 197.15229936214011 - // Au {422} at 80 kV, d = 0.8325 Å (ROADMAP's example, a/√24 with a = 4.0782 Å is 0.83246), R = 150 px: + // Au {422} at 80 kV, d = 0.8325 Å (a/√24 with a = 4.0782 Å is 0.83246), R = 150 px: // λ/2d = 0.041757160772834212 / 1.665 = 0.025079375839540067; 2θ = 0.050164011268796932 rad; // tan 2θ = 0.050206131707820426; L_px = 150 / tan 2θ = 2987.6828765247223. private let dAu422 = 0.8325 @@ -58,9 +59,9 @@ struct RingCalibrationTests { private let lengthPixelsAu422At80R150 = 2987.6828765247223 // The small-angle form L = R d / λ, divided by the exact L, minus 1 (= tan 2θ · d/λ − 1): - // 200 kV Au {111}, R = 150: 14082.906234971683 / 14082.307097295722 − 1 = 4.2545420422e-5 (ROADMAP: "4e-5") - // 80 kV Au {422}, R = 150: 2990.5050460528300 / 2987.6828765247223 − 1 = 9.4460143353e-4 (ROADMAP: "9.5e-4"; - // to two figures it is 9.4e-4: the registered figure is rounded up by 0.6 %) + // 200 kV Au {111}, R = 150: 14082.906234971683 / 14082.307097295722 − 1 = 4.2545420422e-5 (often quoted as "4e-5") + // 80 kV Au {422}, R = 150: 2990.5050460528300 / 2987.6828765247223 − 1 = 9.4460143353e-4 (often quoted as "9.5e-4"; + // to two figures it is 9.4e-4: the quoted figure is rounded up by 0.6 %) private let smallAngleExcessAu111At200 = 4.2545420422e-5 private let smallAngleExcessAu422At80 = 9.4460143353e-4 @@ -95,8 +96,7 @@ struct RingCalibrationTests { assertClose(RingCalibration.wavelengthAngstroms(volts: 300_000), lambda300, "300 kV") } - /// A second source for the same four wavelengths: the study's script (docs/archive/s3-camera-length-plan-2026-10-12.md - /// §2.1, "[calc]"), 80 kV 4.17572 pm, 120 kV 3.34922, 200 kV 2.50793, 300 kV 1.96875, given to six figures (so to + /// A second source for the same four wavelengths: an independent calculation written for the study, 80 kV 4.17572 pm, 120 kV 3.34922, 200 kV 2.50793, 300 kV 1.96875, given to six figures (so to /// 3e-6 relative here). It was written before this file and does not share its arithmetic. /// RED UNDER: kV as V; non-relativistic λ; a wrong constant. @Test func testWavelengthAgreesWithTheStudiesSixFigureValues() { @@ -151,18 +151,18 @@ struct RingCalibrationTests { } /// The small-angle camera constant λL = Rd is not used: the exact L differs from it by tan 2θ·d/λ − 1, which is - /// 4.3e-5 at 200 kV Au {111} (ROADMAP: 4e-5) and 9.4e-4 at 80 kV Au {422} (ROADMAP: 9.5e-4). + /// 4.3e-5 at 200 kV Au {111} (quoted as 4e-5) and 9.4e-4 at 80 kV Au {422} (quoted as 9.5e-4). /// RED UNDER: small-angle L (the difference is then 0); `radius / tan(twoTheta)` written `radius / twoTheta` (the - /// difference is then 4.7e-6 at 200 kV Au {111}, against the registered 4e-5). - @Test func testTheExactLengthDiffersFromTheSmallAngleOneByTheRegisteredAmount() throws { + /// difference is then 4.7e-6 at 200 kV Au {111}, against the quoted 4e-5). + @Test func testTheExactLengthDiffersFromTheSmallAngleOneByTheQuotedAmount() throws { let exactA = try #require(RingCalibration.calibrate(radius: 150, d: dAu111, highTensionVolts: 200_000).lengthPixels) #expect(abs((150 * dAu111 / lambda200 / exactA - 1) - smallAngleExcessAu111At200) <= 1e-11) let exactB = try #require(RingCalibration.calibrate(radius: 150, d: dAu422, highTensionVolts: 80_000).lengthPixels) #expect(abs((150 * dAu422 / lambda80 / exactB - 1) - smallAngleExcessAu422At80) <= 1e-11) - // The registration's own figures, quoted, against what production gives (not against the literals above): 4e-5 + // The quoted figures, against what production gives (not against the literals above): 4e-5 // to one figure, and 9.5e-4 within 1 % of the worked 9.446e-4. - #expect(abs((150 * dAu111 / lambda200 / exactA - 1) - (4e-5)) <= 5e-6, "ROADMAP: 4e-5 at 200 kV Au {111}") - #expect(abs((150 * dAu422 / lambda80 / exactB - 1) - (9.5e-4)) <= 1e-5, "ROADMAP: 9.5e-4 at 80 kV Au {422}") + #expect(abs((150 * dAu111 / lambda200 / exactA - 1) - (4e-5)) <= 5e-6, "quoted: 4e-5 at 200 kV Au {111}") + #expect(abs((150 * dAu422 / lambda80 / exactB - 1) - (9.5e-4)) <= 1e-5, "quoted: 9.5e-4 at 80 kV Au {422}") } /// Δq = (1/d)/R for Au {111} (d = 2.3546 Å) at R = 150 px: 1/2.3546 = 0.4247005860868088 Å⁻¹, over 150 = @@ -222,7 +222,7 @@ struct RingCalibrationTests { #expect(r.lengthRefusal == .noHighTension) } - /// A ring whose d the wavelength cannot reach. This refusal is an addition beyond the registration, which lists no + /// A ring whose d the wavelength cannot reach. This refusal is an addition beyond the reference implementation, which has no /// Bragg bound (2θ ≥ 90° makes tan 2θ negative): λ/2d = 1 exactly (0.05 / 0.05) and 1.003 are refused, and so is 0.72, /// between 1/√2 = 0.7071 and 1, where tan 2θ is negative; 0.70 (2θ = 88.85°) is not. Δq is still given. /// RED UNDER: the bound removed (asin of 1.003 is NaN, of 1 is π/2); the bound at λ/2d ≥ 1 (0.72 would pass @@ -307,7 +307,7 @@ struct RingCalibrationTests { } /// R is (a + b)/2 and a/b − 1 uses the larger axis over the smaller, in either order. - /// RED UNDER: radius = a (the ROADMAP's `radius` mutation: 101.5 for 100, 1.5 %); radius = b; the axes taken as given + /// RED UNDER: radius = a (101.5 for 100, 1.5 %); radius = b; the axes taken as given /// when b > a (the ellipticity negative, never refused). @Test func testTheRadiusIsTheMeanOfTheSemiAxesAndTheEllipticityIsTheirRatioMinusOne() { var fit = cleanFit() @@ -320,7 +320,7 @@ struct RingCalibrationTests { } /// Model ≠ asymmetricGaussian: the conic's radius is not offered. The conic has no ring sigmas (nil). - /// RED UNDER: the conic accepted (the model check removed: ROADMAP's `refuse-conic`). + /// RED UNDER: the conic accepted (the model check removed). @Test func testAConicFitIsRefusedAsModelNotAsymmetricGaussian() { var fit = cleanFit() fit.model = .conic; fit.innerSigma = nil; fit.outerSigma = nil @@ -328,8 +328,8 @@ struct RingCalibrationTests { } /// Fewer than 30 of 36 bins: 29 is refused (with its count), 30 is not. - /// RED UNDER: the bound at ≤ 30 (30 refused) or ≥ 30 off by one (29 accepted); the bound removed (ROADMAP's - /// `refuse-gap`: a 120° gap ring has 24 of 36). + /// RED UNDER: the bound at ≤ 30 (30 refused) or ≥ 30 off by one (29 accepted); the bound removed (a 120° gap ring + /// has 24 of 36). @Test func testFewerThanThirtyOfThirtySixBinsIsRefusedByCount() { var fit = cleanFit() fit.occupiedAngularBins = 30 @@ -342,7 +342,7 @@ struct RingCalibrationTests { } /// A fit accepted only because the caller asserted one ring in a sparse annulus is refused, even at full coverage. - /// RED UNDER: `sparseCoverage` ignored (ROADMAP's `acceptSparseCoverage: true`). + /// RED UNDER: `sparseCoverage` ignored (`acceptSparseCoverage: true`). @Test func testASparseCoverageFitIsRefusedEvenWhenAllBinsAreOccupied() { var fit = cleanFit() fit.sparseCoverage = true @@ -351,7 +351,7 @@ struct RingCalibrationTests { } /// A ring sigma at or above the annulus width is refused (the larger of the two is reported); just below is not. - /// RED UNDER: the sigma bound unchecked (ROADMAP's `refuse-bound`: the 1/r³ background plant's σ is the fit's bound + /// RED UNDER: the sigma bound unchecked (the 1/r³ background plant's σ is the fit's bound /// of 243 px at a 40 px annulus); the bound at > rather than ≥; only the inner or only the outer sigma tested. @Test func testARingSigmaAtOrAboveTheAnnulusWidthIsRefusedAndTheLargerIsReported() { var fit = cleanFit() // annulus width 40 @@ -399,7 +399,7 @@ struct RingCalibrationTests { } /// No high tension: refused by name (the refusal that leaves Δq given), and the same fit with V is not. - /// RED UNDER: V defaulted to 200 kV (ROADMAP's `refuse-noV`). + /// RED UNDER: V defaulted to 200 kV. @Test func testAMissingHighTensionIsRefusedByName() { #expect(RingCalibration.refusals(cleanFit(), highTensionVolts: nil) == [.noHighTension]) #expect(RingCalibration.refusals(cleanFit(), highTensionVolts: 80_000) == []) @@ -429,11 +429,11 @@ struct RingCalibrationTests { only({ _ in }, volts: .nan, "NaN high tension") } - /// Several refusals at once are all listed, in the registered order: model, coverage, sigma, residual, ellipticity, + /// Several refusals at once are all listed, in the documented order: model, coverage, sigma, residual, ellipticity, /// high tension. Here all six, from a conic fit that carries sigmas (as a careless caller might pass). /// RED UNDER: the order changed; a refusal dropped from the list once an earlier one has fired (a first-match /// return). - @Test func testEveryRefusalThatAppliesIsListedInTheRegisteredOrder() { + @Test func testEveryRefusalThatAppliesIsListedInTheDocumentedOrder() { let fit = RingCalibration.FitReport(model: .conic, semiAxisA: 120, semiAxisB: 100, occupiedAngularBins: 20, sparseCoverage: true, normalizedResidual: 0.3, innerSigma: 50, outerSigma: 60, annulusWidth: 40) @@ -477,10 +477,10 @@ struct RingCalibrationTests { #expect(Set(all.map(\.name)).count == 8, "eight cases, eight names") } - /// The bin bounds this file registers equal the fit's own constants (if a re-sync of the copied fit moves one, this - /// goes red and the registration is re-read), and the residual and ellipticity caps are the registered 0.22 and 0.093. - /// RED UNDER: either bin constant changed here, (slice 3b adds the equality with the ring fit's own); either cap moved. - @Test func testTheBoundsAreTheRegisteredOnesAndTheBinsTheFitsOwn() { + /// The bin bounds this file states equal the fit's own constants (if a re-sync of the copied fit moves one, this + /// goes red), and the residual and ellipticity caps are the documented 0.22 and 0.093. + /// RED UNDER: either bin constant changed here; either cap moved. + @Test func testTheBoundsAreTheDocumentedOnesAndTheBinsTheFitsOwn() { #expect(RingCalibration.angularBinCount == 36) #expect(RingCalibration.minimumOccupiedAngularBins == 30) #expect(RingCalibration.maximumResidual == 0.22) diff --git a/Tests/DiffractoGeometryTests/RingFitAmendmentTests.swift b/Tests/DiffractoGeometryTests/RingFitAmendmentTests.swift index 2e4e11e..0e28a71 100644 --- a/Tests/DiffractoGeometryTests/RingFitAmendmentTests.swift +++ b/Tests/DiffractoGeometryTests/RingFitAmendmentTests.swift @@ -1,9 +1,9 @@ // // RingFitAmendmentTests.swift -// DiffractoKit 0.1.0: the ring-fit amendment (docs/registration-0.0.4.md §1). Each slice 3b Gate B gap (registration-slice-3 -// §7, F1, F2, F3, F6, F7) and the reference implementation's `refuse-bound` plant, on plants rendered here with fresh parameters and seeds (not -// the refuter's). Every test names under "RED UNDER" the production mutation that turns it red -// (tools/kit-0.0.4/mutants-ringfit.py). +// DiffractoKit 0.1.0: the ring-fit evidence. Each gap an independent review found in the ring fit (noise in a gap, things +// that are not one ring, isolated impulses, scale, one sharp edge) and the reference implementation's `refuse-bound` +// plant, on plants rendered here with fresh parameters and seeds (not the reviewer's). Every test names under +// "RED UNDER" the production mutation that turns it red. // // The plant renderer: a function of (row offset, column offset) from the plant's centre, float32, rows x columns with // columns fastest, plus Gaussian noise from SplitMix64 + Box-Muller (the parity renderer's generator, seeded here). @@ -87,10 +87,10 @@ struct RingFitAmendmentTests { static func names(_ result: RingFitCalibration) -> [String] { result.refusals.map(\.name) } - // MARK: F1: noise in an angular gap + // MARK: Noise in an angular gap /// A ring with a 95-125 degree gap and Gaussian noise is not calibrated: the noise makes pixels in the gap bright, not a - /// ring at the fitted radius. Slice 3b's F1 calibrated such rings with the centre 0.7-2.7 px off and R -0.9 to -3.5 %. + /// ring at the fitted radius. Before 0.1.0 such rings were calibrated with the centre 0.7-2.7 px off and R -0.9 to -3.5 %. /// RED UNDER: the evidence's occupancy bound (alpha >= 0.5 A) dropped; the coverage refusal from the evidence removed. @Test(arguments: [(95.0, 0.30, UInt64(11)), (110.0, 0.25, UInt64(12)), (125.0, 0.35, UInt64(13)), (110.0, 0.40, UInt64(14))]) func aNoisyRingWithAWideGapIsNotCalibrated(gap: Double, noise: Double, seed: UInt64) throws { @@ -104,8 +104,8 @@ struct RingFitAmendmentTests { /// The same rings without the gap are still calibrated (the evidence does not refuse noise on a whole ring). The radius /// band is 1 %: at noise 0.4 of the peak the profile's radius trades against its asymmetry (σ_in 3.27, σ_out 2.76 fitted - /// on a symmetric 3 px ring) and reads +0.78 %, before and after 0.1.0 (registration-0.0.4 §1.8 amendment; a radius - /// uncertainty is the reference implementation's ask G3). + /// on a symmetric 3 px ring) and reads +0.78 %, before and after 0.1.0 (a radius uncertainty is an open ask of the + /// reference implementation). /// RED UNDER: the significance bound raised (alpha >= 30 SE). @Test(arguments: [(0.30, UInt64(11)), (0.40, UInt64(14))]) func theSameNoisyRingWithoutAGapIsCalibrated(noise: Double, seed: UInt64) throws { @@ -122,7 +122,7 @@ struct RingFitAmendmentTests { /// A ring whose 120 degrees hold 0.35 of its amplitude (noise-free, so every sector is significant; above the pre-fit /// coverage threshold of 0.2 of the range, so the fit runs) is not covered: - /// a sector holds the ring only at half the median amplitude (registration-0.0.4 §1.3). + /// a sector holds the ring only at half the median amplitude. /// RED UNDER: the occupancy fraction lowered from 0.5 to 0.05. @Test func aRingWithAWeakSectorIsNotCovered() throws { let pattern = Self.render { dr, dc in @@ -162,9 +162,9 @@ struct RingFitAmendmentTests { == ["notARingPeak", "ringGeometryInconsistent", "noHighTension"]) } - // MARK: F2: things that are not one ring + // MARK: Things that are not one ring - /// A bright disk's blurred edge is not a ring: its profile does not fall on the inner side. Slice 3b's F2 calibrated + /// A bright disk's blurred edge is not a ring: its profile does not fall on the inner side. Before 0.1.0 the fit calibrated /// such edges at -10 to -18 %. /// RED UNDER: the peak test's inner drop not required. @Test(arguments: [1.0, 2.5]) @@ -188,7 +188,7 @@ struct RingFitAmendmentTests { } /// A circular ring whose intensity varies as 0.25 + 0.75 cos^2 of the azimuth (texture) is not read as an ellipse: - /// slice 3b's F2 read such a ring as 4.6 % ellipticity. The kit either fits it as the circle it is (b/a within 0.2 %) + /// before 0.1.0 the fit read such a ring as 4.6 % ellipticity. The kit either fits it as the circle it is (b/a within 0.2 %) /// or refuses it by name. /// RED UNDER: the geometry check's ellipse shift bound removed. @Test(arguments: [(36.0, 0.0), (36.0, 30.0), (48.0, 75.0)]) @@ -206,7 +206,7 @@ struct RingFitAmendmentTests { } } - /// Two rings closer than about twice their width are one ring to the fit (registration-0.0.4 §1.6, not claimed): the + /// Two rings closer than about twice their width are one ring to the fit (not claimed): the /// fitted radius lies between them, within the separation of the stronger. A bound, not a refusal. @Test(arguments: [(1.5, 0.7), (3.0, 0.7), (3.0, 1.0)]) func twoRingsCloserThanTheirWidthAreFittedBetweenThem(separation: Double, secondAmplitude: Double) throws { @@ -220,7 +220,7 @@ struct RingFitAmendmentTests { } } - /// A second ring inside the annulus never gives a wrong number (Gate B on the 0.1.0 candidate, F1: such fits landed on the other ring + /// A second ring inside the annulus never gives a wrong number (before 0.1.0 such fits landed on the other ring /// or between the two, 1-2.3 % off): the calibration is refused by name, or reads the intended ring within 0.3 %. /// Measured: at 9 px (3σ) the dip between the rings is 0.23 of the amplitude, under the 0.25 bound, and the fit reads /// R 44.096 (0.2 %); at 14 and 24 px it is refused as secondRingInAnnulus. @@ -250,10 +250,10 @@ struct RingFitAmendmentTests { #expect(result.calibration != nil, "refused as \(Self.names(result))") } - // MARK: F3: isolated impulses + // MARK: Isolated impulses /// One hot pixel on the ring (5x, 20x, 1000x its peak) does not refuse a perfect ring, and one strongly negative pixel - /// does not move it: the fit equals the ring without it within 0.05 px and 0.05 %. Slice 3b's F3 refused the hot one as + /// does not move it: the fit equals the ring without it within 0.05 px and 0.05 %. Before 0.1.0 the fit refused the hot one as /// insufficientAngularCoverage(1). /// RED UNDER: the impulse exclusion removed; its factor raised from 2 to 2000 robust ranges. @Test(arguments: [5.0, 20.0, 1000.0, -5.0, -50.0]) @@ -274,13 +274,13 @@ struct RingFitAmendmentTests { #expect(result.calibration != nil) } - // MARK: F6: scale + // MARK: Scale /// The fit does not depend on the pattern's scale. Ranges below 0.5 run normalised into [0.5, 1) by an exact power of /// two, so a ring of amplitude 2^-40 gives the amplitude-2^-3 fit bit for bit; across scales (2^-40, 1e-6, 1e-3, 1e3, /// 1e6 against amplitude 1, background in proportion) the fit agrees within 1e-6 relative (the Float pixels round s·v - /// unlike v by up to 6e-8 relative, and the optimisers' step scales differ by less than 2 between the regimes; - /// registration amended). Slice 3b's F6 refused amplitudes of 1e-6 and below as insufficientSignal. + /// unlike v by up to 6e-8 relative, and the optimisers' step scales differ by less than 2 between the regimes). + /// Before 0.1.0 the fit refused amplitudes of 1e-6 and below as insufficientSignal. /// RED UNDER: the power-of-two normalisation removed (the 2^-40 case refused); the signal test made absolute again. @Test(arguments: [0x1p-40, 1e-6, 1e-3, 1e3, 1e6]) func theFitDoesNotDependOnScale(scale: Double) throws { @@ -303,10 +303,10 @@ struct RingFitAmendmentTests { } } - // MARK: F7: one sharp edge + // MARK: One sharp edge - /// A ring with one edge sharper than the 0.30 px floor guard is fitted and calibrated when the data hold the ring - /// (registration-0.0.4 §1.5). Slice 3b's F7 refused it as ringWidthAtFloor. + /// A ring with one edge sharper than the 0.30 px floor guard is fitted and calibrated when the data hold the ring. + /// Before 0.1.0 the fit refused it as ringWidthAtFloor. /// RED UNDER: the one-sided floor kept as a refusal. @Test(arguments: [0.15, 0.05]) func aRingWithOneSharpEdgeIsCalibrated(sigmaOut: Double) throws { @@ -326,7 +326,7 @@ struct RingFitAmendmentTests { // MARK: the reference implementation's refuse-bound plant (its ring-fit-parity line 37) /// A 3 px ring at R 120 on a 1/r^3 background at the ring's peak, annulus +-20 px: the fit reads the ring and the kit - /// calibrates it (Δq within 0.05 %); `sigmaAtAnnulusWidth` was the plant's wrong expectation (registration-0.0.4 §1.7). + /// calibrates it (Δq within 0.05 %); `sigmaAtAnnulusWidth` was the plant's wrong expectation. @Test func referenceRefuseBoundPlantIsCalibrated() throws { let scene = Scene(rows: 320, cols: 316, row0: 158.41, col0: 160.27) let pattern = Self.render(scene) { dr, dc in @@ -338,8 +338,8 @@ struct RingFitAmendmentTests { #expect(abs(calibration.deltaQ / ((1 / 2.3546) / 120) - 1) < 5e-4) } - /// A ring whose sigma reaches the annulus width is refused by name (σ 16 px in a 16 px annulus; registration-0.0.4 §1.7 - /// amended: σ 8 px there is half the width, fitted and refused for its geometry). + /// A ring whose sigma reaches the annulus width is refused by name (σ 16 px in a 16 px annulus; + /// σ 8 px there is half the width, fitted and refused for its geometry). @Test func aRingWiderThanItsAnnulusIsRefusedAsSigmaAtAnnulusWidth() throws { let pattern = Self.render { dr, dc in Self.ring(dr, dc, radius: 40, sigmaIn: 16, sigmaOut: 16) + 0.1 } let result = try? Self.calibrate(pattern, inner: 32, outer: 48) @@ -352,7 +352,7 @@ struct RingFitAmendmentTests { } } -// MARK: G1 and G7 (registration-0.0.4 §2, §4) +// MARK: The fit and calibration split, and cancellation struct RingFitSplitAndCancellationTests { typealias T = RingFitAmendmentTests @@ -439,7 +439,7 @@ struct RingFitSplitAndCancellationTests { } #expect(calls.values.count == 4) - // Monotone also through the one-sided-floor refit (Gate B on the 0.1.0 candidate, F2: it reported backwards by up to 0.04). + // Monotone also through the one-sided-floor refit (F2: it reported backwards by up to 0.04). let sharp = RingFitAmendmentTests.render { dr, dc in RingFitAmendmentTests.ring(dr, dc, radius: 40, sigmaIn: 3, sigmaOut: 0.1) } let sharpBox = Box() diff --git a/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift b/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift index 4443016..8f71722 100644 --- a/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift @@ -1,10 +1,10 @@ // // RingFitCalibrationTests.swift -// DiffractoKit slice 3b: the fit-driven calibration (`RingCalibration.fit`) on rings planted here, in Swift, with their truth +// The fit-driven calibration (`RingCalibration.fit`) on rings planted here, in Swift, with their truth // known. Ported from reference commit 9482831 Tests/RingFitCalibrationTests.swift (8 tests, same names, same plant, same bands), // RUN ON THE KIT'S FIT, which is reference commit 7d6fdbd1's `EllipseCalibration` (ADR 069: two starts and the 0.30 px floor guard); // the reference implementation's own copy there is the pre-ADR-069 file. No number of these eight moved: every assertion holds as the reference implementation wrote it -// (registration section 2 said they might; the measured values are in tools/slice-3b/README.md). +// (said they might, the measured values are in tools/slice-3b/README.md). // // The plant is the one tools/ring-fit-parity's plants.py draws: a Gaussian ring on an elliptical coordinate, // I = exp(-((rho - 1) R)^2 / 2s^2), rho^2 = u^2/a^2 + v^2/b^2 in the frame rotated by theta, R = (a + b)/2, inside the @@ -76,8 +76,8 @@ struct RingFitCalibrationTests { // MARK: A clean ring - /// The planted ring calibrates to its own Δq and L within the registered 0.1 %, its centre within 0.05 px, and the - /// whole chain is the closed forms of slice 3a applied to the fitted radius. + /// The planted ring calibrates to its own Δq and L within the documented 0.1 %, its centre within 0.05 px, and the + /// whole chain is the closed forms of RingCalibration applied to the fitted radius. /// RED UNDER: the radius handed to `calibrate` taken as `fit.a` (1.0 % off at 2 % ellipticity); the d argument /// ignored for a fixed one. (Swapping the START centre's row and column is not caught: the fit converges from that /// 5 px-off start to the same ring; measured by the reference implementation 2026-10-13.) diff --git a/Tests/DiffractoGeometryTests/RingIndexingTests.swift b/Tests/DiffractoGeometryTests/RingIndexingTests.swift index dad5f69..04bd6a2 100644 --- a/Tests/DiffractoGeometryTests/RingIndexingTests.swift +++ b/Tests/DiffractoGeometryTests/RingIndexingTests.swift @@ -1,12 +1,12 @@ // // RingIndexingTests.swift -// The indexing decision (registration docs/registration-slice-3.md §1 "RingIndexing", §4 "Truth"), on the kit's own ring -// lists (slice 2's `kinematicReflections(kMax: 1.2).families`). It ports every test of reference commit 9482831 +// The indexing decision ("RingIndexing", "Truth"), on the kit's own ring +// lists (DiffractoScattering's `kinematicReflections(kMax: 1.2).families`). It ports every test of reference commit 9482831 // `DSpacingIndexingTests` (28) and `MonoclinicIndexingTests` (13) that concerns the decision or the family lists, -// re-expressed on the kit's rings; the ones that test what slices 1 and 2 own are mapped to a comment at the end. +// re-expressed on the kit's rings; the ones that test what DiffractoCore and DiffractoScattering own are mapped to a comment at the end. // The dyadic lists are the kit's rings of an exactly-scaled orthorhombic cell (a 2, b 1, c 1/2 Å: g = h/2, k, 2 l, so // g in {1, 1.5, 2, 3, ...} is exact), so z, the k test, the margin and the reach are pinned to the last bit. -// Each test names the source mutants (tools/slice-3/mutants.py or the break-once run) that turn it red. +// Each test names the source mutations that turn it red. // import DiffractoKitTestSupport @@ -229,7 +229,7 @@ struct RingIndexingTests { #expect(Index.minimumMargin == log(19.0)) } - /// The registered margin plant: Au {331} -> {420}, sigma = gap/5.7, z = 2.5 (next 3.2, margin 1.995 < ln 19): + /// The margin plant stated before the code: Au {331} -> {420}, sigma = gap/5.7, z = 2.5 (next 3.2, margin 1.995 < ln 19): /// `ambiguous`. (reference testAuMarginPlantIsAmbiguous.) Mutants: the margin clause dropped; the margin as z2 - z1. @Test func testAuMarginPlantIsAmbiguous() throws { let au = try IndexPresets.rings(IndexPresets.gold) @@ -260,7 +260,7 @@ struct RingIndexingTests { } /// q + R = k_max exactly is accepted (the clause is `q + R > k_max`): the same measurement with k_max one ulp below the - /// reach is `beyondKMax`, one ulp above is accepted. (The registration's "q + R = kMax exactly" boundary.) Mutant: + /// reach is `beyondKMax`, one ulp above is accepted. (The "q + R = kMax exactly" boundary.) Mutant: /// `>=` for `>`. @Test func testReachEqualToKMaxIsAccepted() throws { let list = try IndexPresets.dyadic([1, 1.5, 2]) @@ -410,7 +410,7 @@ struct RingIndexingTests { // MARK: The refusals, by name and in order /// `invalidInput` from `index`: q, sigma, k and k_max each at 0, -1, NaN, +/-infinity. (reference - /// testInvalidInputIsRefusedByName; the list builder's own k_max refusal is slice 1's `ReflectionListError.invalidKMax`, + /// testInvalidInputIsRefusedByName; the list builder's own k_max refusal is DiffractoCore's `ReflectionListError.invalidKMax`, /// tested in ScatteringTruthTests.testRefusals.) Mutants: sigma = 0 accepted, k_max unchecked. @Test func testInvalidInputIsRefusedByName() throws { let au = try IndexPresets.rings(IndexPresets.gold) @@ -464,7 +464,7 @@ struct RingIndexingTests { // MARK: The overloads, and beta'' - /// The two overloads (slice 2's `KinematicFamily` and slice 1's `ReflectionFamily`) give one answer, in the same + /// The two overloads (DiffractoScattering's `KinematicFamily` and DiffractoCore's `ReflectionFamily`) give one answer, in the same /// payloads, for an accepted, an ambiguous and a refused plant. @Test func testKinematicAndReflectionOverloadsAgree() throws { let kin = try IndexPresets.mSq.kinematicReflections(kMax: 1.2).families @@ -499,7 +499,7 @@ struct RingIndexingTests { #expect(Set(c.map(\.family.name)).isSuperset(of: ["{3 1 5}", "{7 3 0}"]), "every ring within k sigma is named, these two among them") } - /// reference ADR 024, registered: at s = 0 (q = g of each ring) and sigma_q = u q, the number of beta'' rings that are + /// reference ADR 024, stated before the code: at s = 0 (q = g of each ring) and sigma_q = u q, the number of beta'' rings that are /// accepted is 8 at u = 1e-2, 23 at 3e-3 and 4 at 3e-2; at 1e-2 they are {2 0 0}, {0 0 1}, {2 0 -1}, {2 0 1}, {1 1 0}, /// {1 1 -1}, {3 1 0}, {3 1 -1}. Mutants: z <= k to z < k, the margin clause moved, reach without the next ring. @Test func testBetaDoublePrimeAcceptedCountsAtZeroOffset() throws { @@ -589,7 +589,7 @@ struct RingIndexingTests { #expect(o10m1.d > o101.d) } - /// M-C: no member with h + k odd; beta'' (C-centred) the same, and its first ten rings are the registered ones (d to 4 + /// M-C: no member with h + k odd; beta'' (C-centred) the same, and its first ten rings are the predicted ones (d to 4 /// decimals, multiplicity, name). (reference testCentredCellsAndTheFirstBetaFamilies.) Mutant: absences kept. @Test func testCentredCellsAndTheFirstBetaFamilies() throws { for s in [IndexPresets.mC, IndexPresets.betaDoublePrime] { @@ -617,7 +617,7 @@ struct RingIndexingTests { let a = try family(l, containing: hkl(1, 0, 0)) #expect(a.name == "{1 0 0} + {0 0 1}") #expect(a.multiplicity == 4) - // With its full symmetry (slice 1b) the same cell's {1 0 0} and {0 0 1} are one orbit, not a coincidence. + // With its full symmetry the same cell's {1 0 0} and {0 0 1} are one orbit, not a coincidence. let full = try IndexPresets.rings(IndexPresets.mSqFull) let merged = try family(full, containing: hkl(1, 0, 0)) #expect(merged.multiplicity == 4) @@ -650,7 +650,7 @@ struct RingIndexingTests { /// beta'' through CIFImport: 22 sites, space group 12, 417 rings, 1442 reflections at k_max 1.2, 113 rings of /// multiplicity 2 and 304 of 4, none with several orbits. (reference testBetaDoublePrimeFromCIFText; the CIF reading - /// itself is slice 1's CIFImportTests.) + /// itself is DiffractoCore's CIFImportTests.) @Test func testBetaDoublePrimeFromCIFText() throws { let s = IndexPresets.betaDoublePrime #expect(s.sites.count == 22) diff --git a/Tests/DiffractoGeometryTests/ZoneAxisTests.swift b/Tests/DiffractoGeometryTests/ZoneAxisTests.swift index 68b433c..14a59ea 100644 --- a/Tests/DiffractoGeometryTests/ZoneAxisTests.swift +++ b/Tests/DiffractoGeometryTests/ZoneAxisTests.swift @@ -1,8 +1,8 @@ // // ZoneAxisTests.swift -// Zone-axis primitives (registration docs/registration-slice-3.md §1 "ZoneAxis", §4 "Truth"): the 49 low-index axes by +// Zone-axis primitives ("ZoneAxis", "Truth"): the 49 low-index axes by // value, the Cartesian axis of a cell against closed forms and the zone law, the detector basis, and the projection with -// the flat and the curved Ewald sphere. Source mutants (break-once): the detector-basis reference threshold, the sign of +// the flat and the curved Ewald sphere. Source mutations (each broken once): the detector-basis reference threshold, the sign of // the curvature term, the slab test (<= to <), the Cartesian axis over the reciprocal rows. // @@ -288,7 +288,7 @@ struct ZoneAxisTests { /// The DEVIATION in sign, stated and tested: py4DSTEM f050d207 writes s_g = (2 g_z - lambda |g|^2)/(2 - 2 lambda g_z) with /// k0 = [0, 0, -1/lambda], and its lab z is the NEGATIVE of this beam direction, so g_z = -(g . n). To first order /// (the denominator dropped) its s_g is then -(g . n) - lambda |g|^2 / 2, exactly the negative of this kit's s_g. A - /// mutant with the opposite curvature sign fails here and in testCurvedSphereAddsHalfLambdaGSquared. + /// mutation with the opposite curvature sign fails here and in testCurvedSphereAddsHalfLambdaGSquared. @Test func testCurvatureSignIsTheNegativeOfPy4DSTEMsInTheLabFrameOfTheBeam() throws { let s = IndexPresets.magnesium let n = try #require(ZoneAxis(0, 0, 1).cartesian(in: s.cell)) diff --git a/Tests/DiffractoScatteringTests/ConventionalRingsTests.swift b/Tests/DiffractoScatteringTests/ConventionalRingsTests.swift index ae05fc9..20c5d35 100644 --- a/Tests/DiffractoScatteringTests/ConventionalRingsTests.swift +++ b/Tests/DiffractoScatteringTests/ConventionalRingsTests.swift @@ -1,6 +1,6 @@ // // ConventionalRingsTests.swift -// Slice 1b truth that needs slice 2's cut (registration docs/registration-slice-1b.md section 3): the rings after the +// Conventional-setting truth that needs the |F| cut: the rings after the // |F| cut of a PRIMITIVE cell, taken in its conventional setting, are the conventional cell's rings exactly (names, // g to 1e-12, multiplicities): Au 8, Si 10, and β″'s 417 rings from 1442 kept reflections. // diff --git a/Tests/DiffractoScatteringTests/PolicyTests.swift b/Tests/DiffractoScatteringTests/PolicyTests.swift index 53f45cd..7033003 100644 --- a/Tests/DiffractoScatteringTests/PolicyTests.swift +++ b/Tests/DiffractoScatteringTests/PolicyTests.swift @@ -1,10 +1,10 @@ // // PolicyTests.swift -// DiffractoKit 0.2.0 (docs/registration-0.2.0.md §1, §2): the policies of DiffractoScattering. The |F| cut is a parameter -// since slice 2; the list's refusal bounds and ring rule and the structure validation reach the kinematic list through -// `policy:` and `validation:`. Tier 2: a non-default value moves the outcome in the stated direction, an invalid one is +// DiffractoKit 0.2.0: the policies of DiffractoScattering. The |F| cut has always been a +// parameter; the list's refusal bounds and ring rule and the structure validation reach the kinematic list through +// `policy:` and `validation:`. A non-default value moves the outcome in the stated direction, an invalid one is // refused or taken as documented. Every test names under "RED UNDER" the production mutation that turns a default-path test -// red (tools/kit-0.2.0/mutants-policy.py). +// red. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoScatteringTests/ScatteringRefuterTests.swift b/Tests/DiffractoScatteringTests/ScatteringRefuterTests.swift index 149da01..96dbb37 100644 --- a/Tests/DiffractoScatteringTests/ScatteringRefuterTests.swift +++ b/Tests/DiffractoScatteringTests/ScatteringRefuterTests.swift @@ -1,6 +1,6 @@ // // ScatteringRefuterTests.swift -// The Gate B refuter's findings on slice 2 (be6b832; registration slice 2 §8, amendment of 2026-10-15), each a test. +// An independent review's findings on the structure factors, each a test. // import DiffractoKitTestSupport @@ -18,7 +18,7 @@ struct ScatteringRefuterTests { + fcc.map { AtomSite(z: 17, fractional: wrapFractional($0 + SIMD3(0.5, 0.5, 0.5))) }) } - /// F2: a NaN or infinite form factor (for every element or one) is refused at the first reflection, never scored as + /// a NaN or infinite form factor (for every element or one) is refused at the first reflection, never scored as /// nothing kept. Mutant: the finiteness check removed. @Test func testNonFiniteFormFactorsAreRefused() { let s = Self.rockSalt @@ -29,7 +29,7 @@ struct ScatteringRefuterTests { } } - /// F2: an invalid structure (NaN occupancy, infinite coordinate, occupancy 1.5, no sites) is refused before any F. + /// an invalid structure (NaN occupancy, infinite coordinate, occupancy 1.5, no sites) is refused before any F. /// Mutant: the validation not called. @Test func testInvalidStructuresAreRefused() { let cell = UnitCell.cubic(a: 5) @@ -46,7 +46,7 @@ struct ScatteringRefuterTests { } } - /// F3: a form factor with no value at some g² names the element; one with no value for one element anywhere names it. + /// a form factor with no value at some g² names the element; one with no value for one element anywhere names it. @Test func testMissingFormFactorNamesTheElement() { let s = Self.rockSalt let noneAboveZero: FormFactor = { z, g in g > 0 ? nil : ElectronScatteringFactors.f(z: z, gSquared: g) } @@ -55,7 +55,7 @@ struct ScatteringRefuterTests { #expect(throws: KinematicError.unsupportedElements([17])) { try s.kinematicReflections(kMax: 1.2, formFactor: noChlorine) } } - /// F4: magnesium with ⅓ written as 0.333 keeps 24 operation-absent reflections above the cut (|F| up to 3e-4), in + /// magnesium with ⅓ written as 0.333 keeps 24 operation-absent reflections above the cut (|F| up to 3e-4), in /// extra rings (28 against 24, 306 kept against 270, the refuter's numbers); the list names them. With exact thirds there are none. Mutant: the property filtering nothing. @Test func testOperationAbsentReflectionsAboveTheCutAreNamed() throws { let coarse = Presets.structure(.hexagonal(a: 3.2094, c: 5.2108), @@ -71,13 +71,13 @@ struct ScatteringRefuterTests { #expect(list.families.count == 28 && exact.families.count == 24, "rings \(list.families.count), \(exact.families.count)") } - /// F5: a needle-shaped cell with a tiny volume but a long enumeration box is refused, not enumerated. + /// a needle-shaped cell with a tiny volume but a long enumeration box is refused, not enumerated. @Test func testNeedleCellIsRefused() { let needle = CrystalStructure(cell: .orthorhombic(a: 1e7, b: 1e-3, c: 1e-3) , sites: [], operations: [.identity]) #expect(throws: ReflectionListError.self) { try needle.reflections(kMax: 1.2) } } - /// F6: a coplanar cell (α = β = γ = 120°) whose volume is rounding is invalid, so no structure factor of 1e6 comes out. + /// a coplanar cell (α = β = γ = 120°) whose volume is rounding is invalid, so no structure factor of 1e6 comes out. @Test func testCoplanarCellIsInvalid() { let flat = UnitCell(a: 5, b: 5, c: 5, alphaDeg: 120, betaDeg: 120, gammaDeg: 120) #expect(flat.issues.contains(.degenerateVolume)) @@ -85,7 +85,7 @@ struct ScatteringRefuterTests { #expect(throws: KinematicError.self) { try s.kinematicReflections(kMax: 1.2) } } - /// F8: f_e is nil for a negative or non-finite g². + /// f_e is nil for a negative or non-finite g². @Test func testScatteringFactorDomain() { #expect(ElectronScatteringFactors.f(z: 13, gSquared: -0.1) == nil) #expect(ElectronScatteringFactors.f(z: 13, gSquared: .nan) == nil) diff --git a/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift b/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift index 7a8cc44..4aa3f6f 100644 --- a/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift +++ b/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift @@ -1,6 +1,6 @@ // // ScatteringTruthTests.swift -// Planted truth for slice 2 (registration docs/registration-slice-2.md §5), independent of every oracle: exact +// Planted truth for the structure factors, independent of every oracle: exact // extinctions, closed forms of F, the reference implementations' ring counts after the |F| cut, the weak cut, and f_e's own shape. // @@ -37,7 +37,7 @@ enum Presets { AtomSite(z: 29, fractional: [0, 0, 0], occupancy: 0.4)] + fcc.dropFirst().map { AtomSite(z: 29, fractional: $0) }) } - /// The β″ CIF exactly as the generator writes it (slice 1's fixture builder). + /// The β″ CIF exactly as the generator writes it (the core fixture builder). static var betaDoublePrime: CrystalStructure { let sites: [(String, String, Double, Double, Double, Double)] = [ ("Mg1", "Mg", 0.0, 0.0, 0.0, 0.5), ("Mg2", "Mg", 0.3459, 0.0, 0.089, 1.0), ("Mg3", "Mg", 0.430, 0.0, 0.652, 0.8), @@ -154,7 +154,7 @@ struct ScatteringTruthTests { #expect(dropped == [MillerIndex(3, 3, 3), MillerIndex(14, 0, 1)], "\(dropped)") } - /// β″ at 1.6 and 2.0 Å⁻¹: 953 and 1805 rings (the reference implementation's S2c predictions). + /// β″ at 1.6 and 2.0 Å⁻¹: 953 and 1805 rings (the reference implementation's predictions). @Test func testBetaDoublePrimeRingsAtHigherKMax() throws { let s = Presets.betaDoublePrime #expect(try s.kinematicReflections(kMax: 1.6).families.count == 953) @@ -216,7 +216,7 @@ struct ScatteringTruthTests { #expect(ElectronScatteringFactors.parameters.allSatisfy { $0.count == 10 }) } - /// Refusals: an element without a form factor, before any F; a bad cut; slice 1's k_max refusal passed through. + /// Refusals: an element without a form factor, before any F; a bad cut; DiffractoCore's k_max refusal passed through. @Test func testRefusals() throws { let rf = Presets.structure(.cubic(a: 4), [AtomSite(z: 104, fractional: [0, 0, 0])]) #expect(throws: KinematicError.unsupportedElements([104])) { try rf.kinematicReflections(kMax: 1.2) } From bb5bad9e2c660fc693881529b2a2f39cf8858f67 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:46:54 +0000 Subject: [PATCH 07/17] test(orientation, parity): drop references to documents outside the repo Comment, suite-name and known-issue text in DiffractoOrientationTests, DiffractoKitParityTests and DiffractoKitTestSupport: registration sections, amendments, gates and work-session names removed or named by subject; "registered" values renamed stated, documented or predicted. The apps-parity tests are left as they are: #18 retires them. No assertion changes. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .../EllipseCalibrationTests.swift | 2 +- .../Core/PinsTests.swift | 6 +-- .../Geometry/RingCalibrationPinsTests.swift | 18 ++++---- .../Geometry/RingCalibrationQTests.swift | 10 ++--- .../Geometry/RingIndexingPinsTests.swift | 18 ++++---- .../Orientation/Py4DSTEMFamilyTests.swift | 2 +- .../Orientation/Py4DSTEMRealFamilyTests.swift | 2 +- .../Orientation/Slice4PinsTests.swift | 45 +++++++++---------- .../Scattering/ScatteringPinsTests.swift | 24 +++++----- .../CoreFixtures.swift | 6 +-- .../IndexFixtures.swift | 6 +-- .../OrientationFixtures.swift | 2 +- .../MatrixChallengeTests.swift | 2 +- .../OrientationResultTests.swift | 6 +-- .../OrientationSymmetryTests.swift | 6 +-- .../PhaseReferenceLibrarySlabTests.swift | 2 +- .../PhaseVectorMatchingTests.swift | 6 +-- .../PhaseVectorResolutionTests.swift | 2 +- .../PhaseVectorReviewFindingTests.swift | 2 +- .../ReflectionOrderTests.swift | 2 +- .../ScanInputTests.swift | 8 ++-- .../TrigonalSettingTests.swift | 2 +- .../ZoneAxisFitTests.swift | 2 +- 23 files changed, 90 insertions(+), 91 deletions(-) diff --git a/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift b/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift index 6ded29b..47047ec 100644 --- a/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseCalibrationTests.swift @@ -141,7 +141,7 @@ struct EllipseCalibrationTests { // MARK: The annulus boundaries /// The conic's annulus is `inner < r <= outer` and the profile's `inner < r < outer` (kept for bit identity with the reference implementations). - On a lattice centred on a pixel the radii 10 and 20 are hit exactly by pixels such as (6, 8) and + /// On a lattice centred on a pixel the radii 10 and 20 are hit exactly by pixels such as (6, 8) and /// (12, 16), so the counts differ by exactly the pixels on the boundaries; the expected counts are worked in integers. /// RED UNDER: `<=` for `<` or `<` for `<=` at either outer boundary; `>=` for `>` at the inner one. @Test func testTheAnnulusBoundariesAreInnerExcludedAndOuterIncludedForTheConicOnly() throws { diff --git a/Tests/DiffractoKitParityTests/Core/PinsTests.swift b/Tests/DiffractoKitParityTests/Core/PinsTests.swift index 70b9747..52818a2 100644 --- a/Tests/DiffractoKitParityTests/Core/PinsTests.swift +++ b/Tests/DiffractoKitParityTests/Core/PinsTests.swift @@ -1,6 +1,6 @@ // // PinsTests.swift -// Agreement with pymatgen (registration section 4), read from slice-1-pins.json (tools/slice-1/pins.py, run in +// Agreement with pymatgen, read from slice-1-pins.json (tools/slice-1/pins.py, run in // ../References/python/orix before any Swift existed). Agreement is agreement, not truth; a difference is a failure of // the port, never a tolerance to widen. // @@ -34,7 +34,7 @@ struct PinsTests { private func assertMatrix(_ got: [SIMD3], _ want: [[Double]], scale: Double, _ name: String) { for i in 0..<3 { for j in 0..<3 { - // D17: numpy assert_allclose defaults (rtol 1e-7), with an absolute floor of 1e-12 of the matrix scale for exact zeros. + // numpy assert_allclose defaults (rtol 1e-7), with an absolute floor of 1e-12 of the matrix scale for exact zeros. #expect(abs(got[i][j] - want[i][j]) <= 1e-12 * scale + 1e-7 * abs(want[i][j]), "\(name) [\(i)][\(j)]") } } } @@ -120,7 +120,7 @@ struct PinsTests { #expect(s.operations.contains { $0.isEquivalent(to: op.operation, translationTolerance: 0.01 / max(s.cell.a, s.cell.b, s.cell.c)) }, "\(name): missing \(op.xyz)") } #expect(Set(s.laueGroup.rotations) == Set(pin.laueRotations.map { IntegerMatrix($0) }), "\(name) Laue rotations") - // The file's loop against the detected set (amendment 2). + // The file's loop against the detected set. #expect(s.fileOperations?.count == pin.fileOperations?.count, "\(name) file operations") #expect(s.fileOperationsAreSymmetries == pin.fileOperations?.subsetOfDetected, "\(name) file operations are symmetries") if let file = pin.fileOperations { diff --git a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift index b6801df..906a7e4 100644 --- a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift +++ b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift @@ -1,9 +1,9 @@ // // RingCalibrationPinsTests.swift -// DiffractoKit slice 3a (docs/registration-slice-3.md section 3): `ElectronWavelength` and `RingCalibration` against the +// `ElectronWavelength` and `RingCalibration` against the // pins tools/slice-3/ring-pins.py writes (Fixtures/slice-3-ring-pins.json): the closed forms evaluated in 60-digit decimal -// arithmetic, 320 cases, the refusal decision from the registered thresholds (68 plants), the Bragg limit (18 plants) and the -// radius (10 plants). Band 1e-7 relative (D17, 0.9.1; it was 1e-12); a difference is a failure of the port, never a band to widen. +// arithmetic, 320 cases, the refusal decision from the documented thresholds (68 plants), the Bragg limit (18 plants) and the +// radius (10 plants). Band 1e-7 relative (the reference's tolerance since 0.9.1; it was 1e-12); a difference is a failure of the port, never a band to widen. // Ported from reference commit 9482831 tools/ring-calibration-parity/main.swift. The fixture is read by #filePath (the Fixtures folder // is excluded from the target). // @@ -85,10 +85,10 @@ private func reported(_ refusal: RingCalibration.Refusal) -> Double? { struct RingCalibrationPinsTests { private let pins = RingPins.shared - /// The registration's band; the fixture's own tolerance must not be looser. - private let band = 1e-7 // D17: assert_allclose default (rtol 1e-7); was 1e-12, measured worst is far below + /// The band; the fixture's own tolerance must not be looser. + private let band = 1e-7 // assert_allclose default (rtol 1e-7); was 1e-12, measured worst is far below - @Test func testThePinsAreTheRegisteredSetAndTheirBandIsNotLoosened() { + @Test func testThePinsAreTheDocumentedSetAndTheirBandIsNotLoosened() { #expect(pins.tolerance <= band) #expect(pins.cases.count == 320) #expect(pins.refusals.count == 68) @@ -160,7 +160,7 @@ struct RingCalibrationPinsTests { } } - @Test func testEveryRefusalPlantGivesTheRegisteredNamesInOrderWithTheirNumbers() { + @Test func testEveryRefusalPlantGivesTheDocumentedNamesInOrderWithTheirNumbers() { for r in pins.refusals { let fit = RingCalibration.FitReport( model: r.model == "asymmetricGaussian" ? .asymmetricGaussian : .conic, semiAxisA: r.a_px, semiAxisB: r.b_px, @@ -168,7 +168,7 @@ struct RingCalibrationPinsTests { innerSigma: r.inner_sigma, outerSigma: r.outer_sigma, annulusWidth: r.annulus_width_px) let got = RingCalibration.refusals(fit, highTensionVolts: r.volts) #expect(got.map(\.name) == r.expect_names, - "\(r.name) \(r.check): refused as \(got.map(\.name)), the registered thresholds say \(r.expect_names)") + "\(r.name) \(r.check): refused as \(got.map(\.name)), the documented thresholds say \(r.expect_names)") if got.map(\.name) == r.expect_names { for (refusal, want) in zip(got, r.expect_values) { let have = reported(refusal) @@ -213,7 +213,7 @@ struct RingCalibrationPinsTests { } } - @Test func testTheRegisteredTruthLiteralsAreWhatThePinsRecomputeAndTheCodeGives() throws { + @Test func testTheDocumentedTruthLiteralsAreWhatThePinsRecomputeAndTheCodeGives() throws { let t = pins.truth for (kv, want) in t.lambda_A { let volts = (Double(kv) ?? 0) * 1000 diff --git a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationQTests.swift b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationQTests.swift index 2f5368a..e7e2dd9 100644 --- a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationQTests.swift +++ b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationQTests.swift @@ -1,10 +1,10 @@ // // RingCalibrationQTests.swift -// DiffractoKit 0.1.0 (docs/registration-0.0.4.md section 3, G2): `RingCalibration.q(radius:cameraLengthPixels:wavelength +// DiffractoKit 0.1.0: `RingCalibration.q(radius:cameraLengthPixels:wavelength // Angstroms:)` and its inverse `radius(q:cameraLengthPixels:wavelengthAngstroms:)` against the pins tools/kit-0.0.4/q-pins.py // writes (Fixtures/q-pins.json): the closed forms q = 2 sin(atan(R/L)/2)/λ and R = L tan(2 asin(qλ/2)) in 60-digit decimal // arithmetic (two independent evaluations that agree to 1e-50), on 100 forward and 120 inverse cases; the round trip; the -// identity q(R, L_px, λ) = 1/d on slice 3a's 320 cases; and the NaN plants. Band 1e-7 relative (D17, 0.9.1; it was 1e-12); a +// identity q(R, L_px, λ) = 1/d on the ring-calibration pins' 320 cases; and the NaN plants. Band 1e-7 relative (the reference's tolerance since 0.9.1; it was 1e-12); a // difference is a failure of the port, never a band to widen. The fixture is read by #filePath. // @@ -53,10 +53,10 @@ private func relative(_ x: Double, _ truth: Double) -> Double { abs(x - truth) / struct RingCalibrationQTests { private let pins = QPins.shared - /// The registration's band; the fixture's own tolerance must not be looser. - private let band = 1e-7 // D17: assert_allclose default (rtol 1e-7); was 1e-12, measured worst is far below + /// The band; the fixture's own tolerance must not be looser. + private let band = 1e-7 // assert_allclose default (rtol 1e-7); was 1e-12, measured worst is far below - @Test func testThePinsAreTheRegisteredSetAndTheirBandIsNotLoosened() { + @Test func testThePinsAreTheDocumentedSetAndTheirBandIsNotLoosened() { #expect(pins.tolerance <= band) #expect(pins.forward.count == 100) // 5 V x 4 L x 5 R #expect(pins.inverse.count == 120) // the q those give, and 20 at 0.99 of the Bragg limit diff --git a/Tests/DiffractoKitParityTests/Geometry/RingIndexingPinsTests.swift b/Tests/DiffractoKitParityTests/Geometry/RingIndexingPinsTests.swift index 099846d..9147fa9 100644 --- a/Tests/DiffractoKitParityTests/Geometry/RingIndexingPinsTests.swift +++ b/Tests/DiffractoKitParityTests/Geometry/RingIndexingPinsTests.swift @@ -1,8 +1,8 @@ // // RingIndexingPinsTests.swift -// The indexing decision against the pins written BEFORE its code (registration docs/registration-slice-3.md §3, §5): -// slice-3-index-pins.json tools/slice-3/index-pins.py: NumPy from the registration text on the kit's own ring lists -// (slice 2's families after the cut), T1 (every family x u x s) and T4 (refusal plants). +// The indexing decision against the pins written BEFORE its code: +// slice-3-index-pins.json tools/slice-3/index-pins.py: NumPy from the written definition on the kit's own ring lists +// (DiffractoScattering's families after the cut), T1 (every family x u x s) and T4 (refusal plants). // Verdict equal, payload family names equal and in order, delta 1e-14, z 1e-10 max(1, z), margin 1e-9 max(1, margin), // reach 1e-9 max(1, reach). A failure is a failure of the port, never a tolerance to widen. // @@ -117,7 +117,7 @@ struct RingIndexingPinsTests { private typealias Index = RingIndexing - /// The pins carry what the registration says (k = 3, ln 19, k_max 1.2, the five u and nine s), and the plant counts. + /// The pins carry their definitions (k = 3, ln 19, k_max 1.2, the five u and nine s), and the plant counts. @Test func testPinsCarryTheirDefinitions() throws { let pins = try IndexPinFixtures.pins() #expect(pins.definitions.k == Index.defaultCoverage) @@ -224,11 +224,11 @@ struct RingIndexingPinsTests { #expect(bad.isEmpty, "\(bad.count) of \(checked) differ, first: \(bad.prefix(5))") } - /// the reference implementation's registered T1 counts (accepted as own, accepted as another, ambiguous, beyondKMax, none) per u over its six + /// the reference implementation's predicted T1 counts (accepted as own, accepted as another, ambiguous, beyondKMax, none) per u over its six /// lists, and beta'' accepted per u (2584, 508, 184, 58, 25), reproduced from the Swift decision on the kit's rings. - @Test func testT1CountsReproduceReferenceRegisteredPrediction() throws { + @Test func testT1CountsReproduceReferencePrediction() throws { let pins = try IndexPinFixtures.pins() - let registered: [Double: [Int]] = [1e-4: [711, 0, 0, 36, 0], 1e-3: [691, 0, 20, 36, 0], 3e-3: [647, 0, 64, 36, 0], + let predicted: [Double: [Int]] = [1e-4: [711, 0, 0, 36, 0], 1e-3: [691, 0, 20, 36, 0], 3e-3: [647, 0, 64, 36, 0], 1e-2: [408, 0, 309, 30, 0], 3e-2: [131, 19, 572, 11, 14]] let beta: [Double: Int] = [1e-4: 2584, 1e-3: 508, 3e-3: 184, 1e-2: 58, 3e-2: 25] var six: [Double: [Int]] = [:], betaAccepted: [Double: Int] = [:] @@ -253,8 +253,8 @@ struct RingIndexingPinsTests { if name != "beta''", verdictIndex < 5 { six[u, default: [0, 0, 0, 0, 0]][verdictIndex] += 1 } } } - for (u, want) in registered { #expect(six[u] == want, "u \(u): \(String(describing: six[u])), reference registered \(want)") } - for (u, want) in beta { #expect(betaAccepted[u] == want, "beta'' u \(u): \(String(describing: betaAccepted[u])), registered \(want)") } + for (u, want) in predicted { #expect(six[u] == want, "u \(u): \(String(describing: six[u])), reference predicted \(want)") } + for (u, want) in beta { #expect(betaAccepted[u] == want, "beta'' u \(u): \(String(describing: betaAccepted[u])), predicted \(want)") } } /// T4: the refusal plants on the kit's rings: invalid numbers (q, sigma, k each 0, -1, NaN, +-inf), beyondKMax (Mg diff --git a/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMFamilyTests.swift b/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMFamilyTests.swift index 71280a0..85b6d1c 100644 --- a/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMFamilyTests.swift +++ b/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMFamilyTests.swift @@ -6,7 +6,7 @@ import DiffractoCore import DiffractoOrientation /// Pins: tools/symmetry/family-pins.py (py4DSTEM f050d207 `symmetry_reduce_directions`, 0.14.19, NumPy 2.5.0). The -/// written tolerance is 1e-7 absolute (D17; was 1e-12) on family entries (the operator products are float64 sums of three terms, entries of +/// written tolerance is 1e-7 absolute (the reference's tolerance; was 1e-12) on family entries (the operator products are float64 sums of three terms, entries of /// order 1; NumPy's BLAS and the Swift loops differ at 1e-16). Where more than one operator satisfies the in-range test /// the pin records it (`hits`); those cases are compared too because both implementations take the first operator in the /// same order. Columns on a sector wall (`boundary`) are not compared: whether the exact `>= 0` test passes is rounding noise. diff --git a/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMRealFamilyTests.swift b/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMRealFamilyTests.swift index 8b02636..0c52575 100644 --- a/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMRealFamilyTests.swift +++ b/Tests/DiffractoKitParityTests/Orientation/Py4DSTEMRealFamilyTests.swift @@ -8,7 +8,7 @@ import DiffractoOrientation /// Pins: tools/symmetry/family-real-pins.py. Real py4DSTEM `Crystal`s (0.14.19 at f050d207, pymatgen 2025.1.9 and spglib 2.8.0 in /// its venv) built from pymatgen-expanded structures of centrosymmetric space groups; `orientation_plan('auto')` takes the plan sector /// from the detected point group and `symmetry_operators` from `SpacegroupAnalyzer.get_point_group_operations()`, so the operator -/// order is pymatgen's. Written tolerance: 1e-7 absolute (D17; was 1e-12) on family entries (float64 sums of three products of order 1). Columns +/// order is pymatgen's. Written tolerance: 1e-7 absolute (the reference's tolerance; was 1e-12) on family entries (float64 sums of three products of order 1). Columns /// within 1e-12 of a sector wall (`boundary`) are decided by BLAS rounding in NumPy and are not compared. private struct RealPins: Decodable { struct Lattice: Decodable { let a, b, c, alpha, beta, gamma: Double } diff --git a/Tests/DiffractoKitParityTests/Orientation/Slice4PinsTests.swift b/Tests/DiffractoKitParityTests/Orientation/Slice4PinsTests.swift index 5ef8b25..3924035 100644 --- a/Tests/DiffractoKitParityTests/Orientation/Slice4PinsTests.swift +++ b/Tests/DiffractoKitParityTests/Orientation/Slice4PinsTests.swift @@ -1,7 +1,6 @@ // // Slice4PinsTests.swift -// Role: the kit against the pins written BEFORE its code (registration docs/registration-slice-4.md, gates G2, G3, G4 and -// amendment 2026-10-15 15:55), read from Fixtures/slice-4-pins.json (tools/slice-4/pins.py): +// Role: the kit against the pins written BEFORE its code (the checks G2, G3 and G4 below), read from Fixtures/slice-4-pins.json (tools/slice-4/pins.py): // G2 the reference library against py4DSTEM f050d207 (lambda -> 0) and against the NumPy float64 reference: hkl sets, // |q|, the kit's q rotated by the frame map, relative intensity; before the intensity floor and the density cap // (`entries`) and after both (`library`), and through `PhaseReferenceLibrary.build`. @@ -179,14 +178,14 @@ private func report(_ gate: String, _ label: String, _ items: [(String, Double)] // MARK: - G2: the reference library against py4DSTEM and the NumPy float64 reference -@Suite("Slice 4 G2: reference library vs py4DSTEM and NumPy") +@Suite("Orientation pins G2: reference library vs py4DSTEM and NumPy") struct Slice4G2Tests { - // The registered bars (amendment 2026-10-15 15:55, 1). - static let qTolerance = 1e-7 // D17 (was 1e-12); |q| and the rotated q against py4DSTEM, Å⁻¹ + // The stated bars. + static let qTolerance = 1e-7 // the reference's tolerance (was 1e-12); |q| and the rotated q against py4DSTEM, Å⁻¹ static let intensityVsPy4DSTEM = 5e-5 // relative (py4DSTEM holds |F|² in complex64) static let intensityVsNumPy = 1e-12 // relative, against the float64 reference - /// Axes where a reflection far below the floor exceeds the registered 1e-12 against the float64 reference. + /// Axes where a reflection far below the floor exceeds the stated 1e-12 against the float64 reference. static let weakReflectionCases: Set = ["theta_prime_Al2Cu [0 1 1]"] /// Every excited reflection, before the intensity floor and the density cap. @@ -212,7 +211,7 @@ struct Slice4G2Tests { #expect(dn <= 1e-7, "n differs from the pin by \(dn)") #expect(d1 <= 1e-7, "e1 differs from the pin by \(d1)") #expect(d2 <= 1e-7, "e2 differs from the pin by \(d2)") - // The pinned frame map is a rotation of the plane (det +1), the registered conversion for a reader of py4DSTEM. + // The pinned frame map is a rotation of the plane (det +1), the stated conversion for a reader of py4DSTEM. let m = z.frameMap #expect(m.handedness == "rotation") let c0 = cos(m.phiPyRad), s0 = sin(m.phiPyRad) @@ -264,9 +263,9 @@ struct Slice4G2Tests { #expect(wRot <= Self.qTolerance, "rotated q vs py4DSTEM \(wRot)") #expect(wIpy <= Self.intensityVsPy4DSTEM, "relative intensity vs py4DSTEM \(wIpy)") if Self.weakReflectionCases.contains(c.name) { - // FINDING (see the report): the registered 1e-12 is exceeded on a reflection far below the intensity floor, + // FINDING: the stated 1e-12 is exceeded on a reflection far below the intensity floor, // where |F|² is a small difference of large terms. The same axis passes after the floor (next test). - withKnownIssue("relative intensity of a sub-floor reflection vs NumPy float64 exceeds the registered 1e-12") { + withKnownIssue("relative intensity of a sub-floor reflection vs NumPy float64 exceeds the stated 1e-12") { #expect(wInp <= Self.intensityVsNumPy, "relative intensity vs NumPy \(wInp) at \(worstNumPyHKL)") } #expect(wInp <= 1e-9, "the excess is bounded (a parity row, not a gate): \(wInp)") @@ -333,7 +332,7 @@ struct Slice4G2Tests { var byHKL: [HKL: [Double]] = [:] for r in z.library { byHKL[HKL(h: Int(r[0]), k: Int(r[1]), l: Int(r[2]))] = r } #expect(entries[0].vectors.count == z.library.count) - // Entry k: the pinned vectors rotated by theta_k = k 2pi/N (the registered definition). + // Entry k: the pinned vectors rotated by theta_k = k 2pi/N (the stated definition). for k in [0, 1, 7, 90, 179] { let theta = Double(k) * (2 * Double.pi / 180) #expect(entries[k].inPlaneRotationRad == theta) @@ -385,12 +384,12 @@ private enum EulerGroup: String, CaseIterable, Sendable, CustomTestStringConvert } } -@Suite("Slice 4 G3: Euler angles, matrices and the frame vs SciPy") +@Suite("Orientation pins G3: Euler angles, matrices and the frame vs SciPy") struct Slice4G3Tests { static let matrixTolerance = 2e-7 // the round trip, and the cases where the two gimbal rules differ static let exactMatrixTolerance = 1e-12 // from_euler on the same angles - @Test("the pin sets are the registered ones: 1000 random, the gimbal ladder, 37 py4DSTEM matrices") + @Test("the pin sets are the stated ones: 1000 random, the gimbal ladder, 37 py4DSTEM matrices") func pinCounts() { #expect(S4.pins.euler.random.count == 1000) #expect(S4.pins.euler.gimbal.count == 48) @@ -412,17 +411,17 @@ struct Slice4G3Tests { "gimbal-Phi=1e-7-phi1=-2.5-phi2=2.0", "gimbal-Phi=1e-6-phi1=0.7-phi2=-1.1", "gimbal-Phi=1e-6-phi1=-2.5-phi2=2.0", "gimbal-Phi=1e-6-phi1=3.0-phi2=3.0", "gimbal-Phi=1e-5-phi1=0.7-phi2=-1.1", "gimbal-Phi=1e-5-phi1=-2.5-phi2=2.0", - // Since the gimbal fix (amendment 16:26) these fold into phi1 as SciPy does; only Phi's acos precision is left. + // Since the gimbal fix (2026-10-15) these fold into phi1 as SciPy does; only Phi's acos precision is left. "gimbal-Phi=0-phi1=-2.5-phi2=2.0", "gimbal-Phi=1e-9-phi1=-2.5-phi2=2.0", "gimbal-Phi=1e-10-phi1=-2.5-phi2=2.0", "gimbal-Phi=5e-9-phi1=-2.5-phi2=2.0", ] - /// FINDING 2 (the lock is not detected): the registered lock |sin Phi| <= 1e-8 is applied to Phi = acos(R22), whose + /// FINDING 2 (the lock is not detected): the stated lock |sin Phi| <= 1e-8 is applied to Phi = acos(R22), whose /// smallest non-zero value is 1.49e-8 (R22 = 1 - 2^-53), so a rotation at Phi = 0, pi or within 1e-8 of them is locked /// only when R22 is exactly +-1. Otherwise the unlocked formula is used: a valid but different split of the same /// rotation (phi1, phi2 as generated, Phi 1.5e-8), where SciPy puts everything in phi1. One row loses the rotation. - /// Fixed (amendment 16:26): the general branch now also needs R20, R21 to carry the angle, so every such row is locked. + /// Fixed (2026-10-15): the general branch now also needs R20, R21 to carry the angle, so every such row is locked. static let lockNotDetectedRows: Set = [] - /// The one random rotation whose round trip exceeds the registered 2e-7 (2.10e-7): three Float roundings. + /// The one random rotation whose round trip exceeds the stated 2e-7 (2.10e-7): three Float roundings. static let floatRoundTripRow = "random-579" @Test("M -> angles equal SciPy's after Float rounding (rtol 1e-7 + atol 1e-12, modulo 2π); where the two gimbal rules differ, the rotation matrix within 2e-7; the two findings are pinned as known issues", @@ -436,7 +435,7 @@ struct Slice4G3Tests { for row in group.rows { let e = EulerAngles(py4DSTEMOrientationMatrix: mat3(row.M)) if row.rulesAgree == false { - // Amendment 3: SciPy locks below ~1e-7, the source at |sin Phi| <= 1e-8: the same rotation, split differently. + // SciPy locks below ~1e-7, the source at |sin Phi| <= 1e-8: the same rotation, split differently. let d = maxAbsDiff(e.crystalToLabMatrix, mat3(row.crystalToLab)) worstMatrix = max(worstMatrix, d) matrixChecked += 1 @@ -447,7 +446,7 @@ struct Slice4G3Tests { var bad: [Int] = [] for i in 0..<3 { let pinned = row.angles[i] - // D17: SciPy's assert_allclose defaults (rtol 1e-7, atol 1e-12); the Float storage rounds by at most 6e-8 relative. + // SciPy's assert_allclose defaults (rtol 1e-7, atol 1e-12); the Float storage rounds by at most 6e-8 relative. let tolerance = 1e-7 * abs(pinned) + 1e-12 let d = circularDifference(kit[i], pinned) if Self.phiPrecisionRows.contains(row.label) && i == 1 { worstPhi = max(worstPhi, d) } else { @@ -466,7 +465,7 @@ struct Slice4G3Tests { note: "phi_only_known=\(phiOnly.count) lock_missed_known=\(lockMissed.count)") #expect(failures.isEmpty, "\(failures.count) failures, first: \(failures.prefix(6))") if group == .gimbal { #expect(matrixChecked == 11) } else { #expect(matrixChecked == 0) } - // The known rows are exactly the registered list: a row that starts to pass, or a new one that fails, is a change. + // The known rows are exactly the stated list: a row that starts to pass, or a new one that fails, is a change. if group == .gimbal { withKnownIssue("Phi = acos(R22) misses SciPy's Phi by more than rtol 1e-7 + atol 1e-12 for Phi <= 1e-5") { #expect(phiOnly.isEmpty) @@ -521,7 +520,7 @@ struct Slice4G3Tests { else { failures.append("\(row.label): \(d)") } } // The rigorous Float bound: each matrix entry is 1-Lipschitz in each angle, so rounding three angles to Float - // moves it by at most the sum of half ulps (+1e-12 for the double arithmetic, + Phi's conditioning below). (The Phi = 0 row that lost its rotation before amendment 16:26 is held to it too.) + // moves it by at most the sum of half ulps (+1e-12 for the double arithmetic, + Phi's conditioning below). (The Phi = 0 row that lost its rotation before the gimbal fix is held to it too.) // Phi = acos(R22) also carries its own conditioning error, about 2.3e-16 / sin Phi, capped by the resolution of // R22 near 1 (the acos of 1 - 2^-53 is 1.49e-8): allowed for here, it is finding 1. let sinPhi = abs(sin(Double(e.Phi))) @@ -541,7 +540,7 @@ struct Slice4G3Tests { } #expect(Set(over.keys) == expected, "rows over 2e-7: \(over)") if !expected.isEmpty { - withKnownIssue("registered round trip 2e-7: three Float roundings can reach 2.1e-7 (random-579); (the Phi = 0 row that lost its rotation is fixed, amendment 16:26)") { + withKnownIssue("stated round trip 2e-7: three Float roundings can reach 2.1e-7 (random-579); (the Phi = 0 row that lost its rotation is fixed, 2026-10-15)") { #expect(over.isEmpty) } } @@ -580,7 +579,7 @@ struct Slice4G3Tests { // MARK: - G4: the closed forms -@Suite("Slice 4 G4: closed forms") +@Suite("Orientation pins G4: closed forms") struct Slice4G4Tests { /// |a - b| <= 1e-15 |b|; exact when b is 0. private static func relativeDifference(_ a: Double, _ b: Double) -> Double { @@ -710,7 +709,7 @@ struct Slice4G4Tests { #expect(kit.first == 0) } - @Test("the registered step set covers 11 and 13 (rounding differs from truncation) and 0, -1, nan, 400, 360, 0.3") + @Test("the stated step set covers 11 and 13 (rounding differs from truncation) and 0, -1, nan, 400, 360, 0.3") func stepSetIsComplete() { let steps = Set(S4.pins.closedForms.inPlaneSteps.cases.map(\.step)) for s in ["11", "13", "0", "-1", "nan", "400", "360", "0.3"] { #expect(steps.contains(s), "missing step \(s)") } diff --git a/Tests/DiffractoKitParityTests/Scattering/ScatteringPinsTests.swift b/Tests/DiffractoKitParityTests/Scattering/ScatteringPinsTests.swift index cfc91ac..694ff89 100644 --- a/Tests/DiffractoKitParityTests/Scattering/ScatteringPinsTests.swift +++ b/Tests/DiffractoKitParityTests/Scattering/ScatteringPinsTests.swift @@ -1,6 +1,6 @@ // // ScatteringPinsTests.swift -// The kit against the pins written BEFORE its code (registration docs/registration-slice-2.md §4.1–4.4 and amendments 1–5): +// The kit against the pins written BEFORE its code: // slice-2-pins.json tools/slice-2/pins.py: NumPy from the definitions, pymatgen's Laue group and X-ray pattern // slice-2-apps.json tools/slice-2/apps-parity: reference commit 7d6fdbd1's Crystal.reflections, compiled -O // Public API only (DiffractoScattering and DiffractoCore). A failure is a failure of the port, never a tolerance to widen. @@ -72,7 +72,7 @@ private struct Apps: Decodable { let cases: [AppsCase] } enum ScatteringFixtures { static let directory: URL = FixtureDirs.target("DiffractoScatteringTests").appendingPathComponent("Fixtures") - /// Slice 1's CIF fixtures (read by file name, as the pins name them). + /// DiffractoCore's CIF fixtures (read by file name, as the pins name them). static let coreDirectory: URL = FixtureDirs.fixtures(of: "DiffractoCoreTests") fileprivate static func decode(_ type: T.Type, _ name: String) throws -> T { @@ -142,7 +142,7 @@ struct ScatteringPinsTests { // MARK: 4.2 the table /// All 103 rows × 10 parameters equal to the fetched file bit for bit; f_e at g² ∈ {0, 0.25, 1, 4} for every Z within - /// 1e-7 relative of NumPy's value (D17; was 1e-15, measured at rounding level). (Also holds the JSON decoder to exact doubles: a lossy decode fails here first.) + /// 1e-7 relative of NumPy's value (the reference's tolerance; was 1e-15, measured at rounding level). (Also holds the JSON decoder to exact doubles: a lossy decode fails here first.) @Test func testTableBitForBitAndSamples() throws { let pins = try ScatteringFixtures.pins() #expect(pins.tableSha256 == "be845d1d541cdb75ae16f8928cd9e56a9ea38af8a3cc43da9137e73cc72bb6f4") @@ -181,7 +181,7 @@ struct ScatteringPinsTests { // MARK: 4.1 NumPy - /// Every structure at k_max 1.2: Re F, Im F, |F| within 1e-7 relative (D17; was 1e-9) of max(|F|, 1e-4) on every pinned reflection; the + /// Every structure at k_max 1.2: Re F, Im F, |F| within 1e-7 relative (the reference's tolerance; was 1e-9) of max(|F|, 1e-4) on every pinned reflection; the /// kept set equal (a reflection within 1e-12 relative of the cut may fall either side); the unlisted ones (|F| ≤ 1e-6 /// in NumPy) stay below 1e-6; the families equal (count, g 1e-7, members, multiplicity, orbits, name, Σ|F|² 1e-7). @Test(arguments: ScatteringFixtures.structureNames) @@ -249,7 +249,7 @@ struct ScatteringPinsTests { } } - // MARK: amendment 4 the cut's strictness + // MARK: The cut's strictness /// a = b = 100, c = 1 Å, a unit form factor: every reflection has |F| = 1/V = 1e-4, so `>` keeps none (`>=` would keep /// all of them). @@ -276,7 +276,7 @@ struct ScatteringPinsTests { #expect(list.all.count >= ce.reflectionsWithinKMax) } - // MARK: amendment 2 the sign of Im F + // MARK: The sign of Im F /// Zinc-blende GaAs, non-centrosymmetric about the origin: F = 4 (f_Ga + i^(h+k+l) f_As) / V for unmixed h, k, l, /// absent otherwise (1e-13 relative); Im F(1 1 1) < 0 and Im F(−1 −1 −1) > 0. A flipped phase sign fails both. @@ -312,7 +312,7 @@ struct ScatteringPinsTests { // MARK: 4.3 pymatgen X-ray /// For each pymatgen peak: Σ|F_X|² over its half-space members through the kit's phase sum with pymatgen's X-ray form - /// factor f(s) = Z − 41.78214 s² Σ a exp(−b s²), s = |g|/2, equals pymatgen's intensity / (2 LP) within 1e-3 relative (XRDCalculator.SCALED_INTENSITY_TOL; D17, was 1e-6). + /// factor f(s) = Z − 41.78214 s² Σ a exp(−b s²), s = |g|/2, equals pymatgen's intensity / (2 LP) within 1e-3 relative (XRDCalculator.SCALED_INTENSITY_TOL; was 1e-6). @Test(arguments: ScatteringFixtures.xrayNames) func testXRayPhaseSumsMatchPymatgen(_ name: String) throws { let pins = try ScatteringFixtures.pins() @@ -342,13 +342,13 @@ struct ScatteringPinsTests { #expect(worst <= 1e-7, "\(name): worst relative difference \(worst) at \(worstAt)") } - // MARK: 4.4 the reference implementations, exact today (amendment 1; D12 allows a tolerance) + // MARK: The reference implementations, exact today (the reference's tolerance would allow more) /// The reference implementations' `Crystal.reflections(kMax:tolerance: 1e-4)` (reference commit 7d6fdbd1, -O): the kept sets equal, and per kept /// reflection the Double bit patterns of |g|, Re F and |F|² equal; Im F bit-equal where |Im F| ≥ 1e-15 and within 1e-15 - /// below that (amendment 1). β″ is built from the CIF here, so its site order differs from the reference implementations' hand list and a + /// below that. β″ is built from the CIF here, so its site order differs from the reference implementations' hand list and a /// summation-order difference is expected: for it, Re F and Im F are held to 1e-12 relative of max(|F|, 1e-4) (Im F also - /// passes below 1e-15 within 1e-15), |F|² to 2e-12 relative, |g| exact today (registration 4.4 as amended 2026-10-15: the measured worst is 2.7e-13; bit identity is the reference-site-list test). + /// passes below 1e-15 within 1e-15), |F|² to 2e-12 relative, |g| exact today (the measured worst is 2.7e-13, bit identity is the reference-site-list test). @Test(arguments: ScatteringFixtures.appsCaseIndices) func testAppsBitForBit(_ index: Int) throws { let c = try ScatteringFixtures.apps().cases[index] @@ -359,7 +359,7 @@ struct ScatteringPinsTests { } /// The cause of the β″ differences above: built from the reference implementations' own site list (same order, same coordinates) the kit is - /// exact to the reference implementations like every other preset (D12 quick check). + /// exact to the reference implementations like every other preset. @Test(arguments: [8, 9]) func testAppsBetaDoublePrimeFromTheAppsSiteListIsBitIdentical(_ index: Int) throws { let c = try ScatteringFixtures.apps().cases[index] @@ -417,7 +417,7 @@ private func compareToApps(_ c: AppsCase, _ s: CrystalStructure, summationOrderT return (fails, stats) } -// MARK: - retainingAll: false (registration 0.2.0, ask 1) +// MARK: - retainingAll: false extension ScatteringPinsTests { diff --git a/Tests/DiffractoKitTestSupport/CoreFixtures.swift b/Tests/DiffractoKitTestSupport/CoreFixtures.swift index bd46bb1..75e5574 100644 --- a/Tests/DiffractoKitTestSupport/CoreFixtures.swift +++ b/Tests/DiffractoKitTestSupport/CoreFixtures.swift @@ -2,7 +2,7 @@ // TestSupport.swift // Role: the fixtures directory (read by #filePath, not as resources), the β″ CIF text rebuilt from the pinned // generator's values (tools/crystal-structures/make_beta_double_prime.py, verbatim from reference commit 7448401b; the -// pins carry its sha256), and the pins file written by tools/slice-1/pins.py (registration section 4). +// pins carry its sha256), and the pins file written by tools/slice-1/pins.py. // import Foundation @@ -170,7 +170,7 @@ package struct ReflectionPin: Decodable { package let canonical: [Int] } -/// What the file's loop is against the detected set (amendment 2). +/// What the file's loop is against the detected set. package struct FileOperationsPin: Decodable { package let count: Int package let equalsDetected: Bool @@ -203,7 +203,7 @@ package struct StructurePin: Decodable { package let metric: [[Double]] package let volume: Double package let reciprocal: [[Double]] - /// As pymatgen expanded them (coordinates within 1e-4 of ⅓ and ⅔ snapped to the fraction, amendment 4). + /// As pymatgen expanded them (coordinates within 1e-4 of ⅓ and ⅔ snapped to the fraction). package let sites: [SitePin] /// The sites with the written coordinates, where pymatgen snapped any; what the port is compared with. package let sitesAsWritten: [SitePin]? diff --git a/Tests/DiffractoKitTestSupport/IndexFixtures.swift b/Tests/DiffractoKitTestSupport/IndexFixtures.swift index a484743..4c2396b 100644 --- a/Tests/DiffractoKitTestSupport/IndexFixtures.swift +++ b/Tests/DiffractoKitTestSupport/IndexFixtures.swift @@ -36,7 +36,7 @@ package enum IndexPresets { structure(.cubic(a: 3), [AtomSite(z: 29, fractional: [0, 0, 0]), AtomSite(z: 29, fractional: [0.5, 0.5, 0.5], occupancy: 0.9981472011156598)]) } - /// The β″ CIF exactly as the generator writes it (slice 1's fixture builder). + /// The β″ CIF exactly as the generator writes it (the core fixture builder). package static var betaDoublePrime: CrystalStructure { let sites: [(String, String, Double, Double, Double, Double)] = [ ("Mg1", "Mg", 0.0, 0.0, 0.0, 0.5), ("Mg2", "Mg", 0.3459, 0.0, 0.089, 1.0), ("Mg3", "Mg", 0.430, 0.0, 0.652, 0.8), @@ -64,7 +64,7 @@ package enum IndexPresets { } package static var mP: CrystalStructure { mono(7.2, 4.9, 5.6, 104) } package static var mC: CrystalStructure { mono(7.2, 4.9, 5.6, 104, sites: alCentred) } - /// a = c and cos β = −1/4 make this metric C-centred orthorhombic: full detection (slice 1b, `conventional: true`) + /// a = c and cos β = −1/4 make this metric C-centred orthorhombic: full detection (`conventional: true`) /// reads mmm and relates {1 0 0} and {0 0 1} by symmetry. the reference implementation's accidental-coincidence case is the file setting's /// 2/m, so this preset is detected in the file's setting only (`conventional: false`). package static var mSq: CrystalStructure { @@ -73,7 +73,7 @@ package enum IndexPresets { } package static var mSqFull: CrystalStructure { mono(5, 4, 5, acos(-0.25) * 180 / .pi) } - /// The rings of `s` after the |F| cut, slice 1's families. + /// The rings of `s` after the |F| cut, DiffractoCore's families. package static func rings(_ s: CrystalStructure, kMax: Double = 1.2) throws -> [ReflectionFamily] { try s.kinematicReflections(kMax: kMax).families.map(\.family) } diff --git a/Tests/DiffractoKitTestSupport/OrientationFixtures.swift b/Tests/DiffractoKitTestSupport/OrientationFixtures.swift index d79cd69..9cb5e27 100644 --- a/Tests/DiffractoKitTestSupport/OrientationFixtures.swift +++ b/Tests/DiffractoKitTestSupport/OrientationFixtures.swift @@ -1,6 +1,6 @@ // // TestSupport.swift -// Role: the structures and scan fixtures the slice 4 tests build. The structures are the cells and sites the planted-truth +// Role: the structures and scan fixtures the orientation tests build. The structures are the cells and sites the planted-truth // tests of vector matching use (an fcc cell, the hexagonal and monoclinic cells, the published beta-double-prime // refinement), built as `CrystalStructure` with the identity operation only: a vector-matching library reads the // kinematic reflections, not the symmetry. The scan fixtures wrap the kit's `[[SIMD2]]` scan form. diff --git a/Tests/DiffractoOrientationTests/MatrixChallengeTests.swift b/Tests/DiffractoOrientationTests/MatrixChallengeTests.swift index 8b76dd4..97b3543 100644 --- a/Tests/DiffractoOrientationTests/MatrixChallengeTests.swift +++ b/Tests/DiffractoOrientationTests/MatrixChallengeTests.swift @@ -1,6 +1,6 @@ // // MatrixChallengeTests.swift -// Role: the matrix's last word before a position may be called a precipitate (registration slice 4, gate G5). +// Role: the matrix's last word before a position may be called a precipitate. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoOrientationTests/OrientationResultTests.swift b/Tests/DiffractoOrientationTests/OrientationResultTests.swift index 0ddd4f0..01d168e 100644 --- a/Tests/DiffractoOrientationTests/OrientationResultTests.swift +++ b/Tests/DiffractoOrientationTests/OrientationResultTests.swift @@ -1,7 +1,7 @@ // // OrientationResultTests.swift // Role: the value types of an orientation result: the Euler angles in both directions, the zone-axis frame and its switch of -// reference axis (registration slice 4, section 4). The comparison with SciPy on 1000 seeded rotations is gate G3 and +// reference axis. The comparison with SciPy on 1000 seeded rotations is check G3 and // lives with the pins; these are the closed forms. // @@ -70,8 +70,8 @@ import DiffractoOrientation /// A rotation about z whose R22 rounds to 1 − 2⁻⁵³ (acos gives Φ = 1.49e-8, sin Φ above the 1e-8 threshold) while R20, R21, /// R02 and R12 are exactly 0: the source's general branch took atan2(0, 0) for both angles and lost the rotation (round trip - /// 0.479 off; the slice 4 pins' gimbal row "Φ = 0, φ1 = −2.5, φ2 = 2.0"). The kit folds it into φ1 (registration slice 4, - /// amendment 16:26). Mutation: the general branch taken on sin Φ alone. + /// 0.479 off; the orientation pins' gimbal row "Φ = 0, φ1 = −2.5, φ2 = 2.0"). The kit folds it into φ1 (fixed + /// 2026-10-15). Mutation: the general branch taken on sin Φ alone. @Test func aRotationAboutTheBeamWithR22JustBelowOneKeepsItsAngle() { let angle = -0.5 let r22 = 1 - 0x1p-53 diff --git a/Tests/DiffractoOrientationTests/OrientationSymmetryTests.swift b/Tests/DiffractoOrientationTests/OrientationSymmetryTests.swift index bdd3386..e9ba93d 100644 --- a/Tests/DiffractoOrientationTests/OrientationSymmetryTests.swift +++ b/Tests/DiffractoOrientationTests/OrientationSymmetryTests.swift @@ -66,7 +66,7 @@ struct OrientationSymmetryTests { #expect(gap(op.transpose * op, matrix_identity_double3x3) < 2e-14) let g = p.operators.map { gap(op, matrix($0)) }.min()! worst = max(worst,g) - #expect(g < 1e-7) // D17 (was 2e-14); closure and orthogonality above and below stay at 2e-14 + #expect(g < 1e-7) // the reference's tolerance (was 2e-14); closure and orthogonality above and below stay at 2e-14 for other in ops { #expect(ops.contains { gap(op * other, $0) < 2e-14 }) } } } @@ -165,14 +165,14 @@ struct OrientationSymmetryTests { let symmetry = CrystalOrientationSymmetry(laueClass:try #require(LaueClass(rawValue:group.symbol))) for (d,e) in zip(p.nativeDirections,group.foldedDirections) { let g = simd_length(try symmetry.reduceDirection(vector(d))-vector(e)) - worst = max(worst,g); #expect(g < 1e-7) // D17 (was 2e-12) + worst = max(worst,g); #expect(g < 1e-7) // the reference's tolerance (was 2e-12) } for sample in group.samples { let actual = try symmetry.sampleFundamentalZone(count:sample.count) #expect(actual.count == sample.directions.count) for (v,e) in zip(actual,sample.directions) { let g = simd_length(v-vector(e)); worst = max(worst,g) - #expect(g < 1e-7) // D17 (was 2e-12) + #expect(g < 1e-7) // the reference's tolerance (was 2e-12) } } } diff --git a/Tests/DiffractoOrientationTests/PhaseReferenceLibrarySlabTests.swift b/Tests/DiffractoOrientationTests/PhaseReferenceLibrarySlabTests.swift index 6359482..f5b4de0 100644 --- a/Tests/DiffractoOrientationTests/PhaseReferenceLibrarySlabTests.swift +++ b/Tests/DiffractoOrientationTests/PhaseReferenceLibrarySlabTests.swift @@ -1,7 +1,7 @@ // // PhaseReferenceLibrarySlabTests.swift // Role: the excitation slab is a property of a phase's shape: the per-phase override widens only its own phase's entries and -// its absence changes nothing (registration slice 4, gate G5). +// its absence changes nothing. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoOrientationTests/PhaseVectorMatchingTests.swift b/Tests/DiffractoOrientationTests/PhaseVectorMatchingTests.swift index 724f48d..f3e8657 100644 --- a/Tests/DiffractoOrientationTests/PhaseVectorMatchingTests.swift +++ b/Tests/DiffractoOrientationTests/PhaseVectorMatchingTests.swift @@ -1,7 +1,7 @@ // // PhaseVectorMatchingTests.swift // Role: the planted-truth and unit-level tests of the reference library and the vector matcher, moved to Swift Testing from the -// application tests of the ported types with every expectation kept (registration slice 4, gate G5). Each test's comment +// application tests of the ported types with every expectation kept. Each test's comment // names the mutation it catches. // @@ -992,8 +992,8 @@ import DiffractoOrientation role: .candidate, zoneAxes: [SIMD3(0, 1, 0)]) } - /// An exact tie between phases is broken by the lower phase index, not by the process's hash seed (registration slice 4, - /// amendment 16:08 item 5). Eight identical candidate phases score identically on a pattern of one of them; the label + /// An exact tie between phases is broken by the lower phase index, not by the process's hash seed. + /// Eight identical candidate phases score identically on a pattern of one of them; the label /// must be the first, the runner-up the second. Under the source (a Dictionary sorted by score alone) both hold in about /// 1 run in 56. Mutation: the phase-index tie-break dropped. @Test func anExactTieBetweenPhasesGoesToTheLowerPhaseIndex() throws { diff --git a/Tests/DiffractoOrientationTests/PhaseVectorResolutionTests.swift b/Tests/DiffractoOrientationTests/PhaseVectorResolutionTests.swift index 996e2c1..b8ef919 100644 --- a/Tests/DiffractoOrientationTests/PhaseVectorResolutionTests.swift +++ b/Tests/DiffractoOrientationTests/PhaseVectorResolutionTests.swift @@ -1,6 +1,6 @@ // // PhaseVectorResolutionTests.swift -// Role: what the tolerances in Å⁻¹ mean on a particular detector (registration slice 4, gate G5). +// Role: what the tolerances in Å⁻¹ mean on a particular detector. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoOrientationTests/PhaseVectorReviewFindingTests.swift b/Tests/DiffractoOrientationTests/PhaseVectorReviewFindingTests.swift index 9b8e497..be3f99d 100644 --- a/Tests/DiffractoOrientationTests/PhaseVectorReviewFindingTests.swift +++ b/Tests/DiffractoOrientationTests/PhaseVectorReviewFindingTests.swift @@ -1,7 +1,7 @@ // // PhaseVectorReviewFindingTests.swift // Role: the two review findings (2026-09-12) that were fixed in the matcher rather than only recorded; both were invisible to -// every check that existed at the time (registration slice 4, gate G5). +// every check that existed at the time. // import DiffractoKitTestSupport diff --git a/Tests/DiffractoOrientationTests/ReflectionOrderTests.swift b/Tests/DiffractoOrientationTests/ReflectionOrderTests.swift index 40b8de0..cd99b37 100644 --- a/Tests/DiffractoOrientationTests/ReflectionOrderTests.swift +++ b/Tests/DiffractoOrientationTests/ReflectionOrderTests.swift @@ -1,6 +1,6 @@ // // ReflectionOrderTests.swift -// Role: the order of the reflections a library is made from (registration slice 4, section 2.1). The density cap's sort and +// Role: the order of the reflections a library is made from. The density cap's sort and // the final length sort break ties on input order, so the input order is part of the definition: it must be the // order of the original reflection list (h, k, l enumerated ascending, filtered, then sorted by increasing |g| with // a stable sort), not the kit's own order of equal |g| (descending h, k, l). These tests re-derive the original diff --git a/Tests/DiffractoOrientationTests/ScanInputTests.swift b/Tests/DiffractoOrientationTests/ScanInputTests.swift index 126e481..a0a7cb4 100644 --- a/Tests/DiffractoOrientationTests/ScanInputTests.swift +++ b/Tests/DiffractoOrientationTests/ScanInputTests.swift @@ -1,6 +1,6 @@ // // ScanInputTests.swift -// Role: the scan as the kit takes it (registration slice 4, section 2.2 to 2.4): the two input forms, the refusal of a +// Role: the scan as the kit takes it: the two input forms, the refusal of a // malformed scan, the worker count that moves no number, progress and cancellation, and the documented defaults of // the two settings structs. // @@ -60,7 +60,7 @@ private func sameResult(_ a: PhaseVectorResult, _ b: PhaseVectorResult) -> Bool } } - /// Mutation 16 of the registration: the experimental vector computed in Double before the subtraction. The pixel form + /// Mutation: the experimental vector computed in Double before the subtraction. The pixel form /// subtracts the origin in Float, converts, then scales. Positions are chosen so that the Float difference is inexact /// (a large coordinate and a small origin), where the two orders differ. @Test func thePixelFormSubtractsInFloatAndTheCalibratedFormIsExact() { @@ -108,7 +108,7 @@ private func sameResult(_ a: PhaseVectorResult, _ b: PhaseVectorResult) -> Bool } } - /// The worker count moves no number (registration gate G1 runs 1 and 8): each position's result depends only on its + /// The worker count moves no number: each position's result depends only on its /// own vectors. Compared bit for bit, NaN scores included. @Test func theWorkerCountMovesNoNumber() throws { let library = try library() @@ -202,7 +202,7 @@ private func sameResult(_ a: PhaseVectorResult, _ b: PhaseVectorResult) -> Bool #expect(a.matrixEntryIndex == b.matrixEntryIndex) } - /// Registration section 3: the 24 settings and their documented defaults, and `default` is the empty initialiser. + /// The 24 settings and their documented defaults, and `default` is the empty initialiser. @Test func theDocumentedDefaultsAreTheTunedValues() { let v = PhaseVectorSettings.default #expect(v == PhaseVectorSettings()) diff --git a/Tests/DiffractoOrientationTests/TrigonalSettingTests.swift b/Tests/DiffractoOrientationTests/TrigonalSettingTests.swift index bc3f8a8..ca53260 100644 --- a/Tests/DiffractoOrientationTests/TrigonalSettingTests.swift +++ b/Tests/DiffractoOrientationTests/TrigonalSettingTests.swift @@ -46,7 +46,7 @@ struct TrigonalSettingTests { for e in proper { let g = s.operators.map { gap($0, e) }.min()! worst = max(worst, g) - #expect(g < 1e-7, "\(c.spaceGroup) \(c.setting)") // D17 (was 1e-12) + #expect(g < 1e-7, "\(c.spaceGroup) \(c.setting)") // the reference's tolerance (was 1e-12) } // Exact: the fractional integer matrices. let expected = Set(c.operatorsFractional.filter { m in diff --git a/Tests/DiffractoOrientationTests/ZoneAxisFitTests.swift b/Tests/DiffractoOrientationTests/ZoneAxisFitTests.swift index 3721e5c..fa3c833 100644 --- a/Tests/DiffractoOrientationTests/ZoneAxisFitTests.swift +++ b/Tests/DiffractoOrientationTests/ZoneAxisFitTests.swift @@ -1,7 +1,7 @@ // // ZoneAxisFitTests.swift // Role: asking the data which beam direction the specimen is on: the planted-family recovery, the two nulls, the total order -// of the ranking (registration slice 4, gate G5). +// of the ranking. // import DiffractoKitTestSupport From 4aa0b6d99f589253b4cfb4bc447e0318c7504715 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:50:35 +0000 Subject: [PATCH 08/17] docs(tools): drop references to documents outside the repo Pin generators and tools READMEs: registration sections, the private design note, the old roadmap, review rounds and the missing mutation scripts (tools/kit-0.0.4/mutants-*.py, tools/slice-3b/mutants.py, tools/kit-0.2.0/mutants-policy.py) no longer cited; the mutation results stay as measured facts. Package.swift no longer names the missing tools/test.sh. Only comments, docstrings and README text change: string literals the generators write into fixtures are left as they are, so a regenerated fixture stays byte-identical. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- Package.swift | 2 +- .../consumer-check/GeometryChecks.swift | 2 +- .../Sources/consumer-check/main.swift | 2 +- tools/kit-0.0.4/README.md | 15 +++--- tools/kit-0.0.4/q-pins.py | 4 +- tools/kit-0.0.4/tetragonal-pins.py | 2 +- tools/orix/misorientation-pins.py | 2 +- tools/slice-1/README.md | 29 ++++++------ tools/slice-1/pins.py | 46 +++++++++---------- tools/slice-1b/pins.py | 14 +++--- tools/slice-2/README.md | 6 +-- tools/slice-2/fetch-table.sh | 2 +- tools/slice-2/pins.py | 32 ++++++------- tools/slice-3/README.md | 16 +++---- tools/slice-3/index-pins.py | 8 ++-- tools/slice-3/ring-pins.py | 18 ++++---- tools/slice-3b/README.md | 11 ++--- tools/slice-3b/sweep.py | 2 +- tools/slice-4/README.md | 30 ++++++------ tools/slice-4/pins-py4dstem.py | 2 +- tools/slice-4/pins.py | 24 +++++----- 21 files changed, 133 insertions(+), 136 deletions(-) diff --git a/Package.swift b/Package.swift index ce8cb3d..342fa78 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ // // Swift 6 language mode, nonisolated by default (no `.defaultIsolation`), the three upcoming features consuming packages // typically enable, no unsafe flags (a versioned dependency cannot carry them): a consumer builds the kit optimised -// itself, and tools/test.sh runs this package's tests with `-c release`. One product per module. What one module needs +// itself, and CI runs this package's tests with `swift test -c release`. One product per module. What one module needs // from another is `package`, never `public`; what a consumer reads is `public` and is guarded by tools/consumer-check. import PackageDescription diff --git a/tools/consumer-check/Sources/consumer-check/GeometryChecks.swift b/tools/consumer-check/Sources/consumer-check/GeometryChecks.swift index 5826e9b..55c392d 100644 --- a/tools/consumer-check/Sources/consumer-check/GeometryChecks.swift +++ b/tools/consumer-check/Sources/consumer-check/GeometryChecks.swift @@ -1,6 +1,6 @@ // // GeometryChecks.swift -// Role: DiffractoGeometry's public API (slice 3, DiffractoKit 0.0.3) used from outside the package, against values a +// Role: DiffractoGeometry's public API (DiffractoKit 0.0.3) used from outside the package, against values a // reader verifies by hand: lambda at 200 kV, one Au {111} ring at R 150 px, indexing a q at Au {111}'s g, the beta'' // [0 0 1] axis, a clean ring rendered here and recovered, and the 2:1 ellipse's closed-form correction. // `runGeometryChecks` is called from main.swift. diff --git a/tools/consumer-check/Sources/consumer-check/main.swift b/tools/consumer-check/Sources/consumer-check/main.swift index a28722c..3b2f90e 100644 --- a/tools/consumer-check/Sources/consumer-check/main.swift +++ b/tools/consumer-check/Sources/consumer-check/main.swift @@ -169,7 +169,7 @@ report.section("CrystalStructure, CIFImport, ReflectionList") { r in r.equal("fractional helpers", fractionalAxisDelta(0.99, 0.01) < 0.021 && wrapFractional(SIMD3(-0.25, 1.5, 0)) == SIMD3(0.75, 0.5, 0), true) } -report.section("DiffractoScattering (slice 2)") { r in +report.section("DiffractoScattering") { r in let fcc: [SIMD3] = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5]] let gold = CrystalStructure.detectingSymmetry(cell: .cubic(a: 4.0782), sites: fcc.map { AtomSite(z: 79, fractional: $0) }) let list = try gold.kinematicReflections(kMax: 1.2) diff --git a/tools/kit-0.0.4/README.md b/tools/kit-0.0.4/README.md index 200ca4b..cf279a0 100644 --- a/tools/kit-0.0.4/README.md +++ b/tools/kit-0.0.4/README.md @@ -1,13 +1,12 @@ -# tools/kit-0.0.4: pins and break-once runs of DiffractoKit 0.0.4 +# tools/kit-0.0.4: pins of DiffractoKit 0.0.4 -Registered in the design note (archived in the suite's private folder). Each script works in a copy of the repository, never the working tree. +Each script works in a copy of the repository, never the working tree. | Script | What | Result (2026-10-15) | |---|---|---| -| `q-pins.py` | G2: q and its inverse, 60-digit decimal truth computed two ways (algebraic and series, agreeing to 1e-50) beside NumPy 2.5.4; writes `Tests/DiffractoGeometryTests/Fixtures/q-pins.json` (byte-identical on rerun) | 100 forward, 120 inverse, 320 identity, 51 NaN plants | -| `tetragonal-pins.py` | §6: pymatgen 2026.9.24 / spglib 2.8.0 (symprec 1e-4) on rutile, anatase, β-Sn in three settings and two controls; writes `Tests/DiffractoCoreTests/Fixtures/tetragonal-pins.json` | the kit equals spglib in every setting | -| `mutants-ringfit.py` | §1, §2, §4 and Gate B's F1, F2: the ring-fit amendment, the G1 split, G7, the second ring, monotone progress | 18 of 18 red (first pass 15 of 17, with a parallel-order mutant since removed with the parallel path: the occupancy fraction and the annulus refusal had no test that saw them alone; `aRingWithAWeakSectorIsNotCovered` and `theAnnulusRefusalReadsTheReportsRadii` added, both mutants then red) | -| `mutants-lane-a.py` | §5, §6: tetragonal and the error texts | 12 of 12 red | -| `mutants-lane-b.py` | §3: q and radius | 6 of 6 red (the Bragg-guard mutant was green until the inverse's last check became `isFinite && != 0`) | +| `q-pins.py` | q and its inverse, 60-digit decimal truth computed two ways (algebraic and series, agreeing to 1e-50) beside NumPy 2.5.4; writes `Tests/DiffractoGeometryTests/Fixtures/q-pins.json` (byte-identical on rerun) | 100 forward, 120 inverse, 320 identity, 51 NaN plants | +| `tetragonal-pins.py` | pymatgen 2026.9.24 / spglib 2.8.0 (symprec 1e-4) on rutile, anatase, β-Sn in three settings and two controls; writes `Tests/DiffractoCoreTests/Fixtures/tetragonal-pins.json` | the kit equals spglib in every setting | -Run, for example: `python3 tools/kit-0.0.4/mutants-ringfit.py [mutant-id ...]` (release builds). +The 0.0.4 changes were also checked by breaking the code once per mutation (2026-10-15): the ring fit 18 of 18 red, the +tetragonal setting and the error texts 12 of 12, q and radius 6 of 6. Each test's `RED UNDER` line names the mutation that +turns it red. diff --git a/tools/kit-0.0.4/q-pins.py b/tools/kit-0.0.4/q-pins.py index 6d421a4..fe7d892 100644 --- a/tools/kit-0.0.4/q-pins.py +++ b/tools/kit-0.0.4/q-pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit 0.0.4, G2 (docs/registration-0.0.4.md section 3): the pin generator for the exact q from a ring radius and its inverse. +"""DiffractoKit 0.0.4: the pin generator for the exact q from a ring radius and its inverse. Run, from any directory, with the pinned oracle: @@ -22,7 +22,7 @@ NumPy 2.5.4's float64 route (np.sin(np.arctan(R / L) / 2) * 2 / lambda; L * np.tan(2 * np.arcsin(q lambda / 2))) is stored beside the truth (`numpy_*`), and the script fails if it leaves 1e-12 of the truth. -THE GRID (as the registration): V in {60, 80, 120, 200, 300} kV, L_px in {500, 2000, 14082.307097295722, 1e5}, R in {1, 40, 150, +THE GRID: V in {60, 80, 120, 200, 300} kV, L_px in {500, 2000, 14082.307097295722, 1e5}, R in {1, 40, 150, 600, 2000} px (100 forward cases); the inverse on the q those give (100) plus q at 0.99 of the Bragg limit, q lambda / 2 = 0.99 / sqrt(2) (20: every V and L); THE IDENTITY: slice 3a's 320 cases, q(R, L_px(R, 2 theta), lambda) = 1/d (stored with the decimal 1/d and the decimal q of the same float64 inputs, which must agree to 1e-12: L_px is rounded to float64 there); diff --git a/tools/kit-0.0.4/tetragonal-pins.py b/tools/kit-0.0.4/tetragonal-pins.py index 0aa761d..32849d2 100644 --- a/tools/kit-0.0.4/tetragonal-pins.py +++ b/tools/kit-0.0.4/tetragonal-pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit 0.0.4, registration section 6 (tier 1): the tetragonal pins, in three axis settings. +"""DiffractoKit 0.0.4: the tetragonal pins, in three axis settings. Run, from any directory, with the pinned oracle: diff --git a/tools/orix/misorientation-pins.py b/tools/orix/misorientation-pins.py index 3df1fa9..e5affa3 100644 --- a/tools/orix/misorientation-pins.py +++ b/tools/orix/misorientation-pins.py @@ -1,4 +1,4 @@ -"""orix Misorientation / Orientation pins (DiffractoKit 0.10.0, ROADMAP task 35). +"""orix Misorientation / Orientation pins (DiffractoKit 0.10.0). ../References/python/orix/bin/python -I -B tools/orix/misorientation-pins.py diff --git a/tools/slice-1/README.md b/tools/slice-1/README.md index 02632c7..c8b0bc1 100644 --- a/tools/slice-1/README.md +++ b/tools/slice-1/README.md @@ -1,7 +1,6 @@ # tools/slice-1: the pin generator -`pins.py` writes `Tests/DiffractoCoreTests/Fixtures/slice-1-pins.json` before any Swift exists (registration -`docs/registration-slice-1.md`, sections 2, 3, 4, 5.1, 5.3 and amendment 8). The pins are what pymatgen says; the +`pins.py` writes `Tests/DiffractoCoreTests/Fixtures/slice-1-pins.json` before any Swift exists. The pins are what pymatgen says; the Swift tests read them and the planted truth. A difference is a failure of the port, never a tolerance to widen. ``` @@ -16,41 +15,41 @@ rebuild it byte for byte. Every other CIF is a fixture in `Tests/DiffractoCoreTe ## What it writes -`cells`: the seven parameter cells of 5.1 (monoclinic first): a, b, c, angles, the lattice matrix (`vesta=True`: +`cells`: the seven parameter cells (monoclinic first): a, b, c, angles, the lattice matrix (`vesta=True`: a along x, b in xy), metric, volume, reciprocal rows, and |g|, d for every (h, k, l) in -3 ... 3 except 0. `structures`: beta'' from the CIF, beta'' as a P1 list of its 22 expanded sites (symmetry detected, not read), and the -fixtures of 5.3 (plus a few extras, named below). Per structure: cell, matrix, metric, volume, reciprocal, the expanded +fixtures with planted truth (plus a few extras, named below). Per structure: cell, matrix, metric, volume, reciprocal, the expanded sites (Z, fractional in [0, 1), occupancy), the declared IT number, spglib's number and point-group symbol, the Laue class, every operation (integer rotation, translation, `x,y,z` string), the Laue rotations {R} and {-R}, every reflection at k_max 1.2 Å⁻¹ (g, d, allowed, canonical form; the orbit only on the first 24 reflections of each structure, to keep the file small) and the families on the allowed reflections; `families_all` counts the families on every reflection and gives the first 12 names; beta'' also has counts at 1.6 and 2.0. -## Definitions applied (written from the registration, not from any Swift) +## Definitions applied (written before the code, not from any Swift) -- Lattice (3.1): `Lattice.from_parameters(..., vesta=True)` rebuilt from the cell the CIF states; `g = h b1 + k b2 + l b3` +- Lattice: `Lattice.from_parameters(..., vesta=True)` rebuilt from the cell the CIF states; `g = h b1 + k b2 + l b3` with rows b from `reciprocal_lattice_crystallographic` (asserted a_i . b_j = delta_ij and B = G^-1 A); d = 1/|g| (asserted equal to `Lattice.d_hkl` to 1e-12 relative for every reflection). -- Operations (3.3): `SpacegroupAnalyzer(structure, symprec=1e-4).get_symmetry_operations(cartesian=False)`; rotations +- Operations: `SpacegroupAnalyzer(structure, symprec=1e-4).get_symmetry_operations(cartesian=False)`; rotations asserted integer; translations mod 1 in [0, 1), snapped to the nearest k/24 (asserted within 1e-9; pymatgen has already rounded spglib's to `Fraction.limit_denominator(1000)`). The set is asserted distinct, closed under composition, and a permutation of the sites. -- Laue class (3.5): from the rotation set {±R} by its order and its proper rotations of order 4 and 6, asserted equal to the +- Laue class: from the rotation set {±R} by its order and its proper rotations of order 4 and 6, asserted equal to the 32 -> 11 table applied to pymatgen's point-group symbol. "A 4-fold / a 6-fold" is read as a proper rotation (det +1): read over every element, -3 (order 6) makes -3m, m-3 and the -3m of CdI2 come out as 6/m or 6/mmm (the run prints the literal reading beside the right one). -- Orbit (3.6): `{hkl @ R : R in the Laue rotations}` (row vector times the matrix that acts on fractional coordinates, no +- Orbit: `{hkl @ R : R in the Laue rotations}` (row vector times the matrix that acts on fractional coordinates, no transpose); canonical form = the member that is greatest under (number of non-negative indices, (h, k, l)). Asserted: the orbit is closed, the canonical form is constant on an orbit and differs between orbits. -- Allowed (3.8): absent iff some operation has `hkl @ R == hkl` and `hkl . t` is not an integer within 1e-9. -- Reflections (3.8): `get_points_in_sphere` on the crystallographic reciprocal lattice, asserted equal to the brute-force box +- Allowed: absent iff some operation has `hkl @ R == hkl` and `hkl . t` is not an integer within 1e-9. +- Reflections: `get_points_in_sphere` on the crystallographic reciprocal lattice, asserted equal to the brute-force box |h| <= ceil(k_max |a_i|) and its distances equal to |g| to 1e-12. Order: increasing |g|; ties (within 1e-12 relative of the cluster's first) by descending (h, k, l). -- Families (3.9): sweep of the allowed reflections; a reflection joins while g <= g0 (1 + 1e-9); g = smallest member; +- Families: sweep of the allowed reflections; a reflection joins while g <= g0 (1 + 1e-9); g = smallest member; members descending; orbits = distinct canonical forms descending; name = braces joined by ` + `, four Miller-Bravais indices (i = -(h + k) third) for the Laue classes -3, -3m, 6/m, 6/mmm. So the Si ring is `{5 1 1} + {3 3 3}` (the descending order - macTEM's `Set.sorted(by: >)` gives), not the registration prose's `{3 3 3} + {5 1 1}`. -- Truth checks run in the same script (they stop it if they fail): the closed forms of 5.1 for all seven cells against the metric - tensor, the macTEM anchors of the monoclinic cell, the reflection conditions and counts of 5.3 that are group theory. + macTEM's `Set.sorted(by: >)` gives), not the `{3 3 3} + {5 1 1}` of the first written prediction. +- Truth checks run in the same script (they stop it if they fail): the closed forms for all seven cells against the metric + tensor, the macTEM anchors of the monoclinic cell, the reflection conditions and counts that are group theory. ## Notes the Swift side needs diff --git a/tools/slice-1/pins.py b/tools/slice-1/pins.py index f81c633..91d0436 100644 --- a/tools/slice-1/pins.py +++ b/tools/slice-1/pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit slice 1: the pin generator (registration docs/registration-slice-1.md, sections 3, 4, 5.1, 5.3, amendment 8). +"""DiffractoKit slice 1: the pin generator for cells, symmetry and reflections. Run, from any directory, with the pinned oracle: @@ -8,8 +8,8 @@ pymatgen-core 2026.9.23, spglib 2.8.0, NumPy 2.5.4, Python 3.14.7) is the oracle for the cell, the expanded sites, the space-group number, the point-group symbol, the symmetry operations, the (h, k, l) set with |g| <= k_max and d. The Laue orbit, the canonical form, the absence test and the families are NOT pymatgen functions: they are written -here, in NumPy, from the definitions of registration section 3 (3.5, 3.6, 3.8, 3.9), not from any Swift. -Agreement with pymatgen is agreement, not truth; truth is registration section 5 (closed forms are checked below too). +here, in NumPy, from the definitions in the comments below, not from any Swift. +Agreement with pymatgen is agreement, not truth; truth is the closed forms and planted structures checked below. File:line pins into the pymatgen-core clone (References/pymatgen-core-ae9a834/src/pymatgen/): core/lattice.py Lattice.from_parameters (vesta=True branch) 389-445, metric_tensor 206, volume 182, @@ -52,14 +52,14 @@ # ---------------------------------------------------------------------------------------------------------------- # The registered constants # ---------------------------------------------------------------------------------------------------------------- -K_MAX = 1.2 # Angstrom^-1 (registration 4) +K_MAX = 1.2 # Angstrom^-1 EXTRA_K_MAX = (1.6, 2.0) # beta'' only, counts only -FAMILY_TOL = 1e-9 # registration 3.9 -TIE_TOL = 1e-12 # registration 3.8: equal |g| within 1e-12 relative -SYMPREC = 1e-4 # registration 2 / 4 +FAMILY_TOL = 1e-9 # the ring rule: a reflection joins while g <= g0 (1 + 1e-9) +TIE_TOL = 1e-12 # equal |g| within 1e-12 relative +SYMPREC = 1e-4 # spglib's symprec, Angstrom ORBIT_ITEMS = 24 # reflections per structure that carry their "orbit" in the pins (canonical is on every item) -ABSENCE_TOL = 1e-9 # registration 3.8: (h k l) . t an integer within 1e-9 -HEX_AXES_CLASSES = ("-3", "-3m", "6/m", "6/mmm") # registration 3.6: four Miller-Bravais indices +ABSENCE_TOL = 1e-9 # (h k l) . t an integer within 1e-9 +HEX_AXES_CLASSES = ("-3", "-3m", "6/m", "6/mmm") # four Miller-Bravais indices # The 32 -> 11 table. Keys are spglib/pymatgen point-group symbols. LAUE_OF_POINT_GROUP = { @@ -76,7 +76,7 @@ "432": "m-3m", "-43m": "m-3m", "m-3m": "m-3m", } -# Cell contents the registration (5.3) states, per fixture (the cell content the file's formula states). +# Cell contents stated before the run, per fixture (the cell content the file's formula states). EXPECTED_SITES = { "beta_double_prime": 22, "beta_double_prime_P1": 22, "gold_P1": 4, "gold_centring_ops": 4, "silicon_Fd-3m": 8, "magnesium_P63mmc": 2, @@ -85,7 +85,7 @@ # extras beyond the 5.3 list "magnesium_P63mmc_24ops": 2, "tetragonal_a_equals_c_4ops": 5, "alpha_quartz_P3221_as_seeded": 12, "pyrite_Pa-3_as_seeded": 8, } -# Fixtures in the order of registration 5.3 (monoclinic first), then the extras. +# Fixtures in the planted-truth order (monoclinic first), then the extras. FIXTURE_ORDER = [ "gold_P1", "gold_centring_ops", "silicon_Fd-3m", "magnesium_P63mmc", "rocksalt_Fm-3m_192ops", "alpha_quartz_P3221", "calcite_R-3c_hex", "fluorapatite_P63m", "pyrite_Pa-3", "ws2_2H", "cdi2_P-3m1", @@ -93,7 +93,7 @@ "magnesium_P63mmc_24ops", "tetragonal_a_equals_c_4ops", "alpha_quartz_P3221_as_seeded", "pyrite_Pa-3_as_seeded", ] -# Registration 5.1: the seven parameter cells (monoclinic first). +# The seven parameter cells (monoclinic first). PARAMETER_CELLS = [ ("monoclinic", (15.16, 4.05, 6.74, 90.0, 105.3, 90.0)), ("cubic", (4.0, 4.0, 4.0, 90.0, 90.0, 90.0)), @@ -119,7 +119,7 @@ def check(name, ok, detail=""): # ---------------------------------------------------------------------------------------------------------------- -# Lattice (registration 3.1) +# Lattice # ---------------------------------------------------------------------------------------------------------------- def lattice_block(a, b, c, alpha, beta, gamma): """pymatgen's vesta=True lattice (a along x, b in xy) and everything the pin file records about it.""" @@ -134,13 +134,13 @@ def lattice_block(a, b, c, alpha, beta, gamma): check(f"{tag}: a along x, b in xy", abs(A[0, 1]) == 0 and abs(A[0, 2]) == 0 and abs(A[1, 2]) == 0) # reciprocal rows: a_i . b_j = delta_ij check(f"{tag}: a_i . b_j = delta_ij", np.allclose(A @ B.T, np.eye(3), rtol=0, atol=1e-12)) - # registration 3.1: B = G^-1 A and G = A A^T + # B = G^-1 A and G = A A^T check(f"{tag}: B = G^-1 A", np.allclose(B, np.linalg.inv(G) @ A, rtol=0, atol=1e-12)) # volume: |det A| equals the closed form to 1e-12 relative ca, cb, cg = (math.cos(math.radians(x)) for x in (alpha, beta, gamma)) v_closed = a * b * c * math.sqrt(1 - ca * ca - cb * cb - cg * cg + 2 * ca * cb * cg) check(f"{tag}: volume closed form", abs(V - v_closed) <= 1e-12 * v_closed, f"{V} {v_closed}") - # registration 3.1's third row + # the third row of A sg = math.sin(math.radians(gamma)) a3 = np.array([c * cb, -c * (cb * cg - ca) / sg, V / (a * b * sg)]) check(f"{tag}: third lattice row closed form", np.allclose(A[2], a3, rtol=0, atol=1e-12 * max(a, b, c))) @@ -154,7 +154,7 @@ def g_vectors(H, B): def hkl_range_table(L, B, lo=-3, hi=3): - """Every h, k, l in lo..hi except 0,0,0 with |g| and d (registration 5.1).""" + """Every h, k, l in lo..hi except 0,0,0 with |g| and d.""" H = np.array([h for h in itertools.product(range(lo, hi + 1), repeat=3) if h != (0, 0, 0)], dtype=np.int64) _, g = g_vectors(H, B) d = 1.0 / g @@ -219,7 +219,7 @@ def check_truth_5_1(system, params, table): def check_monoclinic_anchors(table): - # macTEM S2c hand anchors, registration 5.1 (5-6 significant digits as written there) + # macTEM's hand anchors (5-6 significant digits as written there) want = {(2, 0, 0): 7.31135, (0, 0, 1): 6.50112, (0, 2, 0): 2.025, (1, 1, 0): 3.90306, (2, 0, 1): 4.32457, (2, 0, -1): 5.65554} by = {tuple(e["hkl"]): e["d"] for e in table} @@ -228,7 +228,7 @@ def check_monoclinic_anchors(table): # ---------------------------------------------------------------------------------------------------------------- -# Symmetry: operations, Laue group, Laue class (registration 3.3, 3.5) +# Symmetry: operations, Laue group, Laue class # ---------------------------------------------------------------------------------------------------------------- I3 = np.eye(3, dtype=np.int64) @@ -318,7 +318,7 @@ def check_group_closed(name, Rs): def laue_class_from_group(Rs, proper_only=True): - """Registration 3.5: by the order and the axis orders present. 'A 4-fold / a 6-fold' is read as a PROPER rotation + """By the order and the axis orders present. 'A 4-fold / a 6-fold' is read as a PROPER rotation (det +1) of that order; read over every element (improper ones included) the table fails, because -3 has order 6 (see the report): proper_only=False reproduces the literal reading for the diagnostic.""" n = len(Rs) @@ -347,7 +347,7 @@ def laue_class_from_group(Rs, proper_only=True): # ---------------------------------------------------------------------------------------------------------------- -# Reflections (registration 3.6, 3.8, 3.9) +# Reflections # ---------------------------------------------------------------------------------------------------------------- def enumerate_hkl(L, A, B, kmax): """pymatgen's get_points_in_sphere on the crystallographic reciprocal lattice, cross-checked against a brute-force @@ -415,7 +415,7 @@ def canonical(self, hkl): def allowed_flags(H, Rs, ts): - """Registration 3.8: absent iff some operation has (hkl) R == (hkl) and (hkl) . t not an integer within 1e-9.""" + """Absent iff some operation has (hkl) R == (hkl) and (hkl) . t not an integer within 1e-9.""" HR = np.einsum("mi,nij->mnj", H, Rs) fixed = np.all(HR == H[:, None, :], axis=2) dot = H.astype(float) @ np.asarray(ts, dtype=float).T @@ -431,7 +431,7 @@ def orbit_name(c, hex_axes): def build_families(seq, cache, hex_axes): - """seq: [(hkl tuple, g)] already in sweep order. Registration 3.9.""" + """seq: [(hkl tuple, g)] already in sweep order.""" fams = [] cur = [] g0 = None @@ -578,7 +578,7 @@ def iff(label, rule): check("5.3 pyrite: 12 sites, m-3 (not m-3m), 205", entry["laue_class"] == "m-3" and entry["space_group_number"] == 205) fa = entry["families_all"] # the two orbits of the (2 1 0) ring under m-3 have canonical forms (2 1 0) and (2 0 1) by 3.6's key (the cyclic - # permutation carries (1 2 0) to (2 0 1)); the registration's prose calls them {2 1 0} and {1 2 0} + # permutation carries (1 2 0) to (2 0 1)); the prose of the prediction calls them {2 1 0} and {1 2 0} fam_all = [n for n in fa["first_names"] if n == "{2 1 0} + {2 0 1}"] check("5.3 pyrite: the (2 1 0) ring is two orbits of 12 on the full list", len(fam_all) == 1, f"{fa['first_names']}") if name == "ws2_2H": diff --git a/tools/slice-1b/pins.py b/tools/slice-1b/pins.py index c46fe5a..7811e18 100644 --- a/tools/slice-1b/pins.py +++ b/tools/slice-1b/pins.py @@ -1,7 +1,7 @@ -"""DiffractoKit slice 1b: the pin generator (registration docs/registration-slice-1b.md, section 2). +"""DiffractoKit slice 1b: the pin generator for the conventional setting. -Run, from any directory, with the pinned oracle, UNDER THE SUITE MEMORY WATCHDOG (owner, 2026-10-15 12:30: setrlimit and -`ulimit -v` fail on macOS 27, so the cap is the watchdog's, 8 GB, exit 137; one such worker at a time): +Run, from any directory, with the pinned oracle, UNDER A MEMORY CAP (setrlimit and +`ulimit -v` fail on macOS 27, so the cap is an external watchdog's, 8 GB; one such worker at a time): ../References/python/orix/bin/python -I -B tools/slice-1b/pins.py @@ -45,7 +45,7 @@ OUT = os.path.join(FIXTURES, "slice-1b-pins.json") SEED = 20261015 -RANDOM_PER_LATTICE = 200 # registration 2: 200; the report states it if this is reduced +RANDOM_PER_LATTICE = 200 # 200; the report states it if this is reduced SYMPREC = 1e-4 NIGGLI_TOL = 1e-5 # spglib niggli_reduce(eps=) and pymatgen get_niggli_reduced_lattice(tol=) K_MAX = 1.2 @@ -196,7 +196,7 @@ def structure_entry(name, source, params, Z, occ, frac): # ---------------------------------------------------------------------------------------------------------------- -# The named non-conventional cells (registration 2) +# The named non-conventional cells # ---------------------------------------------------------------------------------------------------------------- def transformed(A, frac, P): """P: columns are the new basis vectors in terms of the old (x_old = P x_new). Returns params, frac_new.""" @@ -245,7 +245,7 @@ def named_cells(slice1): check("corundum rhombohedral cell holds 10 atoms", len(prim) == 10, f"{len(prim)}") out.append(("corundum_rhombohedral_axes", params_of(prim.lattice.matrix), [int(s.specie.Z) for s in prim], [1.0] * len(prim), [list(map(float, s.frac_coords)) for s in prim])) - # the 7x supercell fixture of Gate B (a P1 list) + # the 7x supercell fixture of the review (a P1 list) path = os.path.join(FIXTURES, "refuter_F3_super7.cif") with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -359,7 +359,7 @@ def main(): for name, params, Z, occ, frac in named_cells(slice1): structures.append(structure_entry(name, "named", params, Z, occ, frac)) - # registration 3: the counts the registration states + # the counts stated before the run by = {s["name"]: s for s in structures} check("truth: Au primitive is cF with 192 conventional operations, m-3m", by["au_fcc_primitive"]["lattice"]["bravais"] == "cF" and by["au_fcc_primitive"]["conventional_operations_count"] == 192 diff --git a/tools/slice-2/README.md b/tools/slice-2/README.md index 2a14a33..6f2abe9 100644 --- a/tools/slice-2/README.md +++ b/tools/slice-2/README.md @@ -1,6 +1,6 @@ # tools/slice-2: the pin generators -Registration `docs/registration-slice-2.md` (sections 2, 4, 5, 6). Written before any Swift of slice 2 exists; the Swift tests +Written before any Swift of slice 2 (DiffractoScattering) existed; the Swift tests read the files these write. A difference is a failure of the port, never a tolerance to widen. ``` @@ -14,7 +14,7 @@ tools/slice-2/apps-parity/run.sh # about 4 s apps' presets Au, Al, Si, Mg, WS2; Cu3Au-occ and Weak-cut from macTEM a58c84c; ideal hcp; the extra non-centrosymmetric zincblende GaAs; beta'' and ten slice-1 CIF fixtures), each with every reflection above 1e-6 in |F|, the kept count, the rings after the cut, the ring counts on the operation-allowed and on all reflections, the operation-absent check; the - mutation-moves (registration 6.3); pymatgen's X-ray pattern per peak with the explicit half-space members (4.3); the + mutation-moves; pymatgen's X-ray pattern per peak with the explicit half-space members; the `cut_edge` cell whose every |F| is exactly 1e-4; the registered predictions with the measured values. - The oracle is NumPy written from the definitions (not from any Swift), the Laue group comes from pymatgen `SpacegroupAnalyzer` (symprec 1e-4), the ring rule and canonical forms are slice 1's (copied, not imported), and @@ -24,4 +24,4 @@ tools/slice-2/apps-parity/run.sh # about 4 s predicted count differs (printed twice; the number is never edited to fit). - `apps-parity/` compiles `Crystal.swift` and `ScatteringFactors.swift` of mac4DSTEM 7d6fdbd1 with `main.swift` (swiftc `-O` and `-Onone`, `-swift-version 6 -package-name appsparity`) in `.build/apps-parity/` and writes `slice-2-apps.json`; it - compares the two optimisation levels (the -O dump is the reference; amendment 1). + compares the two optimisation levels (the -O dump is the reference). diff --git a/tools/slice-2/fetch-table.sh b/tools/slice-2/fetch-table.sh index 2f8b0f7..9c57bea 100755 --- a/tools/slice-2/fetch-table.sh +++ b/tools/slice-2/fetch-table.sh @@ -3,7 +3,7 @@ # Adapted: the same py4DSTEM lock commit and the same sha256, the destination is this kit's gitignored References/ (the # script sits one level deeper than macTEM's, tools/slice-2/, and the root is still two levels up). # END PROVENANCE -# The py4DSTEM electron-scattering-factor table lock (registration docs/registration-slice-2.md, section 2) - fetched, not vendored. +# The py4DSTEM electron-scattering-factor table lock - fetched, not vendored. # # tools/slice-2/pins.py builds its reference f_e from THIS table, not from any Swift table: the pins are independent of the # port only if the table comes from elsewhere. The file is py4DSTEM/process/utils/scattering_factors.txt at the lock commit diff --git a/tools/slice-2/pins.py b/tools/slice-2/pins.py index 2980c5d..11ba329 100644 --- a/tools/slice-2/pins.py +++ b/tools/slice-2/pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit slice 2: the pin generator (registration docs/registration-slice-2.md, sections 2, 3, 4.1-4.3, 5, 6). +"""DiffractoKit slice 2: the pin generator for scattering factors and structure factors. Run, from any directory, with the pinned oracle: @@ -7,10 +7,10 @@ It writes Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json BEFORE any Swift exists. Deterministic: no clock, no random numbers, sorted sets; two runs give the same bytes. -Oracle 1 is NumPy, written from the definitions of registration section 3 (not from any Swift): the table is +Oracle 1 is NumPy, written from the definitions in the comments below (not from any Swift): the table is np.loadtxt of the fetched py4DSTEM file, F(hkl) = (1/V) sum_sites f_e(Z, |g|^2) occ exp(2 pi i (h x + k y + l z)). The Laue group (for orbits and names) comes from pymatgen's SpacegroupAnalyzer (symprec 1e-4), the family rule and the -canonical form are slice 1's (registration-slice-1 sections 3.6, 3.8, 3.9; copied from tools/slice-1/pins.py, imported +canonical form are slice 1's (copied from tools/slice-1/pins.py, imported from nowhere). Oracle 2 is pymatgen's XRDCalculator (an independent implementation of the phase sum with X-ray form factors). Agreement is agreement, not truth: section 5's closed forms and counts are asserted below too. @@ -64,7 +64,7 @@ CUT = 1e-4 # |F| cut, Angstrom (the apps') LISTED = 1e-6 # everything above this is listed so the edge band of the cut is visible LIST_RADIUS = K_MAX * (1 + 1e-6) -FAMILY_TOL = 1e-9 # registration 3.3 / slice 1 3.9 +FAMILY_TOL = 1e-9 # slice 1's ring rule TIE_TOL = 1e-12 # slice 1 3.8: equal |g| within 1e-12 relative are ordered by descending (h, k, l) SYMPREC = 1e-4 ABSENCE_TOL = 1e-9 # slice 1 3.8 @@ -104,7 +104,7 @@ def predict(name, predicted, measured): # ---------------------------------------------------------------------------------------------------------------- -# The table and f_e (registration 3.1) +# The table and f_e # ---------------------------------------------------------------------------------------------------------------- def load_table(): data = open(TABLE_PATH, "rb").read() @@ -125,7 +125,7 @@ def f_e(table, z, g2): # ---------------------------------------------------------------------------------------------------------------- -# Lattice (registration slice 1, 3.1; the apps' Crystal.latReal), written from the definitions +# Lattice (slice 1's; the apps' Crystal.latReal), written from the definitions # ---------------------------------------------------------------------------------------------------------------- def lattice(a, b, c, al, be, ga): ca, cb, cg = (math.cos(math.radians(x)) for x in (al, be, ga)) @@ -156,7 +156,7 @@ def metric_is_hexagonal(cell, eps=1e-9): # ---------------------------------------------------------------------------------------------------------------- -# Slice 1's reflection order, orbits, names, ring rule (copied from tools/slice-1/pins.py, registration 3.6, 3.8, 3.9) +# Slice 1's reflection order, orbits, names, ring rule (copied from tools/slice-1/pins.py) # ---------------------------------------------------------------------------------------------------------------- I3 = np.eye(3, dtype=np.int64) @@ -208,7 +208,7 @@ def check_group_closed(name, Rs): def laue_class_from_group(Rs): - """Registration slice 1, 3.5, with 'a 4-fold / a 6-fold' read as a proper rotation (det +1) of that order.""" + """Slice 1's Laue class rule, with 'a 4-fold / a 6-fold' read as a proper rotation (det +1) of that order.""" n = len(Rs) dets = [int(round(np.linalg.det(R))) for R in Rs] orders = [matrix_order(R) for R in Rs] @@ -288,7 +288,7 @@ def orbit_name(c, hex_axes): def build_families(seq, cache, hex_axes): - """seq: [(hkl tuple, g, |F|^2)] in sweep order (kept reflections). Registration 3.3 = slice 1 3.9, plus sumF2.""" + """seq: [(hkl tuple, g, |F|^2)] in sweep order (kept reflections). Slice 1's ring rule, plus sumF2.""" fams = [] cur = [] g0 = None @@ -319,7 +319,7 @@ def close(): # ---------------------------------------------------------------------------------------------------------------- -# Reflections and structure factors (registration 3.2), from the definitions +# Reflections and structure factors, from the definitions # ---------------------------------------------------------------------------------------------------------------- def enumerate_box(A, B, kmax): """Every (h, k, l) != 0 with |g| <= kmax in the per-axis box |h| <= ceil(kmax |a_i|) + 1 (one more than the registered @@ -629,7 +629,7 @@ def make_entry(name, how, cell, sites, snapped_sites, sym_structure, build_extra # ---------------------------------------------------------------------------------------------------------------- -# Truth (registration section 5): extinctions, closed forms, weak cut +# Truth: extinctions, closed forms, weak cut # ---------------------------------------------------------------------------------------------------------------- def truth_checks(table, SS): def S_of(n): @@ -756,10 +756,10 @@ def rel_close(computed, closed, scale, label): # ---------------------------------------------------------------------------------------------------------------- -# Mutation-moves (registration 6.3): each planned mutant must move its target in NumPy by more than 1e-6 +# Mutation-moves: each planned mutation must move its target in NumPy by more than 1e-6 # ---------------------------------------------------------------------------------------------------------------- def mutation_moves(table, SS): - moves = {} # mutant -> {structure: value} + moves = {} # mutation -> {structure: value} designated = { "table_row_z_plus1": ["Au"], "fe_of_g_not_g2": ["Al"], @@ -825,7 +825,7 @@ def rec(mut, val): # ---------------------------------------------------------------------------------------------------------------- -# pymatgen's X-ray pattern (registration 4.3) +# pymatgen's X-ray pattern # ---------------------------------------------------------------------------------------------------------------- def xray_f(z, s2): sym = Element.from_Z(z).symbol @@ -987,7 +987,7 @@ def rc(n, k): return SS[n]["entry"]["ring_counts"][k] predict("beta'' operation-allowed rings at 1.2 (slice 1)", 419, rc("beta_double_prime", "operation_allowed")) members_b = sum(f["multiplicity"] for f in b["families"]) predict("beta'' reflections in the rings at 1.2 (sum of multiplicities)", 1442, members_b) - # operation-allowed reflections the cut dropped in beta'' (the registration names {3 3 3} and {14 0 1}) + # operation-allowed reflections the cut dropped in beta'' (the prediction names {3 3 3} and {14 0 1}) m = SS["beta_double_prime"]["main"] drop_names = {} for i in range(len(m["H"])): @@ -1038,7 +1038,7 @@ def rc(n, k): return SS[n]["entry"]["ring_counts"][k] "note": "with this form factor every reflection has |F| == 1e-4 exactly, so '>=' for '>' keeps all of them and '>' keeps none"} print(f"\ncut_edge: {len(Hc)} reflections within 1.2, every |F| == 1e-4 exactly") - # ---- X-ray (registration 4.3) ---- + # ---- X-ray ---- print("\n--- pymatgen X-ray pins (lambda 0.5 A) ---") xr = [] for n in ("Au", "Si", "Mg", "beta_double_prime", "Cu3Au-occ", "WS2"): diff --git a/tools/slice-3/README.md b/tools/slice-3/README.md index c73fd05..aaaa858 100644 --- a/tools/slice-3/README.md +++ b/tools/slice-3/README.md @@ -1,6 +1,6 @@ -# tools/slice-3: the ring-calibration and indexing pins (slice 3a) +# tools/slice-3: the ring-calibration and indexing pins -Registration `docs/registration-slice-3.md` (sections 1, 3, 4, 5). Written before the Swift tests read it; a difference between +Written before the Swift tests read it; a difference between the Swift and the pins is a failure of the port, never a band to widen. ``` @@ -12,7 +12,7 @@ the Swift and the pins is a failure of the port, never a band to widen. {311} {422} at a = 4.0782 and Al {111} {200} {220} at a = 4.0495; V, R in {40, 80, 200, 250} px, p in {0.014, 0.05} mm) with lambda, 2 theta, L_px, dq and L_mm; 68 refusal plants (each refusal on its boundary, several at once, invalid input); 18 Bragg-limit plants (lambda/2d = 1/sqrt(2), both sides at 1e-9, 1e-6 and a factor 2, at 60 / 200 / 300 kV); 10 radius plants - (3 % ellipticity, either axis major); the registration's section 4 literals recomputed in decimal (`truth`); the mutation list. + (3 % ellipticity, either axis major); the literals of `RingCalibrationTests` recomputed in decimal (`truth`); the mutation list. - The oracle is 60-digit `decimal` (arcsin by its power series, tan 2 theta by the identity 2 s sqrt(1 - s^2) / (1 - 2 s^2), so no trigonometric library is trusted), with NumPy 2.5.4 routes beside it that must agree to 1e-14. It is written from the definitions, ported from macTEM 9482831 `tools/ring-calibration-parity/reference.py` (read by `git show`). @@ -22,13 +22,13 @@ the Swift and the pins is a failure of the port, never a band to widen. - `RingCalibrationPinsTests` (Swift Testing) reads the fixture by `#filePath` and holds every number to 1e-12 relative, every refusal by name and in order, `withholdsDeltaQ` as registered. `RingCalibrationTests` are the 30 hand-arithmetic tests of macTEM 9482831, converted. -- One literal to know: the typed L_px of Au {111}, R 150 px, 200 kV (14082.307097295722, macTEM's test and the registration - section 4) is one ulp (1.3e-16 relative) from the correctly rounded decimal result (14082.307097295723). It is far inside the +- One literal to know: the typed L_px of Au {111}, R 150 px, 200 kV (14082.307097295722, macTEM's test and + `RingCalibrationTests`) is one ulp (1.3e-16 relative) from the correctly rounded decimal result (14082.307097295723). It is far inside the 1e-12 band; the generator holds the typed literals to 1e-15 relative and says so. -## The indexing pins (slice 3a) +## The indexing pins -Registration `docs/registration-slice-3.md` (sections 1, 3, 4, 5). Written before the Swift test reads it; a difference is a +Written before the Swift test reads it; a difference is a failure of the port, never a tolerance to widen. ``` @@ -38,7 +38,7 @@ failure of the port, never a tolerance to widen. - `index-pins.py` ports macTEM 9482831 `tools/indexing-parity/reference.py` (read by `git show`) onto the KIT's ring lists: the families after the |F| cut that slice 2 pinned in `Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json` (so `tools/slice-2/pins.py` runs first), at k_max 1.2: Au, Al, Si, Mg, ideal hcp, Weak-cut (macTEM's six) and beta''. -- The decision is NumPy written from the registration text, with a plain-Python scalar re-implementation that must agree on +- The decision is NumPy written from the definition in `RingIndexing.swift`'s header, with a plain-Python scalar re-implementation that must agree on every plant (z = |q - g|/sigma, delta = g/q - 1, candidates by (z, g), within = z <= k, ambiguous on two within or a margin below ln 19, reach = sqrt(k^2 + 2 ln 19) sigma, the verdict order invalidInput, ambiguous, beyondKMax, none, accepted). - **T1**: every family x u in {1e-4, 1e-3, 3e-3, 1e-2, 3e-2} x s in {+-2.9, +-2, +-1, +-0.5, 0}; q = g(1 + s u), sigma = u q, diff --git a/tools/slice-3/index-pins.py b/tools/slice-3/index-pins.py index 2c192e8..21e609a 100644 --- a/tools/slice-3/index-pins.py +++ b/tools/slice-3/index-pins.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""Pins for RingIndexing (registration docs/registration-slice-3.md sections 1, 3, 4, 5; slice 3a). +"""Pins for RingIndexing. Port of macTEM 9482831 tools/indexing-parity/reference.py (read by `git show`) onto the KIT's own ring lists: the families after the |F| cut that slice 2 pinned (Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json, written by -tools/slice-2/pins.py), at k_max 1.2. The decision is written here from the registration text (NumPy, plus a plain-Python +tools/slice-2/pins.py), at k_max 1.2. The decision is written here from its definition in DiffractoGeometry/RingIndexing.swift's header (NumPy, plus a plain-Python scalar re-implementation that must agree on every plant), NOT from any Swift: z = |q - g| / sigma, delta = g / q - 1, candidates ordered by (z, g); within = z <= k (not z^2 <= k); @@ -17,7 +17,7 @@ q = g (1 + s u), sigma = u q, k = 3 (closed forms P1: z = |s| / (1 + s u), delta = -s u / (1 + s u)). T4: the refusal plants macTEM registered, on the kit's lists (invalid numbers, beyondKMax, forbidden positions, ambiguous pairs, the margin plant, the one-family and the empty list). Lists: Au, Al, Si, Mg, ideal hcp, Weak-cut (macTEM's six, so the T1 counts equal its -registered prediction) and beta'' (macTEM's S2c, its accepted counts). Non-finite numbers are written as the strings +registered prediction) and beta'' (macTEM's accepted counts). Non-finite numbers are written as the strings "NaN", "Infinity", "-Infinity". Writes Tests/DiffractoGeometryTests/Fixtures/slice-3-index-pins.json BEFORE the Swift test is run. Deterministic (no clock, @@ -45,7 +45,7 @@ U1 = (1e-4, 1e-3, 3e-3, 1e-2, 3e-2) S1 = (-2.9, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 2.9) # macTEM's registered T1 counts per u over its six crystals (accepted as own, accepted as another family, ambiguous, -# beyondKMax, none): Au, Al, Si, Mg, Mg-ideal, Weak-cut; and beta'' (S2c, 'accepted in all' per u). +# beyondKMax, none): Au, Al, Si, Mg, Mg-ideal, Weak-cut; and beta'' ('accepted in all' per u). T1_PREDICT = {1e-4: (711, 0, 0, 36, 0), 1e-3: (691, 0, 20, 36, 0), 3e-3: (647, 0, 64, 36, 0), 1e-2: (408, 0, 309, 30, 0), 3e-2: (131, 19, 572, 11, 14)} T1_BETA_ACCEPTED = {1e-4: 2584, 1e-3: 508, 3e-3: 184, 1e-2: 58, 3e-2: 25} diff --git a/tools/slice-3/ring-pins.py b/tools/slice-3/ring-pins.py index 8116755..154fb08 100644 --- a/tools/slice-3/ring-pins.py +++ b/tools/slice-3/ring-pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit slice 3a: the ring-calibration pin generator (registration docs/registration-slice-3.md sections 1, 3, 4, 5). +"""DiffractoKit slice 3a: the ring-calibration pin generator. Run, from any directory, with the pinned oracle: @@ -37,7 +37,7 @@ decided in decimal arithmetic. Radius (10 plants): FitReport.radius = (a + b)/2 on axes planted with 3 % ellipticity, the major axis either way; the expected radius is numpy's mean, checked against the decimal sum of the two float64 axes. -Slice 3a additions to the seed: `truth` (the registration's section 4 literals recomputed in decimal and asserted equal to +Slice 3a additions to the seed: `truth` (the literals of RingCalibrationTests recomputed in decimal and asserted equal to the typed ones: lambda at 80/120/200/300 kV, Au {111} at R 150 px, 200 kV, p 0.014 mm; the small-angle excess; the ellipticity-bound gap), and the mutation list, written into the fixture beside the quantity each moves. @@ -64,7 +64,7 @@ import numpy as np NUMPY = "2.5.4" -TOL = 1e-12 # the registration's P1: the formula and lambda to 1e-12 relative +TOL = 1e-12 # P1: the formula and lambda to 1e-12 relative getcontext().prec = 60 # CODATA 2018, as exact decimal strings (see the docstring) @@ -79,7 +79,7 @@ MATERIALS = [("Au", 4.0782, [(1, 1, 1), (2, 0, 0), (2, 2, 0), (3, 1, 1), (4, 2, 2)]), ("Al", 4.0495, [(1, 1, 1), (2, 0, 0), (2, 2, 0)])] -# The registered refusal thresholds (ROADMAP.md "Refused, each by its own error") +# The refusal thresholds (each refused by its own error) ANGULAR_BINS = 36 MIN_BINS = 30 # fewer than 30 of 36 azimuthal bins RESIDUAL_CAP = 0.22 # the profile fit's cap on the normalised residual @@ -87,7 +87,7 @@ REFERENCE_HT_V = 200000.0 NAN, INF = float("nan"), float("inf") -# the refusal names (the Swift enum's `name`) and the check each is held under (the registration's mutation names) +# the refusal names (the Swift enum's `name`) and the check each is held under (the mutation names) CHECK_OF = {"accepted": "accept", "modelNotAsymmetricGaussian": "refuse-conic", "insufficientAngularCoverage": "refuse-bins", "sparseCoverage": "refuse-gap", "sigmaAtAnnulusWidth": "refuse-bound", "excessiveResidual": "refuse-residual", "excessiveEllipticity": "refuse-ellipticity", "noHighTension": "refuse-noV", "invalidInput": "refuse-invalid", @@ -192,7 +192,7 @@ def finite_positive(x): def decide(c): - """The ordered list of (name, value) the registration's thresholds give: model, coverage, sigma, residual, ellipticity, + """The ordered list of (name, value) the thresholds give: model, coverage, sigma, residual, ellipticity, high tension. `value` is the number the refusal reports (bins, widest sigma, residual, a/b - 1), None where it has none. A number that is not finite and positive, or a bin count out of range, is invalidInput alone.""" sigmas = [s for s in (c["inner_sigma"], c["outer_sigma"]) if s is not None] @@ -342,13 +342,13 @@ def jsonable(x): def truth_section(): - """The registration's section 4 literals, recomputed in 60-digit decimal and asserted equal to the typed ones.""" + """The literals of RingCalibrationTests, recomputed in 60-digit decimal and asserted equal to the typed ones.""" typed_lambda = {80: 0.041757160772834212, 120: 0.033492152726723738, 200: 0.025079340450548003, 300: 0.019687489006848796} for kv, want in typed_lambda.items(): got = float(dec_wavelength_a(kv * 1000.0)) if abs(got - want) > 1e-15 * want: fail("lambda(%d kV) = %r, the registration's literal is %r" % (kv, got, want)) - # The registration worked these in decimal from the DECIMAL d = 2.3546 and p = 0.014 (not from the float64 nearest to them); + # These were worked in decimal from the DECIMAL d = 2.3546 and p = 0.014 (not from the float64 nearest to them); # L_px differs from the typed literal by one ulp (1.3e-16 relative), far inside the 1e-12 band. lam, two_theta, l_px, dq, l_mm = dec_chain(200000.0, Decimal("2.3546"), 150.0, Decimal("0.014")) typed = {"two_theta_rad": 0.010651260937060768, "l_px": 14082.307097295722, "dq_per_A_per_px": 0.0028313372405787253, @@ -525,7 +525,7 @@ def mean_gap(eps): # (a + b)/2 over sqrt(ab) - 1 at b = 1, a = 1 + eps print("ring-pins: mutation %-17s -> %-22s %-10s %s" % (mut, target, check, what)) if not ok: fail("mutation %s does not move its target %s %s enough (%s)" % (mut, target, check, what)) - # the small-angle error the registration quotes, as a printed fact + # the quoted small-angle error, as a printed fact for name in ("Au111-200kV-R200-p0.014", "Au422-80kV-R200-p0.014"): c = by_name[name] print("ring-pins: small-angle L = R d / lambda is off by %.2e at %s (registration: 4e-5 at 200 kV {111}, 9.5e-4 at 80 kV {422})" diff --git a/tools/slice-3b/README.md b/tools/slice-3b/README.md index fb6a9e4..360d4b4 100644 --- a/tools/slice-3b/README.md +++ b/tools/slice-3b/README.md @@ -1,6 +1,6 @@ -# tools/slice-3b: the ring and ellipse fit (DiffractoGeometry, slice 3b) +# tools/slice-3b: the ring and ellipse fit (DiffractoGeometry) -Registration: `docs/registration-slice-3.md` sections 2, 3 and 5. Seeds read only by `git show`: mac4DSTEM `7d6fdbd1` +Seeds, from the apps' history before they were public: mac4DSTEM `7d6fdbd1` (`EllipseCalibration.swift`, `DiffractionPattern.swift`, `Calibration.swift`, the noisy, stop-rule and row-29 tests, the `tools/ellipse-calibration-test` sweep) and macTEM `9482831` (`RingCalibration+Fit.swift`, its 8 tests). @@ -11,20 +11,19 @@ Registration: `docs/registration-slice-3.md` sections 2, 3 and 5. Seeds read onl files go to `.build/apps-parity-3b/sweep`. The test `EllipseAppsParityTests` requires the kit equal in every bit. - `sweep.py` (`../References/python/orix/bin/python -I -B tools/slice-3b/sweep.py`): SciPy 1.18.1 `least_squares` optimum of fit1D's objective on the 20 sweep rings -> `Fixtures/slice-3b-sweep.json`; held in `EllipseCalibrationTests`. -- `mutants.py `: the break-once run, in a copy of the repository. Order: `apps-parity/run.sh` first (it writes the dump and the pixel files), then `sweep.py`. -## Results (2026-10-15, DiffractoKit lane 3b) +## Results (2026-10-15) - Bit identity: the kit's `fit1D` / `fitAmorphousRing` / `fitBestAvailable` equal the apps' (mac4DSTEM 7d6fdbd1) in every output bit on 35 rings x 6 calls, compiled -O and compiled -Onone (run.sh kit-check, and `EllipseAppsParityTests` in a Debug `swift test` against the -Onone dump; a release `swift test` reads the -O dump). The apps' own -O and -Onone dumps differ in the last bits of some profile fits (25 ulps in a centre, 98 in a theta: reassociation/contraction at -O), so each build is held to the apps built the same way; this is why there are two fixtures. -- The parity set does not see three of the nine mutants (floor 0.30, the profile residual cap, the outer annulus boundary: no ring +- The parity set does not see three of the nine mutations (floor 0.30, the profile residual cap, the outer annulus boundary: no ring of the set ends within 0.30 px of the floor, or at the cap, or has a pixel exactly on the boundary); `EllipseCalibrationTests` - holds those three (`parity-mutants.py` shows which). + holds those three. - macTEM's 8 `RingFitCalibrationTests` pass unchanged on the 7d6fdbd1 fit. Against macTEM's pre-ADR-069 copy the planted ring's fit moved by 1e-9 px / 1e-9 relative (clean 2 %: a 60.594059408098644 against 60.59405940811859) and the 12 % ring likewise. - Clean sweep against SciPy 1.18.1: centre 7.1e-8 px, axes 1.2e-9 relative, theta 5.0e-8 rad (bands 1e-5, 1e-7, 1e-6). diff --git a/tools/slice-3b/sweep.py b/tools/slice-3b/sweep.py index de5fc67..0bbf419 100644 --- a/tools/slice-3b/sweep.py +++ b/tools/slice-3b/sweep.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""DiffractoKit slice 3b, optional gate (registration docs/registration-slice-3.md section 3): the clean sweep against SciPy. +"""DiffractoKit slice 3b, optional check: the clean sweep against SciPy. Reads the 20 clean sweep rings tools/slice-3b/apps-parity/run.sh dumped (the raw Float32 pixels the Swift renderer made, .build/apps-parity-3b/sweep/sweep-.f32, and their parameters in .build/apps-parity-3b/dump-O.json), reads the annulus fit1D diff --git a/tools/slice-4/README.md b/tools/slice-4/README.md index 7543d32..b3d961f 100644 --- a/tools/slice-4/README.md +++ b/tools/slice-4/README.md @@ -1,7 +1,7 @@ # tools/slice-4: the pin generators -Registration `docs/registration-slice-4.md` (sections 4, 5, 6, 7). Written before the Swift of slice 4 is trusted; the Swift -tests read the file these write. A difference is a failure of the port, never a tolerance to widen (except the amendments +Written before the Swift of slice 4 (DiffractoOrientation) was trusted; the Swift +tests read the file these write. A difference is a failure of the port, never a tolerance to widen (except the changes named under "Measured floors and open points"). ``` @@ -23,7 +23,7 @@ named under "Measured floors and open points"). | Name | What | Where in the JSON | |---|---|---| -| V2, the reference | NumPy float64 from the registration's definitions: f_e from the sha-locked py4DSTEM table, F = (1/V) sum f_e occ exp(2 pi i h.x), every hkl with `|F| > 1e-4` (strict) and `|g| <= kMax` enumerated by the bound `|h| <= kMax |a_1|` (cannot miss a lattice point), `s = g.n`, `I = |F|^2 exp(-(s/w)^2)`, kept when `|s| <= slab`, `1e-9 < |q| <= kMax`, `I > 0`; frame `e1 = normalise(n x r)`, `e2 = n x e1`; floor, cap, relative intensity, sort by `|q|` | `phases[].zone_axes[].entries` (pre-cap, pre-floor) and `.library` (after floor and cap) | +| V2, the reference | NumPy float64 from the written definitions: f_e from the sha-locked py4DSTEM table, F = (1/V) sum f_e occ exp(2 pi i h.x), every hkl with `|F| > 1e-4` (strict) and `|g| <= kMax` enumerated by the bound `|h| <= kMax |a_1|` (cannot miss a lattice point), `s = g.n`, `I = |F|^2 exp(-(s/w)^2)`, kept when `|s| <= slab`, `1e-9 < |q| <= kMax`, `I > 0`; frame `e1 = normalise(n x r)`, `e2 = n x e1`; floor, cap, relative intensity, sort by `|q|` | `phases[].zone_axes[].entries` (pre-cap, pre-floor) and `.library` (after floor and cap) | | V0, py4DSTEM | `Crystal.generate_diffraction_pattern` at 1e30 V (lambda about 1.2e-26 A), `sigma_excitation_error = w/sqrt(2)`, `tol_excitation_error_mult = slab/sigma`, `tol_intensity = 0`, `k_max = kMax`, `keep_qz`; (0 0 0) dropped; `parse_orientation(zone_axis_lattice=uvw)` | the `py4dstem_*` columns of `entries`, `py4dstem`, `frame_map` | | V1 | NumPy flat excitation on py4DSTEM's own hkl, g and float32 `|F|^2` | only its residual against V0, `counts.lambda0_vs_flat_numpy_on_py4dstem_table` | | 200 kV | the same call at `setup_diffraction(200e3)`, curved Ewald sphere | `zone_axes[].kv200` | @@ -56,7 +56,7 @@ matches by hkl). boundary), `containsFriedelPair` (20 + the boundary), `inPlaneSteps` (13 steps), `lowIndexZoneAxes`, `PhaseVectorResolution` (not pinned, see below). -## Measured floors and open points (the report to the registration) +## Measured floors and open points 1. **lambda -> 0 works through py4DSTEM's own API**: `setup_diffraction(1e30)` gives s = g.n exactly in practice. Against NumPy flat excitation on py4DSTEM's own table the residual is 0 on six phases and 1.5e-14 relative on beta'' (the `exp(-s^2/(2 sigma^2))` @@ -65,7 +65,7 @@ matches by hkl). 2. **py4DSTEM's structure factors are complex64.** `calculate_structure_factors` accumulates `struct_factors` as `complex64` (`|F|^2` float32), so its relative intensity differs from the float64 value by up to 1.15e-5 (beta'', the weak reflections), 3.1e-6 (theta'), 1.8-3.6e-7 on the others, and `|F|^2` by up to 1.3e-5. **G2's "relative intensity within 1e-9" cannot hold against - py4DSTEM**; proposed amendment: hkl sets equal and `|q|` within 1e-12 against py4DSTEM (measured 8.9e-16), relative intensity within + py4DSTEM**; adopted instead: hkl sets equal and `|q|` within 1e-12 against py4DSTEM (measured 8.9e-16), relative intensity within 5e-5 against py4DSTEM (the float32 floor, measured 1.2e-5), within 1e-12 against the float64 reference V2 (the `entries` / `library` columns); the kit's q rotated by phi_py equals py4DSTEM's x', y' within 1e-12 (measured 8.9e-16). 3. **|F|^2 against slice 2** (kit float64, Al, Au, Mg, beta'' at kMax 1.2, kept sets equal): max relative difference 1.6e-15 (Al), @@ -80,18 +80,18 @@ matches by hkl). 6. **200 kV, curved Ewald sphere** (the same sigma and tolerance): the slab set changes only for theta' [112] (6 hkl enter, 0 leave), theta' [123] (21 in, 0 out), beta'' [111] (51 in, 0 out), beta'' [123] (0 in, 54 out); the other 33 zone axes are identical sets (relative intensities differ by 1-24%, `max_abs_dI_relative_common`). A parity row, not a gate. -7. **The low-index zone axes: the registration says 62, the stated definition gives 49** (98 primitive vectors in the box, divided by +7. **The low-index zone axes: the first prediction said 62, the stated definition gives 49** (98 primitive vectors in the box, divided by 2 for sign). 62 is the count of the 124 nonzero vectors deduplicated up to sign without the gcd reduction; both lists are in the - file (`axes`, `alternative_unreduced`). The apps-parity dump of the source decides; the amendment is the registration's. -8. **SciPy's gimbal rule is not the registered one.** SciPy 1.18.1 locks (sets the third angle to 0) when the middle angle is below - 1e-7 (100% at 9.9e-8, 50% at exactly 1e-7, 0% at 1.01e-7), registration 4 says |sin Phi| <= 1e-8. Cases with Phi in {2e-8, 5e-8, - 9e-8, 1e-7} are marked `rules_agree: false`; tests skip them or the registration amends the threshold to SciPy's. Phi = 1e-5, 1e-6 - and 1e-3 are unlocked in both, so mutant 13 (1e-4) is caught. -9. **Definitions the registration does not give** (so the pins hold a stated reading, flagged in the file): `chanceMatchFraction` guards + file (`axes`, `alternative_unreduced`). The apps-parity dump of the source decides. +8. **SciPy's gimbal rule is not the one first written.** SciPy 1.18.1 locks (sets the third angle to 0) when the middle angle is below + 1e-7 (100% at 9.9e-8, 50% at exactly 1e-7, 0% at 1.01e-7), the written rule was |sin Phi| <= 1e-8. Cases with Phi in {2e-8, 5e-8, + 9e-8, 1e-7} are marked `rules_agree: false`; tests skip them. Phi = 1e-5, 1e-6 + and 1e-3 are unlocked in both, so the threshold mutation (1e-4) is caught. +9. **Definitions not written before the run** (so the pins hold a stated reading, flagged in the file): `chanceMatchFraction` guards (R <= 0, non-finite inputs: not pinned), `score` (pinned as the nearest-assignment reading; the any-pair reading is recorded and the cases where they differ are flagged; none differ in the 20 seeded cases), `containsFriedelPair` (some pair with |v_i + v_j| <= radius), - the unit of `inPlaneSteps` rounding (half away from zero). `PhaseVectorResolution` and `scaledToDetector` are **not pinned**: section 4 - does not define them and the oracle may not read the Swift. + the unit of `inPlaneSteps` rounding (half away from zero). `PhaseVectorResolution` and `scaledToDetector` are **not pinned**: the written + definitions do not cover them and the oracle may not read the Swift. 10. theta'-Al2Cu has no definition in the apps or the kit. Pinned: body-centred tetragonal 4.04 / 5.80 A, Cu 2a (0,0,0), (1/2,1/2,1/2), Al 4d (0,1/2,1/4), (1/2,0,1/4), (0,1/2,3/4), (1/2,0,3/4) (the fluorite-derived arrangement), sites written out. The triclinic cell is - registration-slice-1's (4, 5, 6, 80, 85, 95) with two invented sites: the kit has no triclinic CIF fixture. + slice 1's (4, 5, 6, 80, 85, 95) with two invented sites: the kit has no triclinic CIF fixture. diff --git a/tools/slice-4/pins-py4dstem.py b/tools/slice-4/pins-py4dstem.py index fc1b05b..ac553bc 100644 --- a/tools/slice-4/pins-py4dstem.py +++ b/tools/slice-4/pins-py4dstem.py @@ -1,4 +1,4 @@ -"""DiffractoKit slice 4: the py4DSTEM half of the pin generator (registration docs/registration-slice-4.md, sections 4, 5, 6). +"""DiffractoKit slice 4: the py4DSTEM half of the pin generator. Not run by hand: tools/slice-4/pins.py starts it with the py4DSTEM venv's interpreter, sends the phases as JSON on stdin and reads the JSON on stdout. Run through pins.py the whole chain sits under ONE memory cap. diff --git a/tools/slice-4/pins.py b/tools/slice-4/pins.py index 6bed99f..948826d 100644 --- a/tools/slice-4/pins.py +++ b/tools/slice-4/pins.py @@ -1,4 +1,4 @@ -"""DiffractoKit slice 4: the pin generator (registration docs/registration-slice-4.md, sections 4, 5, 6, 7). +"""DiffractoKit slice 4: the pin generator for the orientation and phase-matching module. Run, from any directory, under the memory cap, with the pymatgen venv (Python 3.14, NumPy 2.5.4, SciPy 1.18.1): @@ -13,10 +13,10 @@ What is an oracle for what -------------------------- -V2 "reference": NumPy float64 written from the registration's DEFINITIONS (section 4), the structure factors from the +V2 "reference": NumPy float64 written from the DEFINITIONS in this file, the structure factors from the py4DSTEM electron-scattering table (slice 2's sha-locked file) with the cell/g conventions py4DSTEM uses, every hkl with |F| > 1e-4 (strict, slice 2) and |g| <= kMax enumerated by a bound that cannot miss a lattice point, the FLAT excitation - s = g.n, I = |F|^2 exp(-(s/w)^2), the in-plane frame (e1, e2) of the registration. This is what the Swift library must equal + s = g.n, I = |F|^2 exp(-(s/w)^2), the in-plane frame (e1, e2) of `ZoneAxisFrame`. This is what the Swift library must equal (G4, and the pre-cap entries of G2). V0 py4DSTEM f050d207 as it is called (Crystal.generate_diffraction_pattern at a huge accelerating voltage, sigma = w/sqrt(2), tol_excitation_error_mult = slab/sigma, tol_intensity 0): its hkl set, g in its projected frame, relative intensity. @@ -58,7 +58,7 @@ OUT = os.path.join(OUT_DIR, "slice-4-pins.json") # ---------------------------------------------------------------------------------------------------------------- -# The registered constants (registration section 3: the defaults; section 4: the method's constants) +# The registered constants (the defaults and the method's constants) # ---------------------------------------------------------------------------------------------------------------- TABLE_SHA256 = "be845d1d541cdb75ae16f8928cd9e56a9ea38af8a3cc43da9137e73cc72bb6f4" K_MAX = 1.6 @@ -162,7 +162,7 @@ def structure_factors(table, vol, H, glen, sites): # ---------------------------------------------------------------------------------------------------------------- -# The phases (registration 6) +# The phases # ---------------------------------------------------------------------------------------------------------------- FCC = [(0.0, 0.0, 0.0), (0.5, 0.5, 0.0), (0.5, 0.0, 0.5), (0.0, 0.5, 0.5)] HCP = [(0.0, 0.0, 0.0), (2.0 / 3.0, 1.0 / 3.0, 0.5)] @@ -252,10 +252,10 @@ def phases(): # ---------------------------------------------------------------------------------------------------------------- -# The reference (V2): the registration's definitions, in float64 +# The reference (V2): the definitions, in float64 # ---------------------------------------------------------------------------------------------------------------- def kit_frame(n): - """Registration 4, 'The frame': e1 = normalise(n x r), e2 = n x e1, r = x-hat if |n_x| < 0.9 else y-hat.""" + """The frame: e1 = normalise(n x r), e2 = n x e1, r = x-hat if |n_x| < 0.9 else y-hat.""" r = np.array([1.0, 0.0, 0.0]) if abs(n[0]) < BASIS_SWITCH else np.array([0.0, 1.0, 0.0]) e1 = np.cross(n, r) e1 = e1 / np.linalg.norm(e1) @@ -269,7 +269,7 @@ def cartesian_axis(A, uvw): def excited_entries(H, g, F2, n, e1, e2, kmax=K_MAX, slab=SLAB, width=WIDTH): - """The pre-cap, pre-floor set of registration 4, 'Reference vector'. Returns arrays (all in the input order).""" + """The pre-cap, pre-floor set of reference vectors. Returns arrays (all in the input order).""" s = g @ n q1, q2 = g @ e1, g @ e2 qlen = np.sqrt(q1 * q1 + q2 * q2) @@ -334,7 +334,7 @@ def euler_block(rng, orientation_matrices): for i in range(1000): cases.append(euler_case(f"random-{i}", rots[i])) # gimbal and near-gimbal: Phi = 0, pi and 1e-9, 1e-10 away from each; 5e-9 (inside |sin Phi| <= 1e-8), then 2e-8, 1e-7, - # 1e-6, 1e-5, 1e-3 (outside; a threshold of 1e-4 would treat 1e-5 and 1e-6 as locked, which the registration does not) + # 1e-6, 1e-5, 1e-3 (outside; a threshold of 1e-4 would treat 1e-5 and 1e-6 as locked, which the definition does not) pairs = [(0.7, -1.1), (-2.5, 2.0), (3.0, 3.0)] phis = [("0", 0.0), ("pi", math.pi), ("1e-9", 1e-9), ("1e-10", 1e-10), ("pi-1e-9", math.pi - 1e-9), ("pi-1e-10", math.pi - 1e-10), ("5e-9", 5e-9), ("2e-8", 2e-8), ("5e-8", 5e-8), ("9e-8", 9e-8), ("1e-7", 1e-7), ("1e-6", 1e-6), ("1e-5", 1e-5), ("1e-3", 1e-3), @@ -359,7 +359,7 @@ def euler_block(rng, orientation_matrices): def rz_cases(rng, frames): - """Registration 4: matrix(detectorBasis: B, inPlaneAngle: a) = B . R_z(a), R_z columns (c, -s, 0), (s, c, 0), (0, 0, 1).""" + """matrix(detectorBasis: B, inPlaneAngle: a) = B . R_z(a), R_z columns (c, -s, 0), (s, c, 0), (0, 0, 1).""" out = [] bases = [(f"frame-{lbl}", B) for lbl, B in frames[:10]] rots = Rotation.random(10, rng=rng).as_matrix() @@ -375,10 +375,10 @@ def rz_cases(rng, frames): # ---------------------------------------------------------------------------------------------------------------- -# Closed forms (NumPy from the registration's words) +# Closed forms (NumPy from the written definitions) # ---------------------------------------------------------------------------------------------------------------- def chance_match_fraction(V, rho, R): - """p = min(1, V rho^2 / R^2), registration 4 'Matching'. Defined here for V >= 0, rho >= 0, R > 0.""" + """p = min(1, V rho^2 / R^2), the matching's chance fraction. Defined here for V >= 0, rho >= 0, R > 0.""" return min(1.0, V * rho * rho / (R * R)) From 36c09db01eee1e17615bc841200f25bfef00ab16 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:54:30 +0000 Subject: [PATCH 09/17] docs: rewrite status, open items, parity and benchmarks for a public reader status.md is one screen without test counts, open-items.md one plain list (the venv note moves to CONTRIBUTING.md, the app-adoption note is added), parity.md and benchmarks.md drop references to material outside the repo, and a short public ROADMAP.md names the items parity.md cites. Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- CONTRIBUTING.md | 10 + ROADMAP.md | 21 ++ .../IndexFixtures.swift | 2 +- .../ScatteringTruthTests.swift | 2 +- Tests/Fixtures/README.md | 2 +- docs/benchmarks.md | 27 +- docs/open-items.md | 19 +- docs/orientation-symmetry.md | 5 +- docs/parity.md | 236 +++++++++--------- docs/status.md | 28 ++- 10 files changed, 195 insertions(+), 157 deletions(-) create mode 100644 ROADMAP.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bea3aa9..f5211ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,16 @@ A test's tolerance is the one the Python package's own tests use for that functi that moves a fixture says why in the pull request and in the CHANGELOG; regenerate a fixture with its script and the pinned versions of the reference. +## Regenerating fixtures + +The generators run in pinned Python virtual environments outside the repository (Python 3.12, NumPy 2.5.4, SciPy 1.18.1; +pymatgen, spglib and orix in one, py4DSTEM in the other), placed at `../References/python/orix` and +`../References/python/py4dstem` beside the checkout; each script's header names the one it needs. The current `orix` venv +holds pymatgen-core 2026.10.16 under a `pymatgen` 2026.9.24 metapackage; the README and `docs/parity.md` say 2026.9.24, and the +older fixtures record pymatgen-core 2026.9.23 (a previous venv). `cif-symmetry-pins.json` was regenerated on the current +install on 2026-10-21 and came out byte-identical; the other fixtures were not regenerated. Regenerate and re-pin them +together at the next fixture change. + ## Pull requests - Keep a change focused, with a test that fails before the change and passes after it. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..d6fbd06 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,21 @@ +# Roadmap + +Where DiffractoKit is going next, in order. `docs/parity.md` says what is held today, `docs/open-items.md` what is known to be +open, and the GitHub issues track each piece of work. The numbers below are the ones `docs/parity.md` cites as "ROADMAP item n". + +1. **Diagnose before fixing.** Phase matching (`minimumVectors` = 2 makes "matrix" by exclusion; the cross-phase winner + ignores completeness), the ACOM mirror (a mirrored win carries `inPlaneAngle` + π with no flag; some off-grid + self-recoveries land 36 or 60 degrees away) and CIF symmetry (a group named by Hermann–Mauguin symbol alone, a partial + operation list). Each gets its cause stated first; what the diagnosis supports is fixed, the rest stays an open item. +2. **Space-group facts, public.** Space group symbol, number and Hall symbol through spglib's definitions (no Wyckoff + positions first); a public `ReflectionFamily` initialiser; `RingIndexing`'s "accepted" documented as a likelihood ratio, + not family membership, or changed. +3. **orix parity beyond the IPF key.** `Rotation` and `Orientation` types, misorientation, the orientation fundamental + zone and its sampling, pole-figure points (stereographic projection), and the `.native` IPF key for every Laue class. +4. **Kinematic diffraction patterns.** A diffsims-like pattern API (the spots of a beam direction in one call) with + Debye–Waller factors, and a streamed q-calibration reflection list (today it peaks near 800 MB for a 30 Å cell at + k_max 2.5 Å⁻¹). +5. **py4DSTEM ACOM extras, only if needed.** `match_single_pattern` refinement and `num_matches_return` > 1; until then the + parity table says the scores are not comparable. + +Not planned: CIF export, grain clustering, `.ang` export, database CIF sources (COD, ICSD), plotting. diff --git a/Tests/DiffractoKitTestSupport/IndexFixtures.swift b/Tests/DiffractoKitTestSupport/IndexFixtures.swift index 4c2396b..bb4f2a8 100644 --- a/Tests/DiffractoKitTestSupport/IndexFixtures.swift +++ b/Tests/DiffractoKitTestSupport/IndexFixtures.swift @@ -51,7 +51,7 @@ package enum IndexPresets { lines += ["loop_", "_atom_site_label", "_atom_site_type_symbol", "_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z"] lines += sites.map { " \($0.0) \($0.1) \($0.2) \($0.3) \($0.4)" } - // The CIF parse cannot fail on this fixed text; a failure is a slice-1 regression the CIF tests name. + // The CIF parse cannot fail on this fixed text; a failure is a CIF-parser regression the CIF tests name. return (try? CIFImport.read(lines.joined(separator: "\n")).structure)! } diff --git a/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift b/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift index 4aa3f6f..8c08050 100644 --- a/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift +++ b/Tests/DiffractoScatteringTests/ScatteringTruthTests.swift @@ -52,7 +52,7 @@ enum Presets { lines += ["loop_", "_atom_site_label", "_atom_site_type_symbol", "_atom_site_fract_x", "_atom_site_fract_y", "_atom_site_fract_z"] lines += sites.map { " \($0.0) \($0.1) \($0.2) \($0.3) \($0.4)" } - // The CIF parse cannot fail on this fixed text; a failure is a slice-1 regression the CIF tests name. + // The CIF parse cannot fail on this fixed text; a failure is a CIF-parser regression the CIF tests name. return (try? CIFImport.read(lines.joined(separator: "\n")).structure)! } } diff --git a/Tests/Fixtures/README.md b/Tests/Fixtures/README.md index f50f32b..2ca6cfd 100644 --- a/Tests/Fixtures/README.md +++ b/Tests/Fixtures/README.md @@ -3,7 +3,7 @@ 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. 55 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*. No fixture is a copy of a real dataset: the committed text fixtures carry no local path or personal name (checked by search), and real datasets are read from `$MACEM_DATA` and never copied into the repo. ## Third party diff --git a/docs/benchmarks.md b/docs/benchmarks.md index fdd5972..baf80ed 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -1,7 +1,6 @@ # DiffractoKit benchmarks against the Python counterparts -Measured 2026-10-21 on the first table of the kit audit (suite ROADMAP task 4, protocol step 2 and 3). Small and typical sizes -only; the **large** rows are the nightly review's (`DIFFRACTO_BENCH_LARGE=1`). Best of five after one warm-up: the median is +Measured 2026-10-21. Small and typical sizes only; the **large** rows run with `DIFFRACTO_BENCH_LARGE=1`. Best of five after one warm-up: the median is the figure, the spread (min-max) is in brackets. Same inputs on both sides (`tools/benchmarks/gen_inputs.py` writes them), Float64 unless the function is Float32 by design (ACOM, ellipse pattern pixels). @@ -10,11 +9,11 @@ Float64 unless the function is Float32 by design (ACOM, ellipse pattern pixels). | Machine | Apple M5 Pro, 64 GB, macOS 27.0.1 (Darwin 27.0.0) | | Toolchain | Apple Swift 6.4, `swift build -c release` (separate package `tools/benchmarks`) | | Python | 3.12.14, NumPy 2.5.4, SciPy 1.18.1; py4DSTEM 0.14.19 (`References/python/py4dstem`); pymatgen-core 2026.10.16 (the code; its `pymatgen` metapackage dist-info reads 2026.9.24) and orix 0.15.0 and spglib 2.8.0 (`References/python/orix`) | -| Load | **10.5 on a 12-core machine while five other kit sessions ran** (uptime 17:39): every figure carries that noise, the single-core rows least. Re-run when the machine is quiet before quoting a ratio. | +| Load | **10.5 on a 12-core machine while other builds ran** (uptime 17:39): every figure carries that noise, the single-core rows least. Re-run when the machine is quiet before quoting a ratio. | | Pin note | `References/python/orix` runs pymatgen-core **2026.10.16** under a `pymatgen` 2026.9.24 metapackage; the fixtures record `pymatgen` 2026.9.24 and, except `cif-symmetry-pins.json` (generated 2026-10-21 on this install, byte-identical on regeneration), `pymatgen-core` 2026.9.23. Nothing here compares numbers, only time | -| 0.9.1 | The `symmetry_detection` and `cif_import` rows above are the 2026-10-21 measurement of 0.9.0. 0.9.1 replaced the closure check that took 95 % of them (see `docs/open-items.md`); measured in-process on the same `al_super2.cif` (machine load 3.6): `cif_import` typical median 67 ms (min 67, max 77), against 297 ms (one run at load 9) before the change and the table's 351 ms. Not yet re-run through `run.sh`; the nightly's row replaces these | +| 0.9.1 | The `symmetry_detection` and `cif_import` rows above are the 2026-10-21 measurement of 0.9.0. 0.9.1 replaced the closure check that took 95 % of them (see `docs/open-items.md`); measured in-process on the same `al_super2.cif` (machine load 3.6): `cif_import` typical median 67 ms (min 67, max 77), against 297 ms (one run at load 9) before the change and the table's 351 ms. Not yet re-run through `run.sh` | -Re-run: `tools/benchmarks/gen_inputs.py` (References/python/orix Python), then `tools/benchmarks/run.sh` +Re-run: `tools/benchmarks/gen_inputs.py` (with the `orix` venv's Python, see `CONTRIBUTING.md`), then `tools/benchmarks/run.sh` for the kit and `tools/benchmarks/bench_python.py pymatgen|py4dstem` for Python, merged by `merge.py`. The raw lines are `tools/benchmarks/results/2026-10-21.jsonl`. @@ -24,31 +23,31 @@ for the kit and `tools/benchmarks/bench_python.py pymatgen|py4dstem` for Python, |---|---|---|---|---|---| | `niggli_reduce` | small | 100 | 0.0679 ms (0.064-0.0839) | 11.3 ms (9.96-11.7) [cpu]
0.293 ms (0.29-0.296) [spglib] | 0.006 | | `niggli_reduce` | typical | 1000 | 0.51 ms (0.508-0.512) | 163 ms (149-267) [cpu]
3.13 ms (2.88-3.5) [spglib] | 0.0031 | -| `niggli_reduce` | large | 10000 | nightly | nightly [cpu]
nightly [spglib] | - | +| `niggli_reduce` | large | 10000 | not run | not run [cpu]
not run [spglib] | - | | `reflections` | small | 10 | 0.0606 ms (0.0581-0.065) | 0.0261 ms (0.024-0.0353) | 2.3 | | `reflections` | typical | 20 | 0.753 ms (0.56-0.916) | 0.115 ms (0.0952-0.191) | 6.6 | -| `reflections` | large | 40 | nightly | nightly | - | +| `reflections` | large | 40 | not run | not run | - | | `kinematic_reflections` | small | 10 | 0.98 ms (0.849-1.14) | 4.45 ms (3.96-55.3) | 0.22 | | `kinematic_reflections` | typical | 20 | 7 ms (6.64-7.55) | 12.7 ms (11.4-34.7) | 0.55 | -| `kinematic_reflections` | large | 30 | nightly | nightly | - | +| `kinematic_reflections` | large | 30 | not run | not run | - | | `symmetry_detection` | small | 4 | 6.87 ms (6.45-7.47) | 5.38 ms (4.78-5.69) | 1.3 | | `symmetry_detection` | typical | 32 | 361 ms (350-438) | 13.1 ms (11.9-16.5) | 28 | -| `symmetry_detection` | large | 108 | nightly | nightly | - | +| `symmetry_detection` | large | 108 | not run | not run | - | | `cif_import` | small | 962 | 0.652 ms (0.632-0.713) | 1.28 ms (1.27-1.31) | 0.51 | | `cif_import` | typical | 2364 | 351 ms (329-506) | 2.52 ms (2.32-2.64) | 1.4e+02 | -| `cif_import` | large | 6483 | nightly | nightly | - | +| `cif_import` | large | 6483 | not run | not run | - | | `orientation_reduce` | small | 1000 | 1.88 ms (1.87-5.39) | 22.1 ms (17.8-26) | 0.085 | | `orientation_reduce` | typical | 10000 | 19.3 ms (17.8-29.4) | 48.2 ms (42.2-50.7) | 0.4 | -| `orientation_reduce` | large | 100000 | nightly | nightly | - | +| `orientation_reduce` | large | 100000 | not run | not run | - | | `ellipse_fit_1d` | small | 128 | 1.25 ms (1.2-1.41) | 1.34 ms (1.22-1.8) | 0.94 | | `ellipse_fit_1d` | typical | 256 | 5.86 ms (4.35-17.7) | 5.26 ms (4.87-5.62) | 1.1 | -| `ellipse_fit_1d` | large | 512 | nightly | nightly | - | +| `ellipse_fit_1d` | large | 512 | not run | not run | - | | `acom_library` | small | 100 | 7.81 ms (6.88-16.9) | 13.7 ms (12.4-40.2) | 0.57 | | `acom_library` | typical | 600 | 60.6 ms (49.4-80.7) | 62.5 ms (39-132) | 0.97 | -| `acom_library` | large | 2400 | nightly | nightly | - | +| `acom_library` | large | 2400 | not run | not run | - | | `acom_match_scan` | small | 64 | 1.85 ms (1.83-1.88) [cpu 1 worker]
0.245 ms (0.235-0.274) [cpu all cores]
0.294 ms (0.285-0.314) [metal] | 25.7 ms (14.4-37.9) | 0.072 / 0.0095 / 0.011 | | `acom_match_scan` | typical | 1024 | 1.94 ms (1.91-1.98) [cpu 1 worker]
0.216 ms (0.211-0.236) [cpu all cores]
0.236 ms (0.215-0.285) [metal] | 17.5 ms (10.4-21.5) | 0.11 / 0.012 / 0.014 | -| `acom_match_scan` | large | 16384 | nightly [cpu 1 worker]
nightly [cpu all cores]
nightly [metal] | - | - | +| `acom_match_scan` | large | 16384 | not run [cpu 1 worker]
not run [cpu all cores]
not run [metal] | - | - | ## What the rows compare, and what they do not diff --git a/docs/open-items.md b/docs/open-items.md index 6edab4a..547fde5 100644 --- a/docs/open-items.md +++ b/docs/open-items.md @@ -1,7 +1,5 @@ # Open items -- **0.10.0 left open:** a beam centre for a pattern whose beam is blocked (Friedel-pair symmetry; `PatternCentre.find` refuses it); `fitRing` at R 600 px on 2048² (tens of seconds; a coarse-then-fine annulus or Metal); a one-ring profile model that tolerates a neighbouring ring (the {111} bias of `RingFitNeighbourTailTests` is the model's, not a bug); Hall-symbol-only CIF groups and a partial operation list of the right count but the wrong operations; `Orientation.from_euler(symmetry:)`, `dot`, `dot_outer`, `get_distance_matrix` and the orientation (Rodrigues) fundamental zone; orix's `reduce()` disagreement with the smallest angle (reported in parity P36, not reported upstream yet). - A plain list, worst first. Each line: the item, the cause where it is known, what would close it. - **`cif_import` is 27 times slower than pymatgen's `CifParser` on a 32-atom P1 supercell (about 67 ms against 2.5 ms; 351 ms @@ -21,16 +19,19 @@ A plain list, worst first. Each line: the item, the cause where it is known, wha absence flag, the rings and their orbits. Close by timing the kit's own enumeration without the ring grouping if it matters. - **Phase matching.** Defaults unchanged. (a) `minimumMatrixEvidence` (new, off) keeps vacuum and stray-peak positions out of the matrix class; (b) `completenessAwareCrossPhaseRanking` stays off: it loses at small noise and wins near the pair radius - (`PhaseDiagnosisTests`). Both need one labelled scan to be switched on: the owner's study scan with the phases marked. + (`PhaseDiagnosisTests`). Both need one labelled scan to be switched on: a study scan with the phases marked. - **ACOM.** 7 of 200 off-grid self-recoveries land 36 or 60 degrees away (zone axes whose centrosymmetric pattern has a symmetry of its own; not separable on spots alone). "Zone axis misses up to 12.8 degrees beyond the bank sampling" (mac4DSTEM's note) is not reproduced here: within 2.9 degrees of a template the nearest-bank gap is at most 1.6 degrees at 600 axes; a pattern between templates in a sparse corner of the zone is untested. Metal is not faster than all CPU cores up to 1 024 patterns - at 600 axes; 16 384 patterns and 2 400 axes are the nightly's. + at 600 axes; 16 384 patterns and 2 400 axes are not timed yet. - **CIF symmetry.** Still unchecked: a group named by Hall symbol only, and a partial list whose count equals the primitive group's order but whose operations are the wrong ones (a count cannot see that). -- **Python pins.** `References/python/orix` and `py4dstem` hold pymatgen-core 2026.10.16 under a `pymatgen` 2026.9.24 metapackage - (dist-info of both); the README and `docs/parity.md` say 2026.9.24, the older fixtures record pymatgen-core 2026.9.23 (a - previous venv). `cif-symmetry-pins.json` records `pymatgen` 2026.9.24 (a literal in `pins.py`) and was generated on this - install, so on pymatgen-core 2026.10.16: regenerating it on 2026-10-21 gave a byte-identical file. The other fixtures were not - regenerated; re-generate and re-pin them together at the next fixture change. +- **Beam centre for a blocked beam.** `PatternCentre.find` refuses a pattern whose beam is blocked; Friedel-pair symmetry would + find it. +- **Ring fit.** `fitRing` at R 600 px on 2048² takes tens of seconds (a coarse-then-fine annulus, or Metal). A one-ring profile + model that tolerates a neighbouring ring is missing (the {111} bias of `RingFitNeighbourTailTests` is the model's, not a bug). +- **orix API gaps.** `Orientation.from_euler(symmetry:)`, `dot`, `dot_outer`, `get_distance_matrix` and the orientation + (Rodrigues) fundamental zone. orix's `reduce()` disagrees with the smallest angle (parity P36); not reported upstream yet. +- **Not yet used by a known consumer** (0.10.0): `PatternCentre.find`, `PatternRadialProfile.about`, `LinearScale.fit` and + `Misorientation`. Revisit at 0.12. diff --git a/docs/orientation-symmetry.md b/docs/orientation-symmetry.md index d49ae5f..ee2680e 100644 --- a/docs/orientation-symmetry.md +++ b/docs/orientation-symmetry.md @@ -163,9 +163,8 @@ From the worktree, with the existing pinned references (no installation or netwo swift test -c release --jobs 4 ``` -The orix pins (`tools/orix/ipf-pins.py`, `tools/orix/disorientation-pins.py`) use the orix venv, provisioned by -a pinned-environment setup script (orix 0.15.0, NumPy 2.5.4, SciPy 1.18.1, Python 3.12, freeze file in -`References/python/orix/freeze.txt`); the direction-family pins use the py4DSTEM venv. `MACEM_SUITE_ROOT` can locate references from a differently placed checkout. `pins.py` invokes the pinned py4DSTEM +The orix pins (`tools/orix/ipf-pins.py`, `tools/orix/disorientation-pins.py`) use the orix venv (orix 0.15.0, NumPy 2.5.4, SciPy 1.18.1, Python 3.12, freeze file in +`References/python/orix/freeze.txt`; see `CONTRIBUTING.md`); the direction-family pins use the py4DSTEM venv. `MACEM_SUITE_ROOT` can locate references from a differently placed checkout. `pins.py` invokes the pinned py4DSTEM interpreter and compiles only a `git show` extraction of the app's symmetry classes at c809679c into this worktree's `.build/symmetry-generator`. It does not build or modify the app. The JSON contains seed 20261016, package versions, source digests (including pymatgen symm_data.json), every proper group, random rotations, half-turn/boundary cases, original native Float colour bits and diff --git a/docs/parity.md b/docs/parity.md index 33b5c45..4c492ae 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -1,9 +1,8 @@ # Parity with pymatgen, py4DSTEM and pyxem / diffsims / orix A checklist of what DiffractoKit holds against the documented public API of the Python packages it is the Mac-native -counterpart of, so that "complete" is a measured claim (suite ROADMAP, "Kits", phase 4). Started 2026-10-15 for 0.1.0 -(`main` at 3a83d95); updated at every tag. The rows slice 1b holds were updated at bc99fa0 (ships at 0.1.1). It claims only what a test in this repository checks. A row is a Python -function or a small group of them, not every method of the class. +counterpart of, so that "complete" is a measured claim. It is updated at every tag and claims only what a test in this +repository checks. A row is a Python function or a small group of them, not every method of the class. ## What the three states mean @@ -11,25 +10,24 @@ function or a small group of them, not every method of the class. kit with that package's output (a committed pin generated by the pinned package) within the stated tolerance. - **partly**: the kit computes the same or an equivalent quantity but (a) only a subset of what the function does, or (b) the check is against an independent truth (a closed form, a 60-digit decimal, a NumPy evaluation of the written - definition, the suite apps' compiled code), not against this package's own output, or (c) there is no comparison at all + definition, the apps' compiled code), not against this package's own output, or (c) there is no comparison at all yet. The "How checked" column says which; read it before relying on a row. -- **not held**: no kit symbol. "planned" names the place where it is registered or intended; it is not a promise of a date. +- **not held**: no kit symbol. "planned" names the `ROADMAP.md` item where it is intended; it is not a promise of a date. -Numerics are held to the tolerance the Python package's own tests use; readers, I/O, indices and metadata are exact. Agreement with a package is agreement, not truth; where the kit and a package differ by design the difference -is named. Counts are of rows. +Numerics are held to the tolerance the Python package's own tests use for the function, never stricter; where upstream +states none, `assert_allclose` defaults for the dtype (rtol 1e-7 for float64). Readers, I/O, indices and metadata are +exact. Agreement with a package is agreement, not truth; where the kit and a package differ by design the difference is +named. Counts are of rows. +Columns: -The tables in sections 1 to 3 were rewritten on 2026-10-21 (kit audit, step 1) by walking the public API of each package from the -source on disk (`refs/`), one row per function or option group. Kit symbols were checked against `Sources/` at the audited commit -(uncommitted work in the tree is not counted). Columns: - -- **Plan**: `study` = on the owner's path (the TEM study and the two apps' open kit asks: phase matching and maps, ACOM, ring and - ellipse calibration, CIF import and symmetry, structure factors and kinematic patterns, zone axes, IPF and orientation - symmetry, and the DiffractoKit items of the suite ROADMAP, section 4); `useful`; or `not planned (reason)`. +- **Plan**: `study` = on the path of the TEM study the suite reproduces and of the two apps' open asks (phase matching and + maps, ACOM, ring and ellipse calibration, CIF import and symmetry, structure factors and kinematic patterns, zone axes, + IPF and orientation symmetry, and the items of `ROADMAP.md`); `useful`; or `not planned (reason)`. - **Upstream tolerance (test file)**: orix rows quote the tolerance the orix test of that function uses, with the file under `orix/tests/` (`np.allclose` defaults are rtol 1e-5, atol 1e-8). pymatgen and py4DSTEM ship no tests: the cell says so and - names the tolerance arguments of the function itself where there are any. D17: `assert_allclose` defaults (rtol 1e-7). -- **[S]** (none left since 0.9.1): the kit's pinned tolerance was stricter than upstream's (D17: never stricter). The 33 rows that carried it + names the tolerance arguments of the function itself where there are any, else `assert_allclose` defaults (rtol 1e-7). +- **[S]** (none left since 0.9.1): the kit's pinned tolerance was stricter than upstream's. The 33 rows that carried it were loosened to the upstream tolerance in 0.9.1; the tolerance columns and "How checked" cells state the new pins, and the measured values stay in brackets. **[L]**: looser, with the reason (upstream computes in float32). @@ -53,23 +51,23 @@ and are counted apart. | useful | 24 | 0 | 8 | 16 | | not planned | 30 | 0 | 0 | 30 | -"Held" is the only state that means a test compares the kit with that package's own output; every "partly" row names its check. The plan table counts the same rows by plan; its `study` line is the owner's path. +"Held" is the only state that means a test compares the kit with that package's own output; every "partly" row names its check. The plan table counts the same rows by plan; its `study` line is the TEM study's path. ## The pins and references behind the rows | Id | What | Generator and fixture | Test | Tolerance | |---|---|---|---|---| -| P1 | pymatgen `Lattice` (`vesta=True`) for the 7 crystal systems: matrix, metric, volume, reciprocal rows, d and \|g\| for every hkl in -3...3 but 000 | `tools/slice-1/pins.py` -> `Tests/DiffractoCoreTests/Fixtures/slice-1-pins.json` | `PinsTests.testCellsAgreeWithPymatgen` | rtol 1e-7 (D17; was 1e-12) with 1e-12 of scale for exact zeros in the matrix, metric and reciprocal; volume, d, \|g\| rtol 1e-7 | -| P2 | pymatgen `CifParser` + `SpacegroupAnalyzer` (symprec 1e-4) on 19 structures (the slice-1 CIF fixtures and beta''): cell, expanded sites, point group (as Laue class), every symmetry operation, reflections inside k_max 1.2 with the absence flag, Laue rotations, orbits and rings | same file | `PinsTests.testEveryStructureAgreesWithPymatgen`, `testBetaDoublePrimeCIFTextMatchesTheGenerator` | cell exact; sites 1e-7; operation translations within `symprec` 0.01 angstrom (a fraction of the longest cell edge); \|g\|, d rtol 1e-7; counts, order, flags, names exact | +| P1 | pymatgen `Lattice` (`vesta=True`) for the 7 crystal systems: matrix, metric, volume, reciprocal rows, d and \|g\| for every hkl in -3...3 but 000 | `tools/slice-1/pins.py` -> `Tests/DiffractoCoreTests/Fixtures/slice-1-pins.json` | `PinsTests.testCellsAgreeWithPymatgen` | rtol 1e-7 (was 1e-12) with 1e-12 of scale for exact zeros in the matrix, metric and reciprocal; volume, d, \|g\| rtol 1e-7 | +| P2 | pymatgen `CifParser` + `SpacegroupAnalyzer` (symprec 1e-4) on 19 structures (the CIF fixtures and beta''): cell, expanded sites, point group (as Laue class), every symmetry operation, reflections inside k_max 1.2 with the absence flag, Laue rotations, orbits and rings | same file | `PinsTests.testEveryStructureAgreesWithPymatgen`, `testBetaDoublePrimeCIFTextMatchesTheGenerator` | cell exact; sites 1e-7; operation translations within `symprec` 0.01 angstrom (a fraction of the longest cell edge); \|g\|, d rtol 1e-7; counts, order, flags, names exact | | P3 | pymatgen/spglib Laue class and operation count of tetragonal cells in three settings (rutile, anatase, beta-Sn, two controls) | `tools/kit-0.0.4/tetragonal-pins.py` -> `Fixtures/tetragonal-pins.json` | `TetragonalSettingTests` | counts exact; ring \|g\| 1e-9 | | P4 | NumPy evaluation of F(hkl) written from the definition, 20 structures, k_max 1.2: Re F, Im F, \|F\|, kept set, rings, sum \|F\|^2 | `tools/slice-2/pins.py` -> `Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json` | `ScatteringPinsTests.testStructureMatchesNumPyPins` | 1e-7 relative of max(\|F\|, 1e-4); \|g\| 1e-7; sum \|F\|^2 1e-7 | | P5 | py4DSTEM f050d207 `scattering_factors.txt` (sha256 be845d1d...): 103 x 10 table; f_e at g^2 = 0, 0.25, 1, 4 for Z = 1...103 | `tools/slice-2/fetch-table.sh`, `pins.py` | `ScatteringPinsTests.testTableBitForBitAndSamples` | table bit for bit; f_e 1e-7 relative (was 1e-15) | | P6 | pymatgen `XRDCalculator` peaks (6 structures): sum of \|F_X\|^2 over each peak's members | `tools/slice-2/pins.py` (same file as P4) | `ScatteringPinsTests.testXRayPhaseSumsMatchPymatgen` | 1e-7 relative (`assert_allclose` default; `SCALED_INTENSITY_TOL` is pymatgen's peak-drop threshold, not a comparison tolerance; was 1e-6) | | P7 | the suite apps' compiled `Crystal.reflections` and scattering table (mac4DSTEM 7d6fdbd1, `git show`, swiftc -O) | `tools/slice-2/apps-parity` -> `slice-2-apps.json` | `ScatteringPinsTests.testAppsBitForBit`, `testAppsBetaDoublePrimeFromTheAppsSiteListIsBitIdentical` | Double bit patterns (beta'' built from a CIF: 1e-12 to 2e-12 relative, summation order) | -| P8 | 60-digit decimal and NumPy truth for lambda(V), 2 theta, L, delta-q, camera length, q(R) and R(q): 320 + 100 + 120 cases | `tools/slice-3/ring-pins.py`, `tools/kit-0.0.4/q-pins.py` -> `slice-3-ring-pins.json`, `q-pins.json` | `RingCalibrationPinsTests`, `RingCalibrationQTests` | 1e-7 relative (D17; was 1e-12) | +| P8 | 60-digit decimal and NumPy truth for lambda(V), 2 theta, L, delta-q, camera length, q(R) and R(q): 320 + 100 + 120 cases | `tools/slice-3/ring-pins.py`, `tools/kit-0.0.4/q-pins.py` -> `slice-3-ring-pins.json`, `q-pins.json` | `RingCalibrationPinsTests`, `RingCalibrationQTests` | 1e-7 relative (was 1e-12) | | P9 | NumPy reading of the registered indexing rule on 7 ring lists, the T1 counts and the T4 refusal plants | `tools/slice-3/index-pins.py` -> `slice-3-index-pins.json` | `RingIndexingPinsTests` | decision and counts exact | -| P10 | the apps' compiled ellipse fit (mac4DSTEM 7d6fdbd1, -O and -Onone) on 35 planted rings x 6 calls | `tools/slice-3b/apps-parity` -> `slice-3b-apps.json`, `slice-3b-apps-onone.json` | `EllipseAppsParityTests` | every output bit (inputs without impulses and with dynamic range of at least 1; see the 0.0.4 design note, archived in the suite's private folder) | +| P10 | the apps' compiled ellipse fit (mac4DSTEM 7d6fdbd1, -O and -Onone) on 35 planted rings x 6 calls | `tools/slice-3b/apps-parity` -> `slice-3b-apps.json`, `slice-3b-apps-onone.json` | `EllipseAppsParityTests` | every output bit (inputs without impulses and with dynamic range of at least 1) | | P11 | SciPy 1.18.1 `least_squares` optimum of `fit1D`'s objective, 20 clean rings | `tools/slice-3b/sweep.py` -> `slice-3b-sweep.json` | `EllipseCalibrationTests.testCleanSweepEqualsSciPysOptimum` | centre 1e-5 px, axes 1e-7 relative, theta 1e-6 rad (measured 7.1e-8, 1.2e-9, 5.0e-8) | | P12 | spglib 2.8.0 `niggli_reduce` and pymatgen `Lattice.get_niggli_reduced_lattice` (two independent implementations; tol 1e-5): reduced cell parameters of 2827 cells (27 structures: slice 1's fixtures plus the non-conventional cells, and 200 random cells of each of the 14 Bravais lattices under a random unimodular transformation, seed 20261015) | `tools/slice-1b/pins.py` -> `Tests/DiffractoCoreTests/Fixtures/slice-1b-pins.json` (pymatgen 2026.9.24, pymatgen-core 2026.9.23, spglib 2.8.0, numpy 2.5.4) | `ReductionTests.testNiggliReducedCellAgreesWithSpglibAndPymatgen`; the Niggli conditions and P unimodular (truth, not the oracle): `testReducedBasisSatisfiesTheNiggliConditionsAndPIsUnimodular` | 1e-5 (the functions' own `tol`; was 1e-10; relative on lengths, degrees on angles); measured worst 3.5e-11, the two oracles agree to 2.8e-13 | | P13 | spglib 2.8.0 `get_symmetry_dataset` and `standardize_cell` (symprec 1e-4) on the same file: the Bravais lattice name (spglib's lattice-only dataset, 2800 random cells), the conventional cell's volume, metric and parameters, operation counts and Laue class in the file's and the conventional setting (27 structures), and the rings after slice 1's definitions on the standardised cell (4332 rings, k_max 1.2) | same generator and file as P12 | `ConventionalCellTests.testBravaisNameOfRandomCellsAgreesWithConstructionAndSpglib`, `testConventionalCellAgreesWithSpglibAndTheTransformationReproducesIt`; `ConventionalSymmetryTests.testOperationsLaueClassAndBravaisInBothSettingsAgreeWithSpglib`, `testRingsInBothSettingsAgreeWithSpglibsStandardisedCell`, `testIndicesAndOperationsTransformBothWays` | names, counts, Laue class exact; volume 1e-7; metric 1e-7; ring g 1e-7 (was 1e-10, 1e-9, 1e-8; ring names only where the conventional cell has spglib's parameters: 1294 compared, 872 skipped) | @@ -106,62 +104,62 @@ as a pinned f050d207 environment the pins run (P14, P18). The remaining unexecut | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Lattice.from_parameters(a, b, c, alpha, beta, gamma, vesta=True)` | `UnitCell(a:b:c:alphaDeg:betaDeg:gammaDeg:)` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1, P2 (1e-7 relative, D17). The kit's orientation is `vesta=True` (a along x, b in the xy plane). pymatgen's default (`vesta=False`) orients the cell differently: Cartesian components differ by a rotation, every invariant is equal | -| `Lattice.cubic`, `tetragonal`, `orthorhombic`, `monoclinic`, `hexagonal`, `rhombohedral` | `UnitCell.cubic(a:)`, `tetragonal(a:c:)`, `orthorhombic`, `monoclinic(a:b:c:betaDeg:)` (unique axis b), `hexagonal(a:c:)`, `rhombohedral(a:alphaDeg:)`, `triclinic` | **partly** | useful | tests not shipped; assert_allclose defaults (D17) | the constructors are thin wrappers of the initialiser P1 pins; `UnitCellTests` checks them against closed forms, not against pymatgen | -| `matrix` | `UnitCell.realLattice` (rows) | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1, P2 (`vesta=True` matrix, rtol 1e-7 with 1e-12 of scale for exact zeros) | -| `a`, `b`, `c`, `abc`, `lengths`, `alpha`, `beta`, `gamma`, `angles`, `parameters` | `UnitCell.a ... gammaDeg` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P2 (cell parameters exact) | -| `volume` | `UnitCell.volume` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1, P2 (rtol 1e-7) | -| `metric_tensor` | `UnitCell.metricTensor` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1 (rtol 1e-7, 1e-12 of scale^2 for exact zeros) | -| `reciprocal_lattice_crystallographic` | `UnitCell.reciprocalLattice` (rows, no 2 pi) | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1, P2 (rtol 1e-7, 1e-12 of scale for exact zeros) | -| `reciprocal_lattice` (with 2 pi) | none; the kit has no 2 pi by design (`g = h b1 + k b2 + l b3`, d = 1/\|g\|) | not held | not planned (by design, stated in `UnitCell` and `NOTICE`) | tests not shipped; assert_allclose defaults (D17) | by design | -| `d_hkl(miller_index)` | `UnitCell.d(_:)`, `gLength(_:)`, `gSquaredFromMetric(_:)` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P1 (7 cells x 342 hkl), P2 (every reflection of 19 structures), rtol 1e-7 | -| `get_points_in_sphere` (reciprocal lattice, centre at the origin) | `CrystalStructure.reflections(kMax:)` -> `ReflectionList` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P2: the set, the order and \|g\| equal pymatgen's at k_max 1.2. Only the reciprocal sphere about the origin; not the general method (arbitrary centre, images, real space) | -| `get_miller_index_from_coords` | none (`ZoneAxis.cartesian(in:)` goes the other way) | not held | useful | tests not shipped; assert_allclose defaults (D17) | | +| `Lattice.from_parameters(a, b, c, alpha, beta, gamma, vesta=True)` | `UnitCell(a:b:c:alphaDeg:betaDeg:gammaDeg:)` | **held** | study | tests not shipped; assert_allclose defaults | P1, P2 (1e-7 relative, D17). The kit's orientation is `vesta=True` (a along x, b in the xy plane). pymatgen's default (`vesta=False`) orients the cell differently: Cartesian components differ by a rotation, every invariant is equal | +| `Lattice.cubic`, `tetragonal`, `orthorhombic`, `monoclinic`, `hexagonal`, `rhombohedral` | `UnitCell.cubic(a:)`, `tetragonal(a:c:)`, `orthorhombic`, `monoclinic(a:b:c:betaDeg:)` (unique axis b), `hexagonal(a:c:)`, `rhombohedral(a:alphaDeg:)`, `triclinic` | **partly** | useful | tests not shipped; assert_allclose defaults | the constructors are thin wrappers of the initialiser P1 pins; `UnitCellTests` checks them against closed forms, not against pymatgen | +| `matrix` | `UnitCell.realLattice` (rows) | **held** | study | tests not shipped; assert_allclose defaults | P1, P2 (`vesta=True` matrix, rtol 1e-7 with 1e-12 of scale for exact zeros) | +| `a`, `b`, `c`, `abc`, `lengths`, `alpha`, `beta`, `gamma`, `angles`, `parameters` | `UnitCell.a ... gammaDeg` | **held** | study | tests not shipped; assert_allclose defaults | P2 (cell parameters exact) | +| `volume` | `UnitCell.volume` | **held** | study | tests not shipped; assert_allclose defaults | P1, P2 (rtol 1e-7) | +| `metric_tensor` | `UnitCell.metricTensor` | **held** | study | tests not shipped; assert_allclose defaults | P1 (rtol 1e-7, 1e-12 of scale^2 for exact zeros) | +| `reciprocal_lattice_crystallographic` | `UnitCell.reciprocalLattice` (rows, no 2 pi) | **held** | study | tests not shipped; assert_allclose defaults | P1, P2 (rtol 1e-7, 1e-12 of scale for exact zeros) | +| `reciprocal_lattice` (with 2 pi) | none; the kit has no 2 pi by design (`g = h b1 + k b2 + l b3`, d = 1/\|g\|) | not held | not planned (by design, stated in `UnitCell` and `NOTICE`) | tests not shipped; assert_allclose defaults | by design | +| `d_hkl(miller_index)` | `UnitCell.d(_:)`, `gLength(_:)`, `gSquaredFromMetric(_:)` | **held** | study | tests not shipped; assert_allclose defaults | P1 (7 cells x 342 hkl), P2 (every reflection of 19 structures), rtol 1e-7 | +| `get_points_in_sphere` (reciprocal lattice, centre at the origin) | `CrystalStructure.reflections(kMax:)` -> `ReflectionList` | **partly** | study | tests not shipped; assert_allclose defaults | P2: the set, the order and \|g\| equal pymatgen's at k_max 1.2. Only the reciprocal sphere about the origin; not the general method (arbitrary centre, images, real space) | +| `get_miller_index_from_coords` | none (`ZoneAxis.cartesian(in:)` goes the other way) | not held | useful | tests not shipped; assert_allclose defaults | | | `get_niggli_reduced_lattice(tol=1e-5)` (and spglib `niggli_reduce(eps)`) | `UnitCell.niggliReduced(relativeEpsilon:)` -> `ReducedCell` (reduced `cell`, the integer change of basis `transformation` P with determinant +1, `epsilon`, `converged`, `steps`) | **held** | study | tests not shipped; function argument `tol=1e-5` (the kit's `relativeEpsilon` default is the same 1e-5) | P12 against both oracles on 2827 cells, 1e-5 (the functions' own `tol`; worst 3.5e-11), and the Niggli conditions and P unimodular checked directly (`ReductionTests`). Differences by design: the kit also returns P; its epsilon is relative to V^(2/3), the value 1e-5 of both packages; an invalid cell comes back unconverged with the identity instead of raising. Cells within 1e-6 of a boundary between two reduced forms stop at one of them (`testReductionStopsAtBoundariesAndIsIdempotent`, the kit's own check) | -| `selling_vector`, `selling_dist` | none | not held | not planned (no consumer) | tests not shipped; assert_allclose defaults (D17) | not registered | -| `get_lll_reduced_lattice`, `lll_matrix`, `lll_mapping`, `lll_inverse`, `get_lll_frac_coords` | none | not held | not planned (Niggli is the reduction the kit uses) | tests not shipped; assert_allclose defaults (D17) | not registered | -| `find_mapping`, `find_all_mappings` | none | not held | useful | tests not shipped; assert_allclose defaults (D17) | not registered; would serve orientation-relationship and cell matching between phases | -| `get_cartesian_coords`, `get_fractional_coords`, `get_vector_along_lattice_directions` | none public (`realLattice`, `reciprocalLattice` give the matrices) | not held | useful | tests not shipped; assert_allclose defaults (D17) | | -| `dot`, `norm`, `get_all_distances`, `get_distance_and_image` | none public (the minimum-image distance behind `CrystalStructure.shortestCloseContact` is internal) | not held | useful | tests not shipped; assert_allclose defaults (D17) | | +| `selling_vector`, `selling_dist` | none | not held | not planned (no consumer) | tests not shipped; assert_allclose defaults | not planned yet | +| `get_lll_reduced_lattice`, `lll_matrix`, `lll_mapping`, `lll_inverse`, `get_lll_frac_coords` | none | not held | not planned (Niggli is the reduction the kit uses) | tests not shipped; assert_allclose defaults | not planned yet | +| `find_mapping`, `find_all_mappings` | none | not held | useful | tests not shipped; assert_allclose defaults | not planned yet; would serve orientation-relationship and cell matching between phases | +| `get_cartesian_coords`, `get_fractional_coords`, `get_vector_along_lattice_directions` | none public (`realLattice`, `reciprocalLattice` give the matrices) | not held | useful | tests not shipped; assert_allclose defaults | | +| `dot`, `norm`, `get_all_distances`, `get_distance_and_image` | none public (the minimum-image distance behind `CrystalStructure.shortestCloseContact` is internal) | not held | useful | tests not shipped; assert_allclose defaults | | | `is_hexagonal(hex_angle_tol=5, hex_length_tol=0.01)` | `UnitCell.metricFamily(lengthTolerance:angleToleranceDeg:)` | not held | not planned (`metricFamily` decides the family with the kit's own tolerances) | tests not shipped; function arguments `hex_angle_tol=5` degrees, `hex_length_tol=0.01` | a related decision with different tolerances (kit 1e-9 relative / 1e-9 degrees); never compared with pymatgen | -| `get_recp_symmetry_operation`, `get_wigner_seitz_cell`, `get_brillouin_zone`, `scale`, `pbc`, `is_orthogonal`, `is_3d_periodic`, `inv_matrix`, `copy`, `params_dict`, `as_dict`, `from_dict` | none | not held | not planned (k-space cells and serialisation are not a kit's job) | tests not shipped; assert_allclose defaults (D17) | | +| `get_recp_symmetry_operation`, `get_wigner_seitz_cell`, `get_brillouin_zone`, `scale`, `pbc`, `is_orthogonal`, `is_3d_periodic`, `inv_matrix`, `copy`, `params_dict`, `as_dict`, `from_dict` | none | not held | not planned (k-space cells and serialisation are not a kit's job) | tests not shipped; assert_allclose defaults | | ### 1.2 `pymatgen.core.Structure` (what diffraction uses) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Structure.lattice` | `CrystalStructure.cell` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P2 | -| `Structure.sites` (species, fractional coordinates, occupancy) | `CrystalStructure.sites` (`AtomSite`: Z, fractional, occupancy, label) | **held** | study | tests not shipped; assert_allclose defaults (D17) | P2: the expanded sites as a multiset, coordinates 1e-7 (pymatgen's CIF `frac_tolerance` snapping of 1/3 and 2/3 is not applied: the kit keeps the coordinates as written). One atomic number per site: oxidation states, magnetic moments, site properties and mixed-species sites are not represented | -| `Structure.volume` | `CrystalStructure.cell.volume` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P2 | -| `Structure.composition` / `formula` | `CrystalStructure.formula` ("Mg10 Si12", cell content) | **partly** | useful | tests not shipped; assert_allclose defaults (D17) | `CrystalStructure` tests; not compared with pymatgen's string | -| site properties for scattering: isotropic Debye-Waller (B or U) per species or site (`TEMCalculator(debye_waller_factors=...)`) | none (`AtomSite` has no displacement parameter) | not held | study | tests not shipped; assert_allclose defaults (D17) | roadmap item 4: the diffsims-like kinematic pattern API with Debye-Waller | -| `Structure.get_primitive_structure`, `get_reduced_structure("niggli")` | none for a structure (`UnitCell.niggliReduced` reduces the cell only; `ConventionalSetting` goes from any cell to the conventional one) | not held | useful | tests not shipped; assert_allclose defaults (D17) | not registered | -| `Structure.density`, `get_neighbors`, `get_all_neighbors`, `get_distance`, `make_supercell`, `get_sorted_structure`, `interpolate`, `get_symmetric_neighbor_list` and the rest | none | not held | not planned (not diffraction) | tests not shipped; assert_allclose defaults (D17) | | +| `Structure.lattice` | `CrystalStructure.cell` | **held** | study | tests not shipped; assert_allclose defaults | P2 | +| `Structure.sites` (species, fractional coordinates, occupancy) | `CrystalStructure.sites` (`AtomSite`: Z, fractional, occupancy, label) | **held** | study | tests not shipped; assert_allclose defaults | P2: the expanded sites as a multiset, coordinates 1e-7 (pymatgen's CIF `frac_tolerance` snapping of 1/3 and 2/3 is not applied: the kit keeps the coordinates as written). One atomic number per site: oxidation states, magnetic moments, site properties and mixed-species sites are not represented | +| `Structure.volume` | `CrystalStructure.cell.volume` | **held** | study | tests not shipped; assert_allclose defaults | P2 | +| `Structure.composition` / `formula` | `CrystalStructure.formula` ("Mg10 Si12", cell content) | **partly** | useful | tests not shipped; assert_allclose defaults | `CrystalStructure` tests; not compared with pymatgen's string | +| site properties for scattering: isotropic Debye-Waller (B or U) per species or site (`TEMCalculator(debye_waller_factors=...)`) | none (`AtomSite` has no displacement parameter) | not held | study | tests not shipped; assert_allclose defaults | ROADMAP item 4: the diffsims-like kinematic pattern API with Debye-Waller | +| `Structure.get_primitive_structure`, `get_reduced_structure("niggli")` | none for a structure (`UnitCell.niggliReduced` reduces the cell only; `ConventionalSetting` goes from any cell to the conventional one) | not held | useful | tests not shipped; assert_allclose defaults | not planned yet | +| `Structure.density`, `get_neighbors`, `get_all_neighbors`, `get_distance`, `make_supercell`, `get_sorted_structure`, `interpolate`, `get_symmetric_neighbor_list` and the rest | none | not held | not planned (not diffraction) | tests not shipped; assert_allclose defaults | | ### 1.3 `pymatgen.symmetry.analyzer.SpacegroupAnalyzer` | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `get_symmetry_operations(cartesian=False)` | `CrystalStructure.operations` (found from the atoms by `SymmetryDetection.detect`; a centred cell's centring translations are operations with the identity rotation) | **held** | study | tests not shipped; function arguments `symprec=0.01` angstrom, `angle_tolerance=5` degrees | P2 on 19 structures: same count, every pymatgen operation found (translation within `symprec` 0.01 angstrom, as a fraction of the longest cell edge); P3 on tetragonal settings; 29 mutants of the detector red (mutation report archived in the suite's private folder). The kit's tolerance is a fixed 5e-3 fractional position, not spglib's `symprec` in angstrom (row below) | +| `get_symmetry_operations(cartesian=False)` | `CrystalStructure.operations` (found from the atoms by `SymmetryDetection.detect`; a centred cell's centring translations are operations with the identity rotation) | **held** | study | tests not shipped; function arguments `symprec=0.01` angstrom, `angle_tolerance=5` degrees | P2 on 19 structures: same count, every pymatgen operation found (translation within `symprec` 0.01 angstrom, as a fraction of the longest cell edge); P3 on tetragonal settings; 29 mutations of the detector red. The kit's tolerance is a fixed 5e-3 fractional position, not spglib's `symprec` in angstrom (row below) | | `SpacegroupAnalyzer(structure, symprec, angle_tolerance)` (symprec in angstrom, the minimum-image distance); `get_symmetry_operations(cartesian=True)` | `SymmetryDetection.Tolerance.cartesian(_:)` (`.proposedCartesian` = 1e-3 A), the default `.fractional(5e-3)`; `SymmetryDetection.Search` | **partly** | study | tests not shipped; function arguments `symprec=0.01` angstrom, `angle_tolerance=5` degrees | `FastDetectionTests` checks the Cartesian sense on constructed pseudo-symmetries (0.013 A in a 50 A cell is symmetry at the fractional default, not at 1e-3 A) and that the cell-list search equals the brute-force scan bit for bit on 69 fixtures; no comparison with pymatgen at a given symprec, and the default stays fractional until one is added. `angle_tolerance` has no counterpart (the metric family has its own tolerances); Cartesian operation matrices are not returned | -| `get_point_group_symbol` | `CrystalStructure.laueClass` (`LaueClass`, 11 Laue classes) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P2, P3: the detected Laue class equals the 32 -> 11 map (`LaueClass(pointGroupSymbol:)`) of pymatgen's symbol. The 32 point groups are not told apart (Friedel's law makes a kinematic pattern centrosymmetric) | -| `get_point_group_operations` | `CrystalStructure.laueGroup` (`PointSymmetry`, integer rotations {R} and {-R}) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P2 compares the Laue rotations with the definition in the slice 1 design note (archived in the suite's private folder) applied to pymatgen's operations, not with `get_point_group_operations` (which has no inversion for a non-centrosymmetric group) | -| `get_crystal_system` | `CrystalStructure.crystalSystem`, `LaueClass.crystalSystem` | **partly** | useful | tests not shipped; assert_allclose defaults (D17) | derived from the Laue class; trigonal/hexagonal checked on quartz, calcite, fluorapatite, CdI2, WS2 through P2's Laue class; the call itself is not compared | -| `get_space_group_number` | `CrystalStructure.spaceGroupNumber` (the number the CIF states; nil = not recorded) | not held | study | tests not shipped; assert_allclose defaults (D17) | the kit does not determine the space group; the pinned spglib number is not used (roadmap item 2: spglib, public) | -| `get_space_group_symbol`, `get_hall` | none | not held | study | tests not shipped; assert_allclose defaults (D17) | the symbol and the Hall symbol are not determined (roadmap item 2) | +| `get_point_group_symbol` | `CrystalStructure.laueClass` (`LaueClass`, 11 Laue classes) | **partly** | study | tests not shipped; assert_allclose defaults | P2, P3: the detected Laue class equals the 32 -> 11 map (`LaueClass(pointGroupSymbol:)`) of pymatgen's symbol. The 32 point groups are not told apart (Friedel's law makes a kinematic pattern centrosymmetric) | +| `get_point_group_operations` | `CrystalStructure.laueGroup` (`PointSymmetry`, integer rotations {R} and {-R}) | **partly** | study | tests not shipped; assert_allclose defaults | P2 compares the Laue rotations with the definition written in `tools/slice-1/pins.py` applied to pymatgen's operations, not with `get_point_group_operations` (which has no inversion for a non-centrosymmetric group) | +| `get_crystal_system` | `CrystalStructure.crystalSystem`, `LaueClass.crystalSystem` | **partly** | useful | tests not shipped; assert_allclose defaults | derived from the Laue class; trigonal/hexagonal checked on quartz, calcite, fluorapatite, CdI2, WS2 through P2's Laue class; the call itself is not compared | +| `get_space_group_number` | `CrystalStructure.spaceGroupNumber` (the number the CIF states; nil = not recorded) | not held | study | tests not shipped; assert_allclose defaults | the kit does not determine the space group; the pinned spglib number is not used (ROADMAP item 2: spglib, public) | +| `get_space_group_symbol`, `get_hall` | none | not held | study | tests not shipped; assert_allclose defaults | the symbol and the Hall symbol are not determined (ROADMAP item 2) | | `get_symmetry_dataset` (spglib's `transformation_matrix`, `std_lattice`, `rotations`; the lattice type of a one-atom cell) | `CrystalStructure.conventionalSetting` -> `ConventionalSetting` (`conventionalCell` with `bravais` and `transformation`, `cell`, `operations`, `laueClass`; `UnitCell.conventionalCell(tolerance:)` for the lattice alone) | **partly** | study | tests not shipped; function arguments `symprec=0.01` angstrom, `angle_tolerance=5` degrees | P13: the Bravais name equals spglib's on 2800 random cells and 27 structures; the conventional cell's volume and metric equal spglib's standardised cell (1e-7); the operation count and Laue class equal spglib's in the file's and the conventional setting on 27 structures. The transformation is not compared entry for entry: the kit's setting rules (unique axis b with C centring and beta at least 90, hexagonal axes for R, the basis closest to the identity) can pick another basis of the same lattice, so `P^T G P` is checked against the conventional metric instead. `origin_shift`, `hall_number`, `international`, `std_positions`, `mapping_to_primitive` and `equivalent_atoms` are not held | -| `get_pearson_symbol`, `get_lattice_type`, `get_crystal_family`, `get_space_group_operations` | none; `ConventionalCell.bravais` is the Bravais lattice (`BravaisLattice`, 14 names) and `CrystalStructure.crystalSystem` the family, neither compared with these functions | not held | useful | tests not shipped; assert_allclose defaults (D17) | the Bravais name is held against spglib's lattice-only dataset (P13, row above); the Pearson symbol is not built from it | +| `get_pearson_symbol`, `get_lattice_type`, `get_crystal_family`, `get_space_group_operations` | none; `ConventionalCell.bravais` is the Bravais lattice (`BravaisLattice`, 14 names) and `CrystalStructure.crystalSystem` the family, neither compared with these functions | not held | useful | tests not shipped; assert_allclose defaults | the Bravais name is held against spglib's lattice-only dataset (P13, row above); the Pearson symbol is not built from it | | `get_conventional_standard_structure`, `get_conventional_to_primitive_transformation_matrix` | `ConventionalSetting.structure` (`CrystalStructure`, `setting == .conventional`), `ConventionalCell.transformation`, `centringTranslations`; `CellStandardisation.standardise` (a primitive cell given with a known centring to the conventional cell) | **partly** | study | tests not shipped; function arguments `symprec=0.01`, `angle_tolerance=5`; P20 uses `symprec=1e-4` | P13 compares against spglib's `standardize_cell` (the dataset pymatgen calls): the cell's parameters and volume, the operations, the Laue class and the rings in the conventional setting (4332 rings to 1e-7 in g, ring names where the conventional cell has spglib's parameters). P20 compares `CellStandardisation` with pymatgen's own call on three Materials Project structures (Al Fm-3m, Si Fd-3m, Mg P6_3/mmc): parameters 1e-9 (app route) and 1e-6 (detected route), sites 1e-9 modulo one rigid origin shift; only the F conversion is pinned against pymatgen, the I matrix against a closed form, and the A, B, C and R matrices are not tested. pymatgen's own call adds its Setyawan and Curtarolo conventions on top of spglib's, which are not reproduced. `ConventionalRingsTests` (scattering) shows a primitive cell of Au, Si and beta'' gives the conventional cell's rings exactly. The kit's P maps the conventional basis into the INPUT basis | -| `get_primitive_standard_structure`, `find_primitive`, `get_refined_structure` | none | not held | useful | tests not shipped; assert_allclose defaults (D17) | not registered; the kit goes from any cell to the conventional cell only (the lattice points inside it are `centringTranslations`, with no test of a primitive result) | -| `get_symmetrized_structure` (`SymmetrizedStructure`: `equivalent_sites`, Wyckoff symbols) | none | not held | useful | tests not shipped; assert_allclose defaults (D17) | roadmap item 2 says no Wyckoff first | -| `get_ir_reciprocal_mesh`, `get_ir_reciprocal_mesh_map`, `get_kpoint_weights`, `is_laue`, `PointGroupAnalyzer` | none | not held | not planned (k-point meshes and molecules) | tests not shipped; assert_allclose defaults (D17) | out of scope | +| `get_primitive_standard_structure`, `find_primitive`, `get_refined_structure` | none | not held | useful | tests not shipped; assert_allclose defaults | not planned yet; the kit goes from any cell to the conventional cell only (the lattice points inside it are `centringTranslations`, with no test of a primitive result) | +| `get_symmetrized_structure` (`SymmetrizedStructure`: `equivalent_sites`, Wyckoff symbols) | none | not held | useful | tests not shipped; assert_allclose defaults | ROADMAP item 2 says no Wyckoff first | +| `get_ir_reciprocal_mesh`, `get_ir_reciprocal_mesh_map`, `get_kpoint_weights`, `is_laue`, `PointGroupAnalyzer` | none | not held | not planned (k-point meshes and molecules) | tests not shipped; assert_allclose defaults | out of scope | ### 1.4 `pymatgen.symmetry.groups` (`SpaceGroup`, `PointGroup`) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `SpaceGroup.from_int_number(n)` / `SpaceGroup(symbol)` -> `symmetry_ops` (all operations of a space group from its number or symbol), `sg_symbol_from_int_number`, `get_settings` | none (the kit expands only the operations a CIF lists) | not held | study | tests not shipped; `get_orbit` tol=1e-5 (argument) | needed to read a CIF that names a group and lists no operations (roadmap item 2, and the CIF-symmetry diagnosis) | -| order of `SpaceGroup.symmetry_ops` (number of general-position operations) | `CIFImport.spaceGroupOrder(_:)`, `requiredOperationCount(spaceGroup:alphaDeg:betaDeg:gammaDeg:)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | `CIFImportTests` checks Fm-3m 192, C2/m 8, P6_3/mmc 24, R-3m 36 (hand values), not all 230 groups against pymatgen or spglib | +| `SpaceGroup.from_int_number(n)` / `SpaceGroup(symbol)` -> `symmetry_ops` (all operations of a space group from its number or symbol), `sg_symbol_from_int_number`, `get_settings` | none (the kit expands only the operations a CIF lists) | not held | study | tests not shipped; `get_orbit` tol=1e-5 (argument) | needed to read a CIF that names a group and lists no operations (ROADMAP item 2, and the CIF-symmetry diagnosis) | +| order of `SpaceGroup.symmetry_ops` (number of general-position operations) | `CIFImport.spaceGroupOrder(_:)`, `requiredOperationCount(spaceGroup:alphaDeg:betaDeg:gammaDeg:)` | **partly** | study | tests not shipped; assert_allclose defaults | `CIFImportTests` checks Fm-3m 192, C2/m 8, P6_3/mmc 24, R-3m 36 (hand values), not all 230 groups against pymatgen or spglib | | `SpaceGroup.is_compatible(lattice, tol, angle_tol)`, `crystal_system`, `is_subgroup`, `is_supergroup` | none (`UnitCell.metricFamily` classifies a cell; it is not a group-versus-lattice test) | not held | useful | tests not shipped; function arguments `tol=1e-5`, `angle_tol=5` | | | `SpaceGroup.get_orbit(p, tol)`, `get_orbit_and_generators` | none (`CrystalStructure.detectingSymmetry` and `CIFImport` expand sites with the operations they hold) | not held | useful | tests not shipped; function argument `tol=1e-5` | | | `PointGroup(symbol).symmetry_ops`, `get_orbit`, `PointGroup.from_space_group(symbol)` for all 32 point groups | none; the kit holds the 11 Laue groups (`LaueClass`, `PointSymmetry.holohedry(of:)`, `CrystalOrientationSymmetry`) | not held | study | tests not shipped; `get_orbit` tol=1e-5 (argument) | the 21 non-centrosymmetric point groups are not told apart; their proper subgroups (for example T for -43m, where the Laue group gives O) are not available for orientation symmetry. Same gap as orix `get_point_group` (section 3.1) | @@ -170,37 +168,37 @@ as a pinned f050d207 environment the pins run (P14, P18). The remaining unexecut | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Structure.from_spacegroup`, `SpacegroupAnalyzer.get_symmetry_operations`, `get_point_group_symbol` on the preset crystals (fcc, bcc, diamond, hcp, beta'' C2/m, 2H-WS2) | `CrystalPresets`, `CrystalStructure.operations`, `laueClass` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P29 (independent expansions; volume, d, sites 1e-7; Laue class, operation count exact) | -| `symmetry.groups.PointGroup.symmetry_ops` (proper operations of the 11 Laue groups) | `CrystalOrientationSymmetry(laueClass:).operators` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P17; all eleven operation sets, counts and Cartesian transforms checked (operations 1e-7 absolute, closure 2e-14); standard settings documented in `docs/orientation-symmetry.md`. Minimum-angle representative checked independently with SciPy; no unknown-class fallback | -| `SpacegroupAnalyzer.get_point_group_operations()` of P-31m, P-31c, P-3m1, P-3c1, R-3m, R-3c (hexagonal axes) | `CrystalOrientationSymmetry(laueClass: .threeBarM, setting:)`, `PhaseModel.orientationSymmetry(of:)` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P32; both -3m settings equal the proper part of pymatgen's operations (operators 1e-7), the setting of each structure named exactly | +| `Structure.from_spacegroup`, `SpacegroupAnalyzer.get_symmetry_operations`, `get_point_group_symbol` on the preset crystals (fcc, bcc, diamond, hcp, beta'' C2/m, 2H-WS2) | `CrystalPresets`, `CrystalStructure.operations`, `laueClass` | **held** | study | tests not shipped; assert_allclose defaults | P29 (independent expansions; volume, d, sites 1e-7; Laue class, operation count exact) | +| `symmetry.groups.PointGroup.symmetry_ops` (proper operations of the 11 Laue groups) | `CrystalOrientationSymmetry(laueClass:).operators` | **held** | study | tests not shipped; assert_allclose defaults | P17; all eleven operation sets, counts and Cartesian transforms checked (operations 1e-7 absolute, closure 2e-14); standard settings documented in `docs/orientation-symmetry.md`. Minimum-angle representative checked independently with SciPy; no unknown-class fallback | +| `SpacegroupAnalyzer.get_point_group_operations()` of P-31m, P-31c, P-3m1, P-3c1, R-3m, R-3c (hexagonal axes) | `CrystalOrientationSymmetry(laueClass: .threeBarM, setting:)`, `PhaseModel.orientationSymmetry(of:)` | **held** | study | tests not shipped; assert_allclose defaults | P32; both -3m settings equal the proper part of pymatgen's operations (operators 1e-7), the setting of each structure named exactly | ### 1.6 `pymatgen.io.cif` and Materials Project documents | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| | `CifParser.from_str(...).parse_structures()` | `CIFImport.read(_:fileBaseName:)` -> `CIFDocument.structure` | **partly** | study | tests not shipped; function arguments `occupancy_tolerance=1.0`, `site_tolerance=1e-4`, `frac_tolerance=1e-4`, `comp_tol=0.01` | P2 on 19 structures: cell, expanded sites, operations equal. Differences by design: the kit reads the subset that defines a structure (cell, atom-site loop, operation loop), refuses what it cannot expand safely (named errors, `CIFImportError`), merges images within the written precision and 0.10 A (pymatgen: `site_tolerance` 1e-4; the kit is looser by design) and detects the symmetry from the expanded cell; no file is read from disk (the caller passes text) | -| symmetry of a CIF that names a group but lists no operations (`get_symops` falling back to `SpaceGroup`), and a group named by Hermann-Mauguin symbol alone | `CIFDocument.declaredSpaceGroupSymbol`, `declaredSpaceGroupNumber`, `CIFImport.spaceGroupOrder`; `hadSymmetryOperations` | not held | study | tests not shipped; assert_allclose defaults (D17) | the completeness check works from the IT number; a symbol-only group is unchecked at the tagged commit (roadmap item 1, CIF symmetry, diagnose first). A fixture of spglib's spellings (`tools/cif-symmetry/pins.py`) and tests exist in the working tree, uncommitted, and are not counted here | -| `CifParser(filename)`, `CifFile.from_file`, `CifBlock` | none | not held | not planned (`CIFImport` takes text; the caller reads files) | tests not shipped; assert_allclose defaults (D17) | | -| `CifParser.get_symops` / `SymmOp.from_xyz_str`, `as_xyz_str` | `SymmetryOperation(xyz:)`, `xyzString` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P2 builds the pinned operations from pymatgen's strings; the grammar is mac4DSTEM 7d6fdbd1's with a refusal of singular rotations added (`SymmetryOperationTests`); `xyzString` is not compared with `as_xyz_str` | -| `MPRester` summary documents (`structure`, `symmetry`) as a source of structures | `MaterialsProjectImport` (`decode`, `structure(from:)`, `check(_:against:)`), `MaterialsProjectDocument` | **partly** | useful | tests not shipped; assert_allclose defaults (D17) | P20, P30: the conventional cell built from the committed MP-shaped inputs equals pymatgen's `SpacegroupAnalyzer` conventional cell (volume 1e-7); the request builder is not run against the service. MP and ICSD database access beyond the request builder is not planned | -| `CifParser.get_magsymops`, magnetic moments, oxidation states | none | not held | not planned (magnetic structures) | tests not shipped; assert_allclose defaults (D17) | | -| `CifParser.check`, `has_errors` | `CrystalStructure.validationIssues` | not held | useful | tests not shipped; assert_allclose defaults (D17) | related, different checks; never compared | -| `CifWriter`, `get_bibtex_string` | none | not held | not planned (CIF export is out of scope (ROADMAP)) | tests not shipped; assert_allclose defaults (D17) | no CIF is written | +| symmetry of a CIF that names a group but lists no operations (`get_symops` falling back to `SpaceGroup`), and a group named by Hermann-Mauguin symbol alone | `CIFDocument.declaredSpaceGroupSymbol`, `declaredSpaceGroupNumber`, `CIFImport.spaceGroupOrder`; `hadSymmetryOperations` | not held | study | tests not shipped; assert_allclose defaults | the completeness check works from the IT number; a symbol-only group is unchecked at the tagged commit (ROADMAP item 1, CIF symmetry, diagnose first). A fixture of spglib's spellings (`tools/cif-symmetry/pins.py`) and tests exist in the working tree, uncommitted, and are not counted here | +| `CifParser(filename)`, `CifFile.from_file`, `CifBlock` | none | not held | not planned (`CIFImport` takes text; the caller reads files) | tests not shipped; assert_allclose defaults | | +| `CifParser.get_symops` / `SymmOp.from_xyz_str`, `as_xyz_str` | `SymmetryOperation(xyz:)`, `xyzString` | **partly** | study | tests not shipped; assert_allclose defaults | P2 builds the pinned operations from pymatgen's strings; the grammar is mac4DSTEM 7d6fdbd1's with a refusal of singular rotations added (`SymmetryOperationTests`); `xyzString` is not compared with `as_xyz_str` | +| `MPRester` summary documents (`structure`, `symmetry`) as a source of structures | `MaterialsProjectImport` (`decode`, `structure(from:)`, `check(_:against:)`), `MaterialsProjectDocument` | **partly** | useful | tests not shipped; assert_allclose defaults | P20, P30: the conventional cell built from the committed MP-shaped inputs equals pymatgen's `SpacegroupAnalyzer` conventional cell (volume 1e-7); the request builder is not run against the service. MP and ICSD database access beyond the request builder is not planned | +| `CifParser.get_magsymops`, magnetic moments, oxidation states | none | not held | not planned (magnetic structures) | tests not shipped; assert_allclose defaults | | +| `CifParser.check`, `has_errors` | `CrystalStructure.validationIssues` | not held | useful | tests not shipped; assert_allclose defaults | related, different checks; never compared | +| `CifWriter`, `get_bibtex_string` | none | not held | not planned (CIF export is out of scope (ROADMAP)) | tests not shipped; assert_allclose defaults | no CIF is written | ### 1.7 `pymatgen.analysis.diffraction` (TEMCalculator, XRDCalculator, NDCalculator) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `TEMCalculator.wavelength_rel` | `ElectronWavelength.angstroms(volts:)` (volts, CODATA 2018) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P8 against 60-digit decimals, not against pymatgen. A one-off comparison on 2026-10-15 (SciPy 1.18.1's constants, `TEMCalculator`'s formula): relative difference -6.3e-10 (80 kV), -5.7e-10 (200 kV), -5.3e-10 (300 kV), from the electron-mass edition (CODATA 2022 in SciPy, 2018 in the kit); no test holds this | -| `TEMCalculator.get_interplanar_spacings` | `UnitCell.d(_:)`, `ReflectionList` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the d values are P1/P2 (`Lattice.d_hkl`, rtol 1e-7); `zone_axis_filter` and the hkl box are not held | -| `TEMCalculator.bragg_angles` | `RingCalibration.twoTheta(wavelengthAngstroms:d:)` (2 theta = 2 asin(lambda / 2d)) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P8 (decimal truth, 1e-7); the kit returns 2 theta, pymatgen theta; not compared with pymatgen | +| `TEMCalculator.wavelength_rel` | `ElectronWavelength.angstroms(volts:)` (volts, CODATA 2018) | **partly** | study | tests not shipped; assert_allclose defaults | P8 against 60-digit decimals, not against pymatgen. A one-off comparison on 2026-10-15 (SciPy 1.18.1's constants, `TEMCalculator`'s formula): relative difference -6.3e-10 (80 kV), -5.7e-10 (200 kV), -5.3e-10 (300 kV), from the electron-mass edition (CODATA 2022 in SciPy, 2018 in the kit); no test holds this | +| `TEMCalculator.get_interplanar_spacings` | `UnitCell.d(_:)`, `ReflectionList` | **partly** | study | tests not shipped; assert_allclose defaults | the d values are P1/P2 (`Lattice.d_hkl`, rtol 1e-7); `zone_axis_filter` and the hkl box are not held | +| `TEMCalculator.bragg_angles` | `RingCalibration.twoTheta(wavelengthAngstroms:d:)` (2 theta = 2 asin(lambda / 2d)) | **partly** | study | tests not shipped; assert_allclose defaults | P8 (decimal truth, 1e-7); the kit returns 2 theta, pymatgen theta; not compared with pymatgen | | `TEMCalculator.get_interplanar_angle` | `UnitCell.interplanarAngleDeg(_:_:)` | **partly** | study | tests not shipped; the source tests parallelism at `atol=1e-5` degrees (`is_parallel`) | closed forms (90, 45, 0 degrees, 1e-12) in `UnitCellTests`; not compared with pymatgen | -| `TEMCalculator.x_ray_factors`, `get_s2`, `electron_scattering_factors` (Mott-Bethe from X-ray factors) | none; the kit uses the Lobato-Van Dyck table of py4DSTEM (`ElectronScatteringFactors`) | not held | not planned (different scattering model by design; the numbers differ) | tests not shipped; assert_allclose defaults (D17) | | -| `TEMCalculator.cell_scattering_factors`, `cell_intensity`, `normalized_cell_intensity` (with `debye_waller_factors`) | `StructureFactors.compute(_:in:formFactor:)` (no Debye-Waller factor, no Lorentz term) | not held | study | tests not shipped; assert_allclose defaults (D17) | P4 holds the kit's own model; pymatgen's electron model is not compared; the Debye-Waller part is roadmap item 4 | -| `TEMCalculator.get_pattern`, `get_positions`, `tem_dots`, `get_first_point`, `is_parallel`, `generate_points`, `zone_axis_filter` (the spots of a beam direction) | none as one call; `ZoneAxis.project` gives s_g and detector-plane positions from a unit beam direction | not held | study | tests not shipped; assert_allclose defaults (D17) | not compared; the diffsims-like kinematic pattern API is roadmap item 4 | -| `TEMCalculator.get_plot_2d`, `get_plot_2d_concise`, `get_plot_coeffs`; `AbstractDiffractionPatternCalculator.get_plot` | none | not held | not planned (plotting is not a kit's job) | tests not shipped; assert_allclose defaults (D17) | | +| `TEMCalculator.x_ray_factors`, `get_s2`, `electron_scattering_factors` (Mott-Bethe from X-ray factors) | none; the kit uses the Lobato-Van Dyck table of py4DSTEM (`ElectronScatteringFactors`) | not held | not planned (different scattering model by design; the numbers differ) | tests not shipped; assert_allclose defaults | | +| `TEMCalculator.cell_scattering_factors`, `cell_intensity`, `normalized_cell_intensity` (with `debye_waller_factors`) | `StructureFactors.compute(_:in:formFactor:)` (no Debye-Waller factor, no Lorentz term) | not held | study | tests not shipped; assert_allclose defaults | P4 holds the kit's own model; pymatgen's electron model is not compared; the Debye-Waller part is ROADMAP item 4 | +| `TEMCalculator.get_pattern`, `get_positions`, `tem_dots`, `get_first_point`, `is_parallel`, `generate_points`, `zone_axis_filter` (the spots of a beam direction) | none as one call; `ZoneAxis.project` gives s_g and detector-plane positions from a unit beam direction | not held | study | tests not shipped; assert_allclose defaults | not compared; the diffsims-like kinematic pattern API is ROADMAP item 4 | +| `TEMCalculator.get_plot_2d`, `get_plot_2d_concise`, `get_plot_coeffs`; `AbstractDiffractionPatternCalculator.get_plot` | none | not held | not planned (plotting is not a kit's job) | tests not shipped; assert_allclose defaults | | | `XRDCalculator.get_pattern` | `StructureFactors.compute(_:in:formFactor:)` with an injected X-ray form factor | **partly** | useful | tests not shipped; class constants `SCALED_INTENSITY_TOL=1e-3`, `TWO_THETA_TOL=1e-5` | P6: sum of \|F\|^2 over each pymatgen peak's members, with pymatgen's X-ray form factor injected from the pin, equals its intensity / (2 LP) within 1e-3 relative (the class's `SCALED_INTENSITY_TOL`; was 1e-6) on 6 structures. No 2 theta, Lorentz-polarisation or peak-merging output; no X-ray table in the kit | -| `NDCalculator.get_pattern` | none | not held | not planned (neutron diffraction) | tests not shipped; assert_allclose defaults (D17) | | +| `NDCalculator.get_pattern` | none | not held | not planned (neutron diffraction) | tests not shipped; assert_allclose defaults | | ## 2. py4DSTEM 0.14.19 (f050d207; source walked from `References/python/py4dstem/lib/python3.12/site-packages/py4DSTEM`, no tests shipped; executed for P5, P14, P18, P25, P28, P34) @@ -208,68 +206,68 @@ as a pinned f050d207 environment the pins run (P14, P18). The remaining unexecut | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Crystal.from_CIF(CIF, primitive=True, conventional_standard_structure=True)` | `CIFImport.read` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the CIF reader is P2 (against pymatgen, which py4DSTEM calls); the conventional-cell conversion is `CrystalStructure.conventionalSetting` (partly: P13 against spglib's standardised cell, which pymatgen and py4DSTEM call; py4DSTEM's call itself is not run) | -| `Crystal.from_pymatgen_structure`, `Crystal(positions, numbers, cell)` | `CrystalStructure(cell:sites:operations:)`, `CrystalStructure.detectingSymmetry(cell:sites:)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | a structure built in code; not compared with py4DSTEM's constructor | -| `Crystal.from_unitcell_parameters(latt_params, elements, positions, space_group, lattice_type)` | `CrystalPresets.structure(a:b:c:alphaDeg:betaDeg:gammaDeg:sites:name:)` (symmetry detected from the atoms, no space-group expansion) | not held | useful | tests not shipped; assert_allclose defaults (D17) | the space-group route needs `SpaceGroup` operations (section 1.4) | -| `Crystal.from_ase`, `from_prismatic` | none | not held | not planned (other file and structure formats) | tests not shipped; assert_allclose defaults (D17) | | -| `Crystal.lat_real`, `lat_inv` (convention: a along x, b in xy, no 2 pi), `calculate_lattice` | `UnitCell.realLattice`, `reciprocalLattice` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the convention is the one the kit takes from py4DSTEM (`NOTICE`) and checks against pymatgen `vesta=True` (P1); py4DSTEM itself is not run | -| `Crystal.cartesian_to_lattice`, `lattice_to_cartesian`, `hexagonal_to_lattice`, `lattice_to_hexagonal`, `cartesian_to_miller`, `miller_to_cartesian`, `rational_ind` | `ZoneAxis.cartesian(in:)` (lattice direction to Cartesian only), `MillerIndex.millerBravaisI` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | `ZoneAxisTests` (hand arithmetic); the inverse direction (Cartesian beam direction to the nearest low-index uvw, `rational_ind`) and Miller-Bravais conversions of directions are not held | -| `Crystal.get_strained_crystal`, `calculate_strain` | none | not held | not planned (strain is CuboKit's) | tests not shipped; assert_allclose defaults (D17) | | +| `Crystal.from_CIF(CIF, primitive=True, conventional_standard_structure=True)` | `CIFImport.read` | **partly** | study | tests not shipped; assert_allclose defaults | the CIF reader is P2 (against pymatgen, which py4DSTEM calls); the conventional-cell conversion is `CrystalStructure.conventionalSetting` (partly: P13 against spglib's standardised cell, which pymatgen and py4DSTEM call; py4DSTEM's call itself is not run) | +| `Crystal.from_pymatgen_structure`, `Crystal(positions, numbers, cell)` | `CrystalStructure(cell:sites:operations:)`, `CrystalStructure.detectingSymmetry(cell:sites:)` | **partly** | study | tests not shipped; assert_allclose defaults | a structure built in code; not compared with py4DSTEM's constructor | +| `Crystal.from_unitcell_parameters(latt_params, elements, positions, space_group, lattice_type)` | `CrystalPresets.structure(a:b:c:alphaDeg:betaDeg:gammaDeg:sites:name:)` (symmetry detected from the atoms, no space-group expansion) | not held | useful | tests not shipped; assert_allclose defaults | the space-group route needs `SpaceGroup` operations (section 1.4) | +| `Crystal.from_ase`, `from_prismatic` | none | not held | not planned (other file and structure formats) | tests not shipped; assert_allclose defaults | | +| `Crystal.lat_real`, `lat_inv` (convention: a along x, b in xy, no 2 pi), `calculate_lattice` | `UnitCell.realLattice`, `reciprocalLattice` | **partly** | study | tests not shipped; assert_allclose defaults | the convention is the one the kit takes from py4DSTEM (`NOTICE`) and checks against pymatgen `vesta=True` (P1); py4DSTEM itself is not run | +| `Crystal.cartesian_to_lattice`, `lattice_to_cartesian`, `hexagonal_to_lattice`, `lattice_to_hexagonal`, `cartesian_to_miller`, `miller_to_cartesian`, `rational_ind` | `ZoneAxis.cartesian(in:)` (lattice direction to Cartesian only), `MillerIndex.millerBravaisI` | **partly** | study | tests not shipped; assert_allclose defaults | `ZoneAxisTests` (hand arithmetic); the inverse direction (Cartesian beam direction to the nearest low-index uvw, `rational_ind`) and Miller-Bravais conversions of directions are not held | +| `Crystal.get_strained_crystal`, `calculate_strain` | none | not held | not planned (strain is CuboKit's) | tests not shipped; assert_allclose defaults | | ### 2.2 Structure factors, scattering factors and kinematic patterns (`crystal.py`, `utils`) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Crystal.calculate_structure_factors(k_max, tol_structure_factor, return_intensities)` | `CrystalStructure.kinematicReflections(kMax:cut:formFactor:)` (`KinematicReflectionList`; `retainingAll: false` streams the list and keeps only the reflections above the cut) | **partly** | study | tests not shipped; function argument `tol_structure_factor=1e-4` (the kit's `cut`) | P4 (NumPy from the definition, 20 structures; 1e-7 relative) and P7 (the apps' code, which follows py4DSTEM, bit for bit); py4DSTEM's function is not run. F is in A^-2 (f in A over V in A^3); the default cut 1e-4 is the apps'. `retainingAll: false` already streams the list (`ACOMTemplateLibrary.generate` uses it); roadmap item 4 asks the q-calibration path, whose default list peaks near 800 MB for a 30 A cell at kMax 2.5, to use it | -| `process.utils.single_atom_scatter.electron_scattering_factor(Z, gsq, units="A")` (Lobato and Van Dyck 2014) | `ElectronScatteringFactors.f(z:gSquared:)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P5: the table is py4DSTEM's file bit for bit (103 x 10) and f_e equals NumPy's evaluation of the formula within 1e-7 relative; py4DSTEM's function is not run; its `units="VA"` is not ported | -| `electron_wavelength_angstrom(E_eV)` (`process.utils.utils`) and `Crystal.setup_diffraction(accelerating_voltage)` | `ElectronWavelength.angstroms(volts:)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P8 against decimals (section 1.7); py4DSTEM's function is not run | -| `Crystal.excitation_errors(g, foil_normal=None, precession_angle_degrees=None)` | `ZoneAxis.project(_:onto:wavelengthAngstroms:sgMax:)` (s_g = g.n + lambda \|g\|^2 / 2) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | `ZoneAxisTests` (hand arithmetic; `testCurvatureSignIsTheNegativeOfPy4DSTEMsInTheLabFrameOfTheBeam` fixes the stated overall sign difference from py4DSTEM's expression and the dropped denominator); py4DSTEM not run; no foil normal, no precession | +| `Crystal.calculate_structure_factors(k_max, tol_structure_factor, return_intensities)` | `CrystalStructure.kinematicReflections(kMax:cut:formFactor:)` (`KinematicReflectionList`; `retainingAll: false` streams the list and keeps only the reflections above the cut) | **partly** | study | tests not shipped; function argument `tol_structure_factor=1e-4` (the kit's `cut`) | P4 (NumPy from the definition, 20 structures; 1e-7 relative) and P7 (the apps' code, which follows py4DSTEM, bit for bit); py4DSTEM's function is not run. F is in A^-2 (f in A over V in A^3); the default cut 1e-4 is the apps'. `retainingAll: false` already streams the list (`ACOMTemplateLibrary.generate` uses it); ROADMAP item 4 asks the q-calibration path, whose default list peaks near 800 MB for a 30 A cell at kMax 2.5, to use it | +| `process.utils.single_atom_scatter.electron_scattering_factor(Z, gsq, units="A")` (Lobato and Van Dyck 2014) | `ElectronScatteringFactors.f(z:gSquared:)` | **partly** | study | tests not shipped; assert_allclose defaults | P5: the table is py4DSTEM's file bit for bit (103 x 10) and f_e equals NumPy's evaluation of the formula within 1e-7 relative; py4DSTEM's function is not run; its `units="VA"` is not ported | +| `electron_wavelength_angstrom(E_eV)` (`process.utils.utils`) and `Crystal.setup_diffraction(accelerating_voltage)` | `ElectronWavelength.angstroms(volts:)` | **partly** | study | tests not shipped; assert_allclose defaults | P8 against decimals (section 1.7); py4DSTEM's function is not run | +| `Crystal.excitation_errors(g, foil_normal=None, precession_angle_degrees=None)` | `ZoneAxis.project(_:onto:wavelengthAngstroms:sgMax:)` (s_g = g.n + lambda \|g\|^2 / 2) | **partly** | study | tests not shipped; assert_allclose defaults | `ZoneAxisTests` (hand arithmetic; `testCurvatureSignIsTheNegativeOfPy4DSTEMsInTheLabFrameOfTheBeam` fixes the stated overall sign difference from py4DSTEM's expression and the dropped denominator); py4DSTEM not run; no foil normal, no precession | | `Crystal.generate_diffraction_pattern(...)` (orientation, zone_axis_lattice, proj_x_lattice, sigma_excitation_error, tol_intensity, k_max, keep_qz, foil normal, precession) | `ZoneAxis.lowIndex`, `ZoneAxis.cartesian(in:)`, `ZoneAxis.detectorBasis(_:)`, `ZoneAxis.project` | **partly** | study | tests not shipped; function arguments `sigma_excitation_error=0.01`, `tol_excitation_error_mult=3`, `tol_intensity=1e-5` | geometry only (s_g and detector-plane position of the kinematic reflections); no intensity weighting by sigma_excitation_error, no orientation object; hand arithmetic and the apps' 49 low-index axes (`ZoneAxisTests`). The weighted spots at a given orientation matrix are the two held rows below | | `Crystal.generate_diffraction_pattern(orientation_matrix=..., sigma_excitation_error, tol_excitation_error_mult, tol_intensity, k_max)` at a given 3x3 orientation matrix, flat (lambda -> 0) and at 200 kV | `ACOMTemplateLibrary.project(reflections:zoneAxis:settings:)` with `ACOMExcitationModel.flat` and `.py4DSTEM(wavelengthAngstrom:)` (py4DSTEM's own excitation error) | **held** | study | tests not shipped; function arguments `sigma_excitation_error=0.01`, `tol_excitation_error_mult=3`, `tol_intensity=1e-5` | P25: r, azimuth and weight elementwise on 1808 spots of 4 crystals, 5.1e-15 relative worst (1e-6 bound [L]: py4DSTEM's intensity is a float32 product in places). `.appCurvature` is the apps' sign of the curvature term, which is the negative of py4DSTEM's in the lab frame (`ZoneAxisTests`); the model has to follow the frame of the patterns being matched (the planted-pattern test fails by tens of degrees in the in-plane angle with the wrong sign) | -| `Crystal.generate_diffraction_pattern(zone_axis_lattice, sigma_excitation_error, tol_excitation_error_mult, k_max)` (the spots of one zone axis) | `PhaseReferenceLibrary.projectedVectors(reflections:crystal:zoneAxis:settings:)`, `build(phases:settings:)` (flat Ewald sphere, weight exp(-(s/w)^2) = py4DSTEM's at sigma = w/sqrt(2)) | **held** | study | tests not shipped; function arguments as the row above | P14 at lambda -> 0 (py4DSTEM's own output; \|q\| 1e-7, intensity 5e-5 against its complex64 [L], 1.15e-5 measured); at 200 kV py4DSTEM's curved sphere changes the hkl set on 4 of 37 axes (recorded in the slice 4 design note, archived in the suite's private folder), so the kit is py4DSTEM's flat limit | -| `Crystal.parse_orientation(zone_axis_lattice)` (the in-plane x and y of a zone axis) | `ZoneAxisFrame.detectorBasis(zoneAxis:)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | a different in-plane frame by design (the apps' maps were made in it); the rotation phi_py between the two is pinned per axis and the kit's vectors rotated by it equal py4DSTEM's within 1e-7 (P14; measured 1.1e-15) | +| `Crystal.generate_diffraction_pattern(zone_axis_lattice, sigma_excitation_error, tol_excitation_error_mult, k_max)` (the spots of one zone axis) | `PhaseReferenceLibrary.projectedVectors(reflections:crystal:zoneAxis:settings:)`, `build(phases:settings:)` (flat Ewald sphere, weight exp(-(s/w)^2) = py4DSTEM's at sigma = w/sqrt(2)) | **held** | study | tests not shipped; function arguments as the row above | P14 at lambda -> 0 (py4DSTEM's own output; \|q\| 1e-7, intensity 5e-5 against its complex64 [L], 1.15e-5 measured); at 200 kV py4DSTEM's curved sphere changes the hkl set on 4 of 37 axes (`tools/slice-4/README.md`, point 6), so the kit is py4DSTEM's flat limit | +| `Crystal.parse_orientation(zone_axis_lattice)` (the in-plane x and y of a zone axis) | `ZoneAxisFrame.detectorBasis(zoneAxis:)` | **partly** | study | tests not shipped; assert_allclose defaults | a different in-plane frame by design (the apps' maps were made in it); the rotation phi_py between the two is pinned per axis and the kit's vectors rotated by it equal py4DSTEM's within 1e-7 (P14; measured 1.1e-15) | | `Crystal.generate_ring_pattern(k_max, ...)` | `kinematicReflections(kMax:).families` (rings with d, multiplicity, sum \|F\|^2) | **partly** | useful | tests not shipped; function arguments `sigma_excitation_error=0.02`, `tol_intensity=1e-3` | P4; py4DSTEM returns a radial intensity profile, the kit a ring list; not compared | -| `Crystal.calculate_dynamical_structure_factors`, `generate_dynamical_diffraction_pattern`, `generate_CBED` (Bloch), `generate_projected_potential`, `generate_moire_diffraction_pattern`, `tdesign` | none | not held | not planned (dynamical scattering is SimulatoKit's) | tests not shipped; assert_allclose defaults (D17) | | +| `Crystal.calculate_dynamical_structure_factors`, `generate_dynamical_diffraction_pattern`, `generate_CBED` (Bloch), `generate_projected_potential`, `generate_moire_diffraction_pattern`, `tdesign` | none | not held | not planned (dynamical scattering is SimulatoKit's) | tests not shipped; assert_allclose defaults | | ### 2.3 ACOM: orientation plan, matching, orientation maps (`crystal_ACOM.py`, `utils.py`) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| | `Crystal.orientation_plan(...)` | `ACOMTemplateLibrary.generate(reflections:kMax:laueClass:settings:)`, `generate(crystal:kMax:settings:)` (`ACOMTemplateSettings`: `power_intensity` 0.25, `corr_kernel_size` 0.08, `power_radial` 0 by default, sigma and tolerance as `sgWidth` and `sgMax`) | **partly** | study | tests not shipped; function arguments `tol_distance=0.01`, `sigma_excitation_error=0.02`, `corr_kernel_size=0.08` | P25: the apps' simplified polar-correlation route, **not** py4DSTEM's: no SO(3) correlogram, a uniform radial grid (py4DSTEM groups reflections into shells of equal \|g\|), a Fibonacci sample of the fundamental sector (py4DSTEM an angular grid, `angle_step_zone_axis`), each ring's mean removed (py4DSTEM keeps it), `power_radial` 0 (py4DSTEM 1.0). The library equals the NumPy transcription of the app source (template images bit exact, spots 1e-6 [L: Float arithmetic, 1.2e-7 measured]); py4DSTEM's plan arrays are not compared | -| `Crystal.match_orientations`, `match_single_pattern` (template matching) | `ACOMMatcher.match(peaks:originX:originY:invAngstromPerPixel:)`, `templateScores`, `experimentalPolarImage`; `matchAll` over a scan (`ACOMScanSelection`, `ACOMScanResults`, cancellation, progress; scheduling only, each position equals `match`, `ACOMScanTests`; speed in `tools/acom-benchmark`), and `matchAll(backend: .metal)` (the correlation on the GPU, equal to the CPU reference within 1e-6, P31) | **partly** | study | tests not shipped; function argument `min_number_peaks=3` (the kit's `minimumPeakCount` is 2) | P25: the same route's correlation (conj(E) T and the mirror pass E T per template, the in-plane angle from the correlation peak, the runner-up at least `distinctOrientationDegrees` away), pinned to the NumPy transcription of the app source. Against py4DSTEM only the answer is compared: py4DSTEM's own peaks of 18 planted orientations, its own `match_single_pattern`, and the kit's route with `ACOMExcitationModel.py4DSTEM` (0.3-4.0 degrees from the truth for 17 of 18, 0.4-3.9 from py4DSTEM's matrix; one 2/m pattern off by 127 degrees, py4DSTEM 1.5). The vector-matching route is `PhaseVectorMatcher` (section 2.4). The diagnose-first ACOM mirror item (mirrored win carries angle + pi with no flag in py4DSTEM; 37 to 43 of 200 off-grid self-recoveries fail at 1.4 degrees) belongs to this row (roadmap item 1) | -| `match_single_pattern` internals: the correlogram score (`corr_value`), sub-pixel zone-axis refinement (the in-plane sub-pixel fit is the row above), `num_matches_return` > 1 and `multiple_corr_reset`, `orientation_refine` (coarse-to-fine), precession, `fiber` ranges | none | not held | study | tests not shipped; assert_allclose defaults (D17) | the scores are not comparable: py4DSTEM correlates a shell-grouped image that keeps its mean and is not unit-normalised, the route a unit-normalised, ring-mean-free image (for the 18 planted patterns py4DSTEM's `corr` runs from 0.67 to 1.14, not bounded by 1, the route's score from 0.79 to 0.99; neither is pinned to the other); the refinement and multi-match features are not ported (roadmap item 5: port only if the study needs them, say so in the table) | -| `match_single_pattern` in-plane sub-pixel angular fit (`dc = (c[2] - c[0]) / (4 c[1] - 2 c[0] - 2 c[2])` on the three correlation bins around the peak, `corr_in_plane_angle`) | `ACOMMatcher(library:refinesInPlaneAngle:)` (opt-in, default false; CPU `match` and the CPU scan; the Metal path and `result(scores:bins:mirrored:)` carry the bin) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the vertex formula is algebraically py4DSTEM's (offset = 0.5 (y-1 - y+1) / (y-1 - 2 y0 + y+1) bins), guarded to a strict local maximum and clamped to half a bin, but it is applied to the kit's own correlation (the P25 route), not py4DSTEM's correlogram, so no py4DSTEM output is compared. Checked against planted truth and by self-consistency: `ACOMOffGridRecoveryTests` (200 seeded off-grid patterns, failures above 1.4 degrees 35 to 14 at 128 bins, median falls) and `refinementMovesOnlyTheInPlaneAngleByAtMostHalfABin` (template, mirror flag and score unchanged, moves at most half a bin, nearer the plant). Added 0.9.0 | -| (no py4DSTEM counterpart) the mirror flag and the pattern's own in-plane angle: py4DSTEM records an inversion-pair win as in-plane angle + pi and keeps no flag | `OrientationMapAnalysis.mirroredPlane(_:)` (1 where the winner was the mirrored pass), `patternInPlaneAnglePlane(_:)` (in turns, a mirrored match's added half turn taken off), `OrientationResult.mirrored` | **partly** | study | none upstream; derived planes, so exact (D17) | no Python output exists to compare. `OrientationMapMirrorPlaneTests.mirroredPlaneAndPatternAngle` checks hand-computed values (flags exact, angles 1e-6 for the Float turns, a wrap case); the `mirrored` flag itself is part of the matcher rows (P25, P31). `docs/orientation-symmetry.md` states the convention. Added 0.9.0 | -| `Orientation` / `OrientationMap` (`set_orientation`, `get_orientation`, `get_orientation_single`, `sort_orientation_maps`) and `utils.calc_1D_profile` | `OrientationResult`, `ACOMScanResults` (subscript), `EulerAngles` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the kit's per-position result (template, Euler, in-plane angle, scores, mirror flag) is checked through the matcher rows; the py4DSTEM container is not run and `num_matches` > 1 has no counterpart | -| orientation export (`Orientation.matrix` -> SciPy `as_euler("zxz")` of the transpose) | `EulerAngles(py4DSTEMOrientationMatrix:)`, `py4DSTEMOrientationMatrix`, `crystalToLabMatrix` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P15 against SciPy, which py4DSTEM calls (angles rtol 1e-7 + atol 1e-12, which the Float storage meets); py4DSTEM's own export is not run; angles stored as Float | -| `Crystal.symmetry_reduce_directions` | `CrystalOrientationSymmetry.py4DSTEMDirectionFamily(_:order:)` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P28 (the real function under the P18 harness, `order: .sortedFractional`, bit identical) and P34 (real `Crystal`s, pymatgen operator order, `order: .pymatgen`, the default, 1e-7; measured 4.4e-16); families, not a rotation: `reduce` still selects the minimum-angle proper rotation. Wall columns are not compared; space groups that list the operators in another order than the shipped one gave the same families on every pinned matrix | -| `Crystal.plot_orientation_maps` (automatic plan sector, full-mask beam RGB) | `CrystalOrientationSymmetry.ipfColor(direction:key: .py4DSTEM)` | **held** | study | tests not shipped; assert_allclose defaults (D17) | P18 at public Float precision, bits exact; input is the Python family pole except for 2/m (original pole). No plotting, masking, custom sectors or rotated in-plane key. `.native` is the app's **own convention** for m-3m, 6/mmm and 2/m (P19), not a Python colour key; the eight other classes now have a native key (0.6.0, P19) that is the kit's own policy | -| `Crystal.cluster_grains`, `cluster_orientation_map`, `save_ang_file`, `orientation_map_to_orix_CrystalMap` | none | not held | not planned (out of scope (ROADMAP)) | tests not shipped; assert_allclose defaults (D17) | | -| `process.diffraction.flowlines`, `digital_dark_field` | none | not held | not planned (CuboKit's) | tests not shipped; assert_allclose defaults (D17) | | +| `Crystal.match_orientations`, `match_single_pattern` (template matching) | `ACOMMatcher.match(peaks:originX:originY:invAngstromPerPixel:)`, `templateScores`, `experimentalPolarImage`; `matchAll` over a scan (`ACOMScanSelection`, `ACOMScanResults`, cancellation, progress; scheduling only, each position equals `match`, `ACOMScanTests`; speed in `tools/acom-benchmark`), and `matchAll(backend: .metal)` (the correlation on the GPU, equal to the CPU reference within 1e-6, P31) | **partly** | study | tests not shipped; function argument `min_number_peaks=3` (the kit's `minimumPeakCount` is 2) | P25: the same route's correlation (conj(E) T and the mirror pass E T per template, the in-plane angle from the correlation peak, the runner-up at least `distinctOrientationDegrees` away), pinned to the NumPy transcription of the app source. Against py4DSTEM only the answer is compared: py4DSTEM's own peaks of 18 planted orientations, its own `match_single_pattern`, and the kit's route with `ACOMExcitationModel.py4DSTEM` (0.3-4.0 degrees from the truth for 17 of 18, 0.4-3.9 from py4DSTEM's matrix; one 2/m pattern off by 127 degrees, py4DSTEM 1.5). The vector-matching route is `PhaseVectorMatcher` (section 2.4). The diagnose-first ACOM mirror item (mirrored win carries angle + pi with no flag in py4DSTEM; 37 to 43 of 200 off-grid self-recoveries fail at 1.4 degrees) belongs to this row (ROADMAP item 1) | +| `match_single_pattern` internals: the correlogram score (`corr_value`), sub-pixel zone-axis refinement (the in-plane sub-pixel fit is the row above), `num_matches_return` > 1 and `multiple_corr_reset`, `orientation_refine` (coarse-to-fine), precession, `fiber` ranges | none | not held | study | tests not shipped; assert_allclose defaults | the scores are not comparable: py4DSTEM correlates a shell-grouped image that keeps its mean and is not unit-normalised, the route a unit-normalised, ring-mean-free image (for the 18 planted patterns py4DSTEM's `corr` runs from 0.67 to 1.14, not bounded by 1, the route's score from 0.79 to 0.99; neither is pinned to the other); the refinement and multi-match features are not ported (ROADMAP item 5: port only if the study needs them, say so in the table) | +| `match_single_pattern` in-plane sub-pixel angular fit (`dc = (c[2] - c[0]) / (4 c[1] - 2 c[0] - 2 c[2])` on the three correlation bins around the peak, `corr_in_plane_angle`) | `ACOMMatcher(library:refinesInPlaneAngle:)` (opt-in, default false; CPU `match` and the CPU scan; the Metal path and `result(scores:bins:mirrored:)` carry the bin) | **partly** | study | tests not shipped; assert_allclose defaults | the vertex formula is algebraically py4DSTEM's (offset = 0.5 (y-1 - y+1) / (y-1 - 2 y0 + y+1) bins), guarded to a strict local maximum and clamped to half a bin, but it is applied to the kit's own correlation (the P25 route), not py4DSTEM's correlogram, so no py4DSTEM output is compared. Checked against planted truth and by self-consistency: `ACOMOffGridRecoveryTests` (200 seeded off-grid patterns, failures above 1.4 degrees 35 to 14 at 128 bins, median falls) and `refinementMovesOnlyTheInPlaneAngleByAtMostHalfABin` (template, mirror flag and score unchanged, moves at most half a bin, nearer the plant). Added 0.9.0 | +| (no py4DSTEM counterpart) the mirror flag and the pattern's own in-plane angle: py4DSTEM records an inversion-pair win as in-plane angle + pi and keeps no flag | `OrientationMapAnalysis.mirroredPlane(_:)` (1 where the winner was the mirrored pass), `patternInPlaneAnglePlane(_:)` (in turns, a mirrored match's added half turn taken off), `OrientationResult.mirrored` | **partly** | study | none upstream; derived planes, so exact | no Python output exists to compare. `OrientationMapMirrorPlaneTests.mirroredPlaneAndPatternAngle` checks hand-computed values (flags exact, angles 1e-6 for the Float turns, a wrap case); the `mirrored` flag itself is part of the matcher rows (P25, P31). `docs/orientation-symmetry.md` states the convention. Added 0.9.0 | +| `Orientation` / `OrientationMap` (`set_orientation`, `get_orientation`, `get_orientation_single`, `sort_orientation_maps`) and `utils.calc_1D_profile` | `OrientationResult`, `ACOMScanResults` (subscript), `EulerAngles` | **partly** | study | tests not shipped; assert_allclose defaults | the kit's per-position result (template, Euler, in-plane angle, scores, mirror flag) is checked through the matcher rows; the py4DSTEM container is not run and `num_matches` > 1 has no counterpart | +| orientation export (`Orientation.matrix` -> SciPy `as_euler("zxz")` of the transpose) | `EulerAngles(py4DSTEMOrientationMatrix:)`, `py4DSTEMOrientationMatrix`, `crystalToLabMatrix` | **partly** | study | tests not shipped; assert_allclose defaults | P15 against SciPy, which py4DSTEM calls (angles rtol 1e-7 + atol 1e-12, which the Float storage meets); py4DSTEM's own export is not run; angles stored as Float | +| `Crystal.symmetry_reduce_directions` | `CrystalOrientationSymmetry.py4DSTEMDirectionFamily(_:order:)` | **held** | study | tests not shipped; assert_allclose defaults | P28 (the real function under the P18 harness, `order: .sortedFractional`, bit identical) and P34 (real `Crystal`s, pymatgen operator order, `order: .pymatgen`, the default, 1e-7; measured 4.4e-16); families, not a rotation: `reduce` still selects the minimum-angle proper rotation. Wall columns are not compared; space groups that list the operators in another order than the shipped one gave the same families on every pinned matrix | +| `Crystal.plot_orientation_maps` (automatic plan sector, full-mask beam RGB) | `CrystalOrientationSymmetry.ipfColor(direction:key: .py4DSTEM)` | **held** | study | tests not shipped; assert_allclose defaults | P18 at public Float precision, bits exact; input is the Python family pole except for 2/m (original pole). No plotting, masking, custom sectors or rotated in-plane key. `.native` is the app's **own convention** for m-3m, 6/mmm and 2/m (P19), not a Python colour key; the eight other classes now have a native key (0.6.0, P19) that is the kit's own policy | +| `Crystal.cluster_grains`, `cluster_orientation_map`, `save_ang_file`, `orientation_map_to_orix_CrystalMap` | none | not held | not planned (out of scope (ROADMAP)) | tests not shipped; assert_allclose defaults | | +| `process.diffraction.flowlines`, `digital_dark_field` | none | not held | not planned (CuboKit's) | tests not shipped; assert_allclose defaults | | ### 2.4 Phase matching and crystal calibration (`crystal_phase.py`, `crystal_calibrate.py`) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `process.diffraction.crystal_phase.CrystalPhase` (`quantify_single_pattern`, `quantify_phase`: phase weights by non-negative least squares on orientation-matched Bragg peaks) | `PhaseVectorMatcher.map`, `classify`, `classifyKnownVariants` (vector matching after Thronsen et al. 2024), `PhaseReferenceLibrary`, `PhaseVectorSettings` | not held | study | tests not shipped; assert_allclose defaults (D17) | a different method; the kit's verdicts are checked against the apps' code (P16) and planted truth, not against py4DSTEM. Roadmap item 1 (diagnosed in 0.9.0): `minimumVectors` = 2 makes "matrix" by exclusion and the cross-phase winner ignores completeness; the opt-in `minimumMatrixEvidence` and `completenessAwareCrossPhaseRanking` are in the rows below | -| (no py4DSTEM counterpart) "the matrix, by exclusion" needing evidence: `CrystalPhase` has no matrix class | `PhaseVectorSettings.minimumMatrixEvidence` (default 0, off; with n > 0 the verdict "matrix" by exclusion needs at least n vectors removed as the matrix's, else "not indexed"; both classification rules) | **partly** | study | none upstream; verdicts are exact (D17) | no Python output exists. `PhaseVectorMatchingTests.matrixByExclusionCanRequireTheMatrixToHaveBeenSeen` (constructed plants: one stray peak is the shipped matrix verdict, `.notIndexed` with the setting at 1, a pattern the matrix explains stays matrix; both rules) and `ScanInputTests` (default 0). Whether it helps on real scans needs the owner's labelled study scan (`docs/open-items.md`). Added 0.9.0 | +| `process.diffraction.crystal_phase.CrystalPhase` (`quantify_single_pattern`, `quantify_phase`: phase weights by non-negative least squares on orientation-matched Bragg peaks) | `PhaseVectorMatcher.map`, `classify`, `classifyKnownVariants` (vector matching after Thronsen et al. 2024), `PhaseReferenceLibrary`, `PhaseVectorSettings` | not held | study | tests not shipped; assert_allclose defaults | a different method; the kit's verdicts are checked against the apps' code (P16) and planted truth, not against py4DSTEM. Roadmap item 1 (diagnosed in 0.9.0): `minimumVectors` = 2 makes "matrix" by exclusion and the cross-phase winner ignores completeness; the opt-in `minimumMatrixEvidence` and `completenessAwareCrossPhaseRanking` are in the rows below | +| (no py4DSTEM counterpart) "the matrix, by exclusion" needing evidence: `CrystalPhase` has no matrix class | `PhaseVectorSettings.minimumMatrixEvidence` (default 0, off; with n > 0 the verdict "matrix" by exclusion needs at least n vectors removed as the matrix's, else "not indexed"; both classification rules) | **partly** | study | none upstream; verdicts are exact | no Python output exists. `PhaseVectorMatchingTests.matrixByExclusionCanRequireTheMatrixToHaveBeenSeen` (constructed plants: one stray peak is the shipped matrix verdict, `.notIndexed` with the setting at 1, a pattern the matrix explains stays matrix; both rules) and `ScanInputTests` (default 0). Whether it helps on real scans needs a labelled study scan (`docs/open-items.md`). Added 0.9.0 | | `Crystal.calibrate_pixel_size`, `calibrate_unit_cell`, `calculate_bragg_peak_histogram` (multi-peak fit of the pixel size or the unit cell to a Bragg-peak histogram) | `RingCalibration.calibrate(radius:d:highTensionVolts:pixelPitchMM:)` (one ring of known d); `LinearScale.estimate` (innermost reflection over many positions) | not held | study | tests not shipped; function arguments `k_step=0.002`, `k_broadening=0.002` (pixel size) and `0.005`, `0.02` (unit cell) | the kit has the single-ring closed form and the linear-scale estimate, not py4DSTEM's multi-peak fit | ### 2.5 Calibration (`process.calibration`, `data.Calibration`) | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `Calibration.set_Q_pixel_size` (A^-1 per pixel, no 2 pi), `qpixelsize.get_Q_pixel_size(q_meas, q_known)` | `RingCalibration.qPerPixel(d:radius:)`, `q(radius:cameraLengthPixels:wavelengthAngstroms:)`, `radius(q:...)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P8 (decimal truth, 1e-7; the exact form, not the small-angle one); py4DSTEM not run | -| `qpixelsize.get_dq_from_indexed_peaks(qs, hkl, a)` (least-squares pixel size from indexed peaks) | `LinearScale.fit(ringRadiiPixels:indexedInvAngstrom:weights:)`, `LinearScaleFit` (0.10.0; with a standard error) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | `PatternMeasurementTests`: planted truth only, no py4DSTEM number compared: exact on planted rings, the standard error covers the truth in 68 % of 400 noisy draws (measured band 55 to 82 %); `validation: "none"` | -| `ellipse.fit_ellipse_1D(ar, center, fitradii, mask)` | `EllipseCalibration.fit1D(pattern:centerQX:centerQY:innerRadius:outerRadius:...)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P10 (bit identical to the apps' port, which is py4DSTEM's conic fit with the apps' changes) and P11 (SciPy optimum of its objective; centre 1e-5 px, axes 1e-7 relative: the default rtol 1e-7 at centres of 54 to 80 px, so not stricter than D17); py4DSTEM's output is not compared. Row index is `qx`, column `qy`, as py4DSTEM; theta in [0, pi) | -| `ellipse.fit_ellipse_amorphous_ring(data, center, fitradii, p0, mask)` | `EllipseCalibration.fitAmorphousRing(pattern:initial:...)`, `fitBestAvailable(...)` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | P10; the model is py4DSTEM's 11-parameter central Gaussian + two-sided ring Gaussian, optimised in (a, b, theta) by a bounded Levenberg-Marquardt instead of (A, B, C): a stated deviation; py4DSTEM's output is not compared | -| `ellipse.constrain_degenerate_ellipse`, `double_sided_gaussian` | `EllipseCalibration.Policy` (degeneracy bound, sparse floor), `EllipseProfileParameters` | **partly** | study | tests not shipped; assert_allclose defaults (D17) | the degeneracy rules are the apps' (P10); py4DSTEM's are not compared | -| `utils.elliptical_coords.convert_ellipse_params`, `convert_ellipse_params_r` ((A, B, C) <-> (a, b, theta)) | none public (the fits return (a, b, theta)) | not held | study | tests not shipped; assert_allclose defaults (D17) | the conversion is in the tests only | -| elliptical correction of detector coordinates from `p_ellipse` | `EllipseTransform` (`correctedOffset`, `uncorrectedOffset`, `accepted`) | **partly** | study | tests not shipped; assert_allclose defaults (D17) | `EllipseTransformTests` (hand arithmetic and the apps' rules); py4DSTEM not run; offsets of single points, not resampling of whole patterns | -| `elliptical_coords.elliptical_resample`, `elliptical_resample_datacube`, `cartesian_to_polarelliptical_transform`, `radial_elliptical_integral` | none | not held | not planned (image and datacube resampling is CuboKit's) | tests not shipped; assert_allclose defaults (D17) | | -| `calibration.origin` (`get_origin`, `fit_origin`, `get_origin_friedel`), `rotation` (`compare_QR_rotation`), `probe`; `find_Bragg_disks` | none | not held | not planned (CuboKit's or the apps') | tests not shipped; assert_allclose defaults (D17) | | -| `process.latticevectors.*` (`fit_lattice_vectors`, `index_bragg_directions`) | none | not held | not planned (strain is CuboKit's) | tests not shipped; assert_allclose defaults (D17) | | -| `Crystal.plot_structure`, `plot_structure_factors`, `plot_scattering_intensity`, `plot_orientation_zones`, `plot_orientation_plan`, `plot_diffraction_pattern`, `plot_fiber_orientation_maps`, `plot_clusters`, `plot_ring_pattern` | none | not held | not planned (plotting is not a kit's job) | tests not shipped; assert_allclose defaults (D17) | | +| `Calibration.set_Q_pixel_size` (A^-1 per pixel, no 2 pi), `qpixelsize.get_Q_pixel_size(q_meas, q_known)` | `RingCalibration.qPerPixel(d:radius:)`, `q(radius:cameraLengthPixels:wavelengthAngstroms:)`, `radius(q:...)` | **partly** | study | tests not shipped; assert_allclose defaults | P8 (decimal truth, 1e-7; the exact form, not the small-angle one); py4DSTEM not run | +| `qpixelsize.get_dq_from_indexed_peaks(qs, hkl, a)` (least-squares pixel size from indexed peaks) | `LinearScale.fit(ringRadiiPixels:indexedInvAngstrom:weights:)`, `LinearScaleFit` (0.10.0; with a standard error) | **partly** | study | tests not shipped; assert_allclose defaults | `PatternMeasurementTests`: planted truth only, no py4DSTEM number compared: exact on planted rings, the standard error covers the truth in 68 % of 400 noisy draws (measured band 55 to 82 %); `validation: "none"` | +| `ellipse.fit_ellipse_1D(ar, center, fitradii, mask)` | `EllipseCalibration.fit1D(pattern:centerQX:centerQY:innerRadius:outerRadius:...)` | **partly** | study | tests not shipped; assert_allclose defaults | P10 (bit identical to the apps' port, which is py4DSTEM's conic fit with the apps' changes) and P11 (SciPy optimum of its objective; centre 1e-5 px, axes 1e-7 relative: the default rtol 1e-7 at centres of 54 to 80 px, so not stricter than D17); py4DSTEM's output is not compared. Row index is `qx`, column `qy`, as py4DSTEM; theta in [0, pi) | +| `ellipse.fit_ellipse_amorphous_ring(data, center, fitradii, p0, mask)` | `EllipseCalibration.fitAmorphousRing(pattern:initial:...)`, `fitBestAvailable(...)` | **partly** | study | tests not shipped; assert_allclose defaults | P10; the model is py4DSTEM's 11-parameter central Gaussian + two-sided ring Gaussian, optimised in (a, b, theta) by a bounded Levenberg-Marquardt instead of (A, B, C): a stated deviation; py4DSTEM's output is not compared | +| `ellipse.constrain_degenerate_ellipse`, `double_sided_gaussian` | `EllipseCalibration.Policy` (degeneracy bound, sparse floor), `EllipseProfileParameters` | **partly** | study | tests not shipped; assert_allclose defaults | the degeneracy rules are the apps' (P10); py4DSTEM's are not compared | +| `utils.elliptical_coords.convert_ellipse_params`, `convert_ellipse_params_r` ((A, B, C) <-> (a, b, theta)) | none public (the fits return (a, b, theta)) | not held | study | tests not shipped; assert_allclose defaults | the conversion is in the tests only | +| elliptical correction of detector coordinates from `p_ellipse` | `EllipseTransform` (`correctedOffset`, `uncorrectedOffset`, `accepted`) | **partly** | study | tests not shipped; assert_allclose defaults | `EllipseTransformTests` (hand arithmetic and the apps' rules); py4DSTEM not run; offsets of single points, not resampling of whole patterns | +| `elliptical_coords.elliptical_resample`, `elliptical_resample_datacube`, `cartesian_to_polarelliptical_transform`, `radial_elliptical_integral` | none | not held | not planned (image and datacube resampling is CuboKit's) | tests not shipped; assert_allclose defaults | | +| `calibration.origin` (`get_origin`, `fit_origin`, `get_origin_friedel`), `rotation` (`compare_QR_rotation`), `probe`; `find_Bragg_disks` | none | not held | not planned (CuboKit's or the apps') | tests not shipped; assert_allclose defaults | | +| `process.latticevectors.*` (`fit_lattice_vectors`, `index_bragg_directions`) | none | not held | not planned (strain is CuboKit's) | tests not shipped; assert_allclose defaults | | +| `Crystal.plot_structure`, `plot_structure_factors`, `plot_scattering_intensity`, `plot_orientation_zones`, `plot_orientation_plan`, `plot_diffraction_pattern`, `plot_fiber_orientation_maps`, `plot_clusters`, `plot_ring_pattern` | none | not held | not planned (plotting is not a kit's job) | tests not shipped; assert_allclose defaults | | ## 2.6 Kit additions without an upstream function (not counted in the summary) @@ -301,9 +299,9 @@ Single-pattern and calibration helpers the apps asked for that no Python package | `Orientation` (`from_euler(symmetry=)`, `from_matrix`, `from_axes_angles`, `symmetry`, `unit`, `in_euler_fundamental_region`, `dot`, `dot_outer`) | `Orientation(_:symmetry:)` with `reduce()`, `angle(with:)`, `inEulerFundamentalRegion()` (0.10.0; `OrientationResult` still holds Euler angles and a score) | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=1e-4` (`test_from_euler_symmetry`) in `test_quaternion/test_orientation.py` | P36, orix 0.15.0 executed: `in_euler_fundamental_region` for the 11 classes and -3m in both settings (kit settings, 61 draws each; orix's proper-subgroup names 1, 121, 222, 4, 422, 3, 312, 32, 6, 622, 23, 432), Euler angles within 2e-15. Not held: `from_euler(symmetry=)`, `from_matrix`, `from_axes_angles`, `dot`, `dot_outer` | | `Orientation.get_distance_matrix`, `Misorientation.get_distance_matrix`, `angle_with_outer` | none | not held | useful | `atol=1e-4` in `test_quaternion/test_misorientation.py`; `atol=1e-6` in `test_quaternion/test_orientation.py` | | | `Symmetry` as a set of 32 point groups (`order`, `is_proper`, `subgroups`, `proper_subgroup`, `contains_inversion`, `diads`, `from_generators`, `system`) and the 11 Laue groups (`laue`, `laue_proper_subgroup`) | `PointSymmetry`, `LaueClass` (the 11 Laue classes as integer rotations acting on hkl), `CrystalOrientationSymmetry.operators` (proper operators as matrices) | **partly** | study | equality of operator sets, no numeric tolerance (`test_quaternion/test_symmetry.py`) | operation counts and rotation sets per class vs pymatgen/spglib (P2, P3, P17), and vs orix as asserted operator sets inside the P26/P27/P32 generators (the Swift tests compare the kit with the resulting numbers, not with the operator sets); the 21 non-centrosymmetric point groups, `subgroups` and `from_generators` are not held | -| `get_point_group(space_group_number, proper=False)` | none (`PhaseModel.orientationClass(of:)` gives the Laue class of a detected structure) | not held | study | exact equality for all 230 numbers (`test_quaternion/test_symmetry.py::test_get_point_group`) | needs the space group number (roadmap item 2); `PhaseModel.detectedOrientationClassIsSpglibsLaueClassOrNil` (P29) checks 15 groups against spglib's Laue class | +| `get_point_group(space_group_number, proper=False)` | none (`PhaseModel.orientationClass(of:)` gives the Laue class of a detected structure) | not held | study | exact equality for all 230 numbers (`test_quaternion/test_symmetry.py::test_get_point_group`) | needs the space group number (ROADMAP item 2); `PhaseModel.detectedOrientationClassIsSpglibsLaueClassOrNil` (P29) checks 15 groups against spglib's Laue class | | `Symmetry.fundamental_sector` (`FundamentalSector`: `vertices`, `center`, `edges`), `fundamental_zone`, `euler_fundamental_region` | internal sector tables generated from orix (`OrixIPFSectors.swift`, P26); `CrystalOrientationSymmetry.sampleFundamentalZone(count:)` for the directions | **partly** | study | `atol=1e-3`, `1e-4` for the 32 sectors in `test_quaternion/test_symmetry.py`; `atol=1e-3` in `test_vector3d/test_fundamental_sector.py` | the sectors enter only through the colour key (P26) and the sector-corner pins; they are not a public type and `euler_fundamental_region` is held as a private table behind `Orientation.inEulerFundamentalRegion()` (P36) | -| `OrientationRegion.from_symmetry`, `vertices`, `faces`, `get_proper_groups`, `get_distinguished_points`, `Symmetry.fundamental_zone` | none | not held | study | `atol=1e-3` in `test_quaternion/test_orientation_region.py` | the orientation (Rodrigues) fundamental zone is not held; roadmap item 3 | +| `OrientationRegion.from_symmetry`, `vertices`, `faces`, `get_proper_groups`, `get_distinguished_points`, `Symmetry.fundamental_zone` | none | not held | study | `atol=1e-3` in `test_quaternion/test_orientation_region.py` | the orientation (Rodrigues) fundamental zone is not held; ROADMAP item 3 | | `Symmetry.get_axis_orders`, `get_highest_order_axis`, `plot`, `Symmetry.fundamental_sector` plots, `unit_cell_plot` | none | not held | not planned (plotting and diagnostics) | no value test | | ### 3.2 `orix.vector` and `orix.plot` colour keys @@ -316,7 +314,7 @@ Single-pattern and calibration helpers the apps asked for that no Python package | `plot.IPFColorKeyTSL(symmetry, direction).orientation2color(orientation)` (colour of a sample direction in crystal coordinates), `EulerColorKey` | `CrystalOrientationSymmetry.ipfColor(direction:key:)` on `crystalToLabMatrix` columns; `OrientationMapAnalysis.ipfZRGBA` (sample z only, low-reliability mask) | **partly** | study | `atol=1e-2`..`0.2`, defaults in `test_plot/test_orientation_color_keys.py` | the composition (orientation applied to a sample x, y or z) is two held steps; not compared with orix as a whole. `EulerColorKey` is not held | | `Miller` coordinate formats (`hkl`, `hkil`, `uvw`, `UVTW`, `coordinate_format`, `h k i l`, `space`, `is_hexagonal`) | `MillerIndex` (`h`, `k`, `l`, `millerBravaisI`), `ReflectionList.usesMillerBravais` | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=0.01` in `test_vector3d/test_miller.py` (`test_set_hkl_hkil`, `test_set_uvw_UVTW`) | P2 for the index sets; the kit has no direction-versus-plane distinction and no uvw/UVTW conversion; not compared with orix | | `Miller.length`, `Miller.multiplicity` | `UnitCell.d`, `gLength`, `ReflectionFamily.multiplicity` | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=1e-3` in `test_vector3d/test_miller.py` (`test_length`) | P1/P2 for d and \|g\| (against pymatgen); orix not run | -| `Miller.symmetrise`, `unique`, `round` | `PointSymmetry.orbit(of:)`, `canonical(_:)`, `orbitName`, `ReflectionList.families(allowedOnly:)`, `ReflectionFamily` (no public initialiser) | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=1e-3` for trigonal and hexagonal groups, `atol=1e-4` (`test_symmetrise`) in `test_vector3d/test_miller.py` | P2 for orbits and canonical forms (the slice 1 design note's definitions applied to pymatgen's operations); not compared with orix; the kit symmetrises under the Laue group (inversion added), orix under the point group. Roadmap item 2: a public `ReflectionFamily` initialiser | +| `Miller.symmetrise`, `unique`, `round` | `PointSymmetry.orbit(of:)`, `canonical(_:)`, `orbitName`, `ReflectionList.families(allowedOnly:)`, `ReflectionFamily` (no public initialiser) | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=1e-3` for trigonal and hexagonal groups, `atol=1e-4` (`test_symmetrise`) in `test_vector3d/test_miller.py` | P2 for orbits and canonical forms (the definitions written in `tools/slice-1/pins.py` applied to pymatgen's operations); not compared with orix; the kit symmetrises under the Laue group (inversion added), orix under the point group. Roadmap item 2: a public `ReflectionFamily` initialiser | | `Miller.angle_with`, `dot`, `dot_outer`, `cross` (zone axis of two planes, plane of two directions) | `UnitCell.interplanarAngleDeg(_:_:)`; no cross product | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=0.01`..`0.1` in `test_vector3d/test_miller.py` | closed forms in `UnitCellTests`; `cross` (zone axis from two reflections) is not held | | `Miller.from_min_dspacing(phase, min_dspacing)`, `from_highest_indices` | `CrystalStructure.reflections(kMax:)` (a ball in \|g\|, not a box of indices) | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `atol=1e-4` (`test_init_from_min_dspacing`) in `test_vector3d/test_miller.py` | P2 (the set, the order and \|g\| equal pymatgen's at k_max 1.2); orix not run; no highest-index box | | `Vector3d.pole_density_function`, `inverse_pole_density_function`, `measure.pole_density_function` | none | not held | useful | `rtol=1e-3`, `0.01`, `0.1` (`test_measure/test_measure.py`) | | @@ -326,9 +324,9 @@ Single-pattern and calibration helpers the apps asked for that no Python package | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| -| `sampling.get_sample_fundamental(resolution, point_group, space_group)`, `get_sample_reduced_fundamental`, `get_sample_local`, `uniform_SO3_sample`, `cubochoric_sampling` | none (`ACOMTemplateLibrary` samples the zone-axis sector with a Fibonacci grid and the in-plane angle on a regular step) | not held | study | `atol=1e-7` (`test_get_sample_reduced_fundamental`), `atol=0.2`, `rtol=0.025`, `rtol=0.05` in `test_sampling/test_sampling.py`; defaults in `test_sampling/test_cubochoric_sampling.py` | an SO(3) orientation grid in the fundamental zone is not held (roadmap item 3, fundamental zone) | +| `sampling.get_sample_fundamental(resolution, point_group, space_group)`, `get_sample_reduced_fundamental`, `get_sample_local`, `uniform_SO3_sample`, `cubochoric_sampling` | none (`ACOMTemplateLibrary` samples the zone-axis sector with a Fibonacci grid and the in-plane angle on a regular step) | not held | study | `atol=1e-7` (`test_get_sample_reduced_fundamental`), `atol=0.2`, `rtol=0.025`, `rtol=0.05` in `test_sampling/test_sampling.py`; defaults in `test_sampling/test_cubochoric_sampling.py` | an SO(3) orientation grid in the fundamental zone is not held (ROADMAP item 3, fundamental zone) | | `sampling.sample_S2` and the S2 meshes (`uv`, `equal_area`, `cube`, `hexagonal`, `random`, `icosahedral`) | `CrystalOrientationSymmetry.sampleFundamentalZone(count:)` (Fibonacci / farthest-point directions in the sector) | **partly** | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `test_sampling/test_s2_sampling.py` | P19: a native policy (native Float colours and Double sample bits exact against the pinned original source; new classes 1e-7), not orix's meshes; covering radii recorded in `docs/orientation-symmetry.md` | -| `projections.StereographicProjection` (`vector2xy`, `spherical2xy`, `vector2xy_split`) and `InverseStereographicProjection` (`xy2vector`, `xy2spherical`) | none | not held | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `test_projections/test_stereographic_projection.py` | pole figure points (roadmap item 3). orix 0.15.0 ships no Lambert (equal-area) projection class and no Rodrigues projection; `Rodrigues` is a vector class (section 3.1) | +| `projections.StereographicProjection` (`vector2xy`, `spherical2xy`, `vector2xy_split`) and `InverseStereographicProjection` (`xy2vector`, `xy2spherical`) | none | not held | study | np.allclose defaults (rtol 1e-5, atol 1e-8); `test_projections/test_stereographic_projection.py` | pole figure points (ROADMAP item 3). orix 0.15.0 ships no Lambert (equal-area) projection class and no Rodrigues projection; `Rodrigues` is a vector class (section 3.1) | ### 3.4 `orix.crystal_map`, `orix.io` @@ -342,8 +340,8 @@ Single-pattern and calibration helpers the apps asked for that no Python package | Python function or option group | DiffractoKit symbol | State | Plan | Upstream tolerance (test file) | How checked | |---|---|---|---|---|---| | diffsims `utils.sim_utils.get_electron_wavelength(accelerating_voltage)` (kV) | `ElectronWavelength.angstroms(volts:)` | **partly** | useful | not on disk | P8 against decimals; diffsims not run (its constants may differ in the last digits, as pymatgen's do: section 1.7) | -| diffsims `get_kinematical_structure_factor`, `get_kinematical_intensities`, `scattering_params` ("lobato" and the others) | `StructureFactors.compute`, `kinematicReflections` | **partly** | study | not on disk | P4 (own NumPy); diffsims' normalisation (no 1/V) and Debye-Waller factors are not reproduced; not compared (roadmap item 4) | -| diffsims `SimulationGenerator.calculate_diffraction_data` / `DiffractionGenerator.calculate_ed_data` (reciprocal_radius, max_excitation_error, shape_factor_model) | `kinematicReflections` + `ZoneAxis.project` | **partly** | study | not on disk | geometry and F of a beam direction; no shape factor, no sweep over orientations; not compared (roadmap item 4) | +| diffsims `get_kinematical_structure_factor`, `get_kinematical_intensities`, `scattering_params` ("lobato" and the others) | `StructureFactors.compute`, `kinematicReflections` | **partly** | study | not on disk | P4 (own NumPy); diffsims' normalisation (no 1/V) and Debye-Waller factors are not reproduced; not compared (ROADMAP item 4) | +| diffsims `SimulationGenerator.calculate_diffraction_data` / `DiffractionGenerator.calculate_ed_data` (reciprocal_radius, max_excitation_error, shape_factor_model) | `kinematicReflections` + `ZoneAxis.project` | **partly** | study | not on disk | geometry and F of a beam direction; no shape factor, no sweep over orientations; not compared (ROADMAP item 4) | | diffsims `crystallography.ReciprocalLatticeVector` (`from_min_dspacing`, `symmetrise`, `unique`, `multiplicity`, `gspacing`, `allowed`, `structure_factor`, `angle_with`) | `ReflectionList`, `ReflectionFamily` (orbits under the Laue group), `UnitCell.interplanarAngleDeg` | **partly** | study | not on disk | P2 (against pymatgen, not diffsims); the kit symmetrises under the Laue group (inversion added), diffsims under the point group; no vector class | | diffsims `DiffractionLibraryGenerator.get_diffraction_library`, `DiffractionLibrary`, `generators.rotation_list_generators` | `ACOMTemplateLibrary` (templates over a zone-axis sector and in-plane angles) | **partly** | useful | not on disk | the kit's library is the apps' route (P25), not diffsims' orientation-list library; not compared | | pyxem `Diffraction2D` / `ElectronDiffraction2D` signals, `get_azimuthal_integral1d`, `get_direct_beam_position` | none | not held | not planned (signals are FormatoKit's, processing CuboKit's) | not on disk | | diff --git a/docs/status.md b/docs/status.md index 9c64985..2786998 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,11 +1,21 @@ # Status -DiffractoKit 0.10.0 (2026-10-22), on ImagoKit `"0.2.0"..<"1.0.0"`. Live: cells (seven systems, Niggli, conventional cell), CIF import -with symmetry detection and a completeness check by IT number or by symbol, space-group-free symmetry from the atoms, reflections and rings, kinematic -structure factors (py4DSTEM table bit for bit), pattern geometry (ring and ellipse fits, calibration, zone axes, indexing), the -ACOM matcher (CPU reference, Metal option, optional in-plane refinement), vector-matched phase maps, orientation symmetry -and IPF keys, misorientation between two groups, orientation-relationship variants (beta'' in Al: 12), orientation-map planes, -the ring-sequence assignment of the known-crystal Q, delta q from several rings, and single-pattern beam centre, radial profile and spot refinement. What is checked against what: `docs/parity.md`; timings against Python: `docs/benchmarks.md`; -what is open: `docs/open-items.md`. - -Tests are split in two (ROADMAP task 31): `DiffractoKitParityTests` holds the pinned comparisons against the Python references (slice pins for cells and structures, scattering, ring calibration and indexing, ellipse apps parity, slice 4 pins and apps parity, py4DSTEM family and orix pins; 91 tests), the other targets hold the fast unit tests (519 tests), and `DiffractoKitTestSupport` holds the helpers both use (`FixtureDirs`, the pin decoders, structure builders; the fixtures stay in the Fixtures folders of the older test targets). Measured on the Mac (2026-10-21, warm build): `swift test -c release --parallel --skip DiffractoKitParityTests` 15 s, the full `swift test -c release --parallel` 21 s, before the split 19 s; 610 tests before and 519 + 91 = 610 after. +DiffractoKit 0.10.0 (2026-10-22), on ImagoKit `"0.2.0"..<"1.0.0"`. + +Live: + +- Cells in the seven systems, Niggli reduction, the conventional cell. +- CIF import with symmetry detection and a completeness check by IT number or by symbol; space-group-free symmetry from the + atoms; reflections and rings. +- Kinematic structure factors (py4DSTEM's scattering-factor table, bit for bit). +- Pattern geometry: ring and ellipse fits, calibration, zone axes, indexing; the ring-sequence assignment of a known crystal, + Δq from several rings, and the single-pattern beam centre, radial profile and spot refinement. +- The ACOM matcher (CPU reference, Metal option, optional in-plane refinement) and vector-matched phase maps. +- Orientation symmetry and IPF keys, misorientation between two groups, orientation-relationship variants (beta'' in Al: 12), + orientation-map planes. + +Tests: `DiffractoKitParityTests` holds the pinned comparisons against the Python references; the other test targets hold the +fast unit tests; `DiffractoKitTestSupport` holds the helpers both use. + +What is checked against what: `docs/parity.md`. Timings against Python: `docs/benchmarks.md`. What is open: +`docs/open-items.md`. Where it is going: `ROADMAP.md`. From 57c88de831820df0083688b4b8aae34befa89eef Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:56:16 +0000 Subject: [PATCH 10/17] docs(geometry): drop references to archived notes outside the repo Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- Sources/DiffractoGeometry/EllipseCalibration.swift | 8 ++++---- Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift | 3 +-- Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/DiffractoGeometry/EllipseCalibration.swift b/Sources/DiffractoGeometry/EllipseCalibration.swift index d729f45..cb6a24f 100644 --- a/Sources/DiffractoGeometry/EllipseCalibration.swift +++ b/Sources/DiffractoGeometry/EllipseCalibration.swift @@ -147,7 +147,7 @@ public enum EllipseCalibration { /// That is a floor, not a correction: background and the annulus width /// move it, and the coefficient is the Gaussian's. A caller applies only /// b/a and θ (`EllipseTransform`), so a and b are not a - /// calibrated ring radius (`archive/v5/ellipse-stop-rule-gateD-2026-10-12.md`). + /// calibrated ring radius. /// The fit with its ring evidence attached (DiffractoKit 0.1.0). static func attaching(_ evidence: RingEvidence?, to fit: EllipseCalibrationFit) -> EllipseCalibrationFit { EllipseCalibrationFit( @@ -478,7 +478,7 @@ public enum EllipseCalibration { // until the candidate equals the point, and with `converged` // set only on an accepted step the 100-pass cap threw // didNotConverge AT the optimum: 12 of 84 clean planted rings - // (the reference implementation's report, `docs/archive/v5/ellipse-stop-rule-gateD-2026-10-12.md`). + // (measured on the reference implementation, 2026-10-12). // A step this small that roundoff will not let lower the cost is // converged; the fit returns the last accepted point. py4DSTEM's // `leastsq` (MINPACK) tests its xtol whether or not the step is @@ -903,7 +903,7 @@ public enum EllipseCalibration { } // A ring sigma within 0.05 px of its 0.25 px floor is a fit that drained the ring into // the background and the central Gaussian: a, b and the centre then - // carry no ring information (`archive/v5/ring-fit-two-start-gateD-2026-10-14.md`). + // carry no ring information. // DiffractoKit 0.1.0: with `requireRingEvidence`, ONE sigma at the // floor is a sharp edge when the data hold the ring at this fit (its ring evidence passes); both at the // floor is still refused. @@ -974,7 +974,7 @@ public enum EllipseCalibration { // (ro - ri)/4. On a noisy ring that start (10-17 px for a 3 px ring) // can end in a minimum with no ring information, and a start at the // ring's own half-maximum width can end worse on a sloping background - // (`archive/v5/ring-fit-two-start-gateD-2026-10-14.md`). Both are + // (measured 2026-10-14). Both are // refined and the fit with the lower cost on the same pixels is kept. if let ringSigma = halfMaximumRingSigma( values: samples.map(\.value), xs: samples.map(\.x), ys: samples.map(\.y), diff --git a/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift b/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift index 87436f1..c82afb7 100644 --- a/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseNoisyRingTests.swift @@ -8,8 +8,7 @@ // The profile ring fit (`EllipseCalibration.fitAmorphousRing`, behind `fitBestAvailable`) is refined from py4DSTEM's start, both // ring sigmas at max(1, ringWidth / 4), 10-17 px for a +-20 px annulus around a 3 px ring. On a noisy ring the 11-parameter LM // from there can fall into a basin that carries no ring information, and the residual cap does not refuse it: noise inflates the -// normalised residual of any model (reference `archive/v5/ring-fit-two-start-gateD-2026-10-14.md`, and -// `archive/v5/ring-sigma-start-gateD-2026-10-13.md` for the first step). A second start at the ring's half-maximum width escapes +// normalised residual of any model (measured 2026-10-13 and 2026-10-14). A second start at the ring's half-maximum width escapes // those basins, but alone it ends in a worse one on a 1/r^2 tail with noise. The fit therefore refines from both starts and keeps // the lower-cost fit (ADR 069). // diff --git a/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift b/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift index 569320b..ddf0fa4 100644 --- a/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift +++ b/Tests/DiffractoGeometryTests/EllipseStopRuleTests.swift @@ -3,7 +3,7 @@ // `EllipseCalibration.fit1D`'s Levenberg-Marquardt loop (the reference implementation 2026-10-12) once tested convergence only // after an ACCEPTED step. At the optimum the cost change of a step is roundoff, every step was refused, damping grew until the // candidate equalled the point, and the 100-pass cap threw `didNotConverge` on about one clean ring in seven -// (reference `archive/v5/ellipse-stop-rule-gateD-2026-10-12.md`). Ported from reference commit 7d6fdbd1 +// (measured 2026-10-12). Ported from reference commit 7d6fdbd1 // `EllipseStopRuleTests.swift` (one test, same name, same four rings, same bands). // // These four rings are clean 96 x 96 plants the old rule refused (the reference implementations scanned 675 such plants, 111 of them refused, with From 65a1dd25c2691fcfc5a66a943a2a8c397a6e383f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:58:30 +0000 Subject: [PATCH 11/17] docs: cite macTEM's squashed history by version, credit orix macTEM's public history starts at v1.0.0, so its commits a58c84c and 9482831 cannot be found. NOTICE, the source comments and the tools now cite "macTEM before v1.0.0 (history squashed)". NOTICE gains an orix entry (the oracle for P26 and P36, no code copied into the library) and credits the Materials Project fixtures under CC BY 4.0. Part of #17 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- NOTICE | 42 ++++++++++++++++--- Sources/DiffractoCore/ReflectionList.swift | 2 +- .../ElectronWavelength.swift | 2 +- .../RingCalibration+Fit.swift | 2 +- .../DiffractoGeometry/RingCalibration.swift | 2 +- Sources/DiffractoGeometry/RingIndexing.swift | 2 +- Sources/DiffractoGeometry/ZoneAxis.swift | 2 +- Tests/DiffractoGeometryTests/Fixtures/.keep | 0 .../RingCalibrationTests.swift | 2 +- .../RingFitCalibrationTests.swift | 2 +- .../RingIndexingTests.swift | 2 +- .../Geometry/RingCalibrationPinsTests.swift | 2 +- tools/slice-2/README.md | 2 +- tools/slice-2/fetch-table.sh | 2 +- tools/slice-2/pins.py | 4 +- tools/slice-3/README.md | 6 +-- tools/slice-3/index-pins.py | 2 +- tools/slice-3/ring-pins.py | 4 +- tools/slice-3b/README.md | 2 +- 19 files changed, 58 insertions(+), 26 deletions(-) delete mode 100644 Tests/DiffractoGeometryTests/Fixtures/.keep diff --git a/NOTICE b/NOTICE index a86b1f4..957c4d6 100644 --- a/NOTICE +++ b/NOTICE @@ -22,7 +22,8 @@ The cell, lattice and reflection-list code, the CIF parser with its written-precision expansion and its refusals, the family (ring) rule and the monoclinic orbit convention are ported from mac4DSTEM (commit 7448401b: mac4DSTEM/Core/Crystal/Crystal.swift, CrystalModel.swift, CIFImport.swift) and -macTEM (commit a58c84c: Sources/TEMCore/Analysis/DSpacingIndexing.swift, and its design notes), generalised to the seven crystal systems. +macTEM before v1.0.0 (history squashed: Sources/TEMCore/Analysis/DSpacingIndexing.swift, +and its design notes), generalised to the seven crystal systems. The CIF fixtures under Tests/DiffractoCoreTests/Fixtures come from mac4DSTEM's tests at the same commit, each with its provenance in a comment line. @@ -30,9 +31,10 @@ Later parts were read at mac4DSTEM 7d6fdbd1 (structure factors and the |F| cut, Crystal.swift and ScatteringFactors.swift; EllipseCalibration.swift with ADR 069; the ellipse transform of Calibration.swift; the zone axes of PhaseReferenceLibrary.swift, OrientationPlan.swift and OrientationResult.swift) -and macTEM 9482831 (RingCalibration.swift, RingCalibration+Fit.swift and -DSpacingIndexing.swift). The ring-fit tests port those applications' plants and their -renderer at those commits. +and macTEM before v1.0.0 (history squashed: RingCalibration.swift, +RingCalibration+Fit.swift and DSpacingIndexing.swift). The ring-fit tests port +those applications' plants and their renderer at those versions. macTEM's public +history starts at v1.0.0, so its earlier commits are named by version, not hash. mac4DSTEM - https://github.com/mac4DSTEM/mac4DSTEM macTEM - https://github.com/mac4DSTEM/macTEM @@ -109,4 +111,34 @@ plot_orientation_maps (GPL-3.0); its automatic sector bases are generated by calling that version's orientation_plan. pymatgen PointGroup (2026.9.24 / core 2026.9.23, MIT) is an oracle for all eleven proper rotation tables; SciPy Rotation is an oracle for the minimum-angle representative. No orix or -MTEX code is copied or executed. tools/symmetry/pins.py records provenance. +MTEX code is copied into the library. tools/symmetry/pins.py records provenance. + +================================================================================ +orix (used as a reference for checking; no code copied) +================================================================================ + +tools/orix/*-pins.py run orix 0.15.0 as the oracle for the IPF colour key, +direction reduction, disorientation and misorientation (docs/parity.md P26 and +P36); their output is committed as Tests/DiffractoOrientationTests/Fixtures/ +orix-*-pins.json. No orix code is copied into the library. + + orix - https://github.com/pyxem/orix + Copyright (C) The pyxem developers + Licensed under the GNU General Public License v3.0 + +================================================================================ +Materials Project (test fixtures) +================================================================================ + +Tests/DiffractoCoreTests/Fixtures/mp-import/input-*.json are decoder inputs in +the Materials Project API's response format, copied from the applications' tests: +input-aluminium-v3.json is a recorded API response for mp-134, and the +magnesium and silicon files are smaller records in the same format. They are +credited as Materials Project data, licensed CC BY 4.0 +(https://creativecommons.org/licenses/by/4.0/). Changes: none to the recorded +response; the other two were written by hand in its format. + + Source: The Materials Project, https://materialsproject.org + A. Jain et al., "Commentary: The Materials Project: A materials genome + approach to accelerating materials innovation", APL Materials 1, 011002 + (2013), doi:10.1063/1.4812323 diff --git a/Sources/DiffractoCore/ReflectionList.swift b/Sources/DiffractoCore/ReflectionList.swift index 685d9d1..5fcf109 100644 --- a/Sources/DiffractoCore/ReflectionList.swift +++ b/Sources/DiffractoCore/ReflectionList.swift @@ -3,7 +3,7 @@ // Role: every (h, k, l) with 0 < |g| ≤ k_max, its d, whether the space-group operations allow it, and the rings // (families) the list falls into. The enumeration is reference commit 7448401b `Crystal.reflections` without the // structure factor (the per-axis index bound, DEVIATION of 2026-09-14, complete for oblique cells); the absence -// rule is the general reflection condition; the family rule is reference commit a58c84c `DSpacingIndexing.families`. +// rule is the general reflection condition; the family rule is macTEM before v1.0.0 (history squashed) `DSpacingIndexing.families`. // import Foundation diff --git a/Sources/DiffractoGeometry/ElectronWavelength.swift b/Sources/DiffractoGeometry/ElectronWavelength.swift index beadef0..5c39d68 100644 --- a/Sources/DiffractoGeometry/ElectronWavelength.swift +++ b/Sources/DiffractoGeometry/ElectronWavelength.swift @@ -1,7 +1,7 @@ // // ElectronWavelength.swift // Role: the one electron wavelength of the kit, the relativistic de Broglie wavelength at an accelerating voltage. -// Ported from reference commit 9482831 `RingCalibration.wavelengthAngstroms` +// Ported from macTEM before v1.0.0 (history squashed) `RingCalibration.wavelengthAngstroms` // with the same arithmetic order, so the results are bit identical. // // λ = hc / √(eV (2 m₀c² + eV)), V in volts, λ in Å. diff --git a/Sources/DiffractoGeometry/RingCalibration+Fit.swift b/Sources/DiffractoGeometry/RingCalibration+Fit.swift index fd81f14..41c0dac 100644 --- a/Sources/DiffractoGeometry/RingCalibration+Fit.swift +++ b/Sources/DiffractoGeometry/RingCalibration+Fit.swift @@ -3,7 +3,7 @@ // Role: the fit-driven calibration: a ring of known d fitted in a pattern, the fit's numbers checked against every // refusal, and Δq and the camera length computed from the fitted radius only when nothing refuses them. // -// SEED: reference commit 9482831 `Sources/TEMCore/Analysis/RingCalibration+Fit.swift`, logic unchanged. The one difference is the +// SEED: macTEM before v1.0.0 (history squashed) `Sources/TEMCore/Analysis/RingCalibration+Fit.swift`, logic unchanged. The one difference is the // fit it runs: the reference implementation's own `EllipseCalibration` copy there is the pre-ADR-069 file; this runs the kit's // `EllipseCalibration` (reference commit 7d6fdbd1, with ADR 069's two starts and the 0.30 px ring-width floor), so the numbers // the reference implementation measured on its copy move when it imports the kit. diff --git a/Sources/DiffractoGeometry/RingCalibration.swift b/Sources/DiffractoGeometry/RingCalibration.swift index 983361e..6d727d8 100644 --- a/Sources/DiffractoGeometry/RingCalibration.swift +++ b/Sources/DiffractoGeometry/RingCalibration.swift @@ -1,7 +1,7 @@ // // RingCalibration.swift // Role: the calibration a diffraction ring of known d gives (Δq in 1/Å per pixel, and the camera length) as closed forms, -// and the decision of when a ring is NOT to be calibrated from. Ported from reference commit 9482831 `RingCalibration.swift` with every formula, constant, +// and the decision of when a ring is NOT to be calibrated from. Ported from macTEM before v1.0.0 (history squashed) `RingCalibration.swift` with every formula, constant, // NaN rule, refusal order and message kept. The fit that fills a `FitReport` is in `RingCalibration+Fit.swift`. // // THE FORMULAS (exact; nothing here is a small-angle form): diff --git a/Sources/DiffractoGeometry/RingIndexing.swift b/Sources/DiffractoGeometry/RingIndexing.swift index 0a6289d..7e42287 100644 --- a/Sources/DiffractoGeometry/RingIndexing.swift +++ b/Sources/DiffractoGeometry/RingIndexing.swift @@ -1,7 +1,7 @@ // // RingIndexing.swift // Role: the (hkl) family a measured |q| belongs to, against a ring list (DiffractoScattering's `KinematicFamily`, or DiffractoCore's -// `ReflectionFamily`), and the decision of when it is NOT to be named. The decision is reference commit 9482831 +// `ReflectionFamily`), and the decision of when it is NOT to be named. The decision is macTEM before v1.0.0 (history squashed) // `DSpacingIndexing.index` (macTEM ADR 022 and ADR 024), generalised: the list is DiffractoCore's family rule over any // crystal system ("RingIndexing"), so the lattice, orbit and // family-construction code of the seed is not ported and `unsupportedLattice` / `unsupportedElement` cannot occur diff --git a/Sources/DiffractoGeometry/ZoneAxis.swift b/Sources/DiffractoGeometry/ZoneAxis.swift index 3614137..6e7611f 100644 --- a/Sources/DiffractoGeometry/ZoneAxis.swift +++ b/Sources/DiffractoGeometry/ZoneAxis.swift @@ -2,7 +2,7 @@ // ZoneAxis.swift // Role: zone-axis primitives ("ZoneAxis"): the 49 low-index beam // directions, a direction [u v w] as a Cartesian unit vector of a cell, the detector basis of a beam direction, -// and the projection of a reflection list onto it with the excitation error. Seeds, read at reference commit 7d6fdbd1 +// and the projection of a reflection list onto it with the excitation error. Seeds, read at mac4DSTEM 7d6fdbd1 // by `git show`: `PhaseReferenceLibrary.lowIndexZoneAxes` and `cartesianZoneAxis`, `ACOMOrientation.detectorBasis` // (Core/Analysis/OrientationResult.swift), `OrientationPlan.project` (Core/Crystal/OrientationPlan.swift). // Units: Å⁻¹ without 2π, as the rest of the kit. diff --git a/Tests/DiffractoGeometryTests/Fixtures/.keep b/Tests/DiffractoGeometryTests/Fixtures/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/Tests/DiffractoGeometryTests/RingCalibrationTests.swift b/Tests/DiffractoGeometryTests/RingCalibrationTests.swift index ad24cfb..982803e 100644 --- a/Tests/DiffractoGeometryTests/RingCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/RingCalibrationTests.swift @@ -1,7 +1,7 @@ // // RingCalibrationTests.swift // The closed forms (λ, 2θ, L_px, Δq, L_mm) against hand arithmetic, -// and every refusal by its own name. Ported from reference commit 9482831 Tests/RingCalibrationTests.swift (30 tests, same +// and every refusal by its own name. Ported from macTEM before v1.0.0 (history squashed) Tests/RingCalibrationTests.swift (30 tests, same // names, literals unchanged) to Swift Testing. No fit, no Python at run time: the expected numbers below were worked once, // by hand, in 60-digit decimal arithmetic from the CODATA 2018 constants stated in ElectronWavelength.swift (h and c and e // exact, m₀ = 9.1093837015e-31 kg) and are typed here as literals. The many-case parity with a 60-digit decimal script is diff --git a/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift b/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift index 8f71722..111afa3 100644 --- a/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift +++ b/Tests/DiffractoGeometryTests/RingFitCalibrationTests.swift @@ -1,7 +1,7 @@ // // RingFitCalibrationTests.swift // The fit-driven calibration (`RingCalibration.fit`) on rings planted here, in Swift, with their truth -// known. Ported from reference commit 9482831 Tests/RingFitCalibrationTests.swift (8 tests, same names, same plant, same bands), +// known. Ported from macTEM before v1.0.0 (history squashed) Tests/RingFitCalibrationTests.swift (8 tests, same names, same plant, same bands), // RUN ON THE KIT'S FIT, which is reference commit 7d6fdbd1's `EllipseCalibration` (ADR 069: two starts and the 0.30 px floor guard); // the reference implementation's own copy there is the pre-ADR-069 file. No number of these eight moved: every assertion holds as the reference implementation wrote it // (said they might, the measured values are in tools/slice-3b/README.md). diff --git a/Tests/DiffractoGeometryTests/RingIndexingTests.swift b/Tests/DiffractoGeometryTests/RingIndexingTests.swift index 04bd6a2..9e0e2b3 100644 --- a/Tests/DiffractoGeometryTests/RingIndexingTests.swift +++ b/Tests/DiffractoGeometryTests/RingIndexingTests.swift @@ -1,7 +1,7 @@ // // RingIndexingTests.swift // The indexing decision ("RingIndexing", "Truth"), on the kit's own ring -// lists (DiffractoScattering's `kinematicReflections(kMax: 1.2).families`). It ports every test of reference commit 9482831 +// lists (DiffractoScattering's `kinematicReflections(kMax: 1.2).families`). It ports every test of macTEM before v1.0.0 (history squashed) // `DSpacingIndexingTests` (28) and `MonoclinicIndexingTests` (13) that concerns the decision or the family lists, // re-expressed on the kit's rings; the ones that test what DiffractoCore and DiffractoScattering own are mapped to a comment at the end. // The dyadic lists are the kit's rings of an exactly-scaled orthorhombic cell (a 2, b 1, c 1/2 Å: g = h/2, k, 2 l, so diff --git a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift index 906a7e4..8d7c7cc 100644 --- a/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift +++ b/Tests/DiffractoKitParityTests/Geometry/RingCalibrationPinsTests.swift @@ -4,7 +4,7 @@ // pins tools/slice-3/ring-pins.py writes (Fixtures/slice-3-ring-pins.json): the closed forms evaluated in 60-digit decimal // arithmetic, 320 cases, the refusal decision from the documented thresholds (68 plants), the Bragg limit (18 plants) and the // radius (10 plants). Band 1e-7 relative (the reference's tolerance since 0.9.1; it was 1e-12); a difference is a failure of the port, never a band to widen. -// Ported from reference commit 9482831 tools/ring-calibration-parity/main.swift. The fixture is read by #filePath (the Fixtures folder +// Ported from macTEM before v1.0.0 (history squashed) tools/ring-calibration-parity/main.swift. The fixture is read by #filePath (the Fixtures folder // is excluded from the target). // diff --git a/tools/slice-2/README.md b/tools/slice-2/README.md index 6f2abe9..abf130a 100644 --- a/tools/slice-2/README.md +++ b/tools/slice-2/README.md @@ -11,7 +11,7 @@ tools/slice-2/apps-parity/run.sh # about 4 s - `pins.py` writes `Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json` (about 1.7 MB): the versions, the table's sha256 and the 103 x 10 table as `np.loadtxt` reads it, `f_e` at g2 in {0, 0.25, 1, 4} for Z = 1..103, 20 structures (the - apps' presets Au, Al, Si, Mg, WS2; Cu3Au-occ and Weak-cut from macTEM a58c84c; ideal hcp; the extra non-centrosymmetric + apps' presets Au, Al, Si, Mg, WS2; Cu3Au-occ and Weak-cut from macTEM before v1.0.0 (history squashed); ideal hcp; the extra non-centrosymmetric zincblende GaAs; beta'' and ten slice-1 CIF fixtures), each with every reflection above 1e-6 in |F|, the kept count, the rings after the cut, the ring counts on the operation-allowed and on all reflections, the operation-absent check; the mutation-moves; pymatgen's X-ray pattern per peak with the explicit half-space members; the diff --git a/tools/slice-2/fetch-table.sh b/tools/slice-2/fetch-table.sh index 9c57bea..4355d9b 100755 --- a/tools/slice-2/fetch-table.sh +++ b/tools/slice-2/fetch-table.sh @@ -1,5 +1,5 @@ #!/bin/zsh -# PROVENANCE: macTEM a58c84c tools/lib/fetch-py4dstem-scattering.sh adapted +# PROVENANCE: macTEM before v1.0.0 (history squashed) tools/lib/fetch-py4dstem-scattering.sh adapted # Adapted: the same py4DSTEM lock commit and the same sha256, the destination is this kit's gitignored References/ (the # script sits one level deeper than macTEM's, tools/slice-2/, and the root is still two levels up). # END PROVENANCE diff --git a/tools/slice-2/pins.py b/tools/slice-2/pins.py index 11ba329..8d41f73 100644 --- a/tools/slice-2/pins.py +++ b/tools/slice-2/pins.py @@ -360,7 +360,7 @@ def sites_of(z, positions, occ=1.0): def preset_structures(table): - """The apps' presets (Crystal.swift 7d6fdbd1) in the apps' site order, plus macTEM a58c84c's two disordered cases and + """The apps' presets (Crystal.swift 7d6fdbd1) in the apps' site order, plus the two disordered cases of macTEM before v1.0.0 and the ideal hcp. Returns [(name, cell, sites, note)].""" a_hcp = 3.2094 q = weak_cut_occupancy(table) @@ -392,7 +392,7 @@ def preset_structures(table): def weak_cut_occupancy(table): - """macTEM a58c84c: q solved so that the lowest odd-sum reflection (1 0 0), |g| = 1/3, has |F| = 2.5e-4: + """macTEM before v1.0.0 (history squashed): q solved so that the lowest odd-sum reflection (1 0 0), |g| = 1/3, has |F| = 2.5e-4: F = (1 - q) f_e(29, g^2) / V, V = 27.""" a = 3.0 return float(1 - 2.5e-4 * a ** 3 / float(f_e(table, 29, (1 / a) ** 2))) diff --git a/tools/slice-3/README.md b/tools/slice-3/README.md index aaaa858..8ebeab2 100644 --- a/tools/slice-3/README.md +++ b/tools/slice-3/README.md @@ -15,13 +15,13 @@ the Swift and the pins is a failure of the port, never a band to widen. (3 % ellipticity, either axis major); the literals of `RingCalibrationTests` recomputed in decimal (`truth`); the mutation list. - The oracle is 60-digit `decimal` (arcsin by its power series, tan 2 theta by the identity 2 s sqrt(1 - s^2) / (1 - 2 s^2), so no trigonometric library is trusted), with NumPy 2.5.4 routes beside it that must agree to 1e-14. It is written from the - definitions, ported from macTEM 9482831 `tools/ring-calibration-parity/reference.py` (read by `git show`). + definitions, ported from macTEM before v1.0.0 (history squashed) `tools/ring-calibration-parity/reference.py`. - Exit 0: every self-check holds. 1: a self-check failed. Includes: each mutation (small angle, q = 2/d, Angstrom read as nm, pitch in um, non-relativistic lambda, kV as V, V defaulted, radius = a) moves its target quantity by more than 1e-6, so a mutation that moves nothing cannot be mistaken for a proof. - `RingCalibrationPinsTests` (Swift Testing) reads the fixture by `#filePath` and holds every number to 1e-12 relative, every refusal by name and in order, `withholdsDeltaQ` as registered. `RingCalibrationTests` are the 30 hand-arithmetic tests of - macTEM 9482831, converted. + macTEM before v1.0.0 (history squashed), converted. - One literal to know: the typed L_px of Au {111}, R 150 px, 200 kV (14082.307097295722, macTEM's test and `RingCalibrationTests`) is one ulp (1.3e-16 relative) from the correctly rounded decimal result (14082.307097295723). It is far inside the 1e-12 band; the generator holds the typed literals to 1e-15 relative and says so. @@ -35,7 +35,7 @@ failure of the port, never a tolerance to widen. ../References/python/orix/bin/python -I -B tools/slice-3/index-pins.py # about 2 s, deterministic: writes slice-3-index-pins.json ``` -- `index-pins.py` ports macTEM 9482831 `tools/indexing-parity/reference.py` (read by `git show`) onto the KIT's ring lists: +- `index-pins.py` ports macTEM before v1.0.0 (history squashed) `tools/indexing-parity/reference.py` onto the KIT's ring lists: the families after the |F| cut that slice 2 pinned in `Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json` (so `tools/slice-2/pins.py` runs first), at k_max 1.2: Au, Al, Si, Mg, ideal hcp, Weak-cut (macTEM's six) and beta''. - The decision is NumPy written from the definition in `RingIndexing.swift`'s header, with a plain-Python scalar re-implementation that must agree on diff --git a/tools/slice-3/index-pins.py b/tools/slice-3/index-pins.py index 21e609a..f68534d 100644 --- a/tools/slice-3/index-pins.py +++ b/tools/slice-3/index-pins.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Pins for RingIndexing. -Port of macTEM 9482831 tools/indexing-parity/reference.py (read by `git show`) onto the KIT's own ring lists: the families +Port of macTEM before v1.0.0 (history squashed) tools/indexing-parity/reference.py onto the KIT's own ring lists: the families after the |F| cut that slice 2 pinned (Tests/DiffractoScatteringTests/Fixtures/slice-2-pins.json, written by tools/slice-2/pins.py), at k_max 1.2. The decision is written here from its definition in DiffractoGeometry/RingIndexing.swift's header (NumPy, plus a plain-Python scalar re-implementation that must agree on every plant), NOT from any Swift: diff --git a/tools/slice-3/ring-pins.py b/tools/slice-3/ring-pins.py index 154fb08..8ec425f 100644 --- a/tools/slice-3/ring-pins.py +++ b/tools/slice-3/ring-pins.py @@ -5,8 +5,8 @@ ../References/python/orix/bin/python -I -B tools/slice-3/ring-pins.py [out.json] It writes Tests/DiffractoGeometryTests/Fixtures/slice-3-ring-pins.json BEFORE the Swift tests read it. Deterministic: no -clock, no random numbers; two runs give the same bytes. Ported from macTEM 9482831 tools/ring-calibration-parity/ -reference.py (read by `git show`), which was written from the definitions and not from any Swift. +clock, no random numbers; two runs give the same bytes. Ported from macTEM before v1.0.0 (history squashed) tools/ring-calibration-parity/ +reference.py, which was written from the definitions and not from any Swift. Constants, CODATA 2018 (Tiesinga, Mohr, Newell, Taylor, Rev. Mod. Phys. 93, 025010 (2021)): h = 6.626 070 15e-34 J s exact (the SI 2019 definition) diff --git a/tools/slice-3b/README.md b/tools/slice-3b/README.md index 360d4b4..543354a 100644 --- a/tools/slice-3b/README.md +++ b/tools/slice-3b/README.md @@ -2,7 +2,7 @@ Seeds, from the apps' history before they were public: mac4DSTEM `7d6fdbd1` (`EllipseCalibration.swift`, `DiffractionPattern.swift`, `Calibration.swift`, the noisy, stop-rule and row-29 tests, the -`tools/ellipse-calibration-test` sweep) and macTEM `9482831` (`RingCalibration+Fit.swift`, its 8 tests). +`tools/ellipse-calibration-test` sweep) and macTEM before v1.0.0 (history squashed) (`RingCalibration+Fit.swift`, its 8 tests). - `apps-parity/run.sh` (+ `main.swift`): compiles the apps' own `EllipseCalibration.swift` and `DiffractionPattern.swift` at 7d6fdbd1 with `swiftc -O`, renders 35 planted rings (3 + 4 noisy, the 4 stop-rule rings, 20 clean sweep rings, 4 sparse rings) with the From f45049422423ee601d72dd1efc43569990c1037b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:58:30 +0000 Subject: [PATCH 12/17] docs: README, CITATION and fixture list match 0.10.0 README: install pin 0.10.0, CI and docs badges, the Metal path and the ImagoKit/Accelerate dependency, the module table up to 0.10.0 and two Known limits lines reworded. CITATION gains version, date-released and orix, and loses the ORCID TODO. The fixture README lists the orix misorientation pins and credits the Materials Project files; the stray .keep is gone. Part of #17 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CITATION.cff | 8 +++++++- README.md | 27 +++++++++++++++++---------- Tests/Fixtures/README.md | 9 +++++---- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 62bd4d6..0fe8be4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -6,7 +6,7 @@ body: id: version attributes: label: DiffractoKit version - placeholder: "0.4.1" + placeholder: "0.10.0" validations: required: true - type: input diff --git a/CITATION.cff b/CITATION.cff index ba0ae90..874bad9 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,7 +6,8 @@ abstract: "A Swift library for crystallography and electron diffraction: unit ce authors: - family-names: Lobpreis given-names: Paul - # TODO(Paul): add orcid: "https://orcid.org/…" and affiliation: "…" here +version: 0.10.0 +date-released: 2026-10-22 license: GPL-3.0 repository-code: "https://github.com/mac4DSTEM/DiffractoKit" keywords: @@ -34,3 +35,8 @@ references: authors: - name: "The pyxem developers" url: "https://github.com/pyxem/pyxem" + - type: software + title: "orix" + authors: + - name: "The pyxem developers" + url: "https://github.com/pyxem/orix" diff --git a/README.md b/README.md index 12a5b99..f8d732b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ # DiffractoKit +[![CI](https://github.com/mac4DSTEM/DiffractoKit/actions/workflows/ci.yml/badge.svg)](https://github.com/mac4DSTEM/DiffractoKit/actions/workflows/ci.yml) +[![Docs](https://github.com/mac4DSTEM/DiffractoKit/actions/workflows/docs.yml/badge.svg)](https://mac4dstem.github.io/DiffractoKit/) + Crystallography and electron diffraction as a Swift package: the numbers a scientist reads, never UI. DiffractoKit is the Swift counterpart of the crystallography parts of **pymatgen**, **py4DSTEM** and **pyxem**: the unit cell and its reciprocal lattice, symmetry found from the atoms, CIF import, d-spacings and the list of reflections with systematic @@ -9,8 +12,10 @@ absences, electron scattering factors and kinematic structure factors, rings and diffraction pattern (wavelength, camera length, reciprocal-space pixel size, the ring and ellipse fit). It is built to be imported by tagged version into applications. -Swift 6 strict concurrency, `Foundation`, `simd` and `CryptoKit` (no Python). Headless, one SwiftPM product per module. -Where a path is accelerated (none yet), it will keep a CPU reference path with a stated tolerance. +Swift 6 strict concurrency on `Foundation`, `simd`, `CryptoKit`, Metal (the ACOM matcher's optional GPU path) and +[ImagoKit](https://github.com/mac4DSTEM/ImagoKit) (its FFT, on Accelerate); no Python. Headless, one SwiftPM product per +module. The one accelerated path, `ACOMMatcher.matchAll(..., backend: .metal)`, keeps the CPU path as its reference and is +checked against it. **Status: 0.x** ([semantic versioning](https://semver.org): a minor release may change the API, and every release has a `CHANGELOG.md` entry with a migration note where needed). Pin an exact tag and bump on purpose. @@ -23,17 +28,17 @@ One SwiftPM product per module; a module imports only the ones above it in this | Module | Holds | Since | |---|---|---| -| `DiffractoCore` | `UnitCell` (all seven crystal systems: real and reciprocal lattice, metric, volume, d and g of an hkl, the angle between planes), `SymmetryOperation` and `PointSymmetry` (the eleven Laue classes, `LaueClass`, `CrystalSystem`), symmetry found from the atom sites (`SymmetryDetection`, `CrystalStructure`), `CIFImport` (CIF text to `CrystalStructure`, with named refusals), `ReflectionList` (every hkl up to a k_max with its systematic-absence flag), orbits and families (rings) with their names | 0.1.0; Niggli reduction, the conventional cell and a Cartesian symmetry tolerance since 0.1.1 | +| `DiffractoCore` | `UnitCell` (all seven crystal systems: real and reciprocal lattice, metric, volume, d and g of an hkl, the angle between planes), `SymmetryOperation` and `PointSymmetry` (the eleven Laue classes, `LaueClass`, `CrystalSystem`), symmetry found from the atom sites (`SymmetryDetection`, `CrystalStructure`), `CIFImport` (CIF text to `CrystalStructure`, with named refusals), `ReflectionList` (every hkl up to a k_max with its systematic-absence flag), orbits and families (rings) with their names, `CrystalPresets`, the Materials Project import | 0.1.0; Niggli reduction, the conventional cell and a Cartesian symmetry tolerance 0.1.1; presets and Materials Project import 0.6.0 | | `DiffractoScattering` | `ElectronScatteringFactors` (Lobato and Van Dyck, the table of py4DSTEM, Z = 1 to 103), `StructureFactors` (F(hkl) in A^-2), `CrystalStructure.kinematicReflections(kMax:)`: the reflections after an \|F\| cut, and their rings with the summed \|F\|^2 | 0.1.0 | -| `DiffractoGeometry` | `ElectronWavelength` (relativistic, CODATA 2018), `RingCalibration` (camera length, reciprocal-space pixel size from one ring of known d, with the refusals that say when a ring must not be calibrated from; `fit` runs the whole chain on a pattern), `EllipseCalibration` and `EllipseTransform` (the ring and detector-ellipse fit, py4DSTEM's model), `RingIndexing` (which family a measured \|q\| belongs to, and when it must not be named), `ZoneAxis` (beam directions, the detector basis, excitation errors) | 0.1.0 | -| `DiffractoOrientation` | Phase reference libraries and vector matching, Euler angles and orientation-result values; `CrystalOrientationSymmetry` (minimum-angle reduction for all 11 Laue classes), direction sampling and explicit `.py4DSTEM` / `.native` IPF colour keys | phase mapping and orientation results 0.3.0; orientation symmetry and the ACOM matcher 0.4.0; [conventions](docs/orientation-symmetry.md), parity P17–P19 | +| `DiffractoGeometry` | `ElectronWavelength` (relativistic, CODATA 2018), `RingCalibration` (camera length, reciprocal-space pixel size from one ring of known d, with the refusals that say when a ring must not be calibrated from; `fit` runs the whole chain on a pattern), `EllipseCalibration` and `EllipseTransform` (the ring and detector-ellipse fit, py4DSTEM's model), `RingIndexing` (which family a measured \|q\| belongs to, and when it must not be named), `ZoneAxis` (beam directions, the detector basis, excitation errors), `LinearScale` (delta q from rings: estimate, assign, fit), single-pattern measurements (`PatternCentre`, `PatternRadialProfile`, `PatternSpot`) | 0.1.0; `LinearScale.estimate` 0.8.0; `LinearScale.assign` and `fit` and the pattern measurements 0.10.0 | +| `DiffractoOrientation` | Phase reference libraries and vector matching, Euler angles and orientation-result values; `CrystalOrientationSymmetry` (minimum-angle reduction for all 11 Laue classes), direction sampling and explicit `.py4DSTEM` / `.native` / `.orix` IPF colour keys, the ACOM matcher and scan (CPU and Metal), `OrientationMapAnalysis`, `PhaseModel`, `Misorientation` and `Orientation`, orientation-relationship variants | phase mapping and orientation results 0.3.0; orientation symmetry and the ACOM matcher 0.4.0; ACOM scans 0.5.0; the orix key, disorientation, `PhaseModel` and the Metal backend 0.6.0; map analysis 0.8.1; misorientation and variants 0.10.0; [conventions](docs/orientation-symmetry.md) | ## Install ```swift // Package.swift dependencies: [ - .package(url: "https://github.com/mac4DSTEM/DiffractoKit", exact: "0.4.1"), + .package(url: "https://github.com/mac4DSTEM/DiffractoKit", exact: "0.10.0"), ], targets: [ .target(name: "MyApp", dependencies: [ @@ -44,8 +49,9 @@ targets: [ ] ``` -The package needs macOS 15 or iOS 18 and a Swift 6.2 toolchain (`swift-tools-version: 6.2`). Its only dependency is -[ImagoKit](https://github.com/mac4DSTEM/ImagoKit), used by `DiffractoOrientation` alone (its `FFT1D`). It carries no unsafe +The package needs macOS 15 or iOS 18 and a Swift 6.2 toolchain (`swift-tools-version: 6.2`). Its only package dependency is +[ImagoKit](https://github.com/mac4DSTEM/ImagoKit), used by `DiffractoOrientation` alone (its `FFT1D`, on Accelerate); +the system frameworks are Foundation, simd, CryptoKit and, for the optional GPU matcher, Metal. It carries no unsafe flags: your build compiles it optimised itself; a Debug build of the reflection loops and of the ring fit is slow. `DiffractoGeometry` depends on the other two, but each file that names a type of a module should import that module itself (`MemberImportVisibility` is on in this package). @@ -249,9 +255,10 @@ and ported from: pymatgen, spglib, py4DSTEM and, for the orientation work, pyxem - Nothing here is validated on a real diffraction pattern; the ring-fit evidence is measured on synthetic plants only. - No database CIF (COD, ICSD) is read yet. -- theta'-Al2Cu in the slice 4 pins is a test structure (Al4Cu2 body-centred, a 4.04 A, c 5.80 A), not checked against a published CIF. +- theta'-Al2Cu in the orientation pins is a test structure (Al4Cu2 body-centred, a 4.04 A, c 5.80 A), not checked against a published CIF. - With py4DSTEM's patterns the ACOM matcher finds 17 of 18 planted orientations within 4 degrees; one 2/m pattern matches another beam direction. -- `testRinglessFitOnARealMeanPatternFallsBackToTheConic` is disabled by name until the owner's Ni65Cu35 mean pattern has a manifest line. +- One real-data test (`testRinglessFitOnARealMeanPatternFallsBackToTheConic`, a Ni65Cu35 mean pattern) is disabled until that + dataset is published with the tests. - Policies not yet parameters (profile-fit plausibility bounds, cell volume 1e-6) remain. - The open list, worst first, with causes: `docs/open-items.md`; timings against Python: `docs/benchmarks.md`. diff --git a/Tests/Fixtures/README.md b/Tests/Fixtures/README.md index 2ca6cfd..45ad0ad 100644 --- a/Tests/Fixtures/README.md +++ b/Tests/Fixtures/README.md @@ -1,7 +1,7 @@ # Test fixtures 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. 55 files. +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. 56 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*. No fixture is a copy of a real dataset: the committed text fixtures carry no local path or personal name (checked by search), and real datasets are read from `$MACEM_DATA` and never copied into the repo. @@ -24,9 +24,9 @@ Licence: every fixture is generated from this kit's own code and the pinned Pyth | `DiffractoCoreTests/Fixtures/ideal_hcp_P1.cif` | 504 B | `tools/slice-1/pins.py` | | | `DiffractoCoreTests/Fixtures/magnesium_P63mmc.cif` | 401 B | `tools/slice-4/pins.py` | | | `DiffractoCoreTests/Fixtures/magnesium_P63mmc_24ops.cif` | 896 B | `tools/slice-1/pins.py` | | -| `DiffractoCoreTests/Fixtures/mp-import/input-aluminium-v3.json` | 1.6 KB | `tools/mp-import/pins.py` | third party: Materials Project export; licence terms to be confirmed by Paul | -| `DiffractoCoreTests/Fixtures/mp-import/input-magnesiumJSON.json` | 1.1 KB | `tools/mp-import/pins.py` | third party: Materials Project export; licence terms to be confirmed by Paul | -| `DiffractoCoreTests/Fixtures/mp-import/input-siliconJSON.json` | 2.9 KB | `tools/mp-import/pins.py` | third party: Materials Project export; licence terms to be confirmed by Paul | +| `DiffractoCoreTests/Fixtures/mp-import/input-aluminium-v3.json` | 1.6 KB | `tools/mp-import/pins.py` | third party: Materials Project data, CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), source https://materialsproject.org; credited in `NOTICE` | +| `DiffractoCoreTests/Fixtures/mp-import/input-magnesiumJSON.json` | 1.1 KB | `tools/mp-import/pins.py` | third party: Materials Project data, CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), source https://materialsproject.org; credited in `NOTICE` | +| `DiffractoCoreTests/Fixtures/mp-import/input-siliconJSON.json` | 2.9 KB | `tools/mp-import/pins.py` | third party: Materials Project data, CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), source https://materialsproject.org; credited in `NOTICE` | | `DiffractoCoreTests/Fixtures/mp-import/pymatgen-pins.json` | 6.3 KB | `tools/mp-import/pins.py` | | | `DiffractoCoreTests/Fixtures/presets-pins.json` | 21.1 KB | `tools/presets/pins.py` | | | `DiffractoCoreTests/Fixtures/pyrite_Pa-3.cif` | 1.1 KB | `tools/slice-1/pins.py`, `tools/slice-2/pins.py` | | @@ -60,6 +60,7 @@ Licence: every fixture is generated from this kit's own code and the pinned Pyth | `DiffractoOrientationTests/Fixtures/orix-direction-pins.json` | 1.5 MB | `tools/orix/direction-pins.py` | | | `DiffractoOrientationTests/Fixtures/orix-disorientation-pins.json` | 1.5 MB | `tools/orix/disorientation-pins.py` | | | `DiffractoOrientationTests/Fixtures/orix-ipf-pins.json` | 484.0 KB | `tools/orix/ipf-pins.py` | | +| `DiffractoOrientationTests/Fixtures/orix-misorientation-pins.json` | 896.0 KB | `tools/orix/misorientation-pins.py` | | | `DiffractoOrientationTests/Fixtures/py4dstem-family-pins.json` | 1005.7 KB | `tools/symmetry/family-pins.py` | | | `DiffractoOrientationTests/Fixtures/py4dstem-family-real-pins.json` | 3.7 MB | `tools/symmetry/family-real-pins.py` | | | `DiffractoOrientationTests/Fixtures/slice-4-apps.json` | 12.9 MB | `tools/slice-4/apps-parity/main.swift`, `tools/slice-4/apps-parity/run.sh` | | From 9f19b4c957aae49ad0c8a4b43d5d29381721fbc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:59:47 +0000 Subject: [PATCH 13/17] docs: join two dangling doc-comment fragments (SwiftLint comment_spacing) Part of #16 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- Sources/DiffractoCore/CrystalStructure.swift | 3 +-- Tests/DiffractoCoreTests/CIFImportTests.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/DiffractoCore/CrystalStructure.swift b/Sources/DiffractoCore/CrystalStructure.swift index 2719b22..72fc8af 100644 --- a/Sources/DiffractoCore/CrystalStructure.swift +++ b/Sources/DiffractoCore/CrystalStructure.swift @@ -32,8 +32,7 @@ public struct CrystalStructure: Sendable, Equatable { case operationsGiven /// Found from the sites by the holohedry test. case detected - /// A file's loop expanded the asymmetric unit, then the operations were detected from the expanded cell - ///; the loop is `fileOperations`. + /// A file's loop expanded the asymmetric unit, then the operations were detected from the expanded cell; the loop is `fileOperations`. case detectedAfterFileExpansion } diff --git a/Tests/DiffractoCoreTests/CIFImportTests.swift b/Tests/DiffractoCoreTests/CIFImportTests.swift index 4af1dbd..31401c9 100644 --- a/Tests/DiffractoCoreTests/CIFImportTests.swift +++ b/Tests/DiffractoCoreTests/CIFImportTests.swift @@ -112,8 +112,7 @@ struct CIFImportTests { } /// One atom and the four centring translations: a legal loop that declares less than the atoms have (Laue -1 by - /// the loop, m-3m by the cell it builds). The structure's operations are detected from the expanded cell - ///; the loop is kept and is a subset of them. + /// the loop, m-3m by the cell it builds). The structure's operations are detected from the expanded cell; the loop is kept and is a subset of them. @Test func testGoldWithCentringOperationsExpandsToFourSitesAndDetectsFm3m() throws { let doc = try read("gold_centring_ops.cif") let s = doc.structure From 3b986bfb2189011ac719b8378dab910754c9199a Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Thu, 29 Oct 2026 02:01:23 +0100 Subject: [PATCH 14/17] Licence: GPL-3.0-only for DiffractoKit's own lines Co-Authored-By: Claude Sonnet 5.5 --- CITATION.cff | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- Tests/Fixtures/README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 874bad9..d4e6cd1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,7 +8,7 @@ authors: given-names: Paul version: 0.10.0 date-released: 2026-10-22 -license: GPL-3.0 +license: GPL-3.0-only repository-code: "https://github.com/mac4DSTEM/DiffractoKit" keywords: - crystallography diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5211ab..fe83f5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ together at the next fixture change. - Tests use Swift Testing (`import Testing`, `@Test`, `#expect`). - Public API only: new types and functions are `public`, and the separate package in `tools/consumer-check` must still build. - Add a line to `CHANGELOG.md` for anything a user can see. -- By contributing you agree that your contribution is licensed under GPL-3.0, like the rest of the library. +- By contributing you agree that your contribution is licensed under GPL-3.0-only, like the rest of the library. ## Reporting issues diff --git a/README.md b/README.md index f8d732b..ff311ab 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ under "Oracles and pins" above, and `tools/*/pins.py` are the generators. ## Licence -GPL-3.0 (`LICENSE`, `NOTICE`). The code is ported from earlier GPL-3.0 applications by the same author (named, with their commits, in `NOTICE`), whose +GPL-3.0-only (`LICENSE`, `NOTICE`). The code is ported from earlier GPL-3.0 applications by the same author (named, with their commits, in `NOTICE`), whose lattice and reflection conventions follow py4DSTEM's `Crystal` class (GPL-3.0): the real-lattice matrix with a along x and b in the xy plane, reciprocal vectors without 2 pi, the per-axis index bound. The electron scattering table of `DiffractoScattering` is py4DSTEM's (Lobato and Van Dyck 2014), also GPL-3.0. **pymatgen** (MIT) is an oracle: cells, diff --git a/Tests/Fixtures/README.md b/Tests/Fixtures/README.md index 45ad0ad..400aabc 100644 --- a/Tests/Fixtures/README.md +++ b/Tests/Fixtures/README.md @@ -3,7 +3,7 @@ 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. 56 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*. No fixture is a copy of a real dataset: the committed text fixtures carry no local path or personal 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-only licence, except the rows marked *third party*. No fixture is a copy of a real dataset: the committed text fixtures carry no local path or personal name (checked by search), and real datasets are read from `$MACEM_DATA` and never copied into the repo. ## Third party From 6c93965e5fedb3666c2949258942a12239b01aeb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 01:27:33 +0000 Subject: [PATCH 15/17] docs: NOTICE grants GPL-3.0-only, matching the licence id Part of #17 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VkKZEWxuR53HJvmXUenLwd --- NOTICE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NOTICE b/NOTICE index 957c4d6..aceff3a 100644 --- a/NOTICE +++ b/NOTICE @@ -3,8 +3,7 @@ Copyright (C) 2026 Paul Lobpreis This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version (LICENSE). +Foundation, version 3 only (GPL-3.0-only, LICENSE). This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A From aa157ab272f4a2e0ceb97eb8c2a4e6a681187f16 Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Thu, 29 Oct 2026 23:15:45 +0100 Subject: [PATCH 16/17] docs: CHANGELOG headings in release-please form, with dates, compare links and 0.0.3 Every version heading is `## [x.y.z](compare link) (date)`, the form release-please writes from 0.11.0 on, so the file reads as one list. 0.3.0, 0.1.1 and 0.1.0 get their tag dates, 0.0.3 gets a pre-release entry, and the "archived" lines and internal ask and task numbers are gone. The CI tag check's regex already reads both heading forms (`^## \[?v?x.y.z`), so ci.yml is unchanged; it reads 0.10.0 here. Done: the CHANGELOG bullet of #17. Next: Paul merges #28. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 55 +++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92121df..106c22b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog All notable changes to DiffractoKit. Versions are semantic (0.x: a minor release may change the API). The numbers each module -reproduces, and against what, are in `docs/parity.md`. +reproduces, and against what, are in `docs/parity.md`. From 0.11.0 on, release-please writes each entry from the +conventional-commit titles of the merged pull requests. The entries up to 0.10.0 use Keep a Changelog's sections (Added, Changed, Fixed, Removed, Migration). -## 0.10.0 (2026-10-22) — misorientation, orientation-relationship variants, ring-sequence assignment and the apps' kit asks +## [0.10.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.9.1...0.10.0) (2026-10-22) -ROADMAP task 35 and the asks of both apps' open items. Additive except one case in an error enum (see Migration). +Misorientation, orientation-relationship variants, ring-sequence assignment, and what the consuming applications asked of the kit. Additive except one case in an error enum (see Migration). ### Added - **Misorientation and orientation** (`DiffractoOrientation`, orix 0.15.0): `Misorientation(matrix, source:, target:)` with `equivalent(grainExchange:)` and `reduce()` / @@ -18,12 +19,12 @@ ROADMAP task 35 and the asks of both apps' open items. Additive except one case (planted truth; no Python reference computes the set). - **`LinearScale.assign`** (and `estimateAssigned`, `LinearScaleAssignment`, `LinearScaleAssignmentError`): which of the crystal's first shells is the first observed ring, from the sequence of rings, with named refusals; Al [001] reads {200} and delta q is exact (the old assumption of {111} read 13.4 % low). Moved from mac4DSTEM's `ShellAssignment`. -- **`LinearScale.fit`** (`LinearScaleFit`): delta q from several indexed rings by least squares with a standard error (macTEM ask 2). +- **`LinearScale.fit`** (`LinearScaleFit`): delta q from several indexed rings by least squares with a standard error. - **Single-pattern measurements:** `PatternCentre.find` (the unscattered beam's centre), `PatternRadialProfile.about` (ellipse-corrected radial profile), `PatternSpot.refine` (subpixel - spot centre); macTEM asks 1, 3 and 9. Planted truth, `validation: "none"`. + spot centre). Asked for by macTEM. Planted truth, `validation: "none"`. - `ReflectionFamily` has a public initialiser. `OrientationSymmetryError.differentSymmetry`. -### Diagnosed +### Changed: diagnosed, no number changed - The {111} ring fit reading 0.1 % low (macTEM open item): the cause is the tail of the neighbouring {200} ring inside the annulus, which the one-ring profile model absorbs. A single ring is exact to 3e-8 px; the bias is -0.041 px for the annulus 29 to 37, -0.003 px for 29 to 36 and -3e-6 px for 29 to 35, and linear in the neighbour's amplitude (`RingFitNeighbourTailTests`). No number changed: end the annulus before the neighbour's tail. @@ -34,7 +35,7 @@ ROADMAP task 35 and the asks of both apps' open items. Additive except one case `OrientationSymmetryError` has a new case, `differentSymmetry`: a `switch` over it that has no `default` must handle it. Nothing else changes for a consumer. mac4DSTEM can delete `ShellAssignment` (marked `delete at DiffractoKit 0.10.x`) and call `LinearScale.assign`; macTEM can delete its `refineSpot` interim. -## 0.9.1 (2026-10-21) +## [0.9.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.9.0...0.9.1) (2026-10-21) Tests, docs and one speed fix. No number, default or public symbol changes. @@ -46,7 +47,7 @@ Tests, docs and one speed fix. No number, default or public symbol changes. file: 67 ms (median), the 0.9.0 table's 351 ms. Still about 27 times pymatgen's parser, which does not detect symmetry; the rest is in `docs/open-items.md`, which now lists `cif_import` first with the measured cause. -### Tests (D17) +### Changed: tests - The 33 parity rows marked [S] (pins stricter than upstream): 31 loosened, 2 lost only the marker (see below). Loosened to the upstream tolerance stated in each row: rtol 1e-7 (with a 1e-12 floor for exact zeros) where upstream states none, `tol` 1e-5 for the Niggli reduction, orix's allclose (atol 1e-8, rtol 1e-5) for the orix rows, `symprec` 0.01 angstrom (as a fraction of the longest edge) for operation translations, @@ -56,7 +57,7 @@ Tests, docs and one speed fix. No number, default or public symbol changes. TrigonalSettingTests. No fixture moved. Two rows were not stricter and lost the marker only (P11's SciPy optimum; the P25 spot bound is [L], float32 upstream). Closure, orthogonality and the kit's own invariance checks keep their tight bounds. -### Docs +### Changed: docs - `docs/parity.md`: no [S] left; the three 0.9.0 numerics have rows (opt-in in-plane parabola refinement: partly, the vertex formula is py4DSTEM's but is applied to the kit's own correlation; the mirror flag and pattern-angle planes: partly, no Python counterpart; `minimumMatrixEvidence`: partly, no Python counterpart), the Summary counts follow (148 rows, 58 partly). @@ -68,7 +69,7 @@ Tests, docs and one speed fix. No number, default or public symbol changes. ### Migration None. -## 0.9.0 (2026-10-21) +## [0.9.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.8.1...0.9.0) (2026-10-21) The kit audit: the three diagnose-first items (phase matching, ACOM mirror and in-plane angle, CIF symmetry) diagnosed with the experiment named in each test, the parity table and the benchmark table rewritten against the Python sources on disk. @@ -93,7 +94,7 @@ import and a missed refusal into one. - A primitive C-centred monoclinic cell with beta = 90 (two right angles, like the conventional cell) was refused with its correct 4-operation list; the listed rotations (not diagonal in a primitive basis) now decide. -### Diagnosed, nothing changed by default (the experiments are tests) +### Changed: diagnosed, nothing changed by default (the experiments are tests) - Phase matching: `minimumVectors` = 2 is the paper's rule (one or no survivor is the matrix) and the survivor count cannot tell "the matrix explained everything" from "nothing was there" (`minimumMatrixEvidence` above). The completeness-aware cross-phase ranking loses at small noise (23 wrong of 400 against 0 at sigma 0.003 A^-1) and wins only near the pair radius (20 against @@ -110,7 +111,7 @@ Source-compatible except: (1) `CIFImportError` gains a case, so an exhaustive `s `ACOMMatcher(library:refinesInPlaneAngle: true)` for the scan. Phase maps: set `minimumMatrixEvidence = 1` to stop vacuum and stray-peak positions counting as matrix once a labelled scan has cleared it. -## 0.8.1 (2026-10-21) +## [0.8.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.8.0...0.8.1) (2026-10-21) ### Added - `OrientationMapAnalysis` in `DiffractoOrientation`: `eulerText`, `reliabilityThreshold(percentile:)`, `fractionOfMatched`, the per-position planes @@ -122,7 +123,7 @@ positions counting as matrix once a labelled scan has cleared it. mac4DSTEM (`OrientationMap`): forward its methods to `OrientationMapAnalysis` on `results`, wrapping the planes in `FloatImage` / `RGBAImage`. Not moved: the learned detector (Paul's decision), `ACOMCrystalSymmetry`'s test-only unreduced fallback; the open phase-matching, ACOM-mirror and CIF-symmetry items are diagnose-first. -## 0.8.0 (2026-10-21) +## [0.8.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.7.1...0.8.0) (2026-10-21) The one numeric piece mac4DSTEM still carried that belongs here. No existing number changes. @@ -139,11 +140,11 @@ mac4DSTEM: build `radiiPerPosition` from its Bragg vectors (distance of each pea Not moved, by design: `ACOMCrystalSymmetry` / `OrientationMap` / `MetalACOMMatcher` (app, by the app's own design); the open phase-matching, ACOM-mirror and CIF-symmetry items are diagnose-first behaviour questions, not missing code. -## 0.7.1 (2026-10-21) +## [0.7.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.7.0...0.7.1) (2026-10-21) The ImagoKit dependency accepts `"0.2.0"..<"1.0.0"` (was 0.2.x), so a consumer can take ImagoKit 0.4.0. Only `FFT1D` is used; the ACOM, phase and orientation tests pass against ImagoKit 0.4.0. -## 0.7.0 (2026-10-21) +## [0.7.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.6.0...0.7.0) (2026-10-21) The ACOM asks of the apps' kit switch: what mac4DSTEM still carries a copy of around `ACOMMatcher` and `ACOMTemplateLibrary`. No number changes: every addition is checked against the existing CPU route (`match`, `matchAll(peaks:)`), which stays the reference. @@ -170,7 +171,7 @@ number changes: every addition is checked against the existing CPU route (`match - `ACOMError` has a new case `cancelled`: an exhaustive `switch` over it needs one more branch. - The generic `match`/`matchAll` overloads accept everything the array forms did; no call site changes. -## 0.6.0 (2026-10-20) +## [0.6.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.5.0...0.6.0) (2026-10-20) Orientation symmetry beyond the three app classes, pinned to orix; the py4DSTEM direction families; the preset crystals, the Materials Project models and the phase model that the two apps carry. @@ -246,7 +247,7 @@ Materials Project models and the phase model that the two apps carry. `PhaseModel.orientationClass`; the apps' `.identity` is `nil`, and the kit also reduces tetragonal, orthorhombic, cubic m-3, trigonal and 6/m cells that the apps called unreduced). -## 0.5.0 (2026-10-20) +## [0.5.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.4.1...0.5.0) (2026-10-20) ACOM matching over a whole scan, so the apps can drop their own scan loop (mac4DSTEM `OrientationMatching.matchAll`, CPU path). @@ -268,7 +269,7 @@ mapping (a Bragg-vector lattice fit with no crystal in it; it belongs to CuboKit Additive: a consumer of 0.4.1 recompiles unchanged. mac4DSTEM replaces its CPU `matchAll` loop and `ACOMScanSelection` with the kit's; the Metal path and the `OrientationMap` presentation stay. -## 0.4.1 (2026-10-16) +## [0.4.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.4.0...0.4.1) (2026-10-16) Dependency range only; no API, source or number changes. @@ -277,7 +278,7 @@ Dependency range only; no API, source or number changes. ### Migration None for consumers of 0.4.0 beyond re-pinning to 0.4.1. A consumer may now use any ImagoKit 0.2.x. -## 0.4.0 (2026-10-16) +## [0.4.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.3.1...0.4.0) (2026-10-16) Orientation symmetry for all eleven Laue classes, the ACOM matcher, the Materials Project cell import, and helpers for application front ends (crystal loading, ring overlay, indexing display, ring scale). @@ -314,12 +315,12 @@ non-lattice rotations) is included in this tag. patterns; one 2/m pattern returns a different beam direction (127 degrees). - Dependency: ImagoKit 0.2.0 (exact), for `FFT1D` (ImagoFFT), used by DiffractoOrientation only. -### Conventions +### Changed: conventions - py4DSTEM's direction-family reduction is not ported (returns families, not a rotation). Neither the minimum-angle rotation nor the native direction fold substitutes for Python's family array. See `docs/orientation-symmetry.md`. - Additive API; no orientation-map container added. -## 0.3.1 (2026-10-15) +## [0.3.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.3.0...0.3.1) (2026-10-15) ### Fixed - Symmetry detection: a CIF whose operation loop lists rotations that are not symmetries of the cell's lattice (the 48 signed @@ -329,7 +330,7 @@ non-lattice rotations) is included in this tag. class and the atoms decide it. Reflections are unchanged on every pinned structure; `SymmetryDetection.Result.candidates` no longer lists such a rotation. - Not a change: hcp written in a cell with γ = 60° is read as 6/mmm (it is hcp); 0.1.0's 2/m was the metric not being recognised. -## 0.3.0 +## [0.3.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.1.1...0.3.0) (2026-10-15) One release for two steps (there is no separate 0.2.0 tag): the generality pass, where every policy is a parameter with the previous value as its documented default, the platform floor is lowered and the docs stand alone (the default path is bit @@ -400,7 +401,7 @@ identical to 0.1.1 on 31,725 outputs); and vector-matched phase mapping with the and loosening coverage needs both `RingCalibration.Policy.minimumOccupiedAngularBins` and the ellipse policy's `degeneracyBoundBins`; removing the `ReflectionList.Policy` caps removes the memory and time protection (a k_max of 100 reached 10 GB). Error texts quote the default bounds. A kinematic pattern cannot tell +c from −c on a polar structure: no consumer may -claim a polarity from a kinematic match. Earlier per-release migration notes are archived (see the 0.1.0 entry) and in git history. +claim a polarity from a kinematic match. **Phase mapping.** An application that carried its own vector matching can delete it when it adopts the kit's. For code moving from such an implementation: `Crystal` becomes `CrystalStructure` (`PhaseDefinition.crystal`); a structure the kit refuses throws `PhaseReferenceLibrary.Failure.structureRefused`; `BraggVectors` becomes `scan:scanWidth:scanHeight:` with @@ -410,15 +411,17 @@ throws; a scan whose position count is not width × height gives nil (or `[]`) f `matrixEntryIndex` makes `map` return nil; a cancelled `build` throws `CancellationError`. The decision rule of `.knownVariants` and its two constants follow Thronsen et al., Ultramicroscopy 255 (2024) 113861 (CC BY 4.0; NOTICE). No other number moves. -## 0.1.1 +## [0.1.1](https://github.com/mac4DSTEM/DiffractoKit/compare/0.1.0...0.1.1) (2026-10-15) Niggli reduction, the conventional cell in both settings, the Cartesian symmetry tolerance, detection on a cell list; the CI type-check fixes. -## 0.1.0 +## [0.1.0](https://github.com/mac4DSTEM/DiffractoKit/compare/0.0.3...0.1.0) (2026-10-15) Cells of the seven systems, symmetry from the atoms, CIF import, reflections, absences, orbits and rings; electron scattering factors and structure factors; the wavelength, ring and ellipse calibration, camera length, indexing and zone axes; error texts; progress and cancellation of the fits. -Consumer notes for 0.0.1 to 0.3.0 are archived. +## [0.0.3](https://github.com/mac4DSTEM/DiffractoKit/releases/tag/0.0.3) (2026-10-15) + +Pre-releases (0.0.1 to 0.0.3): the first cells, reflections, scattering factors and ring fits, before the API settled. See the git history. From 73764e876578cf6793478897b3c605d337dc7ac1 Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Thu, 29 Oct 2026 23:16:06 +0100 Subject: [PATCH 17/17] ci: release-please bumps the CITATION version and the README install pin Both lines carry an x-release-please-version annotation and the files are release-please extra-files, so the Release PR moves them with the manifest. CITATION's date-released stays a hand edit in the Release PR. Done: the CITATION 'bumped with every tag' bullet of #17. Next: Paul merges #28. Co-Authored-By: Claude Opus 5.5 --- CITATION.cff | 2 +- README.md | 2 +- release-please-config.json | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index d4e6cd1..5831a4c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,7 +6,7 @@ abstract: "A Swift library for crystallography and electron diffraction: unit ce authors: - family-names: Lobpreis given-names: Paul -version: 0.10.0 +version: 0.10.0 # x-release-please-version date-released: 2026-10-22 license: GPL-3.0-only repository-code: "https://github.com/mac4DSTEM/DiffractoKit" diff --git a/README.md b/README.md index ff311ab..790e399 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ One SwiftPM product per module; a module imports only the ones above it in this ```swift // Package.swift dependencies: [ - .package(url: "https://github.com/mac4DSTEM/DiffractoKit", exact: "0.10.0"), + .package(url: "https://github.com/mac4DSTEM/DiffractoKit", exact: "0.10.0"), // x-release-please-version ], targets: [ .target(name: "MyApp", dependencies: [ diff --git a/release-please-config.json b/release-please-config.json index f4d6bc8..a12a8ef 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,7 +7,11 @@ "packages": { ".": { "package-name": "DiffractoKit", - "changelog-path": "CHANGELOG.md" + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "CITATION.cff", + "README.md" + ] } } }