1use std::fs;
4use std::path::{Path, PathBuf};
5
6use physics_in_parallel::prelude::basic::{
7 RngConfig, SquareLattice, SquareLatticeConfig, SquareLatticeConfigError,
8 SquareLatticeInitMethod,
9};
10use scientific_workflow::prelude::basics::{
11 ArtifactDescriptor, ArtifactDisposition, ArtifactError, ArtifactLoadError, ExecutionScope,
12 RngRecord, RngRecordError, load_verified_artifact, persist_artifact,
13};
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value};
16use thiserror::Error;
17
18pub const INITIALIZATION_RNG_NAMESPACE: &str = "ecological_model_core.initial_state";
19pub const INITIAL_STATE_FORMAT: &str = "ecological.initial-state.v1";
20pub const INITIAL_STATE_METADATA_KEY: &str = "initial_state";
21
22pub type CategoricalSpace = SquareLattice<usize>;
23pub type TaxonCounts = Vec<usize>;
24
25#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
27#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
28pub enum DistributionSource {
29 Uniform,
30 Inline { weights: Vec<f64> },
31 Json { path: PathBuf },
32}
33
34impl DistributionSource {
35 pub fn path(&self) -> Option<&Path> {
36 match self {
37 Self::Json { path } => Some(path),
38 Self::Uniform | Self::Inline { .. } => None,
39 }
40 }
41
42 fn validate(&self, num_taxa: usize) -> Result<(), InitialStateError> {
43 match self {
44 Self::Uniform => Ok(()),
45 Self::Inline { weights } => validate_distribution(weights, num_taxa),
46 Self::Json { path } if path.as_os_str().is_empty() => Err(InitialStateError::EmptyPath),
47 Self::Json { .. } => Ok(()),
48 }
49 }
50
51 fn resolve(self, num_taxa: usize) -> Result<Option<Vec<f64>>, InitialStateError> {
52 let weights = match self {
53 Self::Uniform => return Ok(None),
54 Self::Inline { weights } => weights,
55 Self::Json { path } => {
56 let bytes = fs::read(&path).map_err(|source| InitialStateError::Io {
57 operation: "read distribution",
58 path: path.clone(),
59 source,
60 })?;
61 serde_json::from_slice(&bytes)
62 .map_err(|source| InitialStateError::Json { path, source })?
63 }
64 };
65 validate_distribution(&weights, num_taxa)?;
66 Ok(Some(weights))
67 }
68}
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
71#[serde(rename_all = "snake_case")]
72pub enum InitializationMethod {
73 Random,
74 BalancedUniform,
75 CenteredSeed,
76 CenteredDominantSeed,
77}
78
79#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
81#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
82pub enum InitialStateRecipe {
83 Random {
84 distribution: DistributionSource,
85 #[serde(default)]
86 rng: RngConfig,
87 },
88 BalancedUniform {
90 #[serde(default)]
91 rng: RngConfig,
92 },
93 CenteredSeed {
94 distribution: DistributionSource,
95 seed_taxon: usize,
96 seed_radius: usize,
97 #[serde(default)]
98 rng: RngConfig,
99 },
100 CenteredDominantSeed {
101 distribution: DistributionSource,
102 seed_radius: usize,
103 #[serde(default)]
104 rng: RngConfig,
105 },
106}
107
108impl InitialStateRecipe {
109 pub const fn method(&self) -> InitializationMethod {
110 match self {
111 Self::Random { .. } => InitializationMethod::Random,
112 Self::BalancedUniform { .. } => InitializationMethod::BalancedUniform,
113 Self::CenteredSeed { .. } => InitializationMethod::CenteredSeed,
114 Self::CenteredDominantSeed { .. } => InitializationMethod::CenteredDominantSeed,
115 }
116 }
117
118 pub fn distribution_path(&self) -> Option<&Path> {
119 match self {
120 Self::Random { distribution, .. }
121 | Self::CenteredSeed { distribution, .. }
122 | Self::CenteredDominantSeed { distribution, .. } => distribution.path(),
123 Self::BalancedUniform { .. } => None,
124 }
125 }
126
127 pub fn validate(
128 &self,
129 lattice: &SquareLatticeConfig,
130 num_taxa: usize,
131 ) -> Result<(), InitialStateError> {
132 if num_taxa == 0 {
133 return Err(InitialStateError::ZeroTaxa);
134 }
135 match self {
136 Self::Random { distribution, .. } => distribution.validate(num_taxa),
137 Self::BalancedUniform { .. } => Ok(()),
138 Self::CenteredSeed {
139 distribution,
140 seed_taxon,
141 seed_radius,
142 ..
143 } => {
144 distribution.validate(num_taxa)?;
145 validate_seed(lattice, num_taxa, *seed_taxon, *seed_radius)
146 }
147 Self::CenteredDominantSeed {
148 distribution,
149 seed_radius,
150 ..
151 } => {
152 distribution.validate(num_taxa)?;
153 validate_seed_geometry(lattice, *seed_radius)
154 }
155 }
156 }
157
158 pub fn create(
159 self,
160 lattice: SquareLatticeConfig,
161 num_taxa: usize,
162 ) -> Result<InitialState, InitialStateError> {
163 self.validate(&lattice, num_taxa)?;
164 let method = self.method();
165 let (distribution, rng, explicit_seed, seed_radius, dominant) = match self {
166 Self::Random { distribution, rng } => (distribution, rng, None, None, false),
167 Self::CenteredSeed {
168 distribution,
169 seed_taxon,
170 seed_radius,
171 rng,
172 } => (
173 distribution,
174 rng,
175 Some(seed_taxon),
176 Some(seed_radius),
177 false,
178 ),
179 Self::CenteredDominantSeed {
180 distribution,
181 seed_radius,
182 rng,
183 } => (distribution, rng, None, Some(seed_radius), true),
184 Self::BalancedUniform { rng } => {
185 let values = balanced_uniform_values(lattice.num_sites(), num_taxa);
186 let space = CategoricalSpace::new(
187 lattice,
188 SquareLatticeInitMethod::ShuffledValues { values, rng },
189 )?;
190 let counts = count_taxa(&space, num_taxa)?;
191 let rng_record = Some(rng_record_from_space(&space)?);
192 return Ok(InitialState {
193 num_taxa,
194 method,
195 seed_taxon: None,
196 rng_record,
197 space,
198 counts,
199 });
200 }
201 };
202 let mut weights = distribution.resolve(num_taxa)?;
203 let seed_taxon = if dominant {
204 let background = weights.get_or_insert_with(|| vec![1.0; num_taxa]);
205 Some(remove_dominant_taxon(background)?)
206 } else {
207 explicit_seed
208 };
209 let mut space = CategoricalSpace::new(
210 lattice,
211 SquareLatticeInitMethod::RandomChoices {
212 choices: (0..num_taxa).collect(),
213 weights,
214 rng,
215 },
216 )?;
217 let mut counts = count_taxa(&space, num_taxa)?;
218 if let (Some(taxon), Some(radius)) = (seed_taxon, seed_radius) {
219 let shape = space.config().shape().to_vec();
220 plant_centered_seed(&mut space, &mut counts, &shape, taxon, radius);
221 }
222 let rng_record = Some(rng_record_from_space(&space)?);
223 Ok(InitialState {
224 num_taxa,
225 method,
226 seed_taxon,
227 rng_record,
228 space,
229 counts,
230 })
231 }
232}
233
234#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
236#[serde(tag = "source", rename_all = "snake_case", deny_unknown_fields)]
237pub enum InitialStateSource {
238 Recipe {
239 recipe: InitialStateRecipe,
240 },
241 VerifiedArtifact {
242 execution_directory: PathBuf,
243 descriptor: InitialStateArtifactDescriptor,
244 },
245}
246
247impl InitialStateSource {
248 pub fn validate(
249 &self,
250 lattice: &SquareLatticeConfig,
251 num_taxa: usize,
252 ) -> Result<(), InitialStateError> {
253 match self {
254 Self::Recipe { recipe } => recipe.validate(lattice, num_taxa),
255 Self::VerifiedArtifact {
256 execution_directory,
257 descriptor,
258 } => {
259 if execution_directory.as_os_str().is_empty() {
260 return Err(InitialStateError::EmptyPath);
261 }
262 if descriptor.lattice() != lattice {
263 return Err(InitialStateError::LatticeMismatch);
264 }
265 if descriptor.num_taxa() != num_taxa {
266 return Err(InitialStateError::TaxonDimensionMismatch {
267 expected: num_taxa,
268 actual: descriptor.num_taxa(),
269 });
270 }
271 Ok(())
272 }
273 }
274 }
275
276 pub fn resolve(
277 &self,
278 lattice: SquareLatticeConfig,
279 num_taxa: usize,
280 ) -> Result<InitialState, InitialStateError> {
281 self.validate(&lattice, num_taxa)?;
282 match self {
283 Self::Recipe { recipe } => recipe.clone().create(lattice, num_taxa),
284 Self::VerifiedArtifact {
285 execution_directory,
286 descriptor,
287 } => load_verified_initial_state(execution_directory, descriptor),
288 }
289 }
290}
291
292#[derive(Debug)]
293pub struct InitialState {
294 num_taxa: usize,
295 method: InitializationMethod,
296 seed_taxon: Option<usize>,
297 rng_record: Option<RngRecord>,
298 space: CategoricalSpace,
299 counts: TaxonCounts,
300}
301
302impl InitialState {
303 pub const fn num_taxa(&self) -> usize {
304 self.num_taxa
305 }
306 pub const fn method(&self) -> InitializationMethod {
307 self.method
308 }
309 pub const fn seed_taxon(&self) -> Option<usize> {
310 self.seed_taxon
311 }
312 pub const fn rng_record(&self) -> Option<&RngRecord> {
313 self.rng_record.as_ref()
314 }
315 pub const fn space(&self) -> &CategoricalSpace {
316 &self.space
317 }
318 pub fn counts(&self) -> &[usize] {
319 &self.counts
320 }
321 pub fn frequencies(&self) -> Vec<f64> {
323 let total = self.space.num_sites() as f64;
324 self.counts
325 .iter()
326 .map(|&count| count as f64 / total)
327 .collect()
328 }
329 pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
331 serde_json::to_vec(&InitialStateDocumentRef::from(self))
332 }
333 pub fn clone_space(&self) -> CategoricalSpace {
334 self.space.clone()
335 }
336 pub fn into_parts(self) -> (CategoricalSpace, TaxonCounts) {
337 (self.space, self.counts)
338 }
339}
340
341#[derive(Serialize)]
342struct InitialStateDocumentRef<'a> {
343 format: &'static str,
344 num_taxa: usize,
345 method: InitializationMethod,
346 seed_taxon: Option<usize>,
347 rng_record: Option<&'a RngRecord>,
348 lattice: &'a SquareLatticeConfig,
349 sites: &'a [usize],
350}
351
352impl<'a> From<&'a InitialState> for InitialStateDocumentRef<'a> {
353 fn from(initial: &'a InitialState) -> Self {
354 Self {
355 format: INITIAL_STATE_FORMAT,
356 num_taxa: initial.num_taxa,
357 method: initial.method,
358 seed_taxon: initial.seed_taxon,
359 rng_record: initial.rng_record.as_ref(),
360 lattice: initial.space.config(),
361 sites: initial.space.data(),
362 }
363 }
364}
365
366#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
367#[serde(deny_unknown_fields)]
368pub struct InitialStateArtifactDescriptor {
369 format: String,
370 num_taxa: usize,
371 lattice: SquareLatticeConfig,
372 method: InitializationMethod,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 seed_taxon: Option<usize>,
375 #[serde(flatten)]
376 artifact: ArtifactDescriptor,
377}
378
379impl InitialStateArtifactDescriptor {
380 pub fn format(&self) -> &str {
381 &self.format
382 }
383 pub const fn num_taxa(&self) -> usize {
384 self.num_taxa
385 }
386 pub const fn lattice(&self) -> &SquareLatticeConfig {
387 &self.lattice
388 }
389 pub const fn method(&self) -> InitializationMethod {
390 self.method
391 }
392 pub const fn seed_taxon(&self) -> Option<usize> {
393 self.seed_taxon
394 }
395 pub fn sha256(&self) -> &str {
396 self.artifact.sha256()
397 }
398 pub fn path(&self) -> &str {
399 self.artifact.path()
400 }
401 pub fn insert_into_metadata(&self, metadata: &mut Map<String, Value>) -> Option<Value> {
402 metadata.insert(
403 INITIAL_STATE_METADATA_KEY.to_owned(),
404 serde_json::to_value(self).expect("initial-state descriptor is JSON-compatible"),
405 )
406 }
407}
408
409#[derive(Clone, Debug)]
410pub struct PersistedInitialState {
411 descriptor: InitialStateArtifactDescriptor,
412 disposition: ArtifactDisposition,
413}
414
415impl PersistedInitialState {
416 pub const fn descriptor(&self) -> &InitialStateArtifactDescriptor {
417 &self.descriptor
418 }
419 pub const fn disposition(&self) -> ArtifactDisposition {
420 self.disposition
421 }
422 pub fn into_descriptor(self) -> InitialStateArtifactDescriptor {
423 self.descriptor
424 }
425}
426
427pub fn persist_initial_state(
428 scope: &ExecutionScope,
429 initial: &InitialState,
430) -> Result<PersistedInitialState, InitialStateError> {
431 let bytes = serde_json::to_vec(&InitialStateDocumentRef::from(initial))?;
432 let persisted = persist_artifact(scope, "initial-state", "json", &bytes)?;
433 Ok(PersistedInitialState {
434 descriptor: InitialStateArtifactDescriptor {
435 format: INITIAL_STATE_FORMAT.to_owned(),
436 num_taxa: initial.num_taxa,
437 lattice: initial.space.config().clone(),
438 method: initial.method,
439 seed_taxon: initial.seed_taxon,
440 artifact: persisted.descriptor().clone(),
441 },
442 disposition: persisted.disposition(),
443 })
444}
445
446pub fn load_verified_initial_state(
447 execution_directory: impl AsRef<Path>,
448 descriptor: &InitialStateArtifactDescriptor,
449) -> Result<InitialState, InitialStateError> {
450 if descriptor.format != INITIAL_STATE_FORMAT {
451 return Err(InitialStateError::UnsupportedFormat {
452 actual: descriptor.format.clone(),
453 });
454 }
455 let verified = load_verified_artifact(execution_directory, &descriptor.artifact)?;
456 let document: InitialStateDocument =
457 serde_json::from_slice(verified.bytes()).map_err(|source| InitialStateError::Json {
458 path: verified.path().to_path_buf(),
459 source,
460 })?;
461 let initial = document.resolve(descriptor.lattice.clone(), descriptor.num_taxa)?;
462 if initial.method != descriptor.method || initial.seed_taxon != descriptor.seed_taxon {
463 return Err(InitialStateError::DescriptorMismatch);
464 }
465 Ok(initial)
466}
467
468#[derive(Debug, Deserialize)]
469#[serde(deny_unknown_fields)]
470struct InitialStateDocument {
471 format: String,
472 num_taxa: usize,
473 method: InitializationMethod,
474 seed_taxon: Option<usize>,
475 rng_record: Option<RngRecord>,
476 lattice: SquareLatticeConfig,
477 sites: Vec<usize>,
478}
479
480impl InitialStateDocument {
481 fn resolve(
482 self,
483 expected_lattice: SquareLatticeConfig,
484 expected_num_taxa: usize,
485 ) -> Result<InitialState, InitialStateError> {
486 if self.format != INITIAL_STATE_FORMAT {
487 return Err(InitialStateError::UnsupportedFormat {
488 actual: self.format,
489 });
490 }
491 if self.num_taxa != expected_num_taxa {
492 return Err(InitialStateError::TaxonDimensionMismatch {
493 expected: expected_num_taxa,
494 actual: self.num_taxa,
495 });
496 }
497 if self.lattice != expected_lattice {
498 return Err(InitialStateError::LatticeMismatch);
499 }
500 if self.seed_taxon.is_some_and(|taxon| taxon >= self.num_taxa) {
501 return Err(InitialStateError::SeedTaxonOutOfRange {
502 seed_taxon: self.seed_taxon.expect("checked Some"),
503 num_taxa: self.num_taxa,
504 });
505 }
506 let space = CategoricalSpace::new(
507 self.lattice,
508 SquareLatticeInitMethod::Values { values: self.sites },
509 )?;
510 let counts = count_taxa(&space, self.num_taxa)?;
511 Ok(InitialState {
512 num_taxa: self.num_taxa,
513 method: self.method,
514 seed_taxon: self.seed_taxon,
515 rng_record: self.rng_record,
516 space,
517 counts,
518 })
519 }
520}
521
522fn rng_record_from_space(space: &CategoricalSpace) -> Result<RngRecord, InitialStateError> {
523 let config = space
524 .initialization_rng_config()
525 .ok_or(InitialStateError::MissingResolvedRngConfig)?;
526 let method = config
527 .method()
528 .ok_or(InitialStateError::MissingResolvedRngConfig)?;
529 let key = config
530 .encode_seed()
531 .ok_or(InitialStateError::MissingResolvedRngConfig)?;
532 let parameters = Map::new();
533 Ok(RngRecord::new(
534 INITIALIZATION_RNG_NAMESPACE,
535 method.name(),
536 method.version(),
537 method.seed_encoding(),
538 key,
539 Some(parameters),
540 )?)
541}
542
543fn validate_distribution(weights: &[f64], num_taxa: usize) -> Result<(), InitialStateError> {
544 if weights.len() != num_taxa {
545 return Err(InitialStateError::DistributionLength {
546 expected: num_taxa,
547 actual: weights.len(),
548 });
549 }
550 let mut total = 0.0;
551 for (taxon, &weight) in weights.iter().enumerate() {
552 if !weight.is_finite() || weight < 0.0 {
553 return Err(InitialStateError::InvalidWeight { taxon, weight });
554 }
555 total += weight;
556 }
557 if !total.is_finite() || total <= 0.0 {
558 return Err(InitialStateError::NonPositiveDistribution);
559 }
560 Ok(())
561}
562
563fn remove_dominant_taxon(weights: &mut [f64]) -> Result<usize, InitialStateError> {
564 validate_distribution(weights, weights.len())?;
565 let dominant = weights
566 .iter()
567 .enumerate()
568 .max_by(|left, right| left.1.total_cmp(right.1).then(right.0.cmp(&left.0)))
569 .map(|(taxon, _)| taxon)
570 .expect("validated distribution is nonempty");
571 let background_total = weights.iter().sum::<f64>() - weights[dominant];
572 if background_total <= 0.0 {
573 return Err(InitialStateError::NoDominantSeedBackground);
574 }
575 weights[dominant] = 0.0;
576 for weight in weights {
577 *weight /= background_total;
578 }
579 Ok(dominant)
580}
581
582fn count_taxa(space: &CategoricalSpace, num_taxa: usize) -> Result<TaxonCounts, InitialStateError> {
583 let mut counts = vec![0; num_taxa];
584 for (site, &taxon) in space.data().iter().enumerate() {
585 let count = counts
586 .get_mut(taxon)
587 .ok_or(InitialStateError::SpaceTaxonOutOfRange {
588 site,
589 taxon,
590 num_taxa,
591 })?;
592 *count += 1;
593 }
594 Ok(counts)
595}
596
597fn balanced_uniform_values(num_sites: usize, num_taxa: usize) -> Vec<usize> {
598 let per_taxon = num_sites / num_taxa;
599 let remainder = num_sites % num_taxa;
600 let mut values = Vec::with_capacity(num_sites);
601 for taxon in 0..num_taxa {
602 values.extend(std::iter::repeat_n(
603 taxon,
604 per_taxon + usize::from(taxon < remainder),
605 ));
606 }
607 debug_assert_eq!(values.len(), num_sites);
608 values
609}
610
611fn validate_seed(
612 lattice: &SquareLatticeConfig,
613 num_taxa: usize,
614 seed_taxon: usize,
615 seed_radius: usize,
616) -> Result<(), InitialStateError> {
617 if seed_taxon >= num_taxa {
618 return Err(InitialStateError::SeedTaxonOutOfRange {
619 seed_taxon,
620 num_taxa,
621 });
622 }
623 validate_seed_geometry(lattice, seed_radius)
624}
625
626fn validate_seed_geometry(
627 lattice: &SquareLatticeConfig,
628 seed_radius: usize,
629) -> Result<(), InitialStateError> {
630 let width = seed_radius
631 .checked_mul(2)
632 .and_then(|diameter| diameter.checked_add(1))
633 .ok_or(InitialStateError::SeedWidthOverflow {
634 radius: seed_radius,
635 })?;
636 for (axis, &length) in lattice.shape().iter().enumerate() {
637 if width > length {
638 return Err(InitialStateError::SeedDoesNotFit {
639 axis,
640 width,
641 length,
642 });
643 }
644 }
645 Ok(())
646}
647
648fn plant_centered_seed(
649 space: &mut CategoricalSpace,
650 counts: &mut [usize],
651 shape: &[usize],
652 seed_taxon: usize,
653 radius: usize,
654) {
655 let starts = shape
656 .iter()
657 .map(|length| length / 2 - radius)
658 .collect::<Vec<_>>();
659 let widths = vec![radius * 2 + 1; shape.len()];
660 let seed_sites = widths.iter().product::<usize>();
661 let mut coordinate = vec![0isize; shape.len()];
662 for local_flat in 0..seed_sites {
663 let mut local = local_flat;
664 for axis in (0..shape.len()).rev() {
665 coordinate[axis] = (starts[axis] + local % widths[axis]) as isize;
666 local /= widths[axis];
667 }
668 let previous = *space.get(&coordinate);
669 if previous != seed_taxon {
670 counts[previous] -= 1;
671 counts[seed_taxon] += 1;
672 space.set(&coordinate, seed_taxon);
673 }
674 }
675}
676
677#[derive(Debug, Error)]
678#[non_exhaustive]
679pub enum InitialStateError {
680 #[error("num_taxa must be positive")]
681 ZeroTaxa,
682 #[error("distribution length is {actual}, expected {expected}")]
683 DistributionLength { expected: usize, actual: usize },
684 #[error("distribution weight {taxon} is invalid: {weight}")]
685 InvalidWeight { taxon: usize, weight: f64 },
686 #[error("distribution must have positive finite total weight")]
687 NonPositiveDistribution,
688 #[error("dominant-seed initialization requires positive background mass")]
689 NoDominantSeedBackground,
690 #[error("seed taxon {seed_taxon} is outside 0..{num_taxa}")]
691 SeedTaxonOutOfRange { seed_taxon: usize, num_taxa: usize },
692 #[error("seed radius {radius} is too large")]
693 SeedWidthOverflow { radius: usize },
694 #[error("seed width {width} does not fit lattice axis {axis} of length {length}")]
695 SeedDoesNotFit {
696 axis: usize,
697 width: usize,
698 length: usize,
699 },
700 #[error("path must not be empty")]
701 EmptyPath,
702 #[error("initial-state format `{actual}` is unsupported")]
703 UnsupportedFormat { actual: String },
704 #[error("initial state declares {actual} taxa, expected {expected}")]
705 TaxonDimensionMismatch { expected: usize, actual: usize },
706 #[error("initial-state lattice does not match the expected lattice")]
707 LatticeMismatch,
708 #[error("initial-state descriptor does not match its verified document")]
709 DescriptorMismatch,
710 #[error("initial-state site {site} contains taxon {taxon}, outside 0..{num_taxa}")]
711 SpaceTaxonOutOfRange {
712 site: usize,
713 taxon: usize,
714 num_taxa: usize,
715 },
716 #[error("failed to {operation} at `{path}`")]
717 Io {
718 operation: &'static str,
719 path: PathBuf,
720 #[source]
721 source: std::io::Error,
722 },
723 #[error("invalid JSON initial state at `{path}`")]
724 Json {
725 path: PathBuf,
726 #[source]
727 source: serde_json::Error,
728 },
729 #[error(transparent)]
730 Serialize(#[from] serde_json::Error),
731 #[error(transparent)]
732 Lattice(#[from] SquareLatticeConfigError),
733 #[error("initialized lattice has no resolved RNG configuration")]
734 MissingResolvedRngConfig,
735 #[error(transparent)]
736 RngRecord(#[from] RngRecordError),
737 #[error(transparent)]
738 Artifact(#[from] ArtifactError),
739 #[error(transparent)]
740 ArtifactLoad(#[from] ArtifactLoadError),
741}