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_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)
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 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_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_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 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_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_near_surface_consistency(): """For small beads, the results (Rf, Rd, and kappa) with or without the hydrodynamically correct model should be the same near a surface. Since internally, the effect of the surface is handled very differently in all the calibration options (hydro on/off, active on/off), this provides an integration test that actually tests whether all the parts are functioning correctly. AC on, Hydro off - Drag is measured and used directly. AC on, Hydro on - Drag is measured and used directly. AC off, Hydro off - Surface effect on drag coefficient is modelled with Faxen's law. AC off, Hydro on - Surface effect on drag coefficient is in equation describing the spectrum. """ bead_diameter = 0.5 shared_pars = { "bead_diameter": bead_diameter, "viscosity": 1.1e-3, "temperature": 25, "rho_sample": 997.0, "rho_bead": 1040.0, "distance_to_surface": bead_diameter, } sim_pars = { "sample_rate": 78125, "stiffness": 0.1, "pos_response_um_volt": 0.618, "driving_sinusoid": (500, 31.95633), "diode": (0.6, 15000), } np.random.seed(1337) 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, } 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) parameters_of_interest = { "kappa": sim_pars["stiffness"], "Rd": sim_pars["pos_response_um_volt"], "Rf": sim_pars["stiffness"] * sim_pars["pos_response_um_volt"] * 1e3, } for active_calibration in (True, False): for hydrodynamic_model in (True, False): fit = fit_spectrum(active_calibration, hydrodynamic_model) # Note that the corner frequency of the hydrodynamic model is specified in bulk, while # the regular model has its corner frequency specified at the current height. fc_bulk = (fit["fc"].value if hydrodynamic_model else fit["fc"].value * faxen_factor(shared_pars["distance_to_surface"] * 1e-6, bead_diameter * 1e-6 / 2)) np.testing.assert_allclose(fc_bulk, 3070.33, rtol=2e-2) for param, ref_value in parameters_of_interest.items(): np.testing.assert_allclose(fit[param].value, ref_value, rtol=2e-2)
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_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")
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)
def power(data): return calculate_power_spectrum(data, params["sample_rate"], fit_range=(1, 2e4)).power
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_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)