1use std::path::Path;
4
5use ndarray::Array2;
6
7use crate::error::IoError;
8
9fn escape_md_cell(s: &str) -> String {
11 s.replace('|', "\\|").replace('\n', " ")
12}
13
14pub fn export_density_tiff(path: &Path, data: &Array2<f64>, label: &str) -> Result<(), IoError> {
22 export_map_tiff(path, data, &format!("{label}_density"))
23}
24
25pub fn export_map_tiff(path: &Path, data: &Array2<f64>, name: &str) -> Result<(), IoError> {
29 let filename = path.join(format!("{name}.tiff"));
30 let file = std::fs::File::create(&filename)
31 .map_err(|e| IoError::TiffEncode(format!("cannot create {}: {e}", filename.display())))?;
32 let mut encoder =
33 tiff::encoder::TiffEncoder::new(file).map_err(|e| IoError::TiffEncode(e.to_string()))?;
34
35 let (height, width) = (data.shape()[0] as u32, data.shape()[1] as u32);
36
37 let pixels: Vec<f32> = data.iter().map(|&v| v as f32).collect();
40 encoder
41 .write_image::<tiff::encoder::colortype::Gray32Float>(width, height, &pixels)
42 .map_err(|e| IoError::TiffEncode(e.to_string()))?;
43
44 Ok(())
45}
46
47#[cfg(feature = "hdf5")]
56pub fn export_results_hdf5(
57 path: &Path,
58 density_maps: &[Array2<f64>],
59 uncertainty_maps: &[Array2<f64>],
60 chi_squared_map: &Array2<f64>,
61 converged_map: &Array2<bool>,
62 labels: &[String],
63 temperature_map: Option<&Array2<f64>>,
64) -> Result<(), IoError> {
65 let file = hdf5::File::create(path).map_err(|e| IoError::Hdf5Error(format!("create: {e}")))?;
66
67 let density_group = file
69 .create_group("density")
70 .map_err(|e| IoError::Hdf5Error(format!("create group /density: {e}")))?;
71 for (i, map) in density_maps.iter().enumerate() {
72 let name = labels
73 .get(i)
74 .map_or_else(|| format!("isotope_{i}"), |s| s.clone());
75 let shape = [map.shape()[0], map.shape()[1]];
76 let data: Vec<f64> = map.iter().copied().collect();
77 density_group
78 .new_dataset::<f64>()
79 .shape(shape)
80 .create(name.as_str())
81 .and_then(|ds| ds.write_raw(&data))
82 .map_err(|e| IoError::Hdf5Error(format!("write /density/{name}: {e}")))?;
83 }
84
85 let unc_group = file
87 .create_group("uncertainty")
88 .map_err(|e| IoError::Hdf5Error(format!("create group /uncertainty: {e}")))?;
89 for (i, map) in uncertainty_maps.iter().enumerate() {
90 let name = labels
91 .get(i)
92 .map_or_else(|| format!("isotope_{i}"), |s| s.clone());
93 let shape = [map.shape()[0], map.shape()[1]];
94 let data: Vec<f64> = map.iter().copied().collect();
95 unc_group
96 .new_dataset::<f64>()
97 .shape(shape)
98 .create(name.as_str())
99 .and_then(|ds| ds.write_raw(&data))
100 .map_err(|e| IoError::Hdf5Error(format!("write /uncertainty/{name}: {e}")))?;
101 }
102
103 {
105 let shape = [chi_squared_map.shape()[0], chi_squared_map.shape()[1]];
106 let data: Vec<f64> = chi_squared_map.iter().copied().collect();
107 file.new_dataset::<f64>()
108 .shape(shape)
109 .create("chi_squared")
110 .and_then(|ds| ds.write_raw(&data))
111 .map_err(|e| IoError::Hdf5Error(format!("write /chi_squared: {e}")))?;
112 }
113
114 {
116 let shape = [converged_map.shape()[0], converged_map.shape()[1]];
117 let data: Vec<u8> = converged_map.iter().map(|&b| u8::from(b)).collect();
118 file.new_dataset::<u8>()
119 .shape(shape)
120 .create("converged")
121 .and_then(|ds| ds.write_raw(&data))
122 .map_err(|e| IoError::Hdf5Error(format!("write /converged: {e}")))?;
123 }
124
125 if let Some(t_map) = temperature_map {
127 let shape = [t_map.shape()[0], t_map.shape()[1]];
128 let data: Vec<f64> = t_map.iter().copied().collect();
129 file.new_dataset::<f64>()
130 .shape(shape)
131 .create("temperature")
132 .and_then(|ds| ds.write_raw(&data))
133 .map_err(|e| IoError::Hdf5Error(format!("write /temperature: {e}")))?;
134 }
135
136 Ok(())
137}
138
139pub fn export_markdown_report(
141 path: &Path,
142 labels: &[String],
143 density_maps: &[Array2<f64>],
144 converged_map: &Array2<bool>,
145 n_converged: usize,
146 n_total: usize,
147 provenance: &[(String, String)],
148) -> Result<(), IoError> {
149 let mut report = String::new();
150 report.push_str("# NEREIDS Spatial Mapping Report\n\n");
151
152 let pct = if n_total > 0 {
154 100.0 * n_converged as f64 / n_total as f64
155 } else {
156 0.0
157 };
158 report.push_str(&format!(
159 "## Convergence\n\n- Converged: {n_converged} / {n_total} ({pct:.1}%)\n\n"
160 ));
161
162 report.push_str("## Per-Isotope Density Statistics\n\n");
164 report.push_str("| Isotope | Mean Density (atoms/barn) | Std Dev |\n");
165 report.push_str("|---------|--------------------------|----------|\n");
166 for (i, map) in density_maps.iter().enumerate() {
167 let label = escape_md_cell(labels.get(i).map_or("unknown", |s| s.as_str()));
168 let conv_vals: Vec<f64> = map
169 .iter()
170 .zip(converged_map.iter())
171 .filter(|&(_, &conv)| conv)
172 .map(|(&d, _)| d)
173 .filter(|d| d.is_finite())
174 .collect();
175 if conv_vals.is_empty() {
176 report.push_str(&format!("| {label} | N/A | N/A |\n"));
177 } else {
178 let mean: f64 = conv_vals.iter().sum::<f64>() / conv_vals.len() as f64;
179 let variance: f64 =
180 conv_vals.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / conv_vals.len() as f64;
181 let std_dev = variance.sqrt();
182 report.push_str(&format!("| {label} | {mean:.6e} | {std_dev:.6e} |\n"));
183 }
184 }
185 report.push('\n');
186
187 if !provenance.is_empty() {
189 report.push_str("## Provenance Log\n\n");
190 report.push_str("| Time | Event |\n");
191 report.push_str("|------|-------|\n");
192 for (timestamp, message) in provenance {
193 let ts = escape_md_cell(timestamp);
194 let msg = escape_md_cell(message);
195 report.push_str(&format!("| {ts} | {msg} |\n"));
196 }
197 report.push('\n');
198 }
199
200 std::fs::write(path, report).map_err(|e| {
201 IoError::WriteError(format!("cannot write report to {}: {e}", path.display()))
202 })?;
203
204 Ok(())
205}