stupidf/
test_information.rs

1use crate::records::RecordSummary;
2use crate::records::Records;
3use crate::records::records::PTR;
4use crate::records::records::Record;
5use crate::records::records::TSR;
6use polars::frame::DataFrame;
7use polars::prelude::Column;
8use pyo3::Bound;
9use pyo3::IntoPyObject;
10use pyo3::Python;
11use pyo3::types::PyString;
12use serde::Serialize;
13use std::collections::HashMap;
14use std::convert::Infallible;
15use std::fmt;
16
17/// `TestInformation` for a single test
18///
19/// The metadata for a test is uniquely determined by (`test_num`, `head_num`, `site_num`). This
20/// structure does not contain information about any executions of the test, it only contains
21/// metadata associated with the test.
22#[derive(Debug, IntoPyObject)]
23pub struct TestInformation {
24    pub test_num: u32,
25    pub head_num: u8,
26    pub site_num: u8,
27    pub test_type: TestType,
28    pub execution_count: u32,
29    pub test_name: String,
30    pub sequence_name: String,
31    pub test_label: String,
32    pub test_time: f32,
33    pub test_text: String,
34    pub low_limit: f32,
35    pub high_limit: f32,
36    pub units: String,
37    pub complete: Complete,
38}
39
40/// Enum describing if a `TestInformation` has been completed
41///
42/// `TestInformation` is determined by the combination of a `TSR` and at least one `PTR`.
43/// The `TSR` variant indicates that the metadata from a `TSR` has been added.
44/// The `PTR` variant indicates that the metadata from a `PTR` has been added.
45/// The `Complete` variant indicates that both a `TSR` and `PTR` have been seen.
46#[derive(Debug)]
47pub enum Complete {
48    /// Metadata from a PTR has been added to the owning TestInformation
49    PTR,
50    /// Metadata from a TSR has been added to the owning TestInformation
51    TSR,
52    /// Metadata from both a PTR and TSR have been added to the owning TestInformation
53    Complete,
54}
55
56/// Determines how to convert `Complete` into Python objects
57///
58/// Can't derive `IntoPyObject` for enums, so implement manually.
59/// The variants are simply converted to strings of their variant names
60impl<'py> IntoPyObject<'py> for Complete {
61    type Target = PyString;
62    type Output = Bound<'py, Self::Target>;
63    type Error = Infallible;
64
65    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
66        let s = match self {
67            Self::TSR => "TSR".to_string().into_pyobject(py),
68            Self::PTR => "PTR".to_string().into_pyobject(py),
69            Self::Complete => "Complete".to_string().into_pyobject(py),
70        };
71        Ok(s?)
72    }
73}
74
75impl TestInformation {
76    /// Create a new `TestInformation` from a `PTR` record
77    pub fn new_from_ptr(ptr: &PTR) -> Self {
78        let test_num = ptr.test_num;
79        let head_num = ptr.head_num;
80        let site_num = ptr.site_num;
81        let test_type = TestType::Unknown;
82        let execution_count = 0;
83        let test_name = String::new();
84        let sequence_name = String::new();
85        let test_label = String::new();
86        let test_time = f32::NAN;
87        let test_text = ptr.test_txt.clone();
88        let low_limit = ptr.lo_limit;
89        let high_limit = ptr.hi_limit;
90        let units = ptr.units.clone();
91        let complete = Complete::PTR;
92
93        Self {
94            test_num,
95            head_num,
96            site_num,
97            test_type,
98            execution_count,
99            test_name,
100            sequence_name,
101            test_label,
102            test_time,
103            test_text,
104            low_limit,
105            high_limit,
106            units,
107            complete,
108        }
109    }
110
111    /// Add to an existing `TestInformation` with a `TSR` record
112    pub fn add_from_tsr(&mut self, tsr: &TSR) {
113        if (self.head_num != tsr.head_num)
114            || (self.site_num != tsr.site_num)
115            || (self.test_num != tsr.test_num)
116        {
117            panic!("head_num/site_num/test_num from TSR does not match!");
118        }
119        if let Complete::PTR = self.complete {
120            self.test_type = match tsr.test_typ {
121                'P' => TestType::P,
122                'F' => TestType::F,
123                'M' => TestType::M,
124                'S' => TestType::S,
125                _ => TestType::Unknown,
126            };
127            self.execution_count = tsr.exec_cnt;
128            self.test_name = tsr.test_nam.clone();
129            self.sequence_name = tsr.seq_name.clone();
130            self.test_label = tsr.test_lbl.clone();
131            self.test_time = tsr.test_tim;
132            self.complete = Complete::Complete;
133        }
134    }
135
136    /// Create a new `TestInformation` from a `TSR` record
137    pub fn new_from_tsr(tsr: &TSR) -> Self {
138        let test_num = tsr.test_num;
139        let head_num = tsr.head_num;
140        let site_num = tsr.site_num;
141        let test_type = match tsr.test_typ {
142            'P' => TestType::P,
143            'F' => TestType::F,
144            'M' => TestType::M,
145            'S' => TestType::S,
146            _ => TestType::Unknown,
147        };
148        let execution_count = tsr.exec_cnt;
149        let test_name = tsr.test_nam.clone();
150        let sequence_name = tsr.seq_name.clone();
151        let test_label = tsr.test_lbl.clone();
152        let test_time = tsr.test_tim;
153        let test_text = String::new();
154        let low_limit = f32::NAN;
155        let high_limit = f32::NAN;
156        let units = String::new();
157        let complete = Complete::TSR;
158
159        Self {
160            test_num,
161            head_num,
162            site_num,
163            test_type,
164            execution_count,
165            test_name,
166            sequence_name,
167            test_label,
168            test_time,
169            test_text,
170            low_limit,
171            high_limit,
172            units,
173            complete,
174        }
175    }
176
177    /// Add to an existing `TestInformation` with a `PTR`
178    pub fn add_from_ptr(&mut self, ptr: &PTR) {
179        if (self.head_num != ptr.head_num)
180            || (self.site_num != ptr.site_num)
181            || (self.test_num != ptr.test_num)
182        {
183            panic!("head_num/site_num/test_num from PTR does not match!");
184        }
185        if let Complete::TSR = self.complete {
186            self.test_text = ptr.test_txt.clone();
187            self.low_limit = ptr.lo_limit;
188            self.high_limit = ptr.hi_limit;
189            self.units = ptr.units.clone();
190            self.complete = Complete::Complete;
191        }
192    }
193}
194
195/// `TestType` describes the category of test
196#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize)]
197pub enum TestType {
198    /// A parametric test, i.e. one that measures a value
199    P,
200    /// A functional test, i.e. one with only pass/fail
201    F,
202    /// A multi-result parametric test, i.e. one that measures many values
203    M,
204    /// A scan test
205    S,
206    /// An unknown test type
207    Unknown,
208}
209
210impl fmt::Display for TestType {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        write!(f, "{:?}", self)
213    }
214}
215
216/// Determines how to convert `TestType` into Python objects
217///
218/// Can't derive `IntoPyObject` for enums, so implement manually.
219/// The variants are simply converted to strings of their variant names
220impl<'py> IntoPyObject<'py> for TestType {
221    type Target = PyString;
222    type Output = Bound<'py, Self::Target>;
223    type Error = Infallible;
224
225    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
226        let s = match self {
227            Self::P => "P".to_string().into_pyobject(py),
228            Self::F => "F".to_string().into_pyobject(py),
229            Self::M => "M".to_string().into_pyobject(py),
230            Self::S => "S".to_string().into_pyobject(py),
231            Self::Unknown => "Unknown".to_string().into_pyobject(py),
232        };
233        Ok(s?)
234    }
235}
236
237/// A collection of all `TestInformation`s in a STDF file
238///
239/// Indexed by (`test_num`, `site_num`, `head_num`)
240#[derive(Debug, IntoPyObject)]
241pub struct FullTestInformation {
242    pub test_infos: HashMap<(u32, u8, u8), TestInformation>,
243}
244
245impl FullTestInformation {
246    /// Initialize with an empty HashMap
247    pub fn new() -> Self {
248        let test_infos = HashMap::new();
249        Self { test_infos }
250    }
251
252    /// Add the metadata from a `PTR`.
253    ///
254    /// Looks up the appropriate `TestInformation` using the (`test_num`, `site_num`, `head_num`)
255    /// in the `PTR` and adds to this `TestInformation`
256    pub fn add_from_ptr(&mut self, ptr: &PTR) {
257        let key = (ptr.test_num, ptr.site_num, ptr.head_num);
258        self.test_infos
259            .entry(key)
260            .and_modify(|e| e.add_from_ptr(ptr))
261            .or_insert(TestInformation::new_from_ptr(ptr));
262    }
263
264    /// Add the metadata from a `TSR`.
265    ///
266    /// Looks up the appropriate `TestInformation` using the (`test_num`, `site_num`, `head_num`)
267    /// in the `TSR` and adds to this `TestInformation`
268    pub fn add_from_tsr(&mut self, tsr: &TSR) {
269        if tsr.head_num == 255 {
270            return;
271        }
272        let key = (tsr.test_num, tsr.site_num, tsr.head_num);
273        self.test_infos
274            .entry(key)
275            .and_modify(|e| e.add_from_tsr(tsr))
276            .or_insert(TestInformation::new_from_tsr(tsr));
277    }
278
279    /// Merges down the `FullTestInformation` to a `FullMergedTestInformation`
280    ///
281    /// The `FullMergedTestInformation` is indexed by only `test_num` rather than
282    /// (`test_num`, `site_num`, `head_num`). Usually all sites and all heads implement the
283    /// same tests and just run them in parallel, so it's not necessary to keep track of the
284    /// `site_num` and `head_num`.
285    pub fn merge(&self) -> FullMergedTestInformation {
286        let mut merged_test_info = FullMergedTestInformation::new();
287        for ti in self.test_infos.values() {
288            merged_test_info.add_from_test_information(ti);
289        }
290        merged_test_info
291    }
292
293    /// Gather all of the test information from a STDF specified by `fname`
294    ///
295    /// Iterates over all records in the STDF.
296    ///
297    /// Optionally allows for printing of the records while iterating over them, e.g. if you want
298    /// to make an ASCII text version.
299    ///
300    /// TODO: Make which records are printed configurable
301    ///
302    /// # Errors
303    /// If for some reason the file can't be parsed, returns a std::io::Error
304    pub fn from_fname(fname: &str, verbose: bool) -> std::io::Result<Self> {
305        let records = Records::new(&fname)?;
306        let mut test_info = Self::new();
307
308        for record in records {
309            if let Some(resolved) = record.resolve() {
310                let header = &record.header;
311
312                if verbose {
313                    println!(
314                        "{}.{} (0x{:x} @ 0x{:x}): {:?}",
315                        header.rec_typ, header.rec_sub, header.rec_len, record.offset, record.rtype
316                    );
317                }
318                if let Record::TSR(ref tsr) = resolved {
319                    test_info.add_from_tsr(&tsr);
320                }
321                if let Record::PIR(_) = resolved {
322                    continue;
323                }
324                if let Record::FTR(_) = resolved {
325                    continue;
326                }
327                if let Record::PTR(ref ptr) = resolved {
328                    test_info.add_from_ptr(&ptr);
329                }
330                //if let Record::PRR(_) = resolved {
331                //    continue;
332                //}
333                if verbose {
334                    println!("{resolved:#?}");
335                }
336            }
337        }
338        Ok(test_info)
339    }
340
341    pub fn from_fname_and_summarize(
342        fname: &str,
343        verbose: bool,
344    ) -> std::io::Result<(Self, RecordSummary)> {
345        let records = Records::new(&fname)?;
346        let mut summary = RecordSummary::new();
347        let mut test_info = Self::new();
348
349        for record in records {
350            summary.add(&record);
351            if let Some(resolved) = record.resolve() {
352                let header = &record.header;
353
354                if verbose {
355                    println!(
356                        "{}.{} (0x{:x} @ 0x{:x}): {:?}",
357                        header.rec_typ, header.rec_sub, header.rec_len, record.offset, record.rtype
358                    );
359                }
360                if let Record::TSR(ref tsr) = resolved {
361                    test_info.add_from_tsr(&tsr);
362                }
363                if let Record::PIR(_) = resolved {
364                    continue;
365                }
366                if let Record::FTR(_) = resolved {
367                    continue;
368                }
369                if let Record::PTR(ref ptr) = resolved {
370                    test_info.add_from_ptr(&ptr);
371                }
372                //if let Record::PRR(_) = resolved {
373                //    continue;
374                //}
375                if verbose {
376                    println!("{resolved:#?}");
377                }
378            }
379        }
380        Ok((test_info, summary))
381    }
382}
383
384impl IntoIterator for FullTestInformation {
385    type Item = ((u32, u8, u8), TestInformation);
386    type IntoIter = <HashMap<(u32, u8, u8), TestInformation> as IntoIterator>::IntoIter;
387    fn into_iter(self) -> Self::IntoIter {
388        self.test_infos.into_iter()
389    }
390}
391
392/// `MergedTestInformation` for a single test
393///
394/// The metadata for a test is uniquely determined by (`test_num`, `head_num`, `site_num`),
395/// however typically the `head_num` and `site_num` are redundant since all heads and sites are
396/// measuring the same thing in parallel. Given that, the `MergedTestInformation` contains the
397/// `TestInformation` associated with all `head_num`s and `site_num`s for a given `test_num`
398/// merged down to one structure.
399///
400/// The first (`test_num`, `head_num`, `site_num`) for a given `test_num` determines all of the
401/// metadata. Subsequent triplets with the same `test_num` simply add to the `execution_count`.
402///
403/// This structure does not contain information about any executions of the test, it only contains
404/// metadata associated with the test.
405#[derive(Debug, IntoPyObject)]
406pub struct MergedTestInformation {
407    pub test_num: u32,
408    pub test_type: TestType,
409    pub execution_count: u32,
410    pub test_name: String,
411    pub sequence_name: String,
412    pub test_label: String,
413    pub test_time: f32,
414    pub test_text: String,
415    pub low_limit: f32,
416    pub high_limit: f32,
417    pub units: String,
418}
419impl MergedTestInformation {
420    /// Initialize a new `MergedTestInformation` from a `TestInformation` record
421    pub fn new_from_test_information(test_information: &TestInformation) -> Self {
422        let test_num = test_information.test_num;
423        let test_type = test_information.test_type.clone();
424        let execution_count = test_information.execution_count;
425        let test_name = test_information.test_name.clone();
426        let sequence_name = test_information.sequence_name.clone();
427        let test_label = test_information.test_label.clone();
428        let test_time = test_information.test_time;
429        let test_text = test_information.test_text.clone();
430        let low_limit = test_information.low_limit;
431        let high_limit = test_information.high_limit;
432        let units = test_information.units.clone();
433        Self {
434            test_num,
435            test_type,
436            execution_count,
437            test_name,
438            sequence_name,
439            test_label,
440            test_time,
441            test_text,
442            low_limit,
443            high_limit,
444            units,
445        }
446    }
447
448    /// Add the `execution_count` from a `TestInformation` to an existing `MergedTestInformation`
449    pub fn add(&mut self, test_information: &TestInformation) {
450        if self.test_num != test_information.test_num {
451            panic!("TestInformation.test_num does not match that of MergedTestInformation!")
452        }
453        self.execution_count += test_information.execution_count;
454    }
455}
456
457/// A collection of all `MergedTestInformation`s in a STDF file
458///
459/// Indexed by `test_num`
460#[derive(Debug, IntoPyObject)]
461pub struct FullMergedTestInformation {
462    pub test_infos: HashMap<u32, MergedTestInformation>,
463}
464impl FullMergedTestInformation {
465    /// Initialize a new `FullMergedTestInformation` with an empty HashMap
466    pub fn new() -> Self {
467        let test_infos = HashMap::new();
468        Self { test_infos }
469    }
470
471    /// Adds the metadata from a `TestInformation` record
472    ///
473    /// If there is not a corresponding `MergedTestInformation` for the `test_num`, a new one is
474    /// made. If there is already a corresponding `MergedTestInformation`, adds the
475    /// `execution_count`.
476    pub fn add_from_test_information(&mut self, test_information: &TestInformation) {
477        let key = test_information.test_num;
478        self.test_infos
479            .entry(key)
480            .and_modify(|e| e.add(test_information))
481            .or_insert(MergedTestInformation::new_from_test_information(
482                test_information,
483            ));
484    }
485
486    /// Get the number of different tests with a given `TestType`
487    pub fn get_num(&self, test_type: TestType) -> usize {
488        self.test_infos
489            .values()
490            .filter(|&mti| mti.test_type == test_type)
491            .collect::<Vec<_>>()
492            .len()
493    }
494}
495
496/// Make a DataFrame containing the info in a `FullMergedTestInformation`
497impl Into<DataFrame> for &FullMergedTestInformation {
498    fn into(self) -> DataFrame {
499        let mut test_nums: Vec<u32> = Vec::new();
500        let mut test_types: Vec<String> = Vec::new();
501        let mut execution_counts: Vec<u32> = Vec::new();
502        let mut test_names: Vec<String> = Vec::new();
503        let mut sequence_names: Vec<String> = Vec::new();
504        let mut test_labels: Vec<String> = Vec::new();
505        let mut test_times: Vec<f32> = Vec::new();
506        let mut test_texts: Vec<String> = Vec::new();
507        let mut low_limits: Vec<f32> = Vec::new();
508        let mut high_limits: Vec<f32> = Vec::new();
509        let mut unitss: Vec<String> = Vec::new();
510
511        for (tnum, mti) in &self.test_infos {
512            test_nums.push(*tnum);
513            test_types.push(mti.test_type.to_string());
514            execution_counts.push(mti.execution_count);
515            test_names.push(mti.test_name.clone());
516            sequence_names.push(mti.sequence_name.clone());
517            test_labels.push(mti.test_label.clone());
518            test_times.push(mti.test_time);
519            test_texts.push(mti.test_text.clone());
520            low_limits.push(mti.low_limit);
521            high_limits.push(mti.high_limit);
522            unitss.push(mti.units.clone());
523        }
524
525        let mut columns: Vec<Column> = Vec::new();
526        columns.push(Column::new("test_num".into(), test_nums));
527        columns.push(Column::new("test_type".into(), test_types));
528        columns.push(Column::new("execution_count".into(), execution_counts));
529        columns.push(Column::new("test_name".into(), test_names));
530        columns.push(Column::new("sequence_name".into(), sequence_names));
531        columns.push(Column::new("test_label".into(), test_labels));
532        columns.push(Column::new("test_time".into(), test_times));
533        columns.push(Column::new("test_text".into(), test_texts));
534        columns.push(Column::new("low_limit".into(), low_limits));
535        columns.push(Column::new("high_limit".into(), high_limits));
536        columns.push(Column::new("units".into(), unitss));
537
538        DataFrame::new(columns).unwrap()
539    }
540}