Skip to content

SimulatedPhotometryGenerator

A class for simulating photometry data with advanced support for photobleaching and custom event dynamics.


Generator for simulated fiber photometry data.

Simulated data is generated with the following layers

1) event-driven neural signals rendered from timestamped events 2) B: photobleaching curve, modeled with a 5-param negative bi-exponetial 3) M: movement attenuation artifacts 4) noise_dependent(_exp & _iso): Gaussian noise that scales with photobleaching, calculated seperately for experimental and isosbestic signal 5) noise_independent(_exp & _iso): Gaussian noise that is independent of photobleaching, calculated seperately for experimental and isosbestic signal

Experimental signal = (B + event_signal_exp + noise_exp) * M Isosbestic signal = (B + event_signal_iso + noise_iso) * M

Outputs (attributes): t: time points F_exp, F_iso: experimental and isosbestic signals neural_true, neural_norm: true and null-Z normalized experimental event signals event_times_sec: times of the base neural events events: mapping of event labels to timestamps event_peak_specs: per-label peak-generation parameters B, M: photobleaching curve and movement attenuation

Build simulated photometry data.

Parameters:

  • T_sec (float, default: 1000 ) –

    Amount of seconds in data.

  • fs (float, default: 30.0 ) –

    Sampling frequency in Hz.

  • n_events (int, default: 100 ) –

    Number of base neural events.

  • event_dur_sec (float, default: 3.0 ) –

    How long a base neural event is in seconds.

  • A_neural (float, default: 0.02 ) –

    Magnitude of the base event signal.

  • tau_p_sec (float, default: 0.6 ) –

    Exponential param for neural signal generation.

  • shape_k (int, default: 3 ) –

    Controls the skewness of neural signal, must be >= 2.

  • buffer_sec (float, default: 5 ) –

    Minimum number of seconds between a base neural event and end of time series.

  • n_artifacts (int, default: 60 ) –

    Number of attenuation artifacts.

  • artifact_tau_sec (float, default: 0.8 ) –

    Exponential param for attenuation generation.

  • artifact_depth_range (Tuple[float, float], default: (0.02, 0.15) ) –

    Fractional bounds for attenuation magnitude.

  • dependent_sigma_exp (float, default: 0.0001 ) –

    Magnitude of intensity-proportional noise in experimental signal.

  • independent_sigma_exp (float, default: 1e-05 ) –

    Magnitude of intensity-independent noise in experimental signal.

  • dependent_sigma_iso (float, default: 0.0001 ) –

    Magnitude of intensity-proportional noise in isosbestic signal.

  • independent_sigma_iso (float, default: 1e-05 ) –

    Magnitude of intensity-independent noise in isosbestic signal.

  • bleach_params (dict, default: dict(a1=50, a2=20, tau1=300, tau2=10000, b0=1) ) –

    Params for the negative bi-exponential photobleaching model.

  • artifact_mask (ndarray | None, default: None ) –

    Optional multiplicative attenuation mask of shape (N,).

Returns:

  • None

Source code in pyFiberPhotometry/utils/sim.py
def __init__(
    self,
    # time
    T_sec: float = 1000,
    fs: float = 30.0,
    # event
    n_events: int = 100,
    event_dur_sec: float = 3.0,
    buffer_sec: float = 5,
    # neural signal
    A_neural: float = 0.02,
    tau_p_sec: float = 0.6,
    shape_k: int = 3,
    # photobleaching
    bleach_params: dict = dict(a1=50, a2=20, tau1=300, tau2=10000, b0=1),
    bleach_iso_scale: float = 0.9,
    # movement attenuation
    n_artifacts: int = 60,
    artifact_tau_sec: float = 0.8,
    artifact_depth_range: Tuple[float, float] = (0.02, 0.15),
    # noise
    dependent_sigma_exp: float = 1e-4,
    dependent_sigma_iso: float = 1e-4,
    independent_sigma_exp: float = 1e-5,
    independent_sigma_iso: float = 1e-5,
    seed=0,
    # custom artifacting
    artifact_mask: np.ndarray | None = None,
):
    """Build simulated photometry data.

    Args:
        T_sec (float): Amount of seconds in data.
        fs (float): Sampling frequency in Hz.
        n_events (int): Number of base neural events.
        event_dur_sec (float): How long a base neural event is in seconds.
        A_neural (float): Magnitude of the base event signal.
        tau_p_sec (float): Exponential param for neural signal generation.
        shape_k (int): Controls the skewness of neural signal, must be >= 2.
        buffer_sec (float): Minimum number of seconds between a base neural event and end of time series.
        n_artifacts (int): Number of attenuation artifacts.
        artifact_tau_sec (float): Exponential param for attenuation generation.
        artifact_depth_range (Tuple[float, float]): Fractional bounds for attenuation magnitude.
        dependent_sigma_exp (float): Magnitude of intensity-proportional noise in experimental signal.
        independent_sigma_exp (float): Magnitude of intensity-independent noise in experimental signal.
        dependent_sigma_iso (float): Magnitude of intensity-proportional noise in isosbestic signal.
        independent_sigma_iso (float): Magnitude of intensity-independent noise in isosbestic signal.
        bleach_params (dict): Params for the negative bi-exponential photobleaching model.
        artifact_mask (np.ndarray | None): Optional multiplicative attenuation mask of shape (N,).

    Returns:
        None
    """
    self.fs = float(fs)
    self.rng = np.random.default_rng(seed)

    self.T_sec = float(T_sec)
    self.n_events = int(n_events)
    self.event_dur_sec = float(event_dur_sec)
    self.buffer_sec = float(buffer_sec)

    self.A_neural = float(A_neural)
    self.tau_p_sec = float(tau_p_sec)
    self.shape_k = int(shape_k)

    self.bleach_params = bleach_params
    self.bleach_iso_scale = float(bleach_iso_scale)

    self.n_artifacts = int(n_artifacts)
    self.artifact_tau_sec = float(artifact_tau_sec)
    self.artifact_depth_range = tuple(artifact_depth_range)

    self.dependent_sigma_exp = float(dependent_sigma_exp)
    self.dependent_sigma_iso = float(dependent_sigma_iso)
    self.independent_sigma_exp = float(independent_sigma_exp)
    self.independent_sigma_iso = float(independent_sigma_iso)

    self.artifact_mask = artifact_mask

    self.events: dict[str, np.ndarray] = {}
    self.event_peak_specs: dict[str, dict[str, Any]] = {}
    self.event_signals: dict[str, dict[str, np.ndarray]] = {}

    self.build_layers()

