def test_fixed_diode(reference_models):
    """Test fixed diode model"""
    models, power_spectrum = model_and_data(reference_models,
                                            diode_alpha=0.4,
                                            fast_sensor=True)
    for model in models:
        model._filter = FixedDiodeModel(14000)
        fit = fit_power_spectrum(power_spectrum,
                                 model=model,
                                 bias_correction=False)

        np.testing.assert_allclose(fit.results["fc"].value, 4000, 1e-6)
        np.testing.assert_allclose(fit.results["D"].value, 1.14632, 1e-6)
        np.testing.assert_allclose(fit.results["alpha"].value, 0.4, 1e-6)

        # Diode frequency is a parameter now and not a result
        assert fit.params["f_diode"].value == 14000
        assert "f_diode" not in fit.results

        # Fix diode to the wrong frequency. We should not get a great fit that way.
        model._filter = FixedDiodeModel(13000)
        fit = fit_power_spectrum(power_spectrum,
                                 model=model,
                                 bias_correction=False)
        assert abs(fit.results["fc"].value - 4000) > 1.0
        assert abs(fit.results["D"].value - 1.14632) > 1e-2
def test_bias_correction():
    """Functional end to end test for active calibration"""

    np.random.seed(0)
    force_voltage_data, driving_data = generate_active_calibration_test_data(
        duration=20,
        sample_rate=78125,
        bead_diameter=1.03,
        stiffness=0.2,
        viscosity=1.002e-3,
        temperature=20,
        pos_response_um_volt=0.618,
        driving_sinusoid=(500, 31.95633),
        diode=(0.4, 13000),
    )

    model = ActiveCalibrationModel(driving_data, force_voltage_data, 78125,
                                   1.03, 32, 1.002e-3, 20)

    # Low blocking deliberately leads to higher bias (so it's easier to measure)
    block_size = 3
    power_spectrum_low = calculate_power_spectrum(
        force_voltage_data, 78125, num_points_per_block=block_size)

    fit_biased = fit_power_spectrum(power_spectrum_low,
                                    model,
                                    bias_correction=False)
    fit_debiased = fit_power_spectrum(power_spectrum_low,
                                      model,
                                      bias_correction=True)

    bias_corr = block_size / (block_size + 1)
    np.testing.assert_allclose(fit_debiased["D"].value,
                               fit_biased["D"].value * bias_corr)
    np.testing.assert_allclose(fit_debiased["err_D"].value,
                               fit_biased["err_D"].value * bias_corr)

    # Biased vs debiased estimates (in comments are the reference values for N_pts_per_block = 150
    # Note how the estimates are better on the right.
    comparisons = {
        "fc": [3310.651532245893,
               3310.651532245893],  # Ref: 3277.6576037747836
        "D": [1.472922058628551,
              1.1046915439714131],  # Ref: 1.0896306365192108
        "kappa": [0.15317517466591019,
                  0.2043759281959786],  # Ref: 0.20106518840690035
        "Rd": [0.6108705452113169,
               0.6106577513480039],  # Ref: 0.6168083172053238
        "Rf": [93.57020246100325, 124.8037447418174],  # Ref: 124.0186805098316
    }

    for key, values in comparisons.items():
        for fit, value in zip([fit_biased, fit_debiased], values):
            np.testing.assert_allclose(fit[key].value, value)

    assert fit_biased.params["Bias correction"].value is False
    assert fit_debiased.params["Bias correction"].value is True
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)
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)
def test_underfit_fast_sensor(reference_models):
    """Diode effect in the data, but the model doesn't actually have it. Should have a poor fit."""
    models, power_spectrum = model_and_data(reference_models,
                                            diode_alpha=0.4,
                                            fast_sensor=True)
    for model in models:
        fit = fit_power_spectrum(power_spectrum, model=model)
        assert abs(fit.results["fc"].value - 4000) > 1.0
        assert abs(fit.results["D"].value - 1.14632) > 0.1
