From cab7595a0d2d966aaef8f284d14a813d6461c2bb Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Fri, 30 Oct 2026 11:08:59 +0100 Subject: [PATCH] feat!: Sample.elements, the four metadata groups and the segment times of Velox single spectra VeloxSpectrumSignals maps Sample.elements as HyperSpy sets it when it applies RosettaSciIO's _convert_element_list (the first of DictionaryTreeBrowser's sorted, slugified keys; atomic_number2name 1 to 119), parses Displays, Operations, SharedProperties and Features as read_file does, and refuses the read wherever RosettaSciIO or HyperSpy raises (int(version) included). FormatoValue gains a .texts([String]) case for the list. VeloxEMDReader.spectrumMetadata() and VeloxMetadata.binaryResultSegmentTimes give each spectrum's live and real times as Velox 4 to 13 store them in Detectors. FormatoLoader.load keeps the images of a file whose spectra are refused. Pinned by tools/velox-elements/pins.py (planted and public). Migration: an exhaustive switch over FormatoValue needs a .texts arm (or default). Done: issue 10 item 2 (single spectra). Next: item 3 (partial PixelSize/Offset, detector name, Ceta date) and item 4 (camera pixel pitch). Co-Authored-By: Claude Opus 5.5 --- Sources/Formato/Formato.swift | 3 +- .../FormatoCore.docc/The data model.md | 2 +- Sources/FormatoCore/FormatoMetadata.swift | 3 + Sources/FormatoEMD/VeloxEMDReader.swift | 25 + Sources/FormatoEMD/VeloxMetadata.swift | 13 + Sources/FormatoEMD/VeloxSampleElements.swift | 120 ++ Sources/FormatoEMD/VeloxSpectrumSignals.swift | 25 +- Sources/FormatoEMD/VeloxTitles.swift | 21 +- Sources/FormatoMSA/FormatoMSA.swift | 2 + .../Fixtures/elements/el-bad-json.emd | Bin 0 -> 21864 bytes .../Fixtures/elements/el-bad-number.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-digit-string.emd | Bin 0 -> 20464 bytes .../Fixtures/elements/el-empty-group.emd | Bin 0 -> 18832 bytes .../Fixtures/elements/el-empty-selection.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-empty-string.emd | Bin 0 -> 20464 bytes .../Fixtures/elements/el-entry-not-object.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-no-version-key.emd | Bin 0 -> 18832 bytes .../Fixtures/elements/el-none.emd | Bin 0 -> 18832 bytes .../Fixtures/elements/el-null-selection.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-settings-list.emd | Bin 0 -> 18832 bytes .../Fixtures/elements/el-slug-order.emd | Bin 0 -> 19976 bytes .../elements/el-v11-operations-only.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-v11-two-settings.emd | Bin 0 -> 27808 bytes .../Fixtures/elements/el-v13-disabled.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-v4.emd | Bin 0 -> 25096 bytes .../Fixtures/elements/el-v6-one-segment.emd | Bin 0 -> 20464 bytes .../Fixtures/elements/el-version-text.emd | Bin 0 -> 18832 bytes .../Fixtures/elements/el-z-0.emd | Bin 0 -> 19432 bytes .../Fixtures/elements/el-z-120.emd | Bin 0 -> 19432 bytes .../elements/velox-elements-pins.json | 1560 +++++++++++++++++ .../VeloxSpectrumElementsTests.swift | 151 ++ .../VeloxSpectrumSignalsTests.swift | 3 +- docs/open-items.md | 2 +- docs/parity.md | 17 +- tools/velox-elements/pins.py | 241 +++ 35 files changed, 2157 insertions(+), 31 deletions(-) create mode 100644 Sources/FormatoEMD/VeloxSampleElements.swift create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-bad-json.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-bad-number.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-digit-string.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-empty-group.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-empty-selection.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-empty-string.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-entry-not-object.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-no-version-key.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-none.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-null-selection.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-settings-list.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-slug-order.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-v11-operations-only.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-v11-two-settings.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-v13-disabled.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-v4.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-v6-one-segment.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-version-text.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-z-0.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/el-z-120.emd create mode 100644 Tests/FormatoEMDTests/Fixtures/elements/velox-elements-pins.json create mode 100644 Tests/FormatoEMDTests/VeloxSpectrumElementsTests.swift create mode 100644 tools/velox-elements/pins.py diff --git a/Sources/Formato/Formato.swift b/Sources/Formato/Formato.swift index 0a6410c..d995087 100644 --- a/Sources/Formato/Formato.swift +++ b/Sources/Formato/Formato.swift @@ -87,7 +87,8 @@ public nonisolated enum FormatoLoader { func memory(_ s: FormatoSignal) -> Result { .success(FormatoLazySignal(title: s.title, signalType: s.signalType, axes: s.axes, metadata: s.metadata, array: FormatoInMemoryArray(s.data))) } - out += try VeloxSpectrumSignals(path: path).signals.map(memory) + // A refused spectrum (an element selection HyperSpy rejects, say) is its failure; the images still load. + do { out += try VeloxSpectrumSignals(path: path).signals.map(memory) } catch { out.append(.failure(error)) } let images = try VeloxImageSignals(path: path) out += images.images.map { image in Result { try images.signal(image) } } do { out += try VeloxSpectrumImageSignals(path: path).signals.map(memory) } catch { out.append(.failure(error)) } diff --git a/Sources/FormatoCore/FormatoCore.docc/The data model.md b/Sources/FormatoCore/FormatoCore.docc/The data model.md index e8bdd96..aa203fc 100644 --- a/Sources/FormatoCore/FormatoCore.docc/The data model.md +++ b/Sources/FormatoCore/FormatoCore.docc/The data model.md @@ -28,7 +28,7 @@ Two arrays are equal when their type, shape and bytes are equal. That equality i ``FormatoMetadata`` is an ordered list of ``FormatoMetadataEntry`` values. Each entry has: - a key, under RosettaSciIO's metadata name; -- a value, a number or a text (``FormatoValue``); +- a value: a number, a text, a flag or a list of texts (``FormatoValue``); - an optional unit; - a ``FormatoProvenance`` that names the file, the place in the file the value came from, and the rule that turned it into the entry. diff --git a/Sources/FormatoCore/FormatoMetadata.swift b/Sources/FormatoCore/FormatoMetadata.swift index 0a5be6f..f101a4e 100644 --- a/Sources/FormatoCore/FormatoMetadata.swift +++ b/Sources/FormatoCore/FormatoMetadata.swift @@ -11,9 +11,12 @@ public nonisolated enum FormatoValue: Sendable, Equatable { case number(Double) case text(String) case flag(Bool) + /// A list of strings, in order (RosettaSciIO's `Sample.elements` of a Velox single spectrum). + case texts([String]) public var number: Double? { if case .number(let v) = self { return v }; return nil } public var text: String? { if case .text(let v) = self { return v }; return nil } + public var texts: [String]? { if case .texts(let v) = self { return v }; return nil } } /// Where a metadata value came from. diff --git a/Sources/FormatoEMD/VeloxEMDReader.swift b/Sources/FormatoEMD/VeloxEMDReader.swift index d1a7eb8..481fdaf 100644 --- a/Sources/FormatoEMD/VeloxEMDReader.swift +++ b/Sources/FormatoEMD/VeloxEMDReader.swift @@ -1014,6 +1014,31 @@ public nonisolated struct VeloxEMDReader: Sendable { } } + // MARK: Single spectra + + /// The metadata of each single spectrum, column 0 of `Data/Spectrum//Metadata`, in the order `VeloxSpectrumSignals` returns the + /// spectra (empty where it returns none: no `Data/Spectrum`, or `Data/Image` without `Data/SpectrumImage`). The live and real time + /// of the segments a spectrum sums are `binaryResultSegmentTimes`; they are not in the signals' metadata, which stays RosettaSciIO's. + /// Throws `missingMetadata` for a spectrum without a Metadata JSON object (RosettaSciIO raises there too), and as `images()` does + /// for a damaged file. + public func spectrumMetadata(cancelled: @Sendable () -> Bool = FormatoCancellation.task) throws -> [VeloxMetadata] { + try HDF5Serial.run { + try FormatoCancellation.check(cancelled) + let file = try Self.openVeloxFile(path: path) + if file.exists("/Data/Image") && !file.exists("/Data/SpectrumImage") { return [] } + return try file.children(of: "/Data/Spectrum").map { id in + try FormatoCancellation.check(cancelled) + let group = "/Data/Spectrum/" + id + guard file.exists(group + "/Metadata") else { throw VeloxEMDError.missingMetadata("Data/Spectrum/\(id)/Metadata") } + let json = try file.readMetadataJSON(group: group, column: 0) + guard let metadata = try? VeloxMetadata.parse(jsonData: json) else { + throw VeloxEMDError.missingMetadata("a JSON object in Data/Spectrum/\(id)/Metadata") + } + return metadata + } + } + } + // MARK: Stored element selection /// Symbols by atomic number minus one (H to Lr), the same table rsciio's `atomic_number2name` spells for 1...103. diff --git a/Sources/FormatoEMD/VeloxMetadata.swift b/Sources/FormatoEMD/VeloxMetadata.swift index 0506780..48482aa 100644 --- a/Sources/FormatoEMD/VeloxMetadata.swift +++ b/Sources/FormatoEMD/VeloxMetadata.swift @@ -183,6 +183,19 @@ public nonisolated struct VeloxMetadata: Sendable { public var alphaDegrees: Double? { stageAlphaRadians.map { $0 * 180 / .pi } } public var betaDegrees: Double? { stageBetaRadians.map { $0 * 180 / .pi } } + /// The live and real time (s) of each X-ray segment that is not disabled and whose `DetectorName` contains `BinaryResult.Detector` + /// (the name match of RosettaSciIO's energy axis, `_get_dispersion_offset`), in the file's order, as stored. For a single spectrum + /// (`VeloxEMDReader.spectrumMetadata()`): RosettaSciIO's public `fei_emd_spectrum.emd` (Velox 4) names "SuperXG2", all four + /// segments, each stating the same times; a Velox 6 spectrum names one ("SuperXG24"). Unlike the energy axis, every matching + /// `AnalyticalDetector` entry is kept, not the first. Velox 4 to 13 store them as `Detectors.Detector-.LiveTime` / `.RealTime`; RosettaSciIO maps only + /// `DetectorMetadata.LiveTime` / `.RealTime`, which no public single spectrum has. Empty when `BinaryResult.Detector` is absent. + public var binaryResultSegmentTimes: [VeloxSegmentTimes] { + guard let named = binaryResultDetector else { return [] } + return superXDetectors.filter { $0.enabled != false && (named.isEmpty || $0.name.contains(named)) }.map { + VeloxSegmentTimes(name: $0.name, liveTimeSeconds: $0.liveTimeColumn0, realTimeSeconds: $0.realTimeColumn0) + } + } + /// The grid `ScanSize x ScanArea` implies, `(ny, nx)`; nil unless both are /// integers to within 1/100 of a pixel (the Velox rasters are exact binary fractions). public var derivedGrid: (ny: Int, nx: Int)? { diff --git a/Sources/FormatoEMD/VeloxSampleElements.swift b/Sources/FormatoEMD/VeloxSampleElements.swift new file mode 100644 index 0000000..11fd644 --- /dev/null +++ b/Sources/FormatoEMD/VeloxSampleElements.swift @@ -0,0 +1,120 @@ +// +// VeloxSampleElements.swift +// Role: The four file-level metadata groups of a Velox file as RosettaSciIO 049e7d70's `read_file` parses them +// (`_parse_metadata_group`), and `Sample.elements` as RosettaSciIO's mapping gives it once HyperSpy applies it +// (`_get_mapping(map_selected_element=True)` with `_convert_element_list`, through `hyperspy/io.py` `dict2signal`). +// `_convert_element_list` indexes `d.keys()[0]`, which only HyperSpy's `DictionaryTreeBrowser` allows: its `keys()` is the +// sorted list of the slugified keys (`slugify(key, valid_variable_name=True)`), so the entry taken is the first in that order. +// Wherever Python raises (a key missing, a value without `.keys()`, `int()` refusing, an atomic number outside 1...119), the +// read is refused, as HyperSpy's `load` raises. Pinned by tools/velox-elements/pins.py. +// + +import Foundation +import FormatoCore + +/// The children of `Displays`, `Operations`, `SharedProperties` and `Features`, as `_parse_metadata_group` reads them. +nonisolated enum VeloxFileGroups { + /// A child of one of the four groups: a group's datasets by name (as `{name: json}`), or one dataset's JSON. + struct Entry { let path: String; let isGroup: Bool; let value: Any } + + static let names = ["Displays", "Operations", "SharedProperties", "Features"] + + /// The groups the file has, each child parsed. Must run inside `HDF5Serial.run`. Throws `refuse(reason)` where RosettaSciIO raises: + /// a group stored as a dataset, an entry that is not one variable-length string of JSON. (An empty dataset, where RosettaSciIO + /// logs a warning and keeps what it read, is refused here too: the reader cannot tell it from a damaged one.) + static func read(file: VeloxFile, refuse: (String) -> FormatoReadError) throws -> [String: [String: Entry]] { + func json(_ path: String) throws -> Any { + guard let text = file.readString(path) else { throw refuse("\(path) is not one variable-length string") } + guard let value = try? JSONSerialization.jsonObject(with: Data(text.utf8), options: .fragmentsAllowed) else { + throw refuse("\(path) is not JSON") + } + return value + } + func isDataset(_ path: String) -> Bool { (try? file.open(path)).map { file.close($0); return true } ?? false } + var groups: [String: [String: Entry]] = [:] + for name in names where file.exists("/" + name) { + // `_parse_metadata_group` on a dataset: `_get_keys_from_group` raises AttributeError. + guard !isDataset("/" + name) else { throw refuse("/\(name) is a dataset, not a group") } + var entries: [String: Entry] = [:] + for child in file.children(of: "/" + name) { + let path = "/\(name)/\(child)" + if isDataset(path) { + entries[child] = Entry(path: path, isGroup: false, value: try json(path)) + } else { + var sub: [String: Any] = [:] + for leaf in file.children(of: path) { sub[leaf] = try json(path + "/" + leaf) } + entries[child] = Entry(path: path, isGroup: true, value: sub) + } + } + groups[name] = entries + } + return groups + } +} + +/// `Sample.elements` of a Velox single spectrum. +package nonisolated enum VeloxSampleElements { + /// RosettaSciIO's `atomic_number2name` (`rsciio/utils/_elements.py`), 1 to 119, its spellings kept (112 is "Cp", 113 to 119 the + /// placeholder names). + static let symbols = ("H He Li Be B C N O F Ne Na Mg Al Si P S Cl Ar K Ca Sc Ti V Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr " + + "Rb Sr Y Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I Xe Cs Ba La Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu Hf Ta W Re Os Ir Pt Au Hg " + + "Tl Pb Bi Po At Rn Fr Ra Ac Th Pa U Np Pu Am Cm Bk Cf Es Fm Md No Lr Rf Db Sg Bh Hs Mt Ds Rg Cp Uut Uuq Uup Uuh Uus Uuo Uue") + .split(separator: " ").map(String.init) + + /// HyperSpy's `slugify(value, valid_variable_name=True)`: NFKD, non-ASCII dropped, ASCII punctuation dropped (not "_" or " "), + /// stripped, spaces to "_", and "Number_" before a result that is not an identifier. + package static func slugify(_ key: String) -> String { + let ascii = key.decomposedStringWithCompatibilityMapping.unicodeScalars.filter(\.isASCII) + let kept = ascii.filter { $0 == "_" || $0 == " " || CharacterSet.alphanumerics.contains($0) } + var text = String(String.UnicodeScalarView(kept)) + text = text.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: " ", with: "_") + let isIdentifier = text.first.map { $0 == "_" || $0.isLetter } == true + return isIdentifier ? text : "Number_" + text + } + + /// The mapping's source key for this file's version: `int(self.version)` >= 11 reads `SharedProperties`, else `Operations`. + static func settingsPath(version: Int) -> (group: String, entry: String) { + version >= 11 ? ("SharedProperties", "EDSSpectrumQuantificationSettings") : ("Operations", "ImageQuantificationOperation") + } + + /// `_convert_element_list` on what HyperSpy's `get_item` returns for the settings path. `value` is the parsed entry (a dict of + /// the group's datasets, or one dataset's JSON). nil is never returned: Python returns a list or raises (`refuse`). + static func convert(_ value: Any, path: String, refuse: (String) -> FormatoReadError) throws -> (elements: [String], source: String) { + // `d.keys()[0]`: only a dict has `keys()`; HyperSpy sorts the slugified keys and leaves out those starting with "_". Keys + // that slugify alike are one attribute whose values HyperSpy merges: refused here (no file has them). + guard let settings = value as? [String: Any] else { throw refuse("\(path) has no keys() (it is not a group of entries)") } + var bySlug: [String: String] = [:] + for key in settings.keys { + let slug = slugify(key) + guard bySlug.updateValue(key, forKey: slug) == nil else { throw refuse("\(path) has two entries that read as \(slug) (unsupported here)") } + } + guard let first = bySlug.keys.filter({ !$0.hasPrefix("_") }).sorted().first, let key = bySlug[first], let entry = settings[key] else { + throw refuse("\(path) has no entry (keys()[0])") + } + guard let inner = entry as? [String: Any] else { throw refuse("\(path)/\(key) has no keys() (it is not a JSON object)") } + let source = "\(path)/\(key).elementSelection" + // "elementSelection" in `_d.keys()`, absent: no element selected, []. + let matches = inner.keys.filter { slugify($0) == "elementSelection" } + guard !matches.isEmpty else { return ([], source) } + guard matches.count == 1, let selection = inner[matches[0]] else { + throw refuse("\(path)/\(key) has several keys that read as elementSelection (unsupported here)") + } + // `for atomic_number in atomic_number_list`: a list, a string (its characters), an empty object; anything else raises. + let items: [Any] + switch selection { + case let list as [Any]: items = list + case let text as String: items = text.unicodeScalars.map { String($0) } // Python iterates code points + // A dict iterates over its keys that do not slugify to "_..." (none: []); any such key gives a tuple, which int() refuses. + case let object as [String: Any] where object.keys.allSatisfy({ slugify($0).hasPrefix("_") }): items = [] + default: throw refuse("\(source) is not a list or a string") + } + var out: [String] = [] + for item in items { + if item is [Any] || item is [String: Any] { throw refuse("\(source) holds a list or an object (int() raises)") } + guard let z = pythonIntValue(item) else { throw refuse("\(source): int() refuses \(item)") } + guard z >= 1, z <= symbols.count else { throw refuse("\(source): \(z) is not in atomic_number2name") } + out.append(symbols[z - 1]) + } + return (out, source) + } +} diff --git a/Sources/FormatoEMD/VeloxSpectrumSignals.swift b/Sources/FormatoEMD/VeloxSpectrumSignals.swift index e1725fa..9d03748 100644 --- a/Sources/FormatoEMD/VeloxSpectrumSignals.swift +++ b/Sources/FormatoEMD/VeloxSpectrumSignals.swift @@ -7,9 +7,10 @@ // and the mapping HyperSpy applies to `original_metadata` (`_get_mapping`) for every key the spectrum's metadata holds. // RosettaSciIO reads spectra only from a file whose `Data` has no `Image` group (type `Spectrum`) or has both `Image` and // `SpectrumImage` (type `SpectrumStream`); in any other file it returns none, and so does this. -// Eager: a spectrum is a few thousand counts. Registered deviations (the SI path's, docs/parity.md): the four metadata -// groups (`Displays`, `Operations`, `SharedProperties`, `Features`) are not parsed, so a malformed entry there does not -// refuse the read; `Sample.elements` (the element selection) is not mapped. +// Eager: a spectrum is a few thousand counts. The four metadata groups (`Displays`, `Operations`, `SharedProperties`, +// `Features`) are parsed as `read_file` parses them (a malformed entry refuses the read), and `Sample.elements` is mapped as +// HyperSpy applies RosettaSciIO's `_convert_element_list` (VeloxSampleElements.swift). Registered deviation (docs/parity.md): +// `_parse_image_display`, which RosettaSciIO runs for a SpectrumStream file before reading its spectra, is not run here. // import Foundation @@ -60,6 +61,9 @@ private nonisolated struct VeloxSpectrumRead { guard let versionText = file.readString("/Version"), let version = (try? JSONSerialization.jsonObject(with: Data(versionText.utf8))) as? [String: Any], veloxAcceptsFormat(version["format"] as? String) else { throw VeloxEMDError.notVeloxFile(fileName) } + // `read_file`: `_parse_json(f["Version"][0])["version"]` (a KeyError without it), then the four metadata groups. + guard let versionValue = version["version"] else { throw raises("its Version has no version key") } + let groups = try VeloxFileGroups.read(file: file) { raises($0) } guard file.exists("/Data"), !isDataset("/Data") else { throw raises("no Data group") } // `_check_im_type`: Image + SpectrumImage is a stream, Image alone is images, no Image is a spectrum. let hasImage = file.exists("/Data/Image"), hasSpectrumImage = file.exists("/Data/SpectrumImage") @@ -69,12 +73,12 @@ private nonisolated struct VeloxSpectrumRead { var out: [FormatoSignal] = [] for key in file.children(of: "/Data/Spectrum") { try FormatoCancellation.check(cancelled) - out.append(try spectrum(key)) + out.append(try spectrum(key, version: versionValue, groups: groups)) } return out } - func spectrum(_ key: String) throws -> FormatoSignal { + func spectrum(_ key: String, version: Any, groups: [String: [String: VeloxFileGroups.Entry]]) throws -> FormatoSignal { let group = "/Data/Spectrum/" + key guard file.exists(group + "/Data") else { throw raises("\(group) has no Data") } guard file.exists(group + "/Metadata") else { throw raises("\(group) has no Metadata") } @@ -108,7 +112,16 @@ private nonisolated struct VeloxSpectrumRead { axes.append(FormatoAxis(name: "Energy", size: data.bins, scale: energy.scale, offset: energy.offset, units: "keV", navigate: false)) let shape = data.stack > 1 ? [data.stack, data.bins] : [data.bins] let array = try FormatoArray(dataType: data.type, shape: shape, bytes: data.bytes) - let md = try metadata(om: om, source: source) + var md = try metadata(om: om, source: source) + // `_get_mapping()`: `int(self.version)` picks the settings group; `original_metadata.update(self.original_metadata)` put the + // four groups beside the spectrum's own keys, so the mapping finds the group's entries there. + guard let versionNumber = pythonIntValue(version) else { throw raises("int() refuses its Version's version") } + let (groupName, entryName) = VeloxSampleElements.settingsPath(version: versionNumber) + if let settings = groups[groupName]?[entryName] { + let (elements, elementsSource) = try VeloxSampleElements.convert(settings.value, path: String(settings.path.dropFirst())) { raises($0) } + md.set(entry("Sample.elements", .texts(elements), source: elementsSource, + rule: "rsciio emd velox mapping _convert_element_list, as HyperSpy applies it: the first of keys() (sorted), atomic_number2name[int(z)]")) + } return FormatoSignal(title: md["General.title"]?.value.text ?? "EDS", signalType: "EDS_TEM", axes: axes, metadata: md, data: array) } diff --git a/Sources/FormatoEMD/VeloxTitles.swift b/Sources/FormatoEMD/VeloxTitles.swift index be38b26..39b061e 100644 --- a/Sources/FormatoEMD/VeloxTitles.swift +++ b/Sources/FormatoEMD/VeloxTitles.swift @@ -23,8 +23,7 @@ nonisolated struct VeloxTitles: Sendable { let byImage: [String: Title] private struct Fallback: Error {} // Python's KeyError inside `_get_detector_name` - /// A child of one of the four file-level groups: a group's datasets by name (as `{name: json}`), or one dataset's JSON. - private struct Entry { let path: String; let isGroup: Bool; let value: Any } + private typealias Entry = VeloxFileGroups.Entry private static let kinds = ["CameraInputOperation", "StemInputOperation", "SurfaceReconstructionOperation", "MathematicsOperation", "DpcOperation", "IntegrationOperation", "FftOperation"] @@ -50,23 +49,7 @@ nonisolated struct VeloxTitles: Sendable { version.keys.contains("version") else { throw refuse("its Version has no version key") } - var groups: [String: [String: Entry]] = [:] - for name in ["Displays", "Operations", "SharedProperties", "Features"] where file.exists("/" + name) { - // `_parse_metadata_group` on a dataset: `_get_keys_from_group` raises AttributeError. - guard !isDataset("/" + name) else { throw refuse("/\(name) is a dataset, not a group") } - var entries: [String: Entry] = [:] - for child in file.children(of: "/" + name) { - let path = "/\(name)/\(child)" - if isDataset(path) { - entries[child] = Entry(path: path, isGroup: false, value: try json(path)) - } else { - var sub: [String: Any] = [:] - for leaf in file.children(of: path) { sub[leaf] = try json(path + "/" + leaf) } - entries[child] = Entry(path: path, isGroup: true, value: sub) - } - } - groups[name] = entries - } + let groups = try VeloxFileGroups.read(file: file, refuse: refuse) let allImages = file.children(of: "/Data/Image") let images = firstImageOnly ? Array(allImages.prefix(1)) : allImages let spectrumStream = file.exists("/Data/Image") && file.exists("/Data/SpectrumImage") diff --git a/Sources/FormatoMSA/FormatoMSA.swift b/Sources/FormatoMSA/FormatoMSA.swift index 79d20ff..26c58ca 100644 --- a/Sources/FormatoMSA/FormatoMSA.swift +++ b/Sources/FormatoMSA/FormatoMSA.swift @@ -491,6 +491,8 @@ public nonisolated enum MSAWriter { case .text(let t): set(keyword.name, .text(t)) case .number(let d): set(keyword.name, .number(d)) case .flag(let b): set(keyword.name, .text(b ? "True" : "False")) + // Python's `str(list)` of plain strings; no MSA keyword maps to a list today. + case .texts(let l): set(keyword.name, .text("[" + l.map { "'\($0)'" }.joined(separator: ", ") + "]")) } } } diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-bad-json.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-bad-json.emd new file mode 100644 index 0000000000000000000000000000000000000000..81db91717eb6b4e449036fd0e7a37c02748b2ae5 GIT binary patch literal 21864 zcmeHPOHU(55FW##IGYzMMM|deBh)sIKn%rmAM9r`x|5<`&+2 z`1qj)M>MKE(8fiQ{{4$b^#@r(d8*&QLc4=^)?v_c9;F>pX#cC#fLc8RpM3$d7!n zQB2%1(#>AO`1Om6=TCzA-Z3@RaBdiZZgv#*uWrNml2?plI{_{2KV+ z*w=TM>(v}v|2C#4llnI?eams{rVsfwW;wh10r27NU;7j?{Mgr-#p=xVOPp4GDd0yv zr~f3_HFDFpCA+q7JAUjJ&#^I2r1}A+6jcZ)1QY@a0fm4pBXc`8$IWSYB%Iw|KW%r+#RjV1^1VNc+um{r#B~h*K38l&;tZ9f59kd4}`H zq~BuQ+-7q%uVI;cP^8qvz=^FY+NHE#t?TzI8lR%~6M?Y(QXD%%`yo`wUW-(4 zMJm77v_39rF|=FiCv3-8>~3hAcZ&nX&!x(ZCzAHNkNxwN8m~Bn`)b3A9_1AGYI5b~E(Ny2XL@FQn?b=->U~Kw(1i)Ab*E;FN*^ z1j2D3hP8j7{Sfn<>6?&VX1=+JX~>h`<{)!{BOC_|v|ExFw&M$kzE`ZTuM3h69T|~G zzKduOuHpPBUqJ(P;Qw-ITTHZQuMd)f+UtXkzZMJ{-WX(t|37S{ZZje6YJ?UsX}PDp zM?-P%IN6);zSY9I30Oh}&xh~u^L>6k;^&9_{D`0Mx(lxiF@A#U!s9%x-+;ETeuDC} zrh*r#Lm{9LPzWdl6aor?JB&cT=So_5P0y9#eCKnemFZdE1K`7Iz2e&43IsOV&HF!OUI`n377Y1)2mt5z{#xpIm5e$C$Y zke&QbzV!uVbmqF*?-BNUnw8xB?Dyn5$*1hM6W#lh4%;uiKcAuf5E2aEr^!AGRB=ff zo-f)!yCwNyJHBK$L*J}h90>1k+Sl`yUz;_rhFc^8`I2wCKA#a}eyz^ZuV=jRqWMV0p&Fh;#pF>RYiPLif~ijF7WD2#?C_LopY z5}3D5YtQFl*`gZJE&t}<-kCB9MJrod9`omI=5Fu7NaC5C0p-P}<*}mcRG8-xcN=>J;qLZcCPWuWN31cdmNoTX^+*B@;HBv!kQ$3Xp zT!_I}Hr-R%w76zyjf1ajrl+!*!B-ZXcBR3+P6mT~rNZ}dx#G!a&9QvG=mtD2C4309 zo#kP{HJ6yZ?N_0Od&YE5c*AVd^PM_(Kklx6jY)YWpVgPmZM*fjCGgxat)`Hh0l6U7 z$Dp$5G=H|3404OM2P@I|-ZIgsot?Ez55((b({em$-mY7AHks9`i!JbxuZ!XF7%Oz) zf!AtO&2mjdP8h#oh;a0be~F>exRDTETXZ@CHpEXoK8*n-!{dkuBO(MzF`108h~I!Q zm6!$=2-pWhe-~5=Dg+b)3ITy)5Kst&5qN$6Uv9j({@3%^UG)W`$%ejdEhSXwG22szf9Y`Ch&KcsFTv-0yF)l%dmjvfoF)} zl5|bZ-D$Mm5x4Nr=N7G0HC?vZo;9Fua5ODG8qWqXTr=(GN3pW(Q2Eg{13w_*!hS?| t*9OK{Qe*3eztfzxet%K+c-7y*qj3*A{r{f7{u;>Vukq5bTfoie_a9A+A~yg4 literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-bad-number.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-bad-number.emd new file mode 100644 index 0000000000000000000000000000000000000000..12bc4b4b7fe1682ea2703b37339eec85e32612ed GIT binary patch literal 19432 zcmeHPUr*yk5O?5Eqw7CTI;rhdRda5uQmKiY#E_&hA)P9nFDzimu~XYY zP=v%IedDo@Jn}K_k&n?w9{Ua2o!yxv_Fa-YIF$=oBYXeM?Cj3ZGZTBg%b(TC;>UL% z+|}SnCbc`-w8+xsUpy{ zn(wukAKI)_Pxec8O8g0JO4zTwfQ3qqL_)Tpc9c$XI^@6LbnqlNB~4oHl$I3bi3wSq zM0t}5P7p`b|1^y}MN)72T7SA)N%TaC{V6#j(*F$0(+DKt`SOD(-w6`VAISO{RO~Dl z<-5T0r77zXBrP?G4MsHS7Z>CB4U3DH52Lt@C>(0oS45zn9mV~tpZIS%VH%wZeE7-y z>ay@<3`6ACzz4^nz9%EB{!>cNX7wLa`nKmctq{r^%<=a1BjBU3tV4kxhdOguleuA? zr;U(@Xx`wNUIYQwi-!K2XeD{!di-fOuW||jg@8gpA)pXY2q**;0tx|zfI>hapb$_9 zyjuj~^Q<&~XHW*atGD=Dyx(q8?=(;Fh$`MP9X8MP_g7LN&Hufnc*Q>A2=ued3!FbD z^_H5}4#P=*sJpIR_M2a;ekpt0MZ1*ttM&bUMdMTHcp?zDUy5TVXg`Ds*=vm|u9?d3 zHJy)3S_7v5Kssx z1QY@a0foQ_Au#N@QamtnedxI|UhaLalss~#X~1)33E`nfGAtfrdb3Ht_wD$)@L})U zDST0qXxyw$s`vjNf%yA2y+tKwqhnd9)aldauWic>?QOeZ!E)s~3q#x8p$#MCfAXy_ zDxw#TaHy9Xzqjqc#w`+&ay_*Ckk1IR&~7rmf8tPw!n!QzS+ithje=3gWpcCn z(GlNS@z{1P|6q*;Z3i}JaDEo@bsL@!yEW=XQJ3`MV&%az4mQR@dS7^7ZqSgy+>CrQ(LMW?f`6-Gl7`%9=H2`syoa}e^d><~qC z%QyOUcc#oj)#~-xhHdqi4c=?PGJ+drq7<0hB#w>@} zve`_2&MeN()rt)W6WjEe4&sp&K1pqk&7yu8>npLMvT6)`GLyj#XUo7 z9DQZYfy$bruPiw2YKwcF3mtk8uAUZ=Hdt=l4U!uSnCgrmp& zOAOVfjg0Wxn%5JsA%5!V5(X5L#}N@mLp6aoqX xg@8gpA)pW#I|N>x|Cbvt-u?fZd5P;uXeNoEKB3Hek;dnrPM6+`VU64%{sYN!<5>Uz literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-digit-string.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-digit-string.emd new file mode 100644 index 0000000000000000000000000000000000000000..3d1cd6e9e0d6bf2b00dc098b3956d443f3fabcd0 GIT binary patch literal 20464 zcmeHPOHU(55FW##pv{YwA|=@FMUz(!$ca z_aELj;Fz2=?io`eNteIzxGc*Y;;H;8CSpiWk)DsqM9hFc!}tjEAE16!Zex9M(cqG# zpD0!{ta~ywNg0}s6auBixe9c63wvEadVzL^tg*>jzSHoG1v{`IuhL>Z^8!17On#J` ztR)y9IIuW*#F45bW=u#K6pW!I$6_*r>JuI3<&gfEmxCw4Da)kgP8gGd9~+a|lc;Yz z#tXy|_CLcQPm!g!eXXvQ7h+eU#KDAY5%vE$@>A&W)7aqiTv+Y}@|XA^QQY|bt+3oF z7Mwqn`3RB`A4kPQN}}STieJCDc==@*mm!5iH+w(?qU=cZn<({P@Pa9HD)8Ya^W_!c z%NT~puR#uuBXeIySo7PsnKd89&7DT8ZU?|_GPiMH9t$}N%jmYe9&=fpN8&1{HHgMg zqmV{%Q3o!k?{|_qp%KssXaqC@8Uc-fMnEH=5zq)|1T+E~0gb?agn&BFO7n6SCGfu0 zCVz`>wd>RjnkSg0jMt?7=DGg(R4SzTzt>c+#fKb$D7(C%2`22{g5x(``w(aMY3|)H zW(Vwl)cjiM6RB_y?XsW!{`t9-*Y-Q$gBbE^QTBU^_CwuUuG_l|jTrK78dv4_n$E{1 zBaU`UdR057c4!(#ASw=2K9h1y^lupljL7fUVXc}QH3ZZ+K)(mi(0&LH^3Muo+>okq zU>fbVb05WyZ+N>qTRkccY~cs81N`{RI8HUmU1BlJfNY?>un2gCPXeI1tCu z=V(79OzYWJSzvGD+BPU7zb%m9D^fKM6wq!-pK8aisCMMUUS1zKEij0UNhIGvLE8r^k*Qc-uE;8{Wl82G~=8Gb$Kq;4}I?P`n`Ga9qs^llBsy|bC# za-&uX>!)BTC5#W>=I49-e8A5S`1v6}VcH9(4&wZTpK#!59T%;=;#vC82xtT}0vZ90 zfJR`55a>6q6az-y9~xJx{MB)#l#w${J;s$kAUrfAL-qve?UJbXZFL_>zxVCw?`1zE z8n@dgefR$#0rh=bzOC`Gb1~c&@g45Rw(SMZj#Cpm#yVTaK$84UeG3a~>2gu_`wZ{< z+jmm7pZ%V%NjYu5U2I1wrP?nA>lbK0ghjz(DM*u@mZ)Grs?Hbfq1}>x)s6?ejjo0W z9GP>qpWBY_;8v*+Uk&V5z-I(m;M5u4M|ETZvmqHXl`UE+D{tk~$@GkQe9ZS~J$5|1 zb-2#_whKG8xI7E^`mII_HeprUd{6AwlQeARqWooVn;r1N6x60nKU6hgh1fM)G{r2< z%`GflRkd;0gf^Phn(cXxw@cd0tEv;JYn4&cQr9V*^KADp;KAALsD#Gy1wh>$YBNx= zytCu8V9{f(-9uipB$>KoClp=-oHQzZN$ literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-empty-group.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-empty-group.emd new file mode 100644 index 0000000000000000000000000000000000000000..f23d9aecc422761b20bfd5270e3f891edf5a9e07 GIT binary patch literal 18832 zcmeHOOK;mo5T@)H71Eaj6lj~GsKU0W3seFr(R%b0#j;}qcC1>4;bT~7Z4;nKm81-- zhJhU2n~y!_=%3JIkNz1w=GcGG?(EJ;A{$AG4aAkT6v%zd?Cj3RnGw0%{cXLr_Tl}9 z_cb`uY3-i2D6(|<2ak&lSwm%_KgUc8({oHO=VT_O!B5jZ!}#Nl|D7=qaKgRS5?F@_B9t%Rp_q3WF*-%$=!j5bALZUa0?0~iE zf!|?4hapb$_9CH7{ndS)|Q^i}Rljgbp z`cev{`MZ_FAWk8>aGmP54!|v{E^HFi2{*_d@^GK$X{>T3Q zR`SvPzq5cT#a@d*G7iw!-V3xJY7`nW*Y{H*Tr*9^fhDwClApBWYj$(zn~jPC`(I1d zPtd<7#ewxD$xrwH&I6|uj3JPW0~w_Lj`l;$w2ozqMRp6k{=)=>5Kssx1QY@a0foRcBQWW?Qamtn1?jmmSss3_lss~#Y0PtF1@WRsGAzEp z^mda`@7u|B;gjCCGx(w;(YRfmRPXN|f#mx(y+x&$<6~K^HR#jkPhH!KoCBw6M|?*~ zgGG_!9nyvo@;~|37ndFlz5`m=sQXIQN`yqa^%Q{uuFip-E zJx9Bxev)>4!|rCj`KexQ{?c_q2VxH2QxPjSB0Grqj3A4gHWNfB?8<^}S{1`IN=7N4 z%P;F^XMAhM6UVcI-VO`9E^N>^)oU!`>o)uVc55`cd=K3I(>(0qp!yB3#ZEZUgw|9U z#+qi0Sn-)HhN7=mtF^UZQMg!!MWPA%?lZ7FHSI8Oh!(>^*C<6Nq zvt42{&0L{qm6wb8N}(h=*>sKvcfiOQMbj#jN`-Q`GB_Nx0u_fW!Ct}_r>sdj1{`@!0UF7>|IAhP8h#oh;a0T ze~F>mtdSF5Tla?oHpEXoTfu;0@i-#FhzLPajAaRn_!SuQxeBmAz&@M#e?J9)LO>y) Q5Kssx1QY@af!RagUrb%m=>Px# literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-empty-selection.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-empty-selection.emd new file mode 100644 index 0000000000000000000000000000000000000000..e3d3d204593f974e366c9b8ed4de4a8e9c5608df GIT binary patch literal 19432 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 u0vZ90z{nx+>ioalfc5|X->C~+PeL zn(wukAKI*2Pxec8O8g0JM%b^sfQ3qqL_)Tpc9c$YI^;j+bnqlNB~4oHjFuGTsVP~V zM0wK*P7p`b|1^y}MN)72T7R-$N%TdD!x`Bl(*F$0vj`;N`Qp7OUk3^2_htPYDt4BO z@?Bv0(vglM#aU;2T@$c6b{wxDKadet|1qU!v-%Gyeb@7wRtV)S=6Hwt3Gh)^#&Dqhq8IgFfq=)^ z$COj4LO>y)5Kssx1QY@a0fm4f%rTt&EFZ6!4B11tP%R{CiRcz z2_90#8>XY?x&HoA3Z(hJHx#ef#~gt{c6ovG$E4m$)7oP==?`_+waY>CYt=909rw{L zrTuCHzh6=RR63am#O;^jSO@KgP$7G*QN=Y=`Msw5aY;*|-O_#Hc6`m--P-1Z;y~>y zsdDRqOh^5W{qwElqvwBX0aJ>-5`lOeps&5BXg|~_Fl4Ulr$o478jk~WXtyLkZpT-= z&8=@XC=P6YEmc26{~i?ws&kT`p8u@}PAM2cARY%&Nc{`#hnQ&{%Nh%<9bD4_H1gXb zF}`LRj{^qUEy;`9@oS2mIJTG91xbfaOi3g^Ktu@FczK$y3<4VXzg*gu5-ob`gQTF| z`k>SA1%rk^46?wl&%3#s%t*VMqD4$v@w?%@p}2RN9nLptR9H6wYbIcP_&z^B;^#en ze!|aB`3bMP@XCOTpt=&#>@TV zO35QG|Yg^zmQPT`A^MB`?4Qg?s*2*lsF=`AWbAMMLh zrB0tVpSCSGw0G@>74jV=bry!UyGI*F$p7S9UsOhaX^{PP@V@5-$vw(`&wi48%6|LN zot%8!ekoXAp#2aM4Bw~8KC48yVj7<>dWLpO^5b@V#oMT6h`_O4Zv5W10~=xv-%}AO z*F(z>`HUb7?IsgMC~V4to;6EG)+iW-TqZZKpPcZm8INq&@{cxH&~{*h#<5;uAz!!Q z`LJ7~-sXGYwjbwU4+qt+xI65S6SL5oDuYPVv=J#ju|!w&YPnol?iaOr)Pg?h^@ioT zJaUpWy ziw4x!+D^du7QhAz0s=3eF``G7r43^N*f*Hv5L-5z$uF42`Gs65Ul2E0wGVnvz{nU2 zSuF{IS#zke=J+cMPP^XXUMGV=xzS(_ z^To($%X31$=mtD2CwvIBUF2cGw^o_E7w!Xvdq$}!ykW5&gkH0^16Lqd4_R}~+Hp6aoqXg@8gpA)pYLI0Rmv|Cbvt{{R0s^8(kC Z&`c6R-J#4ok;dnrPFLQEZjD_h{sX_#*<%0z literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-no-version-key.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-no-version-key.emd new file mode 100644 index 0000000000000000000000000000000000000000..903276158176363cf7e9cd1387e0ce064241063c GIT binary patch literal 18832 zcmeHOOK;mo5T@i971Ec36lj~GsKU0W3q%4b(R%b$#j;}qb}U$e;bU2OEfb(fm81-- zhJhU2n~pv9n4i&OenyWy_8+u6yE}TYmXyS4T**U&+{ergXFkr1=;iJ&Th-0??>@Mz z!I4U7ceE8z#mnF7=x@s&Y$N>#RY|DwQkBn>vXao?Pt!h8{r8moIA3FbV?*PdC?88T zOf&At+A4NXd8-iE+NjjP;6r7#p~@G^&s0-8U_t2ko>sLZ8~UnF*m3P%7^kDZ&hn>( zwk-TtUcf`8Cnpk80?NU1iPNF}DW^ju!6|7ep~AA3676JCcBhoQr35F414fJENSZMT@3^oPFd`sJkgwXM(O9Z!^BO8?a+e!oKfsJxvB#QhiZ*roCxLc-LHVZ#&QXV+`Qks6I;(Jye*jknR`1}_h zI5Kbyfp{KBE9$Sxe~=m1vFx(QK2mF1fQE5fLdKUY<9WbPev9(re*BVRZynpe*9FOd zP9!Cg?1K~m6geNg{f!Jw%h2HD`(XM@^JR-|7gaS@YN z`ewQ|nD_eTbiUh0g>@6KgbLmd-{t2A{Jh7{kNEj9KjGh9_?IEgPZhenMh7842oM5< z03kpK%rgSB-Yaq8HNIEI+oSK5l1I)oO?j^@E4=WPOf{d)GmNA=AwUQa0)zk|Fjoki z>y_qDT_G-N|Jt+HVnMWdL@=GOF!3%)huvE$i6 zZ=Z!-7dB{|>s1!%9b*wBnI)_iIURrIY& zrMfvPs?qC!j(WXmd!FO9P?|m}I%8fb3{?~ROK>0xta!HDi=3wIB1PPmZ}c1POj(1j zz2oDMMH?OqT0PJtW0ef(uXWv!)dIiC!VtmBXN~d0WEsOK0Q)|(U1T%OY`$QX)(W|D zz9??8#lsC@5vDQ>$t@$?= zf_AUNqfRD+O0&sa7TA!b4tzJ_i*6voaw3Mn+I5~50(*yft>^?OJTl595e@6zF!I|x z{CK!V4wLfg5o_<-EzddTaXmvl4&ulj8st7^b~y8KW<)P{f8D6Bzkm1s zT?39x#<*iF$tqs{)yL(g>cKYBf7F$fE-!TXG_5Kr1O5!-6WxDL%TMy{?XImEf)nLq zsfQWX9aYO<2bH%9fsM6V0}MXURxj)Fnf5c?H1>HIxq)xgS4 zqf8l#(tp(jJXCpdDy1Z$JS-Ok9qOM7Iz$qjil#hw(a6YlI<2}hTHZoR5aav_UqCDN?twCUtA0^{!lEPmJ!jcX?h~Y=8J6kfAbsECB20l2>%oCMi&F{0O zZJXa^&7&afuo&9=+zU?4bH%Bye|$LFqB!VC-?z7Ub37u&-K^mN+8Y?zNC0!pK}Bz`Qj}rS=~j z6j`b^h*KoIVwubX%i3>Ie$tO$@isSp*rYsg@TD^SRLA$MJg~8>`0@GQc;LvuF$9u% zAgigrY5zfHT*tD_V|J+5v;Ym`wt|c=SSIs;rTrG=CH?pX#oju$|E~*D0i8-KB;V7B zAg;+ate^oJ#J^hFmX#xh>w}b_;rgJ!i~ld6~-wOW0BRMcL- z3p$$37V~}AZ=*DGRCLaMt}s+h?k~ZCD6r-;uOGWD<{?GiR&4Ye?o3&MuAQT!h{tO_ z58HjvBX>Y zPr%AsCEF>M%f(8m;*_nzxUq$)#*RIR`8T#W)!3qZX4)I)-&kj=vCjM(3qiZn6;Y>> zL9Nx|9uFC0sr|r<#iARCu!4*suy$3Xg^+D=za1X~MMOrmBBNoo7sWwGgr5l4*yT!I zGv=Lb*7n^a5!Zv%cf!1$lsc2jmQ+F#5j)hh+lxQkgoy@B<%CK`lTXe2?0WY5Fi8y0YZQfAOv0q0{;Qg C{fqwq literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-null-selection.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-null-selection.emd new file mode 100644 index 0000000000000000000000000000000000000000..8dfeb32d4276aa8019696441f66ee5edd1982e4c GIT binary patch literal 19432 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 GJpKcb#{HcD literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-v11-operations-only.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-v11-operations-only.emd new file mode 100644 index 0000000000000000000000000000000000000000..c7d521a655cf195a2e1142289bfee0a9737b344b GIT binary patch literal 19432 zcmeHPUr*yk5O?5E(e>Y*RH^M%Rda5uQmKjT#E_&<&GF|#3r9*2LaHjA8y2wS*lFw_ zC_=i&^o_?p^2o=yM?OX$dF(f6cXnqIymv|Npe+}&M)vIP%#L?|o|&xIyZl~RSbF#F zgS!SC)6>Qsy%yo=wO^%78z^_z27IqkdIxV|{Va;F6@D zC{{D9J2Ev*1)7c(0+q#b6*|0yy)Gg>M?1r;vB_GI7Y4?H8@o`p;6+W}J&cII)?_U= z<`P&!cAF*vg{F)tVZZVM7Ah?{m6A27KG8|ehxEss51s_49*#lWVgB?4~CFRaB zf8oe-1j$HGVucYU{o|rN@i2+Yh{B%5v;l_~PWPW&H9)lc(1E)KfKtPQHY2^Na_Cvz7 zj%Ag_?l!Jz0Ur5nkpy3ns&T+VyCr?99lxU5u@if7U66F>)PzLx9Yln1Rr8a4Wf1Vd z|K-xQw5ZWrA0!p^)(4$@Ed&hwVUQVqeb!CgWJ2221TA7RieC+G4aL0^d$8QNabevA zETMw&;oJOtkDvGX`2jya`t}G!uG$h09Go&|5`n_+f>%xbRA^NuqJHebRUT z{}E8%x8>U!A7{@d+akUn{n&Pc*xT{yVy8%V%Lqu4->Gk5NiDrxKl|p=fAp%Ecx&CY0 zi#*&Sk;vC#w-xglK^A)r#`jMgnUL9#8Pj%3mTeWRLN1$|HII+^){IA9;I1dGcT*o*e_K^O%wY|s3IvW2d;k@^Wf|gN8FZg^y}?RnT4XYot=oq ziverx9zshpPSJw$YTJ)kwH4M`6cKp&td%@$ma(ik5MO7mPhz&6&CfZ-*|}UPUl2E0 z@%DR9z{*;4wv#Uu^2NELQ?PQ$&gKSoHt`@vzq9#)oz079hSoUxoplCw)*1cIg43=w zx!1{HP_EbcJ}$YFb~E&2zUT%#EGK*jdOOdSgDBHv;wM zhUvRBB(o*cq6Df|8%naMl@^W8u1SoK;5|j literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-v11-two-settings.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-v11-two-settings.emd new file mode 100644 index 0000000000000000000000000000000000000000..ac47376f32407e455d163d0af0cde21d4801bb6a GIT binary patch literal 27808 zcmeHQ%~KmU6ki~;H2or-cABPN&bFP>PVwy83$}X-0TMGq0*#prolII5TgGk|+x4z9 z5Qdo?>5XHL9COSu#~eBKPv|j6j=i;d(tFseWbN@lVvMznEIsK-EB*GpCu#j;{>;wZ zc=zI!iw0a{W5xwzTqNn^KfDe+nL{*{uVG>c<2{TY56i@m0e^;Z8S^h;`zqhco%wl# zb5i>hV>QFNAX8(Mq2qxo_O3q3}S-w}oid{xS#;A~A`2i9t zC3$E_N<7Iu#Us2N(x32h&?E@5OiJ#kF(&BYVVOOK?Trla0&zi&)@!eo=y^JKdua|Z zpV;&Do_zq06Y? zq}RX)*N*vs)%|j{V*VI2ZQJ}IW)`dUO*a7gF7v8e<}UEz^l!v~;Mb1HJhsUyL5|1y zKsIpCoQ4L{=KWn#)3{`h;!jvmA(4SkadX?w@Y)7nx5(?W_VrVe8<}`?`Tb^Er!)c@ z0gZr0KqH_L&~Tu2Dvp zai{lO|9mbR)bF{v1ooV;N6F6iY|8zW<2&nQ)c5DyjhfpDTU8al^#`61`<1d$t7|Jc(N0s9f-tOF1kFo>HU&pizAaMCctHE8(TmBiw@q z$^c*a-E~aVX!-}qhMN9C``-x$11APa@Xtrbsa_^jxk`WAzBeRS`#8U~=N_C=@S7Or z0rm%P^Y9)Imw5P)hmU!H?JjH?VmyHAg5Sg&JiNsN$QZaYe89s;Jiyulmiu4uaFqw} z;fV1-{vL2I@V6iu+5-OxqRFR%XX#5Lpb^jrXaqC@8i7}cK&N#jIbOrsQCzr?BD!^5 zDf8r;#y0E9Ot>Ya&d9nlYQL@SKRWHVYttdPXplM{5Co$3+hbE<08V?q-ASL-eW6OX z>a$#r$3%yt_4!#}?Gx7&{KHQ}?vEkwX-X{_Y`T+Ny@GjS0THLIJCcvi#Dj>MC)9I6 zsvV%WDdRK?;%MgQa^&LhQ^TzU9TIv2S0fLkaQls6eaaIs?ou_6i6b~2W2rL0sUo|rOscli;C>*b1D z-@e2Ah6e{UI6n)x-$u0#r!{ho8mq6~Ou$(R%3r7y*cLCep*Cgup{kQsh`jBJu9(@G znYkOSO|5L#ppRxQ?^Y@tF>1|hZFBH6P5mbvQFE< zI{iObP}-#$*E(qoX7YJ{j;k&#;@zqjaHkv4u!PVd80{=K3w3vqRW^bWlyJ?+q=h!j zHvFKv$<>dmYfxsgy2u8eBgQw(XIpSLbCxHl|r?#?lEcP7Aih?qVcn3 zqEeHb^;{pCH&O=Kr*{y1UdB(xr`N?BM==)YwNb99}5 z6GOR^R$OT9a<#=^gZ(roGiXp8ZbyU}5hh4B<2XVh9s*+`o&gpx*t?Olhc3_vXaqC@ z8Uc-fMnEH=5jd|1g!cPDAZEp6Yw|0)bBQaj+(PrRAd+Pp9|yRQ;RieBcrK z7f}!0gyT*-teCV4u9I13>vp2xuDkA3CSAxBZSY79ZUi9meC&W+x_r{OVPgHT|GxpBZv!HW@|D5ZG<8?{yc%?F0e||6+>?!5$*r?O)7y?(I=(q_(*4(n;gsV}dbIkxyrI^kQwF|I znp5?NW{=0E@Bkh@lr+g}HeB2_rLJ2K@gEmZ`zQVXF^~HE!tS?l#%Kp}; J54OkM{vS@4HBtZo literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-v13-disabled.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-v13-disabled.emd new file mode 100644 index 0000000000000000000000000000000000000000..ba2b53c0fe022c92ad4bd6850ec7468633eabb16 GIT binary patch literal 19432 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 UE5f~_nhLP8{0E$vY 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 JLf}mx@GqYAntcEO literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/el-z-0.emd b/Tests/FormatoEMDTests/Fixtures/elements/el-z-0.emd new file mode 100644 index 0000000000000000000000000000000000000000..d1e3d6147bec7a9dddaf824d47ccede1dc124b8d GIT binary patch literal 19432 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 literal 0 HcmV?d00001 diff --git a/Tests/FormatoEMDTests/Fixtures/elements/velox-elements-pins.json b/Tests/FormatoEMDTests/Fixtures/elements/velox-elements-pins.json new file mode 100644 index 0000000..12825d6 --- /dev/null +++ b/Tests/FormatoEMDTests/Fixtures/elements/velox-elements-pins.json @@ -0,0 +1,1560 @@ +{ + "files": { + "el-bad-json": { + "raises": "JSONDecodeError" + }, + "el-bad-number": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "ValueError" + } + } + ], + "el-digit-string": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "O", + "He" + ] + } + } + ], + "el-empty-group": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "IndexError" + } + } + ], + "el-empty-selection": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [] + } + } + ], + "el-empty-string": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [] + } + } + ], + "el-entry-not-object": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "AttributeError" + } + } + ], + "el-no-version-key": { + "raises": "KeyError" + }, + "el-none": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ] + } + ], + "el-null-selection": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "TypeError" + } + } + ], + "el-settings-list": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "AttributeError" + } + } + ], + "el-slug-order": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "Fe" + ] + } + } + ], + "el-v11-operations-only": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ] + } + ], + "el-v11-two-settings": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "Ca", + "Cu", + "H", + "Uue" + ] + } + }, + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "Ca", + "Cu", + "H", + "Uue" + ] + } + } + ], + "el-v13-disabled": [ + { + "binary_result_detector": "UltraX", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": "true", + "key": "Detector-1", + "live_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "name": "UltraX1", + "real_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": "true", + "key": "Detector-2", + "live_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "name": "UltraX2", + "real_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": "false", + "key": "Detector-3", + "live_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "name": "UltraX3", + "real_time_s": { + "bits": "400741d664254386", + "value": 2.9071472 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": "true", + "key": "Detector-4", + "live_time_s": { + "bits": "400a000000000000", + "value": 3.25 + }, + "name": "UltraX4", + "real_time_s": { + "bits": "400c000000000000", + "value": 3.5 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + } + ], + "el-v4": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "Nb", + "O" + ] + } + } + ], + "el-v6-one-segment": [ + { + "binary_result_detector": "SuperXG24", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "403b951bcb368b5c", + "value": 27.58245534974718 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4040624eb42d9996", + "value": 32.76802685 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "403bd1befa684e54", + "value": 27.819320345368695 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4040624d759ee88e", + "value": 32.767988875 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "403be81d4ff9cfd3", + "value": 27.906697271813595 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "40406245260f5e42", + "value": 32.76773525 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "403bb335b231a35b", + "value": 27.700038087004355 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4040624f10088bc0", + "value": 32.7680378 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "value": [ + "Fe", + "Co", + "O", + "Si" + ] + } + } + ], + "el-version-text": { + "raises": "ValueError" + }, + "el-z-0": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "KeyError" + } + } + ], + "el-z-120": [ + { + "binary_result_detector": "SuperXG2", + "detectors": [ + { + "enabled": null, + "key": "Detector-0", + "live_time_s": null, + "name": "HAADF", + "real_time_s": null, + "type": "ScanningDetector" + }, + { + "enabled": null, + "key": "Detector-1", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG21", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-2", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG22", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-3", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG23", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + }, + { + "enabled": null, + "key": "Detector-4", + "live_time_s": { + "bits": "40241f74c46cc830", + "value": 10.061437738684702 + }, + "name": "SuperXG24", + "real_time_s": { + "bits": "4026bace71b2cac3", + "value": 11.36485629375 + }, + "type": "AnalyticalDetector" + } + ], + "elements": { + "raises": "KeyError" + } + } + ] + }, + "generator": "tools/velox-elements/pins.py", + "public": { + "fei_emd_files.zip/fei_SI_EDS-HAADF-4detectors_2frames.emd": [ + { + "elements": { + "value": [] + } + }, + { + "elements": { + "value": [] + } + }, + { + "elements": { + "value": [] + } + }, + { + "elements": { + "value": [] + } + } + ], + "fei_emd_files.zip/fei_SI_SuperX-HAADF_10frames_10x50.emd": [], + "fei_emd_files.zip/fei_emd_image.emd": [], + "fei_emd_files.zip/fei_emd_si.emd": [], + "fei_emd_files.zip/fei_emd_spectrum.emd": [ + { + "elements": null + } + ], + "velox_EELS_EDS.zip/example_velox_EELS_EDS.emd": [ + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + }, + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + }, + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + }, + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + }, + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + }, + { + "elements": { + "value": [ + "O", + "Fe", + "Co" + ] + } + } + ], + "velox_emd_version11.zip/Test SI 16x16 215 kx.emd": [ + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + } + ], + "velox_emd_version11.zip/Test SI 16x16 ReducedData 215 kx.emd": [ + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + }, + { + "elements": { + "value": [ + "C", + "O", + "Ca", + "Cu" + ] + } + } + ] + }, + "reference": "RosettaSciIO 049e7d7070e84779b499adaf3295beee9facb004 (rsciio.emd.file_reader, select_type=single_spectrum); the mapping applied with HyperSpy 2.4.0's DictionaryTreeBrowser as hyperspy/io.py dict2signal applies it", + "sha256": { + "el-bad-json": "a481682ba9e75395fc3e079b8671b0c7295ca5361e5756bb58dd53400b4bec01", + "el-bad-number": "09ea351789d4684ae9458e8108b5e10bc2d9c786d4cbe93d1c200cf48dc15470", + "el-digit-string": "bf371a15c9a40e0c06c9aabb789a001f4726562d0864025339fc0380b05e917c", + "el-empty-group": "be8f2088e8b02e336a9ec476fd5a4432ff9305fdd9b78ab2603ae432a457cf73", + "el-empty-selection": "9941130daf9c9c45888620abde34a32eb1c6247e60cc75bd01df9bd641bf55f2", + "el-empty-string": "14d48db91d3b9dd015b875be67198bff8385d0afb20258cd5bae53b6093c0f21", + "el-entry-not-object": "bb2fe6dc477e39e3ae300a6eda340550c3a1db522ab9d014dc1cc44b9324fa5b", + "el-no-version-key": "376936a40bb4cf1c2b31363f4d66842602b3e02fb8a9b13e3b3ebdcf9bf334d3", + "el-none": "5055061c2b67e6879129f622a219ac35671a490ebee5ac807fdec14b8e3704e1", + "el-null-selection": "81077aa6f8334982284cee730b8d990e36d97d68ab8b590c9ee66bbd771d4bbe", + "el-settings-list": "72ada0cdab1892078e02d14ddad6d5f7f64f43f41293f9e825589e6c2f34a68f", + "el-slug-order": "721699a97a1b1552e493d020f75348f4e77724e70152d822827664845eb3f841", + "el-v11-operations-only": "0f21b2736a352c02a4c7b4d6e128ac2f264571cc0e9cd02428d6134094712b3c", + "el-v11-two-settings": "0f899f3896fcbd2d5ffe69dbecba5e1dfa2f8d43c36580e4b652cf35bd84e3de", + "el-v13-disabled": "c8cb608d494cc80c70f79b3e55946a8773eba82545a559236c58f973fe2a2af3", + "el-v4": "77316431d966e8c65866e52a41f14873a22803ffc9855199d1eed8fae2fe1a79", + "el-v6-one-segment": "0211f7301c3de36fe54078dbf7ede73cd0637fd719e88f2f93ff21efcaa05fe6", + "el-version-text": "6510421008157b7a51aa4d69e7ad07f7ef6ded76ac7df7bd64733c44aae39731", + "el-z-0": "e809b8792c4eeb581fc663c295bf7447721b1346fa62d88516262e18d09097e5", + "el-z-120": "f7ec224ace72419b6567ee9cf3958205417aa8c6b61d6409b51050e9ea3a10a0" + } +} diff --git a/Tests/FormatoEMDTests/VeloxSpectrumElementsTests.swift b/Tests/FormatoEMDTests/VeloxSpectrumElementsTests.swift new file mode 100644 index 0000000..6a7dafb --- /dev/null +++ b/Tests/FormatoEMDTests/VeloxSpectrumElementsTests.swift @@ -0,0 +1,151 @@ +// +// VeloxSpectrumElementsTests.swift +// `Sample.elements`, the four file-level metadata groups and the segment times of Velox single spectra. +// Files: Fixtures/elements (tools/velox-elements/pins.py: planted with h5py, read by RosettaSciIO 049e7d70, the mapping applied with +// HyperSpy 2.4.0's DictionaryTreeBrowser as `dict2signal` applies it). Exact: the element list as HyperSpy sets it, or the refusal +// where RosettaSciIO or HyperSpy raises; the times to the bit against `float()` of RosettaSciIO's `original_metadata`. RosettaSciIO +// has no rule for which segments a spectrum's times are: the test re-derives the kit's documented rule from the pinned entries, so +// it pins the stored values, not the rule. +// + +import Foundation +import Testing +import FormatoCore +import FormatoEMD + +extension VeloxEMDSerialized { + @Suite struct VeloxSpectrumElementsTests { + static let dir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent("Fixtures/elements") + + struct Number: Decodable { let value: Double?; let bits: String } + struct Detector: Decodable { + let name: String?; let type: String?; let enabled: String? + let liveTimeS: Number?; let realTimeS: Number? + } + struct Elements: Decodable { let value: [String]?; let raises: String? } + struct Spectrum: Decodable { let binaryResultDetector: String?; let detectors: [Detector]; let elements: Elements? } + enum File: Decodable { + case raises(String), spectra([Spectrum]) + init(from decoder: Decoder) throws { + if let list = try? [Spectrum](from: decoder) { self = .spectra(list); return } + struct Raise: Decodable { let raises: String } + self = .raises(try Raise(from: decoder).raises) + } + } + struct Pins: Decodable { let files: [String: File] } + + static func pins() throws -> Pins { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(Pins.self, from: Data(contentsOf: dir.appendingPathComponent("velox-elements-pins.json"))) + } + static func path(_ name: String) -> String { dir.appendingPathComponent(name + ".emd").path } + private func bits(_ x: Double?) -> String? { x.map { String(format: "%016llx", $0.bitPattern) } } + + @Test func testThePinsCoverEveryCase() throws { + let files = try Self.pins().files + #expect(files.count == 20) + let read = files.values.filter { if case .spectra(let s) = $0 { return s.allSatisfy { $0.elements?.raises == nil } }; return false } + #expect(read.count == 10) + for name in files.keys { #expect(FileManager.default.fileExists(atPath: Self.path(name)), "\(name)") } + } + + /// The refusal `VeloxSpectrumSignals` gives where RosettaSciIO or HyperSpy raises: `unsupported`, naming RosettaSciIO. + private func expectRefused(_ name: String, _ why: String) { + #expect(FileManager.default.fileExists(atPath: Self.path(name)), "\(name)") + do { + _ = try VeloxSpectrumSignals(path: Self.path(name)) + Issue.record("\(name): read, where \(why)") + } catch let FormatoReadError.unsupported(message) { + #expect(message.contains("RosettaSciIO raises"), "\(name): \(message)") + } catch { + Issue.record("\(name): \(error), not the refusal, where \(why)") + } + } + + @Test func testSlugifyAsHyperSpy() { + let table = ["0b4c": "Number_0b4c", "B111": "B111", "element-Selection": "elementSelection", "_a": "_a", " a b ": "a_b", + "": "Number_", "é1": "e1", "1-2": "Number_12", "a.b": "ab"] + // Checked against HyperSpy 2.4.0's slugify(key, valid_variable_name=True). + for (key, slug) in table { #expect(VeloxSampleElements.slugify(key) == slug, "\(key)") } + } + + @Test func testSampleElementsAsHyperSpySetsThem() throws { + for (name, file) in try Self.pins().files.sorted(by: { $0.key < $1.key }) { + switch file { + case .raises(let why): + expectRefused(name, "RosettaSciIO raises \(why)") + case .spectra(let spectra) where spectra.contains(where: { $0.elements?.raises != nil }): + expectRefused(name, "HyperSpy raises in the mapping") + case .spectra(let spectra): + let signals = try VeloxSpectrumSignals(path: Self.path(name)).signals + #expect(signals.count == spectra.count, "\(name)") + for (signal, pin) in zip(signals, spectra) { + #expect(signal.metadata["Sample.elements"]?.value.texts == pin.elements?.value, "\(name)") + #expect(signal.metadata["Sample.elements"].map { $0.value.texts != nil } ?? true, "\(name)") + } + } + } + } + + @Test func testTheProvenanceNamesTheEntryHyperSpyTakes() throws { + let signal = try #require(try VeloxSpectrumSignals(path: Self.path("el-v11-two-settings")).signals.first) + let entry = try #require(signal.metadata["Sample.elements"]) + #expect(entry.value == .texts(["Ca", "Cu", "H", "Uue"])) + #expect(entry.provenance.source == "SharedProperties/EDSSpectrumQuantificationSettings/0b4c.elementSelection") + let v4 = try #require(try VeloxSpectrumSignals(path: Self.path("el-v4")).signals.first) + #expect(v4.metadata["Sample.elements"]?.provenance.source == "Operations/ImageQuantificationOperation/80e138836ebd4b7aad5be7d3dd071f47.elementSelection") + } + + @Test func testTheSegmentTimesOfEachSpectrum() throws { + for (name, file) in try Self.pins().files.sorted(by: { $0.key < $1.key }) { + guard case .spectra(let spectra) = file else { continue } + let metadata = try VeloxEMDReader(path: Self.path(name)).spectrumMetadata() + #expect(metadata.count == spectra.count, "\(name)") + for (m, pin) in zip(metadata, spectra) { + #expect(m.binaryResultDetector == pin.binaryResultDetector) + let named = pin.binaryResultDetector ?? "" + let expected = pin.detectors.filter { $0.type == "AnalyticalDetector" && $0.enabled != "false" && ($0.name ?? "").contains(named) } + let times = m.binaryResultSegmentTimes + #expect(times.map(\.name) == expected.map { $0.name ?? "" }, "\(name)") + #expect(times.map { bits($0.liveTimeSeconds) } == expected.map { $0.liveTimeS?.bits }, "\(name)") + #expect(times.map { bits($0.realTimeSeconds) } == expected.map { $0.realTimeS?.bits }, "\(name)") + } + } + // Velox 4, "SuperXG2": all four segments, one common time; Velox 6, "SuperXG24": the one segment; Velox 13: the disabled one left out. + let v4 = try VeloxEMDReader(path: Self.path("el-v4")).spectrumMetadata()[0] + #expect(v4.binaryResultSegmentTimes.count == 4 && v4.commonLiveTimeSeconds == 10.061437738684702) + let v6 = try VeloxEMDReader(path: Self.path("el-v6-one-segment")).spectrumMetadata()[0] + #expect(v6.binaryResultSegmentTimes.map(\.name) == ["SuperXG24"] && v6.commonLiveTimeSeconds == nil) + let v13 = try VeloxEMDReader(path: Self.path("el-v13-disabled")).spectrumMetadata()[0] + #expect(v13.binaryResultSegmentTimes.map(\.name) == ["UltraX1", "UltraX2", "UltraX4"]) + } + + /// RosettaSciIO's public Velox files, unzipped by tools/test.sh: the elements of each single spectrum. + @Test func testThePublicFiles() throws { + struct PublicSpectrum: Decodable { let elements: Elements? } + struct Public: Decodable { let `public`: [String: [PublicSpectrum]] } + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let pins = try decoder.decode(Public.self, from: Data(contentsOf: Self.dir.appendingPathComponent("velox-elements-pins.json"))) + #expect(pins.public["velox_emd_version11.zip/Test SI 16x16 215 kx.emd"]?.first?.elements?.value == ["C", "O", "Ca", "Cu"]) + let root = Self.dir.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + let present = pins.public.filter { + FileManager.default.fileExists(atPath: root.appendingPathComponent(".build/rsciio-unzipped/emd/" + $0.key).path) + } + guard !present.isEmpty else { return } // not unzipped (a clone without References): the planted files stand + #expect(present.count == pins.public.count, "every public file is unzipped or none") + for (name, spectra) in present.sorted(by: { $0.key < $1.key }) { + let signals = try VeloxSpectrumSignals(path: root.appendingPathComponent(".build/rsciio-unzipped/emd/" + name).path).signals + #expect(signals.map { $0.metadata["Sample.elements"]?.value.texts } == spectra.map { $0.elements?.value }, "\(name)") + } + } + + @Test func testAFileWhoseSpectraRosettaSciIODoesNotReadGivesNoMetadata() throws { + let spectra = Self.dir.deletingLastPathComponent().appendingPathComponent("spectra") + #expect(try VeloxEMDReader(path: spectra.appendingPathComponent("sp-images-only.emd").path).spectrumMetadata().isEmpty) + #expect(try VeloxEMDReader(path: spectra.appendingPathComponent("sp-none.emd").path).spectrumMetadata().isEmpty) + #expect(try VeloxEMDReader(path: spectra.appendingPathComponent("sp-two.emd").path).spectrumMetadata().count == 2) + } + } +} diff --git a/Tests/FormatoEMDTests/VeloxSpectrumSignalsTests.swift b/Tests/FormatoEMDTests/VeloxSpectrumSignalsTests.swift index 604b94b..2c80d7c 100644 --- a/Tests/FormatoEMDTests/VeloxSpectrumSignalsTests.swift +++ b/Tests/FormatoEMDTests/VeloxSpectrumSignalsTests.swift @@ -131,7 +131,8 @@ extension VeloxEMDSerialized { default: Issue.record("unexpected pin value for \(key)") } } - #expect(Set(signal.metadata.keys) == wantedKeys, "\(label): the metadata keys") + // `Sample.elements` is HyperSpy's step on RosettaSciIO's mapping: pinned apart (VeloxSpectrumElementsTests). + #expect(Set(signal.metadata.keys).subtracting(["Sample.elements"]) == wantedKeys, "\(label): the metadata keys") } } } diff --git a/docs/open-items.md b/docs/open-items.md index 7b9baf9..42fcce5 100644 --- a/docs/open-items.md +++ b/docs/open-items.md @@ -15,4 +15,4 @@ Not done from the task list (each is a change that needs its own pins): - TIFF series still refused by name: SubIFD series, OME, generic pyramids; a shaped description in a second ImageDescription tag (`description1`). - Header-less MIB; `small_datacube` rank-3 reshape; DM4 lazy 4D gather speed (the 4D row above is the eager load; the whole-scan Float32 tile case is the app's) and rank-3 or more than four dimensions; EMPAD `mode="acquire"` scan block; HDF5 version attributes int64 against int32; TIFF vendor branches (Zeiss SEM, TVIPS, Olympus SIS, JEOL SightX, Hamamatsu), ImageJ "nm", FEI2 extended MRC header; unbounded allocation from file sizes in Velox (`markersPerFrame`, frame transformations, detector times). - Then-list: HSPY/ZSPY, compressed TIFF, NCEM/Berkeley EMD, SER/EMI, Bruker BCF, other writers. -- `Sample.elements` (element selection) is not mapped for Velox single spectra; the four metadata groups are not parsed on that path. +- `Sample.elements` (element selection) is not mapped for Velox spectrum images (single spectra map it since #10); `_parse_image_display` is not run on the single-spectrum path of a SpectrumStream file. diff --git a/docs/parity.md b/docs/parity.md index ae8e38d..46eda04 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -28,7 +28,7 @@ every one of these, as D17 prescribes for readers. | Plugin | Extensions | Writes | Kit symbol | State | RosettaSciIO test and tolerance | Priority | Note | |---|---|---|---|---|---|---|---| | DigitalMicrograph | dm3, dm4 | no | `FormatoDM` `DMFile`, `DM4Datacube` | **partial** | test_digitalmicrograph.py: allclose, approx, array_equal, explicit rtol/atol/decimal in places | study | complex, RGB, bool, big-endian refused; metadata mapping partial; scan pixel size of EELS/EDS spectrum images (`xAxis`, `yAxis`, 0.11.0) and of 4D scans pinned to the bit | -| EMD | emd, de5 | yes | `FormatoEMD` `VeloxImageSignals`, `VeloxSpectrumImageSignals`, `VeloxSpectrumSignals` (0.9.0) | **partial** | test_emd_ncem.py, test_emd_prismatic.py, test_emd_velox.py: allclose, almost_equal, array_equal, deep_almost_equal (allclose), equal, explicit rtol/atol/decimal in places | study | Velox: images, spectrum images (live and real time per detector from the SI group, 0.11.0), single spectra, "DevelopersKit" accepted; compound FFT/DPC, `lazy=True` SI, `load_SI_image_stack`, `Sample.elements` not. NCEM/Berkeley EMD, prismatic and DE5: missing (useful) | +| EMD | emd, de5 | yes | `FormatoEMD` `VeloxImageSignals`, `VeloxSpectrumImageSignals`, `VeloxSpectrumSignals` (0.9.0) | **partial** | test_emd_ncem.py, test_emd_prismatic.py, test_emd_velox.py: allclose, almost_equal, array_equal, deep_almost_equal (allclose), equal, explicit rtol/atol/decimal in places | study | Velox: images, spectrum images (live and real time per detector from the SI group, 0.11.0), single spectra (with `Sample.elements`), "DevelopersKit" accepted; compound FFT/DPC, `lazy=True` SI, `load_SI_image_stack`, `Sample.elements` of a spectrum image not. NCEM/Berkeley EMD, prismatic and DE5: missing (useful) | | TIFF | tif, tiff | yes (some dims) | `FormatoTIFF` `TIFFSignals`, `TIFFAppImage` | **partial** | test_tiff.py: allclose, deep_almost_equal (allclose), equal, explicit rtol/atol/decimal in places | study | uncompressed strips; FEI, Gatan, ImageJ, plain, tifffile shaped series (0.11.0); OME, SubIFD series, generic pyramids, Zeiss SEM, TVIPS, Olympus SIS, JEOL SightX, Hamamatsu, compression, tiles refused by name | | MRC | mrc, ali | no | `FormatoMRC` `MRCReader`, `MRCAppImage` | **partial** | test_mrc.py, test_mrcz.py: almost_equal, array_almost_equal, array_equal, deep_almost_equal (allclose), equal, explicit rtol/atol/decimal in places | study | modes 0,1,2,6,12 and 4 refused (complex); DE movie options (`virtual_images`, `external_images`, `scan_file`), `navigation_shape`, `metadata_file` missing | | EMPAD | xml | no | `FormatoEMPAD` `EMPADReader` | **partial** | test_empad.py: allclose | study | `remove_nans` default false only | @@ -166,7 +166,20 @@ never written (RosettaSciIO tests `"General.item"`, a typo, as RosettaSciIO does as RosettaSciIO's `json.loads`, so the energy axis and the detector-metadata match take the same entry (since 0.0.3). Left: a key written twice in one object reads its first value here and its last in Python (no file seen has one). - **Velox metadata mapping:** the image path maps the file name and the beam energy; `VeloxSpectrumSignals` (0.9.0) maps every field - RosettaSciIO's `_get_mapping` maps for a single spectrum except `Sample.elements`. The images and spectrum images keep the narrower + RosettaSciIO's `_get_mapping` maps for a single spectrum. **`Sample.elements` of a single spectrum:** a `FormatoValue.texts` list, as + HyperSpy 2.4.0 sets it when it applies RosettaSciIO's `_convert_element_list` (`d.keys()[0]` works only on HyperSpy's + `DictionaryTreeBrowser`, whose `keys()` sorts the slugified keys; the version picks `SharedProperties/EDSSpectrumQuantificationSettings` + from 11 on, `Operations/ImageQuantificationOperation` before; `atomic_number2name` 1 to 119). Where Python raises (no entry, an entry + that is not an object, `int()` refusing, an atomic number outside the table, `int(version)` refusing) the read is refused, as HyperSpy's + `load` raises. The four file-level groups are parsed as `read_file` parses them, so a malformed entry refuses the read. Exact against + planted files and, when unzipped by `tools/test.sh`, the public ones (`tools/velox-elements/pins.py`). Left, refused here where Python + reads (no file has one): two keys of the settings group or of one entry that slugify alike (HyperSpy merges them); an empty entry + dataset in the four groups (RosettaSciIO logs a warning and keeps what it read); `NaN`/`Infinity` in their JSON (`json.loads` accepts + them, `JSONSerialization` does not). `_parse_image_display` is not run for a SpectrumStream file's spectra. **Live and real time of a single + spectrum:** RosettaSciIO maps only `DetectorMetadata.LiveTime`/`.RealTime`, which no public single spectrum holds; Velox 4 to 13 store + them per segment in `Detectors`. `VeloxEMDReader.spectrumMetadata()` gives each spectrum's metadata and + `VeloxMetadata.binaryResultSegmentTimes` the segments whose name contains `BinaryResult.Detector` (not disabled), to the bit against + `float()` of RosettaSciIO's `original_metadata`; not in the signals' metadata. The images and spectrum images keep the narrower mapping (camera length, magnification, instrument and stage are in `AcquisitionMetadata.velox`; for a spectrum image `AcquisitionMetadata.velox(spectrumImage:)` from `VeloxEMDReader.spectrumImageMetadata()`, 0.11.0, camera length, magnification and beam energy to the bit against RosettaSciIO's returned mapping). **Live and real time of a spectrum image (0.11.0):** RosettaSciIO maps none diff --git a/tools/velox-elements/pins.py b/tools/velox-elements/pins.py new file mode 100644 index 0000000..09e9f63 --- /dev/null +++ b/tools/velox-elements/pins.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Planted Velox single-spectrum files for `Sample.elements`, the four file-level metadata groups and the segment times. + + HYPERSPY_SITE_PACKAGES= \ + References/rsciio-venv/bin/python -I -B tools/velox-elements/pins.py + +writes Tests/FormatoEMDTests/Fixtures/elements/*.emd and Tests/FormatoEMDTests/Fixtures/elements/velox-elements-pins.json. + +Every file is a Velox `Spectrum` file (no `Data/Image`), built with the helpers of tools/velox-spectrum/pins.py, read by +RosettaSciIO 049e7d70 (`rsciio.emd.file_reader(select_type="single_spectrum")`). `Sample.elements` is a HyperSpy step: RosettaSciIO +returns `_convert_element_list` in its `mapping`, and that function indexes `d.keys()[0]`, which only HyperSpy's +`DictionaryTreeBrowser` allows (its `keys()` is a sorted list of the slugified keys). So the mapping is applied here with HyperSpy's +own class, in the loop of `hyperspy/io.py` `dict2signal` (`opattr in original_metadata`, `get_item`, the function, `set_item` when +not None), on an `original_metadata` built as `BaseSignal` builds it (`DictionaryTreeBrowser().add_dictionary(...)`). The class is +imported from HYPERSPY_SITE_PACKAGES, appended after the venv's own packages, so RosettaSciIO and numpy stay the venv's. + +Per file: RosettaSciIO's exception, or per spectrum the elements (a list, absent when the mapping does not apply, or the exception +the mapping raises: HyperSpy's `load` raises it too), `BinaryResult.Detector` and every `Detectors` entry of the spectrum's +`original_metadata` in written order (name, type, `Enabled`, `float()` of `LiveTime` and `RealTime` with their bits). RosettaSciIO +maps no live or real time for these files: their spectra hold no `DetectorMetadata` (asserted). +Built twice in the process, the outputs compared. +""" +import hashlib +import importlib.util +import json +import logging +import os +import shutil +import sys +import tempfile +import warnings +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "Tests" / "FormatoEMDTests" / "Fixtures" / "elements" +HYPERSPY_VERSION = "2.4.0" + + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +step1 = module("step1_pins", ROOT / "tools" / "step-1" / "pins.py") +SP = module("velox_spectrum_pins", ROOT / "tools" / "velox-spectrum" / "pins.py") + + +def tree_class(): + site = os.environ.get("HYPERSPY_SITE_PACKAGES") + if not site: + sys.exit("velox-elements/pins.py: set HYPERSPY_SITE_PACKAGES to the site-packages of a HyperSpy " + HYPERSPY_VERSION + " install") + sys.path.append(site) + from importlib.metadata import version + import hyperspy + if Path(hyperspy.__file__).resolve().parents[1] != Path(site).resolve(): + sys.exit(f"velox-elements/pins.py: hyperspy imported from {hyperspy.__file__}, not from {site}") + found = version("hyperspy") + if found != HYPERSPY_VERSION: + sys.exit(f"velox-elements/pins.py: HyperSpy is {found}, not {HYPERSPY_VERSION}") + from hyperspy.misc._utils import DictionaryTreeBrowser + return DictionaryTreeBrowser + + +V11_SETTINGS = "SharedProperties/EDSSpectrumQuantificationSettings" +V4_SETTINGS = "Operations/ImageQuantificationOperation" + + +def times_doc(names_times, binary): + """A spectrum's Detectors: one scanning detector, then X-ray segments (name, live, real, enabled) as Velox 4 to 13 write them.""" + det = {"Detector-0": {"DetectorName": "HAADF", "DetectorType": "ScanningDetector"}} + for i, (name, live, real, enabled) in enumerate(names_times, start=1): + d = {"DetectorName": name, "DetectorType": "AnalyticalDetector", "Dispersion": "5", "OffsetEnergy": "-480", + "PulseProcessTime": "3.0000000000000001e-006", "RealTime": real, "LiveTime": live} + if enabled is not None: + d["Enabled"] = enabled + det[f"Detector-{i}"] = d + return SP.spectrum_doc(Detectors=det, BinaryResult={"Detector": binary, "DetectorIndex": 1}) + + +FOUR = [("SuperXG21", "10.061437738684702", "11.36485629375", None), ("SuperXG22", "10.061437738684702", "11.36485629375", None), + ("SuperXG23", "10.061437738684702", "11.36485629375", None), ("SuperXG24", "10.061437738684702", "11.36485629375", None)] +SEGMENTS = [("SuperXG21", "27.582455349747178", "32.768026849999998", None), ("SuperXG22", "27.819320345368695", "32.767988875", None), + ("SuperXG23", "27.906697271813595", "32.767735250000001", None), ("SuperXG24", "27.700038087004355", "32.768037800000002", None)] +ULTRA = [("UltraX1", "2.9071471999999998", "2.9071471999999998", "true"), ("UltraX2", "2.9071471999999998", "2.9071471999999998", "true"), + ("UltraX3", "2.9071471999999998", "2.9071471999999998", "false"), ("UltraX4", "3.25", "3.5", "true")] + + +def write_group(f, path, entries): + """`path` a group of variable-length JSON strings, one per entry name (a value that is a str is written as is).""" + g = f.require_group(path) + for name, doc in entries: + text = doc if isinstance(doc, str) else json.dumps(doc) + g.create_dataset(name, data=np.array([text], dtype=object), dtype=SP.S, track_times=False) + + +def build(out): + ramp = np.arange(12, dtype=np.uint32).reshape(12, 1) * 3 + 2 + + def make(name, version, settings=None, extra=None, doc=None, spectra=1): + with SP.new_file(str(out / f"{name}.emd"), version) as f: + for k in range(spectra): + SP.add_spectrum(f, f"s{k}", ramp + k, doc if doc is not None else times_doc(FOUR, "SuperXG2")) + if settings is not None: + write_group(f, settings[0], settings[1]) + for path, entries in (extra or []): + write_group(f, path, entries) + + # Velox 4: elements in Operations, four segments named by "SuperXG2", every one matching. The public fei_emd_spectrum.emd's times. + make("el-v4", 4, (V4_SETTINGS, [("80e138836ebd4b7aad5be7d3dd071f47", {"elementSelection": ["41", "8"], "kind": "q"})]), + extra=[("Features/SIFeature", [("f1", {"x": 1})]), ("Displays/Other", [("d1", {"y": 2})])]) + # Velox 6: one segment named, the others state other times; elements stored as numbers. + make("el-v6-one-segment", 6, (V4_SETTINGS, [("fab3", {"elementSelection": [26, 27.9, "+8", " 1_4 "]})]), + doc=times_doc(SEGMENTS, "SuperXG24")) + # Velox 11: elements in SharedProperties; two entries, the first in HyperSpy's sorted order taken ("Number_0b..." before "a2..."). + make("el-v11-two-settings", 11, (V11_SETTINGS, [("a24a64ef30da49beb12dabaa598d9f16", {"elementSelection": ["6", "8"]}), + ("0b4c", {"elementSelection": ["20", "29", "1", "119"]})]), + extra=[("Operations/ImageQuantificationOperation", [("zz", {"elementSelection": ["79"]})])], spectra=2) + # The slugified order differs from the raw one: "B111" sorts before "Number_0b4c" (raw: "0b4c" first); "_a" is left out of + # HyperSpy's keys(); the inner key "element-Selection" reads as elementSelection. + make("el-slug-order", 11, (V11_SETTINGS, [("0b4c", {"elementSelection": ["8"]}), ("B111", {"element-Selection": ["26"]}), + ("_a", {"elementSelection": ["1"]})])) + # Velox 13: a disabled segment, and a segment the binary result does not name ("UltraX4" holds "UltraX"; all four match). + make("el-v13-disabled", 13, (V11_SETTINGS, [("34d7", {"elementSelection": [8, 26, 27]})]), doc=times_doc(ULTRA, "UltraX")) + # No elementSelection: the mapping gives []; an empty string gives [] too (iterating "" in Python). + make("el-empty-selection", 11, (V11_SETTINGS, [("0ea5", {"other": "x"})])) + make("el-empty-string", 4, (V4_SETTINGS, [("80e1", {"elementSelection": ""})])) + make("el-digit-string", 4, (V4_SETTINGS, [("80e1", {"elementSelection": "82"})])) + # The version picks the group: a Velox 11 file with Operations only maps nothing; a file with no group maps nothing. + make("el-v11-operations-only", 11, extra=[(V4_SETTINGS, [("80e1", {"elementSelection": ["8"]})])]) + make("el-none", 11) + # Refusals (HyperSpy raises in the mapping, or RosettaSciIO raises while reading the groups). + make("el-bad-number", 11, (V11_SETTINGS, [("e1", {"elementSelection": ["8", "abc"]})])) + make("el-z-120", 11, (V11_SETTINGS, [("e1", {"elementSelection": ["120"]})])) + make("el-z-0", 11, (V11_SETTINGS, [("e1", {"elementSelection": [0]})])) + make("el-null-selection", 11, (V11_SETTINGS, [("e1", {"elementSelection": None})])) + make("el-settings-list", 11, extra=[("SharedProperties", [("EDSSpectrumQuantificationSettings", ["8"])])]) + make("el-entry-not-object", 11, (V11_SETTINGS, [("e1", ["8"])])) + make("el-bad-json", 11, (V11_SETTINGS, [("e1", {"elementSelection": ["8"]})]), extra=[("Features/SIFeature", [("f1", "{not json")])]) + with SP.new_file(str(out / "el-empty-group.emd"), 11) as f: + SP.add_spectrum(f, "s0", ramp, times_doc(FOUR, "SuperXG2")) + f.require_group(V11_SETTINGS) + with SP.new_file(str(out / "el-version-text.emd"), 11) as f: # int("11.0") raises in _get_mapping + del f["Version"] + SP.write_json(f, "Version", {"version": "11.0", "format": "Velox"}) + SP.add_spectrum(f, "s0", ramp, times_doc(FOUR, "SuperXG2")) + with SP.new_file(str(out / "el-no-version-key.emd"), 11) as f: + del f["Version"] + SP.write_json(f, "Version", {"format": "Velox"}) + SP.add_spectrum(f, "s0", ramp, times_doc(FOUR, "SuperXG2")) + + +def float_record(raw): + if raw is None: + return None + return {"value": step1.jfloat(float(raw)), "bits": step1.bits(float(raw))} + + +def record(path, tree): + from rsciio.emd import file_reader + try: + dicts = file_reader(str(path), select_type="single_spectrum", lazy=False) + except Exception as e: # noqa: BLE001 + return {"raises": type(e).__name__} + out = [] + for d in dicts: + om = d["original_metadata"] + assert "DetectorMetadata" not in om, path + rec = {"binary_result_detector": om.get("BinaryResult", {}).get("Detector"), + "detectors": [{"key": k, "name": v.get("DetectorName"), "type": v.get("DetectorType"), "enabled": v.get("Enabled"), + "live_time_s": float_record(v.get("LiveTime")), "real_time_s": float_record(v.get("RealTime"))} + for k, v in om["Detectors"].items()]} + browser = tree() + browser.add_dictionary(om) + md = tree() + try: # hyperspy/io.py dict2signal, the mapping loop + for opattr, (mpattr, function) in d["mapping"].items(): + if opattr in browser: + value = browser.get_item(opattr) + if function is not None: + value = function(value) + if value is not None: + md.set_item(mpattr, value) + except Exception as e: # noqa: BLE001 + rec["elements"] = {"raises": type(e).__name__} + else: + if md.has_item("Sample.elements"): + rec["elements"] = {"value": list(md.get_item("Sample.elements"))} + out.append(rec) + return out + + +def public(tree): + """RosettaSciIO's public Velox files with single spectra (tools/velox-spectrum/pins.py's list), unzipped into a fresh folder: + the elements of each spectrum. These files are SpectrumStream or Spectrum files; sparse is in the venv.""" + import zipfile + data = step1.CLONE / "rsciio" / "tests" / "data" / "emd" + out = {} + with tempfile.TemporaryDirectory() as tmp: + for zname in sorted({z for z, _ in SP.PUBLIC}): + zipfile.ZipFile(data / zname).extractall(Path(tmp) / zname) + for zname, fname in SP.PUBLIC: + recs = record(Path(tmp) / zname / fname, tree) + out[f"{zname}/{fname}"] = recs if isinstance(recs, dict) else [{"elements": r.get("elements")} for r in recs] + return out + + +def run(out, tree): + warnings.simplefilter("ignore") + logging.disable(logging.CRITICAL) + step1.load_rsciio() + out.mkdir(parents=True, exist_ok=True) + build(out) + names = sorted(p.stem for p in out.glob("*.emd")) + pins = {"generator": "tools/velox-elements/pins.py", + "public": public(tree), + "reference": f"RosettaSciIO {step1.PIN} (rsciio.emd.file_reader, select_type=single_spectrum); the mapping applied with " + f"HyperSpy {HYPERSPY_VERSION}'s DictionaryTreeBrowser as hyperspy/io.py dict2signal applies it", + "files": {n: record(out / f"{n}.emd", tree) for n in names}, + "sha256": {n: hashlib.sha256((out / f"{n}.emd").read_bytes()).hexdigest() for n in names}} + return json.dumps(pins, indent=1, sort_keys=True, ensure_ascii=False, allow_nan=False) + "\n" + + +def main(): + tree = tree_class() + with tempfile.TemporaryDirectory() as a, tempfile.TemporaryDirectory() as b: + first, second = run(Path(a), tree), run(Path(b), tree) + if first != second: + sys.exit("velox-elements/pins.py: two builds differ") + if OUT.exists(): + shutil.rmtree(OUT) + shutil.copytree(a, OUT) + (OUT / "velox-elements-pins.json").write_text(first, encoding="utf-8") + print("wrote", OUT / "velox-elements-pins.json") + + +if __name__ == "__main__": + main()