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

self.pattern_shape

(height, width) in pixels, set by configure_system

self.pixel_size_um

microns per pixel (accounts for binning)

self.get_um_meshgrid()

returns (xx, yy) arrays in µm, shape pattern_shape

self.center_um()

returns (cx, cy) center of the FOV in µm

self.add_requirement(channel_name, raw, seg)

declare that generate needs raw/seg data for a channel

self.request_stim(raw, seg)

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