Skip to main content

qmbed/
archive.rs

1use std::collections::BTreeMap;
2use std::fs::File;
3use std::io::{Read, Write};
4use std::path::Path;
5
6use num_complex::Complex64;
7use zip::write::SimpleFileOptions;
8use zip::{CompressionMethod, ZipArchive, ZipWriter};
9
10use crate::basis::{Basis, ErasedState};
11use crate::operator::{
12    LinearOperator, MatrixFormat, Operator, QuantumComponent, QuantumOperator, materialize_dense,
13};
14use crate::{QmbedError, Result};
15
16pub const ARCHIVE_VERSION: u8 = 1;
17/// Current portable basis-manifest archive version.
18pub const BASIS_ARCHIVE_VERSION: u8 = 1;
19
20fn archive_error(error: impl std::fmt::Display) -> QmbedError {
21    QmbedError::Archive(error.to_string())
22}
23
24fn npy_header(descriptor: &str, shape: &[usize]) -> Result<Vec<u8>> {
25    let shape = if shape.len() == 1 {
26        format!("({},)", shape[0])
27    } else {
28        format!(
29            "({})",
30            shape
31                .iter()
32                .map(usize::to_string)
33                .collect::<Vec<_>>()
34                .join(", ")
35        )
36    };
37    let dictionary =
38        format!("{{'descr': '{descriptor}', 'fortran_order': False, 'shape': {shape}, }}");
39    let padding = (16 - ((10 + dictionary.len() + 1) % 16)) % 16;
40    let header_length = dictionary
41        .len()
42        .checked_add(padding)
43        .and_then(|value| value.checked_add(1))
44        .ok_or_else(|| QmbedError::Archive("NPY header length overflow".into()))?;
45    let header_length = u16::try_from(header_length)
46        .map_err(|_| QmbedError::Archive("NPY v1 header is too long".into()))?;
47    let mut output = Vec::with_capacity(10 + usize::from(header_length));
48    output.extend_from_slice(b"\x93NUMPY");
49    output.extend_from_slice(&[1, 0]);
50    output.extend_from_slice(&header_length.to_le_bytes());
51    output.extend_from_slice(dictionary.as_bytes());
52    output.extend(std::iter::repeat_n(b' ', padding));
53    output.push(b'\n');
54    Ok(output)
55}
56
57fn write_u8_npy(writer: &mut impl Write, values: &[u8]) -> Result<()> {
58    writer
59        .write_all(&npy_header("|u1", &[values.len()])?)
60        .map_err(archive_error)?;
61    writer.write_all(values).map_err(archive_error)
62}
63
64fn write_u64_npy(writer: &mut impl Write, values: &[u64]) -> Result<()> {
65    writer
66        .write_all(&npy_header("<u8", &[values.len()])?)
67        .map_err(archive_error)?;
68    for value in values {
69        writer
70            .write_all(&value.to_le_bytes())
71            .map_err(archive_error)?;
72    }
73    Ok(())
74}
75
76fn write_complex_npy(
77    writer: &mut impl Write,
78    rows: usize,
79    columns: usize,
80    values: &[Complex64],
81) -> Result<()> {
82    if values.len() != rows.saturating_mul(columns) {
83        return Err(QmbedError::DimensionMismatch(
84            "archive matrix values do not match its shape".into(),
85        ));
86    }
87    writer
88        .write_all(&npy_header("<c16", &[rows, columns])?)
89        .map_err(archive_error)?;
90    for value in values {
91        writer
92            .write_all(&value.re.to_le_bytes())
93            .and_then(|()| writer.write_all(&value.im.to_le_bytes()))
94            .map_err(archive_error)?;
95    }
96    Ok(())
97}
98
99fn parse_npy_header(reader: &mut impl Read) -> Result<(String, Vec<usize>)> {
100    let mut magic = [0_u8; 6];
101    reader.read_exact(&mut magic).map_err(archive_error)?;
102    if &magic != b"\x93NUMPY" {
103        return Err(QmbedError::Archive("invalid NPY magic".into()));
104    }
105    let mut version = [0_u8; 2];
106    reader.read_exact(&mut version).map_err(archive_error)?;
107    let header_length = match version[0] {
108        1 => {
109            let mut bytes = [0_u8; 2];
110            reader.read_exact(&mut bytes).map_err(archive_error)?;
111            usize::from(u16::from_le_bytes(bytes))
112        }
113        2 | 3 => {
114            let mut bytes = [0_u8; 4];
115            reader.read_exact(&mut bytes).map_err(archive_error)?;
116            usize::try_from(u32::from_le_bytes(bytes))
117                .map_err(|_| QmbedError::Archive("NPY header is too large".into()))?
118        }
119        _ => return Err(QmbedError::Archive("unsupported NPY version".into())),
120    };
121    let mut header = vec![0_u8; header_length];
122    reader.read_exact(&mut header).map_err(archive_error)?;
123    let header = std::str::from_utf8(&header).map_err(archive_error)?.trim();
124    if header.contains("'fortran_order': True") || header.contains("\"fortran_order\": True") {
125        return Err(QmbedError::Archive(
126            "Fortran-ordered NPY matrices are not supported".into(),
127        ));
128    }
129    let descriptor_marker = if header.contains("'descr':") {
130        "'descr':"
131    } else {
132        "\"descr\":"
133    };
134    let descriptor_tail = header
135        .split_once(descriptor_marker)
136        .ok_or_else(|| QmbedError::Archive("NPY descriptor is missing".into()))?
137        .1
138        .trim_start();
139    let quote = descriptor_tail
140        .chars()
141        .next()
142        .filter(|character| *character == '\'' || *character == '"')
143        .ok_or_else(|| QmbedError::Archive("invalid NPY descriptor".into()))?;
144    let descriptor = descriptor_tail[1..]
145        .split_once(quote)
146        .ok_or_else(|| QmbedError::Archive("invalid NPY descriptor".into()))?
147        .0
148        .to_string();
149    let shape_marker = if header.contains("'shape':") {
150        "'shape':"
151    } else {
152        "\"shape\":"
153    };
154    let shape_tail = header
155        .split_once(shape_marker)
156        .ok_or_else(|| QmbedError::Archive("NPY shape is missing".into()))?
157        .1;
158    let shape_text = shape_tail
159        .split_once('(')
160        .and_then(|(_, tail)| tail.split_once(')'))
161        .map(|(shape, _)| shape)
162        .ok_or_else(|| QmbedError::Archive("invalid NPY shape".into()))?;
163    let shape = shape_text
164        .split(',')
165        .map(str::trim)
166        .filter(|value| !value.is_empty())
167        .map(|value| value.parse::<usize>().map_err(archive_error))
168        .collect::<Result<Vec<_>>>()?;
169    Ok((descriptor, shape))
170}
171
172fn read_version(reader: &mut impl Read) -> Result<u8> {
173    let (descriptor, shape) = parse_npy_header(reader)?;
174    if descriptor != "|u1" || shape != [1] {
175        return Err(QmbedError::Archive("invalid archive-version array".into()));
176    }
177    let mut version = [0_u8; 1];
178    reader.read_exact(&mut version).map_err(archive_error)?;
179    Ok(version[0])
180}
181
182fn read_u64_vector(reader: &mut impl Read) -> Result<Vec<u64>> {
183    let (descriptor, shape) = parse_npy_header(reader)?;
184    if !matches!(descriptor.as_str(), "<u8" | "=u8" | "|u8") || shape.len() != 1 {
185        return Err(QmbedError::Archive(
186            "sparse index arrays must be one-dimensional uint64 arrays".into(),
187        ));
188    }
189    let mut values = Vec::with_capacity(shape[0]);
190    for _ in 0..shape[0] {
191        let mut bytes = [0_u8; 8];
192        reader.read_exact(&mut bytes).map_err(archive_error)?;
193        values.push(u64::from_le_bytes(bytes));
194    }
195    Ok(values)
196}
197
198fn read_complex_matrix(reader: &mut impl Read) -> Result<(usize, usize, Vec<Complex64>)> {
199    let (descriptor, shape) = parse_npy_header(reader)?;
200    if !matches!(descriptor.as_str(), "<c16" | "=c16" | "|c16") || shape.len() != 2 {
201        return Err(QmbedError::Archive(
202            "matrix.npy must be a C-order complex128 matrix".into(),
203        ));
204    }
205    let count = shape[0]
206        .checked_mul(shape[1])
207        .ok_or_else(|| QmbedError::Archive("archive matrix size overflow".into()))?;
208    let mut values = Vec::with_capacity(count);
209    for _ in 0..count {
210        let mut real = [0_u8; 8];
211        let mut imaginary = [0_u8; 8];
212        reader.read_exact(&mut real).map_err(archive_error)?;
213        reader.read_exact(&mut imaginary).map_err(archive_error)?;
214        values.push(Complex64::new(
215            f64::from_le_bytes(real),
216            f64::from_le_bytes(imaginary),
217        ));
218    }
219    Ok((shape[0], shape[1], values))
220}
221
222fn validate_metadata(key: &str, value: &str) -> Result<()> {
223    if key.is_empty()
224        || key
225            .chars()
226            .any(|character| !(character.is_ascii_alphanumeric() || "_.-".contains(character)))
227    {
228        return Err(QmbedError::Archive(
229            "archive metadata keys may contain only ASCII letters, digits, `_`, `-`, and `.`"
230                .into(),
231        ));
232    }
233    if value
234        .chars()
235        .any(|character| matches!(character, '\t' | '\r' | '\n'))
236    {
237        return Err(QmbedError::Archive(
238            "archive metadata values may not contain tabs or newlines".into(),
239        ));
240    }
241    Ok(())
242}
243
244/// Portable, versioned manifest of ordered physical basis states.
245///
246/// A basis archive stores stable state identifiers, their logical bit width,
247/// public row ordering, and scalar metadata.  It intentionally does not
248/// serialize executable callbacks.  Built-in frontends may store their
249/// language-neutral constructor request in metadata and rebuild local
250/// operator semantics after loading, while every consumer can use the state
251/// manifest to align vectors, projectors, and operator archives safely.
252#[derive(Clone, Debug)]
253pub struct BasisArchive {
254    width_bits: usize,
255    states: Vec<ErasedState>,
256    metadata: BTreeMap<String, String>,
257}
258
259impl BasisArchive {
260    /// Construct an archive from ordered, unique physical state identifiers.
261    pub fn new(width_bits: usize, states: impl IntoIterator<Item = ErasedState>) -> Result<Self> {
262        let states = states.into_iter().collect::<Vec<_>>();
263        if width_bits == 0 || states.iter().any(|state| state.width_bits() != width_bits) {
264            return Err(QmbedError::Archive(
265                "basis states must share one positive logical width".into(),
266            ));
267        }
268        let mut unique = std::collections::HashSet::with_capacity(states.len());
269        if states.iter().any(|state| !unique.insert(*state)) {
270            return Err(QmbedError::Archive(
271                "basis archives require unique physical states".into(),
272            ));
273        }
274        Ok(Self {
275            width_bits,
276            states,
277            metadata: BTreeMap::new(),
278        })
279    }
280
281    /// Snapshot any integer-labelled basis without changing its public order.
282    pub fn from_basis<B>(basis: &B, width_bits: usize) -> Result<Self>
283    where
284        B: Basis,
285        B::State: std::fmt::Display,
286    {
287        let states = (0..basis.len())
288            .map(|index| {
289                let state = basis.state(index)?;
290                ErasedState::from_decimal(width_bits, &state.to_string())
291            })
292            .collect::<Result<Vec<_>>>()?;
293        Self::new(width_bits, states)
294    }
295
296    /// Logical bit width of every archived state.
297    pub const fn width_bits(&self) -> usize {
298        self.width_bits
299    }
300
301    /// Ordered physical states, one per public basis row.
302    pub fn states(&self) -> &[ErasedState] {
303        &self.states
304    }
305
306    /// Attach a small language-neutral reconstruction hint or provenance item.
307    pub fn insert_metadata(
308        &mut self,
309        key: impl Into<String>,
310        value: impl Into<String>,
311    ) -> Result<()> {
312        let key = key.into();
313        let value = value.into();
314        validate_metadata(&key, &value)?;
315        self.metadata.insert(key, value);
316        Ok(())
317    }
318
319    /// Iterate over archived scalar metadata.
320    pub fn metadata(&self) -> impl Iterator<Item = (&str, &str)> {
321        self.metadata
322            .iter()
323            .map(|(key, value)| (key.as_str(), value.as_str()))
324    }
325
326    /// Read one archived metadata value.
327    pub fn metadata_value(&self, key: &str) -> Option<&str> {
328        self.metadata.get(key).map(String::as_str)
329    }
330}
331
332/// Save a pickle-free basis manifest preserving logical width and row order.
333pub fn save_basis_zip(path: impl AsRef<Path>, basis: &BasisArchive) -> Result<()> {
334    let file = File::create(path).map_err(archive_error)?;
335    let mut archive = ZipWriter::new(file);
336    let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
337    archive
338        .start_file("basis_archive_version.npy", options)
339        .map_err(archive_error)?;
340    write_u8_npy(&mut archive, &[BASIS_ARCHIVE_VERSION])?;
341    archive
342        .start_file("basis_manifest.tsv", options)
343        .map_err(archive_error)?;
344    archive
345        .write_all(format!("width_bits\t{}\n", basis.width_bits).as_bytes())
346        .map_err(archive_error)?;
347    for state in &basis.states {
348        archive
349            .write_all(format!("state\t{}\n", state.to_decimal()).as_bytes())
350            .map_err(archive_error)?;
351    }
352    archive
353        .start_file("metadata.tsv", options)
354        .map_err(archive_error)?;
355    for (key, value) in &basis.metadata {
356        archive
357            .write_all(format!("{key}\t{value}\n").as_bytes())
358            .map_err(archive_error)?;
359    }
360    archive.finish().map_err(archive_error)?;
361    Ok(())
362}
363
364/// Load and validate a portable basis manifest.
365pub fn load_basis_zip(path: impl AsRef<Path>) -> Result<BasisArchive> {
366    let file = File::open(path).map_err(archive_error)?;
367    let mut archive = ZipArchive::new(file).map_err(archive_error)?;
368    {
369        let mut version = archive
370            .by_name("basis_archive_version.npy")
371            .map_err(archive_error)?;
372        let version = read_version(&mut version)?;
373        if version != BASIS_ARCHIVE_VERSION {
374            return Err(QmbedError::Archive(format!(
375                "unsupported basis archive version {version}"
376            )));
377        }
378    }
379    let manifest = {
380        let mut entry = archive
381            .by_name("basis_manifest.tsv")
382            .map_err(archive_error)?;
383        let mut text = String::new();
384        entry.read_to_string(&mut text).map_err(archive_error)?;
385        text
386    };
387    let mut lines = manifest.lines();
388    let width_bits = lines
389        .next()
390        .and_then(|line| line.strip_prefix("width_bits\t"))
391        .ok_or_else(|| QmbedError::Archive("basis width is missing".into()))?
392        .parse::<usize>()
393        .map_err(archive_error)?;
394    let states = lines
395        .map(|line| {
396            let value = line
397                .strip_prefix("state\t")
398                .ok_or_else(|| QmbedError::Archive("invalid basis state row".into()))?;
399            ErasedState::from_decimal(width_bits, value)
400        })
401        .collect::<Result<Vec<_>>>()?;
402    let mut basis = BasisArchive::new(width_bits, states)?;
403    let has_metadata = archive.file_names().any(|name| name == "metadata.tsv");
404    if has_metadata {
405        let mut entry = archive.by_name("metadata.tsv").map_err(archive_error)?;
406        let mut text = String::new();
407        entry.read_to_string(&mut text).map_err(archive_error)?;
408        for line in text.lines() {
409            let (key, value) = line
410                .split_once('\t')
411                .ok_or_else(|| QmbedError::Archive("invalid archive metadata row".into()))?;
412            basis.insert_metadata(key, value)?;
413        }
414    }
415    Ok(basis)
416}
417
418/// Save a versioned, pickle-free NPZ archive readable as `np.load(path)["matrix"]`.
419pub fn save_operator_npz(
420    operator: &(impl LinearOperator + ?Sized),
421    path: impl AsRef<Path>,
422) -> Result<()> {
423    let shape = operator.shape();
424    let values = materialize_dense(operator)?;
425    let file = File::create(path).map_err(archive_error)?;
426    let mut archive = ZipWriter::new(file);
427    let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
428    archive
429        .start_file("archive_version.npy", options)
430        .map_err(archive_error)?;
431    write_u8_npy(&mut archive, &[ARCHIVE_VERSION])?;
432    archive
433        .start_file("matrix.npy", options)
434        .map_err(archive_error)?;
435    write_complex_npy(&mut archive, shape.0, shape.1, &values)?;
436    archive.finish().map_err(archive_error)?;
437    Ok(())
438}
439
440/// Load a pickle-free NPZ produced by Rust or NumPy `savez`/`savez_compressed`.
441pub fn load_operator_npz(path: impl AsRef<Path>, format: MatrixFormat) -> Result<Operator> {
442    let file = File::open(path).map_err(archive_error)?;
443    let mut archive = ZipArchive::new(file).map_err(archive_error)?;
444    if let Ok(mut version_entry) = archive.by_name("archive_version.npy") {
445        let version = read_version(&mut version_entry)?;
446        if version != ARCHIVE_VERSION {
447            return Err(QmbedError::Archive(format!(
448                "unsupported operator archive version {version}"
449            )));
450        }
451    }
452    let mut matrix = archive.by_name("matrix.npy").map_err(archive_error)?;
453    let (rows, columns, values) = read_complex_matrix(&mut matrix)?;
454    Operator::from_dense(rows, columns, values)?.converted(format)
455}
456
457#[derive(Clone, Debug)]
458pub struct OperatorArchiveEntry {
459    pub operator: Operator,
460    pub default: Option<Complex64>,
461}
462
463#[derive(Clone, Debug, Default)]
464pub struct OperatorArchive {
465    entries: BTreeMap<String, OperatorArchiveEntry>,
466    metadata: BTreeMap<String, String>,
467}
468
469impl OperatorArchive {
470    pub fn new() -> Self {
471        Self::default()
472    }
473
474    pub fn insert(
475        &mut self,
476        name: impl Into<String>,
477        operator: Operator,
478        default: Option<Complex64>,
479    ) -> Result<()> {
480        let name = name.into();
481        if name.is_empty()
482            || name
483                .chars()
484                .any(|character| !(character.is_ascii_alphanumeric() || "_.-".contains(character)))
485        {
486            return Err(QmbedError::Archive(
487                "archive entry names may contain only ASCII letters, digits, `_`, `-`, and `.`"
488                    .into(),
489            ));
490        }
491        if self
492            .entries
493            .insert(name.clone(), OperatorArchiveEntry { operator, default })
494            .is_some()
495        {
496            return Err(QmbedError::Archive(format!(
497                "duplicate archive entry {name:?}"
498            )));
499        }
500        Ok(())
501    }
502
503    pub fn get(&self, name: &str) -> Option<&OperatorArchiveEntry> {
504        self.entries.get(name)
505    }
506
507    pub fn iter(&self) -> impl Iterator<Item = (&str, &OperatorArchiveEntry)> {
508        self.entries
509            .iter()
510            .map(|(name, entry)| (name.as_str(), entry))
511    }
512
513    /// Attach small, language-neutral metadata to the archive.
514    ///
515    /// Metadata is deliberately scalar text rather than executable
516    /// serialization. This keeps archives safe to inspect and lets frontends
517    /// preserve contracts such as declared scalar dtype or a basis-schema
518    /// version without embedding pickled language objects.
519    pub fn insert_metadata(
520        &mut self,
521        key: impl Into<String>,
522        value: impl Into<String>,
523    ) -> Result<()> {
524        let key = key.into();
525        let value = value.into();
526        validate_metadata(&key, &value)?;
527        self.metadata.insert(key, value);
528        Ok(())
529    }
530
531    pub fn metadata(&self) -> impl Iterator<Item = (&str, &str)> {
532        self.metadata
533            .iter()
534            .map(|(key, value)| (key.as_str(), value.as_str()))
535    }
536
537    pub fn metadata_value(&self, key: &str) -> Option<&str> {
538        self.metadata.get(key).map(String::as_str)
539    }
540
541    pub fn from_quantum_operator(operator: &QuantumOperator) -> Result<Self> {
542        let mut archive = Self::new();
543        for component in operator.components() {
544            archive.insert(
545                component.name(),
546                component.operator().clone(),
547                component.default(),
548            )?;
549        }
550        Ok(archive)
551    }
552
553    pub fn into_quantum_operator(self) -> Result<QuantumOperator> {
554        QuantumOperator::new(self.entries.into_iter().map(|(name, entry)| {
555            if let Some(default) = entry.default {
556                QuantumComponent::with_default(name, entry.operator, default)
557            } else {
558                QuantumComponent::required(name, entry.operator)
559            }
560        }))
561    }
562}
563
564fn format_name(format: MatrixFormat) -> &'static str {
565    match format {
566        MatrixFormat::Dense => "dense",
567        MatrixFormat::Csc => "csc",
568        MatrixFormat::Csr => "csr",
569        MatrixFormat::Dia => "dia",
570        MatrixFormat::MatrixFree => "matrix_free",
571    }
572}
573
574fn parse_format(value: &str) -> Result<MatrixFormat> {
575    match value {
576        "dense" => Ok(MatrixFormat::Dense),
577        "csc" => Ok(MatrixFormat::Csc),
578        "csr" => Ok(MatrixFormat::Csr),
579        "dia" => Ok(MatrixFormat::Dia),
580        "matrix_free" => Ok(MatrixFormat::MatrixFree),
581        _ => Err(QmbedError::Archive(format!(
582            "unknown archived matrix format {value:?}"
583        ))),
584    }
585}
586
587/// Save named dense/sparse components without executing serialization callbacks.
588pub fn save_zip(path: impl AsRef<Path>, entries: &OperatorArchive) -> Result<()> {
589    if entries.entries.is_empty() {
590        return Err(QmbedError::Archive(
591            "an operator archive must contain at least one entry".into(),
592        ));
593    }
594    let file = File::create(path).map_err(archive_error)?;
595    let mut archive = ZipWriter::new(file);
596    let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
597    archive
598        .start_file("archive_version.npy", options)
599        .map_err(archive_error)?;
600    write_u8_npy(&mut archive, &[ARCHIVE_VERSION])?;
601
602    let mut manifest = String::new();
603    for (index, (name, entry)) in entries.entries.iter().enumerate() {
604        let shape = entry.operator.shape();
605        let (present, real, imaginary) = entry
606            .default
607            .map_or((0, 0.0, 0.0), |value| (1, value.re, value.im));
608        manifest.push_str(&format!(
609            "{name}\t{}\t{}\t{}\t{present}\t{real:.17e}\t{imaginary:.17e}\n",
610            format_name(entry.operator.format()),
611            shape.0,
612            shape.1,
613        ));
614        if entry.operator.format() == MatrixFormat::Dense {
615            archive
616                .start_file(format!("entries/{index}/matrix.npy"), options)
617                .map_err(archive_error)?;
618            write_complex_npy(&mut archive, shape.0, shape.1, &entry.operator.to_dense())?;
619        } else {
620            let triplets = entry.operator.triplets();
621            let rows = triplets
622                .iter()
623                .map(|(row, _, _)| u64::try_from(*row).map_err(archive_error))
624                .collect::<Result<Vec<_>>>()?;
625            let columns = triplets
626                .iter()
627                .map(|(_, column, _)| u64::try_from(*column).map_err(archive_error))
628                .collect::<Result<Vec<_>>>()?;
629            let values: Vec<_> = triplets.iter().map(|(_, _, value)| *value).collect();
630            archive
631                .start_file(format!("entries/{index}/rows.npy"), options)
632                .map_err(archive_error)?;
633            write_u64_npy(&mut archive, &rows)?;
634            archive
635                .start_file(format!("entries/{index}/columns.npy"), options)
636                .map_err(archive_error)?;
637            write_u64_npy(&mut archive, &columns)?;
638            archive
639                .start_file(format!("entries/{index}/values.npy"), options)
640                .map_err(archive_error)?;
641            write_complex_npy(&mut archive, values.len(), 1, &values)?;
642        }
643    }
644    archive
645        .start_file("manifest.tsv", options)
646        .map_err(archive_error)?;
647    archive
648        .write_all(manifest.as_bytes())
649        .map_err(archive_error)?;
650    archive
651        .start_file("metadata.tsv", options)
652        .map_err(archive_error)?;
653    for (key, value) in &entries.metadata {
654        archive
655            .write_all(format!("{key}\t{value}\n").as_bytes())
656            .map_err(archive_error)?;
657    }
658    archive.finish().map_err(archive_error)?;
659    Ok(())
660}
661
662/// Load a named operator archive while preserving every requested storage format.
663pub fn load_zip(path: impl AsRef<Path>) -> Result<OperatorArchive> {
664    let file = File::open(path).map_err(archive_error)?;
665    let mut archive = ZipArchive::new(file).map_err(archive_error)?;
666    {
667        let mut version_entry = archive
668            .by_name("archive_version.npy")
669            .map_err(archive_error)?;
670        let version = read_version(&mut version_entry)?;
671        if version != ARCHIVE_VERSION {
672            return Err(QmbedError::Archive(format!(
673                "unsupported operator archive version {version}"
674            )));
675        }
676    }
677    let manifest = {
678        let mut entry = archive.by_name("manifest.tsv").map_err(archive_error)?;
679        let mut text = String::new();
680        entry.read_to_string(&mut text).map_err(archive_error)?;
681        text
682    };
683    let mut result = OperatorArchive::new();
684    let has_metadata = archive.file_names().any(|name| name == "metadata.tsv");
685    if has_metadata {
686        let mut entry = archive.by_name("metadata.tsv").map_err(archive_error)?;
687        let mut text = String::new();
688        entry.read_to_string(&mut text).map_err(archive_error)?;
689        for line in text.lines() {
690            let (key, value) = line
691                .split_once('\t')
692                .ok_or_else(|| QmbedError::Archive("invalid archive metadata row".into()))?;
693            result.insert_metadata(key, value)?;
694        }
695    }
696    for (index, line) in manifest.lines().enumerate() {
697        let fields: Vec<_> = line.split('\t').collect();
698        if fields.len() != 7 {
699            return Err(QmbedError::Archive("invalid archive manifest row".into()));
700        }
701        let format = parse_format(fields[1])?;
702        let rows = fields[2].parse::<usize>().map_err(archive_error)?;
703        let columns = fields[3].parse::<usize>().map_err(archive_error)?;
704        let default = match fields[4] {
705            "0" => None,
706            "1" => Some(Complex64::new(
707                fields[5].parse::<f64>().map_err(archive_error)?,
708                fields[6].parse::<f64>().map_err(archive_error)?,
709            )),
710            _ => return Err(QmbedError::Archive("invalid default marker".into())),
711        };
712        let operator = if format == MatrixFormat::Dense {
713            let mut matrix = archive
714                .by_name(&format!("entries/{index}/matrix.npy"))
715                .map_err(archive_error)?;
716            let (actual_rows, actual_columns, values) = read_complex_matrix(&mut matrix)?;
717            if (actual_rows, actual_columns) != (rows, columns) {
718                return Err(QmbedError::Archive(
719                    "dense entry shape disagrees with the manifest".into(),
720                ));
721            }
722            Operator::from_dense(rows, columns, values)?
723        } else {
724            let row_indices = {
725                let mut entry = archive
726                    .by_name(&format!("entries/{index}/rows.npy"))
727                    .map_err(archive_error)?;
728                read_u64_vector(&mut entry)?
729            };
730            let column_indices = {
731                let mut entry = archive
732                    .by_name(&format!("entries/{index}/columns.npy"))
733                    .map_err(archive_error)?;
734                read_u64_vector(&mut entry)?
735            };
736            let values = {
737                let mut entry = archive
738                    .by_name(&format!("entries/{index}/values.npy"))
739                    .map_err(archive_error)?;
740                let (value_rows, value_columns, values) = read_complex_matrix(&mut entry)?;
741                if value_columns != 1 || value_rows != row_indices.len() {
742                    return Err(QmbedError::Archive(
743                        "sparse values shape disagrees with sparse indices".into(),
744                    ));
745                }
746                values
747            };
748            if row_indices.len() != column_indices.len() || row_indices.len() != values.len() {
749                return Err(QmbedError::Archive(
750                    "sparse archive arrays have unequal lengths".into(),
751                ));
752            }
753            let triplets = row_indices
754                .into_iter()
755                .zip(column_indices)
756                .zip(values)
757                .map(|((row, column), value)| {
758                    Ok((
759                        usize::try_from(row).map_err(archive_error)?,
760                        usize::try_from(column).map_err(archive_error)?,
761                        value,
762                    ))
763                })
764                .collect::<Result<Vec<_>>>()?;
765            Operator::from_triplets(rows, columns, triplets, format)?
766        };
767        result.insert(fields[0], operator, default)?;
768    }
769    Ok(result)
770}