Skip to main content

dose2gmsh/
lib.rs

1//! Convert `3ddose` files to Gmsh `msh` (version 2) files.
2//!
3//! Get started with `cargo install dose2gmsh`.
4
5use std::fmt::Debug;
6use std::fs::File;
7use std::io::{BufRead, BufReader, BufWriter, Write};
8use std::str::FromStr;
9
10use structopt::StructOpt;
11
12/// Command line input parameters.
13#[derive(StructOpt, Debug)]
14#[structopt(name = "dose2gmsh", author = "Max Orok <maxwellorok@gmail.com>", about = "Convert dosxyznrc 3ddose files to Gmsh msh files")]
15pub struct Cli {
16    /// The input 3ddose file
17    #[structopt(parse(from_os_str))]
18    pub input_file: std::path::PathBuf,
19    /// The output file name, defaults to <input_file>
20    #[structopt(parse(from_os_str), short, long)]
21    pub output_file: Option<std::path::PathBuf>,
22    /// The output format (msh2 or csv)
23    #[structopt(short, long, default_value = "msh2")]
24    pub format: Fmt
25}
26
27/// Converter output format
28#[derive(Debug, Copy, Clone)]
29pub enum Fmt {
30    Csv,
31    Msh2,
32}
33
34impl std::str::FromStr for Fmt {
35    type Err = String;
36    fn from_str(fmt: &str) -> Result<Self, Self::Err> {
37        match fmt {
38            "csv" => Ok(Fmt::Csv),
39            "msh2" => Ok(Fmt::Msh2),
40            _ => Err("Could not parse the format".to_string()),
41        }
42    }
43}
44
45/// Dose and uncertainty data for a 3D rectilinear hexahedral mesh.
46///
47/// ## Units
48/// * Coordinate values are centimetres following `EGSnrc` convention.
49/// * Dose values are gray-centimetres squared (dose area product).
50/// * Uncertainty values are fractions of their corresponding dose value.
51/// * Number of voxels can vary in each direction.
52/// * Number of nodes in each direction is `num_voxels + 1`.
53///
54/// ```no_run
55/// //             ------------
56/// //             |\         |\
57/// //  y          |  \       |  \
58/// //  ^          |   ------------
59/// //  |          |   |      |   |
60/// //  +---> x    ----+-------   |
61/// //   \          \  |       \  |
62/// //    z           \|         \|
63/// //                 ------------
64///
65/// // this example uses a 40 x 40 x 40 dose block
66/// # use dose2gmsh::DoseBlock;
67/// # use std::path::PathBuf;
68/// # fn main() -> Result<(), std::io::Error> {
69/// # let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
70/// # path.push("props");
71/// # path.push("water_block.3ddose");
72/// // parse a 3ddose file, exiting on any parse errors
73/// let data = DoseBlock::from_3d_dose(&path)?;
74///
75/// // expecting 40 voxels in each direction
76/// assert!(data.num_x() == 40 &&
77///         data.num_y() == 40 &&
78///         data.num_z() == 40);
79///
80/// // expecting 64000 (40³) dose values and a matching uncertainty vector
81/// assert!(data.doses.len() == 64000);
82/// assert!(data.doses.len() == data.num_voxels());
83/// assert!(data.doses.len() == data.uncerts.len());
84///
85/// data.write_msh2("output.msh")?;
86/// # Ok(())
87/// # }
88/// ```
89#[derive(Debug, Clone)]
90pub struct DoseBlock {
91    /// Node coordinates along *x* in `[cm]`.
92    pub xs: Vec<f64>,
93    /// Node coordinates along *y* in `[cm]`.
94    pub ys: Vec<f64>,
95    /// Node coordinates along *z* in `[cm]`.
96    pub zs: Vec<f64>,
97    /// Unnormalized voxel dose per fluence data in `[Gy · cm2]`.
98    pub doses: Vec<f64>,
99    /// Fractional dose uncertainties.
100    pub uncerts: Vec<f64>,
101}
102
103impl DoseBlock {
104    /// Create a new `DoseBlock` by parsing a `3ddose` data file.
105    pub fn from_3d_dose<P: AsRef<std::path::Path>>(input_file: P) -> Result<DoseBlock, std::io::Error> {
106        let dose_input = BufReader::new(File::open(input_file)?);
107
108        let mut lines = dose_input.lines().map(|l| l.unwrap());
109        // first line is number of x, y, z voxels
110        let (num_x, num_y, num_z) = {
111            let voxel_nums = lines.next().expect("voxel numbers");
112            let voxel_nums = parse_simple_line::<usize>(voxel_nums, "voxel number", 3);
113            (voxel_nums[0], voxel_nums[1], voxel_nums[2])
114        };
115
116        // second line is x-coordinates
117        let xs = parse_simple_line::<f64>(
118            lines.next().expect("x-coordinates"),
119            "x-coordinate",
120            num_x + 1,
121        );
122
123        // third is y-coordinates
124        let ys = parse_simple_line::<f64>(
125            lines.next().expect("y-coordinates"),
126            "y-coordinate",
127            num_y + 1,
128        );
129
130        // fourth is z-coordinates
131        let zs = parse_simple_line::<f64>(
132            lines.next().expect("z-coordinates"),
133            "z-coordinate",
134            num_z + 1,
135        );
136
137        let num_voxels = num_x * num_y * num_z;
138
139        // fifth is deposited dose
140        let doses = parse_simple_line::<f64>(lines.next().expect("doses"), "dose value", num_voxels);
141
142        // sixth is uncertainty values
143        let uncerts = parse_simple_line::<f64>(
144            lines.next().expect("uncerts"),
145            "uncertainty value",
146            num_voxels,
147        );
148
149        Ok(DoseBlock {
150            xs,
151            ys,
152            zs,
153            doses,
154            uncerts,
155        })
156
157    }
158
159    /// Number of voxels in the *x*-direction.
160    pub fn num_x(&self) -> usize {
161        self.xs.len() - 1
162    }
163
164    /// Number of voxels in the *y*-direction.
165    pub fn num_y(&self) -> usize {
166        self.ys.len() - 1
167    }
168
169    /// Number of voxels in the *z*-direction.
170    pub fn num_z(&self) -> usize {
171        self.zs.len() - 1
172    }
173
174    /// Total number of mesh voxels.
175    pub fn num_voxels(&self) -> usize {
176        self.num_x() * self.num_y() * self.num_z()
177    }
178
179    /// Total number of mesh nodes.
180    pub fn num_nodes(&self) -> usize {
181        self.xs.len() * self.ys.len() * self.zs.len()
182    }
183
184    /// `[i, j, k]` node list indexing.
185    pub fn grid_index(&self, i: usize, j: usize, k: usize) -> usize {
186        i + self.xs.len() * j + self.xs.len() * self.ys.len() * k
187    }
188
189    /// Convert the `3ddose` data to a Gmsh `.msh` file (version 2.2).
190    pub fn write_msh2<P: AsRef<std::path::Path>>(&self, output: P) -> Result<(), std::io::Error> {
191        use itertools::Itertools;
192
193        let mut filestream = BufWriter::new(File::create(output)?);
194
195        // gmsh header
196        writeln!(&mut filestream, "$MeshFormat\n2.2 0 8\n$EndMeshFormat")?;
197        // nodes
198        write!(&mut filestream, "$Nodes\n{}\n", self.num_nodes())?;
199        for (k, z) in self.zs.iter().enumerate() {
200            for (j, y) in self.ys.iter().enumerate() {
201                for (i, x) in self.xs.iter().enumerate() {
202                    // gmsh expects 1-indexing
203                    writeln!(
204                        &mut filestream,
205                        "{} {} {} {}",
206                        self.grid_index(i, j, k) + 1,
207                        x,
208                        y,
209                        z
210                    )?;
211                }
212            }
213        }
214        writeln!(&mut filestream, "$EndNodes")?;
215
216        // todo find exact len
217        let mut x_nodes = 1..=self.num_nodes();
218        let gmsh_y_index = |x_index| x_index + self.xs.len();
219        let gmsh_z_index = |x_index| x_index + self.xs.len() * self.ys.len();
220
221        writeln!(&mut filestream, "$Elements\n{}", self.num_voxels())?;
222        for index in 0..self.num_voxels() {
223            // we order nodes following the gmsh numbering
224            // source: http://gmsh.info/doc/texinfo/gmsh.html#Low-order-elements
225            //               v
226            //        3----------2
227            //        |\     ^   |\
228            //        | \    |   | \
229            //        |  \   |   |  \
230            //        |   7------+---6
231            //        |   |  +-- |-- | -> u
232            //        0---+---\--1   |
233            //         \  |    \  \  |
234            //          \ |     \  \ |
235            //           \|      w  \|
236            //            4----------5
237            //
238
239            // skip rightmost node to start a new row
240            if index != 0 && index % self.num_x() == 0 {
241                x_nodes = x_nodes.dropping(1);
242            }
243
244            // skip top row of nodes to move to the next x-y block
245            if index != 0 && index % (self.num_x() * self.num_y()) == 0 {
246                x_nodes = x_nodes.dropping(self.xs.len());
247            }
248
249            let xl = x_nodes.next().unwrap(); // 0 node
250            let xr = xl + 1; // 1
251
252            let yl = gmsh_y_index(xl); // 3
253            let yr = yl + 1; // 2
254
255            let zl = gmsh_z_index(xl); // 4
256            let zr = zl + 1; // 5
257
258            let yzl = gmsh_z_index(yl); // 7
259            let yzr = yzl + 1; // 6
260
261            writeln!(
262                &mut filestream,
263                // 5 is the gmsh magic number for a hexahedron
264                // 2 0 0 doesn't matter for us -- see element type section of
265                // gmsh doc for more: http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format
266                "{} 5 2 0 0 {} {} {} {} {} {} {} {}",
267                index + 1,
268                xl,  // 0
269                xr,  // 1
270                yr,  // 2
271                yl,  // 3
272                zl,  // 4
273                zr,  // 5
274                yzr, // 6
275                yzl, // 7
276            )?;
277        }
278        writeln!(&mut filestream, "$EndElements")?;
279
280        let mut write_elt_data = |name: &str, data: &Vec<f64>| -> Result<(), std::io::Error> {
281            writeln!(&mut filestream, "$ElementData")?;
282            // one string - the field name
283            writeln!(&mut filestream, "1\n{}", name)?;
284            // one real value - the time
285            writeln!(&mut filestream, "1\n0.0")?;
286            // three int tags
287            //   timestep 0
288            //   1-component (scalar) field
289            //   num_elt values
290            writeln!(&mut filestream, "3\n0\n1\n{}", data.len())?;
291            for (index, val) in data.iter().enumerate() {
292                writeln!(&mut filestream, "{} {}", index + 1, val)?;
293            }
294            writeln!(&mut filestream, "$EndElementData")?;
295            Ok(())
296        };
297
298        write_elt_data(r#""Dose [Gy·cm2]""#, &self.doses)?;
299        write_elt_data(r#""Uncertainty fraction""#, &self.uncerts)
300    }
301
302    /// Convert the `3ddose` data to `csv`.
303    pub fn write_csv<P: AsRef<std::path::Path>>(&self, output: P) -> Result<(), std::io::Error> {
304        let calc_centroids = |pts: &Vec<f64>| -> Vec<f64> {
305            let num_centroids = pts.len() - 1;
306            let mut cs = Vec::with_capacity(num_centroids);
307            for i in 0..num_centroids {
308                cs.push((pts[i] + pts[i+1]) / 2.0);
309            }
310            cs
311        };
312
313        let voxel_idx = |i: usize, j: usize, k: usize| -> usize {
314            i + self.num_x() * j + self.num_x() * self.num_y() * k
315        };
316
317        let mut file = BufWriter::new(File::create(output)?);
318        writeln!(&mut file, "xc [cm],yc [cm],zc [cm],Dose [Gy cm2],Uncertainty fraction")?;
319        for (k, z) in calc_centroids(&self.zs).into_iter().enumerate() {
320            for (j, y) in calc_centroids(&self.ys).into_iter().enumerate() {
321                for (i, x) in calc_centroids(&self.xs).into_iter().enumerate() {
322                    writeln!(&mut file, "{},{},{},{},{}", x, y, z,
323                             self.doses[voxel_idx(i, j, k)],
324                             self.uncerts[voxel_idx(i, j, k)])?;
325                }
326            }
327        }
328        Ok(())
329    }
330}
331
332fn parse_simple_line<T>(line: String, title: &'static str, expect_len: usize) -> Vec<T>
333where
334    T: FromStr,
335    <T as std::str::FromStr>::Err: Debug,
336{
337    let entries: Vec<T> = line
338        .trim()
339        .split_whitespace()
340        .map(|num| num.parse::<T>().expect(title))
341        .collect();
342    assert!(entries.len() == expect_len);
343    entries
344}
345
346#[cfg(test)]
347mod tests {
348
349    use super::*;
350
351    #[test]
352    fn read_3ddose() {
353        let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
354        path.push("props");
355        path.push("water_block.3ddose");
356
357        let data = DoseBlock::from_3d_dose(&path).expect("couldn't parse 3ddose file");
358
359        assert_eq!(data.num_voxels(), 40 * 40 * 40);
360        assert_eq!(data.num_nodes(), 41 * 41 * 41);
361        // x-nodes come first, then y, then z
362        assert_eq!(data.grid_index(0, 0, 0), 0);
363        assert_eq!(data.grid_index(0, 1, 0), 41);
364        assert_eq!(data.grid_index(0, 0, 1), 1681);
365        assert_eq!(data.grid_index(0, 0, 1), 1681);
366        // a random uncertainty to check
367        assert_eq!(data.uncerts[21503], 0.37652693977336593);
368    }
369
370    #[test]
371    fn write_csv() {
372        let data = DoseBlock {
373            xs: vec![0.0, 2.0],
374            ys: vec![0.0, 2.0, 4.0],
375            zs: vec![0.0, 2.0, 4.0, 8.0],
376            doses: vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
377            uncerts: vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6],
378        };
379
380        let file = "tmp.csv";
381        data.write_csv(file).unwrap();
382        let mut rdr = csv::Reader::from_path(file).unwrap();
383        assert_eq!(
384            rdr.headers().unwrap(),
385            vec!["xc [cm]", "yc [cm]", "zc [cm]", "Dose [Gy cm2]", "Uncertainty fraction"]
386        );
387
388        let mut records = rdr.records();
389        let record = records.next().unwrap().unwrap();
390        approx::relative_eq!(record[0].parse::<f64>().unwrap(), 1.0);
391        approx::relative_eq!(record[1].parse::<f64>().unwrap(), 1.0);
392        approx::relative_eq!(record[2].parse::<f64>().unwrap(), 1.0);
393        approx::relative_eq!(record[3].parse::<f64>().unwrap(), 10.0);
394        approx::relative_eq!(record[4].parse::<f64>().unwrap(), 0.1);
395
396        let record = records.next().unwrap().unwrap();
397        approx::relative_eq!(record[0].parse::<f64>().unwrap(), 1.0);
398        approx::relative_eq!(record[1].parse::<f64>().unwrap(), 3.0);
399        approx::relative_eq!(record[2].parse::<f64>().unwrap(), 1.0);
400        approx::relative_eq!(record[3].parse::<f64>().unwrap(), 20.0);
401        approx::relative_eq!(record[4].parse::<f64>().unwrap(), 0.2);
402
403        let record = records.next().unwrap().unwrap();
404        approx::relative_eq!(record[0].parse::<f64>().unwrap(), 1.0);
405        approx::relative_eq!(record[1].parse::<f64>().unwrap(), 1.0);
406        approx::relative_eq!(record[2].parse::<f64>().unwrap(), 3.0);
407        approx::relative_eq!(record[3].parse::<f64>().unwrap(), 30.0);
408        approx::relative_eq!(record[4].parse::<f64>().unwrap(), 0.3);
409
410        std::fs::remove_file(file).unwrap();
411    }
412}