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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Sources/FormatoCore/AcquisitionMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public nonisolated struct AcquisitionMetadata: Sendable, Equatable {
/// The instrument, as the file names it.
public var instrument: MetadataReading<String>?
/// The operating mode as the file states it: a name ("STEM", "TEM · DIFFRACTION") for Velox and DM; the stored
/// integer for MRC, labelled unvalidated (no pinned reference decodes it).
/// integer for MRC, with a note that no published reader decodes it.
public var mode: MetadataReading<String>?

// MARK: Added in 0.3.1 (additive). Read by the `.rosettaSciIO` convention only, where RosettaSciIO 049e7d70 maps the key
Expand Down Expand Up @@ -120,7 +120,7 @@ public nonisolated struct AcquisitionMetadata: Sendable, Equatable {
/// RosettaSciIO uses it as the axis scale, not as metadata, so the `note` says what the file does not settle.
public var pixelSize: MetadataReading<PixelSize>?
/// Convergence semi-angle in mrad. A Velox EELS spectrum image (`velox(eelsSpectrumImage:)`, `.rosettaSciIO` only) also fills
/// it, labelled unvalidated: no published reader reads the stored value.
/// it, with a note that no published reader reads the stored value.
public var convergenceAngleMrad: MetadataReading<Double>?

// MARK: Added after 0.11.0 (additive). Read by the DM reader from an image object (`AcquisitionMetadata.dm(_:convention:)`), and
Expand All @@ -132,7 +132,7 @@ public nonisolated struct AcquisitionMetadata: Sendable, Equatable {
/// is the first and `rows` the second, units "um" as the tag names them; the note says so when the two differ. Not the
/// binned frame pixel (`Acquisition.Frame.CCD.Pixel Size (um)`). Nil when the tag is absent, does not hold exactly two
/// numbers, or holds one that is not finite and positive. RosettaSciIO maps the tag for a CL signal only (its
/// `Detector.pixel_size`, also in `signalNumbers`); on any other signal `.rosettaSciIO` labels it `unvalidatedNote`. The
/// `Detector.pixel_size`, also in `signalNumbers`); on any other signal `.rosettaSciIO` notes that no published reader reads it. The
/// `.app` convention reads it too (an extension of the consumer's rule set, which has no pitch), unlabelled.
public var cameraPixelPitch: MetadataReading<PixelSize>?

Expand Down Expand Up @@ -206,14 +206,18 @@ public nonisolated struct AcquisitionMetadata: Sendable, Equatable {
///
/// `.rosettaSciIO` (the default) follows RosettaSciIO at the pinned commit for every field RosettaSciIO maps: its tag
/// choice, its mode tests and its unit arithmetic, with no positivity rule (a stored zero is 0, not nil). A field
/// RosettaSciIO does not map is read by the consumer's rule and carries the note ``AcquisitionMetadata/unvalidatedNote``.
/// RosettaSciIO does not map is read by the consumer's rule and carries the format's wording of
/// ``AcquisitionMetadata/noPublishedReaderNote``.
/// `.app` is the consumer's own rule set (a to g above), pinned to its source; `docs/parity.md` lists the differences.
public nonisolated enum AcquisitionMetadataConvention: Sendable, Equatable {
case rosettaSciIO
case app
}

extension AcquisitionMetadata {
/// The note of a value no RosettaSciIO mapping reads.
public static let unvalidatedNote = "unvalidated: no published reader reads it"
/// The note of a value no RosettaSciIO mapping reads: it is read as the file stores it. Each format's reader attaches
/// its own wording of this note, naming where in the file the value is stored.
public static let noPublishedReaderNote = "no published reader reads this field; read as the file stores it"
/// The same note under its older name.
public static let unvalidatedNote = noPublishedReaderNote
}
11 changes: 6 additions & 5 deletions Sources/FormatoDM/AcquisitionMetadataDM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
// the dwell with DigiScan tags, else the exposure; X-ray: neither; CL: the DigiScan sample time). The EELS frame number,
// aperture and spectrometer, the X-ray (EDS) and CL groups and the SI group are read into `signalNumbers`/`signalTexts` under
// RosettaSciIO's own path names, and the date and time take the last source that parses (DataBar, SI, then the signal group; for CL DataBar again, as the mapping ends).
// Fields RosettaSciIO does not map (the detector) are read by the consumer's rule with `unvalidatedNote`. The
// Fields RosettaSciIO does not map (the detector) are read by the consumer's rule with `DMNote.unvalidated`. The
// signal-type mappings (EELS, X-ray, CL, SI) are applied from the tag maps (`Meta Data.Signal`), not from a signal object.
//
// `.app` is the consumer's rule set (AcquisitionMetadata.swift, rules a to g), pinned to its source:
Expand Down Expand Up @@ -139,7 +139,8 @@ extension AcquisitionMetadata {
nonisolated enum DMNote {
static let cameraLengthUnit = "mm (assumed)"
static let monthFirst = "month-first order assumed"
static let unvalidated = "unvalidated: no published reader reads it"
/// This format's wording of `noPublishedReaderNote`: the value is read as the DM image tags store it.
static let unvalidated = "no published reader reads this field; read as stored in the DM image tags"
}

/// The two ImageTags maps with the stack-builder fallback: the direct key, else the same key under
Expand Down Expand Up @@ -267,10 +268,10 @@ extension AcquisitionMetadata {
private static let dmCreationPrefix = "source.Tags at creation."

/// `Acquisition.Device.CCD.Pixel Size (um)` as a pitch: exactly two finite positive numbers (x, y), else nil. `unvalidated`
/// adds `unvalidatedNote` (`.rosettaSciIO` on a signal RosettaSciIO does not map the tag for).
/// adds `DMNote.unvalidated` (`.rosettaSciIO` on a signal RosettaSciIO does not map the tag for).
private static func dmCameraPitch(_ stored: [Double]?, unvalidated: Bool = false) -> MetadataReading<PixelSize>? {
guard let stored, stored.count == 2, stored.allSatisfy({ $0.isFinite && $0 > 0 }) else { return nil }
let notes = [stored[0] == stored[1] ? nil : unequalPitchNote, unvalidated ? unvalidatedNote : nil].compactMap { $0 }
let notes = [stored[0] == stored[1] ? nil : unequalPitchNote, unvalidated ? DMNote.unvalidated : nil].compactMap { $0 }
return MetadataReading(PixelSize(rows: stored[1], columns: stored[0], units: "um"),
note: notes.isEmpty ? nil : notes.joined(separator: "; "))
}
Expand Down Expand Up @@ -463,7 +464,7 @@ extension AcquisitionMetadata {
}

// RosettaSciIO maps no detector: the camera name as stored, labelled.
if let name = text(string("Acquisition.Device.Name")) { meta.detector = MetadataReading(name, note: unvalidatedNote) }
if let name = text(string("Acquisition.Device.Name")) { meta.detector = MetadataReading(name, note: DMNote.unvalidated) }
// The camera pitch, every signal type: the tag as stored. RosettaSciIO maps it for CL only (`Detector.pixel_size` above),
// so on any other signal it is the consumer's rule, labelled.
meta.cameraPixelPitch = dmCameraPitch(arrays[prefix + dmDevicePitchKey], unvalidated: signalType != "CL")
Expand Down
21 changes: 11 additions & 10 deletions Sources/FormatoEMD/AcquisitionMetadataVelox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// field: `Optics.AccelerationVoltage` / 1e3 (kV), `Optics.CameraLength` * 1e3 (mm, in every mode),
// `CustomProperties.StemMagnification.value` as the magnification (Velox stores an unset bool as 0; it is returned as
// stored); no positivity rule. RosettaSciIO maps no detector, dwell, exposure, mode or date for an image; those are read
// by the consumer's rule with `unvalidatedNote`: the detector `BinaryResult.Detector`, the instrument
// by the consumer's rule with `veloxUnvalidated` (no published reader; read as stored): the detector `BinaryResult.Detector`, the instrument
// `Instrument.InstrumentModel` (`.app`; under `.rosettaSciIO` it is `instrumentModel`, and `instrument` is RosettaSciIO's `InstrumentClass`), the dwell `Scan.DwellTime` only for a
// ScanningDetector, the exposure of the detector the image names (`VeloxMetadata.exposureTimeSeconds`), the magnification
// `Optics.NominalMagnification` when no `StemMagnification` is stored, the mode `Optics.OperatingMode` as the stored
Expand All @@ -18,7 +18,7 @@
// the note says it is not a metadata mapping) under `.rosettaSciIO`, and `velox(spectrumImage:)`, the same fields for the metadata
// of a spectrum image (`VeloxEMDReader.spectrumImageMetadata()`). `velox(eelsSpectrumImage:)` reads the same fields from one EELS
// spectrum image's metadata (`VeloxEMDReader.eelsSpectrumImageMetadata()`) plus the collection and convergence semi-angles, where
// RosettaSciIO maps nothing (the collection angle as pyEELSMODEL reads it; the convergence angle labelled unvalidated).
// RosettaSciIO maps nothing (the collection angle as pyEELSMODEL reads it; the convergence angle noted as read by no published reader).
//
// `.app` is the consumer's reading of the same entry, pinned to its source: high tension, detector, instrument, start
// date and dwell only; magnification, camera length, exposure and mode are `unread` (the consumer's reader predated the
Expand All @@ -44,7 +44,7 @@ nonisolated extension AcquisitionMetadata {
if let stem = m.stemMagnification {
out.magnification = MetadataReading(stem)
} else if let nominal = positive(m.nominalMagnification) {
out.magnification = MetadataReading(nominal, note: unvalidatedNote)
out.magnification = MetadataReading(nominal, note: veloxUnvalidated)
}
// The microscope is `Instrument.InstrumentClass` (`_get_mapping`, :947-950), as stored; the stored model is kept beside it.
out.instrument = m.instrumentClass.map { MetadataReading($0) } // as stored, as RosettaSciIO passes it
Expand All @@ -66,9 +66,9 @@ nonisolated extension AcquisitionMetadata {
if let unitY = text(m.pixelUnitY), unitY != unit { note += "; PixelUnitY is '\(unitY)', RosettaSciIO uses PixelUnitX for both" }
out.pixelSize = MetadataReading(PixelSize(rows: size.height, columns: size.width, units: unit), note: note)
}
if let seconds = positive(m.exposureTimeSeconds) { out.exposureS = MetadataReading(seconds, note: unvalidatedNote) }
if let seconds = positive(m.exposureTimeSeconds) { out.exposureS = MetadataReading(seconds, note: veloxUnvalidated) }
if let mode = text(m.operatingMode) {
out.mode = MetadataReading(mode, note: "the stored code, not decoded; " + unvalidatedNote)
out.mode = MetadataReading(mode, note: "the stored code, not decoded; " + veloxUnvalidated)
}
return out
}
Expand All @@ -87,7 +87,7 @@ nonisolated extension AcquisitionMetadata {
/// `velox(spectrumImage:)`, among them the beam energy `Optics.AccelerationVoltage` / 1e3 (kV, RosettaSciIO's mapping for
/// the EDS signal of the same file; pyEELSMODEL's `emd_reader` reads the same volts as E0), and the two angles in mrad:
/// the collection semi-angle `EnergyFilter.CollectionAngleRange.End` × 1e3 (pyEELSMODEL's beta, radians as stored) and
/// the convergence semi-angle `Optics.BeamConvergence` × 1e3 (no published reader reads it: unvalidated). An angle that is
/// the convergence semi-angle `Optics.BeamConvergence` × 1e3 (no published reader reads it; noted so). An angle that is
/// not a finite positive number is nil (pyEELSMODEL passes a stored 0 through as beta; here 0 is no collection angle). The
/// angles are read under `.rosettaSciIO` only; `.app` gives what `velox(spectrumImage:)` gives.
public static func velox(eelsSpectrumImage metadata: VeloxMetadata, timeZone: TimeZone = .current,
Expand Down Expand Up @@ -126,10 +126,11 @@ nonisolated extension AcquisitionMetadata {
return out
}

static let veloxUnvalidated = "unvalidated: no published reader reads it"
/// The registered clause failed (2026-10-13, velox-image-parity): RosettaSciIO's title comes from the file's
/// Operations, not `BinaryResult.Detector`, and differs on 7 of 33 public and owner files ("Ceta" for "BM-Ceta").
static let veloxDetectorNote = "unvalidated: RosettaSciIO names some detectors from another field"
/// This format's wording of `noPublishedReaderNote`: the value is read as the Velox metadata stores it.
static let veloxUnvalidated = "no published reader reads this field; read as stored in the Velox metadata"
/// RosettaSciIO's title comes from the file's Operations, not `BinaryResult.Detector`, and differs on 7 of 33 public
/// and owner files ("Ceta" for "BM-Ceta").
static let veloxDetectorNote = "read from BinaryResult.Detector; RosettaSciIO names some detectors from another field"

/// Unix seconds as `YYYY-MM-DD HH:MM:SS` on `timeZone`'s clock (whole seconds, truncated as RosettaSciIO's
/// `isoformat` is); nil when not finite or out of the four-digit years.
Expand Down
6 changes: 3 additions & 3 deletions Sources/FormatoMRC/AcquisitionMetadataMRC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
// STEM Detector name, else Camera name, else Detector commercial name (FEI2). DEVIATION from the first sketch
// (detector: STEM Detector name, else Camera name): the third source is needed because 4DSTEMscan.mrc names its
// detector only there ("Ceta-S speed"). Exposure = "Integration time" (the layout has no field called exposure;
// this is the camera field in seconds) — unvalidated and labelled so (`unvalidatedNote`): mrcfile declares the
// this is the camera field in seconds) — labelled `unvalidatedNote` (no published reader; read as stored): mrcfile declares the
// field (f8 at offset 419, mask 2 bit 16) and the harness plants and compares it, but no pinned reader treats it
// as an exposure and no real file records a positive value.
//
Expand Down Expand Up @@ -195,10 +195,10 @@ nonisolated struct FEIRecord {
static let fei1Size = 768
static let fei2Size = 888
/// The label of a value whose path no pinned reference reads (exposure, mode).
static let unvalidatedNote = "unvalidated: no published reader reads it"
static let unvalidatedNote = "no published reader reads this field; read as stored in the FEI extended header, at the offset mrcfile declares"
/// RULE (c), open deviation: the label of a camera length shown without a mode test (Instrument mode and
/// Projection mode are enumerations no published reader decodes).
static let modeUncheckedNote = "unvalidated: the mode is not checked"
static let modeUncheckedNote = "shown as stored; the instrument mode is not checked, as no published reader decodes it"

/// A field: its byte offset in the record and the (mask, bit) that says it was written. Masks are 0...3 for
/// Bitmask 1...4; their offsets are `maskOffsets`.
Expand Down
Loading
Loading