"""Read measured data (and datasheet tables) with their units attached.Every CSV this engine reads names each column's unit in its header, the wayphyphox exports do: Time (s), Acceleration z (m/s^2)Square brackets work too: thrust [gf]A column without a unit is refused, because a measurement without a unit isnot a measurement (EL01, section 2.2).read_csv returns a Table: an ordered dict from column name to a Quantity thatwraps a numpy array, plus the file it came from."""from__future__importannotationsimportcsvimportrefromdataclassesimportdataclass,fieldfrompathlibimportPathimportnumpyasnpfromhwe.unitsimportQ_,ureg_HEADER=re.compile(r"^\s*(?P<name>.*?)\s*[\(\[](?P<unit>[^\)\]]*)[\)\]]\s*$")# Header spellings that pint does not read directly._UNIT_ALIASES={"%":"percent","°C":"degC","deg":"degree","g/W":"gf/W","":"dimensionless","-":"dimensionless",}@dataclassclassTable:path:Pathcolumns:dict=field(default_factory=dict)# name -> Quantity arraydef__getitem__(self,name):returnself.columns[name]defnames(self):returnlist(self.columns)deffind(self,*words):"""The first column whose name contains every word (case-insensitive)."""forname,valuesinself.columns.items():low=name.lower()ifall(w.lower()inlowforwinwords):returnvaluesraiseKeyError(f"no column containing {words} in {self.path.name}; have {self.names()}")defparse_header(header:str)->tuple[str,str]:"""'Acceleration z (m/s^2)' -> ('Acceleration z', 'm/s^2')"""match=_HEADER.match(header)ifnotmatch:raiseValueError(f"column {header!r} has no unit; write it as 'name (unit)'")name,unit=match.group("name"),match.group("unit").strip()unit=_UNIT_ALIASES.get(unit,unit)ureg.parse_units(unit)# fail now, with the column named, if pint can't read itreturnname,unitdefread_csv(path:Path|str)->Table:"""Read a CSV whose headers carry units. Decimal points, comma separators."""path=Path(path)withopen(path,newline="",encoding="utf-8-sig")asf:sample=f.read(4096)f.seek(0)dialect=csv.Sniffer().sniff(sample,delimiters=",;\t")rows=list(csv.reader(f,dialect))header,body=rows[0],[rforrinrows[1:]ifany(cell.strip()forcellinr)]table=Table(path=path)forj,headinenumerate(header):name,unit=parse_header(head)numbers=np.array([float(r[j])forrinbody])table.columns[name]=Q_(numbers,unit)returntable