def test_invalid_bead_diameter():
    with pytest.raises(ValueError, match="Invalid bead diameter specified"):
        PassiveCalibrationModel(bead_diameter=0)

    with pytest.raises(ValueError, match="Invalid bead diameter specified"):
        PassiveCalibrationModel(bead_diameter=1e-7)

    PassiveCalibrationModel(bead_diameter=1e-2)
def test_model_parameters():
    params = PassiveCalibrationModel(10, temperature=30)
    assert params.bead_diameter == 10
    assert params.viscosity == viscosity_of_water(30)
    assert params.temperature == 30

    with pytest.raises(TypeError):
        PassiveCalibrationModel(10, invalid_parameter=5)
def test_invalid_viscosity():
    with pytest.raises(
            ValueError,
            match=re.escape("Viscosity must be higher than 0.0003 Pa*s")):
        PassiveCalibrationModel(4.1, viscosity=0.0003)

    with pytest.raises(
            ValueError,
            match=re.escape("Viscosity must be higher than 0.0003 Pa*s")):
        PassiveCalibrationModel(4.1, viscosity=0)

    PassiveCalibrationModel(4.1, viscosity=0.00031)
def test_invalid_temperature():
    with pytest.raises(
            ValueError,
            match=re.escape("Temperature must be between 5 and 90 Celsius")):
        PassiveCalibrationModel(4.1, temperature=90.0)

    with pytest.raises(
            ValueError,
            match=re.escape("Temperature must be between 5 and 90 Celsius")):
        PassiveCalibrationModel(4.1, temperature=5.0)

    PassiveCalibrationModel(4.1, temperature=89.9)
    PassiveCalibrationModel(4.1, temperature=5.1)
def test_faxen_correction():
    """When hydro is off, but a height is given, the interpretation of the Lorentzian fit can still
    benefit from using the distance to the surface in a correction factor for the drag. This will
    only affect thermal calibration. This behaviour is tested here."""
    shared_pars = {
        "bead_diameter": 1.03,
        "viscosity": 1.1e-3,
        "temperature": 25,
        "rho_sample": 997.0,
        "rho_bead": 1040.0,
        "distance_to_surface": 1.03 / 2 + 400e-3,
    }
    sim_pars = {
        "sample_rate": 78125,
        "stiffness": 0.1,
        "pos_response_um_volt": 0.618,
        "driving_sinusoid": (500, 31.95633),
        "diode": (0.4, 15000),
    }

    np.random.seed(10071985)
    volts, _ = generate_active_calibration_test_data(
        10, hydrodynamically_correct=True, **sim_pars, **shared_pars)
    power_spectrum = psc.calculate_power_spectrum(volts,
                                                  sim_pars["sample_rate"])

    model = PassiveCalibrationModel(**shared_pars,
                                    hydrodynamically_correct=False)
    fit = psc.fit_power_spectrum(power_spectrum, model, bias_correction=False)

    # Fitting with *no* hydrodynamically correct model, but *with* Faxen's law
    np.testing.assert_allclose(fit.results["Rd"].value, 0.6136895577998873)
    np.testing.assert_allclose(fit.results["kappa"].value, 0.10312266251783221)
    np.testing.assert_allclose(fit.results["Rf"].value, 63.285301159715466)
    np.testing.assert_allclose(fit.results["gamma_0"].value,
                               1.0678273429551705e-08)

    # Disabling Faxen's correction on the drag makes the estimates *much* worse
    shared_pars["distance_to_surface"] = None
    model = PassiveCalibrationModel(**shared_pars,
                                    hydrodynamically_correct=False)
    fit = psc.fit_power_spectrum(power_spectrum, model, bias_correction=False)
    np.testing.assert_allclose(fit.results["Rd"].value, 0.741747603986908)
    np.testing.assert_allclose(fit.results["kappa"].value, 0.07058936587810064)
    np.testing.assert_allclose(fit.results["Rf"].value, 52.35949300703634)
    # Not affected since this is gamma bulk
    np.testing.assert_allclose(fit.results["gamma_0"].value,
                               1.0678273429551705e-08)
