Custom Calibrations
There are two ways to handle calibrations in Village. The simplest option is to write a standalone script outside of Village, run it independently, and store the results in a format you can read from your tasks. The second option is to create a calibration class that integrates directly with the GUI, stores data as a .csv file, and makes calibration values accessible from any task.
This page describes the second approach.
Creating a Calibration Class
Create a Python file inside your project/code folder with a class that inherits from CalibrationBase. Inside this class, define a data structure using create_data_collection. You will typically also need a task class (inheriting from Task) that runs the calibration procedure, and a small GUI to interact with calibration parameters.
Three built-in calibration examples are included in the Village codebase under village/calibration/:
sound_calibration.pybpod_water_calibration.pycamera_calibration.py
These are a good starting point for understanding how calibration classes work.
Defining the Data Structure
Use create_data_collection in __init__ to define the columns and types of your calibration data. The example below shows how SoundCalibration class is declared:
from village.custom_classes.calibration_base import CalibrationBase
class SoundCalibration(CalibrationBase):
"""Sound speaker calibration and testing panel."""
def __init__(self) -> None:
super().__init__()
name = "sound_calibration"
columns = [
"date",
"speaker",
"sound_name",
"gain",
"dB_obtained",
"calibration_number",
"dB_expected",
"error(%)",
]
types = [str, int, str, float, float, int, float, float]
self.create_data_collection(name=name, columns=columns, types=types)
Querying Calibration Values from a Task
It is useful to add methods to your calibration class that retrieve the values needed during a task. For example, SoundCalibration provides get_sound_gain(), which takes a speaker, a target level in dB, and a sound name, and returns the gain value required to reach that level based on the stored calibration data:
def get_sound_gain(self, speaker: int, dB: float, sound_name: str) -> float:
try:
if dB == 0:
return 0.0
calibration_df = self.df[self.df["speaker"] == speaker]
calibration_df = calibration_df[calibration_df["sound_name"] == sound_name]
max_calibration = calibration_df["calibration_number"].max()
calibration_df = calibration_df[
calibration_df["calibration_number"] == max_calibration
]
val = get_x_value_interp(
calibration_df["gain"].values,
calibration_df["dB_obtained"].values,
dB,
)
if val is None:
raise ValueError
return val
except Exception:
raise ValueError(
f"\n\n\t--> SOUND CALIBRATION PROBLEM !!!!!!\n\n"
f"Cannot provide a valid gain for {dB} dB, "
f"speaker {speaker}, sound {sound_name}.\n"
f"1. Make sure you have calibrated the sound you are using.\n"
f"2. Make sure the dB is within calibration range.\n"
f"3. Check sound_calibration.csv in 'data'.\n"
)
Any method defined in your calibration class is accessible from any task via the calibrations object:
self.calibrations.sound_calibration.get_sound_gain(speaker, dB, sound_name)