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>, 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 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}