Ejemplo n.º 6
0
def test_invalid_model():
    with pytest.raises(
            NotImplementedError,
            match=
            "No hydrodynamically correct axial force model is currently available.",
    ):
        PassiveCalibrationModel(1.0, hydrodynamically_correct=True, axial=True)
def test_invalid_distance_to_surface():
    # Check that zero does not work specifically (since it is falsy).
    with pytest.raises(
            ValueError,
            match=
            "Distance from bead center to surface is smaller than the bead radius",
    ):
        PassiveCalibrationModel(4.11,
                                distance_to_surface=0,
                                hydrodynamically_correct=False)
def reference_calibration_result():
    data = np.load(
        os.path.join(os.path.dirname(__file__), "reference_spectrum.npz"))
    reference_spectrum = data["arr_0"]
    model = PassiveCalibrationModel(4.4, temperature=20, viscosity=0.001002)
    reference_spectrum = psc.calculate_power_spectrum(reference_spectrum,
                                                      sample_rate=78125,
                                                      num_points_per_block=100,
                                                      fit_range=(100.0,
                                                                 23000.0))
    ps_calibration = psc.fit_power_spectrum(power_spectrum=reference_spectrum,
                                            model=model,
                                            bias_correction=False)

    return ps_calibration, model, reference_spectrum
def test_no_data_in_range():
    model = PassiveCalibrationModel(1, temperature=20, viscosity=0.0004)

    # Here the range slices off all the data and we are left with an empty spectrum
    power_spectrum = psc.PowerSpectrum(np.arange(100),
                                       sample_rate=100).in_range(47, 100)

    with pytest.raises(RuntimeError):
        psc.fit_power_spectrum(power_spectrum, model=model)

    # Check whether a failure to get a sufficient number of points in the analytical fit is
    # detected.
    power_spectrum = psc.PowerSpectrum(np.arange(100), sample_rate=1e-3)

    with pytest.raises(RuntimeError):
        psc.fit_power_spectrum(power_spectrum, model=model)
Ejemplo n.º 10
0
def test_integration_passive_calibration_hydrodynamics(
        integration_test_parameters):
    shared_pars, simulation_pars = integration_test_parameters

    np.random.seed(10071985)
    volts, _ = generate_active_calibration_test_data(10, **simulation_pars,
                                                     **shared_pars)
    model = PassiveCalibrationModel(**shared_pars)
    power_spectrum = calculate_power_spectrum(volts,
                                              simulation_pars["sample_rate"])
    fit = fit_power_spectrum(power_spectrum, model, bias_correction=False)

    expected_params = {
        "Sample density": 997.0,
        "Bead density": 1040.0,
        "Distance to surface": 0.7776500000000001,
        "Bead diameter": 1.03,
        "Viscosity": 0.0011,
        "Temperature": 25,
        "Max iterations": 10000,
        "Fit tolerance": 1e-07,
        "Points per block": 2000,
        "Sample rate": 78125,
    }
    expected_results = {
        "Rd": 0.6181013468813382,
        "kappa": 0.10093835959160387,
        "Rf": 62.39013601556319,
        "gamma_0": 1.0678273429551705e-08,
        "fc": 1504.4416105821158,
        "D": 1.0090151317063,
        "err_fc": 13.075876724291339,
        "err_D": 0.0066021439072302835,
        "f_diode": 14675.638696737586,
        "alpha": 0.41651098052983593,
        "err_f_diode": 352.2917702189488,
        "err_alpha": 0.014231238753589254,
        "chi_squared_per_deg": 0.8659867914094764,
        "backing": 14.340689726784328,
    }

    for key, value in expected_params.items():
        np.testing.assert_allclose(fit.params[key].value, value, err_msg=key)

    for key, value in expected_results.items():
        np.testing.assert_allclose(fit.results[key].value, value, err_msg=key)
