Writing Custom Pattern Methods
Overview
Custom patterns provide a way to extend PyCLM and enable a wide range of new experiments using python. The custom pattern API is set up to minimize the amount of boilerplate code the user has to write.
The first step in writing a custom pattern method is to implement a subclass of the PatternMethod base class.
from pyclm import PatternMethod
class MyCustomPattern(PatternMethod):
"""A custom pattern method"""
...
The subclass needs to implement two methods — __init__ and generate — and set a name class attribute.
from pyclm import PatternMethod, PatternContext
class MyCustomPattern(PatternMethod):
"""A custom pattern method."""
name = "custom_method" # used in logging; does not need to match the run_pyclm dict key
# keyword arguments defined here can be set in the experiment .toml
def __init__(self, keyword_a="default_value_a", keyword_b=42, **kwargs):
super().__init__(**kwargs) # boilerplate, always needs to be present
# store keywords as attributes so generate() can access them
self.attribute_a = keyword_a
self.attribute_b = keyword_b
# declare what imaging data generate() needs — called once at startup
self.add_requirement("RFP", raw=True, seg=False) # raw image from the RFP channel
self.add_requirement("GFP", raw=False, seg=True) # segmentation mask from the GFP channel
def generate(self, context: PatternContext):
# unpack data declared in __init__ via add_requirement()
raw = context.raw("RFP") # np.ndarray — most recent RFP image
seg = context.segmentation("GFP") # np.ndarray — labelled segmentation mask
# x/y coordinates of every pixel in microns (same shape as the camera output)
xx, yy = self.get_um_meshgrid()
# center of the field of view in microns
cx, cy = self.center_um()
# return a float array in [0, 1] the same shape as the camera output
# e.g. illuminate the right half at the intensity fraction set by keyword_b:
pattern = (self.attribute_b / 100) * (xx > cx)
return pattern
When you run pyclm, you must supply any custom generated patterns in a dictionary, with the name of the method as it should be referenced in the .toml
from pyclm import run_pyclm
from my_pattern import MyCustomPattern
experiment_directory = ...
run_pyclm(experiment_directory, pattern_methods={"custom_method": MyCustomPattern})
PyCLM will know that you want to use this pattern if you put it in your .toml. The .toml should also supply any keyword arguments that you want to overwrite.
# experiment_a.toml
[pattern]
method = "custom_method"
keyword_b = 75 # illuminate the right half at 75% intensity
Example 1 — static (open-loop) pattern
This method illuminates an annulus whose inner and outer radii are set in the TOML. It requires no live imaging data.
# my_patterns.py
import numpy as np
from pyclm import PatternMethod
class AnnulusPattern(PatternMethod):
"""Illuminates a ring centerd on the FOV."""
name = "annulus"
def __init__(self, inner_radius_um=30, outer_radius_um=60, **kwargs):
super().__init__(**kwargs)
# No add_requirement calls — this pattern needs no image data.
self.r_inner = inner_radius_um
self.r_outer = outer_radius_um
def generate(self, context) -> np.ndarray:
xx, yy = self.get_um_meshgrid()
cx, cy = self.center_um()
dist_sq = (xx - cx) ** 2 + (yy - cy) ** 2
ring = (dist_sq >= self.r_inner ** 2) & (dist_sq <= self.r_outer ** 2)
return ring.astype(np.float32)
Matching TOML:
[pattern]
method = "annulus"
inner_radius_um = 25
outer_radius_um = 80
All keys other than method are passed verbatim as kwargs to __init__. Default values in __init__ are used when a key is absent from the TOML.
Registration:
from pyclm import run_pyclm
from my_patterns import AnnulusPattern
run_pyclm(
experiment_directory,
pattern_methods={"annulus": AnnulusPattern},
)
Example 2 — feedback-controller (closed-loop)
This method reads per-cell mean intensity and illuminates only the cells below a target, with the threshold tunable from the TOML.
# my_patterns.py
import numpy as np
from skimage.measure import regionprops
from pyclm import PatternMethod
class IntensityThresholdPattern(PatternMethod):
"""Illuminates cells whose mean fluorescence is below `target_intensity`."""
name = "intensity_threshold"
def __init__(self, channel="GFP", target_intensity=3000, **kwargs):
super().__init__(**kwargs)
self.channel = channel
self.target = target_intensity
# Declare that generate() needs both the raw image and the
# segmentation mask for the chosen channel.
self.add_requirement(channel_name=channel, raw=True, seg=True)
def generate(self, context) -> np.ndarray:
raw = context.raw(self.channel) # float array, camera coords
seg = context.segmentation(self.channel) # label array, same shape
h, w = self.pattern_shape
out = np.zeros((int(h), int(w)), dtype=np.float32)
for prop in regionprops(seg, intensity_image=raw):
if prop.intensity_mean < self.target:
r0, c0, r1, c1 = prop.bbox
out[r0:r1, c0:c1] += prop.image # binary mask of this cell
return np.clip(out, 0, 1)
Matching TOML:
[segmentation]
method = "cellpose"
model = "cpsam"
[pattern]
method = "intensity_threshold"
channel = "GFP"
target_intensity = 4500
Base class API
from pyclm.core.patterns.pattern import PatternMethod, PatternContext
Method / attribute |
Purpose |
|---|---|
|
|
|
microns per pixel (accounts for binning) |
|
returns |
|
returns |
|
declare that |
|
same, but for the stimulation-output channel |
The generate method receives a PatternContext and must return a float array with values in [0, 1] and shape matching self.pattern_shape.
context.raw(channel_name) # np.ndarray – raw fluorescence image
context.segmentation(channel_name) # np.ndarray – labelled segmentation mask
context.stim_raw() # raw image of the stimulation channel
context.time # elapsed experiment time in seconds