From 1d8f12ee35dbd0820a1853d0d70a79d47a4e1e22 Mon Sep 17 00:00:00 2001 From: Barbara Bertozzi Date: Mon, 4 Aug 2025 12:26:03 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20ini=E2=86=92yaml=20conversion=20a?= =?UTF-8?q?nd=20tests=20for=20SP2-XR=20instrument=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20190417210227 Calibration 20181005.ini | 266 ++++++++++++++++++ src/sp2xr/helpers.py | 77 +++++ tests/data/sample_SP2XR.ini | 266 ++++++++++++++++++ tests/test_xr_ini_to_yaml.py | 39 +++ 4 files changed, 648 insertions(+) create mode 100644 meta_files/20190417210227 Calibration 20181005.ini create mode 100644 tests/data/sample_SP2XR.ini create mode 100644 tests/test_xr_ini_to_yaml.py diff --git a/meta_files/20190417210227 Calibration 20181005.ini b/meta_files/20190417210227 Calibration 20181005.ini new file mode 100644 index 0000000..096fba9 --- /dev/null +++ b/meta_files/20190417210227 Calibration 20181005.ini @@ -0,0 +1,266 @@ +[Custom] +Display Tab=TRUE +Display Names=<2> +Display Names 0=set 1 +Display Names 1=set 2 +Sets=<2> +Sets 0.Cluster.Graph 1=<8> +Sets 0.Cluster.Graph 1 0.Plot.Channel=+5V Laser Rail (V) +Sets 0.Cluster.Graph 1 0.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 1.Plot.Channel= +5V Rail (V) +Sets 0.Cluster.Graph 1 1.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 2.Plot.Channel=+12V Rail (V) +Sets 0.Cluster.Graph 1 2.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 3.Plot.Channel=3.3V Iso Rail (V) +Sets 0.Cluster.Graph 1 3.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 4.Plot.Channel=UPS Output (V) +Sets 0.Cluster.Graph 1 4.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 5.Plot.Channel=Inlet Air Temp (C) +Sets 0.Cluster.Graph 1 5.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 6.Plot.Channel=Crystal TEC Temp (C) +Sets 0.Cluster.Graph 1 6.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 7.Plot.Channel=Laser Heatsink Temp (C) +Sets 0.Cluster.Graph 1 7.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 2=<8> +Sets 0.Cluster.Graph 2 0.Plot.Channel=Laser TEC Temp (C) +Sets 0.Cluster.Graph 2 0.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 1.Plot.Channel=Crystal TEC Temp (C) +Sets 0.Cluster.Graph 2 1.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 2.Plot.Channel=Inlet Air Temp (C) +Sets 0.Cluster.Graph 2 2.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 3.Plot.Channel=Computer Heatsink Temp (C) +Sets 0.Cluster.Graph 2 3.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 4.Plot.Channel=Laser Heatsink Temp (C) +Sets 0.Cluster.Graph 2 4.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 5.Plot.Channel=Outlet Air Temp (C) +Sets 0.Cluster.Graph 2 5.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 6.Plot.Channel=Battery Temp (C) +Sets 0.Cluster.Graph 2 6.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 7.Plot.Channel=Laser TEC Sense +Sets 0.Cluster.Graph 2 7.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 1=<4> +Sets 1.Cluster.Graph 1 0.Plot.Channel=Threshold Crossing Events +Sets 1.Cluster.Graph 1 0.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 1.Plot.Channel=Dual Qualified Scatter and Incand Particles +Sets 1.Cluster.Graph 1 1.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 2.Plot.Channel=Qualified Scatter Only Particles +Sets 1.Cluster.Graph 1 2.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 3.Plot.Channel=Qualified Incand Only Particles +Sets 1.Cluster.Graph 1 3.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2=<8> +Sets 1.Cluster.Graph 2 0.Plot.Channel=Baseline Sizer Lo +Sets 1.Cluster.Graph 2 0.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 1.Plot.Channel=Baseline Sizer Hi +Sets 1.Cluster.Graph 2 1.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 2.Plot.Channel=Baseline Incand Lo +Sets 1.Cluster.Graph 2 2.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 3.Plot.Channel=Baseline Incand Hi +Sets 1.Cluster.Graph 2 3.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 4.Plot.Channel=Bandwidth Sizer Hi +Sets 1.Cluster.Graph 2 4.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 5.Plot.Channel=Bandwidth Sizer Lo +Sets 1.Cluster.Graph 2 5.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 6.Plot.Channel=Bandwidth Incand Lo +Sets 1.Cluster.Graph 2 6.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 7.Plot.Channel=Bandwidth Incand Hi +Sets 1.Cluster.Graph 2 7.Plot.Left/Right=TRUE +[Raw Options] +byte 0: data mux=High Dynamic Range Traces +Raw Data Particle Selection=First Scatter +Scatter relPeak=0 +Incand relPeak=0 +Inter-raw Period (ms)=100 +leader sample count=400 +footer sample count=400 +[Scatter Parameters] +Graph=Counts +X Mode=Size +Norm?=FALSE +Cumulative=FALSE +[Versions] +Instrument Name=SP2XR +SP2 Version=2.01.01.19 +Acq Version=2.00.00.00 +Last Date Updated=4/11/2019 6:24:36 AM +[Trigger Settings] +Scatter Transit Time Min=10 +Scatter Transit Time Max=65535 +Scatter FWHM Min=30 +Scatter FWHM Max=65535 +Scatter Inter Particle Time Min=10 +Incand Transit Time Min=5 +Incand Transit Time Max=65535 +Incand FWHM Min=30 +Incand FWHM Max=65535 +Incand Inter Particle Time Min=10 +Paired Particle Delay Max=10 +Scatter Threshold Min=36100 +Scatter Hysteresis Min=2703 +Incand Threshold Min=50700 +Incand Hysteresis Min=5394 +Scatter Threshold Max=559000000 +Scatter Hysteresis Max=0 +Incand Threshold Max=2147483647 +Incand Hysteresis Max=0 +Forced Trigger=FALSE +Forced Trigger Interval(ms)=1000 +[# Samples S] +# Samples S=0 +[Program] +Data File Path=D:\DMT\SP2XR Data +Restart Files=FALSE +Graph 0 Left=YAG Output Monitor (V) +Graph 0 Right=Laser Driver Current Monitor (A) +Graph 1 Left=Scattering Particle Conc (cts/ccm) +Graph 1 Right=Incand Particle Conc (cts/ccm) +Control Cycle Time=0 +NTP Server= +Write File?=FALSE +Graph Backgrounds=16448250 +Graph 2 Left=Sheath Flow Controller Read (sccm) +Graph 2 Right=Sample Flow Controller Read (sccm) +Description= +Serial Number=0001 +2 or 3 Graphs=TRUE +Time Range=12 Hours +OSDS Format= +Num to Avg=0 +Global 2=0 +Global 3=0 +Global 4=0 +Global 5=0 +Shut Down Sequence= +Crisis Shut Down Seq=turn off pump and laser +Write SP2b Data File=TRUE +Write HK File=TRUE +Write Raw Binary Data=TRUE +TabChannelNum=0 +OptimizeChannelNum=0 +Write HDF5 File=TRUE +NumParticlesPerHDF5File=100000 +Laser Temp Set=29 +Laser Current Set=1.9 +Spare 4 Set=0 +Spare 5 Set=0 +PMT HV Set=0.46 +Interface Board Scaling=<24> +Interface Board Scaling 0=1/(0.000849+0.000261*ln(10000/(65536/VAR-1))+0.000000125*ln(10000/(65536/VAR-1))^3)-273.15 +Interface Board Scaling 1= +Interface Board Scaling 2=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 3=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 4=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 5=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 6=0.0000625*VAR +Interface Board Scaling 7=VAR/72+105.55 +Interface Board Scaling 8=0.125*VAR +Interface Board Scaling 9=0.000125*VAR +Interface Board Scaling 10=0.000125*VAR +Interface Board Scaling 11=2.01*0.0000625 *VAR +Interface Board Scaling 12= +Interface Board Scaling 13=0.000125*VAR +Interface Board Scaling 14=0.000125*VAR +Interface Board Scaling 15=4.57*0.0000625*VAR +Interface Board Scaling 16=0.0152587890625*VAR +Interface Board Scaling 17=1/(0.000894+0.00025*ln((1662.22* VAR)/(39897.3 - VAR))+0.0000002*ln((1662.22*VAR)/(39897.3 -VAR))^3)-273.15 +Interface Board Scaling 18=(69.8+11.5) /11.5*0.0000625*VAR +Interface Board Scaling 19=4.57*0.0000625*VAR +Interface Board Scaling 20=0.000125*VAR +Interface Board Scaling 21=1.1*0.0000625 *VAR +Interface Board Scaling 22= +Interface Board Scaling 23= +ABD 0408 Scaling=<16> +ABD 0408 Scaling 0= +ABD 0408 Scaling 1= +ABD 0408 Scaling 2= +ABD 0408 Scaling 3= +ABD 0408 Scaling 4= +ABD 0408 Scaling 5=VAR*0.00625 +ABD 0408 Scaling 6=7.98*(6.25E-5*VAR) +ABD 0408 Scaling 7=-84.962*(6.25E-5*VAR-1.8639) +ABD 0408 Scaling 8= +ABD 0408 Scaling 9= +ABD 0408 Scaling 10= +ABD 0408 Scaling 11= +ABD 0408 Scaling 12= +ABD 0408 Scaling 13= +ABD 0408 Scaling 14= +ABD 0408 Scaling 15= +Save Every Nth Particle=1 +zip files=TRUE +Particle Density (g/cc)=1.8 +Pump Start-Up State=FALSE +[Detector DAC Settings] +Scatter unused A=23790 +Scatter unused B=65535 +[Incand Parameters] +Graph=Counts +X Mode=Mass +Norm?=FALSE +Cumulative=FALSE +[Control] +Alarms=<3> +Alarms 0.Alarm.Name=TurnPumpON +Alarms 0.Alarm.Channel=Elapsed Time +Alarms 0.Alarm.Condition=> +Alarms 0.Alarm.Threshold=0 +Alarms 0.Alarm.Action=Turn Pump On +Alarms 0.Alarm.Hysteresis=0 +Alarms 0.Alarm.Target Channel= +Alarms 0.Alarm.Set Value=0 +Alarms 0.Alarm.Min Time=0 +Alarms 0.Alarm.Sequence=turn off pump and laser +Alarms 0.Alarm.Target Alarm=TurnPumpON +Alarms 1.Alarm.Name=Turn Laser On +Alarms 1.Alarm.Channel=Elapsed Time +Alarms 1.Alarm.Condition=> +Alarms 1.Alarm.Threshold=5 +Alarms 1.Alarm.Action=Turn Laser On +Alarms 1.Alarm.Hysteresis=0 +Alarms 1.Alarm.Target Channel= +Alarms 1.Alarm.Set Value=0 +Alarms 1.Alarm.Min Time=0 +Alarms 1.Alarm.Sequence= +Alarms 1.Alarm.Target Alarm=Turn Laser On +Alarms 2.Alarm.Name=StartRecording +Alarms 2.Alarm.Channel=Elapsed Time +Alarms 2.Alarm.Condition=> +Alarms 2.Alarm.Threshold=10 +Alarms 2.Alarm.Action=Start Writing Data +Alarms 2.Alarm.Hysteresis=0 +Alarms 2.Alarm.Target Channel= +Alarms 2.Alarm.Set Value=0 +Alarms 2.Alarm.Min Time=0 +Alarms 2.Alarm.Sequence= +Alarms 2.Alarm.Target Alarm=Turn Laser On +Sequences=<0> +Timers=<0> +[Pump] +Pump=TRUE +[# Samples I] +# Samples I=0 +[SampleFlow] +SampleFlow (sccm)=30 +[SheathFlow] +SheathFlow (sccm)=600 +[Polling Interval] +HK Stream Interval (ms)=1000 +PbP Stream Interval (ms)=1000 +[Fans Settings] +Case Fan Mode=normal +Case Fan On Threshold=35 +Case Fan Off Threshold=33 +Laser Fan Mode=forced off +Laser Fan On Threshold=27 +Laser Fan Off Threshold=24 +[Channel Order] +Channel Order=<0> +Digits=<0> +[Streaming Data] +Port=0 +Baud Rate=0 +Channels=<0> +Bus=Serial Port +[Calculated Channels] +Calculated Channels=<0> +[Calculations] +Calculations=<0> diff --git a/src/sp2xr/helpers.py b/src/sp2xr/helpers.py index c44c76f..4dfc0ba 100644 --- a/src/sp2xr/helpers.py +++ b/src/sp2xr/helpers.py @@ -1,5 +1,6 @@ import os import re +import yaml from pathlib import Path from typing import Optional, List @@ -48,3 +49,79 @@ def chunks(lst: List[str], n: int): """ for i in range(0, len(lst), n): yield lst[i : i + n] + + +def read_xr_ini_file(fname): + """ + Read a SP2-XR .ini file and extract parameters of interest into a dictionary. + + Parameters + ---------- + fname : str + Path to the .ini file. + + Returns + ------- + dict + Extracted parameters as {param_name: value}. + """ + # Map from substrings in file → dictionary keys + param_map = { + "Save Every Nth Particle": "SaveRate", + "Incand Transit Time Min": "IncTransitMin", + "Incand Transit Time Max": "IncTransitMax", + "Incand FWHM Min": "IncFWHMMin", + "Incand HWHM Min": "IncHWHMMin", + "Incand FWHM Max": "IncFWHMMax", + "Incand HWHM Max": "IncHWHMMax", + "Incand Inter Particle Time Min": "IncInterTimeMin", + "Scatter Transit Time Min": "ScattTransitMin", + "Scatter Transit Time Max": "ScattTransitMax", + "Scatter FWHM Min": "ScattFWHMMin", + "Scatter HWHM Min": "ScattHWHMMin", + "Scatter FWHM Max": "ScattFWHMMax", + "Scatter HWHM Max": "ScattHWHMMax", + "Scatter Inter Particle Time Min": "ScattInterTimeMin", + } + + def parse_value(raw: str, default=None): + try: + return float(raw.strip()) + except (ValueError, AttributeError): + return default + + params = {} + with open(fname, "r") as f: + for line in f: + if "Disqualified" in line: # skip irrelevant lines + continue + for pattern, key in param_map.items(): + if pattern in line: + raw_value = line.split("=")[-1] + params[key] = parse_value( + raw_value, default=1 if key == "SaveRate" else None + ) + + return params + + +def export_xr_ini_to_yaml(ini_path, yaml_path): + """ + Convert an SP2-XR .ini file to a YAML configuration file. + Users can later edit this file to add calibration/threshold parameters. + """ + params = read_xr_ini_file(ini_path) + yaml_path = Path(yaml_path) + + # If YAML already exists, merge existing user-defined keys + if yaml_path.exists(): + with open(yaml_path, "r") as f: + existing = yaml.safe_load(f) or {} + existing.update(params) + params = existing + + # Save to YAML + with open(yaml_path, "w") as f: + yaml.dump(params, f, sort_keys=False) + + print(f"✅ Parameters exported to {yaml_path}") diff --git a/tests/data/sample_SP2XR.ini b/tests/data/sample_SP2XR.ini new file mode 100644 index 0000000..096fba9 --- /dev/null +++ b/tests/data/sample_SP2XR.ini @@ -0,0 +1,266 @@ +[Custom] +Display Tab=TRUE +Display Names=<2> +Display Names 0=set 1 +Display Names 1=set 2 +Sets=<2> +Sets 0.Cluster.Graph 1=<8> +Sets 0.Cluster.Graph 1 0.Plot.Channel=+5V Laser Rail (V) +Sets 0.Cluster.Graph 1 0.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 1.Plot.Channel= +5V Rail (V) +Sets 0.Cluster.Graph 1 1.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 2.Plot.Channel=+12V Rail (V) +Sets 0.Cluster.Graph 1 2.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 3.Plot.Channel=3.3V Iso Rail (V) +Sets 0.Cluster.Graph 1 3.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 1 4.Plot.Channel=UPS Output (V) +Sets 0.Cluster.Graph 1 4.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 5.Plot.Channel=Inlet Air Temp (C) +Sets 0.Cluster.Graph 1 5.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 6.Plot.Channel=Crystal TEC Temp (C) +Sets 0.Cluster.Graph 1 6.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 1 7.Plot.Channel=Laser Heatsink Temp (C) +Sets 0.Cluster.Graph 1 7.Plot.Left/Right=TRUE +Sets 0.Cluster.Graph 2=<8> +Sets 0.Cluster.Graph 2 0.Plot.Channel=Laser TEC Temp (C) +Sets 0.Cluster.Graph 2 0.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 1.Plot.Channel=Crystal TEC Temp (C) +Sets 0.Cluster.Graph 2 1.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 2.Plot.Channel=Inlet Air Temp (C) +Sets 0.Cluster.Graph 2 2.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 3.Plot.Channel=Computer Heatsink Temp (C) +Sets 0.Cluster.Graph 2 3.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 4.Plot.Channel=Laser Heatsink Temp (C) +Sets 0.Cluster.Graph 2 4.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 5.Plot.Channel=Outlet Air Temp (C) +Sets 0.Cluster.Graph 2 5.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 6.Plot.Channel=Battery Temp (C) +Sets 0.Cluster.Graph 2 6.Plot.Left/Right=FALSE +Sets 0.Cluster.Graph 2 7.Plot.Channel=Laser TEC Sense +Sets 0.Cluster.Graph 2 7.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 1=<4> +Sets 1.Cluster.Graph 1 0.Plot.Channel=Threshold Crossing Events +Sets 1.Cluster.Graph 1 0.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 1.Plot.Channel=Dual Qualified Scatter and Incand Particles +Sets 1.Cluster.Graph 1 1.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 2.Plot.Channel=Qualified Scatter Only Particles +Sets 1.Cluster.Graph 1 2.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 1 3.Plot.Channel=Qualified Incand Only Particles +Sets 1.Cluster.Graph 1 3.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2=<8> +Sets 1.Cluster.Graph 2 0.Plot.Channel=Baseline Sizer Lo +Sets 1.Cluster.Graph 2 0.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 1.Plot.Channel=Baseline Sizer Hi +Sets 1.Cluster.Graph 2 1.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 2.Plot.Channel=Baseline Incand Lo +Sets 1.Cluster.Graph 2 2.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 3.Plot.Channel=Baseline Incand Hi +Sets 1.Cluster.Graph 2 3.Plot.Left/Right=FALSE +Sets 1.Cluster.Graph 2 4.Plot.Channel=Bandwidth Sizer Hi +Sets 1.Cluster.Graph 2 4.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 5.Plot.Channel=Bandwidth Sizer Lo +Sets 1.Cluster.Graph 2 5.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 6.Plot.Channel=Bandwidth Incand Lo +Sets 1.Cluster.Graph 2 6.Plot.Left/Right=TRUE +Sets 1.Cluster.Graph 2 7.Plot.Channel=Bandwidth Incand Hi +Sets 1.Cluster.Graph 2 7.Plot.Left/Right=TRUE +[Raw Options] +byte 0: data mux=High Dynamic Range Traces +Raw Data Particle Selection=First Scatter +Scatter relPeak=0 +Incand relPeak=0 +Inter-raw Period (ms)=100 +leader sample count=400 +footer sample count=400 +[Scatter Parameters] +Graph=Counts +X Mode=Size +Norm?=FALSE +Cumulative=FALSE +[Versions] +Instrument Name=SP2XR +SP2 Version=2.01.01.19 +Acq Version=2.00.00.00 +Last Date Updated=4/11/2019 6:24:36 AM +[Trigger Settings] +Scatter Transit Time Min=10 +Scatter Transit Time Max=65535 +Scatter FWHM Min=30 +Scatter FWHM Max=65535 +Scatter Inter Particle Time Min=10 +Incand Transit Time Min=5 +Incand Transit Time Max=65535 +Incand FWHM Min=30 +Incand FWHM Max=65535 +Incand Inter Particle Time Min=10 +Paired Particle Delay Max=10 +Scatter Threshold Min=36100 +Scatter Hysteresis Min=2703 +Incand Threshold Min=50700 +Incand Hysteresis Min=5394 +Scatter Threshold Max=559000000 +Scatter Hysteresis Max=0 +Incand Threshold Max=2147483647 +Incand Hysteresis Max=0 +Forced Trigger=FALSE +Forced Trigger Interval(ms)=1000 +[# Samples S] +# Samples S=0 +[Program] +Data File Path=D:\DMT\SP2XR Data +Restart Files=FALSE +Graph 0 Left=YAG Output Monitor (V) +Graph 0 Right=Laser Driver Current Monitor (A) +Graph 1 Left=Scattering Particle Conc (cts/ccm) +Graph 1 Right=Incand Particle Conc (cts/ccm) +Control Cycle Time=0 +NTP Server= +Write File?=FALSE +Graph Backgrounds=16448250 +Graph 2 Left=Sheath Flow Controller Read (sccm) +Graph 2 Right=Sample Flow Controller Read (sccm) +Description= +Serial Number=0001 +2 or 3 Graphs=TRUE +Time Range=12 Hours +OSDS Format= +Num to Avg=0 +Global 2=0 +Global 3=0 +Global 4=0 +Global 5=0 +Shut Down Sequence= +Crisis Shut Down Seq=turn off pump and laser +Write SP2b Data File=TRUE +Write HK File=TRUE +Write Raw Binary Data=TRUE +TabChannelNum=0 +OptimizeChannelNum=0 +Write HDF5 File=TRUE +NumParticlesPerHDF5File=100000 +Laser Temp Set=29 +Laser Current Set=1.9 +Spare 4 Set=0 +Spare 5 Set=0 +PMT HV Set=0.46 +Interface Board Scaling=<24> +Interface Board Scaling 0=1/(0.000849+0.000261*ln(10000/(65536/VAR-1))+0.000000125*ln(10000/(65536/VAR-1))^3)-273.15 +Interface Board Scaling 1= +Interface Board Scaling 2=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 3=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 4=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 5=(1.0 / (1.1135E-3 + 2.368E-4 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001)))) + 7.396E-8 * (ln(1E4 * (1-(VAR / (65536.0 * 1.001))) / (VAR / (65536.0 * 1.001))))^3)) - 273.15 +Interface Board Scaling 6=0.0000625*VAR +Interface Board Scaling 7=VAR/72+105.55 +Interface Board Scaling 8=0.125*VAR +Interface Board Scaling 9=0.000125*VAR +Interface Board Scaling 10=0.000125*VAR +Interface Board Scaling 11=2.01*0.0000625 *VAR +Interface Board Scaling 12= +Interface Board Scaling 13=0.000125*VAR +Interface Board Scaling 14=0.000125*VAR +Interface Board Scaling 15=4.57*0.0000625*VAR +Interface Board Scaling 16=0.0152587890625*VAR +Interface Board Scaling 17=1/(0.000894+0.00025*ln((1662.22* VAR)/(39897.3 - VAR))+0.0000002*ln((1662.22*VAR)/(39897.3 -VAR))^3)-273.15 +Interface Board Scaling 18=(69.8+11.5) /11.5*0.0000625*VAR +Interface Board Scaling 19=4.57*0.0000625*VAR +Interface Board Scaling 20=0.000125*VAR +Interface Board Scaling 21=1.1*0.0000625 *VAR +Interface Board Scaling 22= +Interface Board Scaling 23= +ABD 0408 Scaling=<16> +ABD 0408 Scaling 0= +ABD 0408 Scaling 1= +ABD 0408 Scaling 2= +ABD 0408 Scaling 3= +ABD 0408 Scaling 4= +ABD 0408 Scaling 5=VAR*0.00625 +ABD 0408 Scaling 6=7.98*(6.25E-5*VAR) +ABD 0408 Scaling 7=-84.962*(6.25E-5*VAR-1.8639) +ABD 0408 Scaling 8= +ABD 0408 Scaling 9= +ABD 0408 Scaling 10= +ABD 0408 Scaling 11= +ABD 0408 Scaling 12= +ABD 0408 Scaling 13= +ABD 0408 Scaling 14= +ABD 0408 Scaling 15= +Save Every Nth Particle=1 +zip files=TRUE +Particle Density (g/cc)=1.8 +Pump Start-Up State=FALSE +[Detector DAC Settings] +Scatter unused A=23790 +Scatter unused B=65535 +[Incand Parameters] +Graph=Counts +X Mode=Mass +Norm?=FALSE +Cumulative=FALSE +[Control] +Alarms=<3> +Alarms 0.Alarm.Name=TurnPumpON +Alarms 0.Alarm.Channel=Elapsed Time +Alarms 0.Alarm.Condition=> +Alarms 0.Alarm.Threshold=0 +Alarms 0.Alarm.Action=Turn Pump On +Alarms 0.Alarm.Hysteresis=0 +Alarms 0.Alarm.Target Channel= +Alarms 0.Alarm.Set Value=0 +Alarms 0.Alarm.Min Time=0 +Alarms 0.Alarm.Sequence=turn off pump and laser +Alarms 0.Alarm.Target Alarm=TurnPumpON +Alarms 1.Alarm.Name=Turn Laser On +Alarms 1.Alarm.Channel=Elapsed Time +Alarms 1.Alarm.Condition=> +Alarms 1.Alarm.Threshold=5 +Alarms 1.Alarm.Action=Turn Laser On +Alarms 1.Alarm.Hysteresis=0 +Alarms 1.Alarm.Target Channel= +Alarms 1.Alarm.Set Value=0 +Alarms 1.Alarm.Min Time=0 +Alarms 1.Alarm.Sequence= +Alarms 1.Alarm.Target Alarm=Turn Laser On +Alarms 2.Alarm.Name=StartRecording +Alarms 2.Alarm.Channel=Elapsed Time +Alarms 2.Alarm.Condition=> +Alarms 2.Alarm.Threshold=10 +Alarms 2.Alarm.Action=Start Writing Data +Alarms 2.Alarm.Hysteresis=0 +Alarms 2.Alarm.Target Channel= +Alarms 2.Alarm.Set Value=0 +Alarms 2.Alarm.Min Time=0 +Alarms 2.Alarm.Sequence= +Alarms 2.Alarm.Target Alarm=Turn Laser On +Sequences=<0> +Timers=<0> +[Pump] +Pump=TRUE +[# Samples I] +# Samples I=0 +[SampleFlow] +SampleFlow (sccm)=30 +[SheathFlow] +SheathFlow (sccm)=600 +[Polling Interval] +HK Stream Interval (ms)=1000 +PbP Stream Interval (ms)=1000 +[Fans Settings] +Case Fan Mode=normal +Case Fan On Threshold=35 +Case Fan Off Threshold=33 +Laser Fan Mode=forced off +Laser Fan On Threshold=27 +Laser Fan Off Threshold=24 +[Channel Order] +Channel Order=<0> +Digits=<0> +[Streaming Data] +Port=0 +Baud Rate=0 +Channels=<0> +Bus=Serial Port +[Calculated Channels] +Calculated Channels=<0> +[Calculations] +Calculations=<0> diff --git a/tests/test_xr_ini_to_yaml.py b/tests/test_xr_ini_to_yaml.py new file mode 100644 index 0000000..f734096 --- /dev/null +++ b/tests/test_xr_ini_to_yaml.py @@ -0,0 +1,39 @@ +from pathlib import Path +import yaml +from sp2xr.helpers import read_xr_ini_file, export_xr_ini_to_yaml + +# Paths for testing +DATA = Path(__file__).parent / "data" +INI_FILE = DATA / "sample_SP2XR.ini" +YAML_FILE = DATA / "sample_SP2XR_config.yaml" + + +def test_ini_to_yaml_conversion(tmp_path): + """Test that an .ini file is correctly parsed and exported to YAML.""" + + # 1. Read parameters from ini + params = read_xr_ini_file(INI_FILE) + assert isinstance(params, dict) + assert "SaveRate" in params, "Missing expected parameter from ini parsing" + + # 2. Export to YAML + yaml_path = tmp_path / "out_config.yaml" + export_xr_ini_to_yaml(INI_FILE, yaml_path) + assert yaml_path.exists(), "YAML file was not created" + + # 3. Reload YAML and check values + with open(yaml_path, "r") as f: + loaded = yaml.safe_load(f) + assert "SaveRate" in loaded, "SaveRate not found in YAML output" + + # 4. Simulate user adding calibration + loaded["calibration"] = {"mass_factor": 1.05, "mass_offset": -0.1} + with open(yaml_path, "w") as f: + yaml.dump(loaded, f, sort_keys=False) + + # 5. Reload and verify calibration is preserved + with open(yaml_path, "r") as f: + updated = yaml.safe_load(f) + assert "calibration" in updated, "Calibration section missing after update" + + print("✅ INI → YAML conversion test passed successfully")