Ejemplo n.º 11
0
def model_and_data(reference_models, diode_alpha, fast_sensor):
    """Generate model and data for this particular test"""
    params = {"bead_diameter": 1.03, "temperature": 20, "viscosity": 1.002e-3}
    data, f_sample = reference_models.lorentzian_td(4000,
                                                    1.14632,
                                                    alpha=diode_alpha,
                                                    f_diode=14000,
                                                    num_samples=78125)
    passive_model = PassiveCalibrationModel(**params, fast_sensor=fast_sensor)
    sine = np.sin(32.0 * 2.0 * np.pi * np.arange(0, 1, 1.0 / f_sample))
    active_model = ActiveCalibrationModel(sine,
                                          sine,
                                          f_sample,
                                          driving_frequency_guess=32,
                                          **params,
                                          fast_sensor=fast_sensor)
    power_spectrum = calculate_power_spectrum(data,
                                              f_sample,
                                              num_points_per_block=5)
    return (passive_model, active_model), power_spectrum
def test_input_validation_power_spectrum_calibration():
    model = PassiveCalibrationModel(1)

    # Wrong dimensions
    with pytest.raises(TypeError):
        psc.fit_power_spectrum(data=np.array([[1, 2, 3], [1, 2, 3]]),
                               sample_rate=78125,
                               model=model)

    # Wrong type
    with pytest.raises(TypeError):
        psc.fit_power_spectrum(data="bloop", sample_rate=78125, model=model)

    with pytest.raises(TypeError):
        psc.fit_power_spectrum(data=np.array([1, 2, 3]),
                               sample_rate=78125,
                               model="invalid")

    with pytest.raises(TypeError):
        psc.fit_power_spectrum(data=np.array([1, 2, 3]),
                               sample_rate=78125,
                               model=model,
                               settings="invalid")
def test_fit_settings(reference_models):
    """This test tests whether the algorithm parameters ftol, max_function_evals and
    analytical_fit_range for lk.fit_power_spectrum() are applied as intended."""
    sample_rate = 78125
    corner_frequency, diffusion_volt = 4000, 1.14632
    bead_diameter, temperature, viscosity = 1.03, 20, 1.002e-3

    # alpha = 1.0 means no diode effect
    data, f_sample = reference_models.lorentzian_td(corner_frequency,
                                                    diffusion_volt,
                                                    alpha=1.0,
                                                    f_diode=14000,
                                                    num_samples=sample_rate)
    model = PassiveCalibrationModel(bead_diameter,
                                    temperature=temperature,
                                    viscosity=viscosity)
    power_spectrum = psc.calculate_power_spectrum(data,
                                                  f_sample,
                                                  fit_range=(0, 23000),
                                                  num_points_per_block=200)

    # Won't converge with so few maximum function evaluations
    with pytest.raises(
            RuntimeError,
            match="The maximum number of function evaluations is exceeded"):
        psc.fit_power_spectrum(power_spectrum=power_spectrum,
                               model=model,
                               max_function_evals=1)

    # Make the analytical fit fail
    with pytest.raises(
            RuntimeError,
            match=
            "An empty power spectrum was passed to fit_analytical_lorentzian"):
        psc.fit_power_spectrum(power_spectrum=power_spectrum,
                               model=model,
                               analytical_fit_range=(10, 100))
def test_invalid_densities():
    """Densities lower than aerogel should not be accepted."""
    with pytest.raises(
            ValueError,
            match=re.escape(
                "Density of the sample cannot be below 100 kg/m^3")):
        PassiveCalibrationModel(4.1,
                                hydrodynamically_correct=True,
                                rho_sample=99.9)

    # Make sure 0 also fires (since it's falsy)
    with pytest.raises(
            ValueError,
            match=re.escape(
                "Density of the sample cannot be below 100 kg/m^3")):
        PassiveCalibrationModel(4.1,
                                hydrodynamically_correct=True,
                                rho_sample=0)

    with pytest.raises(
            ValueError,
            match=re.escape("Density of the bead cannot be below 100 kg/m^3")):
        PassiveCalibrationModel(4.1,
                                hydrodynamically_correct=True,
                                rho_bead=99.9)

    PassiveCalibrationModel(4.1,
                            hydrodynamically_correct=True,
                            rho_sample=100.1)
    PassiveCalibrationModel(4.1, hydrodynamically_correct=True, rho_bead=100.1)

    # When not using hydro, these arguments are not used (no need to raise).
    PassiveCalibrationModel(4.1,
                            hydrodynamically_correct=False,
                            rho_sample=99.9)
    PassiveCalibrationModel(4.1, hydrodynamically_correct=False, rho_bead=99.9)
