Repository: JuBiotech/bletl (language: python)

Issue:
Add pre-commit to normalize code style



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 0000000..b52d4af
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,14 @@
+name: pre-commit
+
+on:
+  pull_request:
+  push:
+    branches: [main]
+
+jobs:
+  pre-commit:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-python@v2
+    - uses: pre-commit/action@v2.0.0
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a0d6987..def55b3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
         python-version: 3.7
     - name: Install dependencies
       run: |
-        pip install -e . 
+        pip install -e .
         pip install -r requirements-dev.txt
     - name: Test with pytest
       run: |
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 27204a1..a350777 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,7 +15,7 @@ build-bletl:
     paths:
       - ./*.whl
     expire_in: 30 days
- 
+
 
 test-bletl-py38:
   stage: test
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..02bebb9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,21 @@
+# Ignore all docs content
+# Ignore CSV, ZIP and JSON files
+exclude: docs/|\.(csv|zip|json)$
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+  rev: v4.2.0
+  hooks:
+    - id: check-merge-conflict
+    - id: check-toml
+    - id: check-yaml
+    - id: end-of-file-fixer
+    - id: trailing-whitespace
+- repo: https://github.com/PyCQA/isort
+  rev: 5.10.1
+  hooks:
+    - id: isort
+      name: isort
+- repo: https://github.com/psf/black
+  rev: 22.3.0
+  hooks:
+    - id: black
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 40513e4..0f4324e 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -12,7 +12,7 @@ sphinx:
 # Optionally set the version of Python and requirements required to build your docs
 python:
   version: 3.8
-  install:  
+  install:
     - requirements: docs/requirements.txt
     - requirements: requirements.txt
     - method: pip
diff --git a/bletl/__init__.py b/bletl/__init__.py
index 135df7e..18079ad 100644
--- a/bletl/__init__.py
+++ b/bletl/__init__.py
@@ -2,15 +2,21 @@
 BioLector files, applying calibration transformations and representing them in a standardized
 format.
 """
-from . types import (
-    BioLectorModel, BLData, BLDParser, FilterTimeSeries,
-    LotInformationError, LotInformationMismatch, InvalidLotNumberError,
-    NoMeasurementData, IncompatibleFileError, LotInformationNotFound
-)
-from . core import parse, get_parser, parsers
 from . import utils
-from . heuristics import find_do_peak
-from . splines import get_crossvalidated_spline
-
+from .core import get_parser, parse, parsers
+from .heuristics import find_do_peak
+from .splines import get_crossvalidated_spline
+from .types import (
+    BioLectorModel,
+    BLData,
+    BLDParser,
+    FilterTimeSeries,
+    IncompatibleFileError,
+    InvalidLotNumberError,
+    LotInformationError,
+    LotInformationMismatch,
+    LotInformationNotFound,
+    NoMeasurementData,
+)
 
-__version__ = '1.1.0'
+__version__ = "1.1.0"
diff --git a/bletl/core.py b/bletl/core.py
index 54a1601..6531da1 100644
--- a/bletl/core.py
+++ b/bletl/core.py
@@ -1,25 +1,35 @@
 """Specifies the base types for parsing and representing BioLector CSV files."""
 import abc
-from collections.abc import Iterable
 import enum
-import numpy
 import os
-import pandas
 import pathlib
 import typing
-import urllib.request
 import urllib.error
+import urllib.request
 import warnings
+from collections.abc import Iterable
 from typing import Optional, Union
 
-from . import parsing
-from . types import BLData, BLDParser, FilterTimeSeries, BioLectorModel, LotInformationError, InvalidLotNumberError, LotInformationMismatch, LotInformationNotFound, IncompatibleFileError, NoMeasurementData
-from . import utils
+import numpy
+import pandas
 
+from . import parsing, utils
+from .types import (
+    BioLectorModel,
+    BLData,
+    BLDParser,
+    FilterTimeSeries,
+    IncompatibleFileError,
+    InvalidLotNumberError,
+    LotInformationError,
+    LotInformationMismatch,
+    LotInformationNotFound,
+    NoMeasurementData,
+)
 
 parsers = {
-    (BioLectorModel.BL1, '3.3') : parsing.bl1.BioLector1Parser,
-    (BioLectorModel.BLPro, '0.0.0') : parsing.blpro.BioLectorProParser,
+    (BioLectorModel.BL1, "3.3"): parsing.bl1.BioLector1Parser,
+    (BioLectorModel.BLPro, "0.0.0"): parsing.blpro.BioLectorProParser,
 }
 
 
@@ -45,32 +55,34 @@ def get_parser(filepath: Union[str, pathlib.Path]) -> BLDParser:
         # Note:
         # BioLector II files are encoded as UTF8-BOM
         # BioLector Pro files are encoded as UTF-8
-        with open(filepath, 'r', encoding='utf-8') as f:
+        with open(filepath, "r", encoding="utf-8") as f:
             lines = f.readlines()
     except UnicodeDecodeError:
-        with open(filepath, 'r', encoding='latin-1') as f:
+        with open(filepath, "r", encoding="latin-1") as f:
             lines = f.readlines()
 
     model = None
     version = None
 
-    if '=====' in lines[0]:
-        if 'parameters' in lines[0]:
-            raise IncompatibleFileError('It seems like this file has been edited by the BioLection software. Please provide the raw data file.')
+    if "=====" in lines[0]:
+        if "parameters" in lines[0]:
+            raise IncompatibleFileError(
+                "It seems like this file has been edited by the BioLection software. Please provide the raw data file."
+            )
         model = BioLectorModel.BLPro
         for line in lines:
-            if line.startswith('[file_version_number]'):
-                version = line.split(']')[1].strip()
+            if line.startswith("[file_version_number]"):
+                version = line.split("]")[1].strip()
                 break
-    elif 'FILENAME;' in lines[0]:
+    elif "FILENAME;" in lines[0]:
         model = BioLectorModel.BL1
         version = lines[2][13:-2]
     if not model:
-        raise ValueError('Unable to detect the BioLector model from the file contents.')
-    if not version :
-        raise ValueError('Unable to detect the file version from the contents.')
+        raise ValueError("Unable to detect the BioLector model from the file contents.")
+    if not version:
+        raise ValueError("Unable to detect the file version from the contents.")
     if not (model, version) in parsers:
-        raise NotImplementedError(f'Unsupported {model} file version: {version}')
+        raise NotImplementedError(f"Unsupported {model} file version: {version}")
 
     # select a parser for this version
     parser_cls = parsers[(model, version)]
@@ -78,16 +90,16 @@ def get_parser(filepath: Union[str, pathlib.Path]) -> BLDParser:
 
 
 def _parse(
-    filepath:str,
-    drop_incomplete_cycles:bool,
-    lot_number:int,
-    temp:int,
-    cal_0:float=None,
-    cal_100:float=None,
-    phi_min:float=None,
-    phi_max:float=None,
-    pH_0:float=None,
-    dpH:float=None,
+    filepath: str,
+    drop_incomplete_cycles: bool,
+    lot_number: int,
+    temp: int,
+    cal_0: float = None,
+    cal_100: float = None,
+    phi_min: float = None,
+    phi_max: float = None,
+    pH_0: float = None,
+    dpH: float = None,
 ) -> BLData:
     """Parses a raw BioLector CSV file into a BLData object.
 
@@ -140,15 +152,15 @@ def _parse(
 def parse(
     filepaths,
     *,
-    drop_incomplete_cycles:bool=True,
-    lot_number:int=None,
-    temp:int=None,
-    cal_0:float=None,
-    cal_100:float=None,
-    phi_min:float=None,
-    phi_max:float=None,
-    pH_0:float=None,
-    dpH:float=None,
+    drop_incomplete_cycles: bool = True,
+    lot_number: int = None,
+    temp: int = None,
+    cal_0: float = None,
+    cal_100: float = None,
+    phi_min: float = None,
+    phi_max: float = None,
+    pH_0: float = None,
+    dpH: float = None,
 ) -> BLData:
     """Parses a raw BioLector CSV file into a BLData object and applies calibration.
 
@@ -196,43 +208,50 @@ def parse(
     if isinstance(filepaths, Iterable) and not isinstance(filepaths, str):
         fragments = []
         for filepath in filepaths:
-            fragment = _parse(filepath, drop_incomplete_cycles, lot_number, temp, cal_0, cal_100, phi_min, phi_max, pH_0, dpH)
+            fragment = _parse(
+                filepath,
+                drop_incomplete_cycles,
+                lot_number,
+                temp,
+                cal_0,
+                cal_100,
+                phi_min,
+                phi_max,
+                pH_0,
+                dpH,
+            )
             fragments.append(fragment)
-        start_times = [
-            fragment.metadata['date_start']
-            for fragment in fragments
-        ]
-        
+        start_times = [fragment.metadata["date_start"] for fragment in fragments]
+
         data = fragments[0]
 
         # iterate over all DataFrame-attributes
         for attr, stack in data.__dict__.items():
             if isinstance(stack, pandas.DataFrame):
                 # time/cycle aware concatenation of all fragments
-                fragment_frames = [
-                    getattr(fragment, attr)
-                    for fragment in fragments
-                ]
+                fragment_frames = [getattr(fragment, attr) for fragment in fragments]
                 stack = utils._concatenate_fragments(fragment_frames, start_times)
                 setattr(data, attr, stack)
-        
+
         # also iterate over FilterTimeSeries and concatenate them
         if len(fragments) > 1:
             for fs in data.keys():
                 # already increment the time here, because utils._concatenate_fragments won't do that
-                conc_times = utils._concatenate_fragments([
-                   f[fs].time + (fragment_start - start_times[0]).total_seconds() / 3600
-                   for f, fragment_start in zip(fragments, start_times)
-                ], start_times)
-                conc_values = utils._concatenate_fragments([
-                    f[fs].value
-                    for f in fragments
-                ], start_times)
+                conc_times = utils._concatenate_fragments(
+                    [
+                        f[fs].time + (fragment_start - start_times[0]).total_seconds() / 3600
+                        for f, fragment_start in zip(fragments, start_times)
+                    ],
+                    start_times,
+                )
+                conc_values = utils._concatenate_fragments([f[fs].value for f in fragments], start_times)
                 # overwrite with concatenated FilterTimeSeries
                 data[fs] = FilterTimeSeries(conc_times, conc_values)
 
-        data.metadata['date_end'] = fragments[-1].metadata['date_end']
+        data.metadata["date_end"] = fragments[-1].metadata["date_end"]
     else:
-        data = _parse(filepaths, drop_incomplete_cycles, lot_number, temp, cal_0, cal_100, phi_min, phi_max, pH_0, dpH)
+        data = _parse(
+            filepaths, drop_incomplete_cycles, lot_number, temp, cal_0, cal_100, phi_min, phi_max, pH_0, dpH
+        )
 
     return data
diff --git a/bletl/features.py b/bletl/features.py
index e30f679..89db376 100644
--- a/bletl/features.py
+++ b/bletl/features.py
@@ -1,24 +1,25 @@
 import abc
 import inspect
 import logging
-import numpy
-import pandas
 import time
 import typing
-import tsfresh
+
 import fastprogress
+import numpy
+import pandas
+import tsfresh
 
-from . types import BLData, FilterTimeSeries
 from . import splines
+from .types import BLData, FilterTimeSeries
 
 _log = logging.getLogger(__file__)
 
 
 class Extractor(abc.ABC):
-    """ Common base class for all feature extractors. """  
+    """Common base class for all feature extractors."""
 
     def get_methods(self) -> typing.Dict[str, typing.Callable[[numpy.ndarray, numpy.ndarray], float]]:
-        """ Returns the extration methods by name.
+        """Returns the extration methods by name.
 
         All classmethods that are named `extract_*` are considered feature
         extration methods.
@@ -36,10 +37,10 @@ def get_methods(self) -> typing.Dict[str, typing.Callable[[numpy.ndarray, numpy.
 
 
 class StatisticalFeatureExtractor(Extractor):
-    """ Class for statistical feature extraction."""
+    """Class for statistical feature extraction."""
 
     def extract_mean(self, x, y):
-        """ Extracts the mean of y.
+        """Extracts the mean of y.
 
         Parameters
         ----------
@@ -47,16 +48,16 @@ def extract_mean(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             mean
         """
         return numpy.mean(y)
-    
+
     def extract_min(self, x, y):
-        """ Extracts the minimum of y.
+        """Extracts the minimum of y.
 
         Parameters
         ----------
@@ -64,16 +65,16 @@ def extract_min(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             minimum
         """
         return numpy.min(y)
-    
+
     def extract_time_min(self, x, y):
-        """ Extracts the time at the minimum of y.
+        """Extracts the time at the minimum of y.
 
         Parameters
         ----------
@@ -81,16 +82,16 @@ def extract_time_min(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             time at the minimum
         """
         return x[numpy.argmin(y)]
-    
+
     def extract_max(self, x, y):
-        """ Extracts the maximum of y.
+        """Extracts the maximum of y.
 
         Parameters
         ----------
@@ -98,16 +99,16 @@ def extract_max(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             maximum
         """
         return numpy.max(y)
-    
+
     def extract_time_max(self, x, y):
-        """ Extracts the time at the maximum of y.
+        """Extracts the time at the maximum of y.
 
         Parameters
         ----------
@@ -115,16 +116,16 @@ def extract_time_max(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             time at the maximum
         """
         return x[numpy.argmax(y)]
-    
+
     def extract_span(self, x, y):
-        """ Extracts the span between minimum and maximum.
+        """Extracts the span between minimum and maximum.
 
         Parameters
         ----------
@@ -132,16 +133,16 @@ def extract_span(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             span
         """
         return numpy.max(y) - numpy.min(y)
-    
+
     def extract_median(self, x, y):
-        """ Extracts the median of y.
+        """Extracts the median of y.
 
         Parameters
         ----------
@@ -149,16 +150,16 @@ def extract_median(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             median
         """
         return numpy.median(y)
-    
+
     def extract_stan_dev(self, x, y):
-        """ Extracts the standard deviation of y.
+        """Extracts the standard deviation of y.
 
         Parameters
         ----------
@@ -166,7 +167,7 @@ def extract_stan_dev(self, x, y):
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
@@ -176,11 +177,11 @@ def extract_stan_dev(self, x, y):
 
 
 class DOFeatureExtractor(Extractor):
-    """ Class for feature extraction of dissolved oxygen"""
-    
+    """Class for feature extraction of dissolved oxygen"""
+
     def extract_peak(self, x, y):
-        """ Extracts the duration of DO < 5
-        
+        """Extracts the duration of DO < 5
+
         Returns
         -------
         result : float
@@ -189,32 +190,32 @@ def extract_peak(self, x, y):
         tmp = 0
         boolean = False
         time = 0
-        #if values are under 5, the time spans are calculated and added together
+        # if values are under 5, the time spans are calculated and added together
         for i in range(len(y)):
             if y[i] < 5 and boolean is False:
                 boolean = True
                 tmp = i
-            if y[i] > 5 and boolean is True: 
+            if y[i] > 5 and boolean is True:
                 boolean = False
                 time = time + x[i] - x[tmp]
         if boolean is True:
-            time = time + x[len(y)-1] - x[tmp]
+            time = time + x[len(y) - 1] - x[tmp]
         return time
-    
-    
+
+
 class BSFeatureExtractor(Extractor):
-    """ Class for feature extraction of backscatter. """
-    
+    """Class for feature extraction of backscatter."""
+
     def extract_inflection_point_t(self, x, y):
-        """ Extracts value at the turning point.
-        
+        """Extracts value at the turning point.
+
         Parameters
         ----------
         x : numpy.ndarray
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
@@ -223,17 +224,17 @@ def extract_inflection_point_t(self, x, y):
         spline = splines.get_crossvalidated_spline(x, y)
         y_der1 = spline.derivative(1)(x)
         return y[numpy.argmax(y_der1)]
-    
+
     def extract_inflection_point_y(self, x, y):
-        """ Extracts time at the turning point.
-        
+        """Extracts time at the turning point.
+
         Parameters
         ----------
         x : numpy.ndarray
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
@@ -242,58 +243,60 @@ def extract_inflection_point_y(self, x, y):
         spline = splines.get_crossvalidated_spline(x, y)
         y_der1 = spline.derivative(1)(x)
         return x[numpy.argmax(y_der1)]
-    
-    def extract_mue_median(self,x ,y):
-        """ Extracts median of mue at the exponential phase.
-        
+
+    def extract_mue_median(self, x, y):
+        """Extracts median of mue at the exponential phase.
+
         Parameters
         ----------
         x : numpy.ndarray
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
             median
         """
-        #calculate value, which lies in exponential phase
+        # calculate value, which lies in exponential phase
         mid_y = (numpy.max(y) - numpy.min(y)) / 2
-        #get index of a value, which lies in exponential phase
-        mid_i = int(numpy.where(y > mid_y)[0][0]/2)
-        #get mue
-        bsdata = FilterTimeSeries(pandas.DataFrame(x),pandas.DataFrame(y))
-        mue = splines.get_mue(bsdata, method='us')
-        var = 0.1 #allowed variance
-        mue_val = numpy.array(mue.value) 
-        list_values = [mue_val[mid_i]] #list of mue values, which lie in ex. phase 
+        # get index of a value, which lies in exponential phase
+        mid_i = int(numpy.where(y > mid_y)[0][0] / 2)
+        # get mue
+        bsdata = FilterTimeSeries(pandas.DataFrame(x), pandas.DataFrame(y))
+        mue = splines.get_mue(bsdata, method="us")
+        var = 0.1  # allowed variance
+        mue_val = numpy.array(mue.value)
+        list_values = [mue_val[mid_i]]  # list of mue values, which lie in ex. phase
         l_i = mid_i - 1
         r_i = mid_i + 1
-        #checks if mue_val[l_i] lies in variance of mue_val from mid_i, goes left
+        # checks if mue_val[l_i] lies in variance of mue_val from mid_i, goes left
         while l_i >= 0 and mue_val[l_i] < mue_val[mid_i] + var and mue_val[l_i] > mue_val[mid_i] - var:
             list_values.append(mue_val[l_i])
             l_i = l_i - 1
-        #checks if mue_val[l_i] lies in variance of mue_val from mid_i, goes right
-        while r_i < len(mue_val) and mue_val[r_i] < mue_val[mid_i] + var and mue_val[r_i] > mue_val[mid_i] - var:
-            list_values.append(mue_val[r_i])     
+        # checks if mue_val[l_i] lies in variance of mue_val from mid_i, goes right
+        while (
+            r_i < len(mue_val) and mue_val[r_i] < mue_val[mid_i] + var and mue_val[r_i] > mue_val[mid_i] - var
+        ):
+            list_values.append(mue_val[r_i])
             r_i = r_i + 1
         return numpy.median(list_values)
 
 
 class pHFeatureExtractor(Extractor):
-    """ Class for feature extraction of pH."""
-    
+    """Class for feature extraction of pH."""
+
     def extract_sum_of_reduction(self, x, y):
-        """ Extracts sum of reduction.
-        
+        """Extracts sum of reduction.
+
         Parameters
         ----------
         x : numpy.ndarray
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
@@ -303,17 +306,17 @@ def extract_sum_of_reduction(self, x, y):
         y = spline(x)
         changes = numpy.diff(y)
         return numpy.sum(changes[changes < 0])
-    
+
     def extract_sum_of_increase(self, x, y):
-        """ Extracts sum of increase.
-        
+        """Extracts sum of increase.
+
         Parameters
         ----------
         x : numpy.ndarray
             list of time values
         y : numpy.ndarray
             list of values
-        
+
         Returns
         -------
         result : float
@@ -323,10 +326,11 @@ def extract_sum_of_increase(self, x, y):
         y = spline(x)
         changes = numpy.diff(y)
         return numpy.sum(changes[changes > 0])
-    
-    
+
+
 class TSFreshExtractor:
-    """ Class for feature extraction with tsfresh."""
+    """Class for feature extraction with tsfresh."""
+
     def __init__(self, **kwargs):
         """Creates a TSFreshExtractor object.
 
@@ -338,15 +342,15 @@ def __init__(self, **kwargs):
         """
         self._kwargs = kwargs
         super().__init__()
-    
+
     def _extract(self, data):
-        """ Extracts data with tsfresh.
+        """Extracts data with tsfresh.
 
         Parameters
         ----------
         data : pandas.DataFrame
             DataFrame of a filterset
-        
+
         Returns
         -------
         result : pandas.DataFrame
@@ -355,16 +359,16 @@ def _extract(self, data):
         kwargs = dict(column_id="id", column_sort="time")
         kwargs.update(self._kwargs)
         return tsfresh.extract_features(data, **kwargs)
-    
-    
+
+
 def from_bldata(
     bldata: BLData,
     extractors: typing.Dict[str, typing.Sequence[Extractor]],
-    last_cycles: typing.Optional[typing.Dict[str, int]]=None,
+    last_cycles: typing.Optional[typing.Dict[str, int]] = None,
     *,
-    take_wells: typing.Optional[typing.Iterable[str]]=None,
+    take_wells: typing.Optional[typing.Iterable[str]] = None,
 ) -> pandas.DataFrame:
-    """ Apply feature extractors to a dataset.
+    """Apply feature extractors to a dataset.
 
     Parameters
     ----------
@@ -396,15 +400,15 @@ def from_bldata(
     wells = tuple(sorted(wells))
 
     extraction_methods = {
-        f'{fs}_{mname}': method
+        f"{fs}_{mname}": method
         for fs, fs_extractors in extractors.items()
         for extractor in fs_extractors
         if not isinstance(extractor, TSFreshExtractor)
-        for mname, method in extractor.get_methods().items() 
+        for mname, method in extractor.get_methods().items()
     }
-    ts_extractors = { 
-        fs : extractor
-        for fs, fs_extractors in extractors.items() 
+    ts_extractors = {
+        fs: extractor
+        for fs, fs_extractors in extractors.items()
         for extractor in fs_extractors
         if isinstance(extractor, TSFreshExtractor)
     }
@@ -419,13 +423,13 @@ def from_bldata(
     narrow = bldata.get_unified_narrow_data(last_cycles=last_cycles)
     narrow = narrow[narrow.well.isin(wells)]
     for fs, ts_extractor in ts_extractors.items():
-        data = pandas.DataFrame(columns=["id","time","x"])
+        data = pandas.DataFrame(columns=["id", "time", "x"])
         data["id"] = narrow["well"]
         data["time"] = narrow["time"]
         data["x"] = narrow[fs]
         _log.info("Applying tsfresh extractor to %s\n", fs)
         df_fs = ts_extractor._extract(data)
         for i, mname in enumerate(df_fs.columns):
-            df_result[f'{fs}_{mname}'] = df_fs.to_numpy()[:,i]
+            df_result[f"{fs}_{mname}"] = df_fs.to_numpy()[:, i]
     _log.info("Extraction finished in: %s minutes", round((time.time() - s_time) / 60, ndigits=1))
     return df_result
diff --git a/bletl/growth.py b/bletl/growth.py
index 2755541..b221972 100644
--- a/bletl/growth.py
+++ b/bletl/growth.py
@@ -1,13 +1,12 @@
 import logging
-import numpy
-from packaging import version
-import scipy.stats
 import typing
 
 import arviz
-
 import calibr8
+import numpy
+import scipy.stats
 from calibr8.utils import at, pm
+from packaging import version
 
 # Use the new ConstantData container if available,
 # because it gives superior computational performance.
@@ -21,19 +20,20 @@
 
 
 class GrowthRateResult:
-    """ Represents the result of applying the µ(t) model to one dataset. """
+    """Represents the result of applying the µ(t) model to one dataset."""
+
     def __init__(
         self,
         *,
-        t_data:numpy.ndarray,
-        t_segments:numpy.ndarray,
-        y:numpy.ndarray,
+        t_data: numpy.ndarray,
+        t_segments: numpy.ndarray,
+        y: numpy.ndarray,
         calibration_model: calibr8.CalibrationModel,
-        switchpoints:typing.Dict[float, str],
-        pmodel:pm.Model,
-        theta_map:dict,
+        switchpoints: typing.Dict[float, str],
+        pmodel: pm.Model,
+        theta_map: dict,
     ):
-        """ Creates a result object of a growth rate analysis.
+        """Creates a result object of a growth rate analysis.
 
         Parameters
         ----------
@@ -62,83 +62,75 @@ def __init__(
 
     @property
     def t_data(self) -> numpy.ndarray:
-        """ Vector of data timepoints. """
+        """Vector of data timepoints."""
         return self._t_data
 
     @property
     def t_segments(self) -> numpy.ndarray:
-        """ Vector of growth rate segment time mid-points. """
+        """Vector of growth rate segment time mid-points."""
         return self._t_segments
 
     @property
     def y(self) -> numpy.ndarray:
-        """ Vector of backscatter observations. """
+        """Vector of backscatter observations."""
         return self._y
 
     @property
     def switchpoints(self) -> typing.Dict[float, str]:
-        """ Dictionary (by time) of known and detected switchpoints. """
+        """Dictionary (by time) of known and detected switchpoints."""
         return self._switchpoints
 
     @property
     def known_switchpoints(self) -> typing.Tuple[float]:
-        """ Time values of previously known switchpoints in the model. """
-        return tuple(
-            t
-            for t, label in self.switchpoints.items()
-            if label != 'detected'
-        )
+        """Time values of previously known switchpoints in the model."""
+        return tuple(t for t, label in self.switchpoints.items() if label != "detected")
 
     @property
     def detected_switchpoints(self) -> typing.Tuple[float]:
-        """ Time values of switchpoints that were autodetected from the fit. """
-        return tuple(
-            t
-            for t, label in self.switchpoints.items()
-            if label == 'detected'
-        )
+        """Time values of switchpoints that were autodetected from the fit."""
+        return tuple(t for t, label in self.switchpoints.items() if label == "detected")
 
     @property
     def pmodel(self) -> pm.Model:
-        """ The PyMC model underlying this analysis. """
+        """The PyMC model underlying this analysis."""
         return self._pmodel
 
     @property
     def theta_map(self) -> dict:
-        """ MAP estimate of the model parameters. """
+        """MAP estimate of the model parameters."""
         return self._theta_map
 
     @property
     def idata(self) -> typing.Optional[arviz.InferenceData]:
-        """ ArviZ InferenceData object of the MCMC trace. """
+        """ArviZ InferenceData object of the MCMC trace."""
         return self._idata
 
     @property
     def mu_map(self) -> numpy.ndarray:
-        """ MAP estimate of the growth rates in segments between data points. """
-        return self.theta_map['mu_t']
+        """MAP estimate of the growth rates in segments between data points."""
+        return self.theta_map["mu_t"]
 
     @property
     def x_map(self) -> numpy.ndarray:
-        """ MAP estimate of the biomass curve. """
-        return self.theta_map['X']
+        """MAP estimate of the biomass curve."""
+        return self.theta_map["X"]
 
     @property
     def mu_mcmc(self) -> typing.Optional[numpy.ndarray]:
-        """ Posterior samples of growth rates in segments between data points. """
+        """Posterior samples of growth rates in segments between data points."""
         if not self.idata:
             return None
-        return self.idata.posterior.mu_t.stack(sample=('chain', 'draw')).values.T
+        return self.idata.posterior.mu_t.stack(sample=("chain", "draw")).values.T
 
     @property
     def x_mcmc(self) -> typing.Optional[numpy.ndarray]:
-        """ Posterior samples of biomass curve. """
+        """Posterior samples of biomass curve."""
         if not self.idata:
             return None
-        return self._idata.posterior['X'].stack(sample=('chain', 'draw')).T
+        return self._idata.posterior["X"].stack(sample=("chain", "draw")).T
 
     def sample(self, **kwargs) -> None:
-        """ Runs MCMC sampling with default settings on the growth model.
+        """Runs MCMC sampling with default settings on the growth model.
 
         Parameters
         ----------
@@ -148,7 +140,7 @@ def sample(self, **kwargs) -> None:
         sample_kwargs = dict(
             return_inferencedata=True,
             target_accept=0.95,
-            init='jitter+adapt_diag',
+            init="jitter+adapt_diag",
             start=self.theta_map,
             tune=500,
             draws=500,
@@ -159,8 +151,17 @@ def sample(self, **kwargs) -> None:
         return
 
 
-def _make_random_walk(name:str, *, mu: float=0, sigma:float, nu: float=1, length:int, student_t:bool, initval: numpy.ndarray=None):
-    """ Create a random walk with either a Normal or Student-t distribution.
+def _make_random_walk(
+    name: str,
+    *,
+    mu: float = 0,
+    sigma: float,
+    nu: float = 1,
+    length: int,
+    student_t: bool,
+    initval: numpy.ndarray = None,
+):
+    """Create a random walk with either a Normal or Student-t distribution.
 
     For some PyMC versions and for Student-t distributed random walks,
     the distribution is created from a cumsum of a N-dimensional random variable.
@@ -202,13 +203,15 @@ def _make_random_walk(name:str, *, mu: float=0, sigma:float, nu: float=1, length
 
     if pmversion < version.parse("4.0.0b1") and not student_t:
         # Use the gaussian random walk distribution directly.
-        return pm.GaussianRandomWalk(**{
-            "name": name,
-            "mu": mu,
-            "sigma": sigma,
-            "shape": (length,),
-            initval_kwarg: initval,
-        })
+        return pm.GaussianRandomWalk(
+            **{
+                "name": name,
+                "mu": mu,
+                "sigma": sigma,
+                "shape": (length,),
+                initval_kwarg: initval,
+            }
+        )
     else:
         # Create the random walk manually.
         rv_kwargs = {
@@ -231,7 +234,9 @@ def _make_random_walk(name:str, *, mu: float=0, sigma:float, nu: float=1, length
         return pm.Deterministic(name, at.cumsum(rv_cls(**rv_kwargs)))
 
 
-def _get_smoothed_mu(t: numpy.ndarray, y: numpy.ndarray, cm_cdw: calibr8.CalibrationModel, *, clip=0.5) -> numpy.ndarray:
+def _get_smoothed_mu(
+    t: numpy.ndarray, y: numpy.ndarray, cm_cdw: calibr8.CalibrationModel, *, clip=0.5
+) -> numpy.ndarray:
     """Calculate a rough estimate of the specific growth rate from smoothed observations.
 
     Parameters
@@ -251,22 +256,22 @@ def _get_smoothed_mu(t: numpy.ndarray, y: numpy.ndarray, cm_cdw: calibr8.Calibra
         A vector of specific growth rates.
     """
     # apply moving average to reduce backscatter noise
-    y = numpy.convolve(y, numpy.ones(5)/5, "same")
-    
+    y = numpy.convolve(y, numpy.ones(5) / 5, "same")
+
     # convert to biomass
     X = cm_cdw.predict_independent(y)
-    
+
     # calculate growth rate
     dX = numpy.diff(X)
     dt = numpy.diff(t)
     Xsegment = numpy.mean([X[1:], X[:-1]], axis=0)
     mu = (dX / dt) / Xsegment
-    
+
     # clip growth rate into a realistic interval
     mu = numpy.clip(mu, -clip, clip)
-    
+
     # smooth again to reduce peaking
-    mu = numpy.convolve(mu, numpy.ones(5)/5, "same")
+    mu = numpy.convolve(mu, numpy.ones(5) / 5, "same")
 
     # Replace NaNs that can show up with non-linear calibration models
     mu[numpy.isnan(mu)] = 0
@@ -274,21 +279,21 @@ def _get_smoothed_mu(t: numpy.ndarray, y: numpy.ndarray, cm_cdw: calibr8.Calibra
 
 
 def fit_mu_t(
-    t:typing.Sequence[float],
-    y:typing.Sequence[float],
-    calibration_model:calibr8.CalibrationModel,
+    t: typing.Sequence[float],
+    y: typing.Sequence[float],
+    calibration_model: calibr8.CalibrationModel,
     *,
-    switchpoints:typing.Optional[typing.Union[typing.Sequence[float], typing.Dict[float, str]]]=None,
-    mcmc_samples:int=0,
-    mu_prior:float=0,
-    drift_scale:float,
-    nu:float=5,
-    x0_prior:float=0.25,
-    student_t:typing.Optional[bool]=None,
-    switchpoint_prob:float=0.01,
-    replicate_id:str='unnamed'
+    switchpoints: typing.Optional[typing.Union[typing.Sequence[float], typing.Dict[float, str]]] = None,
+    mcmc_samples: int = 0,
+    mu_prior: float = 0,
+    drift_scale: float,
+    nu: float = 5,
+    x0_prior: float = 0.25,
+    student_t: typing.Optional[bool] = None,
+    switchpoint_prob: float = 0.01,
+    replicate_id: str = "unnamed",
 ):
-    """ Models a vector of growth rates to describe the observations.
+    """Models a vector of growth rates to describe the observations.
 
     A MAP estimate is automatically determined.
 
@@ -333,10 +338,7 @@ def fit_mu_t(
         wraps around the data, model and fitting results
     """
     if not isinstance(switchpoints, dict):
-        switchpoints = {
-            t_switch : 'known'
-            for t_switch in switchpoints or []
-        }
+        switchpoints = {t_switch: "known" for t_switch in switchpoints or []}
     t_switchpoints_known = numpy.sort(list(switchpoints.keys()))
     if student_t is None:
         student_t = len(switchpoints) == 0
@@ -366,53 +368,78 @@ def fit_mu_t(
         "segment": numpy.arange(TS),
     }
     with pm.Model(coords=coords) as pmodel:
-        pmData('known_switchpoints', t_switchpoints_known)
-        pmData('t_data', t_data, dims="timepoint")
-        pmData('t_segments', t_segments, dims="segment")
-        dt = pmData('dt', numpy.diff(t_data), dims="segment")
+        pmData("known_switchpoints", t_switchpoints_known)
+        pmData("t_data", t_data, dims="timepoint")
+        pmData("t_segments", t_segments, dims="segment")
+        dt = pmData("dt", numpy.diff(t_data), dims="segment")
 
         if len(t_switchpoints_known) > 0:
-            _log.info('Creating model with %d switchpoints. StudentT=%b', len(t_switchpoints_known), student_t)
+            _log.info(
+                "Creating model with %d switchpoints. StudentT=%b", len(t_switchpoints_known), student_t
+            )
             # the growth rate vector is fragmented according to t_switchpoints_known
             mu_segments = []
             i_from = 0
             for i, t_switch in enumerate(t_switchpoints_known):
                 i_to = numpy.argmax(t > t_switch)
                 i_len = len(t[i_from:i_to])
-                name = f'mu_phase_{i}'
+                name = f"mu_phase_{i}"
                 slc = slice(i_from, i_to)
                 mu_segments.append(
-                    _make_random_walk(name, mu=mu_prior[slc], sigma=drift_scale, nu=nu, length=i_len, student_t=student_t, initval=mu_guess[slc])
+                    _make_random_walk(
+                        name,
+                        mu=mu_prior[slc],
+                        sigma=drift_scale,
+                        nu=nu,
+                        length=i_len,
+                        student_t=student_t,
+                        initval=mu_guess[slc],
+                    )
                 )
                 i_from += i_len
                 # remember the index to ignore it in potential autodetection
                 c_switchpoints_known.append(i_from)
             # the last segment until the end
             i_len = len(t[i_from:]) - 1
-            name = f'mu_phase_{len(mu_segments)}'
+            name = f"mu_phase_{len(mu_segments)}"
             slc = slice(i_from, None)
             mu_segments.append(
-                _make_random_walk(name, mu_prior[slc], sigma=drift_scale, nu=nu, length=i_len, student_t=student_t, initval=mu_guess[slc])
+                _make_random_walk(
+                    name,
+                    mu_prior[slc],
+                    sigma=drift_scale,
+                    nu=nu,
+                    length=i_len,
+                    student_t=student_t,
+                    initval=mu_guess[slc],
+                )
             )
-            mu_t = pm.Deterministic('mu_t', at.concatenate(mu_segments), dims="segment")
+            mu_t = pm.Deterministic("mu_t", at.concatenate(mu_segments), dims="segment")
         else:
-            _log.info('Creating model without switchpoints. StudentT=%b', len(t_switchpoints_known), student_t)
-            mu_t = _make_random_walk('mu_t', mu=mu_prior, sigma=drift_scale, nu=nu, length=TS, student_t=student_t, initval=mu_guess)
+            _log.info(
+                "Creating model without switchpoints. StudentT=%b", len(t_switchpoints_known), student_t
+            )
+            mu_t = _make_random_walk(
+                "mu_t",
+                mu=mu_prior,
+                sigma=drift_scale,
+                nu=nu,
+                length=TS,
+                student_t=student_t,
+                initval=mu_guess,
+            )
 
-        X0 = pm.Lognormal('X0', mu=numpy.log(x0_prior), sd=1)
+        X0 = pm.Lognormal("X0", mu=numpy.log(x0_prior), sd=1)
         Xt = pm.Deterministic(
-            'X',
-            at.concatenate([
-                X0[None],
-                X0 * pm.math.exp(at.extra_ops.cumsum(mu_t * dt))
-            ]),
+            "X",
+            at.concatenate([X0[None], X0 * pm.math.exp(at.extra_ops.cumsum(mu_t * dt))]),
             dims="timepoint",
         )
         calibration_model.loglikelihood(
             x=Xt,
-            y=pmData('backscatter', y, dims=('timepoint',)),
+            y=pmData("backscatter", y, dims=("timepoint",)),
             replicate_id=replicate_id,
-            dependent_key=calibration_model.dependent_key
+            dependent_key=calibration_model.dependent_key,
         )
 
     # MAP fit
@@ -424,15 +451,17 @@ def fit_mu_t(
         # first CDF values at all mu_t elements
         cdf_evals = []
         for rvname in sorted(theta_map.keys()):
-            if '__diff_' in rvname:
+            if "__diff_" in rvname:
                 rv = pmodel[rvname]
                 # for every µ, find out where it lies in the CDF of the StudentT prior distribution
-                cdf_evals += list(scipy.stats.t.cdf(
-                    x=theta_map[rvname],
-                    loc=rv.distribution.mu.eval(),
-                    scale=rv.distribution.sd.eval(),
-                    df=rv.distribution.nu.eval(),
-                ))
+                cdf_evals += list(
+                    scipy.stats.t.cdf(
+                        x=theta_map[rvname],
+                        loc=rv.distribution.mu.eval(),
+                        scale=rv.distribution.sd.eval(),
+                        df=rv.distribution.nu.eval(),
+                    )
+                )
         cdf_evals = numpy.array(cdf_evals)
         # filter for the elements that lie outside of the [0.005, 0.995] interval
         significance_mask = numpy.logical_or(
@@ -443,8 +472,8 @@ def fit_mu_t(
         # (ignore the first timepoint)
         for c_switch, (t_switch, is_switchpoint) in enumerate(zip(t_data, significance_mask[1:])):
             if is_switchpoint and c_switch not in c_switchpoints_known:
-                switchpoints[t_switch] = 'detected'
-    
+                switchpoints[t_switch] = "detected"
+
     # bundle up all relevant variables into a result object
     result = GrowthRateResult(
         t_data=t_data,
@@ -456,7 +485,11 @@ def fit_mu_t(
         theta_map=theta_map,
     )
     if len(result.detected_switchpoints):
-        _log.info('Detected %d previously unknown switchpoints: %s', len(result.detected_switchpoints), result.detected_switchpoints)
+        _log.info(
+            "Detected %d previously unknown switchpoints: %s",
+            len(result.detected_switchpoints),
+            result.detected_switchpoints,
+        )
     if mcmc_samples > 0:
         result.sample(draws=mcmc_samples)
 
diff --git a/bletl/heuristics.py b/bletl/heuristics.py
index 45c7058..7a88feb 100644
--- a/bletl/heuristics.py
+++ b/bletl/heuristics.py
@@ -2,9 +2,10 @@
 Signal analysis based on simple, deterministic heuristics.
 """
 import logging
-import numpy
 import typing
 
+import numpy
+
 _log = logging.getLogger(__file__)
 
 
@@ -12,14 +13,14 @@ def find_do_peak(
     x: numpy.ndarray,
     y: numpy.ndarray,
     *,
-    delay_a:float,
-    threshold_a:float,
-    delay_b:float,
-    threshold_b:float,
-    initial_delay:float=1
+    delay_a: float,
+    threshold_a: float,
+    delay_b: float,
+    threshold_b: float,
+    initial_delay: float = 1
 ) -> typing.Optional[int]:
     """Finds the index of a DO peak in the inputs [x] and [y].
-    
+
     Parameters
     ----------
     x : numpy.ndarray
diff --git a/bletl/parsing/__init__.py b/bletl/parsing/__init__.py
index a66a900..7f683d5 100644
--- a/bletl/parsing/__init__.py
+++ b/bletl/parsing/__init__.py
@@ -1,2 +1,1 @@
-from . import bl1
-from . import blpro
+from . import bl1, blpro
diff --git a/bletl/parsing/bl1.py b/bletl/parsing/bl1.py
index 7105645..379d357 100644
--- a/bletl/parsing/bl1.py
+++ b/bletl/parsing/bl1.py
@@ -11,15 +11,20 @@
 
 from .. import utils
 from ..types import (
-    BioLectorModel, BLData, BLDParser, FilterTimeSeries,
-    InvalidLotNumberError, LotInformationError,
-    LotInformationMismatch, LotInformationNotFound,
-    NoMeasurementData
+    BioLectorModel,
+    BLData,
+    BLDParser,
+    FilterTimeSeries,
+    InvalidLotNumberError,
+    LotInformationError,
+    LotInformationMismatch,
+    LotInformationNotFound,
+    NoMeasurementData,
 )
 
 
 class BioLector1Parser(BLDParser):
-    def calibrate_with_lot(self, data:BLData, lot_number:Optional[int]=None, temp:Optional[int]=None):
+    def calibrate_with_lot(self, data: BLData, lot_number: Optional[int] = None, temp: Optional[int] = None):
         """Applies calibration.
 
         Parameters
@@ -48,40 +53,52 @@ def calibrate_with_lot(self, data:BLData, lot_number:Optional[int]=None, temp:Op
             When lot information given as parameters is not equal to lot information found in data file.
         """
         if (lot_number is None) ^ (temp is None):
-            raise TypeError('Lot number and temperature should be either left None or be set to an appropriate value.')
-        
+            raise TypeError(
+                "Lot number and temperature should be either left None or be set to an appropriate value."
+            )
+
         if (lot_number is None) and (temp is None):
-            if (data.metadata['lot'] in {'UNKNOWN', 'UNKOWN'}):
+            if data.metadata["lot"] in {"UNKNOWN", "UNKOWN"}:
                 data = self.calibrate_with_parameters(data)
             else:
-                cal_data = fetch_calibration_data(*utils._parse_calibration_info(data.metadata['lot']))
+                cal_data = fetch_calibration_data(*utils._parse_calibration_info(data.metadata["lot"]))
                 if cal_data is None:
                     warnings.warn(
                         "Lot information was found in the CSV file, but the calibration data was not found in the cache and the cache could not be updated. "
-                        "No calibration for pH and DO is applied.", LotInformationNotFound
+                        "No calibration for pH and DO is applied.",
+                        LotInformationNotFound,
                     )
                 data = self.calibrate_with_parameters(data, **cal_data)
 
         if isinstance(lot_number, int) and isinstance(temp, int):
-            if not (data.metadata['lot'] in {'UNKNOWN', 'UNKOWN'}):
-                lot_from_csv, temp_from_csv = utils._parse_calibration_info(data.metadata['lot'])
+            if not (data.metadata["lot"] in {"UNKNOWN", "UNKOWN"}):
+                lot_from_csv, temp_from_csv = utils._parse_calibration_info(data.metadata["lot"])
                 if (lot_number != lot_from_csv) or (temp != temp_from_csv):
                     warnings.warn(
-                        f'The lot information (lot_number={lot_number}, temp={temp}) provided mismatches with '
-                        f'lot information found in the data file (lot_number={lot_from_csv}, temp={temp_from_csv}). ',
-                        LotInformationMismatch
+                        f"The lot information (lot_number={lot_number}, temp={temp}) provided mismatches with "
+                        f"lot information found in the data file (lot_number={lot_from_csv}, temp={temp_from_csv}). ",
+                        LotInformationMismatch,
                     )
             cal_data = fetch_calibration_data(lot_number, temp)
             if cal_data is None:
                 raise LotInformationError(
-                    'Data for the lot information provided was not found in the cached file and we were unable to update it. '
-                    'If you want to proceed without calibration, pass no lot number and temperature'
+                    "Data for the lot information provided was not found in the cached file and we were unable to update it. "
+                    "If you want to proceed without calibration, pass no lot number and temperature"
                 )
             data = self.calibrate_with_parameters(data, **cal_data)
 
         return data
 
-    def calibrate_with_parameters(self, data:BLData, cal_0:float=None, cal_100:float=None, phi_min:float=None, phi_max:float=None, pH_0:float=None, dpH:float=None):
+    def calibrate_with_parameters(
+        self,
+        data: BLData,
+        cal_0: float = None,
+        cal_100: float = None,
+        phi_min: float = None,
+        phi_max: float = None,
+        pH_0: float = None,
+        dpH: float = None,
+    ):
         def process_backscatter(raw_data_df, cycle_ref_df, global_ref):
             """
             Calculation of referenced BS signal:
@@ -91,7 +108,7 @@ def process_backscatter(raw_data_df, cycle_ref_df, global_ref):
             for row in raw_data_df.iterrows():
                 current_cycle = row[0]
                 current_raw_measurements = row[1]
-                current_cycle_ref = cycle_ref_df.loc[current_cycle, 'amp_1']
+                current_cycle_ref = cycle_ref_df.loc[current_cycle, "amp_1"]
                 current_values = (global_ref / current_cycle_ref) * current_raw_measurements
                 BS.loc[current_cycle, :] = current_values
             return BS
@@ -122,50 +139,57 @@ def process_DO(raw_data_df, cal_0, cal_100):
             return DO
 
         if data.measurements.empty:
-            warnings.warn('The data yor are parsing contains no measurement data', NoMeasurementData)
+            warnings.warn("The data yor are parsing contains no measurement data", NoMeasurementData)
             return data
 
         for row in data.filtersets.iterrows():
-            filter_number = row[1]['filter_number']
-            filter_name = row[1]['filter_name']
-            gain = row[1]['gain']
-            ref_value = row[1]['reference_value']
-
-            if filter_name == 'Biomass':
-                raw_bs = data.measurements.xs(filter_number, level='filterset')['amp_1'].unstack()
-                bs_times = data.measurements.xs(filter_number, level='filterset')['time'].unstack()
-                cycle_ref_bs = data.references.xs(filter_number, level='filterset')
+            filter_number = row[1]["filter_number"]
+            filter_name = row[1]["filter_name"]
+            gain = row[1]["gain"]
+            ref_value = row[1]["reference_value"]
+
+            if filter_name == "Biomass":
+                raw_bs = data.measurements.xs(filter_number, level="filterset")["amp_1"].unstack()
+                bs_times = data.measurements.xs(filter_number, level="filterset")["time"].unstack()
+                cycle_ref_bs = data.references.xs(filter_number, level="filterset")
                 bs_values = process_backscatter(raw_bs, cycle_ref_bs, ref_value)
-                data['BS' + f'{gain:.0f}'] = FilterTimeSeries(bs_times, bs_values)
+                data["BS" + f"{gain:.0f}"] = FilterTimeSeries(bs_times, bs_values)
 
-            elif filter_name == 'pH-hc':
+            elif filter_name == "pH-hc":
                 if not None in [pH_0, dpH, phi_min, phi_max]:
-                    raw_ph = data.measurements.xs(filter_number, level='filterset')['phase'].unstack()
-                    ph_times = data.measurements.xs(filter_number, level='filterset')['time'].unstack()
+                    raw_ph = data.measurements.xs(filter_number, level="filterset")["phase"].unstack()
+                    ph_times = data.measurements.xs(filter_number, level="filterset")["time"].unstack()
                     ph_values = process_pH(raw_ph, phi_min, phi_max, pH_0, dpH)
-                    data['pH'] = FilterTimeSeries(ph_times, ph_values)
+                    data["pH"] = FilterTimeSeries(ph_times, ph_values)
                 else:
-                    warnings.warn('Calibration values for pH signal are missing. Skipping calibration.')
+                    warnings.warn("Calibration values for pH signal are missing. Skipping calibration.")
 
-            elif filter_name == 'pO2-hc':
+            elif filter_name == "pO2-hc":
                 if not None in [cal_0, cal_100]:
-                    raw_do = data.measurements.xs(filter_number, level='filterset')['phase'].unstack()
-                    do_times = data.measurements.xs(filter_number, level='filterset')['time'].unstack()
+                    raw_do = data.measurements.xs(filter_number, level="filterset")["phase"].unstack()
+                    do_times = data.measurements.xs(filter_number, level="filterset")["time"].unstack()
                     do_values = process_DO(raw_do, cal_0, cal_100)
-                    data['DO'] = FilterTimeSeries(do_times, do_values)
+                    data["DO"] = FilterTimeSeries(do_times, do_values)
                 else:
-                    warnings.warn('Calibration values for DO signal are missing. Skipping calibration.')
+                    warnings.warn("Calibration values for DO signal are missing. Skipping calibration.")
 
             else:
-                raw_values = data.measurements.xs(filter_number, level='filterset')['amp_1'].unstack()
-                times = data.measurements.xs(filter_number, level='filterset')['time'].unstack()
-                data[filter_name + f'{gain:.0f}'] = FilterTimeSeries(times, raw_values)
+                raw_values = data.measurements.xs(filter_number, level="filterset")["amp_1"].unstack()
+                times = data.measurements.xs(filter_number, level="filterset")["time"].unstack()
+                data[filter_name + f"{gain:.0f}"] = FilterTimeSeries(times, raw_values)
         return data
 
     def parse(
-        self, filepath,
-        lot_number:int=None, temp:int=None,
-        cal_0:float=None, cal_100:float=None, phi_min:float=None, phi_max:float=None, pH_0:float=None, dpH:float=None
+        self,
+        filepath,
+        lot_number: int = None,
+        temp: int = None,
+        cal_0: float = None,
+        cal_100: float = None,
+        phi_min: float = None,
+        phi_max: float = None,
+        pH_0: float = None,
+        dpH: float = None,
     ):
         headerlines, data = split_header_data(filepath)
 
@@ -178,7 +202,7 @@ def parse(
 
         data = BLData(
             model=BioLectorModel.BL1,
-            environment = extract_environment(data, process_parameters, comments),
+            environment=extract_environment(data, process_parameters, comments),
             filtersets=filtersets,
             references=references,
             measurements=measurements,
@@ -187,7 +211,9 @@ def parse(
 
         data.metadata = metadata
 
-        if (not (lot_number is None) and not (temp is None)) or all(p is None for p in [lot_number, temp, cal_0, cal_100, phi_min, phi_max, pH_0, dpH]):
+        if (not (lot_number is None) and not (temp is None)) or all(
+            p is None for p in [lot_number, temp, cal_0, cal_100, phi_min, phi_max, pH_0, dpH]
+        ):
             data = self.calibrate_with_lot(data, lot_number, temp)
         else:
             data = self.calibrate_with_parameters(data, cal_0, cal_100, phi_min, phi_max, pH_0, dpH)
@@ -195,15 +221,15 @@ def parse(
 
 
 def read_header_loglines(dir_incremental):
-    fp_header = pathlib.Path(dir_incremental, 'header.csv')
+    fp_header = pathlib.Path(dir_incremental, "header.csv")
 
-    with open(fp_header, encoding='latin-1') as f:
+    with open(fp_header, encoding="latin-1") as f:
         headerlines = f.readlines()
 
     loglines = []
     for tmpfile in dir_incremental.iterdir():
-        if tmpfile.suffix == '.tmp':
-            with open(tmpfile, 'r', encoding='utf-8') as f:
+        if tmpfile.suffix == ".tmp":
+            with open(tmpfile, "r", encoding="utf-8") as f:
                 loglines += f.readlines()
     return headerlines, loglines
 
@@ -223,7 +249,7 @@ def split_header_data(fp):
     dfraw : pandas.DataFrame
         Data table.
     """
-    with open(fp, 'r', encoding='latin-1') as f:
+    with open(fp, "r", encoding="latin-1") as f:
         lines = f.readlines()
 
     headerlines = []
@@ -234,24 +260,24 @@ def split_header_data(fp):
     for l, line in enumerate(lines):
         if not header_end:
             headerlines.append(line)
-            if line.startswith('READING;WELLNUM'):
+            if line.startswith("READING;WELLNUM"):
                 header_end = l
     datalines = lines[header_end:]
-    datalines[0] = datalines[0].strip() + ';IGNORE\n'
+    datalines[0] = datalines[0].strip() + ";IGNORE\n"
     # append a ; to the header line because some lines contain a trailing ;
-    datalines[0] = datalines[0].strip() + ';IGNORE\n'
+    datalines[0] = datalines[0].strip() + ";IGNORE\n"
 
     # parse the data as a DataFrame
-    dfraw = pandas.read_csv(io.StringIO(''.join(datalines)), sep=';', low_memory=False)
+    dfraw = pandas.read_csv(io.StringIO("".join(datalines)), sep=";", low_memory=False)
 
     # add a cycle column to dfraw
-    reflines = list(dfraw.index[(dfraw['READING'] == 'R') & (dfraw['FILTERSET'] == 1)])
+    reflines = list(dfraw.index[(dfraw["READING"] == "R") & (dfraw["FILTERSET"] == 1)])
     cycles = numpy.zeros((len(dfraw),), dtype=int)
     for c in range(len(reflines)):
         l_start = reflines[c]
         l_end = reflines[c + 1] if len(reflines) < c else len(dfraw)
         cycles[l_start:l_end] = c + 1
-    dfraw['cycle'] = cycles
+    dfraw["cycle"] = cycles
 
     # TODO: convert well ids to BioLector Pro format
     # TODO: create column of well numbers
@@ -260,30 +286,32 @@ def split_header_data(fp):
 
 
 def extract_metadata(headerlines):
-    L4 = headerlines[4].split(';')
-    L6 = headerlines[6].split(';')
+    L4 = headerlines[4].split(";")
+    L6 = headerlines[6].split(";")
 
     metadata = {
-        'filename': headerlines[0].split(';')[1].strip(),
-        'protocol': headerlines[1].split(';')[1].strip(),
-        'file_version': headerlines[2].split(';')[1].strip(),
-        'date_start': datetime.datetime.strptime(' '.join(headerlines[3].split(';')[1:3]).strip(), '%Y-%m-%d %H:%M:%S'),
-        'date_end': datetime.datetime.strptime(' '.join(L4[1:3]), '%Y-%m-%d %H:%M:%S'),
-        'last_reading': L4[5],
-        'timestamp': L4[7].strip(),
-        'device': headerlines[5][7:].strip(),
-        'user': L6[1],
-        'comment': L6[3].strip() if L6[3] != 'no comment available\n' else None,
-        'platetype': headerlines[7].split(';')[1],
-        'lot': headerlines[7].split(';')[3].strip(),
-        'mtp_rows': int(headerlines[8][9:-1]),
-        'mtp_columns': int(headerlines[9][12:-1]),
-        'filtersets': int(headerlines[10].split(';')[1]),
-        'reference_mode': headerlines[10].split(';')[3],
-        'multi_pmt': headerlines[10].split(';')[5],
+        "filename": headerlines[0].split(";")[1].strip(),
+        "protocol": headerlines[1].split(";")[1].strip(),
+        "file_version": headerlines[2].split(";")[1].strip(),
+        "date_start": datetime.datetime.strptime(
+            " ".join(headerlines[3].split(";")[1:3]).strip(), "%Y-%m-%d %H:%M:%S"
+        ),
+        "date_end": datetime.datetime.strptime(" ".join(L4[1:3]), "%Y-%m-%d %H:%M:%S"),
+        "last_reading": L4[5],
+        "timestamp": L4[7].strip(),
+        "device": headerlines[5][7:].strip(),
+        "user": L6[1],
+        "comment": L6[3].strip() if L6[3] != "no comment available\n" else None,
+        "platetype": headerlines[7].split(";")[1],
+        "lot": headerlines[7].split(";")[3].strip(),
+        "mtp_rows": int(headerlines[8][9:-1]),
+        "mtp_columns": int(headerlines[9][12:-1]),
+        "filtersets": int(headerlines[10].split(";")[1]),
+        "reference_mode": headerlines[10].split(";")[3],
+        "multi_pmt": headerlines[10].split(";")[5],
     }
-    if metadata['date_end'] == metadata['date_start']:
-        metadata['date_end'] = None
+    if metadata["date_end"] == metadata["date_start"]:
+        metadata["date_end"] = None
 
     return metadata
 
@@ -292,33 +320,35 @@ def extract_filtersets(headerlines):
     filterlines = []
     filter_start = False
     for _, line in enumerate(headerlines):
-        if line.startswith('FILTERSET;'):
+        if line.startswith("FILTERSET;"):
             filter_start = True
             filterlines.append(line)
-        elif filter_start and not line.startswith(';;;'):
+        elif filter_start and not line.startswith(";;;"):
             filterlines.append(line)
         elif filter_start:
             break
 
-    df_filtersets = pandas.read_csv(io.StringIO(''.join(filterlines)), sep=';', usecols=range(12), index_col=False)
+    df_filtersets = pandas.read_csv(
+        io.StringIO("".join(filterlines)), sep=";", usecols=range(12), index_col=False
+    )
 
     ocol_ncol_type = [
-        ('FILTERSET', 'filter_number', int),
-        ('FILTERNAME', 'filter_name', str),
-        ('FILTERNR', 'filter_id', str),
-        (None, 'filter_type', str),
-        ('EX [nm]', 'excitation', float),
-        ('EM [nm]', 'emission', float),
-        #('LAYOUT', 'layout', str),
-        ('GAIN', 'gain', float),
-        (None, 'gain_1', float),
-        ('GAIN2', 'gain_2', float),
-        ('PHASESTATISTICSSIGMA', 'phase_statistic_sigma', float),
-        ('SIGNALQUALITYTOLERANCE', 'signal_quality_tolerance', float),
-        (None, 'reference_gain', float),
-        ('REFERENCE VALUE', 'reference_value', float),
-        (None, 'calibration', str),
-        ('EM2 [nm]', 'emission2', float),
+        ("FILTERSET", "filter_number", int),
+        ("FILTERNAME", "filter_name", str),
+        ("FILTERNR", "filter_id", str),
+        (None, "filter_type", str),
+        ("EX [nm]", "excitation", float),
+        ("EM [nm]", "emission", float),
+        # ('LAYOUT', 'layout', str),
+        ("GAIN", "gain", float),
+        (None, "gain_1", float),
+        ("GAIN2", "gain_2", float),
+        ("PHASESTATISTICSSIGMA", "phase_statistic_sigma", float),
+        ("SIGNALQUALITYTOLERANCE", "signal_quality_tolerance", float),
+        (None, "reference_gain", float),
+        ("REFERENCE VALUE", "reference_value", float),
+        (None, "calibration", str),
+        ("EM2 [nm]", "emission2", float),
     ]
 
     # TODO: infer filter_type column
@@ -331,110 +361,106 @@ def extract_filtersets(headerlines):
 def extract_process_parameters(headerlines):
     fs_start = None
     for l, line in enumerate(headerlines):
-        if line.startswith('FILTERSET;'):
+        if line.startswith("FILTERSET;"):
             fs_start = l
             break
 
     process_parameters = {
-        'temperature': float(headerlines[fs_start + 1].split(';')[13].strip()),
-        'humidity': float(headerlines[fs_start + 2].split(';')[13].strip()),
-        'O2': float(headerlines[fs_start + 3].split(';')[13].strip()),
-        'CO2': float(headerlines[fs_start + 4].split(';')[13].strip()),
-        'shaking': float(headerlines[fs_start + 5].split(';')[13].strip()),
-        'cycle_time': float(headerlines[fs_start + 6].split(';')[13].strip()),
-        'exp_time': float(headerlines[fs_start + 7].split(';')[13].strip()),
+        "temperature": float(headerlines[fs_start + 1].split(";")[13].strip()),
+        "humidity": float(headerlines[fs_start + 2].split(";")[13].strip()),
+        "O2": float(headerlines[fs_start + 3].split(";")[13].strip()),
+        "CO2": float(headerlines[fs_start + 4].split(";")[13].strip()),
+        "shaking": float(headerlines[fs_start + 5].split(";")[13].strip()),
+        "cycle_time": float(headerlines[fs_start + 6].split(";")[13].strip()),
+        "exp_time": float(headerlines[fs_start + 7].split(";")[13].strip()),
     }
     return process_parameters
 
 
 def extract_comments(dfraw):
     ocol_ncol_type = [
-        ('TIME [h]', 'time', float),
-        ('COMMENTS', 'user_comment', str),
+        ("TIME [h]", "time", float),
+        ("COMMENTS", "user_comment", str),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['READING'] == 'K'], ocol_ncol_type)
+    df = utils.__to_typed_cols__(dfraw[dfraw["READING"] == "K"], ocol_ncol_type)
     # TODO: automatically separate comments into user/sys
-    df['sys_comment'] = None
+    df["sys_comment"] = None
     df.index = range(len(df))
     return df
 
 
 def extract_references(dfraw):
-    dfref = dfraw[dfraw['READING'] == 'R'].copy()
+    dfref = dfraw[dfraw["READING"] == "R"].copy()
     ocol_ncol_type = [
-        ('cycle', 'cycle', int),
-        ('TIME [h]', 'time', float),
-        ('FILTERSET', 'filterset', int),
-        ('AMPLITUDE', 'amp_1', float),
-        ('amp_2', 'amp_2', float),
-        ('PHASE', 'phase', float),
+        ("cycle", "cycle", int),
+        ("TIME [h]", "time", float),
+        ("FILTERSET", "filterset", int),
+        ("AMPLITUDE", "amp_1", float),
+        ("amp_2", "amp_2", float),
+        ("PHASE", "phase", float),
     ]
-    dfref['amp_2'] = numpy.nan
+    dfref["amp_2"] = numpy.nan
     df = utils.__to_typed_cols__(dfref, ocol_ncol_type)
-    return df.set_index(['cycle', 'filterset'])
+    return df.set_index(["cycle", "filterset"])
 
 
 def extract_measurements(dfraw):
-    dfmes = dfraw[dfraw['READING'].str.startswith('C')].copy()
-    dfmes['cycle'] = dfmes['READING'].str.replace('C', '').astype(int)
-    dfmes['amp_2'] = numpy.nan
-    dfmes['amp_ref_1'] = numpy.nan
-    dfmes['amp_ref_2'] = numpy.nan
-    dfmes['cal'] = numpy.nan
+    dfmes = dfraw[dfraw["READING"].str.startswith("C")].copy()
+    dfmes["cycle"] = dfmes["READING"].str.replace("C", "").astype(int)
+    dfmes["amp_2"] = numpy.nan
+    dfmes["amp_ref_1"] = numpy.nan
+    dfmes["amp_ref_2"] = numpy.nan
+    dfmes["cal"] = numpy.nan
     ocol_ncol_type = [
-        ('cycle', 'cycle', int),
-        ('WELLNUM', 'well', str),
-        ('FILTERSET', 'filterset', int),
-        ('TIME [h]', 'time', float),
-        ('AMPLITUDE', 'amp_1', float),
-        ('amp_2', 'amp_2', float),
-        ('amp_ref_1', 'amp_ref_1', float),
-        ('amp_ref_2', 'amp_ref_2', float),
-        ('PHASE', 'phase', float),
-        ('cal', 'cal', float),
+        ("cycle", "cycle", int),
+        ("WELLNUM", "well", str),
+        ("FILTERSET", "filterset", int),
+        ("TIME [h]", "time", float),
+        ("AMPLITUDE", "amp_1", float),
+        ("amp_2", "amp_2", float),
+        ("amp_ref_1", "amp_ref_1", float),
+        ("amp_ref_2", "amp_ref_2", float),
+        ("PHASE", "phase", float),
+        ("cal", "cal", float),
     ]
     df = utils.__to_typed_cols__(dfmes, ocol_ncol_type)
-    df = df.set_index(['filterset', 'cycle', 'well'])
+    df = df.set_index(["filterset", "cycle", "well"])
     return df
 
 
-def extract_environment(dfraw, process_parameters:dict, comments:pandas.DataFrame):
+def extract_environment(dfraw, process_parameters: dict, comments: pandas.DataFrame):
     ocol_ncol_type = [
-        ('cycle', 'cycle', int),
-        ('TIME [h]', 'time', float),
-        (None, 'temp_setpoint', float),
-        ('ACT TEMP [°C]', 'temp_up', float),
-        (None, 'temp_down', float),
-        (None, 'temp_water', float),
-        ('ACT O2 [%]', 'o2', float),
-        ('ACT CO2 [%]', 'co2', float),
-        ('ACT HUMIDITY [rH]', 'humidity', float),
-        (None, 'shaker_setpoint', float),
-        (None, 'shaker_actual', float),
+        ("cycle", "cycle", int),
+        ("TIME [h]", "time", float),
+        (None, "temp_setpoint", float),
+        ("ACT TEMP [°C]", "temp_up", float),
+        (None, "temp_down", float),
+        (None, "temp_water", float),
+        ("ACT O2 [%]", "o2", float),
+        ("ACT CO2 [%]", "co2", float),
+        ("ACT HUMIDITY [rH]", "humidity", float),
+        (None, "shaker_setpoint", float),
+        (None, "shaker_actual", float),
     ]
     df = utils.__to_typed_cols__(dfraw, ocol_ncol_type)
-    df['shaker_setpoint'] = process_parameters['shaking']
-    df['temp_setpoint'] = process_parameters['temperature']
+    df["shaker_setpoint"] = process_parameters["shaking"]
+    df["temp_setpoint"] = process_parameters["temperature"]
     # process the comments column to extract setpoint changes
-    for t_comment, cmts in zip(comments['time'], comments['user_comment']):
+    for t_comment, cmts in zip(comments["time"], comments["user_comment"]):
         # multiple comments may be ,-separated in one line
-        cmts = [
-            c.strip()
-            for c in cmts.split(',')
-            if c.strip()
-        ]
+        cmts = [c.strip() for c in cmts.split(",") if c.strip()]
         # process each comment in this line
         for comment in cmts:
-            if comment.startswith('SWITCH SETPOINT:Freq.='):
-                rpm = float(comment.split('=')[1])
-                df.loc[(df['time'] >= t_comment), 'shaker_setpoint'] = rpm
-            elif comment.startswith('SWITCH SETPOINT:Temp.='):
-                temp = float(comment.split('=')[1])
-                df.loc[(df['time'] >= t_comment), 'temp_setpoint'] = temp
+            if comment.startswith("SWITCH SETPOINT:Freq.="):
+                rpm = float(comment.split("=")[1])
+                df.loc[(df["time"] >= t_comment), "shaker_setpoint"] = rpm
+            elif comment.startswith("SWITCH SETPOINT:Temp.="):
+                temp = float(comment.split("=")[1])
+                df.loc[(df["time"] >= t_comment), "temp_setpoint"] = temp
     return df
 
 
-def fetch_calibration_data(lot_number:int, temp:int):
+def fetch_calibration_data(lot_number: int, temp: int):
     """Loads calibration data from calibration file. Also triggers file download.
 
     Parameters
@@ -451,31 +477,31 @@ def fetch_calibration_data(lot_number:int, temp:int):
         Can be readily used in calibration function.
     """
     module_path = pathlib.Path(utils.__spec__.origin).parents[0]
-    calibration_file = pathlib.Path(module_path, 'cache', 'CalibrationLot.ini')
+    calibration_file = pathlib.Path(module_path, "cache", "CalibrationLot.ini")
 
     if not calibration_file.is_file():
         if not utils.download_calibration_data():
             return None
 
-    with open(calibration_file, 'r') as file:
+    with open(calibration_file, "r") as file:
         content = file.read()
 
     parser = configparser.ConfigParser(strict=False)
     parser.read_string(content)
-    calibration_info = f'{lot_number}-hc-Temp{temp}'
+    calibration_info = f"{lot_number}-hc-Temp{temp}"
 
     if not calibration_info in parser:
         if not utils.download_calibration_data():
             return None
-    
+
     if calibration_info in parser:
         calibration_dict = {
-            'cal_0': float(parser[calibration_info]['k0']),
-            'cal_100': float(parser[calibration_info]['k100']),
-            'phi_min': float(parser[calibration_info]['irmin']),
-            'phi_max': float(parser[calibration_info]['irmax']),
-            'pH_0': float(parser[calibration_info]['ph0']),
-            'dpH': float(parser[calibration_info]['dph']),
+            "cal_0": float(parser[calibration_info]["k0"]),
+            "cal_100": float(parser[calibration_info]["k100"]),
+            "phi_min": float(parser[calibration_info]["irmin"]),
+            "phi_max": float(parser[calibration_info]["irmax"]),
+            "pH_0": float(parser[calibration_info]["ph0"]),
+            "dpH": float(parser[calibration_info]["dph"]),
         }
         return calibration_dict
     else:
@@ -484,4 +510,3 @@ def fetch_calibration_data(lot_number:int, temp:int):
             f"but the provided lot number/temperature combination (lot_number={lot_number}, temp={temp}) could not be found. "
             "Please check the parameters."
         )
-    
\ No newline at end of file
diff --git a/bletl/parsing/blpro.py b/bletl/parsing/blpro.py
index 96bdaa8..082f70c 100644
--- a/bletl/parsing/blpro.py
+++ b/bletl/parsing/blpro.py
@@ -1,32 +1,43 @@
 """Parsing functions for the BioLector Pro"""
-from xml.etree.ElementTree import Element
-from bletl.parsing.bl1 import fetch_calibration_data
 import collections
 import datetime
 import io
 import logging
-import numpy
-import pandas
-import re
 import pathlib
-import xml.etree.ElementTree
+import re
 import warnings
+import xml.etree.ElementTree
+from xml.etree.ElementTree import Element
+
+import numpy
+import pandas
 
+from bletl.parsing.bl1 import fetch_calibration_data
+
+from .. import utils
 from ..types import (
-    BioLectorModel, BLData, BLDParser, FilterTimeSeries,
+    BioLectorModel,
+    BLData,
+    BLDParser,
+    FilterTimeSeries,
     InvalidLotNumberError,
 )
-from .. import utils
-
 
-logger = logging.getLogger('blpro')
+logger = logging.getLogger("blpro")
 
 
 class BioLectorProParser(BLDParser):
     def parse(
-        self, filepath,
-        lot_number:int=None, temp:int=None,
-        cal_0:float=None, cal_100:float=None, phi_min:float=None, phi_max:float=None, pH_0:float=None, dpH:float=None
+        self,
+        filepath,
+        lot_number: int = None,
+        temp: int = None,
+        cal_0: float = None,
+        cal_100: float = None,
+        phi_min: float = None,
+        phi_max: float = None,
+        pH_0: float = None,
+        dpH: float = None,
     ) -> BLData:
         metadata, data = parse_metadata_data(filepath)
 
@@ -50,50 +61,53 @@ def parse(
             lot_cal_data = None
 
         if lot_cal_data or (not None in [cal_0, cal_100, phi_min, phi_max, pH_0, dpH]):
-            for key, fts in transform_into_filtertimeseries(bld.metadata, bld.measurements, bld.filtersets, True):
-                if (key == 'pH') and (not None in [phi_min, phi_max, pH_0, dpH]):
+            for key, fts in transform_into_filtertimeseries(
+                bld.metadata, bld.measurements, bld.filtersets, True
+            ):
+                if (key == "pH") and (not None in [phi_min, phi_max, pH_0, dpH]):
                     fts.value = calibrate_pH(fts.value, phi_min, phi_max, pH_0, dpH)
                     bld[key] = fts
-                elif (key == 'pH') and lot_cal_data:
-                    fts.value = calibrate_pH(fts.value, lot_cal_data['phi_min'], lot_cal_data['phi_max'], lot_cal_data['pH_0'], lot_cal_data['dpH'])
+                elif (key == "pH") and lot_cal_data:
+                    fts.value = calibrate_pH(
+                        fts.value,
+                        lot_cal_data["phi_min"],
+                        lot_cal_data["phi_max"],
+                        lot_cal_data["pH_0"],
+                        lot_cal_data["dpH"],
+                    )
                     bld[key] = fts
-                elif (key == 'DO') and (not None in [phi_min, phi_max, pH_0, dpH]):
+                elif (key == "DO") and (not None in [phi_min, phi_max, pH_0, dpH]):
                     fts.value = calibrate_DO(fts.value, cal_0, cal_100)
                     bld[key] = fts
-                elif (key == 'DO') and lot_cal_data:
-                    fts.value = calibrate_DO(fts.value, lot_cal_data['cal_0'], lot_cal_data['cal_100'])
+                elif (key == "DO") and lot_cal_data:
+                    fts.value = calibrate_DO(fts.value, lot_cal_data["cal_0"], lot_cal_data["cal_100"])
                     bld[key] = fts
                 else:
                     bld[key] = fts
         else:
-            for key, fts in transform_into_filtertimeseries(bld.metadata, bld.measurements, bld.filtersets, False):
+            for key, fts in transform_into_filtertimeseries(
+                bld.metadata, bld.measurements, bld.filtersets, False
+            ):
                 bld[key] = fts
 
         return bld
 
 
-def _filter_datalines(datalines:list) -> list:
-    """ Filters out unnecessary intermediate header sections. """
-    datalines = [
-        dl
-        for l, dl in enumerate(datalines)
-        if len(dl) > 1 and dl[1] == ';'
-    ]
+def _filter_datalines(datalines: list) -> list:
+    """Filters out unnecessary intermediate header sections."""
+    datalines = [dl for l, dl in enumerate(datalines) if len(dl) > 1 and dl[1] == ";"]
     return datalines
 
 
 def _parse_datalines(datalines) -> pandas.DataFrame:
     dfraw = pandas.read_csv(
-        io.StringIO(''.join(datalines)), sep=';', low_memory=False,
-        converters={
-            'Filterset': str
-        }
+        io.StringIO("".join(datalines)), sep=";", low_memory=False, converters={"Filterset": str}
     )
     return dfraw
 
 
 def parse_metadata_data(fp):
-    with open(fp, 'r', encoding='utf-8') as f:
+    with open(fp, "r", encoding="utf-8") as f:
         lines = f.readlines()
 
     metadata = collections.defaultdict(dict)
@@ -102,74 +116,81 @@ def parse_metadata_data(fp):
     data_start = None
 
     for l, line in enumerate(lines):
-        if line.startswith('='):
+        if line.startswith("="):
             # any section header encountered
-            section = line.strip().strip('=').strip()
-            if not data_start and section == 'data':
+            section = line.strip().strip("=").strip()
+            if not data_start and section == "data":
                 data_start = l + 1
-        elif line.startswith('['):
+        elif line.startswith("["):
             # register the value
-            key, value = line.split(']')
-            key = key.strip('[')
+            key, value = line.split("]")
+            key = key.strip("[")
             metadata[section][key] = value.strip()
-                
+
     # standardize the metadata keys
-    metadata['date_start'] = datetime.datetime.strptime(metadata['process']['start_date_time'], '%Y-%m-%d, %H:%M:%S')
-    if 'end_process' in metadata and 'end_date_time' in metadata['end_process']:
-        metadata['date_end'] = datetime.datetime.strptime(metadata['end_process']['end_date_time'], '%Y-%m-%d, %H:%M:%S')
+    metadata["date_start"] = datetime.datetime.strptime(
+        metadata["process"]["start_date_time"], "%Y-%m-%d, %H:%M:%S"
+    )
+    if "end_process" in metadata and "end_date_time" in metadata["end_process"]:
+        metadata["date_end"] = datetime.datetime.strptime(
+            metadata["end_process"]["end_date_time"], "%Y-%m-%d, %H:%M:%S"
+        )
     else:
-        metadata['date_end'] = None
+        metadata["date_end"] = None
 
     datalines = lines[data_start:]
-    
+
     datalines = _filter_datalines(datalines)
     # insert full-length section headers
     # append a ; to the header line because the P lines contain a trailing ;
     datalines.insert(
         0,
-        'Type;Cycle;Well;Filterset;Time;Amp_1;Amp_2;AmpRef_1;AmpRef_2;Phase;Cal;' \
-        'Temp_up;Temp_down;Temp_water;O2;CO2;Humidity;Shaker;Service;User_Comment;Sys_Comment;' \
-        'Reservoir;MF_Volume;Temp_Ch4;T_144;T_180;T_181_1;T_192;P_Ch1;P_Ch2;P_Ch3;T_Hum;T_CO2;' \
-        'X-Pos;Y-Pos;T_LED;Ref_Int;Ref_Phase;Ref_Gain;Ch1-MP;Ch2-MF;Ch3-FA;Ch4-OP;Ch5-FB;IGNORE\n'
+        "Type;Cycle;Well;Filterset;Time;Amp_1;Amp_2;AmpRef_1;AmpRef_2;Phase;Cal;"
+        "Temp_up;Temp_down;Temp_water;O2;CO2;Humidity;Shaker;Service;User_Comment;Sys_Comment;"
+        "Reservoir;MF_Volume;Temp_Ch4;T_144;T_180;T_181_1;T_192;P_Ch1;P_Ch2;P_Ch3;T_Hum;T_CO2;"
+        "X-Pos;Y-Pos;T_LED;Ref_Int;Ref_Phase;Ref_Gain;Ch1-MP;Ch2-MF;Ch3-FA;Ch4-OP;Ch5-FB;IGNORE\n",
     )
-    
+
     # parse the data as a DataFrame
     try:
         dfraw = _parse_datalines(datalines)
     except pandas.errors.ParserError:
-        n_allowed = datalines[0].count(';')
+        n_allowed = datalines[0].count(";")
         filtered_datalines = []
         defect_lines = []
         for l, line in enumerate(datalines):
             # ignore lines with too many columns, DEL or NUL characters
-            if line.count(';') <= n_allowed and not ('\x10' in line or '\x00' in line) and not re.search(r'\d*\.\d*\.\d*', line):
+            if (
+                line.count(";") <= n_allowed
+                and not ("\x10" in line or "\x00" in line)
+                and not re.search(r"\d*\.\d*\.\d*", line)
+            ):
                 filtered_datalines.append(line)
             else:
                 defect_lines.append(data_start + l + 1)
-        dfraw = _parse_datalines(filtered_datalines).drop_duplicates(keep='first')
-        logger.warning(f'{fp} contains defects in lines {defect_lines}. Be extra skeptical about the parsed results.')
+        dfraw = _parse_datalines(filtered_datalines).drop_duplicates(keep="first")
+        logger.warning(
+            f"{fp} contains defects in lines {defect_lines}. Be extra skeptical about the parsed results."
+        )
 
     return metadata, dfraw[list(dfraw.columns)[:-1]]
 
 
 def standardize(df):
-    if 'time' in df.columns:
-        df['time'] = df['time'] / 3600
+    if "time" in df.columns:
+        df["time"] = df["time"] / 3600
     return df
 
 
 def extract_filtersets(metadata):
 
     # filterset-related metadata is spread over: channels, measurement channels, process
-    channels = metadata.pop('channels')
-    measurement_channels = metadata.pop('measurement channels')
-    process = metadata['process']
+    channels = metadata.pop("channels")
+    measurement_channels = metadata.pop("measurement channels")
+    process = metadata["process"]
 
     # dictionary that will become the DataFrame
-    filtersets = {
-        fnum : {}
-        for fnum in range(1, int(channels['no_filterset']) + 1)
-    }
+    filtersets = {fnum: {} for fnum in range(1, int(channels["no_filterset"]) + 1)}
 
     # grab data from measurement_channels
     for k, v in measurement_channels.items():
@@ -184,83 +205,83 @@ def extract_filtersets(metadata):
         "203": "DO(PSt3)",
     }
     for fnum, fset in filtersets.items():
-        for k in ['reference_value', 'reference_gain', 'gain']:
-            fn = fname_lookup.get(fset['filter_id'], fset['name'])
-            pk = f'{fnum:02d}_{k}_{fn}'
+        for k in ["reference_value", "reference_gain", "gain"]:
+            fn = fname_lookup.get(fset["filter_id"], fset["name"])
+            pk = f"{fnum:02d}_{k}_{fn}"
             if pk in process:
                 filtersets[fnum][k] = process.pop(pk)
 
     # convert to DataFrame and align column names & types
     df = pandas.DataFrame(filtersets).T
     ocol_ncol_type = [
-        ('no', 'filter_number', int),
-        ('name', 'filter_name', str),
-        ('filter_id', 'filter_id', str),
-        ('filter_type', 'filter_type', str),
-        (None, 'excitation', float),
-        (None, 'emission', float),
-        #(None, 'layout', str),
-        ('gain', 'gain', float),
-        ('gain_1', 'gain_1', float),
-        ('gain_2', 'gain_2', float),
-        (None, 'phase_statistic_sigma', float),
-        (None, 'signal_quality_tolerance', float),
-        ('reference_gain', 'reference_gain', float),
-        ('reference_value', 'reference_value', float),
-        ('calibration', 'calibration', str),
-        (None, 'emission2', float),
+        ("no", "filter_number", int),
+        ("name", "filter_name", str),
+        ("filter_id", "filter_id", str),
+        ("filter_type", "filter_type", str),
+        (None, "excitation", float),
+        (None, "emission", float),
+        # (None, 'layout', str),
+        ("gain", "gain", float),
+        ("gain_1", "gain_1", float),
+        ("gain_2", "gain_2", float),
+        (None, "phase_statistic_sigma", float),
+        (None, "signal_quality_tolerance", float),
+        ("reference_gain", "reference_gain", float),
+        ("reference_value", "reference_value", float),
+        ("calibration", "calibration", str),
+        (None, "emission2", float),
     ]
     return utils.__to_typed_cols__(df, ocol_ncol_type)
 
 
 def extract_comments(dfraw):
     ocol_ncol_type = [
-        ('Time', 'time', float),
-        ('User_Comment', 'user_comment', str),
-        ('Sys_Comment', 'sys_comment', str),
+        ("Time", "time", float),
+        ("User_Comment", "user_comment", str),
+        ("Sys_Comment", "sys_comment", str),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['Type'] == 'C'], ocol_ncol_type)
+    df = utils.__to_typed_cols__(dfraw[dfraw["Type"] == "C"], ocol_ncol_type)
     return standardize(df)
 
 
 def extract_parameters(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Time', 'time', float),
+        ("Cycle", "cycle", int),
+        ("Time", "time", float),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['Type'] == 'P'], ocol_ncol_type)
+    df = utils.__to_typed_cols__(dfraw[dfraw["Type"] == "P"], ocol_ncol_type)
     return standardize(df)
 
 
 def extract_references(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Filterset', 'filterset', int),
-        ('Time', 'time', float),
-        ('Amp_1', 'amp_1', float),
-        ('Amp_2', 'amp_2', float),
-        ('Phase', 'phase', float),
+        ("Cycle", "cycle", int),
+        ("Filterset", "filterset", int),
+        ("Time", "time", float),
+        ("Amp_1", "amp_1", float),
+        ("Amp_2", "amp_2", float),
+        ("Phase", "phase", float),
     ]
-    df = dfraw[dfraw['Type'] == 'R'].copy()
-    df['Phase'] = pandas.to_numeric(df['Phase'], errors='coerce')
+    df = dfraw[dfraw["Type"] == "R"].copy()
+    df["Phase"] = pandas.to_numeric(df["Phase"], errors="coerce")
     df = utils.__to_typed_cols__(df, ocol_ncol_type)
-    return standardize(df).set_index(['cycle', 'filterset'])
+    return standardize(df).set_index(["cycle", "filterset"])
 
 
 def extract_measurements(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Well', 'well', int),
-        ('Filterset', 'filterset', str),
-        ('Time', 'time', float),
-        ('Amp_1', 'amp_1', float),
-        ('Amp_2', 'amp_2', float),
-        ('AmpRef_1', 'amp_ref_1', float),
-        ('AmpRef_2', 'amp_ref_2', float),
-        ('Phase', 'phase', float),
-        ('Cal', 'cal', float),
+        ("Cycle", "cycle", int),
+        ("Well", "well", int),
+        ("Filterset", "filterset", str),
+        ("Time", "time", float),
+        ("Amp_1", "amp_1", float),
+        ("Amp_2", "amp_2", float),
+        ("AmpRef_1", "amp_ref_1", float),
+        ("AmpRef_2", "amp_ref_2", float),
+        ("Phase", "phase", float),
+        ("Cal", "cal", float),
     ]
-    df_M = dfraw[dfraw['Type'] == 'M']
+    df_M = dfraw[dfraw["Type"] == "M"]
 
     # Drop lines with invalid readings
     mask = (df_M["AmpRef_1"] == "REFOVERLD") | (df_M["AmpRef_2"] == "REFOVERLD")
@@ -268,32 +289,31 @@ def extract_measurements(dfraw):
     if ndrop:
         cdrop = df_M[mask]["Cycle"].to_list()
         warnings.warn(
-            f"Dropped {ndrop} measurement rows from cycles {cdrop} because they have REFOVERLD.",
-            UserWarning
+            f"Dropped {ndrop} measurement rows from cycles {cdrop} because they have REFOVERLD.", UserWarning
         )
     df_M = df_M[~mask]
 
     # Convert to the expected data types
     df = utils.__to_typed_cols__(df_M, ocol_ncol_type)
-    df = df.set_index(['filterset', 'cycle', 'well'])
+    df = df.set_index(["filterset", "cycle", "well"])
     return standardize(df)
 
 
 def extract_environment(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Time', 'time', float),
-        (None, 'temp_setpoint', float),
-        ('Temp_up', 'temp_up', float),
-        ('Temp_down', 'temp_down', float),
-        ('Temp_water', 'temp_water', float),
-        ('O2', 'o2', float),
-        ('CO2', 'co2', float),
-        ('Humidity', 'humidity', float),
-        (None, 'shaker_setpoint', float),
-        ('Shaker', 'shaker_actual', float),
+        ("Cycle", "cycle", int),
+        ("Time", "time", float),
+        (None, "temp_setpoint", float),
+        ("Temp_up", "temp_up", float),
+        ("Temp_down", "temp_down", float),
+        ("Temp_water", "temp_water", float),
+        ("O2", "o2", float),
+        ("CO2", "co2", float),
+        ("Humidity", "humidity", float),
+        (None, "shaker_setpoint", float),
+        ("Shaker", "shaker_actual", float),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['Type'] == 'R'], ocol_ncol_type)
+    df = utils.__to_typed_cols__(dfraw[dfraw["Type"] == "R"], ocol_ncol_type)
     # TODO: write initial setpoints (temp & shaker) into df
     # TODO: parse setpoint changes (temp & shaker) from comments
     # TODO: clean up -9999.0 values in co2 column
@@ -302,122 +322,125 @@ def extract_environment(dfraw):
 
 def extract_fluidics(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Well', 'well', int),
-        ('Time', 'time', float),
-        ('Reservoir', 'reservoir', float),
-        ('MF_Volume', 'mf_volume', float),
-        ('Temp_Ch4', 'volume', float),
+        ("Cycle", "cycle", int),
+        ("Well", "well", int),
+        ("Time", "time", float),
+        ("Reservoir", "reservoir", float),
+        ("MF_Volume", "mf_volume", float),
+        ("Temp_Ch4", "volume", float),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['Type'] == 'F'], ocol_ncol_type)
-    df = df.sort_values(['well', 'cycle']).set_index(['well'])
+    df = utils.__to_typed_cols__(dfraw[dfraw["Type"] == "F"], ocol_ncol_type)
+    df = df.sort_values(["well", "cycle"]).set_index(["well"])
     return standardize(df)
 
 
 def extract_valves_module(dfraw):
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Well', 'valve', str),
-        ('Filterset', 'well', str),
-        ('Time', 'volume_1', str),
-        ('Amp_1', 'volume_2', str),
+        ("Cycle", "cycle", int),
+        ("Well", "valve", str),
+        ("Filterset", "well", str),
+        ("Time", "volume_1", str),
+        ("Amp_1", "volume_2", str),
     ]
-    df = utils.__to_typed_cols__(dfraw[dfraw['Type'] == 'N'], ocol_ncol_type)
+    df = utils.__to_typed_cols__(dfraw[dfraw["Type"] == "N"], ocol_ncol_type)
 
     # table of valve actions
-    df_valves = df[df['valve'] != 'Module 1 (BASE)'].copy()
-    df_valves.columns = ['cycle', 'valve', 'well', 'acid', 'base']
-    df_valves['valve'] = df_valves['valve'].str.replace('Valve ', '').astype(int)
-    df_valves['well'] = df_valves['well'].str.replace('Well', '').astype(int)
-    df_valves['acid'] = df_valves['acid'].str.replace('Sollvolumen (Acid) ', '', regex=False).astype(float)
-    df_valves['base'] = df_valves['base'].str.replace('Sollvolumen (Base) ', '', regex=False).astype(float)
-    df_valves = standardize(df_valves).set_index(['well', 'valve', 'cycle'])
+    df_valves = df[df["valve"] != "Module 1 (BASE)"].copy()
+    df_valves.columns = ["cycle", "valve", "well", "acid", "base"]
+    df_valves["valve"] = df_valves["valve"].str.replace("Valve ", "").astype(int)
+    df_valves["well"] = df_valves["well"].str.replace("Well", "").astype(int)
+    df_valves["acid"] = df_valves["acid"].str.replace("Sollvolumen (Acid) ", "", regex=False).astype(float)
+    df_valves["base"] = df_valves["base"].str.replace("Sollvolumen (Base) ", "", regex=False).astype(float)
+    df_valves = standardize(df_valves).set_index(["well", "valve", "cycle"])
 
     # TODO: unknown column purpose
-    df_module = df[df['valve'] == 'Module 1 (BASE)'].copy()
-    df_module.columns = ['cycle', 'module', 'valve', 'well', 'volume']
-    df_module['valve'] = df_module['valve'].str.replace('Valve ', '').astype(int)
-    df_module['well'] = df_module['well'].str.replace('Well ', '').astype(int)
-    df_module['volume'] = df_module['volume'].str.replace('Volume ', '').astype(float)
-    df_module = standardize(df_module).set_index(['well', 'valve', 'cycle'])
+    df_module = df[df["valve"] == "Module 1 (BASE)"].copy()
+    df_module.columns = ["cycle", "module", "valve", "well", "volume"]
+    df_module["valve"] = df_module["valve"].str.replace("Valve ", "").astype(int)
+    df_module["well"] = df_module["well"].str.replace("Well ", "").astype(int)
+    df_module["volume"] = df_module["volume"].str.replace("Volume ", "").astype(float)
+    df_module = standardize(df_module).set_index(["well", "valve", "cycle"])
 
     return df_valves, df_module
 
 
 def extract_diagnostics(dfraw):
-    dff = dfraw[dfraw['Type'] != 'N']
+    dff = dfraw[dfraw["Type"] != "N"]
     ocol_ncol_type = [
-        ('Cycle', 'cycle', int),
-        ('Time', 'time', float),
-        ('Temp_Ch4', 'temp_ch4', float),
-        ('T_144', 't_144', float),
-        ('T_180', 't_180', float),
-        ('T_181_1', 't_181_1', float),
-        ('T_192', 't_192', float),
-        ('P_Ch1', 'p_ch1', float),
-        ('P_Ch2', 'p_ch2', float),
-        ('P_Ch3', 'p_ch3', float),
-        ('T_Hum', 't_hum', float),
-        ('T_CO2', 't_co2', float),
-        ('X-Pos', 'x-pos', float),
-        ('Y-Pos', 'y-pos', float),
-        ('T_LED', 't_led', float),
-        ('Ref_Int', 'ref_int', float),
-        ('Ref_Phase', 'ref_phase', float),
-        ('Ref_Gain', 'ref_gain', float),
-        ('Ch1-MP', 'ch1_mp', float),
-        ('Ch2-MF', 'ch2_mf', float),
-        ('Ch3-FA', 'ch3_fa', float),
-        ('Ch4-OP', 'ch4_op', float),
-        ('Ch5-FB', 'ch5_fb', float),
+        ("Cycle", "cycle", int),
+        ("Time", "time", float),
+        ("Temp_Ch4", "temp_ch4", float),
+        ("T_144", "t_144", float),
+        ("T_180", "t_180", float),
+        ("T_181_1", "t_181_1", float),
+        ("T_192", "t_192", float),
+        ("P_Ch1", "p_ch1", float),
+        ("P_Ch2", "p_ch2", float),
+        ("P_Ch3", "p_ch3", float),
+        ("T_Hum", "t_hum", float),
+        ("T_CO2", "t_co2", float),
+        ("X-Pos", "x-pos", float),
+        ("Y-Pos", "y-pos", float),
+        ("T_LED", "t_led", float),
+        ("Ref_Int", "ref_int", float),
+        ("Ref_Phase", "ref_phase", float),
+        ("Ref_Gain", "ref_gain", float),
+        ("Ch1-MP", "ch1_mp", float),
+        ("Ch2-MF", "ch2_mf", float),
+        ("Ch3-FA", "ch3_fa", float),
+        ("Ch4-OP", "ch4_op", float),
+        ("Ch5-FB", "ch5_fb", float),
     ]
     df = utils.__to_typed_cols__(dff, ocol_ncol_type)
     return standardize(df)
 
 
-def transform_into_filtertimeseries(metadata:dict, measurements:pandas.DataFrame, filtersets:pandas.DataFrame, return_uncalibrated_optode_data:bool):
-    no_to_id = {
-        int(k.split('_')[0]) : v
-        for k, v in metadata['fermentation'].items()
-        if k.endswith('_well')
-    }
+def transform_into_filtertimeseries(
+    metadata: dict,
+    measurements: pandas.DataFrame,
+    filtersets: pandas.DataFrame,
+    return_uncalibrated_optode_data: bool,
+):
+    no_to_id = {int(k.split("_")[0]): v for k, v in metadata["fermentation"].items() if k.endswith("_well")}
     for fs in filtersets.itertuples():
-        filter_number = f'{fs.filter_number:02d}'
+        filter_number = f"{fs.filter_number:02d}"
         key = None
         times = None
         values = None
-        if fs.filter_type == 'Intensity' and ('Biomass' in fs.filter_name or 'BS' in fs.filter_name):
-            key = f'BS{int(fs.gain_1)}'
-            times = measurements.xs(filter_number, level='filterset')['time'].unstack()
-            values = measurements.xs(filter_number, level='filterset')['amp_ref_1'].unstack()       
-        elif fs.filter_type in {'pH', 'DO'} and not return_uncalibrated_optode_data:
+        if fs.filter_type == "Intensity" and ("Biomass" in fs.filter_name or "BS" in fs.filter_name):
+            key = f"BS{int(fs.gain_1)}"
+            times = measurements.xs(filter_number, level="filterset")["time"].unstack()
+            values = measurements.xs(filter_number, level="filterset")["amp_ref_1"].unstack()
+        elif fs.filter_type in {"pH", "DO"} and not return_uncalibrated_optode_data:
             key = fs.filter_type
-            times = measurements.xs(filter_number, level='filterset')['time'].unstack()
-            values = measurements.xs(filter_number, level='filterset')['cal'].unstack()
-        elif fs.filter_type in {'pH', 'DO'} and return_uncalibrated_optode_data:
+            times = measurements.xs(filter_number, level="filterset")["time"].unstack()
+            values = measurements.xs(filter_number, level="filterset")["cal"].unstack()
+        elif fs.filter_type in {"pH", "DO"} and return_uncalibrated_optode_data:
             key = fs.filter_type
-            times = measurements.xs(filter_number, level='filterset')['time'].unstack()
-            values = measurements.xs(filter_number, level='filterset')['phase'].unstack()
-        elif fs.filter_type == 'Intensity':
+            times = measurements.xs(filter_number, level="filterset")["time"].unstack()
+            values = measurements.xs(filter_number, level="filterset")["phase"].unstack()
+        elif fs.filter_type == "Intensity":
             key = fs.filter_name
-            times = measurements.xs(filter_number, level='filterset')['time'].unstack()
-            values = measurements.xs(filter_number, level='filterset')['amp_ref_1'].unstack()       
+            times = measurements.xs(filter_number, level="filterset")["time"].unstack()
+            values = measurements.xs(filter_number, level="filterset")["amp_ref_1"].unstack()
         else:
-            logger.warn(f'Skipped {fs.filter_type} channel with name "{fs.filter_name}" because no processing routine is implemented.')
+            logger.warn(
+                f'Skipped {fs.filter_type} channel with name "{fs.filter_name}" because no processing routine is implemented.'
+            )
             continue
 
         # transform into nicely formatted DataFrames for FilterTimeSeries
         times.columns = [no_to_id[c] for c in times.columns]
         times = times.reindex(sorted(times.columns), axis=1)
-        times.columns.name = 'well'
+        times.columns.name = "well"
         values.columns = [no_to_id[c] for c in values.columns]
         values = values.reindex(sorted(values.columns), axis=1)
-        values.columns.name = 'well'
+        values.columns.name = "well"
         fts = FilterTimeSeries(times, values)
         yield (key, fts)
 
 
-def fetch_calibration_data(lot_number:int, temp:int):
+def fetch_calibration_data(lot_number: int, temp: int):
     """Loads calibration data from calibration file. Also triggers file download.
 
     Parameters
@@ -434,18 +457,18 @@ def fetch_calibration_data(lot_number:int, temp:int):
         Can be readily used in calibration function.
     """
     module_path = pathlib.Path(utils.__spec__.origin).parents[0]
-    calibration_file = pathlib.Path(module_path, 'cache', 'CalibrationLot_II.xml')
+    calibration_file = pathlib.Path(module_path, "cache", "CalibrationLot_II.xml")
 
     if not calibration_file.is_file():
         if not utils.download_calibration_data():
             return None
-    
+
     def search_for_lot(calibration_file, lot_number):
         tree = xml.etree.ElementTree.parse(calibration_file)
         root = tree.getroot()
 
         element = None
-        for i, e in enumerate(root[1].iter('Name')):
+        for i, e in enumerate(root[1].iter("Name")):
             if e.text == str(lot_number):
                 element = root[1][i]
                 break
@@ -457,7 +480,7 @@ def search_for_lot(calibration_file, lot_number):
             return None
         else:
             element = search_for_lot(calibration_file, lot_number)
-    
+
     if not element:
         raise InvalidLotNumberError(
             "Latest calibration information was downloaded from m2p-labs, "
@@ -466,26 +489,22 @@ def search_for_lot(calibration_file, lot_number):
         )
 
     cp = dict()
-    for p in ['fCal0_m', 'fCal0_a', 'fCal100_m', 'fCal100_a']:
-        cp.update({
-            p: float(element.find('DOTempCompensation').find(p).text)
-        })
-
-    for p in ['fMin_m', 'fMin_a', 'fMax_m', 'fMax_a', 'dpH_m', 'dpH_a', 'pH0_m', 'pH0_a']:
-        cp.update({
-            p: float(element.find('PHTempCompensation').find(p).text)
-        })
-    
+    for p in ["fCal0_m", "fCal0_a", "fCal100_m", "fCal100_a"]:
+        cp.update({p: float(element.find("DOTempCompensation").find(p).text)})
+
+    for p in ["fMin_m", "fMin_a", "fMax_m", "fMax_a", "dpH_m", "dpH_a", "pH0_m", "pH0_a"]:
+        cp.update({p: float(element.find("PHTempCompensation").find(p).text)})
+
     calibration_dict = {
-        'cal_0': cp['fCal0_m']*temp+cp['fCal0_a'],
-        'cal_100': cp['fCal100_m']*temp+cp['fCal100_a'],
-        'phi_min': cp['fMin_m']*temp+cp['fMin_a'],
-        'phi_max': cp['fMax_m']*temp+cp['fMax_a'],
-        'pH_0': cp['pH0_m']*temp+cp['pH0_a'],
-        'dpH': cp['dpH_m']*temp+cp['dpH_a'],
+        "cal_0": cp["fCal0_m"] * temp + cp["fCal0_a"],
+        "cal_100": cp["fCal100_m"] * temp + cp["fCal100_a"],
+        "phi_min": cp["fMin_m"] * temp + cp["fMin_a"],
+        "phi_max": cp["fMax_m"] * temp + cp["fMax_a"],
+        "pH_0": cp["pH0_m"] * temp + cp["pH0_a"],
+        "dpH": cp["dpH_m"] * temp + cp["dpH_a"],
     }
 
-    return calibration_dict    
+    return calibration_dict
 
 
 def calibrate_pH(raw, phi_min, phi_max, pH_0, dpH):
diff --git a/bletl/splines.py b/bletl/splines.py
index 4f921d2..e3c07a5 100644
--- a/bletl/splines.py
+++ b/bletl/splines.py
@@ -1,28 +1,27 @@
 import copy
-import joblib
 import logging
 import multiprocessing
 import numbers
+import pickle
+from typing import Callable, Dict, Optional, Sequence, Tuple, Union
+
+import csaps
+import joblib
 import numpy
 import pandas
-import pickle
-import scipy.optimize
 import scipy.interpolate
+import scipy.optimize
 import scipy.stats
-import csaps
-from typing import Dict, Callable, Optional, Sequence, Tuple, Union
-
-
-from . types import FilterTimeSeries
 
+from .types import FilterTimeSeries
 
 logger = logging.getLogger(__file__)
 
 
 class UnivariateCubicSmoothingSpline(csaps.CubicSmoothingSpline):
-    """Overrides `csaps` type to align its API with the `scipy.interpolate` splines.
-    """
-    def derivative(self, order:int=1, *, epsilon=0.001) -> Callable[[numpy.ndarray], numpy.ndarray]:
+    """Overrides `csaps` type to align its API with the `scipy.interpolate` splines."""
+
+    def derivative(self, order: int = 1, *, epsilon=0.001) -> Callable[[numpy.ndarray], numpy.ndarray]:
         """Returns the derivative of the spline.
 
         Parameters
@@ -44,7 +43,9 @@ def derivative(self, order:int=1, *, epsilon=0.001) -> Callable[[numpy.ndarray],
             der = self.derivative(order - 1)
             der_f = lambda x: der(x)
             return lambda x: (der_f(x + epsilon) - der_f(x - epsilon)) / (2 * epsilon)
-        raise NotImplementedError(f'{order}-order derivatives are not implemented for the UnivariateCubicSmoothingSpline')
+        raise NotImplementedError(
+            f"{order}-order derivatives are not implemented for the UnivariateCubicSmoothingSpline"
+        )
 
     def __call__(self, xi: Union[float, numpy.ndarray]):
         """Evaluate the spline at some coordinates.
@@ -65,7 +66,7 @@ def __call__(self, xi: Union[float, numpy.ndarray]):
         return result
 
 
-def _normalize_smoothing_factor(method:str, smooth:float, x: numpy.ndarray, y: numpy.ndarray) -> float:
+def _normalize_smoothing_factor(method: str, smooth: float, x: numpy.ndarray, y: numpy.ndarray) -> float:
     """Normalization of smoothing factor from the interval [0,1] to the value required by the Spline type.
 
     Parameters
@@ -84,9 +85,9 @@ def _normalize_smoothing_factor(method:str, smooth:float, x: numpy.ndarray, y: n
     smoothing_factor : float
         Smoothing factor transformed to the domain used by the spline `method`.
     """
-    if method == 'ucss':
+    if method == "ucss":
         return 1 - smooth
-    elif method == 'us':
+    elif method == "us":
         # the s parameter of the UnivariateSpline describes the maximum sum squared error of the fit
         # a reasonable upper bound for this SSE is the SSE of a linear fit
         slope, intercept, _, _, _ = scipy.stats.linregress(x, y)
@@ -96,9 +97,9 @@ def _normalize_smoothing_factor(method:str, smooth:float, x: numpy.ndarray, y: n
         raise NotImplementedError(f'Unknown method "{method}"')
 
 
-def _evaluate_smoothing_factor(smoothing_factor:float, timepoints, values, k:int, method:str) -> float:
+def _evaluate_smoothing_factor(smoothing_factor: float, timepoints, values, k: int, method: str) -> float:
     """Computes mean sum of squared residuals over K folds of the dataset.
-    
+
     Parameters
     ----------
     smoothing_factor : float
@@ -112,7 +113,7 @@ def _evaluate_smoothing_factor(smoothing_factor:float, timepoints, values, k:int
     method : str
         Kind of spline.
         Options: "ucss" UnivariateCubicSmoothingSpline, "us" UnivariateSpline
-        
+
     Returns
     -------
     mssr : float
@@ -123,8 +124,7 @@ def _evaluate_smoothing_factor(smoothing_factor:float, timepoints, values, k:int
         raise ValueError(f"Need k≥2 splits for crossvalidation. Setting was k={k}.")
     if len(values) < 3 * k:
         raise ValueError(
-            f"Time series of {len(values)} elements is too short. "
-            f"Need at least a length of 3*k ({3*k})."
+            f"Time series of {len(values)} elements is too short. " f"Need at least a length of 3*k ({3*k})."
         )
     ssrs = []
     for kshift in range(k):
@@ -139,7 +139,7 @@ def _evaluate_smoothing_factor(smoothing_factor:float, timepoints, values, k:int
                 train_idxs=train_mask,
                 test_idxs=test_mask,
                 smoothing_factor=smoothing_factor,
-                method=method
+                method=method,
             )
         )
     return numpy.mean(ssrs)
@@ -150,8 +150,8 @@ def _evaluate_spline_test_error(
     y: numpy.ndarray,
     train_idxs: numpy.ndarray,
     test_idxs: numpy.ndarray,
-    smoothing_factor:float,
-    method:str
+    smoothing_factor: float,
+    method: str,
 ) -> float:
     """Fits spline to a test set and returns the sum of squared error on the test set.
 
@@ -177,25 +177,25 @@ def _evaluate_spline_test_error(
         Sum of squared residuals on test set.
     """
     smooth = _normalize_smoothing_factor(method, smoothing_factor[0], x, y)
-    if method == 'ucss':
+    if method == "ucss":
         spline = csaps.CubicSmoothingSpline(x[train_idxs], y[train_idxs], smooth=smooth)
-    elif method == 'us':
+    elif method == "us":
         spline = scipy.interpolate.UnivariateSpline(x[train_idxs], y[train_idxs], s=smooth)
     else:
         raise NotImplementedError(f'Unknown method "{method}"')
     y_val_pred = spline(x[test_idxs])
-    return numpy.sum(numpy.square(y_val_pred - y[test_idxs]))      
+    return numpy.sum(numpy.square(y_val_pred - y[test_idxs]))
 
 
 def get_crossvalidated_spline(
     x: numpy.ndarray,
     y: numpy.ndarray,
-    k_folds:int=5,
-    method:str='us',
-    bounds: Tuple[float, float]=(0.001,1)
+    k_folds: int = 5,
+    method: str = "us",
+    bounds: Tuple[float, float] = (0.001, 1),
 ) -> Union[UnivariateCubicSmoothingSpline, scipy.interpolate.UnivariateSpline]:
     """Returns spline with k-fold crossvalidated smoothing factor
-    
+
     Parameters
     ----------
     x : numpy.ndarray
@@ -216,28 +216,27 @@ def get_crossvalidated_spline(
         Spline of the specified `method` fitted to the data.
         Its optimal smoothing factor is determined by k-fold crossvalidation.
     """
-    opt = scipy.optimize.differential_evolution(_evaluate_smoothing_factor,
-        bounds=[bounds],
-        args=(x, y, k_folds, method)
+    opt = scipy.optimize.differential_evolution(
+        _evaluate_smoothing_factor, bounds=[bounds], args=(x, y, k_folds, method)
     )
     smooth = _normalize_smoothing_factor(method, opt.x[0], x, y)
-    if method == 'ucss':
+    if method == "ucss":
         return UnivariateCubicSmoothingSpline(x, y, smooth=smooth)
-    elif method == 'us':
+    elif method == "us":
         return scipy.interpolate.UnivariateSpline(x, y, s=smooth)
     else:
         raise NotImplementedError(f'Unknown method "{method}"')
 
 
 def get_multiple_splines(
-    fts:FilterTimeSeries,
-    wells:Sequence[str],
-    k_folds:int=5,
-    method:str='us',
-    last_cycle:Optional[int]=None
+    fts: FilterTimeSeries,
+    wells: Sequence[str],
+    k_folds: int = 5,
+    method: str = "us",
+    last_cycle: Optional[int] = None,
 ) -> Dict[str, Union[UnivariateCubicSmoothingSpline, scipy.interpolate.UnivariateSpline]]:
     """Returns multiple splines with k-fold crossvalidated smoothing factor
-    
+
     Parameters
     ----------
     fts : FilterTimeSeries
@@ -248,7 +247,7 @@ def get_multiple_splines(
         "k"s for cross-validation
     method : str
         Kind of spline.
-        Choices: "ucss" UnivariateCubicSmoothingSpline, "us" UnivariateSpline 
+        Choices: "ucss" UnivariateCubicSmoothingSpline, "us" UnivariateSpline
 
     Returns
     -------
@@ -259,34 +258,43 @@ def get_multiple_splines(
     if last_cycle == None:
         last_cycle = len(x)
     elif last_cycle > len(x):
-        raise ValueError('Please change last_cycle.')  
+        raise ValueError("Please change last_cycle.")
+
     def get_spline_parallel(arg):
         well, timepoints, values, k_folds = arg
         spline = get_crossvalidated_spline(timepoints, values, k_folds, method=method)
         return (well, spline)
+
     args_get_spline = []
     for well in wells:
         timepoints, values = fts.get_timeseries(well)
-        args = (copy.deepcopy(well), copy.deepcopy(timepoints[:last_cycle]), copy.deepcopy(values[:last_cycle]), copy.deepcopy(k_folds))
+        args = (
+            copy.deepcopy(well),
+            copy.deepcopy(timepoints[:last_cycle]),
+            copy.deepcopy(values[:last_cycle]),
+            copy.deepcopy(k_folds),
+        )
         args_get_spline.append(args)
     if len(wells) > 1:
         try:
-            return joblib.Parallel(n_jobs=multiprocessing.cpu_count(), verbose=11)(map(joblib.delayed(get_spline_parallel), args_get_spline))
+            return joblib.Parallel(n_jobs=multiprocessing.cpu_count(), verbose=11)(
+                map(joblib.delayed(get_spline_parallel), args_get_spline)
+            )
         except pickle.PicklingError:
             logger.warning("Parallelization failed. Retrying without parallelization.")
     return list(map(get_spline_parallel, args_get_spline))
 
 
 def get_mue(
-    bsdata:FilterTimeSeries,
-    wells: Union[Sequence[str], str]='all',
-    blank: Union[float, str, Dict[str, float]]='first',
-    k_folds:int=5,
-    method:str='us',
-    last_cycle:Optional[int]=None
+    bsdata: FilterTimeSeries,
+    wells: Union[Sequence[str], str] = "all",
+    blank: Union[float, str, Dict[str, float]] = "first",
+    k_folds: int = 5,
+    method: str = "us",
+    last_cycle: Optional[int] = None,
 ):
     """Approximation of specific growth rate over time via spline approximation using splines with k-fold cross validated smoothing factor
-    
+
     Parameters
     ----------
     bsdata : FilterTimeSeries
@@ -319,20 +327,20 @@ def get_mue(
         of if an invalid blank option is passed.
     """
     # check inputs
-    if wells == 'all':
+    if wells == "all":
         wells = list(bsdata.time.columns)
-    if blank == 'first':
-        blank_dict = {well:data.iloc[0] for well, data in bsdata.value.iteritems()}
+    if blank == "first":
+        blank_dict = {well: data.iloc[0] for well, data in bsdata.value.iteritems()}
     elif isinstance(blank, numbers.Number):
-        blank_dict = {well:blank for well, data in bsdata.value.iteritems()}
+        blank_dict = {well: blank for well, data in bsdata.value.iteritems()}
     elif isinstance(blank, dict):
         if set(blank.keys()) != set(wells):
-            raise ValueError('Please provide blanks for every well')
+            raise ValueError("Please provide blanks for every well")
         blank_dict = blank
     else:
-        raise ValueError('Please provide proper blank option.')
+        raise ValueError("Please provide proper blank option.")
     # run spline fitting
-    results = get_multiple_splines(bsdata, wells, method=method, last_cycle=last_cycle)   
+    results = get_multiple_splines(bsdata, wells, method=method, last_cycle=last_cycle)
     # compute derivatives
     time = {}
     mues = {}
@@ -340,11 +348,11 @@ def get_mue(
         well = result[0]
         spline = result[1]
         der = spline.derivative(1)
-        if blank == 'first':
+        if blank == "first":
             time[well] = bsdata.time[well][1:last_cycle]
         else:
             time[well] = bsdata.time[well][:last_cycle]
-        mues[well] = der(time[well])/(spline(time[well]) - blank_dict[well])      
+        mues[well] = der(time[well]) / (spline(time[well]) - blank_dict[well])
     # summarize into FilterTimeSeries
     filtertimeseries = FilterTimeSeries(
         pandas.DataFrame(time),
diff --git a/bletl/types.py b/bletl/types.py
index a79689d..6469c57 100644
--- a/bletl/types.py
+++ b/bletl/types.py
@@ -1,17 +1,18 @@
 """Specifies the base types for parsing and representing BioLector CSV files."""
 import abc
 import enum
-import numpy
-import pandas
 import pathlib
 import typing
 import warnings
-
 from typing import Dict, Optional, Tuple
 
+import numpy
+import pandas
+
 
 class BioLectorModel(enum.Enum):
-    """ Enumeration of BioLector Models. """
+    """Enumeration of BioLector Models."""
+
     BL1 = "bl1"
     BL2 = "bl2"
     BLPro = "blpro"
@@ -20,6 +21,7 @@ class BioLectorModel(enum.Enum):
 
 class BLData(dict):
     """Standardized data type for BioLector data."""
+
     def __init__(self, model, environment, filtersets, references, measurements, comments):
         self._model = model
         self._environment = environment
@@ -31,66 +33,66 @@ def __init__(self, model, environment, filtersets, references, measurements, com
 
     @property
     def model(self) -> BioLectorModel:
-        """ BioLector model that the dataset was acquired with. """
+        """BioLector model that the dataset was acquired with."""
         return self._model
 
     @property
     def environment(self) -> pandas.DataFrame:
-        """ Temperature, humidity etc. measurements. """
+        """Temperature, humidity etc. measurements."""
         return self._environment
 
     @property
     def filtersets(self) -> pandas.DataFrame:
-        """ Filtersets that were used in this process. """
+        """Filtersets that were used in this process."""
         return self._filtersets
 
     @property
     def wells(self) -> typing.Tuple[str]:
-        """ Wells that were measured. """
+        """Wells that were measured."""
         if len(self) == 0:
             return tuple()
         return tuple(self.values())[0].wells
 
     @property
     def references(self) -> pandas.DataFrame:
-        """ Reference measurements that are used for calibration. """
+        """Reference measurements that are used for calibration."""
         return self._references
 
     @property
     def measurements(self) -> pandas.DataFrame:
-        """ Well-wise filterset measurements. """
+        """Well-wise filterset measurements."""
         return self._measurements
 
     @property
     def comments(self) -> pandas.DataFrame:
-        """ User and system comments. """
+        """User and system comments."""
         return self._comments
 
     def get_narrow_data(self) -> pandas.DataFrame:
         """Retrieves data in a narrow format.
-        
+
         Returns
         -------
         narrow : pandas.DataFrame
             Data in a narrow format.
         """
-        narrow = pandas.DataFrame(columns=['well', 'filterset', 'time', 'value'])
+        narrow = pandas.DataFrame(columns=["well", "filterset", "time", "value"])
 
         for filterset, filtertimeseries in self.items():
-            to_add = pandas.melt(filtertimeseries.time, value_name='time')
-            to_add['value'] = pandas.melt(filtertimeseries.value, value_name='value').loc[:, 'value']
-            to_add['filterset'] = filterset
-            to_add.astype({'value': float})
+            to_add = pandas.melt(filtertimeseries.time, value_name="time")
+            to_add["value"] = pandas.melt(filtertimeseries.value, value_name="value").loc[:, "value"]
+            to_add["filterset"] = filterset
+            to_add.astype({"value": float})
             narrow = narrow.append(to_add, sort=False)
 
         return narrow.reset_index()
 
     def get_unified_narrow_data(
         self,
-        source_well: str='first',
-        source_filterset: str='first',
+        source_well: str = "first",
+        source_filterset: str = "first",
         *,
-        last_cycles: Optional[Dict[str, int]]=None
+        last_cycles: Optional[Dict[str, int]] = None,
     ) -> pandas.DataFrame:
         """Retrieves data with unified time in a narrow format. Each filterset forms a seperate column.
 
@@ -113,59 +115,60 @@ def get_unified_narrow_data(
         ------
         KeyError
             If specified source filterset or well cannot be found.
-        """        
-        if source_filterset == 'first':
+        """
+        if source_filterset == "first":
             _source_filterset = list(self.keys())[0]
         else:
             if not source_filterset in self.keys():
-                raise KeyError(f'Specified source filterset "{source_filterset}" not found.') 
+                raise KeyError(f'Specified source filterset "{source_filterset}" not found.')
             _source_filterset = source_filterset
-        
-        if source_well == 'first':
+
+        if source_well == "first":
             _source_well = self[_source_filterset].time.columns[0]
         else:
             if not source_well in self[_source_filterset].time.columns:
-                raise KeyError(f'Specified source well "{source_well}" not found.') 
+                raise KeyError(f'Specified source well "{source_well}" not found.')
             _source_well = source_well
-        
+
         u_narrow = pandas.DataFrame(
-            columns=['well', 'cycle', 'time'] + list(self.keys()),
+            columns=["well", "cycle", "time"] + list(self.keys()),
         )
 
         wells = self[_source_filterset].time.columns
         cycles = self[_source_filterset].time.index
         times = self[_source_filterset].time.loc[:, _source_well].astype(float)
 
-        u_narrow['well'] = [well for well in wells for _ in cycles]
-        u_narrow['cycle'] = [cycle for _ in wells for cycle in cycles]
-        u_narrow['time'] = [time for _ in wells for time in times]
-        
-        u_narrow = u_narrow.set_index(['well', 'cycle'])
+        u_narrow["well"] = [well for well in wells for _ in cycles]
+        u_narrow["cycle"] = [cycle for _ in wells for cycle in cycles]
+        u_narrow["time"] = [time for _ in wells for time in times]
+
+        u_narrow = u_narrow.set_index(["well", "cycle"])
 
         for filterset, filtertimeseries in self.items():
             fcycles = filtertimeseries.value.index
             fwells = filtertimeseries.value.columns
-            
+
             molten_values = filtertimeseries.value.melt(value_name=filterset)
-            molten_values['cycle'] = [cycle for _ in fwells for cycle in fcycles]
-            molten_values = molten_values.set_index(['well', 'cycle'])
+            molten_values["cycle"] = [cycle for _ in fwells for cycle in fcycles]
+            molten_values = molten_values.set_index(["well", "cycle"])
             u_narrow.update(molten_values)
 
-        u_narrow = u_narrow.astype(dict(zip(self.keys(), [float]*len(self.keys()))))
+        u_narrow = u_narrow.astype(dict(zip(self.keys(), [float] * len(self.keys()))))
         u_narrow = u_narrow.reset_index()
 
         if last_cycles:
             for well, last_cycle in last_cycles.items():
                 u_narrow.drop(
-                    u_narrow[(u_narrow.well == well) & (u_narrow.cycle > last_cycle)].index,
-                    inplace=True
+                    u_narrow[(u_narrow.well == well) & (u_narrow.cycle > last_cycle)].index, inplace=True
                 )
 
         return u_narrow
 
-    def get_timeseries(self, filterset:str, well:str, *, last_cycle: Optional[int]=None) -> Tuple[numpy.ndarray, numpy.ndarray]:
+    def get_timeseries(
+        self, filterset: str, well: str, *, last_cycle: Optional[int] = None
+    ) -> Tuple[numpy.ndarray, numpy.ndarray]:
         """Retrieves (time, value) for a specific well in a specified filterset.
-        
+
         Parameters
         ----------
         filterset : str
@@ -185,26 +188,29 @@ def get_timeseries(self, filterset:str, well:str, *, last_cycle: Optional[int]=N
         return self[filterset].get_timeseries(well, last_cycle=last_cycle)
 
     def __repr__(self):
-        return f'BLData(model={self.model.name})' + ' {\n' + '\n'.join([
-            f'  "{key}": {fts.__repr__()},'
-            for key, fts in self.items()
-        ]) + '\n}'
+        return (
+            f"BLData(model={self.model.name})"
+            + " {\n"
+            + "\n".join([f'  "{key}": {fts.__repr__()},' for key, fts in self.items()])
+            + "\n}"
+        )
 
 
 class FilterTimeSeries:
     """Generalizable data type for calibrated timeseries."""
+
     @property
     def wells(self) -> typing.Tuple[str]:
-        """ Well IDs that were measured. """
+        """Well IDs that were measured."""
         return tuple(self.time.columns)
 
     def __init__(self, time_df: pandas.DataFrame, value_df: pandas.DataFrame):
         self.time = time_df
         self.value = value_df
 
-    def get_timeseries(self, well:str, *, last_cycle:int=None) -> Tuple[numpy.ndarray, numpy.ndarray]:
+    def get_timeseries(self, well: str, *, last_cycle: int = None) -> Tuple[numpy.ndarray, numpy.ndarray]:
         """Retrieves (time, value) for a specific well.
-        
+
         Parameters
         ----------
         well : str
@@ -220,12 +226,12 @@ def get_timeseries(self, well:str, *, last_cycle:int=None) -> Tuple[numpy.ndarra
             Measured values.
         """
         if last_cycle is not None and last_cycle <= 0:
-            raise ValueError(f'last_cycle must be > 0')
+            raise ValueError(f"last_cycle must be > 0")
         x = numpy.array(self.time[well])[:last_cycle]
         y = numpy.array(self.value[well])[:last_cycle]
         return x, y
 
-    def get_unified_dataframe(self, well: Optional[str]=None) -> pandas.DataFrame:
+    def get_unified_dataframe(self, well: Optional[str] = None) -> pandas.DataFrame:
         """Retrieves a DataFrame with unified time on index.
 
         Parameters
@@ -241,17 +247,17 @@ def get_unified_dataframe(self, well: Optional[str]=None) -> pandas.DataFrame:
         """
         if not well is None:
             if not well in self.time.columns:
-                raise KeyError('Could not find well id')
+                raise KeyError("Could not find well id")
             time = self.time.loc[:, well]
         else:
             time = self.time.iloc[:, 0]
 
-        new_index = pandas.Index(time, name='time in h')
+        new_index = pandas.Index(time, name="time in h")
         unified_df = self.value.set_index(new_index)
         return unified_df
 
     def __repr__(self):
-        return f'FilterTimeSeries({len(self.time)} cycles, {len(self.time.columns)} wells)'
+        return f"FilterTimeSeries({len(self.time)} cycles, {len(self.time.columns)} wells)"
 
 
 class BLDParser:
@@ -261,16 +267,17 @@ class BLDParser:
 
     @abc.abstractmethod
     def parse(
-        self, filepath:pathlib.Path,
+        self,
+        filepath: pathlib.Path,
         *,
-        lot_number:Optional[int]=None,
-        temp:Optional[int]=None,
-        cal_0:Optional[float]=None,
-        cal_100:Optional[float]=None,
-        phi_min:Optional[float]=None,
-        phi_max:Optional[float]=None,
-        pH_0:Optional[float]=None,
-        dpH:Optional[float]=None,
+        lot_number: Optional[int] = None,
+        temp: Optional[int] = None,
+        cal_0: Optional[float] = None,
+        cal_100: Optional[float] = None,
+        phi_min: Optional[float] = None,
+        phi_max: Optional[float] = None,
+        pH_0: Optional[float] = None,
+        dpH: Optional[float] = None,
     ) -> BLData:
         """Parses the provided BioLector CSV file into a data object.
 
diff --git a/bletl/utils.py b/bletl/utils.py
index a080656..815be8a 100644
--- a/bletl/utils.py
+++ b/bletl/utils.py
@@ -1,14 +1,16 @@
 """Contains helper functions that do not depend on other modules within this package."""
 import datetime
-import pandas
+import pathlib
 import re
 import urllib
-import pathlib
 from typing import Sequence, Tuple, Union
 
-from . import core 
+import pandas
+
+from . import core
 
-def __to_typed_cols__(dfin:pandas.DataFrame, ocol_ncol_type:Tuple[str, str, type]) -> pandas.DataFrame:
+
+def __to_typed_cols__(dfin: pandas.DataFrame, ocol_ncol_type: Tuple[str, str, type]) -> pandas.DataFrame:
     """Can be used to filter & convert data frame columns.
 
     Parameters
@@ -51,7 +53,7 @@ def _unindex(dataframe: pandas.DataFrame) -> Tuple[Sequence[Union[str, None]], p
     return dataframe.index.names, dataframe.reset_index()
 
 
-def _reindex(dataframe:pandas.DataFrame, index_names: Sequence[str]) -> pandas.DataFrame:
+def _reindex(dataframe: pandas.DataFrame, index_names: Sequence[str]) -> pandas.DataFrame:
     """Applies an indexing scheme to a DataFrame.
 
     Parameters
@@ -72,7 +74,9 @@ def _reindex(dataframe:pandas.DataFrame, index_names: Sequence[str]) -> pandas.D
         return dataframe
 
 
-def _concatenate_fragments(fragments:Sequence[pandas.DataFrame], start_times:Sequence[datetime.datetime]) -> pandas.DataFrame:
+def _concatenate_fragments(
+    fragments: Sequence[pandas.DataFrame], start_times: Sequence[datetime.datetime]
+) -> pandas.DataFrame:
     """Concatenate multiple dataframes while shifting time and cycles.
 
     Parameters
@@ -91,16 +95,16 @@ def _concatenate_fragments(fragments:Sequence[pandas.DataFrame], start_times:Seq
     columns = set(stack.columns)
 
     for fragment, fragment_start in zip(fragments[1:], start_times[1:]):
-        assert isinstance(fragment, pandas.DataFrame), 'fragments must be a list of DataFrames'
+        assert isinstance(fragment, pandas.DataFrame), "fragments must be a list of DataFrames"
         index_names_f, fragment = _unindex(fragment)
-        assert set(index_names_f) == set(index_names), 'indices must match across all fragments'
-        assert set(fragment.columns) == columns, 'columns must match across all fragments'
+        assert set(index_names_f) == set(index_names), "indices must match across all fragments"
+        assert set(fragment.columns) == columns, "columns must match across all fragments"
 
         # shift time and cycle columns in the fragment
-        if 'time' in columns:
-            fragment['time'] += (fragment_start - start_times[0]).total_seconds() / 3600
-        if 'cycle' in columns and len(stack) > 0:
-            fragment['cycle'] += max(stack['cycle'])
+        if "time" in columns:
+            fragment["time"] += (fragment_start - start_times[0]).total_seconds() / 3600
+        if "cycle" in columns and len(stack) > 0:
+            fragment["cycle"] += max(stack["cycle"])
 
         # append the fragment to the stack
         stack = pandas.concat((stack, fragment))
@@ -109,7 +113,7 @@ def _concatenate_fragments(fragments:Sequence[pandas.DataFrame], start_times:Seq
     return _reindex(stack, index_names)
 
 
-def _last_well_in_cycle(measurements:pandas.DataFrame) -> str:
+def _last_well_in_cycle(measurements: pandas.DataFrame) -> str:
     """Finds the name of the last well measured in a cycle.
 
     Parameters
@@ -133,7 +137,7 @@ def _last_well_in_cycle(measurements:pandas.DataFrame) -> str:
     return previous_well
 
 
-def _last_full_cycle(measurements:pandas.DataFrame) -> int:
+def _last_full_cycle(measurements: pandas.DataFrame) -> int:
     """Find the number of the last cycle that was measured for all wells and filters.
 
     Parameters
@@ -155,12 +159,12 @@ def _last_full_cycle(measurements:pandas.DataFrame) -> int:
     last_well = measurements.iloc[-1].well
 
     if (last_filter, last_well) != (max_filter, max_well):
-        return last_cycle  - 1
+        return last_cycle - 1
     else:
         return last_cycle
 
 
-def _parse_calibration_info(calibration_info:str):
+def _parse_calibration_info(calibration_info: str):
     """Extracts lot number and temperature for a line of calibration info.
 
     Parameters
@@ -175,7 +179,7 @@ def _parse_calibration_info(calibration_info:str):
     temp : int
         Process temperature
     """
-    result = re.findall(r'(\d*)-hc-Temp(\d{2})', calibration_info)
+    result = re.findall(r"(\d*)-hc-Temp(\d{2})", calibration_info)
     lot_number = int(result[0][0])
     temp = int(result[0][1])
 
@@ -192,18 +196,18 @@ def download_calibration_data() -> bool:
     """
     try:
         module_path = pathlib.Path(core.__spec__.origin).parents[0]
-        
-        url_bl1 = 'http://updates.m2p-labs.com/CalibrationLot.ini'
-        filepath_bl1 = pathlib.Path(module_path, 'cache', 'CalibrationLot.ini')
+
+        url_bl1 = "http://updates.m2p-labs.com/CalibrationLot.ini"
+        filepath_bl1 = pathlib.Path(module_path, "cache", "CalibrationLot.ini")
         filepath_bl1.parents[0].mkdir(exist_ok=True)
         urllib.request.urlretrieve(url_bl1, filepath_bl1)
 
-        url_blpro = 'http://updates.m2p-labs.com/CalibrationLot_II.xml'
-        filepath_blpro = pathlib.Path(module_path, 'cache', 'CalibrationLot_II.xml')
+        url_blpro = "http://updates.m2p-labs.com/CalibrationLot_II.xml"
+        filepath_blpro = pathlib.Path(module_path, "cache", "CalibrationLot_II.xml")
         filepath_blpro.parents[0].mkdir(exist_ok=True)
         urllib.request.urlretrieve(url_blpro, filepath_blpro)
-        
+
         return True
-    
+
     except urllib.error.HTTPError:
         return False
diff --git a/notebooks/models.py b/notebooks/models.py
index f8dc71f..ef1a6da 100644
--- a/notebooks/models.py
+++ b/notebooks/models.py
@@ -1,18 +1,19 @@
-import calibr8
 import pathlib
 import urllib.request
 
+import calibr8
+
+
 class BLProCDWBackscatterModelV1(calibr8.BaseLogIndependentAsymmetricLogisticT):
-    def __init__(self, *, independent_key:str='X', dependent_key:str='Pahpshmir_1400_BS3_CgWT'):
+    def __init__(self, *, independent_key: str = "X", dependent_key: str = "Pahpshmir_1400_BS3_CgWT"):
         super().__init__(independent_key=independent_key, dependent_key=dependent_key, scale_degree=1)
 
 
-
 def get_biomass_calibration() -> BLProCDWBackscatterModelV1:
     fp_savefile = pathlib.Path("Quickstart_04_biomass_calibration.json")
     if not fp_savefile.exists():
         urllib.request.urlretrieve(
             "https://raw.githubusercontent.com/JuBiotech/calibr8-paper/main/data_and_analysis/processed/biomass.json",
-            fp_savefile
+            fp_savefile,
         )
     return BLProCDWBackscatterModelV1.load(fp_savefile)
diff --git a/pyproject.toml b/pyproject.toml
index e2f2d78..9046bc1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,4 +5,7 @@ requires = ["setuptools", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [tool.black]
-line-length = 110
\ No newline at end of file
+line-length = 110
+
+[tool.isort]
+profile = "black"
diff --git a/requirements.txt b/requirements.txt
index 62104c5..910a4b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,4 @@ joblib
 numpy
 pandas
 scipy
-tsfresh
\ No newline at end of file
+tsfresh
diff --git a/setup.py b/setup.py
index d0e4ddc..1908ff6 100644
--- a/setup.py
+++ b/setup.py
@@ -1,41 +1,44 @@
 import pathlib
+
 import setuptools
 
-__packagename__ = 'bletl'
+__packagename__ = "bletl"
 ROOT = pathlib.Path(__file__).parent
 
 
 def get_version():
-    import os, re
-    VERSIONFILE = os.path.join(__packagename__, '__init__.py')
-    initfile_lines = open(VERSIONFILE, 'rt').readlines()
+    import os
+    import re
+
+    VERSIONFILE = os.path.join(__packagename__, "__init__.py")
+    initfile_lines = open(VERSIONFILE, "rt").readlines()
     VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
     for line in initfile_lines:
         mo = re.search(VSRE, line, re.M)
         if mo:
             return mo.group(1)
-    raise RuntimeError('Unable to find version string in %s.' % (VERSIONFILE,))
+    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))
+
 
 __version__ = get_version()
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     setuptools.setup(
-        name = __packagename__,
-        packages = setuptools.find_packages(),
+        name=__packagename__,
+        packages=setuptools.find_packages(),
         zip_safe=False,
         version=__version__,
-        description='Package for parsing and transforming BioLector raw data.',
-        url='https://github.com/jubiotech/bletl',
-        author='Michael Osthege',
-        author_email='m.osthege@fz-juelich.de',
-        license='GNU Affero General Public License v3.0',
-        classifiers= [
-            'Programming Language :: Python',
-            'Operating System :: OS Independent',
-            'Programming Language :: Python :: 3.8',
-            'Intended Audience :: Science/Research',
-            'License :: OSI Approved :: GNU Affero General Public License v3',
+        description="Package for parsing and transforming BioLector raw data.",
+        url="https://github.com/jubiotech/bletl",
+        author="Michael Osthege",
+        author_email="m.osthege@fz-juelich.de",
+        license="GNU Affero General Public License v3.0",
+        classifiers=[
+            "Programming Language :: Python",
+            "Operating System :: OS Independent",
+            "Programming Language :: Python :: 3.8",
+            "Intended Audience :: Science/Research",
+            "License :: OSI Approved :: GNU Affero General Public License v3",
         ],
-        install_requires=open(pathlib.Path(ROOT, 'requirements.txt')).readlines(),
+        install_requires=open(pathlib.Path(ROOT, "requirements.txt")).readlines(),
     )
-


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 8aaf23b..a566efa 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,68 +1,74 @@
 """Contains unit tests for the `bletl` package"""
-import numpy
+import datetime
 import pathlib
-from numpy import random
+
+import numpy
 import pandas
 import pytest
-import datetime
+from numpy import random
 
 import bletl
-from bletl import core
-from bletl import parsing
-from bletl import utils
-
+from bletl import core, parsing, utils
 from bletl.parsing import bl1
 
-dir_testfiles = pathlib.Path(pathlib.Path(__file__).absolute().parent, 'data')
+dir_testfiles = pathlib.Path(pathlib.Path(__file__).absolute().parent, "data")
 
-BL1_files =  [
-    pathlib.Path(dir_testfiles, 'BL1', 'NT_1400rpm_30C_BS15_5min_20180618_102917.csv'),
-    pathlib.Path(dir_testfiles, 'BL1', 'JH_ShakerSteps_20170302_070206.csv'),
-    pathlib.Path(dir_testfiles, 'BL1', 'rj-cg-res_20170927_084112.csv'),
-    pathlib.Path(dir_testfiles, 'BL1', 'incremental', 'NT_1400rpm_30C_BS15_5min_20180503_132133.csv'),
+BL1_files = [
+    pathlib.Path(dir_testfiles, "BL1", "NT_1400rpm_30C_BS15_5min_20180618_102917.csv"),
+    pathlib.Path(dir_testfiles, "BL1", "JH_ShakerSteps_20170302_070206.csv"),
+    pathlib.Path(dir_testfiles, "BL1", "rj-cg-res_20170927_084112.csv"),
+    pathlib.Path(dir_testfiles, "BL1", "incremental", "NT_1400rpm_30C_BS15_5min_20180503_132133.csv"),
 ]
 
 BL1_fragment_files = [
-    pathlib.Path(dir_testfiles, 'BL1', 'fragments', 'fragment0.csv'),
-    pathlib.Path(dir_testfiles, 'BL1', 'fragments', 'fragment1.csv'),
-    pathlib.Path(dir_testfiles, 'BL1', 'fragments', 'fragment2.csv'),
+    pathlib.Path(dir_testfiles, "BL1", "fragments", "fragment0.csv"),
+    pathlib.Path(dir_testfiles, "BL1", "fragments", "fragment1.csv"),
+    pathlib.Path(dir_testfiles, "BL1", "fragments", "fragment2.csv"),
 ]
 
 BL1_files_without_calibration_info = [
-    pathlib.Path(dir_testfiles, 'BL1', 'NT_1400_BS20BS10_15min_20160222_151645.csv'),
+    pathlib.Path(dir_testfiles, "BL1", "NT_1400_BS20BS10_15min_20160222_151645.csv"),
 ]
 
-not_a_bl_file = pathlib.Path(dir_testfiles, 'BL1', 'incremental', 'C42.tmp')
+not_a_bl_file = pathlib.Path(dir_testfiles, "BL1", "incremental", "C42.tmp")
 
-BL2_files = list(pathlib.Path(dir_testfiles, 'BLII').iterdir())
-BLPro_files = list(pathlib.Path(dir_testfiles, 'BLPro').iterdir())
-calibration_test_file = pathlib.Path(dir_testfiles, 'BLPro', '18-FZJ-Test2--2018-02-07-10-01-11.csv')
-incompatible_file = pathlib.Path(dir_testfiles, 'incompatible_files', 'BL2-file-saved-with-biolection.csv')
+BL2_files = list(pathlib.Path(dir_testfiles, "BLII").iterdir())
+BLPro_files = list(pathlib.Path(dir_testfiles, "BLPro").iterdir())
+calibration_test_file = pathlib.Path(dir_testfiles, "BLPro", "18-FZJ-Test2--2018-02-07-10-01-11.csv")
+incompatible_file = pathlib.Path(dir_testfiles, "incompatible_files", "BL2-file-saved-with-biolection.csv")
 
-calibration_test_file_pro = pathlib.Path(dir_testfiles, 'BLPro', '18-FZJ-Test2--2018-02-07-10-01-11.csv')
-calibration_test_file_pro2 = pathlib.Path(dir_testfiles, 'BLPro', '8-HM_CoryneBatch-HM-2018-04-11-14-52-54.csv')
+calibration_test_file_pro = pathlib.Path(dir_testfiles, "BLPro", "18-FZJ-Test2--2018-02-07-10-01-11.csv")
+calibration_test_file_pro2 = pathlib.Path(
+    dir_testfiles, "BLPro", "8-HM_CoryneBatch-HM-2018-04-11-14-52-54.csv"
+)
 
-calibration_test_file = pathlib.Path(dir_testfiles, 'BL1', 'NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv')
+calibration_test_file = pathlib.Path(
+    dir_testfiles, "BL1", "NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv"
+)
 calibration_test_cal_data = {
-    'cal_0': 65.91,
-    'cal_100': 40.60,
-    'phi_min': 57.45,
-    'phi_max': 18.99,
-    'pH_0': 6.46,
-    'dpH': 0.56,
+    "cal_0": 65.91,
+    "cal_100": 40.60,
+    "phi_min": 57.45,
+    "phi_max": 18.99,
+    "pH_0": 6.46,
+    "dpH": 0.56,
 }
-calibration_test_times = {'BS10': (52, 'D03', 10.5221)}
+calibration_test_times = {"BS10": (52, "D03", 10.5221)}
 calibration_test_values = {
-    'BS10': (5, 'A04', 11.7175),
-    'DO': (13, 'A05', 99.4285),
-    'pH': (39, 'D08', 7.06787),
-    'GFP75': (81, 'F07', 216.99),
+    "BS10": (5, "A04", 11.7175),
+    "DO": (13, "A05", 99.4285),
+    "pH": (39, "D08", 7.06787),
+    "GFP75": (81, "F07", 216.99),
 }
 calibration_test_lot_number = 1515
 calibration_test_temp = 30
 
-file_with_lot_info = pathlib.Path(dir_testfiles, 'BL1', 'example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv')
-file_with_no_measurements = pathlib.Path(dir_testfiles, 'BL1', 'broken_or_incomplete', 'file_with_no_measurements.csv')
+file_with_lot_info = pathlib.Path(
+    dir_testfiles, "BL1", "example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv"
+)
+file_with_no_measurements = pathlib.Path(
+    dir_testfiles, "BL1", "broken_or_incomplete", "file_with_no_measurements.csv"
+)
 
 
 class TestParserSelection:
@@ -100,24 +106,28 @@ def test_incompatible_file_detecion(self):
 
 class TestUtils:
     def test_last_well_in_cycle(self):
-        measurements = pandas.DataFrame(data={
-            'cycle': [1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2],
-            'filterset': [1,1,1,1,2,2,2,2,3,3,3,3] * 2,
-            'well': ['A01', 'A02', 'A03', 'A04'] * 3 * 2
-        })
+        measurements = pandas.DataFrame(
+            data={
+                "cycle": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+                "filterset": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3] * 2,
+                "well": ["A01", "A02", "A03", "A04"] * 3 * 2,
+            }
+        )
         last_well = utils._last_well_in_cycle(measurements)
-        assert last_well == 'A04'
+        assert last_well == "A04"
 
         # cut off the last two measurements
         measurements = measurements.iloc[:-2]
         return
 
     def test_last_full_cycle(self):
-        measurements = pandas.DataFrame(data={
-            'cycle': [1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2],
-            'filterset': [1,1,1,1,2,2,2,2,3,3,3,3] * 2,
-            'well': ['A01', 'A02', 'A03', 'A04'] * 3 * 2
-        })
+        measurements = pandas.DataFrame(
+            data={
+                "cycle": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
+                "filterset": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3] * 2,
+                "well": ["A01", "A02", "A03", "A04"] * 3 * 2,
+            }
+        )
 
         last_cycle = utils._last_full_cycle(measurements)
         assert last_cycle == 2
@@ -130,7 +140,7 @@ def test_last_full_cycle(self):
         return
 
     def test_cal_info_parsing(self):
-        example = '1724-hc-Temp37'
+        example = "1724-hc-Temp37"
         lot_number, temp = utils._parse_calibration_info(example)
         assert lot_number == 1724
         assert temp == 37
@@ -140,7 +150,7 @@ def test_cal_info_parsing(self):
 class TestBL1Parsing:
     def test_splitting(self):
         for fp in BL1_files:
-            with open(fp, 'r', encoding='latin-1') as f:
+            with open(fp, "r", encoding="latin-1") as f:
                 lines = f.readlines()
 
             headerlines, data = parsing.bl1.split_header_data(fp)
@@ -176,45 +186,44 @@ def test_concat_parsing(self):
     def test_incomplete_cycle_drop(self):
         filepath = BL1_files[2]
         data = bletl.parse(filepath, drop_incomplete_cycles=False)
-        assert data.measurements.index[-1] == (3, 179, 'C08')
+        assert data.measurements.index[-1] == (3, 179, "C08")
 
         data = bletl.parse(filepath, drop_incomplete_cycles=True)
-        assert data.measurements.index[-1] == (3, 178, 'F01')
+        assert data.measurements.index[-1] == (3, 178, "F01")
         return
 
     def test_temp_setpoint_parsing(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'NT_1400rpm_30C_BS15_5min_20180618_102917.csv')
+        fp = pathlib.Path(dir_testfiles, "BL1", "NT_1400rpm_30C_BS15_5min_20180618_102917.csv")
         data = bletl.parse(fp)
         df = data.environment
-        temps = set(df['temp_setpoint'].unique())
-        assert temps == set([30., 25., 31., 32., 33., 34., 35., 36., 37., 38., 39., 40.])
+        temps = set(df["temp_setpoint"].unique())
+        assert temps == set([30.0, 25.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0])
         # test some individual values
-        assert list(df.loc[numpy.isclose(df['time'], 0.00778), 'temp_setpoint'])[0] == 30.
-        assert list(df.loc[numpy.isclose(df['time'], 55.30764), 'temp_setpoint'])[0] == 37.
-        assert list(df.loc[numpy.isclose(df['time'], 55.44867), 'temp_setpoint'])[0] == 38.
-        assert list(df.loc[numpy.isclose(df['time'], 126.74621), 'temp_setpoint'])[0] == 40.
+        assert list(df.loc[numpy.isclose(df["time"], 0.00778), "temp_setpoint"])[0] == 30.0
+        assert list(df.loc[numpy.isclose(df["time"], 55.30764), "temp_setpoint"])[0] == 37.0
+        assert list(df.loc[numpy.isclose(df["time"], 55.44867), "temp_setpoint"])[0] == 38.0
+        assert list(df.loc[numpy.isclose(df["time"], 126.74621), "temp_setpoint"])[0] == 40.0
         return
 
     def test_shaker_setpoint_parsing(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'JH_ShakerSteps_20170302_070206.csv')
+        fp = pathlib.Path(dir_testfiles, "BL1", "JH_ShakerSteps_20170302_070206.csv")
         data = bletl.parse(fp)
         df = data.environment
-        rpms = set(df['shaker_setpoint'].unique())
-        assert rpms == set([
-           500.,  600.,  700.,  800.,  900., 1000.,
-           1100., 1200., 1300., 1400., 1500.
-        ])
+        rpms = set(df["shaker_setpoint"].unique())
+        assert rpms == set(
+            [500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0, 1200.0, 1300.0, 1400.0, 1500.0]
+        )
         # test some individual values
-        assert list(df.loc[numpy.isclose(df['time'], 0.06265), 'shaker_setpoint'])[0] == 500.
-        assert list(df.loc[numpy.isclose(df['time'], 2.49103), 'shaker_setpoint'])[0] == 900.
-        assert list(df.loc[numpy.isclose(df['time'], 2.50949), 'shaker_setpoint'])[0] == 1000.
-        assert list(df.loc[numpy.isclose(df['time'], 5.65632), 'shaker_setpoint'])[0] == 500.
+        assert list(df.loc[numpy.isclose(df["time"], 0.06265), "shaker_setpoint"])[0] == 500.0
+        assert list(df.loc[numpy.isclose(df["time"], 2.49103), "shaker_setpoint"])[0] == 900.0
+        assert list(df.loc[numpy.isclose(df["time"], 2.50949), "shaker_setpoint"])[0] == 1000.0
+        assert list(df.loc[numpy.isclose(df["time"], 5.65632), "shaker_setpoint"])[0] == 500.0
         return
 
     def test_get_timeseries(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv')
+        fp = pathlib.Path(dir_testfiles, "BL1", "NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv")
         data = bletl.parse(fp, lot_number=calibration_test_lot_number, temp=30)
-        x, y = data['pH'].get_timeseries('A03')
+        x, y = data["pH"].get_timeseries("A03")
         assert len(x) == len(y)
         assert len(x) == 103
         assert numpy.sum(x) == 1098.50712
@@ -222,103 +231,99 @@ def test_get_timeseries(self):
         return
 
     def test_get_timeseries_last_cycle(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv')
+        fp = pathlib.Path(dir_testfiles, "BL1", "NT_1200rpm_30C_DO-GFP75-pH-BS10_12min_20171221_121339.csv")
         data = bletl.parse(fp, lot_number=calibration_test_lot_number, temp=30)
-        
+
         # default to all
-        x, y = data['pH'].get_timeseries('A03')
+        x, y = data["pH"].get_timeseries("A03")
         assert len(x) == len(y)
         assert len(x) == 103
 
         # invalid values
         with pytest.raises(ValueError):
-            data['pH'].get_timeseries('A03', last_cycle=-1)
+            data["pH"].get_timeseries("A03", last_cycle=-1)
 
         with pytest.raises(ValueError):
-            data['pH'].get_timeseries('A03', last_cycle=0)
+            data["pH"].get_timeseries("A03", last_cycle=0)
 
         # valid settings
-        x, y = data['pH'].get_timeseries('A03', last_cycle=1)
+        x, y = data["pH"].get_timeseries("A03", last_cycle=1)
         assert len(x) == 1
         assert len(y) == 1
 
-        x, y = data['pH'].get_timeseries('A03', last_cycle=50)
+        x, y = data["pH"].get_timeseries("A03", last_cycle=50)
         assert len(x) == 50
         assert len(y) == 50
 
         # more than available
-        x, y = data['pH'].get_timeseries('A03', last_cycle=200)
+        x, y = data["pH"].get_timeseries("A03", last_cycle=200)
         assert len(x) == 103
         assert len(y) == 103
         pass
 
     def test_get_unified_dataframe(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv')
+        fp = pathlib.Path(
+            dir_testfiles, "BL1", "example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv"
+        )
         data = bletl.parse(fp)
-        unified_df = data['BS20'].get_unified_dataframe()
+        unified_df = data["BS20"].get_unified_dataframe()
         assert isinstance(unified_df, pandas.DataFrame)
         numpy.testing.assert_approx_equal(unified_df.index[2], 0.35313, significant=4)
         numpy.testing.assert_approx_equal(
-            unified_df.iloc[
-                unified_df.index.get_loc(5, method='nearest'),
-                unified_df.columns.get_loc('A05')
-                ],
+            unified_df.iloc[unified_df.index.get_loc(5, method="nearest"), unified_df.columns.get_loc("A05")],
             63.8517,
             significant=6,
         )
 
     def test_get_narrow_data(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv')
+        fp = pathlib.Path(
+            dir_testfiles, "BL1", "example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv"
+        )
         data = bletl.parse(fp)
         narrow_data = data.get_narrow_data()
         assert isinstance(narrow_data, pandas.DataFrame)
-        numpy.testing.assert_approx_equal(
-            narrow_data.loc[52884, 'value'],
-            102.835,
-            significant=6
-        )
+        numpy.testing.assert_approx_equal(narrow_data.loc[52884, "value"], 102.835, significant=6)
 
     def test_get_unified_narrow_data(self):
-        fp = pathlib.Path(dir_testfiles, 'BL1', 'example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv')
+        fp = pathlib.Path(
+            dir_testfiles, "BL1", "example_with_cal_data_NT_1400rpm_30C_BS20-pH-DO_10min_20180607_115856.csv"
+        )
         data = bletl.parse(fp)
 
         unified_narrow_data_1 = data.get_unified_narrow_data()
         assert isinstance(unified_narrow_data_1, pandas.DataFrame)
-        numpy.testing.assert_approx_equal(
-            unified_narrow_data_1.loc[21771, 'pH'],
-            7.405,
-            significant=4
-        )
+        numpy.testing.assert_approx_equal(unified_narrow_data_1.loc[21771, "pH"], 7.405, significant=4)
 
-        unified_narrow_data_2 = data.get_unified_narrow_data(source_filterset='DO', source_well='B04')
-        numpy.testing.assert_approx_equal(
-            unified_narrow_data_2.loc[0, 'time'],
-            0.09409,
-            significant=5
-        )
+        unified_narrow_data_2 = data.get_unified_narrow_data(source_filterset="DO", source_well="B04")
+        numpy.testing.assert_approx_equal(unified_narrow_data_2.loc[0, "time"], 0.09409, significant=5)
 
         with pytest.raises(KeyError):
-            data.get_unified_narrow_data(source_filterset='machine_that_goes_ping')
-        
+            data.get_unified_narrow_data(source_filterset="machine_that_goes_ping")
+
         with pytest.raises(KeyError):
-            data.get_unified_narrow_data(source_well='O9000')
+            data.get_unified_narrow_data(source_well="O9000")
 
     def test_NoMeasurements_Warning(self):
         with pytest.warns(core.NoMeasurementData):
             bletl.parse(file_with_no_measurements)
 
+
 class TestBL1Calibration:
     def test_calibration_data_type(self):
-        data = bletl.parse(calibration_test_file, lot_number=calibration_test_lot_number, temp=calibration_test_temp)
-        
+        data = bletl.parse(
+            calibration_test_file, lot_number=calibration_test_lot_number, temp=calibration_test_temp
+        )
+
         for _, item in data.items():
             assert isinstance(item, core.FilterTimeSeries)
 
         return
 
     def test_single_file_with_lot(self):
-        data = bletl.parse(calibration_test_file, lot_number=calibration_test_lot_number, temp=calibration_test_temp)
-       
+        data = bletl.parse(
+            calibration_test_file, lot_number=calibration_test_lot_number, temp=calibration_test_temp
+        )
+
         for key, (cycle, well, value) in calibration_test_times.items():
             numpy.testing.assert_approx_equal(data[key].time.loc[cycle, well], value, significant=4)
 
@@ -329,7 +334,7 @@ def test_single_file_with_lot(self):
 
     def test_single_file_with_caldata(self):
         data = bletl.parse(calibration_test_file, **calibration_test_cal_data)
-        
+
         for key, (cycle, well, value) in calibration_test_times.items():
             numpy.testing.assert_approx_equal(data[key].time.loc[cycle, well], value, significant=4)
 
@@ -341,9 +346,9 @@ def test_single_file_with_caldata(self):
     def test_fragments_with_lot(self):
         filepaths = BL1_fragment_files
         data = bletl.parse(filepaths, lot_number=1846, temp=37)
-        
-        numpy.testing.assert_approx_equal(data['DO'].value.loc[666, 'F07'], 12.1887, significant=6)
-        numpy.testing.assert_approx_equal(data['pH'].value.loc[507, 'E06'], 6.6435, significant=5)
+
+        numpy.testing.assert_approx_equal(data["DO"].value.loc[666, "F07"], 12.1887, significant=6)
+        numpy.testing.assert_approx_equal(data["pH"].value.loc[507, "E06"], 6.6435, significant=5)
 
         return
 
@@ -359,9 +364,9 @@ def test_fragments_with_cal_data(self):
             dpH=0.53,
             drop_incomplete_cycles=True,
         )
-        
-        numpy.testing.assert_approx_equal(data['DO'].value.loc[666, 'F07'], 12.1887, significant=6)
-        numpy.testing.assert_approx_equal(data['pH'].value.loc[507, 'E06'], 6.6435, significant=5)
+
+        numpy.testing.assert_approx_equal(data["DO"].value.loc[666, "F07"], 12.1887, significant=6)
+        numpy.testing.assert_approx_equal(data["pH"].value.loc[507, "E06"], 6.6435, significant=5)
 
         return
 
@@ -383,7 +388,7 @@ def test_invalid_lot_number(self):
         return
 
     def test_download_calibration_data(self):
-        utils.download_calibration_data()        
+        utils.download_calibration_data()
         return
 
 
@@ -391,7 +396,7 @@ class TestBL2Parsing:
     def test_parse_metadata_data(self):
         for fp in BL2_files:
             metadata, data = parsing.blpro.parse_metadata_data(fp)
-            
+
             assert isinstance(metadata, dict)
             assert isinstance(data, pandas.DataFrame)
         pass
@@ -404,7 +409,7 @@ def test_parsing(self):
                 assert isinstance(data.wells, tuple)
                 assert len(data.wells) == 48
             except:
-                print('parsing failed for: {}'.format(fp))
+                print("parsing failed for: {}".format(fp))
                 raise
         pass
 
@@ -413,7 +418,7 @@ class TestBLProParsing:
     def test_parse_metadata_data(self):
         for fp in BLPro_files:
             metadata, data = parsing.blpro.parse_metadata_data(fp)
-            
+
             assert isinstance(metadata, dict)
             assert isinstance(data, pandas.DataFrame)
 
@@ -428,41 +433,45 @@ def test_parsing(self):
                 data = bletl.parse(fp)
                 assert isinstance(data, dict)
             except:
-                print('parsing failed for: {}'.format(fp))
+                print("parsing failed for: {}".format(fp))
                 raise
         return
 
     def test_parse_metadata_data_new_format(self):
-        fp = pathlib.Path(dir_testfiles, 'BLPro', 'new_metadata_format.csv')
+        fp = pathlib.Path(dir_testfiles, "BLPro", "new_metadata_format.csv")
         metadata, data = parsing.blpro.parse_metadata_data(fp)
         assert isinstance(metadata, dict)
         assert isinstance(data, pandas.DataFrame)
         pass
 
     def test_parse_new_format(self):
-        fp = pathlib.Path(dir_testfiles, 'BLPro', 'new_metadata_format.csv')
+        fp = pathlib.Path(dir_testfiles, "BLPro", "new_metadata_format.csv")
         bldata = bletl.parse(fp)
-        assert 'BS3' in bldata
-        t, y = bldata['BS3'].get_timeseries('A01')
+        assert "BS3" in bldata
+        t, y = bldata["BS3"].get_timeseries("A01")
         numpy.testing.assert_array_almost_equal(t, [0.01111111, 0.08888889])
         numpy.testing.assert_array_almost_equal(y, [4.19, 1.96])
         pass
 
     def test_parse_with_concat(self):
-        data = bletl.parse(filepaths=[
-                pathlib.Path(dir_testfiles, 'BLPro', '224-MO_Coryne--2019-07-12-16-54-30.csv'),
-                pathlib.Path(dir_testfiles, 'BLPro', '226-MO_Coryne--2019-07-12-17-38-02.csv'),
-        ])
+        data = bletl.parse(
+            filepaths=[
+                pathlib.Path(dir_testfiles, "BLPro", "224-MO_Coryne--2019-07-12-16-54-30.csv"),
+                pathlib.Path(dir_testfiles, "BLPro", "226-MO_Coryne--2019-07-12-17-38-02.csv"),
+            ]
+        )
         assert isinstance(data, bletl.BLData)
-        assert data.metadata['date_start'] == datetime.datetime(2019, 7, 12, 16, 54, 30)
-        assert data.metadata['date_end'] is None
-        numpy.testing.assert_array_equal(data['BS5'].time.index, numpy.arange(1, 4+254+1))
-        numpy.testing.assert_array_almost_equal(data['BS5'].time['A01'][:5], [0.013056, 0.179444, 0.346111, 0.512778, 0.738611])
+        assert data.metadata["date_start"] == datetime.datetime(2019, 7, 12, 16, 54, 30)
+        assert data.metadata["date_end"] is None
+        numpy.testing.assert_array_equal(data["BS5"].time.index, numpy.arange(1, 4 + 254 + 1))
+        numpy.testing.assert_array_almost_equal(
+            data["BS5"].time["A01"][:5], [0.013056, 0.179444, 0.346111, 0.512778, 0.738611]
+        )
         return
 
     def test_parse_file_with_defects(self):
         # this file has some broken & duplicate data line lines 25857-25877
-        bletl.parse(pathlib.Path(dir_testfiles, 'BLPro', 'line_duplication.csv'))
+        bletl.parse(pathlib.Path(dir_testfiles, "BLPro", "line_duplication.csv"))
         pass
 
     def test_refoverld_issue12(self):
@@ -474,73 +483,80 @@ def test_refoverld_issue12(self):
             assert len(y) == n
         pass
 
+
 class TestBLProCalibration:
     def test_filtertimeseries_presence(self):
         bd = bletl.parse(calibration_test_file_pro)
         assert isinstance(bd, dict)
-        assert ('BS2' in bd)
-        assert ('BS5' in bd)
-        assert ('pH' in bd)
-        assert ('DO' in bd)
+        assert "BS2" in bd
+        assert "BS5" in bd
+        assert "pH" in bd
+        assert "DO" in bd
         return
 
     def test_correct_well_association(self):
         bd = bletl.parse(calibration_test_file_pro)
-        
+
         # F01
-        x, y = bd['pH'].get_timeseries('F01')
-        assert (numpy.allclose(x[:3], [0.07972222,  0.16166667,  0.24472222]))
-        assert (numpy.allclose(y[:3], [8.15, 7.93, 7.78]))
+        x, y = bd["pH"].get_timeseries("F01")
+        assert numpy.allclose(x[:3], [0.07972222, 0.16166667, 0.24472222])
+        assert numpy.allclose(y[:3], [8.15, 7.93, 7.78])
 
         # D02
-        x, y = bd['DO'].get_timeseries('D02')
-        assert (numpy.allclose(x[:3], [0.09166667,  0.17305556,  0.25611111]))
-        assert (numpy.allclose(y[:3], [92.09, 93.84, 94.65]))
+        x, y = bd["DO"].get_timeseries("D02")
+        assert numpy.allclose(x[:3], [0.09166667, 0.17305556, 0.25611111])
+        assert numpy.allclose(y[:3], [92.09, 93.84, 94.65])
         return
 
     def test_calibration_with_lot_number(self):
         org = bletl.parse(calibration_test_file_pro2)
         with_lot = bletl.parse(calibration_test_file_pro2, lot_number=1724, temp=30)
 
-        random_cycle = numpy.random.randint(len(org['pH'].time.index)) + 1
+        random_cycle = numpy.random.randint(len(org["pH"].time.index)) + 1
 
         numpy.testing.assert_allclose(
-                org['pH'].value.loc[random_cycle],
-                with_lot['pH'].value.loc[random_cycle],
-                rtol=0.02,
+            org["pH"].value.loc[random_cycle],
+            with_lot["pH"].value.loc[random_cycle],
+            rtol=0.02,
         )
 
         numpy.testing.assert_allclose(
-                org['DO'].value.loc[random_cycle],
-                with_lot['DO'].value.loc[random_cycle],
-                rtol=0.02,
+            org["DO"].value.loc[random_cycle],
+            with_lot["DO"].value.loc[random_cycle],
+            rtol=0.02,
         )
 
     def test_calibration_with_parameters(self):
         org = bletl.parse(calibration_test_file_pro2)
-        with_p = bletl.parse(calibration_test_file_pro2, 
-            cal_0=71.8655, cal_100=42.9188, 
-            phi_min=64.248, phi_max=19.039, pH_0=6.667, dpH=0.4878
+        with_p = bletl.parse(
+            calibration_test_file_pro2,
+            cal_0=71.8655,
+            cal_100=42.9188,
+            phi_min=64.248,
+            phi_max=19.039,
+            pH_0=6.667,
+            dpH=0.4878,
         )
 
-        random_cycle = numpy.random.randint(len(org['pH'].time.index)) + 1
+        random_cycle = numpy.random.randint(len(org["pH"].time.index)) + 1
 
         numpy.testing.assert_allclose(
-                org['pH'].value.loc[random_cycle],
-                with_p['pH'].value.loc[random_cycle],
-                rtol=0.02,
+            org["pH"].value.loc[random_cycle],
+            with_p["pH"].value.loc[random_cycle],
+            rtol=0.02,
         )
 
         numpy.testing.assert_allclose(
-                org['DO'].value.loc[random_cycle],
-                with_p['DO'].value.loc[random_cycle],
-                rtol=0.02,
+            org["DO"].value.loc[random_cycle],
+            with_p["DO"].value.loc[random_cycle],
+            rtol=0.02,
         )
 
+
 class TestDataEquivalence:
     def test_model(self):
         data = bletl.parse(BLPro_files[0])
-        
+
         assert isinstance(data.model, core.BioLectorModel)
         assert data.model == core.BioLectorModel.BLPro
         return
diff --git a/tests/test_features.py b/tests/test_features.py
index 45bd8ce..4dcd7b4 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -1,11 +1,11 @@
-import pandas
 import pathlib
+
+import pandas
 import pytest
 
 import bletl
 from bletl import features
 
-
 dir_testfiles = pathlib.Path(pathlib.Path(__file__).absolute().parent, "data")
 FP_TESTFILE = pathlib.Path(dir_testfiles, "BLPro", "107-AR_Coryne-AR-2019-04-15-12-39-30.csv")
 
@@ -22,24 +22,30 @@ def test_feature_extraction(self):
         extracted_features = features.from_bldata(
             bldata,
             extractors,
-            last_cycles = {
-                "A01" : 20,
-                "B01" : 50,
-            }
+            last_cycles={
+                "A01": 20,
+                "B01": 50,
+            },
         )
         assert isinstance(extracted_features, pandas.DataFrame)
-        assert extracted_features.loc["A01", "pH_x__maximum"] == bldata.get_timeseries("pH", "A01", last_cycle=20)[1].max()
-        assert extracted_features.loc["B01", "pH_x__sum_values"] == bldata.get_timeseries("pH", "B01", last_cycle=50)[1].sum()
-        assert extracted_features.loc["C03", "pH_x__sum_values"] == bldata.get_timeseries("pH", "C03")[1].sum()
-        
+        assert (
+            extracted_features.loc["A01", "pH_x__maximum"]
+            == bldata.get_timeseries("pH", "A01", last_cycle=20)[1].max()
+        )
+        assert (
+            extracted_features.loc["B01", "pH_x__sum_values"]
+            == bldata.get_timeseries("pH", "B01", last_cycle=50)[1].sum()
+        )
+        assert (
+            extracted_features.loc["C03", "pH_x__sum_values"] == bldata.get_timeseries("pH", "C03")[1].sum()
+        )
+
         # extraction without last_cycles
         extracted_features = features.from_bldata(bldata, extractors, None)
         assert isinstance(extracted_features, pandas.DataFrame)
-        
+
         # extraction with invalid filterset
-        extractors = {
-            "xyz" : [features.BSFeatureExtractor()]
-        }
+        extractors = {"xyz": [features.BSFeatureExtractor()]}
         with pytest.raises(KeyError):
             features.from_bldata(bldata, extractors, None)
         return
diff --git a/tests/test_growth.py b/tests/test_growth.py
index 6f99588..8fb53fe 100644
--- a/tests/test_growth.py
+++ b/tests/test_growth.py
@@ -2,12 +2,12 @@
 import pytest
 import scipy.stats
 
-
 try:
-    import bletl.growth
     import calibr8
     from calibr8.utils import at, pm
 
+    import bletl.growth
+
     HAS_DEPENDENCIES = True
 except ImportError:
     HAS_DEPENDENCIES = False
@@ -16,9 +16,12 @@
 @pytest.fixture
 def biomass_calibration():
     """Creates a realistic biomass calibration for testing"""
+
     class LinearModel(calibr8.BasePolynomialModelT):
         def __init__(self) -> None:
-            super().__init__(independent_key="X", dependent_key="Pahpshmir_1400_BS3_CgWT", mu_degree=1, scale_degree=0)
+            super().__init__(
+                independent_key="X", dependent_key="Pahpshmir_1400_BS3_CgWT", mu_degree=1, scale_degree=0
+            )
 
     cmodel = LinearModel()
     cmodel.theta_fitted = [0, 1, 0.1, 5]
@@ -28,7 +31,7 @@ def __init__(self) -> None:
 @pytest.fixture
 def biomass_curve():
     """Simulates a biomass curve with three different growth rate segments"""
-    t_data = numpy.arange(0, 12, step=10/60)
+    t_data = numpy.arange(0, 12, step=10 / 60)
     t_segments = t_data[:-1]
 
     mu_true = numpy.ones_like(t_segments) * 0.05
@@ -37,10 +40,12 @@ def biomass_curve():
 
     # Simulate the biomass concentrations
     X0_true = 0.25
-    X = numpy.concatenate([
-        [X0_true],
-        X0_true * numpy.exp(numpy.cumsum(mu_true * numpy.diff(t_data))),
-    ])
+    X = numpy.concatenate(
+        [
+            [X0_true],
+            X0_true * numpy.exp(numpy.cumsum(mu_true * numpy.diff(t_data))),
+        ]
+    )
 
     assert X.shape == t_data.shape
     assert mu_true.shape == t_segments.shape
@@ -93,7 +98,8 @@ def test_fit_mu_t_gaussian(self, biomass_curve, biomass_calibration):
         bs = scipy.stats.t.rvs(loc=loc, scale=scale, df=df)
 
         result = bletl.growth.fit_mu_t(
-            t=t, y=bs,
+            t=t,
+            y=bs,
             calibration_model=biomass_calibration,
             student_t=False,
             drift_scale=0.01,
@@ -117,7 +123,8 @@ def test_fit_mu_t_studentt(self, biomass_curve, biomass_calibration):
         bs = scipy.stats.t.rvs(loc=loc, scale=scale / 10, df=df)
 
         result = bletl.growth.fit_mu_t(
-            t=t, y=bs,
+            t=t,
+            y=bs,
             calibration_model=biomass_calibration,
             student_t=True,
             mu_prior=0.4,
@@ -146,7 +153,8 @@ def test_custom_mu_zero(self, biomass_calibration, student_t):
 
         numpy.random.seed(2022)
         result = bletl.growth.fit_mu_t(
-            t=t, y=bs,
+            t=t,
+            y=bs,
             calibration_model=biomass_calibration,
             student_t=student_t,
             mu_prior=0.42,
diff --git a/tests/test_heuristics.py b/tests/test_heuristics.py
index e422a5d..c7f1abd 100644
--- a/tests/test_heuristics.py
+++ b/tests/test_heuristics.py
@@ -2,7 +2,6 @@
 
 import bletl
 
-
 dir_testfiles = pathlib.Path(pathlib.Path(__file__).absolute().parent, "data")
 FP_TESTFILE = pathlib.Path(dir_testfiles, "BLPro", "107-AR_Coryne-AR-2019-04-15-12-39-30.csv")
 
@@ -11,9 +10,11 @@ class TestDOPeakDetection:
     def test_find_peak(self):
         bldata = bletl.parse(FP_TESTFILE)
 
-        x, y = bldata['DO'].get_timeseries('A01')
+        x, y = bldata["DO"].get_timeseries("A01")
 
-        c_peak = bletl.find_do_peak(x, y, delay_a=0.5, threshold_a=70, delay_b=0, threshold_b=80, initial_delay=1)
+        c_peak = bletl.find_do_peak(
+            x, y, delay_a=0.5, threshold_a=70, delay_b=0, threshold_b=80, initial_delay=1
+        )
 
         assert c_peak == 60
         return
diff --git a/tests/test_splines.py b/tests/test_splines.py
index 0af54d0..7dea106 100644
--- a/tests/test_splines.py
+++ b/tests/test_splines.py
@@ -1,12 +1,12 @@
 ﻿import pathlib
+
 import numpy
-import scipy.interpolate
 import pytest
+import scipy.interpolate
 
 import bletl
 import bletl.splines
 
-
 dir_testfiles = pathlib.Path(pathlib.Path(__file__).absolute().parent, "data")
 FP_TESTFILE = pathlib.Path(dir_testfiles, "BLPro", "107-AR_Coryne-AR-2019-04-15-12-39-30.csv")
 
@@ -17,7 +17,7 @@ def test_checks_inputs(self):
             timepoints=numpy.arange(50),
             values=numpy.random.uniform(size=50),
             smoothing_factor=0.5,
-            method="us"
+            method="us",
         )
         bletl.splines._evaluate_smoothing_factor(**common, k=10)
 
@@ -34,76 +34,73 @@ def test_get_single_spline(self):
         """Tests the interpolation of backscatters works with an absolute tolerance of <0.1."""
         bldata = bletl.parse(FP_TESTFILE)
 
-        x, y = bldata['BS3'].get_timeseries('F05')
+        x, y = bldata["BS3"].get_timeseries("F05")
+
+        spline = bletl.get_crossvalidated_spline(x, y, method="us")
 
-        spline = bletl.get_crossvalidated_spline(x, y, method='us')
-        
         assert isinstance(spline, scipy.interpolate.UnivariateSpline)
 
         # the last point should be very close
         numpy.testing.assert_allclose(spline(19.4275), 23.66, atol=0.1)
 
         # a range of points at the end of the curve
-        numpy.testing.assert_allclose(spline([18.8275, 19.0275, 19.22777778]), [23.75, 23.67, 23.68], atol=0.1)
+        numpy.testing.assert_allclose(
+            spline([18.8275, 19.0275, 19.22777778]), [23.75, 23.67, 23.68], atol=0.1
+        )
         return
 
     def test_get_mue_wells(self):
         """Tests that the median growth rate over 15 exponential cycles is calculated with <0.02 absolute deviance."""
         bldata = bletl.parse(FP_TESTFILE)
-        wells = 'A01,A02,B03,C05'.split(',')
- 
+        wells = "A01,A02,B03,C05".split(",")
+
         # automatic blank
-        mue_blank_first = bletl.splines.get_mue(bldata['BS3'], wells=wells, method='us')
-        mue_median = numpy.median(mue_blank_first.value.loc[60:75, 'B03'])
+        mue_blank_first = bletl.splines.get_mue(bldata["BS3"], wells=wells, method="us")
+        mue_median = numpy.median(mue_blank_first.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # scalar blank for all
-        mue_blank_scalar = bletl.splines.get_mue(bldata['BS3'], blank=2, wells=wells, method='us')
-        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, 'B03'])
+        mue_blank_scalar = bletl.splines.get_mue(bldata["BS3"], blank=2, wells=wells, method="us")
+        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.30, atol=0.01)
-        
+
         # dictionary of scalars (first 5 cycles)
         blank_dict = {
-            well : data.iloc[:5].mean()
-            for well, data in bldata['BS3'].value.iteritems()
-            if well in wells
+            well: data.iloc[:5].mean() for well, data in bldata["BS3"].value.iteritems() if well in wells
         }
-        mue_blank_dict = bletl.splines.get_mue(bldata['BS3'], wells=wells, blank=blank_dict, method='us')
-        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, 'B03'])
+        mue_blank_dict = bletl.splines.get_mue(bldata["BS3"], wells=wells, blank=blank_dict, method="us")
+        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # check value error when the wells dictionary is incorrect
         with pytest.raises(ValueError):
-            bletl.splines.get_mue(bldata['BS3'], wells=wells, blank=dict(A01=3, C02=4), method='us')
+            bletl.splines.get_mue(bldata["BS3"], wells=wells, blank=dict(A01=3, C02=4), method="us")
         # check value error on invalid blank option
         with pytest.raises(ValueError):
-            bletl.splines.get_mue(bldata['BS3'], wells=wells, blank='last', method='us')
+            bletl.splines.get_mue(bldata["BS3"], wells=wells, blank="last", method="us")
         return
 
     def test_get_mue_on_all(self):
         """Tests that the median growth rate over 15 exponential cycles is calculated with <0.02 absolute deviance."""
         bldata = bletl.parse(FP_TESTFILE)
-        wells = list('A01,A02,B03,C05'.split(','))
-        bldata['BS3'].time = bldata['BS3'].time[wells]
-        bldata['BS3'].value = bldata['BS3'].value[wells]
-        
+        wells = list("A01,A02,B03,C05".split(","))
+        bldata["BS3"].time = bldata["BS3"].time[wells]
+        bldata["BS3"].value = bldata["BS3"].value[wells]
+
         # automatic
-        mue_blank_first = bletl.splines.get_mue(bldata['BS3'], method='us')
-        mue_median = numpy.median(mue_blank_first.value.loc[60:75, 'B03'])
+        mue_blank_first = bletl.splines.get_mue(bldata["BS3"], method="us")
+        mue_median = numpy.median(mue_blank_first.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # scalar blank for all
-        mue_blank_scalar = bletl.splines.get_mue(bldata['BS3'], blank=2, method='us')
-        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, 'B03'])
+        mue_blank_scalar = bletl.splines.get_mue(bldata["BS3"], blank=2, method="us")
+        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.30, atol=0.01)
-        
+
         # dictionary of scalars (first 5 cycles)
-        blank_dict = {
-            well : data.iloc[:5].mean()
-            for well, data in bldata['BS3'].value.iteritems()
-        }
-        mue_blank_dict = bletl.splines.get_mue(bldata['BS3'], blank=blank_dict, method='us')
-        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, 'B03'])
+        blank_dict = {well: data.iloc[:5].mean() for well, data in bldata["BS3"].value.iteritems()}
+        mue_blank_dict = bletl.splines.get_mue(bldata["BS3"], blank=blank_dict, method="us")
+        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
         return
 
@@ -113,76 +110,73 @@ def test_get_single_spline(self):
         """Tests the interpolation of backscatters works with an absolute tolerance of <0.1."""
         bldata = bletl.parse(FP_TESTFILE)
 
-        x, y = bldata['BS3'].get_timeseries('F05')
+        x, y = bldata["BS3"].get_timeseries("F05")
+
+        spline = bletl.get_crossvalidated_spline(x, y, method="ucss")
 
-        spline = bletl.get_crossvalidated_spline(x, y, method='ucss')
-        
         assert isinstance(spline, bletl.splines.UnivariateCubicSmoothingSpline)
 
         # the last point should be very close
         numpy.testing.assert_allclose(spline(19.4275), 23.66, atol=0.1)
 
         # a range of points at the end of the curve
-        numpy.testing.assert_allclose(spline([18.8275, 19.0275, 19.22777778]), [23.75, 23.67, 23.68], atol=0.1)
+        numpy.testing.assert_allclose(
+            spline([18.8275, 19.0275, 19.22777778]), [23.75, 23.67, 23.68], atol=0.1
+        )
         return
 
     def test_get_mue_wells(self):
         """Tests that the median growth rate over 15 exponential cycles is calculated with <0.02 absolute deviance."""
         bldata = bletl.parse(FP_TESTFILE)
-        wells = 'A01,A02,B03,C05'.split(',')
+        wells = "A01,A02,B03,C05".split(",")
 
         # automatic blank
-        mue_blank_first = bletl.splines.get_mue(bldata['BS3'], wells=wells, method='ucss')
-        mue_median = numpy.median(mue_blank_first.value.loc[60:75, 'B03'])
+        mue_blank_first = bletl.splines.get_mue(bldata["BS3"], wells=wells, method="ucss")
+        mue_median = numpy.median(mue_blank_first.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # scalar blank for all
-        mue_blank_scalar = bletl.splines.get_mue(bldata['BS3'], blank=2, wells=wells, method='ucss')
-        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, 'B03'])
+        mue_blank_scalar = bletl.splines.get_mue(bldata["BS3"], blank=2, wells=wells, method="ucss")
+        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.30, atol=0.02)
-        
+
         # dictionary of scalars (first 5 cycles)
         blank_dict = {
-            well : data.iloc[:5].mean()
-            for well, data in bldata['BS3'].value.iteritems()
-            if well in wells
+            well: data.iloc[:5].mean() for well, data in bldata["BS3"].value.iteritems() if well in wells
         }
-        mue_blank_dict = bletl.splines.get_mue(bldata['BS3'], wells=wells, blank=blank_dict, method='ucss')
-        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, 'B03'])
+        mue_blank_dict = bletl.splines.get_mue(bldata["BS3"], wells=wells, blank=blank_dict, method="ucss")
+        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # check value error when the wells dictionary is incorrect
         with pytest.raises(ValueError):
-            bletl.splines.get_mue(bldata['BS3'], wells=wells, blank=dict(A01=3, C02=4), method='ucss')
+            bletl.splines.get_mue(bldata["BS3"], wells=wells, blank=dict(A01=3, C02=4), method="ucss")
         # check value error on invalid blank option
         with pytest.raises(ValueError):
-            bletl.splines.get_mue(bldata['BS3'], wells=wells, blank='last', method='us')
+            bletl.splines.get_mue(bldata["BS3"], wells=wells, blank="last", method="us")
         return
 
     def test_get_mue_on_all(self):
         """Tests that the median growth rate over 15 exponential cycles is calculated with <0.02 absolute deviance."""
         bldata = bletl.parse(FP_TESTFILE)
-        wells = list('A01,A02,B03,C05'.split(','))
-        bldata['BS3'].time = bldata['BS3'].time[wells]
-        bldata['BS3'].value = bldata['BS3'].value[wells]
-        
+        wells = list("A01,A02,B03,C05".split(","))
+        bldata["BS3"].time = bldata["BS3"].time[wells]
+        bldata["BS3"].value = bldata["BS3"].value[wells]
+
         # automatic
-        mue_blank_first = bletl.splines.get_mue(bldata['BS3'], method='ucss')
-        mue_median = numpy.median(mue_blank_first.value.loc[60:75, 'B03'])
+        mue_blank_first = bletl.splines.get_mue(bldata["BS3"], method="ucss")
+        mue_median = numpy.median(mue_blank_first.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
 
         # scalar blank for all
-        mue_blank_scalar = bletl.splines.get_mue(bldata['BS3'], blank=2, method='ucss')
-        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, 'B03'])
+        mue_blank_scalar = bletl.splines.get_mue(bldata["BS3"], blank=2, method="ucss")
+        mue_median = numpy.median(mue_blank_scalar.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.30, atol=0.02)
-        
+
         # dictionary of scalars (first 5 cycles)
-        blank_dict = {
-            well : data.iloc[:5].mean()
-            for well, data in bldata['BS3'].value.iteritems()
-        }
-        mue_blank_dict = bletl.splines.get_mue(bldata['BS3'], blank=blank_dict, method='ucss')
-        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, 'B03'])
+        blank_dict = {well: data.iloc[:5].mean() for well, data in bldata["BS3"].value.iteritems()}
+        mue_blank_dict = bletl.splines.get_mue(bldata["BS3"], blank=blank_dict, method="ucss")
+        mue_median = numpy.median(mue_blank_dict.value.loc[60:75, "B03"])
         numpy.testing.assert_allclose(mue_median, 0.38, atol=0.02)
         return
 
@@ -191,7 +185,7 @@ class TestSplineMethodEquivalence:
     def test_API_comparable(self):
         x = numpy.linspace(0, 50, 20)
         y = numpy.random.normal(x)
-        ucss = bletl.get_crossvalidated_spline(x, y, method='ucss')
+        ucss = bletl.get_crossvalidated_spline(x, y, method="ucss")
         assert isinstance(ucss, bletl.splines.UnivariateCubicSmoothingSpline)
         der_1 = ucss.derivative(1)
         der_2 = ucss.derivative(2)
@@ -211,23 +205,23 @@ def test_ideal_exponential_mue(self):
         mue = 0.352
         y0 = 0.05
         t = numpy.linspace(0, 20, 50)
-        y = y0 * numpy.exp(mue*t)
+        y = y0 * numpy.exp(mue * t)
 
         numpy.random.seed(25)
-        spline_us = bletl.get_crossvalidated_spline(t, y, method='us')
-        spline_ucss = bletl.get_crossvalidated_spline(t, y, method='ucss')
+        spline_us = bletl.get_crossvalidated_spline(t, y, method="us")
+        spline_ucss = bletl.get_crossvalidated_spline(t, y, method="ucss")
         numpy.random.seed(None)
 
         # test that both spline approximations have residuals of less than 3 % of the signal amplitude
         diff_us = numpy.abs(spline_us(t) - y)
         diff_ucss = numpy.abs(spline_ucss(t) - y)
-        assert numpy.all(numpy.max(diff_us) < numpy.ptp(y)*0.03)
-        assert numpy.all(numpy.max(diff_ucss) < numpy.ptp(y)*0.03)
+        assert numpy.all(numpy.max(diff_us) < numpy.ptp(y) * 0.03)
+        assert numpy.all(numpy.max(diff_ucss) < numpy.ptp(y) * 0.03)
 
         # test that the median specific growth rate is close to the true value
         mue_us = spline_us.derivative(1)(t[1:]) / spline_us(t[1:])
         mue_ucss = spline_ucss.derivative(1)(t[1:]) / spline_us(t[1:])
-        
+
         numpy.testing.assert_almost_equal(numpy.median(mue_us), mue, decimal=2)
         numpy.testing.assert_almost_equal(numpy.median(mue_ucss), mue, decimal=2)
-        return
\ No newline at end of file
+        return