From 912c0374de1817719f0c8159e8d7e9c9f49be427 Mon Sep 17 00:00:00 2001
From: Tschan
Date: Fri, 11 Sep 2026 19:45:48 +0200
Subject: [PATCH] Starting to write documentation for TOC code.
---
defs/BTOF.py | 3 +
defs/TOC.py | 120 +++++-
doc/defs/{visualising.html => BTOF.html} | 58 +--
doc/defs/Helper.html | 168 ++++++++
doc/defs/OzoneMonitor.html | 180 +++++++++
doc/defs/{OFR.html => SMPS.html} | 140 ++++---
doc/defs/TOC.html | 468 +++++++++++++++++++++++
doc/defs/index.html | 25 +-
8 files changed, 1035 insertions(+), 127 deletions(-)
rename doc/defs/{visualising.html => BTOF.html} (82%)
create mode 100644 doc/defs/Helper.html
create mode 100644 doc/defs/OzoneMonitor.html
rename doc/defs/{OFR.html => SMPS.html} (84%)
create mode 100644 doc/defs/TOC.html
diff --git a/defs/BTOF.py b/defs/BTOF.py
index e69de29..f5521ad 100644
--- a/defs/BTOF.py
+++ b/defs/BTOF.py
@@ -0,0 +1,3 @@
+r"""
+An empty docstring
+"""
\ No newline at end of file
diff --git a/defs/TOC.py b/defs/TOC.py
index 47e59d1..15a751a 100644
--- a/defs/TOC.py
+++ b/defs/TOC.py
@@ -8,7 +8,7 @@
### ----------------------------------------------------------------------------------------------------------------------------------- ###
### SUBMODULE DOCSTRING
-"""The docstring for this submodule
+"""This submodule contains functionality to calibrate TOC measurement data and analyse them.
"""
### ----------------------------------------------------------------------------------------------------------------------------------- ###
@@ -27,10 +27,37 @@ from defs.Helper import compute_R2
class CalibrationCurve():
+ r"""An object representing a calibration curve for TOC, IC or NPOC Measurements.
+
+ Description
+ -----------
+ This object instanciates a class that relates the area $A$ measured in the TOC Analyser to the concentration $c$ used in the calibration solutions. The calibration curve is affine, $c=aA+b$ where $a$ and $b$ denote slope and intercept, respectively, of the affine map. This object can be used to represent linear calibration curves for TC (total carbon)-, IC (inorganic carbon)- and NPOC (non-purgeable organic carbon) measurements.
+
+ Attributes
+ ----------
+ exists: Bool
+ This value is set to `True` if a calibration curve is already available, else it is set to `False`.
+ slope: float
+ Value of the slope for the calibration of concentration (dependent variable) with measured area (independent variable).
+ intercept: float
+ Value of the intercep for the calibration of concentration (dependent variable) with measured area (independent variable).
+ R2_value: float
+ The coefficient of determination of the calibration curve. For a good calibration curve, this should be above 0.995.\
+ excluded: list
+ A list of integers denoting the samples in the calibrant data that are excluded.
+ lower_bound: float
+ The lower bound value for which this calibration curve can be used.
+ upper_bound: float
+ The upper bound value for which this calibration curve can be used.
+ computed_on: pd.Timestamp
+ A timestamp indicating the time at which the calibration curve was computed.
+ """
+
def __init__(self):
- self.exists = False
+ # initialises all member variables
+ self.exists = False
self.slope = None
self.intercept = None
self.R2_value = None
@@ -41,16 +68,76 @@ class CalibrationCurve():
def __linear_function(self, x_values, a, b):
+ r"""Computes the value of the calibration curve for the passed parameters.
+
+ Description
+ -----------
+ Computes the affine map, $c=aA+b$, given data $A$ and parameters $a$ (slope) and $b$ (intercept).
+
+ Parameters
+ ----------
+ x_values: array-like
+ An array-like (array or list) object of length $n$ containing the data for the independent variable that are to be mapped.
+ a: float
+ The slope of the calibration curve.
+ b: float
+ The intercept of the calibration curve.
+
+ Returns
+ -------
+ y_values: np.ndarray
+ An array (of size $n$) containing the image under the affine map of `x_values`.
+ """
+
+ # check if the input data is an instance of a np.ndarray. If not, cast it to np.ndarray
if not isinstance(x_values, np.ndarray):
x_values = np.array(x_values)
+ # evaluate the image of the input under the affine map and return the results
return a * x_values + b
def __optimise(self, x_data, y_data, p0):
+
+ r"""Finds the optimal parameters of the calibration curve
+
+ Description
+ -----------
+ Uses `scipy.optimize.curve_fit` to find optimal parameters for the calibrant data.
+
+ Parameters
+ ----------
+ x_data: array-like
+ The data representing the independent variable.
+ y_data: array-like
+ The data representing the dependent variable.
+ p0: list
+ A list containing the initial guesses of the parameters.
+
+ Returns
+ -------
+ p: np.ndarray
+ An array containing the optimal parameter values.
+ pcov: The covariance matrix associated with the optimal parameters.
+ """
+
+ # evaluate the optimisation problem and return optimal parameters and their covariance matrix.
return curve_fit(self.__linear_function, x_data, y_data, p0)
def __check_exist(self):
+ r"""Checks if attribute `exists` is set
+
+ Description
+ -----------
+ Checks of the attribute `exists` is set to `None` (its instanciation value) and returns `False` in this case, or returns `True` if attribute value is not `None` (i.e. if it has been set).
+
+ Returns
+ -------
+ t: Bool
+ `True` if attribute has a value, else returns `None`.
+ """
+
+ # this line is equivalent to "if None is True" if attribute is not set, which evaluates to 'False'. Else evaluates to 'True'.
if self.exists == True:
return True
else:
@@ -58,6 +145,13 @@ class CalibrationCurve():
def __check(self):
+ r"""Gives a warning message when overwriting an existing calibration curve.
+
+ Description
+ -----------
+ Calls the private `check_exists` method to see if a calibration curve has already been computed. If yes, this will throw a warning message, but not an error.
+ """
+
if self.__check_exist():
warnings.warn("Calibration alreadyt exists, will be overwritten...")
else:
@@ -65,6 +159,20 @@ class CalibrationCurve():
def calibrate(self, x_data, y_data):
+ """Compute a calibration curve based on the available calibrant data.
+
+ Description
+ -----------
+ Bla Bla Bla
+
+ Parameters
+ ----------
+ x_data: np.ndarray
+ Bla bla bla
+ y_data: np.ndarray
+ Bla Bla Bla
+ """
+
self.__check()
parameters, _ = self.__optimise(x_data=x_data, y_data=y_data, p0=[10, 0.1])
@@ -103,6 +211,8 @@ class Calibration():
self.__read_calibration_file()
+ self.mask = None
+
def __read_calibration_file(self):
conctr = []
@@ -196,12 +306,12 @@ class Calibration():
if excluded_points:
self.curve.excluded = excluded_points
- mask = [False if i + 1 in self.curve.excluded else True for i in range(len(self.calibrant_data))]
+ self.mask = [False if i + 1 in self.curve.excluded else True for i in range(len(self.calibrant_data))]
else:
self.curve.excluded = None
- mask = [True for i in range(len(self.calibrant_data))]
+ self.mask = [True for i in range(len(self.calibrant_data))]
- self.curve.calibrate(x_data=self.calibrant_data['area_average'].values[mask], y_data=self.calibrant_data['concentration'].values[mask])
+ self.curve.calibrate(x_data=self.calibrant_data['area_average'].values[self.mask], y_data=self.calibrant_data['concentration'].values[self.mask])
with open(self.cal_file, 'r', errors = 'ignore') as file:
lines = file.readlines()
diff --git a/doc/defs/visualising.html b/doc/defs/BTOF.html
similarity index 82%
rename from doc/defs/visualising.html
rename to doc/defs/BTOF.html
index 3e93905..6aaf9a9 100644
--- a/doc/defs/visualising.html
+++ b/doc/defs/BTOF.html
@@ -4,8 +4,8 @@
-defs.visualising API documentation
-
+defs.BTOF API documentation
+
@@ -24,9 +24,10 @@ hljs.highlightAll();
-
Module defs.visualising
+
Module defs.BTOF
+
An empty docstring
@@ -35,50 +36,6 @@ hljs.highlightAll();
-
Classes
-
-
-class HelloWorld
-(word='Hellow')
-
-
-
The docstring for this class
-
Description: Bla bla bla
-
Attributes
-
-
word : str
-
A word
-
-
Methods
-
init: HelloWorld
-The class constructor
-
-
-Expand source code
-
-
class HelloWorld():
-
- """The docstring for this class
-
- Description: Bla bla bla
-
- Attributes
- ----------
- word: str
- A word
-
- Methods
- -------
- __init__: HelloWorld
- The class constructor
- """
-
- def __init__(self, word = "Hellow"):
-
- self.word = word
A list of integers indicating which samples to consider
+
on_time_interval : [pd.Timestamp]
+
A list with two elements containing the pd.Timestamp objects when the VOC flow was switched on and off, respectively.
max_samples : int
The highest sample available in the data
data : pandas.DataFrame
@@ -282,23 +284,27 @@ hljs.highlightAll();
----------
data_source_file: str
A string pointing to the data source file
- flows_source_file: str
+ flow_source_file: str
A string pointing to the flow data source file
sample_number_list: [int]
A list of integers indicating which samples to consider
+ on_time_interval: [pd.Timestamp]
+ A list with two elements containing the pd.Timestamp objects when the VOC flow was switched on and off, respectively.
max_samples: int
The highest sample available in the data
data: pandas.DataFrame
A dataframe containing all the data
"""
- def __init__(self, data_source_file, flows_source_file, sample_number_list, data_separator = ',') -> None:
+ def __init__(self, data_source_file, flow_source_file, smpl_number_list, on_time_interval, data_separator = ',') -> None:
self.data_source_file = data_source_file
- self.flows_source_file = flows_source_file
- self.sample_number_list = sample_number_list
+ self.flow_source_file = flow_source_file
+ self.smpl_number_list = smpl_number_list
+ self.on_time_interval = on_time_interval
self.__data_separator = data_separator
+
with open(self.data_source_file, 'r', errors = 'ignore') as file:
for i, line in enumerate(file.readlines()):
if i == 17:
@@ -310,11 +316,11 @@ hljs.highlightAll();
self.max_samples = max_sample_number
self.data = None
- def __sample_number_list_check(self, sample_number_list) -> None:
+ def __smpl_number_list_check(self, smpl_number_list) -> None:
# TODO: implement a similar check for the flow data file!
- last_sample = sample_number_list[-1]
+ last_sample = smpl_number_list[-1]
if last_sample > self.max_samples:
raise IndexError("The last sample(s) are out of range of the number of recorded samples")
@@ -331,32 +337,29 @@ hljs.highlightAll();
# TODO make this work also for a specific sample!
# TODO convert everything is SI units, making live a lot easier...
- self.__sample_number_list_check(self.sample_number_list)
+ self.__smpl_number_list_check(self.smpl_number_list)
+ # Quantities of interest:
numbers_of_sample = [] # (-)
+ starting_times = [] # (timestamp)
+ sampling_times = [] # (s)
number_concentrations = [] # (#/m^3)
length_concentrations = [] # (m/m^3)
surface_concentrations = [] # (m^2/m^3)
volume_concentrations = [] # (m^3/m^3)
aerosol_densities = [] # (kg/m^3)
mass_concentrations = [] # (kg/m^3)
- starting_times = [] # (s)
- stopping_times = [] # (s)
- sampling_times = [] # (s)
- dry_air_flows = [] # (m^3/s)
- species_flows = [] # (m^3/s)
- ozone_flows = [] # (m^3/s)
- total_flows = None # (m^3/s)
- cpc_flows = [] # (m^3/s)
+ filter_flows = [] # (m^3/s)
mass_production_rate = None # (kg/s)
mass_produced = None # (kg)
cumulative_mass_produced = None # (kg)
-
- for sample_number in self.sample_number_list:
+ for sample_number in self.smpl_number_list:
sample = OFRSample(source_file=self.data_source_file, sample_number = sample_number, data_separator = self.__data_separator)
numbers_of_sample.append(sample_number)
+ starting_times.append(sample.start_time)
+
number_concentrations.append(sample.Count.integrate())
length_concentrations.append(sample.Length.integrate())
surface_concentrations.append(sample.Surface.integrate())
@@ -364,14 +367,11 @@ hljs.highlightAll();
aerosol_densities.append(sample.SOA_density)
mass_concentrations.append(sample.Mass.integrate())
- starting_times.append((sample_number - 1) * sample.sampling_time)
- stopping_times.append(sample_number * sample.sampling_time)
-
sampling_times.append(sample.sampling_time)
out_flag = False
- with open(self.flows_source_file, 'r', errors='ignore') as file:
+ with open(self.flow_source_file, 'r', errors='ignore') as file:
j = 1
@@ -391,11 +391,8 @@ hljs.highlightAll();
while i < int(items[-1]):
- if j in self.sample_number_list:
- dry_air_flows.append(float(items[0]) * 6E-4)
- species_flows.append(float(items[1]) * 6E-4)
- ozone_flows.append(float(items[2]) * 6E-4)
- cpc_flows.append(float(items[3]) * 6E-4)
+ if j in self.smpl_number_list:
+ filter_flows.append(float(items[0]) * (1E-3) / 60)
i += 1
j += 1
@@ -403,33 +400,27 @@ hljs.highlightAll();
else:
continue
- total_flows = [(flow_1 + flow_2 + flow_3) for flow_1, flow_2, flow_3 in zip(dry_air_flows, species_flows, ozone_flows)]
- mass_production_rate = [(mass * flow) for mass, flow in zip(mass_concentrations, total_flows)]
+ mass_production_rate = [(mass * flow) for mass, flow in zip(mass_concentrations, filter_flows)]
mass_produced = [(mpr * duration) for mpr, duration in zip(mass_production_rate, sampling_times)]
cumulative_mass_produced = np.cumsum(np.array(mass_produced)).tolist()
self.data = pd.DataFrame({'Sample (-)': numbers_of_sample,
+ 'Starting Time (-)': starting_times,
+ 'Sampling Time (s)': sampling_times,
'Number (#/m^3)': number_concentrations,
'Length (m/m^3)': length_concentrations,
'Surface (m^2/m^3)': surface_concentrations,
'Volume (m^3/m^3)': volume_concentrations,
'Density (kg/m^3)': aerosol_densities,
'Mass (kg/m^3)': mass_concentrations,
- 'Start (s)': starting_times,
- 'Stop (s)': stopping_times,
- 'Duration (s)': sampling_times,
- 'Dry Air Flow (m^3/s)': dry_air_flows,
- 'Species Flow (m^3/s)': species_flows,
- 'Ozone Flow (m^3/s)': ozone_flows,
- 'Total Flow (m^3/s)': total_flows,
- 'CPC Flow (m^3/s)': cpc_flows,
+ 'Filter Flow (m^3/s)': filter_flows,
'Mass Production Rate (kg/s)': mass_production_rate,
'Mass Produced (kg)': mass_produced,
'Cumulative Mass Produced (kg)': cumulative_mass_produced})
Methods
-
+
def GetData(self) ‑> None
@@ -439,7 +430,7 @@ hljs.highlightAll();
-
+
class OFRSample(source_file, sample_number, data_separator=',')
@@ -451,6 +442,8 @@ hljs.highlightAll();
number : int
The sample number
+
start_time : pd.datetime
+
The starting time of the sample recording
sourcefile : str
The path to the data file
headlines : int
@@ -485,6 +478,8 @@ hljs.highlightAll();
number: int
The sample number
+ start_time: pd.datetime
+ The starting time of the sample recording
sourcefile: str
The path to the data file
headlines: int
@@ -513,8 +508,17 @@ hljs.highlightAll();
midpoints = []
counts = []
+ date_line = None
+ time_line = None
+
with open(self.source_file, 'r', errors = 'ignore') as file:
for i, line in enumerate(file.readlines()):
+ if i == 18:
+ date_line = line
+ continue
+ if i == 19:
+ time_line = line
+ continue
if i == 20:
data_flag = True
continue
@@ -548,6 +552,16 @@ hljs.highlightAll();
self.sampling_time = self.scan_up_time + self.scan_down_time
data_lines.pop(-1)
+ # obtaining the starting time
+ raw_date_items = date_line.split(self.__data_separator)
+ raw_time_items = time_line.split(self.__data_separator)
+ del raw_date_items[0]
+ del raw_time_items[0]
+
+ raw_date = raw_date_items[(self.sample_number - 1) * 2].splitlines()[0]
+ raw_time = raw_time_items[(self.sample_number - 1) * 2].splitlines()[0]
+ self.start_time = pd.to_datetime(raw_date + ' ' + raw_time, format=r'%m/%d/%y %H:%M:%S')
+
for line in data_lines:
items = line.split(self.__data_separator)
local_diameter_midpoint = float(items[0])
@@ -563,7 +577,6 @@ hljs.highlightAll();
self.lower_diameter_bound = self.lower_diameter_bound * 1E-9
self.upper_diameter_bound = self.upper_diameter_bound * 1E-9
-
# Input data from file is in (nm)
return [np.array(midpoints), np.array(counts)]
@@ -575,6 +588,7 @@ hljs.highlightAll();
# Sample information
self.source_file = source_file # where the data is stored, i.e. "C:/Users/tschan_d/..."
self.sample_number = sample_number # sample number, i.e. 33
+ self.start_time = None
self.__data_separator = data_separator
self.scan_up_time = None # Time to scan up the voltage range in DMA, in (s)
@@ -621,25 +635,25 @@ hljs.highlightAll();
An object representing a calibration curve for TOC, IC or NPOC Measurements.
+
Description
+
This object instanciates a class that relates the area $A$ measured in the TOC Analyser to the concentration $c$ used in the calibration solutions. The calibration curve is affine, $c=aA+b$ where $a$ and $b$ denote slope and intercept, respectively, of the affine map. This object can be used to represent linear calibration curves for TC (total carbon)-, IC (inorganic carbon)- and NPOC (non-purgeable organic carbon) measurements.
+
Attributes
+
+
exists : Bool
+
This value is set to True if a calibration curve is already available, else it is set to False.
+
slope : float
+
Value of the slope for the calibration of concentration (dependent variable) with measured area (independent variable).
+
intercep : float
+
Value of the intercep for the calibration of concentration (dependent variable) with measured area (independent variable).
+
R2_value : float
+
The coefficient of determination of the calibration curve. For a good calibration curve, this should be above 0.995.\
+
excluded : list
+
A list of integers denoting the samples in the calibrant data that are excluded.
+
lower_bound : float
+
The lower bound value for which this calibration curve can be used.
+
upper_bound : float
+
The upper bound value for which this calibration curve can be used.
+
computed_on : pd.Timestamp
+
A timestamp indicating the time at which the calibration curve was computed.
+
+
+
+Expand source code
+
+
class CalibrationCurve():
+
+ r"""An object representing a calibration curve for TOC, IC or NPOC Measurements.
+
+ Description
+ -----------
+ This object instanciates a class that relates the area $A$ measured in the TOC Analyser to the concentration $c$ used in the calibration solutions. The calibration curve is affine, $c=aA+b$ where $a$ and $b$ denote slope and intercept, respectively, of the affine map. This object can be used to represent linear calibration curves for TC (total carbon)-, IC (inorganic carbon)- and NPOC (non-purgeable organic carbon) measurements.
+
+ Attributes
+ ----------
+ exists: Bool
+ This value is set to `True` if a calibration curve is already available, else it is set to `False`.
+ slope: float
+ Value of the slope for the calibration of concentration (dependent variable) with measured area (independent variable).
+ intercep: float
+ Value of the intercep for the calibration of concentration (dependent variable) with measured area (independent variable).
+ R2_value: float
+ The coefficient of determination of the calibration curve. For a good calibration curve, this should be above 0.995.\
+ excluded: list
+ A list of integers denoting the samples in the calibrant data that are excluded.
+ lower_bound: float
+ The lower bound value for which this calibration curve can be used.
+ upper_bound: float
+ The upper bound value for which this calibration curve can be used.
+ computed_on: pd.Timestamp
+ A timestamp indicating the time at which the calibration curve was computed.
+ """
+
+ def __init__(self):
+
+ # initialises all member variables
+
+ self.exists = False
+ self.slope = None
+ self.intercept = None
+ self.R2_value = None
+ self.excluded = None
+ self.lower_bound = None
+ self.upper_bound = None
+ self.computed_on = None
+
+ def __linear_function(self, x_values, a, b):
+
+ r"""Computes the value of the calibration curve for the passed parameters.
+
+ Description
+ -----------
+ Computes the affine map, $c=aA+b$, given data $A$ and parameters $a$ (slope) and $b$ (intercept).
+
+ Parameters
+ ----------
+ x_values: array-like
+ An array-like (array or list) object of length $n$ containing the data for the independent variable that are to be mapped.
+ a: float
+ The slope of the calibration curve.
+ b: float
+ The intercept of the calibration curve.
+
+ Returns
+ -------
+ y_values: np.ndarray
+ An array (of size $n$) containing the image under the affine map of `x_values`.
+ """
+
+ if not isinstance(x_values, np.ndarray):
+ x_values = np.array(x_values)
+
+ return a * x_values + b
+
+ def __optimise(self, x_data, y_data, p0):
+ return curve_fit(self.__linear_function, x_data, y_data, p0)
+
+ def __check_exist(self):
+
+ if self.exists == True:
+ return True
+ else:
+ return False
+
+ def __check(self):
+
+ if self.__check_exist():
+ warnings.warn("Calibration alreadyt exists, will be overwritten...")
+ else:
+ return
+
+ def calibrate(self, x_data, y_data):
+
+ """The docstring for this method
+
+ Description
+ -----------
+ Bla Bla Bla
+
+ Parameters
+ ----------
+ x_data: np.ndarray
+ Bla bla bla
+ y_data: np.ndarray
+ Bla Bla Bla
+ """
+
+ self.__check()
+
+ parameters, _ = self.__optimise(x_data=x_data, y_data=y_data, p0=[10, 0.1])
+
+ self.exists = True
+ self.slope = parameters[0]
+ self.intercept = parameters[1]
+ self.R2_value = compute_R2(y_true = y_data, y_est = self.__linear_function(x_values=x_data, a = parameters[0], b=parameters[1]))
+ self.lower_bound = np.min(y_data)
+ self.upper_bound = np.max(y_data)
+ self.computed_on = pd.to_datetime(datetime.datetime.now())
+
+ def compute(self, sample_areas):
+ return self.__linear_function(x_values=sample_areas, a = self.slope, b = self.intercept)