def test_good_fit_integration_test(
    reference_models,
    corner_frequency,
    diffusion_constant,
    alpha,
    f_diode,
    num_samples,
    viscosity,
    bead_diameter,
    temperature,
    err_fc,
    err_d,
    err_f_diode,
    err_alpha,
):
    data, f_sample = reference_models.lorentzian_td(corner_frequency,
                                                    diffusion_constant, alpha,
                                                    f_diode, num_samples)
    model = PassiveCalibrationModel(bead_diameter,
                                    temperature=temperature,
                                    viscosity=viscosity)
    power_spectrum = psc.calculate_power_spectrum(data,
                                                  f_sample,
                                                  fit_range=(0, 15000),
                                                  num_points_per_block=20)
    ps_calibration = psc.fit_power_spectrum(power_spectrum=power_spectrum,
                                            model=model,
                                            bias_correction=False)

    np.testing.assert_allclose(ps_calibration["fc"].value,
                               corner_frequency,
                               rtol=1e-4)
    np.testing.assert_allclose(ps_calibration["D"].value,
                               diffusion_constant,
                               rtol=1e-4,
                               atol=0)
    np.testing.assert_allclose(ps_calibration["alpha"].value, alpha, rtol=1e-4)
    np.testing.assert_allclose(ps_calibration["f_diode"].value,
                               f_diode,
                               rtol=1e-4)

    gamma = sphere_friction_coefficient(viscosity, bead_diameter * 1e-6)
    kappa_true = 2.0 * np.pi * gamma * corner_frequency * 1e3
    rd_true = (np.sqrt(sp.constants.k * sp.constants.convert_temperature(
        temperature, "C", "K") / gamma / diffusion_constant) * 1e6)
    np.testing.assert_allclose(ps_calibration["kappa"].value,
                               kappa_true,
                               rtol=1e-4)
    np.testing.assert_allclose(ps_calibration["Rd"].value, rd_true, rtol=1e-4)
    np.testing.assert_allclose(ps_calibration["Rf"].value,
                               rd_true * kappa_true * 1e3,
                               rtol=1e-4)
    np.testing.assert_allclose(ps_calibration["chi_squared_per_deg"].value,
                               0,
                               atol=1e-9)  # Noise free

    np.testing.assert_allclose(ps_calibration["err_fc"].value, err_fc)
    np.testing.assert_allclose(ps_calibration["err_D"].value,
                               err_d,
                               rtol=1e-4,
                               atol=0)
    np.testing.assert_allclose(ps_calibration["err_f_diode"].value,
                               err_f_diode)
    np.testing.assert_allclose(ps_calibration["err_alpha"].value,
                               err_alpha,
                               rtol=1e-6)
