Wavelength-aware optical unmixing
Use this workflow when several measured optical channels are modeled as known linear mixtures of declared sensor, hemodynamic, autofluorescence, background, or other contributions. It produces source estimates only when the coefficient matrix is independently identified and numerically usable.
This is an experimental product API. Wavelength metadata identifies what was measured; it does not generate mixing coefficients. Coefficients must come from a versioned independent calibration or be explicitly marked as user-declared.
The model and its boundary
For each sample, the declared model is:
where \(\mathbf{y}\) contains measured channels, \(\mathbf{s}\) contains declared component values, \(\mathbf{A}\) is a fixed mixing matrix, and \(\mathbf{b}\) contains fixed channel offsets. FiberPhotometry checks that \(\mathbf{A}\) has full component rank and does not exceed the requested condition-number gate.
It does not infer both \(\mathbf{A}\) and \(\mathbf{s}\) from the same biological recording. That factorization is not uniquely identified without additional constraints, even if every channel has a wavelength label.
Route A: calibrate from known components
Use calibrate_optical_mixing only when each calibration component is independently
controlled or measured. Rows are calibration samples; columns have exactly the
order declared in components and channels.
from fiberphotometry import (
ChannelIdentity,
OpticalComponent,
OpticalMixingCalibrationSpec,
calibrate_optical_mixing,
)
components = (
OpticalComponent(
"sensor_fluorescence",
role="sensor",
unit="a.u.",
description="known sensor-fluorophore calibration contribution",
),
OpticalComponent(
"hemoglobin_absorption",
role="hemodynamic",
unit="a.u.",
description="known absorber calibration contribution",
),
)
channels = (
ChannelIdentity(
"green-470",
site="DMS",
sensor="dLight",
role="sensor",
unit="photons/s",
excitation_wavelength_nm=470,
emission_wavelength_nm=525,
),
ChannelIdentity(
"green-405",
site="DMS",
sensor="dLight",
role="reference",
unit="photons/s",
excitation_wavelength_nm=405,
emission_wavelength_nm=525,
),
ChannelIdentity(
"red-560",
site="DMS",
sensor="hemoglobin-control",
role="control",
unit="photons/s",
excitation_wavelength_nm=560,
emission_wavelength_nm=600,
),
)
calibration = calibrate_optical_mixing(
known_component_values, # calibration sample × component
measured_channel_values, # calibration sample × optical channel
components,
channels,
calibration_id="phantom-2026-07-batch-04",
spec=OpticalMixingCalibrationSpec(
fit_intercept=True,
minimum_samples=50,
maximum_component_design_condition=100,
maximum_mixing_condition_number=100,
),
)
mixing_design = calibration.design
The known-component design must identify every coefficient: collinear component manipulations are refused. Each channel diagnostic records sample count, design rank, condition number, in-sample RMSE, and in-sample R². These describe the calibration fit; they are not biological-recording validation. The calibration arrays, masks, identities, choices, and version are bound by an evidence fingerprint.
Route B: declare an external matrix
When coefficients were estimated elsewhere, construct the same artifact directly:
from fiberphotometry import (
OpticalMixingChannel,
OpticalMixingDesign,
assess_optical_mixing_design,
)
mixing_design = OpticalMixingDesign(
components=components,
channels=(
OpticalMixingChannel(channels[0], (1.00, -0.42), offset=10.1),
OpticalMixingChannel(channels[1], (0.71, -0.18), offset=8.4),
OpticalMixingChannel(channels[2], (0.09, -0.93), offset=12.7),
),
coefficient_source="externally_calibrated",
calibration_id="phantom-2026-07-batch-04",
design_version="1",
)
assessment = assess_optical_mixing_design(mixing_design)
if assessment.status == "error":
raise RuntimeError([issue.code for issue in assessment.issues])
Set coefficient_source="user_declared" for exploratory values. That remains a
warning in the result rather than being presented as calibration evidence.
Apply without refitting
The biological recording matrix must follow the channel order in the design:
from fiberphotometry import (
OpticalUnmixingSpec,
extract_unmixed_component,
unmix_optical_signals,
)
result = unmix_optical_signals(
time_s,
optical_values, # time × optical channel
mixing_design,
OpticalUnmixingSpec(
maximum_condition_number=100,
require_wavelength_metadata=True,
require_leave_one_channel_out=False,
minimum_holdout_samples=50,
gap_factor=3,
),
valid=optical_valid, # time × optical channel
)
sensor_series = extract_unmixed_component(result, "sensor_fluorescence")
No temporal interpolation, smoothing, or refitting occurs. Every distinct pattern of available channels gets its own rank and conditioning check:
- a three-channel/two-component pattern can be solved;
- a two-channel/two-component subset can be solved only if its submatrix remains full rank and sufficiently conditioned; and
- a one-channel/two-component subset remains
NaNwith an exclusion reason.
availability_patterns records the channel IDs, sample count, rank, condition
number, solve status, and exclusion reason. The result also retains the original
clock, observation-validity matrix, solved component mask, timestamp-gap count,
reconstructed channel values, channel residuals, and an evidence fingerprint.
Reconstruction diagnostics
When there are more channels than components, each channel can potentially be held out while the remaining channels estimate the sources. The held-out channel is then reconstructed without using its observed value. Diagnostics report RMSE, RMSE normalized by observed standard deviation, and R².
A square two-channel/two-component matrix can unmix if it is full rank, but removing
either channel makes it underidentified. The result records unavailable holdout
diagnostics. Set require_leave_one_channel_out=True only when a redundant design
was prospectively required; it turns that limitation into a refusal gate.
Good reconstruction supports the declared linear optical model. It does not prove that a component is neural, that a reference is inert, or that source values are neurotransmitter concentrations.
Relationship to reference correction
reference_dff fits a reference-to-signal relationship within each biological
recording and is useful as an explicit OLS/IRLS preprocessing family. Optical
unmixing answers a different question: given an independently identified
multichannel mixing system, what component values reproduce each simultaneous
measurement?
Do not substitute one silently for the other. Treat them as named alternatives in a robustness multiverse when both are scientifically defensible. Run sensor/isobestic validity and inspect event-correlated controls; a numerically invertible matrix does not make a control biologically valid.
Assumptions and current limits
- Mixing is linear, additive, and fixed for the applied recording.
- Calibration units and the biological recording's channel units must match.
- Component units and coefficient scales remain those declared by calibration.
- Motion-dependent, saturation-dependent, or time-varying coefficients are not estimated.
- Non-negative, Bayesian, and nonlinear source models are not silently imposed.
- Sensor kinetics are not deconvolved, and source values are not concentrations.
- No public raw-recording validation has yet promoted this API beyond experimental.
See SDR-0052 for the decision and rejected alternatives.