Skip to main content

parse_monitors/pressure/
mirrors.rs

1//! # M1 and M2 segments surface pressure
2//!
3//! Analyze segments surface wind pressure from pressure files either *M1p_M1p_\*.csv.bz2* or
4//! *M2p_M2p_\*.csv.bz2* for M1 or M2, respectively.
5
6use super::{Record, Result};
7use geotrans::{Segment, SegmentTrait, Transform, TransformMut, M1, M2};
8use serde::Deserialize;
9use std::{fs::File, io::Read, marker::PhantomData, path::PathBuf};
10
11fn norm(v: &[f64]) -> f64 {
12    v.iter().map(|&x| x * x).sum::<f64>().sqrt()
13}
14
15/*impl PartialOrd for Record {
16    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
17        self.area.partial_cmp(&other.area)
18    }
19}*/
20#[derive(Deserialize, Debug, PartialEq)]
21struct GeometryRecord {
22    #[serde(rename = "Area in TCS[i] (m^2)")]
23    area_i: f64,
24    #[serde(rename = "Area in TCS[j] (m^2)")]
25    area_j: f64,
26    #[serde(rename = "Area in TCS[k] (m^2)")]
27    area_k: f64,
28    #[serde(rename = "X (m)")]
29    x: f64,
30    #[serde(rename = "Y (m)")]
31    y: f64,
32    #[serde(rename = "Z (m)")]
33    z: f64,
34}
35impl GeometryRecord {
36    fn area_ijk(&self) -> [f64; 3] {
37        [self.area_i, self.area_j, self.area_k]
38    }
39}
40impl PartialOrd for GeometryRecord {
41    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
42        norm(&self.area_ijk()).partial_cmp(&norm(&other.area_ijk()))
43    }
44}
45/// Mirror segments surface pressure
46///
47/// Analyze segments surface wind pressure from pressure files either
48/// *M1p_M1p_\*.csv.bz2* or
49/// *M2p_M2p_\*.csv.bz2* for M1 or M2, respectively.
50#[derive(Default)]
51pub struct Pressure<M>
52where
53    M: Default + geotrans::Gmt,
54    Segment<M>: SegmentTrait,
55{
56    // the segment surface pressure [Pa]
57    pressure: Vec<f64>,
58    // the area magnitude the pressure is applied to
59    area: Vec<f64>,
60    // the area vector along the surface normal
61    area_ijk: Vec<[f64; 3]>,
62    // the (x,y,z) coordinate where the pressure is applied
63    xyz: Vec<[f64; 3]>,
64    // segment data filter
65    segment_filter: Vec<Vec<bool>>,
66    // segment data filter
67    segment_filter_size: Vec<f64>,
68    mirror: PhantomData<M>,
69}
70pub trait MirrorProperties {
71    fn exo_radius(&self) -> f64;
72    fn mirror(&self) -> String;
73    fn center_hole(&self) -> Option<f64>;
74}
75impl MirrorProperties for Pressure<M1> {
76    fn exo_radius(&self) -> f64 {
77        4.5
78    }
79    fn mirror(&self) -> String {
80        String::from("M1")
81    }
82    fn center_hole(&self) -> Option<f64> {
83        Some(2.75 * 0.5)
84    }
85}
86impl MirrorProperties for Pressure<M2> {
87    fn exo_radius(&self) -> f64 {
88        0.55
89    }
90    fn mirror(&self) -> String {
91        String::from("M2")
92    }
93    fn center_hole(&self) -> Option<f64> {
94        None
95    }
96}
97impl<M> Pressure<M>
98where
99    M: Default + geotrans::Gmt,
100    Segment<M>: SegmentTrait,
101    Pressure<M>: MirrorProperties,
102{
103    /// Loads the pressure data
104    pub fn load(csv_pressure: String) -> Result<Self> {
105        let this_pa = Self::load_pressure(csv_pressure)?;
106        /*let this_aijk = Self::load_geometry(csv_geometry)?;
107        let max_diff_area = this_pa
108            .area
109            .iter()
110            .zip(this_aijk.area_ijk.iter().map(|x| norm(x)))
111            .map(|(a0, a1)| (*a0 - a1).abs())
112            .fold(std::f64::NEG_INFINITY, f64::max);
113        assert!(
114            max_diff_area < 1e-14,
115            "Area magnitude do no match area vector: {}",
116            max_diff_area
117        );*/
118        let mut this = Self {
119            pressure: this_pa.pressure,
120            area: this_pa
121                .area_ijk
122                .iter()
123                .map(|a| a.iter().fold(0f64, |s, &a| s + a * a).sqrt())
124                .collect(),
125            area_ijk: this_pa.area_ijk,
126            xyz: this_pa.xyz,
127            segment_filter: Vec::new(),
128            segment_filter_size: Vec::new(),
129            mirror: PhantomData,
130        };
131        let mut n = 0;
132        let xr = this.exo_radius();
133        for sid in 1..=7 {
134            let sf = this
135                .to_local(sid)?
136                .xy_iter()
137                .map(|(x, y)| x.hypot(y) < xr)
138                .collect::<Vec<bool>>();
139            let n_sf = sf
140                .iter()
141                .filter_map(|sf| if *sf { Some(1usize) } else { None })
142                .sum::<usize>();
143            this.segment_filter.push(sf);
144            this.segment_filter_size.push(n_sf as f64);
145            this.from_local(sid);
146            n += n_sf;
147        }
148        assert!(
149            n == this.pressure.len(),
150            "Data length ({}) and segment filter length ({}) do not match",
151            this.pressure.len(),
152            n
153        );
154        Ok(this)
155    }
156    #[cfg(feature = "bzip2")]
157    pub fn decompress(path: PathBuf) -> Result<String> {
158        let csv_file = File::open(path)?;
159        let buf = std::io::BufReader::new(csv_file);
160        let mut bz2 = bzip2::bufread::BzDecoder::new(buf);
161        let mut contents = String::new();
162        bz2.read_to_string(&mut contents)?;
163        Ok(contents)
164    }
165    #[cfg(not(feature = "bzip2"))]
166    pub fn decompress(path: PathBuf) -> Result<String> {
167        let csv_file = File::open(path)?;
168        let mut gz = flate2::read::GzDecoder::new(csv_file);
169        let mut contents = String::new();
170        gz.read_to_string(&mut contents)?;
171        Ok(contents)
172    }
173    /// Loads the pressure from a csv bz2-compressed file
174    pub fn load_pressure(contents: String) -> Result<Self> {
175        let mut this = Pressure::default();
176        let mut rdr = csv::Reader::from_reader(contents.as_bytes());
177        let mut rows = Vec::<Record>::new();
178        for result in rdr.deserialize() {
179            rows.push(result?);
180        }
181        //rows.sort_by(|a, b| a.partial_cmp(b).unwrap());
182        rows.into_iter().for_each(|row| {
183            //this.area.push(row.area);
184            this.pressure.push(row.pressure);
185            this.area_ijk.push([row.area_i, row.area_j, row.area_k]);
186            this.xyz.push([row.x, row.y, row.z]);
187        });
188        Ok(this)
189    }
190    /// Loads the areas and coordinates vector from a csv file
191    pub fn load_geometry(contents: String) -> Result<Self> {
192        let mut this = Pressure::default();
193        let mut rdr = csv::Reader::from_reader(contents.as_bytes());
194        let mut rows = Vec::<GeometryRecord>::new();
195        for result in rdr.deserialize() {
196            rows.push(result?);
197        }
198        rows.sort_by(|a, b| a.partial_cmp(b).unwrap());
199        rows.into_iter().for_each(|row| {
200            this.area_ijk.push([row.area_i, row.area_j, row.area_k]);
201            this.xyz.push([row.x, row.y, row.z]);
202        });
203        Ok(this)
204    }
205    pub fn len(&self) -> usize {
206        self.pressure.len()
207    }
208
209    pub fn is_empty(&self) -> bool {
210        self.len() == 0
211    }
212    /// Iterator over the x coordinate
213    fn xyz_iter(&self, axis: usize) -> impl Iterator<Item = f64> + '_ {
214        self.xyz.iter().map(move |v| v[axis])
215    }
216    pub fn x_iter(&self) -> impl Iterator<Item = f64> + '_ {
217        self.xyz_iter(0)
218    }
219    /// Returns the range of the x cooordinate
220    pub fn x_range(&self) -> (f64, f64) {
221        (
222            self.x_iter().fold(std::f64::INFINITY, f64::min),
223            self.x_iter().fold(std::f64::NEG_INFINITY, f64::max),
224        )
225    }
226    /// Iterator over the y coordinate
227    pub fn y_iter(&self) -> impl Iterator<Item = f64> + '_ {
228        self.xyz_iter(1)
229    }
230    /// Iterator over the (x,y) coordinates
231    pub fn xy_iter(&self) -> impl Iterator<Item = (f64, f64)> + '_ {
232        self.x_iter().zip(self.y_iter())
233    }
234    /// Iterator over the (x,y) coordinates of a given segment
235    pub fn segment_xy(&self, sid: usize) -> impl Iterator<Item = (f64, f64)> + '_ {
236        self.x_iter()
237            .zip(self.y_iter())
238            .zip(self.segment_filter.get(sid - 1).unwrap().iter())
239            .filter(|(_, &f)| f)
240            .map(|(xy, _)| xy)
241    }
242    /// Iterator over the pressure and areas of a given segment
243    pub fn segment_pa(&self, sid: usize) -> impl Iterator<Item = (f64, f64)> + '_ {
244        self.pa_iter()
245            .zip(self.segment_filter.get(sid - 1).unwrap().iter())
246            .filter_map(|((p, a), &f)| f.then(|| (*p, *a)))
247    }
248    /// Returns the range of the y cooordinate
249    pub fn y_range(&self) -> (f64, f64) {
250        (
251            self.y_iter().fold(std::f64::INFINITY, f64::min),
252            self.y_iter().fold(std::f64::NEG_INFINITY, f64::max),
253        )
254    }
255    /// Iterator over the z coordinate
256    pub fn z_iter(&self) -> impl Iterator<Item = f64> + '_ {
257        self.xyz_iter(2)
258    }
259    /// Returns the range of the z cooordinate
260    pub fn z_range(&self) -> (f64, f64) {
261        (
262            self.z_iter().fold(std::f64::INFINITY, f64::min),
263            self.z_iter().fold(std::f64::NEG_INFINITY, f64::max),
264        )
265    }
266    /// Transforms the coordinates into the segment local coordinate system
267    pub fn to_local(&mut self, sid: usize) -> Result<&mut Self> {
268        self.xyz
269            .iter_mut()
270            .map(|v| Segment::<M>::new(sid as i32).map(|segment| v.fro(segment)))
271            .collect::<std::result::Result<Vec<()>, geotrans::Error>>()?;
272        Ok(self)
273    }
274    /// Transforms the coordinates into the OSS
275    pub fn from_local(&mut self, sid: usize) -> &mut Self {
276        self.xyz.iter_mut().for_each(|v| {
277            Segment::<M>::new(sid as i32)
278                .map(|segment| v.to(segment))
279                .unwrap();
280        });
281        self
282    }
283    /// Filter that is apply locally to segment `sid` selecting samplea within the radii `r_in`<0> and `r_out`<`exo_radius`>
284    pub fn local_radial_filter(
285        &self,
286        sid: usize,
287        r_in: Option<f64>,
288        r_out: Option<f64>,
289    ) -> impl Iterator<Item = bool> + '_ {
290        let xr = self.exo_radius();
291        self.xyz
292            .iter()
293            .map(move |v| {
294                Segment::<M>::new(sid as i32)
295                    .map(|segment| v.fro(segment))
296                    .unwrap()
297            })
298            .map(|v| v[0].hypot(v[1]))
299            .map(move |r| r >= r_in.unwrap_or_default() && r < r_out.unwrap_or(xr))
300    }
301    /// Returns the sum of the z forces of all the segments
302    pub fn total_force(&self) -> f64 {
303        self.pressure
304            .iter()
305            .zip(self.area.iter())
306            .map(|(p, a)| -p * a)
307            .sum()
308    }
309    /// Iterator over the pressures and areas
310    pub fn pa_iter(&self) -> impl Iterator<Item = (&f64, &f64)> {
311        self.pressure.iter().zip(self.area.iter())
312    }
313    /// Iterator over the pressures and area vectors
314    pub fn paijk_iter(&self) -> impl Iterator<Item = (&f64, &[f64; 3])> {
315        self.pressure.iter().zip(self.area_ijk.iter())
316    }
317    /// Iterator over the pressures, area vectors and coordinates
318    pub fn p_aijk_xyz(&self) -> impl Iterator<Item = (&f64, &[f64; 3], &[f64; 3])> {
319        self.pressure
320            .iter()
321            .zip(self.area_ijk.iter())
322            .zip(self.xyz.iter())
323            .map(|((a, b), c)| (a, b, c))
324    }
325    /// Iterator over the pressures, area vectors and coordinates for the given segment
326    pub fn segment_p_aijk_xyz(
327        &self,
328        sid: usize,
329    ) -> impl Iterator<Item = (&f64, &[f64; 3], &[f64; 3])> {
330        self.pressure
331            .iter()
332            .zip(self.area_ijk.iter())
333            .zip(self.xyz.iter())
334            .zip(self.segment_filter.get(sid - 1).unwrap().iter())
335            .filter_map(|(((a, b), c), f)| f.then(|| (a, b, c)))
336    }
337    /// Return the area of a given segment
338    pub fn segment_area(&self, sid: usize) -> f64 {
339        self.area
340            .iter()
341            .zip(self.segment_filter.get(sid - 1).unwrap())
342            .filter(|(_, &f)| f)
343            .fold(0f64, |aa, (a, _)| aa + *a)
344    }
345    /// Return the area of each segment
346    pub fn segments_area(&self) -> Vec<f64> {
347        (1..=7).map(|sid| self.segment_area(sid)).collect()
348    }
349    /// Return the mirror area
350    pub fn mirror_area(&self) -> f64 {
351        self.area.iter().fold(0f64, |aa, a| aa + *a)
352    }
353    /// Returns the vectors of forces of a given segment
354    pub fn forces(&mut self, sid: usize) -> Result<Vec<[f64; 3]>> {
355        let xy: Vec<_> = self.to_local(sid)?.xy_iter().map(|(x, y)| (x, y)).collect();
356        let xr = self.exo_radius();
357        Ok(self
358            .from_local(sid)
359            .paijk_iter()
360            .zip(xy)
361            .filter(|(_, (x, y))| x.hypot(*y) < xr)
362            .map(|((p, a), _)| [p * a[0], p * a[1], p * a[2]])
363            .collect())
364    }
365    /// Returns the average pressure over a given segment
366    pub fn average_pressure(&self, sid: usize) -> f64 {
367        let (pa, aa) = self
368            .pa_iter()
369            .zip(self.segment_filter.get(sid - 1).unwrap().iter())
370            .filter(|(_, &f)| f)
371            .fold((0f64, 0f64), |(pa, aa), ((p, a), _)| (pa + p * a, aa + a));
372        pa / aa
373    }
374    /// Returns the average ASM different pressure over a given segment
375    pub fn asm_differential_pressure(&self, sid: usize) -> (f64, Vec<f64>) {
376        let p_mean = self.average_pressure(sid);
377        (
378            p_mean,
379            self.pressure
380                .iter()
381                .zip(self.segment_filter.get(sid - 1).unwrap().iter())
382                .filter_map(|(&p, &f)| f.then(|| (p - 1.1 * p_mean) / 3.))
383                .collect::<Vec<f64>>(),
384        )
385    }
386    /// Returns the pressure variance over a given segment
387    pub fn pressure_var(&mut self, sid: usize) -> f64 {
388        let p_mean = self.average_pressure(sid);
389        let (pa, aa) = self
390            .pa_iter()
391            .zip(self.segment_filter.get(sid - 1).unwrap().iter())
392            .filter(|(_, &f)| f)
393            .fold((0f64, 0f64), |(pa, aa), ((p, a), _)| {
394                let p_net = p - p_mean;
395                (pa + p_net * p_net * a, aa + a)
396            });
397        pa / aa
398    }
399    /// Returns the pressure standard deviation over a given segment
400    pub fn pressure_std(&mut self, sid: usize) -> f64 {
401        self.pressure_var(sid).sqrt()
402    }
403    /// Returns the average pressure over each segment
404    pub fn segments_average_pressure(&mut self) -> Vec<f64> {
405        (1..=7).map(|sid| self.average_pressure(sid)).collect()
406    }
407    /// Returns the pressure variance over each segment
408    pub fn segments_pressure_var(&mut self) -> Vec<f64> {
409        (1..=7).map(|sid| self.pressure_var(sid)).collect()
410    }
411    /// Returns the pressure standard deviation over each segment
412    pub fn segments_pressure_std(&mut self) -> Vec<f64> {
413        (1..=7).map(|sid| self.pressure_std(sid)).collect()
414    }
415    /// Returns the average mean pressure over all segment
416    pub fn mirror_average_pressure(&mut self) -> f64 {
417        let (pa, aa) = self
418            .pa_iter()
419            .fold((0f64, 0f64), |(pa, aa), (p, a)| (pa + p * a, aa + a));
420        pa / aa
421    }
422    /// Returns the average pressure variance over all segment
423    pub fn mirror_average_pressure_var(&mut self) -> f64 {
424        let p_mean = self.mirror_average_pressure();
425        let (pa, aa) = self.pa_iter().fold((0f64, 0f64), |(pa, aa), (p, a)| {
426            let p_net = p - p_mean;
427            (pa + p_net * p_net * a, aa + a)
428        });
429        pa / aa
430    }
431    /// Returns the average pressure standart deviation over all segment
432    pub fn mirror_average_pressure_std(&mut self) -> f64 {
433        self.mirror_average_pressure_var().sqrt()
434    }
435    /// Returns the average mean pressure over all segment within the given `radius`
436    pub fn mirror_average_pressure_within(&self, radius: f64) -> f64 {
437        let (pa, aa) = self
438            .pa_iter()
439            .zip(self.xy_iter())
440            .filter(|(_, (x, y))| x.hypot(*y) < radius)
441            .fold((0f64, 0f64), |(pa, aa), ((p, a), _)| (pa + p * a, aa + a));
442        pa / aa
443    }
444    /// Returns the average mean pressure over all segment with data filtered with `select`
445    pub fn mirror_average_pressure_by(&self, select: impl Iterator<Item = bool>) -> f64 {
446        let (pa, aa) = self
447            .pa_iter()
448            .zip(select)
449            .filter(|(_, m)| *m)
450            .fold((0f64, 0f64), |(pa, aa), ((p, a), _)| (pa + p * a, aa + a));
451        pa / aa
452    }
453    /// Returns the average pressure variance over all segment within the given `radius`
454    pub fn mirror_average_pressure_var_within(&self, radius: f64) -> f64 {
455        let p_mean = self.mirror_average_pressure_within(radius);
456        let (pa, aa) = self
457            .pa_iter()
458            .zip(self.xy_iter())
459            .filter(|(_, (x, y))| x.hypot(*y) < radius)
460            .fold((0f64, 0f64), |(pa, aa), ((p, a), _)| {
461                let p_net = p - p_mean;
462                (pa + p_net * p_net * a, aa + a)
463            });
464        pa / aa
465    }
466    /// Returns the average pressure variance over all segment with data filtered with `select`
467    pub fn mirror_average_pressure_var_by(
468        &self,
469        p_mean: f64,
470        select: impl Iterator<Item = bool>,
471    ) -> f64 {
472        //        let b: Vec<_> = select.collect();
473        let (pa, aa) = self.pa_iter().zip(select).filter(|(_, m)| *m).fold(
474            (0f64, 0f64),
475            |(pa, aa), ((p, a), _)| {
476                let p_net = p - p_mean;
477                (pa + p_net * p_net * a, aa + a)
478            },
479        );
480        pa / aa
481    }
482    /// Returns the center of pressure of a given segment
483    pub fn center_of_pressure(&mut self, sid: usize) -> Result<[f64; 3]> {
484        let xy: Vec<_> = self.to_local(sid)?.xy_iter().map(|(x, y)| (x, y)).collect();
485        let xr = self.exo_radius();
486        let (mut cs, s) = self
487            .from_local(sid)
488            .p_aijk_xyz()
489            .zip(xy)
490            .filter(|(_, (x, y))| x.hypot(*y) < xr)
491            .fold(([0f64; 3], [0f64; 3]), |(mut cs, mut s), ((p, a, c), _)| {
492                for k in 0..3 {
493                    let df = p * a[k];
494                    cs[k] += df * c[k];
495                    s[k] += df;
496                }
497                (cs, s)
498            });
499        cs.iter_mut().zip(s).for_each(|(cs, s)| *cs /= s);
500        Ok(cs)
501    }
502    /// Returns the sum of the forces of a given segment
503    pub fn segment_force(&mut self, sid: usize) -> Result<[f64; 3]> {
504        Ok(self.forces(sid)?.into_iter().fold([0f64; 3], |mut s, a| {
505            s.iter_mut().zip(a).for_each(|(s, a)| *s += a);
506            s
507        }))
508    }
509    /// Returns the center of pressure and the force and moment applied at this location for a given segment
510    pub fn segment_pressure_integral(
511        &mut self,
512        sid: usize,
513    ) -> Result<([f64; 3], ([f64; 3], [f64; 3]))> {
514        let xy: Vec<_> = self.to_local(sid)?.xy_iter().map(|(x, y)| (x, y)).collect();
515        let xr = self.exo_radius();
516        let (mut cop, force) = self
517            .from_local(sid)
518            .p_aijk_xyz()
519            .zip(xy)
520            .filter(|(_, (x, y))| x.hypot(*y) < xr)
521            .fold(([0f64; 3], [0f64; 3]), |(mut cs, mut s), ((p, a, c), _)| {
522                for k in 0..3 {
523                    let df = p * a[k];
524                    cs[k] += df * c[k];
525                    s[k] += df;
526                }
527                (cs, s)
528            });
529        cop.iter_mut().zip(force).for_each(|(cs, s)| *cs /= s);
530        let moment = [
531            cop[1] * force[2] - cop[2] * force[1],
532            cop[2] * force[0] - cop[0] * force[2],
533            cop[0] * force[1] - cop[1] * force[0],
534        ];
535        Ok((cop, (force, moment)))
536    }
537    /// Returns the sum of the forces of all the segments
538    pub fn segments_force(&mut self) -> Result<[f64; 3]> {
539        Ok((1..=7)
540            .map(|sid| self.segment_force(sid))
541            .collect::<Result<Vec<[f64; 3]>>>()?
542            .into_iter()
543            .fold([0f64; 3], |mut s, a| {
544                s.iter_mut().zip(a).for_each(|(s, a)| *s += a);
545                s
546            }))
547    }
548    /// Returns the sum of the vectors in [`Iterator`]
549    pub fn sum_vectors<'a>(&'a mut self, vec: impl Iterator<Item = &'a [f64; 3]>) -> [f64; 3] {
550        vec.fold([0f64; 3], |mut s, a| {
551            s.iter_mut().zip(a).for_each(|(s, a)| *s += a);
552            s
553        })
554    }
555    #[cfg(feature = "plot")]
556    /// Display the pressure map
557    pub fn pressure_map(&mut self, cfd_case_path: PathBuf) {
558        let mut triangles = vec![];
559        let mut tri_pressure = vec![];
560        /*let average_pressure = self.mirror_average_pressure();
561        let min_pressure = self.pressure.iter().cloned().fold(f64::INFINITY, f64::min);
562        let max_pressure = self
563            .pressure
564            .iter()
565            .cloned()
566            .fold(f64::NEG_INFINITY, f64::max);*/
567        for sid in 1..=7 {
568            let mut del = triangle_rs::Delaunay::builder()
569                .add_nodes(
570                    &self
571                        .segment_xy(sid)
572                        .flat_map(|(x, y)| vec![x, y])
573                        .collect::<Vec<f64>>(),
574                )
575                .set_switches("Q")
576                .build();
577            match (sid, self.center_hole()) {
578                (7, Some(radius)) => {
579                    del.filter_within_circle(radius, None);
580                }
581                _ => (),
582            }
583            let pa: Vec<_> = self
584                .segment_pa(sid)
585                //                .map(|(p, a)| (if p < 0f64 { f64::NAN } else { p }, a))
586                .collect();
587            tri_pressure.extend(del.triangle_iter().map(|t| {
588                t.iter().fold(0f64, |s, i| s + pa[*i].0 * pa[*i].1)
589                    / t.iter().fold(0f64, |s, i| s + pa[*i].1)
590            }));
591            triangles.extend(del.triangle_vertex_iter());
592        }
593        let path = cfd_case_path
594            .join("report")
595            .join(format!("{}_pressure_map.png", self.mirror().to_lowercase()));
596        let filename = format!("{}", path.as_path().display());
597        let _: complot::tri::Heatmap = (
598            triangles.into_iter().zip(tri_pressure.into_iter()),
599            complot::complot!(filename),
600        )
601            .into();
602    }
603}