Ejemplo n.º 16
0
def test_axial_calibration(reference_models, hydro):
    """We currently have no way to perform active axial calibration.

    However, we can make the passive calibration slightly more accurate by transferring the active
    calibration result from the lateral calibration over to it.

    This test performs an integration test which tests whether carrying over the drag coefficient
    from a lateral calibration to an axial one produces the correct results. To test this, we
    deliberately mis-specify our viscosity in the models (since active calibration is more
    robust against mis-specification of viscosity and bead radius).

    Approaching the surface leads to an increase in the drag coefficient:

        gamma(h) = gamma_bulk * correction_factor(h)

    For lateral this factor is given by a different function than axial. The experimental drag
    coefficient returned by the calibration procedure (gamma_ex) is the back-corrected bulk drag
    coefficient gamma_bulk. This can be directly transferred to the axial direction which then
    applies the correct forward correction factor for axial. This test verifies that this behaviour
    stays preserved (as we rely on it)."""
    np.random.seed(17256246)
    viscosity = 0.0011
    shared_pars = {"bead_diameter": 0.5, "temperature": 20}
    sim_params = {
        "sample_rate": 78125,
        "duration": 10,
        "stiffness": 0.05,
        "pos_response_um_volt": 0.6,
        "driving_sinusoid": (500, 31.9563),
        "diode": (1.0, 10000),
        **shared_pars,
    }
    # For reference, these parameters lead to a true bulk gamma of:
    gamma_ref = 5.183627878423158e-09

    # Distance to the surface
    dist = 1.5 * shared_pars["bead_diameter"] / 2

    def height_simulation(height_factor):
        """We hack in height dependence using the viscosity. Since the calibration procedure covers
        the same bead, we can do this (gamma_bulk is linearly proportional to the viscosity and
        bead size).

        height_factor : callable
            Provides the height dependent drag model.
        """
        return generate_active_calibration_test_data(
            **sim_params,
            viscosity=viscosity * height_factor(
                dist * 1e-6, shared_pars["bead_diameter"] * 1e-6 / 2),
        )

    volts_lateral, stage = height_simulation(faxen_factor)
    lateral_model = ActiveCalibrationModel(
        stage,
        volts_lateral,
        **shared_pars,
        sample_rate=sim_params["sample_rate"],
        viscosity=viscosity *
        2,  # We mis-specify viscosity since we measure experimental drag
        driving_frequency_guess=32,
        hydrodynamically_correct=hydro,
        distance_to_surface=dist,
    )
    ps_lateral = calculate_power_spectrum(volts_lateral, sample_rate=78125)
    lateral_fit = fit_power_spectrum(ps_lateral, lateral_model)
    np.testing.assert_allclose(
        lateral_fit[lateral_model._measured_drag_fieldname].value,
        gamma_ref,
        rtol=5e-2)

    # Axial calibration
    axial_model = PassiveCalibrationModel(
        **shared_pars,
        viscosity=viscosity *
        2,  # We deliberately mis-specify the viscosity to test the transfer
        hydrodynamically_correct=False,
        distance_to_surface=dist,
        axial=True,
    )

    # Transfer the result to axial calibration
    axial_model._set_drag(
        lateral_fit[lateral_model._measured_drag_fieldname].value)

    volts_axial, stage = height_simulation(brenner_axial)
    ps_axial = calculate_power_spectrum(volts_axial, sample_rate=78125)
    axial_fit = fit_power_spectrum(ps_axial, axial_model)
    np.testing.assert_allclose(axial_fit["gamma_ex_lateral"].value,
                               gamma_ref,
                               rtol=5e-2)
    np.testing.assert_allclose(axial_fit["kappa"].value,
                               sim_params["stiffness"],
                               rtol=5e-2)
    assert (axial_fit["gamma_ex_lateral"].description ==
            "Bulk drag coefficient from lateral calibration")
Ejemplo n.º 17
0
 def fit_spectrum(active, hydro):
     model = (ActiveCalibrationModel(
         **active_pars, **shared_pars, hydrodynamically_correct=hydro)
              if active else PassiveCalibrationModel(
                  **shared_pars, hydrodynamically_correct=hydro))
     return fit_power_spectrum(power_spectrum, model)