add_event(time_range, overall_prob, choices, choice_probs, peak_specs=None, relative_to='event', allow_out_of_bounds=False)

Add stochastic event timestamps relative to an existing event stream and rebuild the simulated signal so the new events contribute peaks.

Parameters:

  • time_range (tuple[float, float]) –

    Relative time bounds around each anchor event.

  • overall_prob (float) –

    Probability that an event occurs for each anchor event.

  • choices (list[str]) –

    Event labels to assign to generated timestamps.

  • choice_probs (list[float]) –

    Relative probabilities for each label in choices.

  • peak_specs (dict[str, dict] | None, default: None ) –

    Optional per-label peak settings. Each label-specific dict can include amplitude, event_dur_sec, tau_p_sec, shape_k, channel ('exp', 'iso', or 'both'), and scale_with_bleach.

  • relative_to (str, default: 'event' ) –

    Existing event label used as the anchor event.

  • allow_out_of_bounds (bool, default: False ) –

    If False, drop generated events outside the session time bounds.

Returns: dict[str, np.ndarray]: Updated timestamp arrays for the labels in choices.

Source code in pyFiberPhotometry/utils/sim.py
def add_event(
    self,
    time_range: tuple[float, float],
    overall_prob: float,
    choices: list[str],
    choice_probs: list[float],
    peak_specs: dict[str, dict[str, Any]] | None = None,
    relative_to: str = "event",
    allow_out_of_bounds: bool = False,
) -> dict[str, np.ndarray]:
    """
    Add stochastic event timestamps relative to an existing event stream and rebuild the
    simulated signal so the new events contribute peaks.

    Args:
        time_range (tuple[float, float]): Relative time bounds around each anchor event.
        overall_prob (float): Probability that an event occurs for each anchor event.
        choices (list[str]): Event labels to assign to generated timestamps.
        choice_probs (list[float]): Relative probabilities for each label in ``choices``.
        peak_specs (dict[str, dict] | None): Optional per-label peak settings. Each label-specific
            dict can include ``amplitude``, ``event_dur_sec``, ``tau_p_sec``, ``shape_k``,
            ``channel`` ('exp', 'iso', or 'both'), and ``scale_with_bleach``.
        relative_to (str): Existing event label used as the anchor event.
        allow_out_of_bounds (bool): If False, drop generated events outside the session time bounds.
    Returns:
        dict[str, np.ndarray]: Updated timestamp arrays for the labels in ``choices``.
    """
    labels, probs = self._validate_add_event_args(
        time_range=time_range,
        overall_prob=overall_prob,
        choices=choices,
        choice_probs=choice_probs,
        relative_to=relative_to,
        peak_specs=peak_specs,
    )

    anchors = np.asarray(self.events[relative_to], dtype=float)
    occurs = self.rng.random(anchors.size) < overall_prob
    new_times = anchors[occurs] + self.rng.uniform(time_range[0], time_range[1], size=occurs.sum())
    new_labels = self.rng.choice(labels, size=occurs.sum(), p=probs)

    if not allow_out_of_bounds:
        in_bounds = (new_times >= self.t[0]) & (new_times <= self.t[-1])
        new_times = new_times[in_bounds]
        new_labels = new_labels[in_bounds]

    peak_specs = {} if peak_specs is None else peak_specs
    updated = {}
    for label in labels:
        label = str(label)
        existing = np.asarray(self.events.get(label, np.array([], dtype=float)), dtype=float)
        added = np.asarray(new_times[new_labels == label], dtype=float)
        merged = existing.copy() if added.size == 0 else np.sort(np.concatenate([existing, added]))
        self.events[label] = merged

        fallback = self.event_peak_specs.get(label, self._default_peak_spec())
        self.event_peak_specs[label] = self._normalize_peak_spec(peak_specs.get(label), fallback=fallback)
        updated[label] = merged.copy()

    self._refresh_event_layers()
    return updated