energyml_storage/array.rs
1//! Finding, and later reading, the arrays that live outside the package.
2//!
3//! An EnergyML object rarely carries its numbers. It carries a *pointer* to
4//! them, and there are two spellings of that pointer, both in use:
5//!
6//! | | RESQML 2.0.1 / eml 2.0-2.2 | RESQML 2.2 / eml 2.3 |
7//! |---|---|---|
8//! | in the object | `Hdf5Dataset`: a DOR to an `EpcExternalPartReference`, plus `PathInHdfFile` | `ExternalDataArrayPart`: `URI`, `PathInExternalFile`, `MimeType`, `Count`, `StartIndex` |
9//! | file name | in the proxy's `.rels`, as an `externalResource` target | in the object, in `URI` |
10//!
11//! The newer form is the richer one: it carries the slice as well as the
12//! location, which is exactly what ETP's `GetDataSubarrays` needs. Both are
13//! read here, into one [`ArrayLocation`].
14//!
15//! A third pointer is **this library's extension, not the standard's**: a
16//! WITSML 2.1 `ChannelData` whose `FileUri` names an HDF5 file and whose `Data`
17//! holds the path of a dataset in it (ADR-0013).
18//! Located here like the other two, so that everything that walks the arrays
19//! of an object - a cut, an import, a rename, a prefetch - carries it along
20//! instead of losing it in silence.
21//!
22//! # The path inside the file is opaque
23//!
24//! `PathInExternalFile` and `PathInHdfFile` are **strings, and nothing else**.
25//! fesapi writes `/resqml22/<uuid>/Points` and the convention is convenient,
26//! but no part of the schema requires the uuid to be in there, or requires any
27//! shape at all. Nothing here parses, splits or validates the path - it is
28//! handed to the backend exactly as the object wrote it, and a package that
29//! calls its dataset `/my/points` works. `a_path_need_not_contain_a_uuid` in
30//! the tests pins that down, because it is the kind of assumption that gets
31//! added later by someone who has only ever seen fesapi files.
32//!
33//! # And the format is not necessarily HDF5
34//!
35//! Both spellings carry a MIME type, so both can point at something else - a
36//! CSV, a Parquet file, whatever a producer and a consumer agree on. eml 2.3
37//! puts it in the part (`MimeType`); eml 2.0-2.2 put it on the
38//! `EpcExternalPartReference`, where it is a **mandatory** element whose
39//! documentation reads "MIME type for HDF proxy is: application/x-hdf5 (by
40//! RESQML convention)" - a convention, therefore, not a rule. The MIME type is
41//! what picks the backend; HDF5 is merely the one that ships.
42//!
43//! [`locate`] is a **free function over an object already in memory**, not a
44//! method on a store. That is the rule the whole crate follows: the store does
45//! I/O, the logic sits outside it, and nothing has to be written twice for the
46//! synchronous and the asynchronous paths.
47
48use energyml_core::identity;
49use energyml_core::reflect::TypeKind;
50use energyml_core::value::{FieldValue, Reflect, Scalar};
51
52/// Where one array - or one slice of one - actually lives.
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub struct ArrayLocation {
55 /// The file, as the package names it. A bare file name in practice, though
56 /// the schema suggests a URI; both are accepted.
57 ///
58 /// Empty when the object points at a proxy the caller has not resolved:
59 /// see [`locate_with`].
60 pub file: String,
61 /// Path of the dataset inside the file, e.g. `/resqml22/<uuid>/Points`.
62 pub path_in_file: String,
63 /// MIME type declared for the data - `application/x-hdf5`, but the field
64 /// exists precisely so it need not be.
65 ///
66 /// eml 2.3 carries it in the part itself; eml 2.0-2.2 carry it on the
67 /// `EpcExternalPartReference`, which is why resolving a proxy yields this
68 /// as well as the file name (see [`ProxyTarget`]).
69 pub mime: Option<String>,
70 /// Start index per dimension. Empty means "from the beginning".
71 pub start: Vec<i64>,
72 /// Element count per dimension. Empty means "the whole dataset".
73 pub count: Vec<i64>,
74 /// Uuid of the `EpcExternalPartReference` this came through, for the older
75 /// form. `None` for the eml 2.3 form, which has no proxy.
76 pub proxy_uuid: Option<String>,
77 /// Dotted path of the array inside the object, for diagnostics:
78 /// `Geometry.Points.Coordinates`.
79 pub field_path: String,
80 /// The ETP address of the array: the URI of the data object whose resource
81 /// holds it, `DataArrayIdentifier.uri`, the path being
82 /// [`ArrayLocation::path_in_file`] (`pathInResource`). Filled by a store that
83 /// is addressed that way (an ETP dataspace); `None` for a file, which is
84 /// addressed by [`ArrayLocation::file`]. See [`ArrayLocation::etp`].
85 ///
86 /// `TODO(verify)`: which object's URI ETP 1.2 expects for the older form, the
87 /// `EpcExternalPartReference` or the object pointing at it (ETP 1.2 §13,
88 /// DataArray; Energistics Identifier Specification).
89 pub resource_uri: Option<String>,
90}
91
92impl ArrayLocation {
93 /// Whether the file is known. It is not, when the object points at a proxy
94 /// and no resolver was given.
95 ///
96 /// # A shape borrowed from the file, and what ETP will do with it
97 ///
98 /// "Resolved" here means *the store filled in a file name*, and that is a
99 /// disk word in a type that is meant to serve a network store too. It is
100 /// honest for an EPC: the `EpcExternalPartReference` is a real indirection
101 /// and following it is a real step, so an unresolved location is a genuine
102 /// state a caller can be handed.
103 ///
104 /// It does not carry over. An ETP dataspace has no files: a data array is
105 /// addressed by the URI of the object holding it plus a `pathInResource`,
106 /// and `GetDataArrays` takes exactly that pair. A client filling
107 /// [`ArrayLocation::file`] would be inventing a file name so that
108 /// `is_resolved` returns true, which is the wrong way round.
109 ///
110 /// Left as it is, deliberately, rather than generalized on a guess: the
111 /// choice is between widening `file` into an opaque "where the store keeps
112 /// it" string and giving the ETP client its own addressing with a
113 /// conversion at the seam. Which one is right depends on what the client
114 /// actually needs to pass to `GetDataArrays`, and that is settled when the
115 /// client is written, not before. It is recorded as an open question so
116 /// it is a decision taken rather than a leak nobody noticed.
117 pub fn is_resolved(&self) -> bool {
118 !self.file.is_empty()
119 }
120
121 /// A location addressed the ETP way: the URI of the object whose resource
122 /// holds the array, and the path inside that resource. The only place the
123 /// pair becomes a location, so that a store and a server never spell the
124 /// conversion twice.
125 pub fn etp(resource_uri: impl Into<String>, path_in_resource: impl Into<String>) -> Self {
126 Self {
127 path_in_file: path_in_resource.into(),
128 resource_uri: Some(resource_uri.into()),
129 ..Self::default()
130 }
131 }
132
133 /// The ETP address of this location, `(uri, pathInResource)`, when it has
134 /// one.
135 pub fn etp_address(&self) -> Option<(&str, &str)> {
136 self.resource_uri
137 .as_deref()
138 .map(|uri| (uri, self.path_in_file.as_str()))
139 }
140}
141
142/// What an `EpcExternalPartReference` stands for.
143///
144/// Two things, not one: the older EnergyML form keeps the file name in the
145/// proxy's `.rels` **and** the MIME type on the proxy object, so resolving a
146/// proxy has to yield both or the format is lost and every external file is
147/// assumed to be HDF5.
148#[derive(Debug, Clone, PartialEq, Eq, Default)]
149pub struct ProxyTarget {
150 /// The file the proxy names.
151 pub file: String,
152 /// The MIME type the proxy declares, when it declares a usable one.
153 pub mime: Option<String>,
154}
155
156impl ProxyTarget {
157 /// A target that is only a file name.
158 pub fn file(name: impl Into<String>) -> Self {
159 Self {
160 file: name.into(),
161 mime: None,
162 }
163 }
164}
165
166/// Turns the uuid of an `EpcExternalPartReference` into what it stands for.
167///
168/// In an EPC that means reading the proxy's `.rels` for the file name and the
169/// proxy object for the MIME type. Implemented by the store, because only the
170/// store can see either.
171pub trait ProxyResolver {
172 /// What the proxy with this uuid names, if the package knows.
173 fn resolve(&self, proxy_uuid: &str) -> Option<ProxyTarget>;
174}
175
176impl<F: Fn(&str) -> Option<ProxyTarget>> ProxyResolver for F {
177 fn resolve(&self, proxy_uuid: &str) -> Option<ProxyTarget> {
178 self(proxy_uuid)
179 }
180}
181
182/// Nothing to resolve with: proxies stay unresolved, the eml 2.3 form still works.
183pub struct NoProxies;
184
185impl ProxyResolver for NoProxies {
186 fn resolve(&self, _proxy_uuid: &str) -> Option<ProxyTarget> {
187 None
188 }
189}
190
191/// Every array the object points at, both spellings, without a resolver.
192///
193/// Locations that go through a proxy come back with an empty `file` and a
194/// `proxy_uuid` set - enough to ask the store afterwards.
195pub fn locate(object: &dyn Reflect) -> Vec<ArrayLocation> {
196 locate_with(object, &NoProxies)
197}
198
199/// Every array the object points at, resolving proxies through `resolver`.
200pub fn locate_with(object: &dyn Reflect, resolver: &dyn ProxyResolver) -> Vec<ArrayLocation> {
201 let mut out = Vec::new();
202 walk(object, String::new(), resolver, &mut out, 0);
203 out
204}
205
206/// The type names that mean "here is where the numbers are".
207///
208/// Three, not two, and the middle one was missing until 31/08. eml 2.1 and 2.2
209/// carry `ExternalDatasetPart` - reached through a field spelled
210/// `ExternalFileProxy` - which is the older shape's proxy reference with the
211/// newer shape's `PathInExternalFile`. It is neither of its neighbours, and
212/// nothing matched it: every array of every eml 2.1/2.2 package came back as
213/// "declares no external data at all". That is witsml 2.0, prodml 2.0 and
214/// resqml 2.2_dev3, whose 362 polylines refused to draw with their coordinates
215/// sitting right there in the document.
216const NEW_FORM: &str = "ExternalDataArrayPart";
217const MID_FORM: &str = "ExternalDatasetPart";
218const OLD_FORM: &str = "Hdf5Dataset";
219
220/// A WITSML `ChannelData` whose rows are a dataset of an HDF5 file - **an
221/// extension beyond the standard** (ADR-0013).
222///
223/// WITSML TRG v2.1 §7.2 p. 146 gives `ChannelData` two forms: `Data`, *"The
224/// data blob in JSON form"*, and `FileUri`, *"The URI of a file containing the
225/// bulk data. If this field is non-null, then the data field is ignored."*
226/// The extension is the pair: `FileUri` names the file as the standard has it
227/// name one (a relative URI, the same string an `ExternalDataArrayPart.URI`
228/// of the package holds), and `Data` - which the standard ignores once
229/// `FileUri` is set - holds the **absolute path of the dataset** inside it.
230/// [`is_channel_data_in_file`] is the one definition of the form.
231const CHANNEL_DATA: &str = "ChannelData";
232
233/// Whether a `ChannelData`'s `FileUri` and `Data` are the extension of
234/// `CHANNEL_DATA` - the **one** definition, which the locator here and the
235/// series reader of `energyml-domain` both ask.
236///
237/// Both present, and `Data` an absolute HDF5 path: it begins with `/`, which
238/// **no JSON text can** - RFC 8259 §2, a JSON text is optional whitespace then
239/// a value, and a value begins with `{`, `[`, `"`, `-`, a digit, `t`, `f` or
240/// `n`. So the test is not a guess on content: the two syntaxes are disjoint,
241/// and a standard file that writes `FileUri` **beside** an inline blob (the
242/// standard then ignores the blob, §7.2 p. 146) stays the standard's form -
243/// never a dataset path made of JSON. `FileUri` alone is the standard's form
244/// too (a bulk file in the release notes' format, not an HDF5 dataset).
245pub fn is_channel_data_in_file(file_uri: &str, data: &str) -> bool {
246 !file_uri.trim().is_empty() && data.starts_with('/')
247}
248
249/// The MIME type the extension implies: the file `FileUri` names is HDF5 by
250/// the extension's own definition - `ChannelData` has no field to say it.
251const CHANNEL_DATA_MIME: &str = "application/x-hdf5";
252
253/// The field of a located part that names its **file** - what a rename or an
254/// import rewrites. `uri` for the eml 2.3 form; `file_uri` for a WITSML
255/// `ChannelData` (`CHANNEL_DATA`). The two proxy forms name no file here
256/// (their proxy's `.rels` does), and are never asked.
257pub fn file_field(part_type: &str) -> &'static str {
258 match part_type {
259 CHANNEL_DATA => "file_uri",
260 _ => "uri",
261 }
262}
263
264/// The field of a located part that holds the **path inside the file** -
265/// `path_in_hdf_file` for the eml 2.0 form, `data` for a WITSML `ChannelData`,
266/// `path_in_external_file` otherwise.
267pub fn path_field(part_type: &str) -> &'static str {
268 match part_type {
269 OLD_FORM => "path_in_hdf_file",
270 CHANNEL_DATA => "data",
271 _ => "path_in_external_file",
272 }
273}
274
275fn walk(
276 object: &dyn Reflect,
277 path: String,
278 resolver: &dyn ProxyResolver,
279 out: &mut Vec<ArrayLocation>,
280 depth: u32,
281) {
282 // Objects reference each other only through DORs, never by containment, so
283 // the tree is finite. The bound is a seatbelt, not a design assumption.
284 if depth > 64 {
285 return;
286 }
287
288 let info = object.type_info();
289 match info.name {
290 NEW_FORM => {
291 out.push(from_new_form(object, &path));
292 return;
293 }
294 MID_FORM => {
295 out.push(from_mid_form(object, &path, resolver));
296 return;
297 }
298 OLD_FORM => {
299 out.push(from_old_form(object, &path, resolver));
300 return;
301 }
302 CHANNEL_DATA => {
303 if let Some(found) = from_channel_data(object, &path) {
304 out.push(found);
305 }
306 return;
307 }
308 _ => {}
309 }
310
311 for field in info.fields {
312 let Some(value) = object.field(field.name) else {
313 continue;
314 };
315 let child_path = if path.is_empty() {
316 field.xml_name.to_string()
317 } else {
318 format!("{path}.{}", field.xml_name)
319 };
320 walk_value(&value, child_path, resolver, out, depth);
321 }
322}
323
324fn walk_value(
325 value: &FieldValue<'_>,
326 path: String,
327 resolver: &dyn ProxyResolver,
328 out: &mut Vec<ArrayLocation>,
329 depth: u32,
330) {
331 match value {
332 FieldValue::Object(child) => walk(*child, path, resolver, out, depth + 1),
333 FieldValue::List(items) => {
334 for (i, item) in items.iter().enumerate() {
335 walk_value(item, format!("{path}[{i}]"), resolver, out, depth);
336 }
337 }
338 _ => {}
339 }
340}
341
342/// A `ChannelData` naming a file **and** a dataset - [`is_channel_data_in_file`];
343/// `None` for either standard form, which carries no dataset to locate.
344///
345/// The path is handed over as written, like every path here: never trimmed or
346/// split beyond the one character that tells the form.
347fn from_channel_data(object: &dyn Reflect, path: &str) -> Option<ArrayLocation> {
348 let file = string_field(object, "file_uri")?;
349 let dataset = string_field(object, "data")?;
350 if !is_channel_data_in_file(&file, &dataset) {
351 return None;
352 }
353 Some(ArrayLocation {
354 file,
355 path_in_file: dataset,
356 mime: Some(CHANNEL_DATA_MIME.to_string()),
357 start: Vec::new(),
358 count: Vec::new(),
359 proxy_uuid: None,
360 field_path: path.to_string(),
361 resource_uri: None,
362 })
363}
364
365fn from_new_form(object: &dyn Reflect, path: &str) -> ArrayLocation {
366 ArrayLocation {
367 file: string_field(object, "uri").unwrap_or_default(),
368 path_in_file: string_field(object, "path_in_external_file").unwrap_or_default(),
369 mime: string_field(object, "mime_type"),
370 start: int_list(object, "start_index"),
371 count: int_list(object, "count"),
372 proxy_uuid: None,
373 field_path: path.to_string(),
374 resource_uri: None,
375 }
376}
377
378/// eml 2.1 / 2.2: a proxy reference like the old form, a path like the new one.
379///
380/// The DOR is spelled `EpcExternalPartReference` here rather than `HdfProxy`,
381/// and it names the same thing: the part whose `.rels` carries the file name.
382/// `Count` and `StartIndex` are single values, not lists - this shape predates
383/// the multi-dimensional slicing eml 2.3 added - but they go into the same
384/// vectors, so every reader above sees one kind of `ArrayLocation`.
385fn from_mid_form(object: &dyn Reflect, path: &str, resolver: &dyn ProxyResolver) -> ArrayLocation {
386 let proxy_uuid = object
387 .field("epc_external_part_reference")
388 .and_then(|value| match value {
389 FieldValue::Object(dor) => identity::uuid_of_reflect(dor).map(str::to_string),
390 _ => None,
391 })
392 .filter(|uuid| !uuid.is_empty());
393
394 let target = proxy_uuid
395 .as_deref()
396 .and_then(|uuid| resolver.resolve(uuid))
397 .unwrap_or_default();
398 ArrayLocation {
399 file: target.file,
400 path_in_file: string_field(object, "path_in_external_file").unwrap_or_default(),
401 // Declared by the proxy, as in the old form: this part says where in
402 // the file, never which file.
403 mime: target.mime,
404 // **`StartIndex` and `Count` are deliberately not carried over.**
405 //
406 // They are single values describing a *flat* range - `ExternalFileProxy`
407 // repeats, so an array may be split across parts - while
408 // `ArrayLocation::start`/`count` are the per-dimension hyperslab eml 2.3
409 // introduced. The two are not the same thing, and passing one as the
410 // other is not a small error: on this corpus it made every polyline ask
411 // for one dimension of a two-dimensional dataset and refuse, with the
412 // coordinates sitting right there. Empty means "the whole dataset",
413 // which is what the old form has always done and what a single part
414 // means anyway.
415 //
416 // A genuinely *split* array would need flat-range reading, which no
417 // backend here offers; it would come back as one location per part,
418 // each naming the whole dataset. No package in the corpus splits one,
419 // so that is a gap named rather than a guess made.
420 start: Vec::new(),
421 count: Vec::new(),
422 proxy_uuid,
423 field_path: path.to_string(),
424 resource_uri: None,
425 }
426}
427
428fn from_old_form(object: &dyn Reflect, path: &str, resolver: &dyn ProxyResolver) -> ArrayLocation {
429 // `HdfProxy` is a DataObjectReference; its uuid names the
430 // `EpcExternalPartReference` whose `.rels` carries the file name.
431 let proxy_uuid = object
432 .field("hdf_proxy")
433 .and_then(|value| match value {
434 FieldValue::Object(dor) => identity::uuid_of_reflect(dor).map(str::to_string),
435 _ => None,
436 })
437 .filter(|uuid| !uuid.is_empty());
438
439 let target = proxy_uuid
440 .as_deref()
441 .and_then(|uuid| resolver.resolve(uuid))
442 .unwrap_or_default();
443 ArrayLocation {
444 file: target.file,
445 path_in_file: string_field(object, "path_in_hdf_file").unwrap_or_default(),
446 // Declared by the proxy, not by this part - which is the whole reason
447 // resolving a proxy returns two things.
448 mime: target.mime,
449 start: Vec::new(),
450 count: Vec::new(),
451 proxy_uuid,
452 field_path: path.to_string(),
453 resource_uri: None,
454 }
455}
456
457fn string_field(object: &dyn Reflect, name: &str) -> Option<String> {
458 match object.field(name)? {
459 FieldValue::Scalar(Scalar::Str(s)) if !s.is_empty() => Some(s.to_string()),
460 _ => None,
461 }
462}
463
464fn int_list(object: &dyn Reflect, name: &str) -> Vec<i64> {
465 let Some(value) = object.field(name) else {
466 return Vec::new();
467 };
468 let one = |v: &FieldValue<'_>| match v {
469 FieldValue::Scalar(Scalar::Int(i)) => Some(*i),
470 FieldValue::Scalar(Scalar::UInt(u)) => i64::try_from(*u).ok(),
471 _ => None,
472 };
473 match value {
474 FieldValue::List(items) => items.iter().filter_map(one).collect(),
475 other => one(&other).into_iter().collect(),
476 }
477}
478
479/// Which end of a multi-byte number the file writes first.
480///
481/// Not part of [`ArrayMetadata`], and that is deliberate: ETP's
482/// `GetDataArrayMetadata` answers a shape and a type and says nothing about
483/// byte order, because a transfer defines its own. This is a property of **the
484/// file**, and it matters to exactly one kind of caller - one that must write
485/// down how the bytes are stored, as an EML 2.3 external array declaration
486/// does. So it is asked for separately, by
487/// [`crate::backend::ArrayBackend::byte_order`], and never inferred.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum ByteOrder {
490 /// Least significant byte first.
491 LittleEndian,
492 /// Most significant byte first.
493 BigEndian,
494}
495
496impl std::fmt::Display for ByteOrder {
497 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498 f.write_str(match self {
499 ByteOrder::LittleEndian => "little-endian",
500 ByteOrder::BigEndian => "big-endian",
501 })
502 }
503}
504
505/// Shape and element type of a dataset, without reading it.
506///
507/// The answer to ETP's `GetDataArrayMetadata`, and what a viewer needs before
508/// deciding whether it can afford the data.
509///
510/// Built with [`ArrayMetadata::new`]; `#[non_exhaustive]`, so that what ETP adds
511/// to `DataArrayMetadata` is not a breaking change for every caller.
512#[derive(Debug, Clone, PartialEq, Eq)]
513#[non_exhaustive]
514pub struct ArrayMetadata {
515 /// Number of elements along each dimension.
516 pub dimensions: Vec<i64>,
517 /// Element type, as the backend names it (`double`, `int64`, …) - the type
518 /// the values travel in (ETP's `transportArrayType`).
519 pub element_type: String,
520 /// ETP `logicalArrayType`, when the store distinguishes what the values mean
521 /// from how they travel (an array of booleans carried as bytes). `None` when
522 /// it does not: the element type is then all there is to say.
523 pub logical_type: Option<String>,
524 /// ETP `preferredSubarrayDimensions`: the block a store would rather be read
525 /// by (its chunking). Empty when it has no preference.
526 pub preferred_subarray_dimensions: Vec<i64>,
527 /// ETP `storeCreated` of the array (microseconds since the Unix epoch, UTC).
528 /// `None` when the store does not keep it.
529 pub store_created: Option<i64>,
530 /// ETP `storeLastWrite` of the array, same unit. `None` when not kept.
531 pub store_last_write: Option<i64>,
532}
533
534impl ArrayMetadata {
535 /// A shape and an element type; everything the store may not know is left
536 /// unknown.
537 pub fn new(dimensions: Vec<i64>, element_type: impl Into<String>) -> Self {
538 Self {
539 dimensions,
540 element_type: element_type.into(),
541 logical_type: None,
542 preferred_subarray_dimensions: Vec::new(),
543 store_created: None,
544 store_last_write: None,
545 }
546 }
547}
548
549/// A block of numbers, in the type the file holds them.
550///
551/// **Typed, not converted.** A dataset of doubles comes back as `Vec<f64>`,
552/// integers as `Vec<i32>`, and so on - because the point of reading a million
553/// points is to hand them to something that draws them, and every conversion on
554/// the way is a copy of the whole array. See [`ArrayData`] for the counting.
555#[derive(Debug, Clone, PartialEq)]
556#[non_exhaustive]
557pub enum ArrayValues {
558 /// `H5T_IEEE_F64` and friends.
559 F64(Vec<f64>),
560 /// 32-bit floats.
561 F32(Vec<f32>),
562 /// Signed integers, by width.
563 I64(Vec<i64>),
564 /// Signed 32-bit.
565 I32(Vec<i32>),
566 /// Signed 16-bit.
567 I16(Vec<i16>),
568 /// Signed 8-bit.
569 I8(Vec<i8>),
570 /// Unsigned 64-bit.
571 U64(Vec<u64>),
572 /// Unsigned 32-bit.
573 U32(Vec<u32>),
574 /// Unsigned 16-bit.
575 U16(Vec<u16>),
576 /// Unsigned 8-bit, and booleans, which HDF5 stores as bytes.
577 U8(Vec<u8>),
578}
579
580impl ArrayValues {
581 /// How many values there are.
582 pub fn len(&self) -> usize {
583 match self {
584 Self::F64(v) => v.len(),
585 Self::F32(v) => v.len(),
586 Self::I64(v) => v.len(),
587 Self::I32(v) => v.len(),
588 Self::I16(v) => v.len(),
589 Self::I8(v) => v.len(),
590 Self::U64(v) => v.len(),
591 Self::U32(v) => v.len(),
592 Self::U16(v) => v.len(),
593 Self::U8(v) => v.len(),
594 }
595 }
596
597 /// Whether there are none.
598 pub fn is_empty(&self) -> bool {
599 self.len() == 0
600 }
601
602 /// The element type, named the way [`ArrayMetadata`] names it.
603 /// Every element widened to `f64` - see [`ArrayData::to_f64`], which is
604 /// this with the shape beside it. **This copies.**
605 pub fn to_f64(&self) -> Vec<f64> {
606 match self {
607 ArrayValues::F64(v) => v.clone(),
608 ArrayValues::F32(v) => v.iter().map(|x| *x as f64).collect(),
609 ArrayValues::I64(v) => v.iter().map(|x| *x as f64).collect(),
610 ArrayValues::I32(v) => v.iter().map(|x| *x as f64).collect(),
611 ArrayValues::I16(v) => v.iter().map(|x| *x as f64).collect(),
612 ArrayValues::I8(v) => v.iter().map(|x| *x as f64).collect(),
613 ArrayValues::U64(v) => v.iter().map(|x| *x as f64).collect(),
614 ArrayValues::U32(v) => v.iter().map(|x| *x as f64).collect(),
615 ArrayValues::U16(v) => v.iter().map(|x| *x as f64).collect(),
616 ArrayValues::U8(v) => v.iter().map(|x| *x as f64).collect(),
617 }
618 }
619
620 /// The element type, named the way [`ArrayMetadata`] names it.
621 pub fn element_type(&self) -> &'static str {
622 match self {
623 Self::F64(_) => "double",
624 Self::F32(_) => "float",
625 Self::I64(_) => "int64",
626 Self::I32(_) => "int32",
627 Self::I16(_) => "int16",
628 Self::I8(_) => "int8",
629 Self::U64(_) => "uint64",
630 Self::U32(_) => "uint32",
631 Self::U16(_) => "uint16",
632 Self::U8(_) => "uint8",
633 }
634 }
635
636 /// The same memory seen as bytes. **No copy** - a borrow, for a caller
637 /// about to hand the block to a GPU, a socket or a file.
638 pub fn as_bytes(&self) -> &[u8] {
639 match self {
640 Self::F64(v) => bytemuck::cast_slice(v),
641 Self::F32(v) => bytemuck::cast_slice(v),
642 Self::I64(v) => bytemuck::cast_slice(v),
643 Self::I32(v) => bytemuck::cast_slice(v),
644 Self::I16(v) => bytemuck::cast_slice(v),
645 Self::I8(v) => bytemuck::cast_slice(v),
646 Self::U64(v) => bytemuck::cast_slice(v),
647 Self::U32(v) => bytemuck::cast_slice(v),
648 Self::U16(v) => bytemuck::cast_slice(v),
649 Self::U8(v) => v,
650 }
651 }
652}
653
654/// A block of numbers, as read, with the shape it came in.
655///
656/// # How many copies
657///
658/// Reading a compressed or chunked dataset cannot be done without the library
659/// materialising it somewhere, so **one** copy - from the file into the
660/// caller's memory - is the floor. What this type is shaped to avoid is the
661/// second and third:
662///
663/// | | |
664/// |---|---|
665/// | [`ArrayData::values`] | filled straight by the backend. One copy |
666/// | [`ArrayValues::as_bytes`] | a borrow of that same memory. **Zero** |
667/// | [`ArrayData::f64`] and friends | a borrow when the type already matches. **Zero** |
668/// | [`ArrayData::to_f64`] | widens every element. One more copy, and you asked for it |
669///
670/// And [`crate::ArrayBackend::read_into`] removes even the first allocation:
671/// the caller supplies the buffer - a vertex buffer, a memory-mapped region -
672/// and the file is decoded straight into it.
673#[derive(Debug, Clone, PartialEq)]
674pub struct ArrayData {
675 /// Shape of what was read.
676 pub metadata: ArrayMetadata,
677 /// The values, in the file's own element type.
678 pub values: ArrayValues,
679}
680
681impl ArrayData {
682 /// How many elements the shape promises.
683 pub fn element_count(&self) -> usize {
684 self.metadata.dimensions.iter().product::<i64>().max(0) as usize
685 }
686
687 /// The block as bytes, borrowed. No copy.
688 pub fn as_bytes(&self) -> &[u8] {
689 self.values.as_bytes()
690 }
691
692 /// The values as `f64`, **borrowed**, when that is what the file holds.
693 ///
694 /// The path a 3D application takes for coordinates: no conversion, no
695 /// allocation, just the numbers.
696 pub fn f64(&self) -> Option<&[f64]> {
697 match &self.values {
698 ArrayValues::F64(v) => Some(v),
699 _ => None,
700 }
701 }
702
703 /// The values as `f32`, borrowed, when that is what the file holds.
704 pub fn f32(&self) -> Option<&[f32]> {
705 match &self.values {
706 ArrayValues::F32(v) => Some(v),
707 _ => None,
708 }
709 }
710
711 /// The values as `i64`, borrowed, when that is what the file holds.
712 pub fn i64(&self) -> Option<&[i64]> {
713 match &self.values {
714 ArrayValues::I64(v) => Some(v),
715 _ => None,
716 }
717 }
718
719 /// The values as `u32`, borrowed, when that is what the file holds -
720 /// what RESQML uses for indices.
721 pub fn u32(&self) -> Option<&[u32]> {
722 match &self.values {
723 ArrayValues::U32(v) => Some(v),
724 _ => None,
725 }
726 }
727
728 /// Every element widened to `f64`.
729 ///
730 /// **This copies**, and it is the only method here that does. Use
731 /// [`ArrayData::f64`] when the file already holds doubles, which for
732 /// coordinates it does.
733 pub fn to_f64(&self) -> Vec<f64> {
734 self.values.to_f64()
735 }
736}
737
738/// A buffer the caller owns, for a backend to decode straight into.
739///
740/// The zero-allocation path: an application that already holds the memory the
741/// numbers must end up in - a vertex buffer, a mapped file, a slice of a larger
742/// block - passes it here and the file is read directly into it. Nothing is
743/// allocated, nothing is copied afterwards.
744#[non_exhaustive]
745pub enum ArrayBuffer<'a> {
746 /// Doubles.
747 F64(&'a mut [f64]),
748 /// Floats.
749 F32(&'a mut [f32]),
750 /// Signed 64-bit.
751 I64(&'a mut [i64]),
752 /// Signed 32-bit.
753 I32(&'a mut [i32]),
754 /// Unsigned 64-bit.
755 U64(&'a mut [u64]),
756 /// Unsigned 32-bit.
757 U32(&'a mut [u32]),
758 /// Bytes.
759 U8(&'a mut [u8]),
760}
761
762impl ArrayBuffer<'_> {
763 /// How many elements it can take.
764 pub fn len(&self) -> usize {
765 match self {
766 Self::F64(b) => b.len(),
767 Self::F32(b) => b.len(),
768 Self::I64(b) => b.len(),
769 Self::I32(b) => b.len(),
770 Self::U64(b) => b.len(),
771 Self::U32(b) => b.len(),
772 Self::U8(b) => b.len(),
773 }
774 }
775
776 /// Whether it can take none.
777 pub fn is_empty(&self) -> bool {
778 self.len() == 0
779 }
780
781 /// The element type it expects, named as [`ArrayMetadata`] names it.
782 pub fn element_type(&self) -> &'static str {
783 match self {
784 Self::F64(_) => "double",
785 Self::F32(_) => "float",
786 Self::I64(_) => "int64",
787 Self::I32(_) => "int32",
788 Self::U64(_) => "uint64",
789 Self::U32(_) => "uint32",
790 Self::U8(_) => "uint8",
791 }
792 }
793}
794
795/// Whether a type is one of the two array pointers.
796///
797/// Useful to a UI that wants to show "this field holds data elsewhere" without
798/// walking into it.
799pub fn is_array_pointer(info: &energyml_core::reflect::TypeInfo) -> bool {
800 info.kind == TypeKind::Struct && (info.name == NEW_FORM || info.name == OLD_FORM)
801}
802
803/// Where an array's numbers were actually found, and why there.
804///
805/// The distance between `declared` and `path` is the whole point: it is what a
806/// person needs told when a package has been renamed, and what the repair
807/// utility writes back so the next reader does not have to search at all.
808#[derive(Clone, PartialEq, Eq, Debug)]
809pub struct Resolution {
810 /// The file name the document wrote. Empty when it wrote none.
811 pub declared: String,
812 /// The file the numbers came out of.
813 pub path: std::path::PathBuf,
814 /// Which kind of place that was - see [`crate::backend::Origin`].
815 pub origin: crate::backend::Origin,
816}
817
818impl Resolution {
819 /// The bare file name of the answer, which is what a package should say.
820 pub fn name(&self) -> String {
821 self.path
822 .file_name()
823 .map(|n| n.to_string_lossy().into_owned())
824 .unwrap_or_else(|| self.path.display().to_string())
825 }
826
827 /// Whether the document already said the right thing.
828 pub fn agrees(&self) -> bool {
829 !self.origin.is_a_departure()
830 }
831}