1use std::collections::BTreeMap;
89use std::io::{self, Write};
90
91use serde::{Deserialize, Serialize};
92
93use super::ion::ReplacementTracker;
94use crate::elements::{element, element_by_symbol};
95use crate::geometry::VoxelGrid;
96use crate::ion::bca::{BcaTally, ElectronicChannel, Face, LatticeDeposit, Particle};
97
98#[derive(Debug, Clone, PartialEq, thiserror::Error)]
100pub enum VoxelTallyError {
101 #[error("voxel tally grid dimension on axis {axis} is zero")]
103 ZeroDimension {
104 axis: usize,
106 },
107 #[error(
109 "voxel tally spacing {spacing_m} m on axis {axis} is invalid: must be finite and positive"
110 )]
111 InvalidSpacing {
112 axis: usize,
114 spacing_m: f64,
116 },
117 #[error("voxel tally origin {origin_m} m on axis {axis} is not finite")]
119 InvalidOrigin {
120 axis: usize,
122 origin_m: f64,
124 },
125 #[error("voxel tally dimensions {dims:?} overflow the voxel count")]
127 TooManyVoxels {
128 dims: [usize; 3],
130 },
131 #[error("voxel tally parse error (line {line}): {message}")]
133 Parse {
134 line: usize,
136 message: String,
138 },
139}
140
141fn parse_err(line: usize, message: impl Into<String>) -> VoxelTallyError {
142 VoxelTallyError::Parse {
143 line,
144 message: message.into(),
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
152pub struct VoxelTallyGrid {
153 origin_m: [f64; 3],
154 spacing_m: [f64; 3],
155 dims: [usize; 3],
156}
157
158impl VoxelTallyGrid {
159 pub fn new(
163 origin_m: [f64; 3],
164 spacing_m: [f64; 3],
165 dims: [usize; 3],
166 ) -> Result<Self, VoxelTallyError> {
167 for axis in 0..3 {
168 if dims[axis] == 0 {
169 return Err(VoxelTallyError::ZeroDimension { axis });
170 }
171 let h = spacing_m[axis];
172 if !(h.is_finite() && h > 0.0) {
173 return Err(VoxelTallyError::InvalidSpacing { axis, spacing_m: h });
174 }
175 if !origin_m[axis].is_finite() {
176 return Err(VoxelTallyError::InvalidOrigin {
177 axis,
178 origin_m: origin_m[axis],
179 });
180 }
181 }
182 (dims[0] as u64)
183 .checked_mul(dims[1] as u64)
184 .and_then(|v| v.checked_mul(dims[2] as u64))
185 .ok_or(VoxelTallyError::TooManyVoxels { dims })?;
186 Ok(Self {
187 origin_m,
188 spacing_m,
189 dims,
190 })
191 }
192
193 pub fn from_voxel_grid(grid: &VoxelGrid) -> Self {
196 Self {
197 origin_m: [0.0; 3],
198 spacing_m: grid.spacing_m(),
199 dims: grid.dims(),
200 }
201 }
202
203 pub fn origin_m(&self) -> [f64; 3] {
205 self.origin_m
206 }
207
208 pub fn spacing_m(&self) -> [f64; 3] {
210 self.spacing_m
211 }
212
213 pub fn dims(&self) -> [usize; 3] {
215 self.dims
216 }
217
218 pub fn n_voxels(&self) -> u64 {
220 self.dims.iter().map(|&n| n as u64).product()
221 }
222
223 pub fn locate(&self, pos: [f64; 3]) -> Option<[usize; 3]> {
228 let mut ijk = [0usize; 3];
229 for a in 0..3 {
230 let s = ((pos[a] - self.origin_m[a]) / self.spacing_m[a]).floor();
231 if !(s >= 0.0 && s < self.dims[a] as f64) {
232 return None;
233 }
234 ijk[a] = s as usize;
235 }
236 Some(ijk)
237 }
238
239 pub fn flat_index(&self, ijk: [usize; 3]) -> u64 {
241 ijk[0] as u64 + self.dims[0] as u64 * (ijk[1] as u64 + self.dims[1] as u64 * ijk[2] as u64)
242 }
243
244 pub fn unflatten(&self, flat: u64) -> [usize; 3] {
247 let nx = self.dims[0] as u64;
248 let ny = self.dims[1] as u64;
249 [
250 (flat % nx) as usize,
251 ((flat / nx) % ny) as usize,
252 (flat / (nx * ny)) as usize,
253 ]
254 }
255
256 pub fn corner_m(&self, ijk: [usize; 3]) -> [f64; 3] {
258 std::array::from_fn(|a| self.origin_m[a] + ijk[a] as f64 * self.spacing_m[a])
259 }
260
261 pub fn center_m(&self, ijk: [usize; 3]) -> [f64; 3] {
263 std::array::from_fn(|a| self.origin_m[a] + (ijk[a] as f64 + 0.5) * self.spacing_m[a])
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
270pub struct SpeciesDefects {
271 pub vacancies: u64,
273 pub interstitials: u64,
275 pub replacements: u64,
277}
278
279impl SpeciesDefects {
280 pub fn add(&mut self, o: &SpeciesDefects) {
282 self.vacancies += o.vacancies;
283 self.interstitials += o.interstitials;
284 self.replacements += o.replacements;
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
290struct DefectAcc {
291 displacements: u64,
292 replacements: u64,
293 interstitials: u64,
294}
295
296impl DefectAcc {
297 fn add(&mut self, o: &DefectAcc) {
298 self.displacements += o.displacements;
299 self.replacements += o.replacements;
300 self.interstitials += o.interstitials;
301 }
302
303 fn report(&self) -> SpeciesDefects {
304 SpeciesDefects {
305 vacancies: self.displacements - self.replacements,
306 interstitials: self.interstitials,
307 replacements: self.replacements,
308 }
309 }
310}
311
312fn report_defects(acc: &[DefectAcc]) -> Vec<SpeciesDefects> {
313 acc.iter().map(DefectAcc::report).collect()
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
318pub struct VoxelCell {
319 pub implanted: u64,
321 pub displacements: u64,
323 pub electronic_ev: f64,
325 pub lattice_ev: f64,
327 pub rest_ev: f64,
329}
330
331impl VoxelCell {
332 pub fn deposited_ev(&self) -> f64 {
335 self.electronic_ev + self.lattice_ev + self.rest_ev
336 }
337
338 pub fn add(&mut self, o: &VoxelCell) {
340 self.implanted += o.implanted;
341 self.displacements += o.displacements;
342 self.electronic_ev += o.electronic_ev;
343 self.lattice_ev += o.lattice_ev;
344 self.rest_ev += o.rest_ev;
345 }
346}
347
348#[derive(Debug, Clone)]
350pub struct VoxelTally {
351 grid: VoxelTallyGrid,
352 histories: u64,
353 cells: BTreeMap<u64, VoxelCell>,
354 outside: VoxelCell,
355 species_z: Vec<u8>,
357 defects: BTreeMap<u64, Vec<DefectAcc>>,
359 outside_defects: Vec<DefectAcc>,
360 tracker: ReplacementTracker,
361 cuts: Vec<f64>,
363}
364
365impl VoxelTally {
366 pub fn new(grid: VoxelTallyGrid) -> Self {
368 Self {
369 grid,
370 histories: 0,
371 cells: BTreeMap::new(),
372 outside: VoxelCell::default(),
373 species_z: Vec::new(),
374 defects: BTreeMap::new(),
375 outside_defects: Vec::new(),
376 tracker: ReplacementTracker::default(),
377 cuts: Vec::new(),
378 }
379 }
380
381 pub fn with_species(mut self, species_z: &[u8]) -> Self {
385 self.species_z = species_z.to_vec();
386 self.outside_defects = vec![DefectAcc::default(); species_z.len()];
387 self
388 }
389
390 pub fn species_z(&self) -> &[u8] {
392 &self.species_z
393 }
394
395 fn defect_at(&mut self, pos: [f64; 3], species: usize) -> &mut DefectAcc {
396 let n = self.species_z.len();
397 match self.grid.locate(pos) {
398 Some(ijk) => &mut self
399 .defects
400 .entry(self.grid.flat_index(ijk))
401 .or_insert_with(|| vec![DefectAcc::default(); n])[species],
402 None => &mut self.outside_defects[species],
403 }
404 }
405
406 pub fn grid(&self) -> &VoxelTallyGrid {
408 &self.grid
409 }
410
411 pub fn occupied(&self) -> usize {
413 self.cells.len()
414 }
415
416 fn cell_at(&mut self, pos: [f64; 3]) -> &mut VoxelCell {
417 match self.grid.locate(pos) {
418 Some(ijk) => self.cells.entry(self.grid.flat_index(ijk)).or_default(),
419 None => &mut self.outside,
420 }
421 }
422
423 fn spread(
426 &mut self,
427 from: [f64; 3],
428 to: [f64; 3],
429 energy_ev: f64,
430 add: fn(&mut VoxelCell, f64),
431 ) {
432 let (a, b) = (self.grid.locate(from), self.grid.locate(to));
433 if a.is_some() && a == b {
434 add(self.cell_at(from), energy_ev);
435 return;
436 }
437 let mut cuts = std::mem::take(&mut self.cuts);
440 cuts.clear();
441 cuts.push(0.0);
442 for ax in 0..3 {
443 let h = self.grid.spacing_m[ax];
444 let f0 = (from[ax] - self.grid.origin_m[ax]) / h;
445 let f1 = (to[ax] - self.grid.origin_m[ax]) / h;
446 if !(f0.is_finite() && f1.is_finite()) || f0 == f1 {
447 continue;
448 }
449 let n = self.grid.dims[ax] as f64;
450 let first = (f0.min(f1).floor() + 1.0).max(0.0);
451 let last = (f0.max(f1).ceil() - 1.0).min(n);
452 let mut k = first;
453 while k <= last {
454 let t = (k - f0) / (f1 - f0);
455 if t > 0.0 && t < 1.0 {
456 cuts.push(t);
457 }
458 k += 1.0;
459 }
460 }
461 cuts.push(1.0);
462 cuts.sort_by(f64::total_cmp);
463 for w in cuts.windows(2) {
464 let (t0, t1) = (w[0], w[1]);
465 if t1 > t0 {
466 let tm = 0.5 * (t0 + t1);
467 let mid = std::array::from_fn(|i| from[i] + tm * (to[i] - from[i]));
468 add(self.cell_at(mid), energy_ev * (t1 - t0));
469 }
470 }
471 self.cuts = cuts;
472 }
473
474 pub fn report(&self) -> VoxelReport {
476 let n = self.species_z.len();
477 VoxelReport {
478 grid: self.grid,
479 histories: self.histories,
480 voxels: self
481 .cells
482 .iter()
483 .map(|(&flat, &cell)| VoxelRecord {
484 index: self.grid.unflatten(flat),
485 cell,
486 defects: match self.defects.get(&flat) {
487 Some(acc) => report_defects(acc),
488 None => vec![SpeciesDefects::default(); n],
489 },
490 })
491 .collect(),
492 outside: self.outside,
493 species_z: self.species_z.clone(),
494 outside_defects: report_defects(&self.outside_defects),
495 }
496 }
497}
498
499impl BcaTally for VoxelTally {
500 fn electronic(
501 &mut self,
502 p: &Particle,
503 from: [f64; 3],
504 channel: ElectronicChannel,
505 energy_ev: f64,
506 ) {
507 let add = |c: &mut VoxelCell, e: f64| c.electronic_ev += e;
508 match channel {
509 ElectronicChannel::NonLocal => self.spread(from, p.pos, energy_ev, add),
510 ElectronicChannel::Local => add(self.cell_at(p.pos), energy_ev),
511 }
512 }
513
514 fn begin_history(&mut self, _index: u64) {
515 self.tracker.clear();
516 }
517
518 fn lattice(&mut self, at: [f64; 3], _layer: usize, kind: LatticeDeposit, energy_ev: f64) {
519 if kind == LatticeDeposit::Subthreshold {
520 self.tracker.clear();
521 }
522 self.cell_at(at).lattice_ev += energy_ev;
523 }
524
525 fn recoil(&mut self, r: &Particle) {
526 self.cell_at(r.pos).displacements += 1;
527 self.tracker.recoil(r);
528 if !self.species_z.is_empty() {
529 self.defect_at(r.pos, r.species).displacements += 1;
530 }
531 }
532
533 fn stopped(&mut self, p: &Particle) {
534 let primary = p.is_primary();
535 let replaced = self.tracker.stopped(p);
536 if !self.species_z.is_empty() {
537 if let Some(species) = replaced {
538 self.defect_at(p.pos, species).replacements += 1;
539 } else if !primary {
540 self.defect_at(p.pos, p.species).interstitials += 1;
541 }
542 }
543 let cell = self.cell_at(p.pos);
544 cell.rest_ev += p.energy_ev;
545 if primary {
546 cell.implanted += 1;
547 }
548 }
549
550 fn escaped(&mut self, _p: &Particle, _face: Face) {
551 self.tracker.clear();
552 }
553
554 fn end_history(&mut self, _index: u64, _budget: &crate::ion::bca::EnergyBudget) {
555 self.tracker.clear();
556 self.histories += 1;
557 }
558
559 fn merge(&mut self, o: Self) {
564 assert!(
565 self.grid == o.grid,
566 "merging voxel tallies built on different grids"
567 );
568 assert!(
569 self.species_z == o.species_z,
570 "merging voxel tallies built for different species"
571 );
572 for (a, b) in self.outside_defects.iter_mut().zip(&o.outside_defects) {
573 a.add(b);
574 }
575 for (flat, acc) in o.defects {
576 let mine = self
577 .defects
578 .entry(flat)
579 .or_insert_with(|| vec![DefectAcc::default(); acc.len()]);
580 for (a, b) in mine.iter_mut().zip(&acc) {
581 a.add(b);
582 }
583 }
584 self.histories += o.histories;
585 self.outside.add(&o.outside);
586 for (flat, cell) in o.cells {
587 self.cells.entry(flat).or_default().add(&cell);
588 }
589 }
590}
591
592#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
594pub struct VoxelRecord {
595 pub index: [usize; 3],
597 pub cell: VoxelCell,
599 #[serde(default)]
602 pub defects: Vec<SpeciesDefects>,
603}
604
605#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
608pub struct VoxelReport {
609 pub grid: VoxelTallyGrid,
611 pub histories: u64,
613 pub voxels: Vec<VoxelRecord>,
615 pub outside: VoxelCell,
617 #[serde(default)]
620 pub species_z: Vec<u8>,
621 #[serde(default)]
623 pub outside_defects: Vec<SpeciesDefects>,
624}
625
626const CSV_COLUMNS: [&str; 12] = [
627 "i",
628 "j",
629 "k",
630 "x_m",
631 "y_m",
632 "z_m",
633 "implanted",
634 "displacements",
635 "electronic_ev",
636 "lattice_ev",
637 "rest_ev",
638 "deposited_ev",
639];
640
641const VTK_SCALARS: [(&str, &str); 6] = [
642 ("implanted", "unsigned_long"),
643 ("displacements", "unsigned_long"),
644 ("electronic_ev", "double"),
645 ("lattice_ev", "double"),
646 ("rest_ev", "double"),
647 ("deposited_ev", "double"),
648];
649
650const VOXEL_CORNERS: [[usize; 3]; 8] = [
652 [0, 0, 0],
653 [1, 0, 0],
654 [0, 1, 0],
655 [1, 1, 0],
656 [0, 0, 1],
657 [1, 0, 1],
658 [0, 1, 1],
659 [1, 1, 1],
660];
661
662const VTK_VOXEL: u8 = 11;
663
664fn join3(v: [f64; 3]) -> String {
665 format!("{:e},{:e},{:e}", v[0], v[1], v[2])
666}
667
668fn parse3<T: std::str::FromStr>(s: &str, line: usize) -> Result<[T; 3], VoxelTallyError> {
669 let v: Vec<T> = s
670 .split(',')
671 .map(|t| t.trim().parse::<T>())
672 .collect::<Result<_, _>>()
673 .map_err(|_| parse_err(line, format!("cannot parse the triple {s:?}")))?;
674 v.try_into()
675 .map_err(|_| parse_err(line, format!("expected three values in {s:?}")))
676}
677
678fn num<T: std::str::FromStr>(s: &str, line: usize) -> Result<T, VoxelTallyError> {
679 s.trim()
680 .parse::<T>()
681 .map_err(|_| parse_err(line, format!("cannot parse the number {s:?}")))
682}
683
684struct Tokens<'a> {
686 iter: Box<dyn Iterator<Item = (usize, &'a str)> + 'a>,
687 last_line: usize,
688}
689
690impl<'a> Tokens<'a> {
691 fn next(&mut self, what: &str) -> Result<(usize, &'a str), VoxelTallyError> {
692 self.iter
693 .next()
694 .ok_or_else(|| parse_err(self.last_line, format!("expected {what}")))
695 }
696
697 fn expect(&mut self, word: &str) -> Result<(), VoxelTallyError> {
698 let (n, t) = self.next(word)?;
699 if t == word {
700 Ok(())
701 } else {
702 Err(parse_err(n, format!("expected `{word}`, found `{t}`")))
703 }
704 }
705}
706
707fn symbol(z: u8) -> &'static str {
708 element(z).map_or("?", |e| e.symbol)
709}
710
711fn species_list(species_z: &[u8]) -> String {
713 let v: Vec<String> = species_z
714 .iter()
715 .enumerate()
716 .map(|(i, &z)| format!("{i}:{}", symbol(z)))
717 .collect();
718 v.join(",")
719}
720
721fn parse_species(s: &str, line: usize) -> Result<Vec<u8>, VoxelTallyError> {
722 let mut out = Vec::new();
723 for (i, item) in s.split(',').enumerate() {
724 let item = item.trim();
725 let z = item
726 .split_once(':')
727 .filter(|(idx, _)| idx.parse::<usize>() == Ok(i))
728 .and_then(|(_, sym)| element_by_symbol(sym))
729 .ok_or_else(|| parse_err(line, format!("cannot parse the species {item:?}")))?
730 .z;
731 out.push(z);
732 }
733 Ok(out)
734}
735
736fn defect_columns(species_z: &[u8]) -> Vec<String> {
739 let mut out = Vec::new();
740 for (i, &z) in species_z.iter().enumerate() {
741 for kind in ["vacancies", "interstitials", "replacements"] {
742 out.push(format!("{kind}_{i}_{}", symbol(z)));
743 }
744 }
745 out
746}
747
748fn defect_values(d: &[SpeciesDefects]) -> impl Iterator<Item = u64> + '_ {
749 d.iter()
750 .flat_map(|s| [s.vacancies, s.interstitials, s.replacements])
751}
752
753fn defects_from_values(v: &[u64]) -> Vec<SpeciesDefects> {
754 v.chunks(3)
755 .map(|c| SpeciesDefects {
756 vacancies: c[0],
757 interstitials: c[1],
758 replacements: c[2],
759 })
760 .collect()
761}
762
763pub const EXPORT_BANNER: &str = "# lindhard damage export v1";
765
766pub const EXPORT_FORMAT_VERSION: u32 = 1;
768
769const EXPORT_UNITS: &str = "length=m; energy=eV; counts=1";
771
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
776pub struct ExportProvenance {
777 pub engine: String,
779 pub lindhard_version: String,
781 pub seed: u64,
783 pub input_sha256: String,
786}
787
788impl ExportProvenance {
789 pub fn new(engine: &str, seed: u64, input_sha256: &str) -> Self {
792 Self {
793 engine: engine.to_string(),
794 lindhard_version: crate::VERSION.to_string(),
795 seed,
796 input_sha256: input_sha256.to_string(),
797 }
798 }
799}
800
801impl VoxelReport {
802 pub fn defect_totals(&self) -> Vec<SpeciesDefects> {
805 let mut t = vec![SpeciesDefects::default(); self.species_z.len()];
806 for v in &self.voxels {
807 for (a, b) in t.iter_mut().zip(&v.defects) {
808 a.add(b);
809 }
810 }
811 t
812 }
813
814 pub fn grand_defect_totals(&self) -> Vec<SpeciesDefects> {
816 let mut t = self.defect_totals();
817 for (a, b) in t.iter_mut().zip(&self.outside_defects) {
818 a.add(b);
819 }
820 t
821 }
822
823 pub fn totals(&self) -> VoxelCell {
826 let mut t = VoxelCell::default();
827 for v in &self.voxels {
828 t.add(&v.cell);
829 }
830 t
831 }
832
833 pub fn grand_totals(&self) -> VoxelCell {
835 let mut t = self.totals();
836 t.add(&self.outside);
837 t
838 }
839
840 pub fn profile(&self, axis: usize) -> Vec<VoxelCell> {
844 let mut out = vec![VoxelCell::default(); self.grid.dims[axis]];
845 for v in &self.voxels {
846 out[v.index[axis]].add(&v.cell);
847 }
848 out
849 }
850
851 pub fn write_csv<W: Write>(&self, w: W) -> io::Result<()> {
855 self.write_csv_inner(w, None)
856 }
857
858 pub fn write_export_csv<W: Write>(&self, w: W, p: &ExportProvenance) -> io::Result<()> {
864 self.write_csv_inner(w, Some(p))
865 }
866
867 pub fn to_export_csv(&self, p: &ExportProvenance) -> String {
869 let mut buf = Vec::new();
870 self.write_export_csv(&mut buf, p)
871 .expect("writing to a Vec");
872 String::from_utf8(buf).expect("the CSV is ASCII")
873 }
874
875 fn write_csv_inner<W: Write>(
876 &self,
877 mut w: W,
878 export: Option<&ExportProvenance>,
879 ) -> io::Result<()> {
880 let g = &self.grid;
881 let o = &self.outside;
882 let v2 = !self.species_z.is_empty();
883 if let Some(p) = export {
884 writeln!(w, "{EXPORT_BANNER}")?;
885 writeln!(w, "# format_version = {EXPORT_FORMAT_VERSION}")?;
886 writeln!(w, "# engine = {}", p.engine)?;
887 writeln!(w, "# lindhard_version = {}", p.lindhard_version)?;
888 writeln!(w, "# seed = {}", p.seed)?;
889 writeln!(w, "# input_sha256 = {}", p.input_sha256)?;
890 writeln!(w, "# units = {EXPORT_UNITS}")?;
891 } else {
892 writeln!(w, "# lindhard voxel tally v{}", if v2 { 2 } else { 1 })?;
893 }
894 writeln!(w, "# origin_m = {}", join3(g.origin_m))?;
895 writeln!(w, "# spacing_m = {}", join3(g.spacing_m))?;
896 writeln!(w, "# dims = {},{},{}", g.dims[0], g.dims[1], g.dims[2])?;
897 writeln!(w, "# histories = {}", self.histories)?;
898 writeln!(
899 w,
900 "# outside = {},{},{:e},{:e},{:e}",
901 o.implanted, o.displacements, o.electronic_ev, o.lattice_ev, o.rest_ev
902 )?;
903 let mut columns: Vec<String> = CSV_COLUMNS.iter().map(|c| (*c).to_string()).collect();
904 if v2 {
905 writeln!(w, "# species = {}", species_list(&self.species_z))?;
906 let od: Vec<String> = defect_values(&self.outside_defects)
907 .map(|n| n.to_string())
908 .collect();
909 writeln!(w, "# outside_defects = {}", od.join(","))?;
910 columns.extend(defect_columns(&self.species_z));
911 }
912 writeln!(w, "{}", columns.join(","))?;
913 for v in &self.voxels {
914 let c = g.center_m(v.index);
915 let t = &v.cell;
916 write!(
917 w,
918 "{},{},{},{:e},{:e},{:e},{},{},{:e},{:e},{:e},{:e}",
919 v.index[0],
920 v.index[1],
921 v.index[2],
922 c[0],
923 c[1],
924 c[2],
925 t.implanted,
926 t.displacements,
927 t.electronic_ev,
928 t.lattice_ev,
929 t.rest_ev,
930 t.deposited_ev()
931 )?;
932 for n in defect_values(&v.defects) {
933 write!(w, ",{n}")?;
934 }
935 writeln!(w)?;
936 }
937 Ok(())
938 }
939
940 pub fn to_csv(&self) -> String {
942 let mut buf = Vec::new();
943 self.write_csv(&mut buf).expect("writing to a Vec");
944 String::from_utf8(buf).expect("the CSV is ASCII")
945 }
946
947 pub fn read_csv(text: &str) -> Result<Self, VoxelTallyError> {
949 Self::parse_csv(text).map(|(r, _)| r)
950 }
951
952 pub fn read_export_csv(text: &str) -> Result<(Self, ExportProvenance), VoxelTallyError> {
956 let first = text.lines().map(str::trim).find(|l| !l.is_empty());
957 if first != Some(EXPORT_BANNER) {
958 return Err(parse_err(1, "not a damage-export file (banner missing)"));
959 }
960 let (report, keys) = Self::parse_csv(text)?;
961 let get = |k: &str| {
962 keys.get(k)
963 .cloned()
964 .ok_or_else(|| parse_err(1, format!("header is missing `{k}`")))
965 };
966 if get("format_version")? != EXPORT_FORMAT_VERSION.to_string() {
967 return Err(parse_err(1, "unsupported damage-export format_version"));
968 }
969 let seed = get("seed")?;
970 let prov = ExportProvenance {
971 engine: get("engine")?,
972 lindhard_version: get("lindhard_version")?,
973 seed: seed
974 .parse()
975 .map_err(|_| parse_err(1, format!("cannot parse the seed {seed:?}")))?,
976 input_sha256: get("input_sha256")?,
977 };
978 get("units")?;
979 Ok((report, prov))
980 }
981
982 fn parse_csv(
983 text: &str,
984 ) -> Result<(Self, std::collections::BTreeMap<String, String>), VoxelTallyError> {
985 let mut extra = std::collections::BTreeMap::new();
986 let mut origin = None;
987 let mut spacing = None;
988 let mut dims = None;
989 let mut histories = None;
990 let mut outside = None;
991 let mut species_z: Vec<u8> = Vec::new();
992 let mut outside_defects: Option<Vec<u64>> = None;
993 let mut lines = text.lines().enumerate().map(|(i, l)| (i + 1, l.trim()));
994 let mut header_row = None;
995 for (n, line) in lines.by_ref() {
996 if line.is_empty() {
997 continue;
998 }
999 let Some(meta) = line.strip_prefix('#') else {
1000 header_row = Some((n, line));
1001 break;
1002 };
1003 let Some((key, value)) = meta.split_once('=') else {
1004 continue; };
1006 match key.trim() {
1007 "origin_m" => origin = Some(parse3::<f64>(value, n)?),
1008 "spacing_m" => spacing = Some(parse3::<f64>(value, n)?),
1009 "dims" => dims = Some(parse3::<usize>(value, n)?),
1010 "histories" => histories = Some(num::<u64>(value, n)?),
1011 "outside" => {
1012 let f: Vec<&str> = value.split(',').collect();
1013 if f.len() != 5 {
1014 return Err(parse_err(n, "outside needs five values"));
1015 }
1016 outside = Some(VoxelCell {
1017 implanted: num(f[0], n)?,
1018 displacements: num(f[1], n)?,
1019 electronic_ev: num(f[2], n)?,
1020 lattice_ev: num(f[3], n)?,
1021 rest_ev: num(f[4], n)?,
1022 });
1023 }
1024 "species" => species_z = parse_species(value, n)?,
1025 "outside_defects" => {
1026 outside_defects = Some(
1027 value
1028 .split(',')
1029 .map(|t| num::<u64>(t, n))
1030 .collect::<Result<_, _>>()?,
1031 );
1032 }
1033 k @ ("format_version" | "engine" | "lindhard_version" | "seed" | "input_sha256"
1034 | "units") => {
1035 extra.insert(k.to_string(), value.trim().to_string());
1036 }
1037 other => return Err(parse_err(n, format!("unknown header key {other:?}"))),
1038 }
1039 }
1040 let missing = |what: &str| parse_err(1, format!("header is missing `{what}`"));
1041 let grid = VoxelTallyGrid::new(
1042 origin.ok_or_else(|| missing("origin_m"))?,
1043 spacing.ok_or_else(|| missing("spacing_m"))?,
1044 dims.ok_or_else(|| missing("dims"))?,
1045 )?;
1046 let (hn, header_row) = header_row.ok_or_else(|| parse_err(1, "no column row"))?;
1047 let mut expected: Vec<String> = CSV_COLUMNS.iter().map(|c| (*c).to_string()).collect();
1048 expected.extend(defect_columns(&species_z));
1049 if header_row != expected.join(",") {
1050 return Err(parse_err(hn, "unexpected column row"));
1051 }
1052 let outside_defects = match outside_defects {
1053 Some(v) if v.len() == 3 * species_z.len() => defects_from_values(&v),
1054 Some(_) => {
1055 return Err(parse_err(
1056 1,
1057 "outside_defects needs three values per species",
1058 ))
1059 }
1060 None if species_z.is_empty() => Vec::new(),
1061 None => return Err(parse_err(1, "header is missing `outside_defects`")),
1062 };
1063 let mut voxels: Vec<VoxelRecord> = Vec::new();
1064 for (n, line) in lines {
1065 if line.is_empty() {
1066 continue;
1067 }
1068 let f: Vec<&str> = line.split(',').collect();
1069 if f.len() != expected.len() {
1070 return Err(parse_err(n, "wrong number of columns"));
1071 }
1072 let index = [num(f[0], n)?, num(f[1], n)?, num(f[2], n)?];
1073 if (0..3).any(|a| index[a] >= grid.dims[a]) {
1074 return Err(parse_err(n, "voxel index outside the grid"));
1075 }
1076 let record = VoxelRecord {
1077 index,
1078 cell: VoxelCell {
1079 implanted: num(f[6], n)?,
1080 displacements: num(f[7], n)?,
1081 electronic_ev: num(f[8], n)?,
1082 lattice_ev: num(f[9], n)?,
1083 rest_ev: num(f[10], n)?,
1084 },
1085 defects: defects_from_values(
1086 &f[CSV_COLUMNS.len()..]
1087 .iter()
1088 .map(|t| num::<u64>(t, n))
1089 .collect::<Result<Vec<_>, _>>()?,
1090 ),
1091 };
1092 if let Some(last) = voxels.last() {
1093 if grid.flat_index(last.index) >= grid.flat_index(index) {
1094 return Err(parse_err(
1095 n,
1096 "voxels must be in increasing flat-index order",
1097 ));
1098 }
1099 }
1100 voxels.push(record);
1101 }
1102 let report = Self {
1103 grid,
1104 histories: histories.ok_or_else(|| missing("histories"))?,
1105 voxels,
1106 outside: outside.ok_or_else(|| missing("outside"))?,
1107 species_z,
1108 outside_defects,
1109 };
1110 Ok((report, extra))
1111 }
1112
1113 pub fn write_vtk<W: Write>(&self, mut w: W) -> io::Result<()> {
1118 let g = &self.grid;
1119 let v2 = !self.species_z.is_empty();
1120 let mut title = format!(
1121 "lindhard voxel tally v{} origin={} spacing={} dims={},{},{} histories={}",
1122 if v2 { 2 } else { 1 },
1123 join3(g.origin_m),
1124 join3(g.spacing_m),
1125 g.dims[0],
1126 g.dims[1],
1127 g.dims[2],
1128 self.histories
1129 );
1130 if v2 {
1131 title.push_str(&format!(" species={}", species_list(&self.species_z)));
1132 }
1133 if title.len() > 255 {
1134 return Err(io::Error::new(
1135 io::ErrorKind::InvalidInput,
1136 "the VTK title line would exceed 255 characters",
1137 ));
1138 }
1139 let n = self.voxels.len();
1140 writeln!(w, "# vtk DataFile Version 3.0")?;
1141 writeln!(w, "{title}")?;
1142 writeln!(w, "ASCII")?;
1143 writeln!(w, "DATASET UNSTRUCTURED_GRID")?;
1144 writeln!(w, "POINTS {} double", 8 * n)?;
1145 for v in &self.voxels {
1146 for c in VOXEL_CORNERS {
1147 let p = g.corner_m(std::array::from_fn(|a| v.index[a] + c[a]));
1148 writeln!(w, "{:e} {:e} {:e}", p[0], p[1], p[2])?;
1149 }
1150 }
1151 writeln!(w, "CELLS {} {}", n, 9 * n)?;
1152 for i in 0..n {
1153 let b = 8 * i;
1154 writeln!(
1155 w,
1156 "8 {} {} {} {} {} {} {} {}",
1157 b,
1158 b + 1,
1159 b + 2,
1160 b + 3,
1161 b + 4,
1162 b + 5,
1163 b + 6,
1164 b + 7
1165 )?;
1166 }
1167 writeln!(w, "CELL_TYPES {n}")?;
1168 for _ in 0..n {
1169 writeln!(w, "{VTK_VOXEL}")?;
1170 }
1171 writeln!(w, "CELL_DATA {n}")?;
1172 for (name, kind) in VTK_SCALARS {
1173 writeln!(w, "SCALARS {name} {kind} 1")?;
1174 writeln!(w, "LOOKUP_TABLE default")?;
1175 for v in &self.voxels {
1176 let c = &v.cell;
1177 match name {
1178 "implanted" => writeln!(w, "{}", c.implanted)?,
1179 "displacements" => writeln!(w, "{}", c.displacements)?,
1180 "electronic_ev" => writeln!(w, "{:e}", c.electronic_ev)?,
1181 "lattice_ev" => writeln!(w, "{:e}", c.lattice_ev)?,
1182 "rest_ev" => writeln!(w, "{:e}", c.rest_ev)?,
1183 _ => writeln!(w, "{:e}", c.deposited_ev())?,
1184 }
1185 }
1186 }
1187 for (col, name) in defect_columns(&self.species_z).iter().enumerate() {
1188 writeln!(w, "SCALARS {name} unsigned_long 1")?;
1189 writeln!(w, "LOOKUP_TABLE default")?;
1190 for v in &self.voxels {
1191 writeln!(w, "{}", defect_values(&v.defects).nth(col).unwrap_or(0))?;
1192 }
1193 }
1194 Ok(())
1195 }
1196
1197 pub fn to_vtk(&self) -> String {
1199 let mut buf = Vec::new();
1200 self.write_vtk(&mut buf).expect("writing to a Vec");
1201 String::from_utf8(buf).expect("the VTK file is ASCII")
1202 }
1203
1204 pub fn read_vtk(text: &str) -> Result<Self, VoxelTallyError> {
1207 let mut lines = text.lines();
1208 if lines.next().map(str::trim) != Some("# vtk DataFile Version 3.0") {
1209 return Err(parse_err(1, "not a legacy VTK 3.0 file"));
1210 }
1211 let title = lines.next().ok_or_else(|| parse_err(2, "no title line"))?;
1212 let mut fields = title.split_whitespace();
1213 let head = fields.by_ref().take(4).collect::<Vec<_>>();
1214 if head[..head.len().min(3)] != ["lindhard", "voxel", "tally"]
1215 || !matches!(head.get(3), Some(&"v1" | &"v2"))
1216 {
1217 return Err(parse_err(2, "not a lindhard voxel tally"));
1218 }
1219 let (mut origin, mut spacing, mut dims, mut histories) = (None, None, None, None);
1220 let mut species_z: Vec<u8> = Vec::new();
1221 for f in fields {
1222 match f.split_once('=') {
1223 Some(("origin", v)) => origin = Some(parse3::<f64>(v, 2)?),
1224 Some(("spacing", v)) => spacing = Some(parse3::<f64>(v, 2)?),
1225 Some(("dims", v)) => dims = Some(parse3::<usize>(v, 2)?),
1226 Some(("histories", v)) => histories = Some(num::<u64>(v, 2)?),
1227 Some(("species", v)) => species_z = parse_species(v, 2)?,
1228 _ => return Err(parse_err(2, format!("unexpected title field {f:?}"))),
1229 }
1230 }
1231 let missing = |what: &str| parse_err(2, format!("title is missing `{what}`"));
1232 let grid = VoxelTallyGrid::new(
1233 origin.ok_or_else(|| missing("origin"))?,
1234 spacing.ok_or_else(|| missing("spacing"))?,
1235 dims.ok_or_else(|| missing("dims"))?,
1236 )?;
1237 let histories = histories.ok_or_else(|| missing("histories"))?;
1238
1239 let mut tk = Tokens {
1241 iter: Box::new(
1242 text.lines()
1243 .enumerate()
1244 .skip(2)
1245 .flat_map(|(i, l)| l.split_whitespace().map(move |w| (i + 1, w))),
1246 ),
1247 last_line: text.lines().count().max(1),
1248 };
1249 tk.expect("ASCII")?;
1250 tk.expect("DATASET")?;
1251 tk.expect("UNSTRUCTURED_GRID")?;
1252 tk.expect("POINTS")?;
1253 let (n, t) = tk.next("a point count")?;
1254 let n_points: usize = num(t, n)?;
1255 if !n_points.is_multiple_of(8) {
1256 return Err(parse_err(n, "the point count must be a multiple of 8"));
1257 }
1258 tk.expect("double")?;
1259 let n_cells = n_points / 8;
1260 let mut corners = Vec::with_capacity(n_cells);
1261 for _ in 0..n_cells {
1262 let mut lo = [0.0f64; 3];
1263 for p in 0..8 {
1264 for coord in &mut lo {
1265 let (n, t) = tk.next("a coordinate")?;
1266 let v: f64 = num(t, n)?;
1267 if p == 0 {
1268 *coord = v;
1269 }
1270 }
1271 }
1272 corners.push(lo);
1273 }
1274 tk.expect("CELLS")?;
1275 for want in [n_cells, 9 * n_cells] {
1276 let (n, t) = tk.next("a cell count")?;
1277 if num::<usize>(t, n)? != want {
1278 return Err(parse_err(n, "inconsistent cell counts"));
1279 }
1280 }
1281 for _ in 0..9 * n_cells {
1282 tk.next("a connectivity entry")?;
1283 }
1284 tk.expect("CELL_TYPES")?;
1285 let (n, t) = tk.next("a cell count")?;
1286 if num::<usize>(t, n)? != n_cells {
1287 return Err(parse_err(n, "inconsistent cell counts"));
1288 }
1289 for _ in 0..n_cells {
1290 let (n, t) = tk.next("a cell type")?;
1291 if num::<u8>(t, n)? != VTK_VOXEL {
1292 return Err(parse_err(n, "only VTK_VOXEL (11) cells are supported"));
1293 }
1294 }
1295 tk.expect("CELL_DATA")?;
1296 let (n, t) = tk.next("a cell count")?;
1297 if num::<usize>(t, n)? != n_cells {
1298 return Err(parse_err(n, "inconsistent cell counts"));
1299 }
1300 let mut cells = vec![VoxelCell::default(); n_cells];
1301 for (name, kind) in VTK_SCALARS {
1302 tk.expect("SCALARS")?;
1303 tk.expect(name)?;
1304 tk.expect(kind)?;
1305 tk.expect("1")?;
1306 tk.expect("LOOKUP_TABLE")?;
1307 tk.expect("default")?;
1308 for cell in &mut cells {
1309 let (n, t) = tk.next("a scalar")?;
1310 match name {
1311 "implanted" => cell.implanted = num(t, n)?,
1312 "displacements" => cell.displacements = num(t, n)?,
1313 "electronic_ev" => cell.electronic_ev = num(t, n)?,
1314 "lattice_ev" => cell.lattice_ev = num(t, n)?,
1315 "rest_ev" => cell.rest_ev = num(t, n)?,
1316 _ => {} }
1318 }
1319 }
1320 let mut counts = vec![vec![0u64; n_cells]; 3 * species_z.len()];
1321 for (name, col) in defect_columns(&species_z).iter().zip(&mut counts) {
1322 tk.expect("SCALARS")?;
1323 tk.expect(name)?;
1324 tk.expect("unsigned_long")?;
1325 tk.expect("1")?;
1326 tk.expect("LOOKUP_TABLE")?;
1327 tk.expect("default")?;
1328 for slot in col.iter_mut() {
1329 let (n, t) = tk.next("a scalar")?;
1330 *slot = num(t, n)?;
1331 }
1332 }
1333 let voxels = corners
1334 .iter()
1335 .zip(cells)
1336 .enumerate()
1337 .map(|(vi, (lo, cell))| {
1338 let mut index = [0usize; 3];
1339 for a in 0..3 {
1340 let s = ((lo[a] - grid.origin_m[a]) / grid.spacing_m[a]).round();
1341 if !(s >= 0.0 && s < grid.dims[a] as f64) {
1342 return Err(parse_err(1, "a cell lies outside the grid"));
1343 }
1344 index[a] = s as usize;
1345 }
1346 let row: Vec<u64> = counts.iter().map(|c| c[vi]).collect();
1347 Ok(VoxelRecord {
1348 index,
1349 cell,
1350 defects: defects_from_values(&row),
1351 })
1352 })
1353 .collect::<Result<Vec<_>, _>>()?;
1354 Ok(Self {
1355 grid,
1356 histories,
1357 voxels,
1358 outside: VoxelCell::default(),
1359 outside_defects: vec![SpeciesDefects::default(); species_z.len()],
1360 species_z,
1361 })
1362 }
1363}