Source code for prsctrl.measurement.pump_power_calibration

"""Perform pump light power calibration.
"""
from multiprocessing import Queue
from devctrl.data import NDArray
from devctrl.devices import DummyDevice, PowerMeter, Laser, LedController
import numpy as np
from time import sleep
import matplotlib.pyplot as plt

import logging
log = logging.getLogger(__name__)

from devctrl.utility.settings import SettingsClass, dataclass, field
from typing import Optional, Literal


[docs] @dataclass class PowerCalibration(SettingsClass): """Class holding calibration data for converting an LED current in A or a laser power in mW to the effective power density in mW/cm^2 (and back).""" light_source_type: Literal["led", "laser"] light_source_name: str = field( metadata={"doc": "Name of the light source."}, ) power_meter_name: str = field( metadata={"doc": "Name of the power meter that was used."}, ) wavelength_nm: float = field( metadata={"doc": "Wavelength of the light source in nm."}, ) beam_area_cm2: float = field( metadata={"doc": "Beam area in cm^2 for calibration (or active detector area)."}, ) calibration_data: NDArray = field( metadata={"doc": "Calibration data with shape (n_steps, 2).\nFirst column is either laser power in mW or led current in A.\nSecond column is measured power density in mW/cm^2."}, ) method: Literal["interpolate", "polyfit"] = field( default="interpolate", metadata={"doc": "Method to use for conversion (both directions):\ninterpolate: linearly interpolate the data\npolyfit: fit a polynomial to the data (polynomial degree is set by polyfit_degree)."}, ) polyfit_degree: int = field( default=2, metadata={"doc": "The degree of the polynomial to fit the calibration data (if method is polyfit').", "valid": (1, 10)}, )
[docs] def setting_to_power_density_mWcm2(self, x: float) -> float: """ :param x: LED current or laser power density :return: Power density in mW/cm^2 """ if self.method == "interpolate": # check increasing sequence, according to doc of np.interp if not np.all(np.diff(self.calibration_data[:,0]) > 0): raise RuntimeError(f"Interpolation x-data is supposed to be strictly increasing, but is: {self.calibration_data[:,0]}") p = np.interp(x, self.calibration_data[:,0], self.calibration_data[:,1]) elif self.method == "polyfit": f = np.polynomial.Polynomial.fit(self.calibration_data[:, 0], self.calibration_data[:, 1], self.polyfit_degree) p = f(x) else: raise RuntimeError(f"Invalid conversion method: '{self.method}'") return p
[docs] def power_density_mWcm2_to_setting(self, p_mWcm2: float) -> float: """ :param p: Power density in mW/cm^2 :return: Power setting, either LED current in A or laser power in mW """ if self.method == "interpolate": # check increasing sequence, according to doc of np.interp if not np.all(np.diff(self.calibration_data[:,1]) > 0): raise RuntimeError(f"Interpolation x-data is supposed to be strictly increasing, but is: {self.calibration_data[:,1]}") x = np.interp(p_mWcm2, self.calibration_data[:,1], self.calibration_data[:,0]) elif self.method == "polyfit": f = np.polynomial.Polynomial.fit(self.calibration_data[:, 1], self.calibration_data[:, 0], self.polyfit_degree) x = f(p_mWcm2) else: raise RuntimeError(f"Invalid conversion method: '{self.method}'") return x
[docs] def led_current_A_to_power_density_mWcm2(self, current_A: float) -> float: """ :param current_A: LED current in A :return: Power density in mW/cm^2 """ self._assert_device_type("led") p = self.setting_to_power_density_mWcm2(current_A) return np.round(p, 3)
[docs] def power_density_mWcm2_to_led_current_A(self, power_density_mWcm2: float) -> float: """ :param power_density_mWcm2: Power density in mW/cm^2 :return: LED current in A """ self._assert_device_type("led") p = self.power_density_mWcm2_to_setting(power_density_mWcm2) return np.round(p, 3)
[docs] def laser_power_mW_to_power_density_mWcm2(self, power_mW: float) -> float: """ :param power_mW: Laser power in mW :return: Power density in mW/cm^2 """ self._assert_device_type("laser") p = self.setting_to_power_density_mWcm2(power_mW) return np.round(p, 3)
[docs] def power_density_mWcm2_to_laser_power_mW(self, power_density_mWcm2: float) -> float: """ :param power_density_mWcm2: Power density in mW/cm^2 :return: Laser power in mW """ self._assert_device_type("laser") p = self.power_density_mWcm2_to_setting(power_density_mWcm2) return np.round(p, 3)
[docs] def _assert_device_type(self, device_type): if not self.light_source_type == device_type: raise RuntimeError(f"Calibration data is for '{self.light_source_type}', but conversion for '{device_type}' was requested.")
[docs] def plot(self, fig=None, axs=None): if fig is None or axs is None: fig, axs = plt.subplots(1, 2, layout="constrained") # setting to density axs[0].scatter(self.calibration_data[:,0], self.calibration_data[:,1], color="red") x = np.linspace(self.calibration_data[0,0], self.calibration_data[-1,0], 100) y = self.setting_to_power_density_mWcm2(x) axs[0].plot(x, y, color="black") # reverse: density to setting axs[1].scatter(self.calibration_data[:,1], self.calibration_data[:,0], color="red") xp = np.linspace(self.calibration_data[0,1], self.calibration_data[-1,1], 100) yp = self.power_density_mWcm2_to_setting(xp) axs[1].plot(xp, yp, color="black") if self.light_source_type == "led": axs[0].set_xlabel("LED Current [A]") axs[1].set_ylabel("LED Current [A]") else: axs[0].set_xlabel("Laser Power [mW]") axs[1].set_ylabel("Laser Power [mW]") axs[0].set_ylabel("Probe Power Density [mW/cm$^2$]") axs[1].set_xlabel("Probe Power Density [mW/cm$^2$]") return fig, axs
# 0.709 is the aperture area of the Thorlabs detector
[docs] def measure_light_intensity(power_meter: PowerMeter, light_source: Laser|LedController, wavelength_nm: float, max_laser_power_mW: float=155, max_led_current_A: float=0.7, n_steps: int=10, beam_area_cm2: float=0.709, data_queue: Optional[Queue]=None) -> PowerCalibration: """Measure the light intensity for multiple power settings. Take ``n_steps`` power measurements using different laser powers up to ``max_laser_power_mW``, or using different led currents up to ``max_led_current_A``. :param power_meter: Power meter instance :param light_source: Supported light source, either a laser or led :param wavelength_nm: The wavelength in nm at which the light is emitted :param max_laser_power_mW: Maximum laser power in mW to apply, if a laser is given :param max_led_current_A: Maximum LED current in A to apply, if a LED is given :param n_steps: Number of measurements to take :param beam_area_cm2: The beam area in cm^2. If the beam is larger than the detector, use the active detector area. :param data_queue: Optional data queue in which the result of each measurement step is put. Useful when this function is run in a separate thread. The following tuple is put in the queue: ``(index: int, power or current: float, unit (mW or A): str, measured power in mW: float, measured power density in mW/cm^2: float)``. :raises TypeError: If an unsupported light_source is given :return: Numpy array of shape ``(n_steps, 2)``, where first column is the applied power in mW or current in A, and the second column is the measurement power density in mW/cm^2 """ if isinstance(light_source, Laser): xargs = np.linspace(max_laser_power_mW/n_steps, max_laser_power_mW, n_steps) setter_f = lambda x: light_source.set_power_mW(x) light_source.set_mode("CP") light_source.set_power_mW(0) light_source.on() dev_type = "laser" dev_name = light_source.get_device_name() header = "Laser Power [mW]" unit = "mW" elif isinstance(light_source, LedController): xargs = np.linspace(max_led_current_A / n_steps, max_led_current_A, n_steps) setter_f = lambda x: light_source.set_current_A(x) light_source.set_mode("CC") light_source.set_current_A(0) light_source.on() dev_type = "led" dev_name = light_source.get_device_name() led_name = light_source.get_led_name() if led_name: dev_name += " with LED: " + led_name header = "LED Current [A]" unit = "A" else: raise TypeError(f"Light source must be a Laser or LedController.") powers = np.empty((xargs.shape[0], 2)) power_meter.set_wavelength_nm(wavelength_nm) for i, x in enumerate(xargs): log.info(f"Step {i+1:02}/{n_steps:02}: {x} {unit}") setter_f(x) sleep(1.0) if isinstance(power_meter, DummyDevice): power_mW = i * 1.0 else: power_mW = power_meter.get_power_W() * 1e3 scaled_power_mWcm2 = power_mW/beam_area_cm2 print(f"power: {power_mW} mW, scaled power = {scaled_power_mWcm2} mW/cm^2") if data_queue: data_queue.put((i, x, unit, power_mW, scaled_power_mWcm2)) powers[i,0] = x powers[i,1] = scaled_power_mWcm2 log.info(f"Done, setting light source to {0} {unit}") setter_f(0) power_cal = PowerCalibration( light_source_type = dev_type, light_source_name= dev_name, power_meter_name=power_meter.get_device_name(), wavelength_nm=wavelength_nm, beam_area_cm2=beam_area_cm2, calibration_data=powers, ) return power_cal