Skip to main content

flow_fcs/
parameter.rs

1use derive_builder::Builder;
2use polars::prelude::*;
3use rustc_hash::FxHashMap;
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6
7use crate::TransformType;
8
9// New Polars-based types for columnar storage
10/// Event data stored as a Polars DataFrame for efficient columnar access
11/// Each column represents one parameter (e.g., FSC-A, SSC-A, FL1-A)
12/// Benefits:
13/// - Zero-copy column access
14/// - Built-in SIMD operations
15/// - Lazy evaluation for complex queries
16/// - Apache Arrow interop
17pub type EventDataFrame = Arc<DataFrame>;
18pub type EventDatum = f32;
19pub type ChannelName = Arc<str>;
20pub type LabelName = Arc<str>;
21pub type ParameterMap = FxHashMap<ChannelName, Parameter>;
22
23/// Instructions for parameter processing transformations
24///
25/// These variants indicate what transformations should be applied to the data,
26/// not the current state of the data (which may already be processed).
27/// This is used to track the processing pipeline for parameters (compensation, unmixing, etc.)
28#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
29#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
30#[cfg_attr(feature = "typescript", ts(export))]
31pub enum ParameterProcessing {
32    /// Raw, unprocessed data from FCS file
33    Raw,
34    /// Compensated for spectral overlap
35    Compensated,
36    /// Spectrally unmixed
37    Unmixed,
38    /// Both compensated and spectrally unmixed
39    UnmixedCompensated,
40}
41
42impl Default for ParameterProcessing {
43    fn default() -> Self {
44        ParameterProcessing::Raw
45    }
46}
47
48/// Category for grouping parameters in user interfaces
49///
50/// This enum helps organize parameters by their processing state and type,
51/// making it easier to present options to users in plotting or analysis interfaces.
52#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
53pub enum ParameterCategory {
54    /// Raw parameters (FSC, SSC, Time, custom params)
55    Raw,
56    /// Fluorescence channels (raw)
57    Fluorescence,
58    /// Compensated fluorescence
59    Compensated,
60    /// Transformed parameters (arcsinh applied)
61    Transformed,
62    /// Both compensated and transformed
63    CompensatedTransformed,
64    /// Spectrally unmixed
65    Unmixed,
66}
67
68/// A parameter option for plotting that includes display information
69///
70/// This struct combines a `Parameter` with UI-specific metadata like display labels
71/// and categories, making it easy to present parameter options to users in plotting interfaces.
72#[derive(Serialize, Debug, Clone)]
73pub struct ParameterOption {
74    /// Unique identifier for this option (e.g., "comp_trans::UV379-A")
75    pub id: String,
76    /// Display label for UI (e.g., "Comp::UV379-A::CD8[T]")
77    pub display_label: String,
78    /// The actual parameter to use
79    pub parameter: Parameter,
80    /// Category for UI grouping
81    pub category: ParameterCategory,
82}
83
84#[derive(Serialize, Debug, Clone, Builder, Hash)]
85#[builder(setter(into))]
86pub struct Parameter {
87    /// The offset of the parameter in the FCS file's event data (1-based index)
88    pub parameter_number: usize,
89    /// The name of the channel ($PnN keyword)
90    pub channel_name: ChannelName,
91    /// The label name of the parameter ($PnS keyword)
92    pub label_name: LabelName,
93    /// The default transform to apply to the parameter
94    pub transform: TransformType,
95
96    /// Instructions for parameter processing (compensation, unmixing, etc.)
97    /// This enum indicates what transformations should be applied.
98    #[builder(default)]
99    #[serde(default)]
100    pub state: ParameterProcessing,
101
102    /// Excitation wavelength in nanometers (from $PnL keyword, if available)
103    #[builder(default)]
104    // #[serde(skip_serializing_if = "Option::is_none")]
105    pub excitation_wavelength: Option<usize>,
106}
107impl Parameter {
108    /// Creates a new `Parameter` with the specified properties
109    ///
110    /// # Arguments
111    /// * `parameter_number` - The 1-based index of the parameter in the FCS file
112    /// * `channel_name` - The channel name from the `$PnN` keyword (e.g., "FSC-A", "FL1-A")
113    /// * `label_name` - The label name from the `$PnS` keyword (e.g., "CD8", "CD4")
114    /// * `transform` - The default transformation type to apply
115    ///
116    /// # Returns
117    /// A new `Parameter` with `Raw` processing state and no excitation wavelength
118    #[must_use]
119    pub fn new(
120        parameter_number: &usize,
121        channel_name: &str,
122        label_name: &str,
123        transform: &TransformType,
124    ) -> Self {
125        Self {
126            parameter_number: *parameter_number,
127            channel_name: channel_name.into(),
128            label_name: label_name.into(),
129            transform: transform.clone(),
130            state: ParameterProcessing::default(),
131            excitation_wavelength: None,
132        }
133    }
134
135    /// Check if this parameter is fluorescence (should be transformed by default)
136    /// Excludes FSC (forward scatter), SSC (side scatter), and Time
137    #[must_use]
138    pub fn is_fluorescence(&self) -> bool {
139        let upper = self.channel_name.to_uppercase();
140        !upper.contains("FSC") && !upper.contains("SSC") && !upper.contains("TIME")
141    }
142
143    /// Get the display label for this parameter
144    /// Format examples:
145    /// - Raw: "UV379-A::CD8" or just "FSC-A"
146    /// - Compensated: "Comp::UV379-A::CD8"
147    /// - Unmixed: "Unmix::UV379-A::CD8"
148    /// - UnmixedCompensated: "Comp+Unmix::UV379-A::CD8"
149    #[must_use]
150    pub fn get_display_label(&self) -> String {
151        let prefix = match self.state {
152            ParameterProcessing::Raw => "",
153            ParameterProcessing::Compensated => "Comp::",
154            ParameterProcessing::Unmixed => "Unmix::",
155            ParameterProcessing::UnmixedCompensated => "Comp+Unmix::",
156        };
157
158        // If label_name is empty or same as channel, just use channel
159        if self.label_name.is_empty() || self.label_name.as_ref() == self.channel_name.as_ref() {
160            format!("{}{}", prefix, self.channel_name)
161        } else {
162            format!("{}{}::{}", prefix, self.channel_name, self.label_name)
163        }
164    }
165
166    /// Get the short label (without state prefix)
167    #[must_use]
168    pub fn get_short_label(&self) -> String {
169        if self.label_name.is_empty() || self.label_name.as_ref() == self.channel_name.as_ref() {
170            self.channel_name.to_string()
171        } else {
172            format!("{}::{}", self.channel_name, self.label_name)
173        }
174    }
175
176    /// Create a new parameter with updated state
177    #[must_use]
178    pub fn with_state(&self, state: ParameterProcessing) -> Self {
179        Self {
180            state,
181            ..self.clone()
182        }
183    }
184
185    /// Create a new parameter with updated transform
186    #[must_use]
187    pub fn with_transform(&self, transform: TransformType) -> Self {
188        Self {
189            transform,
190            ..self.clone()
191        }
192    }
193
194    /// Generate parameter options for plotting interfaces
195    ///
196    /// Creates a list of `ParameterOption` structs representing different processing
197    /// states of this parameter that can be used for plotting.
198    ///
199    /// **For fluorescence parameters:**
200    /// - Always returns transformed versions (arcsinh applied)
201    /// - If `include_compensated` is true, also includes compensated+transformed versions
202    /// - Includes unmixed versions if compensation is available
203    ///
204    /// **For non-fluorescence parameters (FSC, SSC, Time):**
205    /// - Returns raw (untransformed) versions only
206    ///
207    /// # Arguments
208    /// * `include_compensated` - Whether to include compensated and unmixed variants
209    ///
210    /// # Returns
211    /// A vector of `ParameterOption` structs ready for use in plotting UIs
212    pub fn generate_plot_options(&self, include_compensated: bool) -> Vec<ParameterOption> {
213        let mut options = Vec::new();
214
215        if self.is_fluorescence() {
216            // For fluorescence: always return transformed version only
217            let transformed = self.with_transform(TransformType::default());
218            let transformed_label = self.get_short_label();
219            options.push(ParameterOption {
220                id: format!("transformed::{}", self.channel_name),
221                display_label: transformed_label,
222                parameter: transformed,
223                category: ParameterCategory::Fluorescence,
224            });
225
226            // If compensated, add compensated transformed version
227            if include_compensated {
228                let comp_trans = self
229                    .with_state(ParameterProcessing::Compensated)
230                    .with_transform(TransformType::default());
231                let comp_trans_label = comp_trans.get_display_label();
232                options.push(ParameterOption {
233                    id: format!("comp_trans::{}", self.channel_name),
234                    display_label: comp_trans_label,
235                    parameter: comp_trans,
236                    category: ParameterCategory::CompensatedTransformed,
237                });
238
239                // Add unmixed versions (always transformed)
240                let unmix_trans = self
241                    .with_state(ParameterProcessing::Unmixed)
242                    .with_transform(TransformType::default());
243                let unmix_trans_label = unmix_trans.get_display_label();
244                options.push(ParameterOption {
245                    id: format!("unmix_trans::{}", self.channel_name),
246                    display_label: unmix_trans_label,
247                    parameter: unmix_trans,
248                    category: ParameterCategory::Unmixed,
249                });
250
251                // Add combined compensated+unmixed versions (always transformed)
252                let comp_unmix_trans = self
253                    .with_state(ParameterProcessing::UnmixedCompensated)
254                    .with_transform(TransformType::default());
255                let comp_unmix_trans_label = comp_unmix_trans.get_display_label();
256                options.push(ParameterOption {
257                    id: format!("comp_unmix_trans::{}", self.channel_name),
258                    display_label: comp_unmix_trans_label,
259                    parameter: comp_unmix_trans,
260                    category: ParameterCategory::Unmixed,
261                });
262            }
263        } else {
264            // For non-fluorescence (scatter/time): include raw parameter
265            options.push(ParameterOption {
266                id: format!("raw::{}", self.channel_name),
267                display_label: self.get_short_label(),
268                parameter: self.clone(),
269                category: ParameterCategory::Raw,
270            });
271        }
272
273        options
274    }
275}