Exemple #6
0
def test_integration_active_calibration_hydrodynamics_bulk(
        integration_test_parameters):
    shared_pars, simulation_pars = integration_test_parameters

    np.random.seed(10071985)
    shared_pars["distance_to_surface"] = None
    volts, nanostage = generate_active_calibration_test_data(
        10, **simulation_pars, **shared_pars)
    model = ActiveCalibrationModel(
        nanostage,
        volts,
        **shared_pars,
        sample_rate=simulation_pars["sample_rate"],
        driving_frequency_guess=33,
    )
    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,
        "Bead diameter": 1.03,
        "Viscosity": 0.0011,
        "Temperature": 25,
        "Driving frequency (guess)": 33,
        "Sample rate": 78125,
        "num_windows": 5,
        "Max iterations": 10000,
        "Fit tolerance": 1e-07,
        "Points per block": 2000,
    }
    expected_results = {
        "Rd": 0.6095674943889238,
        "kappa": 0.10359295685924054,
        "Rf": 63.14689914902713,
        "gamma_0": 1.0678273429551705e-08,
        "gamma_ex": 1.0978355408018856e-08,
        "fc": 1501.803370440244,
        "D": 1.0091069801313286,
        "f_diode": 14669.862556235465,
        "alpha": 0.41657472149713015,
        "err_fc": 11.599562805624199,
        "err_D": 0.007332334985757522,
        "err_f_diode": 376.8360414675165,
        "err_alpha": 0.014653541838852356,
        "chi_squared_per_deg": 0.8692145118092963,
        "backing": 14.917612794899505,
    }

    assert fit.params["Distance to surface"].value is None
    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)
Exemple #7
0
def test_integration_active_calibration_hydrodynamics(
        integration_test_parameters):
    shared_pars, simulation_pars = integration_test_parameters

    np.random.seed(10071985)
    volts, nanostage = generate_active_calibration_test_data(
        10, **simulation_pars, **shared_pars)
    model = ActiveCalibrationModel(
        nanostage,
        volts,
        **shared_pars,
        sample_rate=simulation_pars["sample_rate"],
        driving_frequency_guess=33,
    )
    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,
        "Driving frequency (guess)": 33,
        "Sample rate": 78125,
        "num_windows": 5,
        "Max iterations": 10000,
        "Fit tolerance": 1e-07,
        "Points per block": 2000,
    }
    expected_results = {
        "Rd": 0.6092796748780891,
        "kappa": 0.10388246375443001,
        "Rf": 63.29347374183399,
        "gamma_0": 1.0678273429551705e-08,
        "gamma_ex": 1.0989730336350438e-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)
