1use std::collections::BTreeMap;
4use std::fs;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use phasesmith_model::{ProjectRecord, RadiationProbe};
9use serde::Serialize;
10
11use crate::{PROJECT_FORMAT_VERSION, PersistenceError};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct ProjectReportSaveOptions {
16 pub overwrite: bool,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
22pub struct HistogramSummary {
23 pub histogram_id: String,
25 pub name: String,
27 pub sample_count: usize,
29 pub has_observations: bool,
31 pub coordinate_kind: String,
33 pub probe: String,
35 pub phase_ids: Vec<String>,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
41pub struct PhaseSummary {
42 pub phase_id: String,
44 pub name: String,
46 pub reflection_count: usize,
48 pub site_count: usize,
50 pub required_providers: Vec<String>,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
56pub struct ProjectSummaryReport {
57 pub format_version: u32,
59 pub project_id: String,
61 pub revision: u64,
63 pub name: String,
65 pub histogram_count: usize,
67 pub phase_count: usize,
69 pub total_sample_count: usize,
71 pub histograms: Vec<HistogramSummary>,
73 pub phases: Vec<PhaseSummary>,
75 pub metadata: BTreeMap<String, String>,
77}
78
79impl ProjectSummaryReport {
80 pub fn from_project(project: &ProjectRecord) -> Result<Self, PersistenceError> {
87 project.validate().map_err(PersistenceError::Domain)?;
88 let total_sample_count = project
89 .histograms
90 .iter()
91 .map(|histogram| histogram.pattern.sample_count())
92 .chain(
93 project
94 .tof_histograms
95 .iter()
96 .map(|histogram| histogram.pattern.sample_count()),
97 )
98 .try_fold(0_usize, |total, sample_count| {
99 total
100 .checked_add(sample_count)
101 .ok_or_else(|| PersistenceError::InvalidRecord {
102 message: "project summary sample count overflow".to_owned(),
103 })
104 })?;
105 Ok(Self {
106 format_version: PROJECT_FORMAT_VERSION,
107 project_id: project.project_id.as_str().to_owned(),
108 revision: project.revision,
109 name: project.name.clone(),
110 histogram_count: project.histograms.len() + project.tof_histograms.len(),
111 phase_count: project.phases.len(),
112 total_sample_count,
113 histograms: project
114 .histograms
115 .iter()
116 .map(|histogram| HistogramSummary {
117 histogram_id: histogram.histogram_id.as_str().to_owned(),
118 name: histogram.name.clone(),
119 sample_count: histogram.pattern.sample_count(),
120 has_observations: histogram.pattern.observed_y.is_some(),
121 coordinate_kind: "two_theta_deg".to_owned(),
122 probe: match histogram.experiment.radiation.probe() {
123 RadiationProbe::Xray => "xray",
124 RadiationProbe::Neutron => "neutron",
125 }
126 .to_owned(),
127 phase_ids: histogram
128 .phase_ids
129 .iter()
130 .map(|value| value.as_str().to_owned())
131 .collect(),
132 })
133 .chain(project.tof_histograms.iter().map(|histogram| {
134 HistogramSummary {
135 histogram_id: histogram.histogram_id.as_str().to_owned(),
136 name: histogram.name.clone(),
137 sample_count: histogram.pattern.sample_count(),
138 has_observations: histogram.pattern.observed_y.is_some(),
139 coordinate_kind: "tof_us".to_owned(),
140 probe: "neutron".to_owned(),
141 phase_ids: histogram
142 .phase_ids
143 .iter()
144 .map(|value| value.as_str().to_owned())
145 .collect(),
146 }
147 }))
148 .collect(),
149 phases: project
150 .phases
151 .iter()
152 .map(|phase| PhaseSummary {
153 phase_id: phase.phase_id.as_str().to_owned(),
154 name: phase.name.clone(),
155 reflection_count: phase.definition.hkl.len(),
156 site_count: phase.definition.fractional_xyz.len(),
157 required_providers: phase
158 .required_providers
159 .iter()
160 .map(|requirement| {
161 format!(
162 "{}@{}",
163 requirement.provider_id, requirement.provider_version
164 )
165 })
166 .collect(),
167 })
168 .collect(),
169 metadata: project.metadata.clone(),
170 })
171 }
172}
173
174pub fn project_summary_json(project: &ProjectRecord) -> Result<String, PersistenceError> {
180 let report = ProjectSummaryReport::from_project(project)?;
181 let mut encoded = serde_json::to_string_pretty(&report)?;
182 encoded.push('\n');
183 Ok(encoded)
184}
185
186pub fn write_project_summary_json(
193 project: &ProjectRecord,
194 path: impl AsRef<Path>,
195) -> Result<PathBuf, PersistenceError> {
196 write_project_summary_json_with_options(
197 project,
198 path,
199 ProjectReportSaveOptions { overwrite: true },
200 )
201}
202
203pub fn write_project_summary_json_with_options(
210 project: &ProjectRecord,
211 path: impl AsRef<Path>,
212 options: ProjectReportSaveOptions,
213) -> Result<PathBuf, PersistenceError> {
214 let path = path.as_ref();
215 if let Some(parent) = path.parent()
216 && !parent.as_os_str().is_empty()
217 {
218 fs::create_dir_all(parent)?;
219 }
220 let encoded = project_summary_json(project)?;
221 if options.overwrite {
222 fs::write(path, encoded)?;
223 } else {
224 fs::OpenOptions::new()
225 .write(true)
226 .create_new(true)
227 .open(path)
228 .map_err(|error| {
229 if error.kind() == std::io::ErrorKind::AlreadyExists {
230 PersistenceError::InvalidDestination {
231 message: format!("report destination already exists: {}", path.display()),
232 }
233 } else {
234 PersistenceError::Io(error)
235 }
236 })?
237 .write_all(encoded.as_bytes())?;
238 }
239 Ok(path.to_owned())
240}