Ejemplo n.º 18
0
def calibrate_force(
    force_voltage_data,
    bead_diameter,
    temperature,
    *,
    viscosity=None,
    active_calibration=False,
    driving_data=np.asarray([]),
    driving_frequency_guess=37,
    axial=False,
    hydrodynamically_correct=False,
    rho_sample=None,
    rho_bead=1060.0,
    distance_to_surface=None,
    fast_sensor=False,
    sample_rate=78125,
    num_points_per_block=2000,
    fit_range=(1e2, 23e3),
    excluded_ranges=[],
    fixed_diode=None,
    drag=None,
):
    """Determine force calibration factors.

    The power spectrum calibration algorithm implemented here is based on [1]_, [2]_, [3]_, [4]_,
    [5]_, [6]_.

    References
    ----------
    .. [1] Berg-Sørensen, K. & Flyvbjerg, H. Power spectrum analysis for optical tweezers. Rev. Sci.
           Instrum. 75, 594 (2004).
    .. [2] Tolić-Nørrelykke, I. M., Berg-Sørensen, K. & Flyvbjerg, H. MatLab program for precision
           calibration of optical tweezers. Comput. Phys. Commun. 159, 225–240 (2004).
    .. [3] Hansen, P. M., Tolic-Nørrelykke, I. M., Flyvbjerg, H. & Berg-Sørensen, K.
           tweezercalib 2.1: Faster version of MatLab package for precise calibration of optical
           tweezers. Comput. Phys. Commun. 175, 572–573 (2006).
    .. [4] Berg-Sørensen, K., Peterman, E. J. G., Weber, T., Schmidt, C. F. & Flyvbjerg, H. Power
           spectrum analysis for optical tweezers. II: Laser wavelength dependence of parasitic
           filtering, and how to achieve high bandwidth. Rev. Sci. Instrum. 77, 063106 (2006).
    .. [5] Tolić-Nørrelykke, S. F, and Flyvbjerg, H, "Power spectrum analysis with least-squares
           fitting: amplitude bias and its elimination, with application to optical tweezers and
           atomic force microscope cantilevers." Review of Scientific Instruments 81.7 (2010)
    .. [6] Tolić-Nørrelykke S. F, Schäffer E, Howard J, Pavone F. S, Jülicher F and Flyvbjerg, H.
           Calibration of optical tweezers with positional detection in the back focal plane,
           Review of scientific instruments 77, 103101 (2006).

    Parameters
    ----------
    force_voltage_data : array_like
        Uncalibrated force data in volts.
    bead_diameter : float
        Bead diameter [um].
    temperature : float
        Liquid temperature [Celsius].
    viscosity : float, optional
        Liquid viscosity [Pa*s].
        When omitted, the temperature will be used to look up the viscosity of water at that
        particular temperature.
    active_calibration : bool, optional
        Active calibration, when set to True, driving_data must also be provided.
    driving_data : array_like, optional
        Array of driving data.
    driving_frequency_guess : float, optional
         Guess of the driving frequency.
    axial : bool, optional
        Is this an axial calibration? Only valid for a passive calibration.
    hydrodynamically_correct : bool, optional
        Enable hydrodynamically correct model.
    rho_sample : float, optional
        Density of the sample [kg/m**3]. Only used when using hydrodynamically correct model.
    rho_bead : float, optional
        Density of the bead [kg/m**3]. Only used when using hydrodynamically correct model.
    distance_to_surface : float, optional
        Distance from bead center to the surface [um]
        When specifying `None`, the model will use an approximation which is only suitable for
        measurements performed deep in bulk.
    fast_sensor : bool, optional
         Fast sensor? Fast sensors do not have the diode effect included in the model.
    sample_rate : float, optional
         Sample rate at which the signals were acquired.
    fit_range : tuple of float, optional
        Tuple of two floats (f_min, f_max), indicating the frequency range to use for the full model
        fit. [Hz]
    num_points_per_block : int, optional
        The spectrum is first block averaged by this number of points per block.
        Default: 2000.
    excluded_ranges : list of tuple of float, optional
        List of ranges to exclude specified as a list of (frequency_min, frequency_max).
    drag : float, optional
        Overrides the drag coefficient to this particular value.
    fixed_diode : float, optional
        Fix diode frequency to a particular frequency.
    """
    if active_calibration:
        if axial:
            raise ValueError("Active calibration is not supported for axial force.")
        if drag:
            raise ValueError("Drag coefficient cannot be carried over to active calibration.")

    if fixed_diode and fast_sensor:
        raise ValueError("When using fast_sensor=True, there is no diode model to fix.")

    if active_calibration and driving_data.size == 0:
        raise ValueError("Active calibration requires the driving_data to be defined.")

    model_params = {
        "bead_diameter": bead_diameter,
        "viscosity": viscosity,
        "temperature": temperature,
        "fast_sensor": fast_sensor,
        "distance_to_surface": distance_to_surface,
        "hydrodynamically_correct": hydrodynamically_correct,
        "rho_sample": rho_sample,
        "rho_bead": rho_bead,
    }

    model = (
        ActiveCalibrationModel(
            driving_data,
            force_voltage_data,
            sample_rate,
            driving_frequency_guess=driving_frequency_guess,
            **model_params,
        )
        if active_calibration
        else PassiveCalibrationModel(**model_params, axial=axial)
    )

    if drag:
        model._set_drag(drag)

    if fixed_diode:
        model._filter = FixedDiodeModel(fixed_diode)

    ps = calculate_power_spectrum(
        force_voltage_data,
        sample_rate,
        fit_range,
        num_points_per_block,
        excluded_ranges=excluded_ranges,
    )

    return fit_power_spectrum(ps, model)