Skip to content

Commit

Permalink
calibration: show application timestamp in table
Browse files Browse the repository at this point in the history
  • Loading branch information
JoepVanlier committed Jul 12, 2024
1 parent b8fbfb6 commit cb0175c
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 55 deletions.
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* Calibration results and parameters are now accessible via properties which are listed when items are printed. See [calibration results](https://lumicks-pylake.readthedocs.io/en/latest/_api/lumicks.pylake.force_calibration.power_spectrum_calibration.CalibrationResults.html) and [calibration item](https://lumicks-pylake.readthedocs.io/en/latest/_api/lumicks.pylake.calibration.ForceCalibrationItem.html) API documentation for more information.
* Added `applied_at` property to a [calibration item](https://lumicks-pylake.readthedocs.io/en/latest/_api/lumicks.pylake.calibration.ForceCalibrationItem.html) obtained from a force slice. This property returns the timestamp in nanoseconds at which the force calibration was applied.
* Added improved printing of calibration items under `channel.calibration` providing a more convenient overview of the items associated with a `Slice`.
* Added parameter `titles` to customize title of each subplot in `[`Kymo.plot_with_channels()`](https://lumicks-pylake.readthedocs.io/en/latest/_api/lumicks.pylake.kymo.Kymo.html#lumicks.pylake.kymo.Kymo.plot_with_channels).
* Added improved printing of calibrations performed with `Pylake`.
* Added parameter `titles` to customize title of each subplot in [`Kymo.plot_with_channels()`](https://lumicks-pylake.readthedocs.io/en/latest/_api/lumicks.pylake.kymo.Kymo.html#lumicks.pylake.kymo.Kymo.plot_with_channels).

## v1.5.1 | 2024-06-03

Expand Down
43 changes: 10 additions & 33 deletions lumicks/pylake/calibration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import datetime
from functools import wraps
from collections import UserDict

Expand Down Expand Up @@ -49,14 +50,6 @@ def _sensor_type(self):
else:
return "unknown sensor"

@property
def kind(self):
kind = self.data.get("Kind", "Unknown")
if kind == "Full calibration":
return "Active calibration" if self.active_calibration else "Passive calibration"
else:
return kind

@_verify_full
def power_spectrum_params(self):
"""Returns parameters with which the power spectrum was calculated
Expand Down Expand Up @@ -199,27 +192,6 @@ def number_of_samples(self):
"""Number of fitted samples (-)."""
return self.data.get("Number of samples")

@property
def active_calibration(self):
"""Returns whether it was an active calibration or not
Calibrations based on active calibration are less sensitive to assumptions about the
bead diameter, viscosity, distance of the bead to the flow cell surface and temperature.
During active calibration, the trap or nano-stage is oscillated sinusoidally. These
oscillations result in a driving peak in the force spectrum. Using power spectral analysis,
the force can then be calibrated without prior knowledge of the drag coefficient.
While this results in improved accuracy, it may lead to an increase in the variability of
results.
.. note::
When active calibration is performed using two beads, correction factors must be
computed which account for the reduced flow field that forms around the beads due to
the presence of a second bead.
"""
return self.data.get("driving_frequency (Hz)") is not None

@property
def num_points_per_block(self):
"""Number of points per block used for spectral down-sampling"""
Expand Down Expand Up @@ -367,11 +339,15 @@ def from_field(hdf5, force_channel, time_field="Stop time (ns)") -> "ForceCalibr

return ForceCalibration(time_field=time_field, items=items)

def print_summary(self, tablefmt):
def _print_summary(self, tablefmt):
def format_timestamp(timestamp):
return datetime.datetime.fromtimestamp(int(timestamp)).strftime("%x %X")

return tabulate(
(
(
idx,
format_timestamp(item.applied_at / 1e9) if item.applied_at else "-",
item.kind,
f"{item.stiffness:.2f}" if item.stiffness else "N/A",
f"{item.force_sensitivity:.2f}" if item.force_sensitivity else "N/A",
Expand All @@ -389,7 +365,8 @@ def print_summary(self, tablefmt):
),
tablefmt=tablefmt,
headers=(
"Index",
"#",
"Applied at",
"Kind",
"Stiffness (pN/nm)",
"Force sens. (pN/V)",
Expand All @@ -401,10 +378,10 @@ def print_summary(self, tablefmt):
)

def _repr_html_(self):
return self.print_summary(tablefmt="html")
return self._print_summary(tablefmt="html")

def __str__(self):
return self.print_summary(tablefmt="text")
return self._print_summary(tablefmt="text")

@staticmethod
def from_dataset(hdf5, n, xy, time_field="Stop time (ns)") -> "ForceCalibration":
Expand Down
39 changes: 30 additions & 9 deletions lumicks/pylake/force_calibration/power_spectrum_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,35 @@ def offset(self):
"""Force offset (pN)"""
return self._get_parameter("NA", "Offset")

@property
def active_calibration(self):
"""Returns whether it was an active calibration or not
Calibrations based on active calibration are less sensitive to assumptions about the
bead diameter, viscosity, distance of the bead to the flow cell surface and temperature.
During active calibration, the trap or nano-stage is oscillated sinusoidally. These
oscillations result in a driving peak in the force spectrum. Using power spectral analysis,
the force can then be calibrated without prior knowledge of the drag coefficient.
While this results in improved accuracy, it may lead to an increase in the variability of
results.
.. note::
When active calibration is performed using two beads, correction factors must be
computed which account for the reduced flow field that forms around the beads due to
the presence of a second bead.
"""
return self.driving_frequency is not None

@property
def kind(self):
kind = self._get_parameter("Kind", "Kind")
if kind == "Full calibration":
return "Active" if self.active_calibration else "Passive"
else:
return kind if kind is not None else "Unknown"


class CalibrationResults(CalibrationPropertiesMixin):
"""Power spectrum calibration results.
Expand Down Expand Up @@ -653,15 +682,6 @@ def _get_parameter(self, pylake_key, _):
if pylake_key in self:
return self[pylake_key].value

@property
def kind(self):
"""Type of calibration performed"""
return (
"active calibration"
if self.model.__class__.__name__ == "ActiveCalibrationModel"
else "passive calibration"
)

@property
def _fitted_diode(self):
"""Diode parameters were fitted"""
Expand Down Expand Up @@ -1006,6 +1026,7 @@ def fit_power_spectrum(
"Chi squared per degree of freedom", chi_squared_per_deg, ""
),
"backing": CalibrationParameter("Statistical backing", backing, "%"),
"Kind": CalibrationParameter("Calibration type", "Full calibration", "-"),
},
params={
**model.calibration_parameters(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ def test_integration_active_calibration(
np.testing.assert_allclose(fit["Viscosity"].value, viscosity)
np.testing.assert_allclose(fit["num_windows"].value, 5)

assert fit.active_calibration
assert fit.kind == "Active"

np.testing.assert_allclose(
fit["driving_amplitude"].value, driving_sinusoid[0] * 1e-3, rtol=1e-5
)
Expand Down
2 changes: 2 additions & 0 deletions lumicks/pylake/force_calibration/tests/test_hydro.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ def test_integration_passive_calibration_hydrodynamics(integration_test_paramete
assert not fit.transferred_lateral_drag_coefficient # Only relevant for axial
assert fit.fit_range == (100, 23000)
assert fit.excluded_ranges == []
assert not fit.active_calibration
assert fit.kind == "Passive"


def test_integration_active_calibration_hydrodynamics_bulk(integration_test_parameters):
Expand Down
38 changes: 26 additions & 12 deletions lumicks/pylake/tests/test_file/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,35 @@ def test_redirect_list(h5_file):
assert f["Point Scan"]["PointScan1"].start == np.int64(20e9)


def test_calibration_str(h5_file):
def test_calibration_str(h5_file, monkeypatch):
# Representation of time is timezone and locale dependent, hence we monkeypatch it
class FakeDateTime:
@classmethod
def fromtimestamp(cls, timestamp, *args, **kwargs):
"""Inject a timezone"""
return FakeDateTime()

def strftime(self, str_format):
return str_format

f = pylake.File.from_h5py(h5_file)
if f.format_version == 2:
print("")
print(str(f.force1x.calibration))
assert str(f.force1x.calibration) == dedent(
(
"""\
Index Kind Stiffness (pN/nm) Force sens. (pN/V) Disp. sens. (µm/V) Hydro Surface Data?
------- ------------------- ------------------- -------------------- -------------------- ------- --------- -------
0 Unknown N/A N/A N/A False False False
1 Reset offset 1.05 504.43 4.57 False True False
2 Passive calibration 1.05 504.43 4.57 False False True"""
with monkeypatch.context() as m:
m.setattr(
"lumicks.pylake.calibration.datetime.datetime",
FakeDateTime,
)

assert str(f.force1x.calibration) == dedent(
(
"""\
# Applied at Kind Stiffness (pN/nm) Force sens. (pN/V) Disp. sens. (µm/V) Hydro Surface Data?
--- ------------ ------------ ------------------- -------------------- -------------------- ------- --------- -------
0 - Unknown N/A N/A N/A False False False
1 %x %X Reset offset 1.05 504.43 4.57 False True False
2 %x %X Passive 1.05 504.43 4.57 False False True"""
)
)
)


def test_repr_and_str(h5_file):
Expand Down

0 comments on commit cb0175c

Please sign in to comment.