nereids_endf/
retrieval.rs1use nereids_core::elements;
11use nereids_core::types::Isotope;
12use std::fs;
13use std::io::Read;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EndfLibrary {
19 EndfB8_0,
21 EndfB8_1,
23 Jeff3_3,
25 Jendl5,
27}
28
29impl EndfLibrary {
30 fn url_path(&self) -> &'static str {
32 match self {
33 Self::EndfB8_0 => "ENDF-B-VIII.0/n",
34 Self::EndfB8_1 => "ENDF-B-VIII.1/n",
35 Self::Jeff3_3 => "JEFF-3.3/n",
36 Self::Jendl5 => "JENDL-5/n",
37 }
38 }
39
40 fn cache_dir_name(&self) -> &'static str {
42 match self {
43 Self::EndfB8_0 => "ENDF-B-VIII.0",
44 Self::EndfB8_1 => "ENDF-B-VIII.1",
45 Self::Jeff3_3 => "JEFF-3.3",
46 Self::Jendl5 => "JENDL-5",
47 }
48 }
49
50 fn zip_filename(&self, isotope: &Isotope, mat: u32) -> String {
56 let sym = elements::element_symbol(isotope.z()).unwrap_or("X");
57 let z = isotope.z();
58 let a = isotope.a();
59 match self {
60 Self::EndfB8_0 | Self::Jeff3_3 => {
61 format!("n_{mat:04}_{z}-{sym}-{a}.zip")
62 }
63 Self::EndfB8_1 | Self::Jendl5 => {
64 format!("n_{z:03}-{sym}-{a}_{mat:04}.zip")
65 }
66 }
67 }
68}
69
70pub struct EndfRetriever {
72 cache_root: PathBuf,
74 base_url: String,
76}
77
78impl EndfRetriever {
79 pub fn new() -> Self {
81 let cache_root = dirs::cache_dir()
82 .unwrap_or_else(|| PathBuf::from(".cache"))
83 .join("nereids")
84 .join("endf");
85 Self {
86 cache_root,
87 base_url: "https://www-nds.iaea.org/public/download-endf".to_string(),
88 }
89 }
90
91 pub fn with_cache_dir(cache_dir: impl Into<PathBuf>) -> Self {
93 Self {
94 cache_root: cache_dir.into(),
95 base_url: "https://www-nds.iaea.org/public/download-endf".to_string(),
96 }
97 }
98
99 fn cache_dir(&self, library: EndfLibrary) -> PathBuf {
101 self.cache_root.join(library.cache_dir_name())
102 }
103
104 fn cache_file_path(&self, isotope: &Isotope, library: EndfLibrary) -> PathBuf {
106 let sym = elements::element_symbol(isotope.z()).unwrap_or("X");
107 let filename = format!("{}-{}.endf", sym, isotope.a());
108 self.cache_dir(library).join(filename)
109 }
110
111 pub fn get_endf_file(
120 &self,
121 isotope: &Isotope,
122 library: EndfLibrary,
123 mat: u32,
124 ) -> Result<(PathBuf, String), EndfRetrievalError> {
125 let cache_path = self.cache_file_path(isotope, library);
126
127 if cache_path.exists() {
129 let contents = fs::read_to_string(&cache_path)?;
130 return Ok((cache_path, contents));
131 }
132
133 let contents = self.download_endf(isotope, library, mat)?;
135
136 if let Some(parent) = cache_path.parent() {
138 fs::create_dir_all(parent)?;
139 }
140 fs::write(&cache_path, &contents)?;
141
142 Ok((cache_path, contents))
143 }
144
145 fn download_endf(
147 &self,
148 isotope: &Isotope,
149 library: EndfLibrary,
150 mat: u32,
151 ) -> Result<String, EndfRetrievalError> {
152 let zip_filename = library.zip_filename(isotope, mat);
153 let url = format!("{}/{}/{}", self.base_url, library.url_path(), zip_filename);
154
155 let response = reqwest::blocking::get(&url).map_err(|e| {
156 EndfRetrievalError::NetworkError(format!("Failed to connect to {}: {}", url, e))
157 })?;
158
159 let status = response.status();
160 if status == reqwest::StatusCode::NOT_FOUND {
161 return Err(EndfRetrievalError::NotInLibrary {
162 isotope: format!(
163 "{}-{}",
164 nereids_core::elements::element_symbol(isotope.z()).unwrap_or("?"),
165 isotope.a()
166 ),
167 library: library.cache_dir_name().to_string(),
168 });
169 }
170 if !status.is_success() {
171 return Err(EndfRetrievalError::NetworkError(format!(
172 "HTTP {} for {}",
173 status, url
174 )));
175 }
176
177 let bytes = response.bytes().map_err(|e| {
178 EndfRetrievalError::NetworkError(format!("Failed to read response body: {}", e))
179 })?;
180
181 self.extract_endf_from_zip(&bytes)
183 }
184
185 fn extract_endf_from_zip(&self, zip_bytes: &[u8]) -> Result<String, EndfRetrievalError> {
189 let cursor = std::io::Cursor::new(zip_bytes);
190 let mut archive = zip::ZipArchive::new(cursor)
191 .map_err(|e| EndfRetrievalError::Parse(format!("Invalid ZIP archive: {}", e)))?;
192
193 for i in 0..archive.len() {
195 let mut file = archive.by_index(i).map_err(|e| {
196 EndfRetrievalError::Parse(format!("Failed to read ZIP entry: {}", e))
197 })?;
198
199 let name = file.name().to_lowercase();
200 if name.ends_with(".endf") || name.ends_with(".dat") || name.ends_with(".txt") {
201 let mut contents = String::new();
202 file.read_to_string(&mut contents).map_err(|e| {
203 EndfRetrievalError::Parse(format!("Failed to read ENDF content: {}", e))
204 })?;
205 return Ok(contents);
206 }
207 }
208
209 if !archive.is_empty() {
211 let mut file = archive.by_index(0).map_err(|e| {
212 EndfRetrievalError::Parse(format!("Failed to read ZIP entry: {}", e))
213 })?;
214 let mut contents = String::new();
215 file.read_to_string(&mut contents).map_err(|e| {
216 EndfRetrievalError::Parse(format!("Failed to read ENDF content: {}", e))
217 })?;
218 return Ok(contents);
219 }
220
221 Err(EndfRetrievalError::Parse(
222 "No ENDF data file found in ZIP archive".to_string(),
223 ))
224 }
225
226 pub fn load_local(path: &Path) -> Result<String, EndfRetrievalError> {
228 fs::read_to_string(path).map_err(EndfRetrievalError::from)
229 }
230
231 pub fn clear_cache(&self, library: Option<EndfLibrary>) -> Result<(), EndfRetrievalError> {
233 match library {
234 Some(lib) => {
235 let dir = self.cache_dir(lib);
236 if dir.exists() {
237 fs::remove_dir_all(&dir)?;
238 }
239 }
240 None => {
241 if self.cache_root.exists() {
242 fs::remove_dir_all(&self.cache_root)?;
243 }
244 }
245 }
246 Ok(())
247 }
248}
249
250impl Default for EndfRetriever {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub fn mat_number(isotope: &Isotope) -> Option<u32> {
263 endf_mat::mat_number(isotope.z(), isotope.a())
264}
265
266#[derive(Debug, thiserror::Error)]
268pub enum EndfRetrievalError {
269 #[error("Network error: {0}")]
271 NetworkError(String),
272
273 #[error("{isotope} is not available in the {library} library")]
275 NotInLibrary { isotope: String, library: String },
276
277 #[error("Parse error: {0}")]
278 Parse(String),
279
280 #[error("I/O error: {0}")]
281 Io(#[from] std::io::Error),
282
283 #[error("Isotope not found in MAT database: {0}")]
284 UnknownIsotope(String),
285}