Skip to main content

flow_gate_xml/
lib.rs

1mod evaluator;
2mod namespace;
3mod parser;
4pub mod schema;
5mod serializer;
6
7use std::collections::HashMap;
8
9use flow_gate_core::{
10    gate::GateKind, BitVec, EventMatrix, EventMatrixView, FlowGateError, GateId, GateRegistry,
11    ParameterName, TransformKind,
12};
13
14pub use parser::FlowGateParser;
15pub use serializer::FlowGateSerializer;
16
17#[derive(Debug, Clone)]
18pub struct RatioTransformSpec {
19    pub id: String,
20    pub numerator: ParameterName,
21    pub denominator: ParameterName,
22    pub a: f64,
23    pub b: f64,
24    pub c: f64,
25}
26
27#[derive(Debug, Clone)]
28pub struct SpectrumMatrixSpec {
29    pub id: String,
30    pub fluorochromes: Vec<ParameterName>,
31    pub detectors: Vec<ParameterName>,
32    pub coefficients: Vec<f64>, // row-major: fluorochrome rows x detector columns
33    pub matrix_inverted_already: bool,
34}
35
36impl SpectrumMatrixSpec {
37    pub fn n_rows(&self) -> usize {
38        self.fluorochromes.len()
39    }
40
41    pub fn n_cols(&self) -> usize {
42        self.detectors.len()
43    }
44}
45
46pub struct FlowGateDocument {
47    pub transforms: HashMap<String, TransformKind>,
48    pub ratio_transforms: HashMap<String, RatioTransformSpec>,
49    pub spectrum_matrices: HashMap<String, SpectrumMatrixSpec>,
50    pub gate_registry: GateRegistry,
51    pub source_xml: Option<String>,
52}
53
54impl FlowGateDocument {
55    pub fn parse_str(xml: &str) -> Result<Self, FlowGateError> {
56        parser::parse_document(xml)
57    }
58
59    pub fn classify(&self, matrix: &EventMatrix) -> Result<HashMap<GateId, BitVec>, FlowGateError> {
60        self.classify_with_fcs_compensation(matrix, None)
61    }
62
63    pub fn classify_view(
64        &self,
65        matrix: &EventMatrixView<'_>,
66    ) -> Result<HashMap<GateId, BitVec>, FlowGateError> {
67        self.classify_view_with_fcs_compensation(matrix, None)
68    }
69
70    pub fn classify_with_fcs_compensation(
71        &self,
72        matrix: &EventMatrix,
73        fcs_compensation: Option<&SpectrumMatrixSpec>,
74    ) -> Result<HashMap<GateId, BitVec>, FlowGateError> {
75        let prepared = self.prepare_owned_matrix_with_fcs_compensation(matrix, fcs_compensation)?;
76        self.gate_registry.classify_all(&prepared)
77    }
78
79    pub fn classify_view_with_fcs_compensation(
80        &self,
81        matrix: &EventMatrixView<'_>,
82        fcs_compensation: Option<&SpectrumMatrixSpec>,
83    ) -> Result<HashMap<GateId, BitVec>, FlowGateError> {
84        let prepared = evaluator::prepare_matrix_from_view(self, matrix, fcs_compensation)?;
85        self.gate_registry.classify_all(&prepared)
86    }
87
88    /// Builds the exact preprocessed matrix (compensation + ratio dimensions) used by classify().
89    pub fn prepare_owned_matrix_with_fcs_compensation(
90        &self,
91        matrix: &EventMatrix,
92        fcs_compensation: Option<&SpectrumMatrixSpec>,
93    ) -> Result<EventMatrix, FlowGateError> {
94        evaluator::prepare_owned_matrix(self, matrix, fcs_compensation)
95    }
96
97    pub fn to_xml(&self) -> Result<String, FlowGateError> {
98        serializer::serialize_document(self)
99    }
100
101    pub fn gates(&self) -> impl Iterator<Item = (&GateId, &GateKind)> {
102        self.gate_registry.iter()
103    }
104}
105
106const SYN_FCS_PREFIX: &str = "__gml_fcs::";
107const SYN_RATIO_PREFIX: &str = "__gml_ratio::";
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub(crate) enum BoundDimension {
111    Fcs {
112        compensation_ref: String,
113        name: String,
114    },
115    Ratio {
116        compensation_ref: String,
117        ratio_id: String,
118    },
119}
120
121pub(crate) fn make_fcs_binding_name(compensation_ref: &str, name: &str) -> ParameterName {
122    ParameterName::from(format!(
123        "{SYN_FCS_PREFIX}{}::{}",
124        escape_binding(compensation_ref),
125        escape_binding(name),
126    ))
127}
128
129pub(crate) fn make_ratio_binding_name(compensation_ref: &str, ratio_id: &str) -> ParameterName {
130    ParameterName::from(format!(
131        "{SYN_RATIO_PREFIX}{}::{}",
132        escape_binding(compensation_ref),
133        escape_binding(ratio_id),
134    ))
135}
136
137pub(crate) fn parse_bound_dimension(name: &ParameterName) -> Option<BoundDimension> {
138    let raw = name.as_str();
139    if let Some(rest) = raw.strip_prefix(SYN_FCS_PREFIX) {
140        let (comp, dim) = rest.split_once("::")?;
141        return Some(BoundDimension::Fcs {
142            compensation_ref: unescape_binding(comp),
143            name: unescape_binding(dim),
144        });
145    }
146    if let Some(rest) = raw.strip_prefix(SYN_RATIO_PREFIX) {
147        let (comp, ratio) = rest.split_once("::")?;
148        return Some(BoundDimension::Ratio {
149            compensation_ref: unescape_binding(comp),
150            ratio_id: unescape_binding(ratio),
151        });
152    }
153    None
154}
155
156fn escape_binding(value: &str) -> String {
157    value.replace('%', "%25").replace(':', "%3A")
158}
159
160fn unescape_binding(value: &str) -> String {
161    value.replace("%3A", ":").replace("%25", "%")
162}