1use std::{
2 collections::BTreeMap,
3 fs::File,
4 io::BufReader,
5 path::{Path, PathBuf},
6};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 Array2, Complex64, Result,
12 configuration::SimulationConfiguration,
13 error::Error,
14 measurements::{ImageSet, MeasurementSpec, MeasurementStack},
15 reconstruction::ReconstructionProblem,
16};
17
18pub const DATASET_FORMAT_VERSION: u32 = 1;
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct DatasetManifest {
25 pub format_version: u32,
26 pub measurement_manifest: PathBuf,
27 pub configuration: PathBuf,
28 #[serde(default)]
30 pub ground_truth_object: Option<PathBuf>,
31 #[serde(default)]
33 pub valid_object_mask: Option<PathBuf>,
34 #[serde(default)]
35 pub provenance: BTreeMap<String, String>,
36 #[serde(default)]
37 pub measurement_units: Option<String>,
38}
39
40#[derive(Clone, Debug)]
42pub struct Dataset {
43 source_path: Option<PathBuf>,
44 measurements: MeasurementStack,
45 configuration: SimulationConfiguration,
46 ground_truth_object: Option<Array2<Complex64>>,
47 valid_object_mask: Option<Array2<u8>>,
48 provenance: BTreeMap<String, String>,
49 measurement_units: Option<String>,
50}
51
52impl Dataset {
53 pub fn new(
55 measurements: MeasurementStack,
56 configuration: SimulationConfiguration,
57 ) -> Result<Self> {
58 measurements.validate()?;
59 configuration.validate()?;
60 ReconstructionProblem::new(
61 measurements.clone(),
62 configuration.compiled_models.reconstruction_model.clone(),
63 )?;
64 Ok(Self {
65 source_path: None,
66 measurements,
67 configuration,
68 ground_truth_object: None,
69 valid_object_mask: None,
70 provenance: BTreeMap::new(),
71 measurement_units: None,
72 })
73 }
74
75 pub fn measurements(&self) -> &MeasurementStack {
76 &self.measurements
77 }
78
79 pub fn source_path(&self) -> Option<&Path> {
81 self.source_path.as_deref()
82 }
83
84 pub fn configuration(&self) -> &SimulationConfiguration {
85 &self.configuration
86 }
87
88 pub fn ground_truth_object(&self) -> Option<&Array2<Complex64>> {
89 self.ground_truth_object.as_ref()
90 }
91
92 pub fn valid_object_mask(&self) -> Option<&Array2<u8>> {
93 self.valid_object_mask.as_ref()
94 }
95
96 pub fn provenance(&self) -> &BTreeMap<String, String> {
97 &self.provenance
98 }
99
100 pub fn measurement_units(&self) -> Option<&str> {
101 self.measurement_units.as_deref()
102 }
103
104 pub fn reconstruction_problem(&self) -> Result<ReconstructionProblem<MeasurementStack>> {
105 ReconstructionProblem::new(
106 self.measurements.clone(),
107 self.configuration
108 .compiled_models
109 .reconstruction_model
110 .clone(),
111 )
112 }
113
114 pub fn subset(&self) -> super::DatasetSubsetBuilder<'_> {
115 super::DatasetSubsetBuilder::new(self)
116 }
117
118 fn with_metadata(
119 mut self,
120 ground_truth_object: Option<Array2<Complex64>>,
121 valid_object_mask: Option<Array2<u8>>,
122 provenance: BTreeMap<String, String>,
123 measurement_units: Option<String>,
124 ) -> Result<Self> {
125 validate_dataset_metadata(
126 &self.configuration,
127 ground_truth_object.as_ref(),
128 valid_object_mask.as_ref(),
129 &provenance,
130 measurement_units.as_deref(),
131 )?;
132 self.ground_truth_object = ground_truth_object;
133 self.valid_object_mask = valid_object_mask;
134 self.provenance = provenance;
135 self.measurement_units = measurement_units;
136 Ok(self)
137 }
138}
139
140#[derive(Clone, Debug)]
142pub struct DatasetLoader {
143 root: PathBuf,
144}
145
146impl DatasetLoader {
147 pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
148 let root = root.into();
149 if !root.is_dir() {
150 return Err(Error::Dataset(format!(
151 "dataset path does not exist or is not a directory: {}",
152 root.display()
153 )));
154 }
155 Ok(Self { root })
156 }
157
158 pub fn root(&self) -> &Path {
159 &self.root
160 }
161
162 pub fn manifest_path(&self) -> PathBuf {
163 self.root.join("dataset.json")
164 }
165
166 pub fn load(&self) -> Result<Dataset> {
167 let manifest_path = self.manifest_path();
168 let manifest: DatasetManifest =
169 serde_json::from_reader(BufReader::new(File::open(&manifest_path)?))?;
170 if manifest.format_version != DATASET_FORMAT_VERSION {
171 return Err(Error::Dataset(format!(
172 "unsupported dataset format version {} in {}; expected {}",
173 manifest.format_version,
174 manifest_path.display(),
175 DATASET_FORMAT_VERSION
176 )));
177 }
178 validate_dataset_manifest_paths(&manifest)?;
179 let measurement_manifest_path = self.root.join(&manifest.measurement_manifest);
180 let measurement_spec = MeasurementSpec::load(&measurement_manifest_path)?;
181 validate_measurement_paths(&measurement_spec)?;
182 let measurement_base = measurement_manifest_path
183 .parent()
184 .unwrap_or_else(|| Path::new("."));
185 let measurements =
186 MeasurementStack::from_manifest_definition(measurement_spec, measurement_base)?;
187 let configuration = SimulationConfiguration::load(self.root.join(&manifest.configuration))?;
188 let ground_truth_object = manifest
189 .ground_truth_object
190 .as_ref()
191 .map(|path| load_json_array(self.root.join(path), "ground-truth object"))
192 .transpose()?;
193 let valid_object_mask = manifest
194 .valid_object_mask
195 .as_ref()
196 .map(|path| load_json_array(self.root.join(path), "valid-object mask"))
197 .transpose()?;
198 let mut dataset = Dataset::new(measurements, configuration)?;
199 dataset.source_path = Some(self.root.clone());
200 dataset.with_metadata(
201 ground_truth_object,
202 valid_object_mask,
203 manifest.provenance,
204 manifest.measurement_units,
205 )
206 }
207}
208
209fn validate_dataset_manifest_paths(manifest: &DatasetManifest) -> Result<()> {
210 validate_contained_path("measurement manifest", &manifest.measurement_manifest)?;
211 validate_contained_path("configuration", &manifest.configuration)?;
212 if let Some(path) = &manifest.ground_truth_object {
213 validate_contained_path("ground-truth object", path)?;
214 }
215 if let Some(path) = &manifest.valid_object_mask {
216 validate_contained_path("valid-object mask", path)?;
217 }
218 Ok(())
219}
220
221fn validate_measurement_paths(spec: &MeasurementSpec) -> Result<()> {
222 for frame in &spec.frames {
223 validate_contained_path("measurement frame", &frame.path)?;
224 }
225 if let Some(path) = &spec.dark_frame {
226 validate_contained_path("dark frame", path)?;
227 }
228 if let Some(path) = &spec.flat_field {
229 validate_contained_path("flat field", path)?;
230 }
231 if let Some(images) = &spec.background {
232 validate_image_set_paths("background", images)?;
233 }
234 if let Some(images) = &spec.mask {
235 validate_image_set_paths("mask", images)?;
236 }
237 Ok(())
238}
239
240fn validate_image_set_paths(label: &str, images: &ImageSet) -> Result<()> {
241 match images {
242 ImageSet::Single(path) => validate_contained_path(label, path),
243 ImageSet::PerFrame(paths) => paths
244 .iter()
245 .try_for_each(|path| validate_contained_path(label, path)),
246 }
247}
248
249fn validate_contained_path(label: &str, path: &Path) -> Result<()> {
250 if path.as_os_str().is_empty()
251 || path
252 .components()
253 .any(|component| !matches!(component, std::path::Component::Normal(_)))
254 {
255 return Err(Error::Dataset(format!(
256 "{label} must be a safe relative path contained by its manifest directory: {}",
257 path.display()
258 )));
259 }
260 Ok(())
261}
262
263fn load_json_array<T>(path: PathBuf, label: &str) -> Result<Array2<T>>
264where
265 T: for<'de> Deserialize<'de>,
266{
267 let file = File::open(&path).map_err(|error| {
268 Error::Dataset(format!(
269 "failed to open {label} at {}: {error}",
270 path.display()
271 ))
272 })?;
273 serde_json::from_reader(BufReader::new(file)).map_err(|error| {
274 Error::Dataset(format!(
275 "failed to parse {label} at {}: {error}",
276 path.display()
277 ))
278 })
279}
280
281fn validate_dataset_metadata(
282 configuration: &SimulationConfiguration,
283 ground_truth_object: Option<&Array2<Complex64>>,
284 valid_object_mask: Option<&Array2<u8>>,
285 provenance: &BTreeMap<String, String>,
286 measurement_units: Option<&str>,
287) -> Result<()> {
288 if let Some(ground_truth) = ground_truth_object {
289 if ground_truth.shape() != configuration.reconstruction_shape {
290 return Err(Error::Dataset(format!(
291 "ground-truth shape {:?} differs from reconstruction shape {:?}",
292 ground_truth.shape(),
293 configuration.reconstruction_shape
294 )));
295 }
296 if ground_truth
297 .as_slice()
298 .iter()
299 .any(|value| !value.re.is_finite() || !value.im.is_finite())
300 {
301 return Err(Error::Dataset(
302 "ground-truth object contains non-finite values".into(),
303 ));
304 }
305 }
306 if let Some(mask) = valid_object_mask {
307 if ground_truth_object.is_none() {
308 return Err(Error::Dataset(
309 "a valid-object mask requires a ground-truth object".into(),
310 ));
311 }
312 if mask.shape() != configuration.reconstruction_shape {
313 return Err(Error::Dataset(format!(
314 "valid-object mask shape {:?} differs from reconstruction shape {:?}",
315 mask.shape(),
316 configuration.reconstruction_shape
317 )));
318 }
319 if mask.as_slice().iter().any(|&value| value > 1) || !mask.as_slice().contains(&1) {
320 return Err(Error::Dataset(
321 "valid-object mask must contain only zero and one and select at least one pixel"
322 .into(),
323 ));
324 }
325 }
326 if provenance
327 .iter()
328 .any(|(key, value)| key.trim().is_empty() || value.trim().is_empty())
329 {
330 return Err(Error::Dataset(
331 "dataset provenance keys and values must not be empty".into(),
332 ));
333 }
334 if measurement_units.is_some_and(|units| units.trim().is_empty()) {
335 return Err(Error::Dataset(
336 "measurement units must not be empty when provided".into(),
337 ));
338 }
339 Ok(())
340}