def test_fit_fixed_pars(reference_models):
    """Test model without diode effect"""
    models, power_spectrum = model_and_data(reference_models,
                                            diode_alpha=1.0,
                                            fast_sensor=True)
    for model in models:
        fit = fit_power_spectrum(power_spectrum,
                                 model=model,
                                 bias_correction=False)
        np.testing.assert_allclose(fit.results["fc"].value, 4000, 1e-6)
        np.testing.assert_allclose(fit.results["D"].value, 1.14632, 1e-6)
        assert "f_diode" not in fit.results
        assert "alpha" not in fit.results
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 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
Exemple #11
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)
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")
Exemple #13
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)
def test_integration_active_calibration(
    stiffness,
    viscosity,
    temperature,
    pos_response_um_volt,
    driving_sinusoid,
    diode,
    driving_frequency_guess,
    power_density,
    response_power,
):
    """Functional end to end test for active calibration"""
    sample_rate, bead_diameter = 78125, 1.03
    np.random.seed(0)
    force_voltage_data, driving_data = generate_active_calibration_test_data(
        duration=20,
        sample_rate=sample_rate,
        bead_diameter=bead_diameter,
        stiffness=stiffness,
        viscosity=viscosity,
        temperature=temperature,
        pos_response_um_volt=pos_response_um_volt,
        driving_sinusoid=driving_sinusoid,
        diode=diode,
    )

    model = ActiveCalibrationModel(
        driving_data,
        force_voltage_data,
        sample_rate,
        bead_diameter,
        driving_frequency_guess,
        viscosity,
        temperature,
    )

    # Validate estimation of the driving input
    np.testing.assert_allclose(model.driving_amplitude,
                               driving_sinusoid[0] * 1e-9,
                               rtol=1e-5)
    np.testing.assert_allclose(model.driving_frequency,
                               driving_sinusoid[1],
                               rtol=1e-5)

    np.testing.assert_allclose(model._response_power_density,
                               power_density,
                               rtol=1e-5)
    num_points_per_window = int(
        np.round(sample_rate * model.num_windows / model.driving_frequency))
    freq_axis = np.fft.rfftfreq(num_points_per_window, 1.0 / sample_rate)
    np.testing.assert_allclose(model._frequency_bin_width,
                               freq_axis[1] - freq_axis[0])

    power_spectrum = calculate_power_spectrum(force_voltage_data, sample_rate)
    fit = fit_power_spectrum(power_spectrum, model)

    np.testing.assert_allclose(fit["kappa"].value, stiffness, rtol=5e-2)
    np.testing.assert_allclose(fit["alpha"].value, diode[0], rtol=5e-2)
    np.testing.assert_allclose(fit["f_diode"].value, diode[1], rtol=5e-2)
    np.testing.assert_allclose(fit["Rd"].value,
                               pos_response_um_volt,
                               rtol=5e-2)

    response_calc = fit["Rd"].value * fit["kappa"].value * 1e3
    np.testing.assert_allclose(fit["Rf"].value, response_calc, rtol=1e-9)

    kt = scipy.constants.k * scipy.constants.convert_temperature(
        temperature, "C", "K")
    drag_coeff_calc = kt / (fit["D"].value * fit["Rd"].value**2)
    np.testing.assert_allclose(
        fit["gamma_0"].value,
        sphere_friction_coefficient(viscosity, bead_diameter * 1e-6),
        rtol=1e-9,
    )
    np.testing.assert_allclose(fit["gamma_ex"].value,
                               drag_coeff_calc * 1e12,
                               rtol=1e-9)

    np.testing.assert_allclose(fit["Bead diameter"].value, bead_diameter)
    np.testing.assert_allclose(fit["Driving frequency (guess)"].value,
                               driving_frequency_guess)
    np.testing.assert_allclose(fit["Sample rate"].value, sample_rate)
    np.testing.assert_allclose(fit["Viscosity"].value, viscosity)
    np.testing.assert_allclose(fit["num_windows"].value, 5)

    np.testing.assert_allclose(fit["driving_amplitude"].value,
                               driving_sinusoid[0] * 1e-3,
                               rtol=1e-5)
    np.testing.assert_allclose(fit["driving_frequency"].value,
                               driving_sinusoid[1],
                               rtol=1e-5)
    np.testing.assert_allclose(fit["driving_power"].value,
                               response_power,
                               rtol=1e-6)
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)
def test_faxen_correction_active():
    """Active calibration should barely be affected by surface corrections for the drag coefficient.
    However, the interpretation of gamma_ex, which may be carried over to the other calibration
    *is* important, so this should be covered by a specific test."""
    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 + 500e-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, stage = generate_active_calibration_test_data(
        10, hydrodynamically_correct=True, **sim_pars, **shared_pars)
    power_spectrum = calculate_power_spectrum(volts, sim_pars["sample_rate"])

    active_pars = {
        "force_voltage_data": volts,
        "driving_data": stage,
        "sample_rate": 78125,
        "driving_frequency_guess": 32,
        "hydrodynamically_correct": False,
    }

    model = ActiveCalibrationModel(**active_pars, **shared_pars)
    fit = 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.5979577465734786)
    np.testing.assert_allclose(fit.results["kappa"].value, 0.10852140970454485)
    np.testing.assert_allclose(fit.results["Rf"].value, 64.89121760190687)
    # gamma_0 and gamma_ex should be the same, since gamma_ex is corrected to be "in bulk".
    np.testing.assert_allclose(fit.results["gamma_0"].value,
                               1.0678273429551705e-08)
    np.testing.assert_allclose(fit.results["gamma_ex"].value,
                               1.1271667835127709e-08)

    # Disabling Faxen's correction on the drag makes the estimates *much* worse
    shared_pars["distance_to_surface"] = None
    model = ActiveCalibrationModel(**active_pars, **shared_pars)
    fit = fit_power_spectrum(power_spectrum, model, bias_correction=False)

    np.testing.assert_allclose(fit.results["Rd"].value, 0.5979577465734786)
    np.testing.assert_allclose(fit.results["kappa"].value, 0.10852140970454485)
    np.testing.assert_allclose(fit.results["Rf"].value, 64.89121760190687)
    # Not affected since this is gamma bulk
    np.testing.assert_allclose(fit.results["gamma_0"].value,
                               1.0678273429551705e-08)
    # The drag is now much different, since we're not using Faxen's law to back-correct the drag
    # to its actual bulk value.
    np.testing.assert_allclose(
        fit.results[model._measured_drag_fieldname].value,
        1.571688034506783e-08)
Exemple #17
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")
Exemple #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)