Skip to main content

flow_gate_xml/
serializer.rs

1use std::fmt::Write;
2
3use flow_gate_core::{
4    gate::{EllipsoidDimension, GateKind, PolygonDimension, PolygonGate, RectangleDimension},
5    FlowGateError, Gate, TransformKind,
6};
7
8use crate::{namespace, parse_bound_dimension, BoundDimension, FlowGateDocument};
9
10pub struct FlowGateSerializer;
11
12impl FlowGateSerializer {
13    pub fn to_string(doc: &FlowGateDocument) -> Result<String, FlowGateError> {
14        serialize_document(doc)
15    }
16}
17
18pub fn serialize_document(doc: &FlowGateDocument) -> Result<String, FlowGateError> {
19    let mut out = String::new();
20    writeln!(&mut out, r#"<?xml version="1.0" encoding="UTF-8"?>"#).ok();
21    writeln!(
22        &mut out,
23        r#"<gating:Gating-ML xmlns:gating="{}" xmlns:transforms="{}" xmlns:data-type="{}">"#,
24        namespace::NS_GATING,
25        namespace::NS_TRANSFORMS,
26        namespace::NS_DATATYPE
27    )
28    .ok();
29
30    let mut transform_ids: Vec<&str> = doc
31        .transforms
32        .keys()
33        .map(String::as_str)
34        .chain(doc.ratio_transforms.keys().map(String::as_str))
35        .collect();
36    transform_ids.sort_unstable();
37    transform_ids.dedup();
38
39    for id in transform_ids {
40        if let Some(transform) = doc.transforms.get(id) {
41            writeln!(
42                &mut out,
43                r#"  <transforms:transformation transforms:id="{}">"#,
44                xml_escape(id)
45            )
46            .ok();
47            write_transform_element(&mut out, transform)?;
48            writeln!(&mut out, "  </transforms:transformation>").ok();
49            continue;
50        }
51        if let Some(ratio) = doc.ratio_transforms.get(id) {
52            writeln!(
53                &mut out,
54                r#"  <transforms:transformation transforms:id="{}">"#,
55                xml_escape(id)
56            )
57            .ok();
58            writeln!(
59                &mut out,
60                r#"    <transforms:fratio transforms:A="{:.15e}" transforms:B="{:.15e}" transforms:C="{:.15e}">"#,
61                ratio.a, ratio.b, ratio.c
62            )
63            .ok();
64            writeln!(
65                &mut out,
66                r#"      <data-type:fcs-dimension data-type:name="{}"/>"#,
67                xml_escape(ratio.numerator.as_str())
68            )
69            .ok();
70            writeln!(
71                &mut out,
72                r#"      <data-type:fcs-dimension data-type:name="{}"/>"#,
73                xml_escape(ratio.denominator.as_str())
74            )
75            .ok();
76            writeln!(&mut out, "    </transforms:fratio>").ok();
77            writeln!(&mut out, "  </transforms:transformation>").ok();
78        }
79    }
80
81    let mut spectrum_entries: Vec<_> = doc.spectrum_matrices.iter().collect();
82    spectrum_entries.sort_by(|a, b| a.0.cmp(b.0));
83    for (_id, spec) in spectrum_entries {
84        if spec.n_rows() == 0 || spec.n_cols() == 0 {
85            continue;
86        }
87        if spec.matrix_inverted_already {
88            writeln!(
89                &mut out,
90                r#"  <transforms:spectrumMatrix transforms:id="{}" transforms:matrix-inverted-already="true">"#,
91                xml_escape(&spec.id)
92            )
93            .ok();
94        } else {
95            writeln!(
96                &mut out,
97                r#"  <transforms:spectrumMatrix transforms:id="{}">"#,
98                xml_escape(&spec.id)
99            )
100            .ok();
101        }
102
103        writeln!(&mut out, "    <transforms:fluorochromes>").ok();
104        for dim in &spec.fluorochromes {
105            writeln!(
106                &mut out,
107                r#"      <data-type:fcs-dimension data-type:name="{}"/>"#,
108                xml_escape(dim.as_str())
109            )
110            .ok();
111        }
112        writeln!(&mut out, "    </transforms:fluorochromes>").ok();
113
114        writeln!(&mut out, "    <transforms:detectors>").ok();
115        for dim in &spec.detectors {
116            writeln!(
117                &mut out,
118                r#"      <data-type:fcs-dimension data-type:name="{}"/>"#,
119                xml_escape(dim.as_str())
120            )
121            .ok();
122        }
123        writeln!(&mut out, "    </transforms:detectors>").ok();
124
125        let n_rows = spec.n_rows();
126        let n_cols = spec.n_cols();
127        for row in 0..n_rows {
128            writeln!(&mut out, "    <transforms:spectrum>").ok();
129            for col in 0..n_cols {
130                let idx = row * n_cols + col;
131                if let Some(value) = spec.coefficients.get(idx) {
132                    writeln!(
133                        &mut out,
134                        r#"      <transforms:coefficient transforms:value="{:.15e}"/>"#,
135                        value
136                    )
137                    .ok();
138                }
139            }
140            writeln!(&mut out, "    </transforms:spectrum>").ok();
141        }
142        writeln!(&mut out, "  </transforms:spectrumMatrix>").ok();
143    }
144
145    for gate_id in doc.gate_registry.topological_order() {
146        let gate = doc
147            .gate_registry
148            .get(gate_id)
149            .ok_or_else(|| FlowGateError::UnknownGateReference(gate_id.clone(), gate_id.clone()))?;
150        write_gate_element(&mut out, doc, gate)?;
151    }
152
153    writeln!(&mut out, "</gating:Gating-ML>").ok();
154    Ok(out)
155}
156
157fn write_transform_element(
158    out: &mut String,
159    transform: &TransformKind,
160) -> Result<(), FlowGateError> {
161    match transform {
162        TransformKind::Logicle(t) => {
163            writeln!(
164                out,
165                r#"    <transforms:logicle transforms:T="{:.15e}" transforms:W="{:.15e}" transforms:M="{:.15e}" transforms:A="{:.15e}"/>"#,
166                t.params.t, t.params.w, t.params.m, t.params.a
167            )
168            .ok();
169        }
170        TransformKind::FASinh(t) => {
171            writeln!(
172                out,
173                r#"    <transforms:fasinh transforms:T="{:.15e}" transforms:M="{:.15e}" transforms:A="{:.15e}"/>"#,
174                t.t, t.m, t.a
175            )
176            .ok();
177        }
178        TransformKind::Logarithmic(t) => {
179            writeln!(
180                out,
181                r#"    <transforms:flog transforms:T="{:.15e}" transforms:M="{:.15e}"/>"#,
182                t.t, t.m
183            )
184            .ok();
185        }
186        TransformKind::Linear(t) => {
187            writeln!(
188                out,
189                r#"    <transforms:flin transforms:T="{:.15e}" transforms:A="{:.15e}"/>"#,
190                t.t, t.a
191            )
192            .ok();
193        }
194        TransformKind::Hyperlog(t) => {
195            writeln!(
196                out,
197                r#"    <transforms:hyperlog transforms:T="{:.15e}" transforms:W="{:.15e}" transforms:M="{:.15e}" transforms:A="{:.15e}"/>"#,
198                t.t, t.w, t.m, t.a
199            )
200            .ok();
201        }
202    }
203    Ok(())
204}
205
206fn write_gate_element(
207    out: &mut String,
208    doc: &FlowGateDocument,
209    gate: &GateKind,
210) -> Result<(), FlowGateError> {
211    match gate {
212        GateKind::Rectangle(g) => {
213            write_gate_open(out, "RectangleGate", gate)?;
214            for dim in g.rectangle_dimensions() {
215                write_rectangle_dimension(out, doc, dim)?;
216            }
217            writeln!(out, "  </gating:RectangleGate>").ok();
218        }
219        GateKind::Polygon(g) => {
220            write_gate_open(out, "PolygonGate", gate)?;
221            write_polygon_gate(out, doc, g)?;
222            writeln!(out, "  </gating:PolygonGate>").ok();
223        }
224        GateKind::Ellipsoid(g) => {
225            write_gate_open(out, "EllipsoidGate", gate)?;
226            for dim in g.dimensions_def() {
227                write_unbounded_dimension(out, doc, dim, false)?;
228            }
229            writeln!(out, "    <gating:mean>").ok();
230            for value in g.mean() {
231                writeln!(
232                    out,
233                    r#"      <gating:coordinate data-type:value="{:.15e}"/>"#,
234                    value
235                )
236                .ok();
237            }
238            writeln!(out, "    </gating:mean>").ok();
239
240            writeln!(out, "    <gating:covarianceMatrix>").ok();
241            let n = g.covariance().n();
242            if g.covariance().uses_general_inverse() {
243                let full = g.covariance().full_matrix();
244                for row in 0..n {
245                    writeln!(out, "      <gating:row>").ok();
246                    for col in 0..n {
247                        writeln!(
248                            out,
249                            r#"        <gating:entry data-type:value="{:.15e}"/>"#,
250                            full[row * n + col]
251                        )
252                        .ok();
253                    }
254                    writeln!(out, "      </gating:row>").ok();
255                }
256            } else {
257                let upper = g.covariance().to_upper_triangular();
258                let mut idx = 0usize;
259                for row in 0..n {
260                    writeln!(out, "      <gating:row>").ok();
261                    for _col in row..n {
262                        writeln!(
263                            out,
264                            r#"        <gating:entry data-type:value="{:.15e}"/>"#,
265                            upper[idx]
266                        )
267                        .ok();
268                        idx += 1;
269                    }
270                    writeln!(out, "      </gating:row>").ok();
271                }
272            }
273            writeln!(out, "    </gating:covarianceMatrix>").ok();
274            writeln!(
275                out,
276                r#"    <gating:distanceSquare data-type:value="{:.15e}"/>"#,
277                g.distance_sq()
278            )
279            .ok();
280            writeln!(out, "  </gating:EllipsoidGate>").ok();
281        }
282        GateKind::Boolean(g) => {
283            write_gate_open(out, "BooleanGate", gate)?;
284            let tag = match g.op() {
285                flow_gate_core::gate::BooleanOp::And => "and",
286                flow_gate_core::gate::BooleanOp::Or => "or",
287                flow_gate_core::gate::BooleanOp::Not => "not",
288            };
289            writeln!(out, "    <gating:{tag}>").ok();
290            for op in g.operands() {
291                if op.complement {
292                    writeln!(
293                        out,
294                        r#"      <gating:gateReference gating:ref="{}" gating:use-as-complement="true"/>"#,
295                        xml_escape(op.gate_id.as_str())
296                    )
297                    .ok();
298                } else {
299                    writeln!(
300                        out,
301                        r#"      <gating:gateReference gating:ref="{}"/>"#,
302                        xml_escape(op.gate_id.as_str())
303                    )
304                    .ok();
305                }
306            }
307            writeln!(out, "    </gating:{tag}>").ok();
308            writeln!(out, "  </gating:BooleanGate>").ok();
309        }
310    }
311    Ok(())
312}
313
314fn write_gate_open(out: &mut String, tag: &str, gate: &GateKind) -> Result<(), FlowGateError> {
315    let gate_id = xml_escape(gate.gate_id().as_str());
316    if let Some(parent) = gate.parent_id() {
317        writeln!(
318            out,
319            r#"  <gating:{tag} gating:id="{}" gating:parent_id="{}">"#,
320            gate_id,
321            xml_escape(parent.as_str())
322        )
323        .ok();
324    } else {
325        writeln!(out, r#"  <gating:{tag} gating:id="{}">"#, gate_id).ok();
326    }
327    Ok(())
328}
329
330fn write_rectangle_dimension(
331    out: &mut String,
332    doc: &FlowGateDocument,
333    dim: &RectangleDimension,
334) -> Result<(), FlowGateError> {
335    let mut attrs = String::new();
336    if let Some(min) = dim.min {
337        write!(&mut attrs, r#" gating:min="{:.15e}""#, min).ok();
338    }
339    if let Some(max) = dim.max {
340        write!(&mut attrs, r#" gating:max="{:.15e}""#, max).ok();
341    }
342    if let Some(tid) = dim.transform.and_then(|t| transform_id_for(doc, &t)) {
343        write!(
344            &mut attrs,
345            r#" gating:transformation-ref="{}""#,
346            xml_escape(tid)
347        )
348        .ok();
349    }
350
351    let (comp_ref, dim_xml) = dimension_reference_xml(&dim.parameter);
352    write!(
353        out,
354        r#"    <gating:dimension gating:compensation-ref="{}"{}>"#,
355        xml_escape(&comp_ref),
356        attrs
357    )
358    .ok();
359    writeln!(out).ok();
360    writeln!(out, "      {dim_xml}").ok();
361    writeln!(out, "    </gating:dimension>").ok();
362    Ok(())
363}
364
365fn write_unbounded_dimension(
366    out: &mut String,
367    doc: &FlowGateDocument,
368    dim: &impl DimensionLike,
369    _indent_polygon: bool,
370) -> Result<(), FlowGateError> {
371    let mut attrs = String::new();
372    if let Some(tid) = dim.transform().and_then(|t| transform_id_for(doc, &t)) {
373        write!(
374            &mut attrs,
375            r#" gating:transformation-ref="{}""#,
376            xml_escape(tid)
377        )
378        .ok();
379    }
380
381    let (comp_ref, dim_xml) = dimension_reference_xml(dim.parameter());
382    let indent = "    ";
383    writeln!(
384        out,
385        r#"{indent}<gating:dimension gating:compensation-ref="{}"{}>"#,
386        xml_escape(&comp_ref),
387        attrs
388    )
389    .ok();
390    writeln!(out, "{indent}  {dim_xml}").ok();
391    writeln!(out, "{indent}</gating:dimension>").ok();
392    Ok(())
393}
394
395fn write_polygon_gate(
396    out: &mut String,
397    doc: &FlowGateDocument,
398    g: &PolygonGate,
399) -> Result<(), FlowGateError> {
400    let dims: [&PolygonDimension; 2] = [g.x_dim(), g.y_dim()];
401    for dim in dims {
402        write_unbounded_dimension(out, doc, dim, true)?;
403    }
404
405    for (x, y) in g.vertices() {
406        writeln!(out, "    <gating:vertex>").ok();
407        writeln!(
408            out,
409            r#"      <gating:coordinate data-type:value="{:.15e}"/>"#,
410            x
411        )
412        .ok();
413        writeln!(
414            out,
415            r#"      <gating:coordinate data-type:value="{:.15e}"/>"#,
416            y
417        )
418        .ok();
419        writeln!(out, "    </gating:vertex>").ok();
420    }
421    Ok(())
422}
423
424trait DimensionLike {
425    fn parameter(&self) -> &flow_gate_core::ParameterName;
426    fn transform(&self) -> Option<TransformKind>;
427}
428
429impl DimensionLike for PolygonDimension {
430    fn parameter(&self) -> &flow_gate_core::ParameterName {
431        &self.parameter
432    }
433
434    fn transform(&self) -> Option<TransformKind> {
435        self.transform
436    }
437}
438
439impl DimensionLike for EllipsoidDimension {
440    fn parameter(&self) -> &flow_gate_core::ParameterName {
441        &self.parameter
442    }
443
444    fn transform(&self) -> Option<TransformKind> {
445        self.transform
446    }
447}
448
449fn dimension_reference_xml(parameter: &flow_gate_core::ParameterName) -> (String, String) {
450    match parse_bound_dimension(parameter) {
451        Some(BoundDimension::Fcs {
452            compensation_ref,
453            name,
454        }) => (
455            compensation_ref,
456            format!(
457                r#"<data-type:fcs-dimension data-type:name="{}"/>"#,
458                xml_escape(&name)
459            ),
460        ),
461        Some(BoundDimension::Ratio {
462            compensation_ref,
463            ratio_id,
464        }) => (
465            compensation_ref,
466            format!(
467                r#"<data-type:new-dimension data-type:transformation-ref="{}"/>"#,
468                xml_escape(&ratio_id)
469            ),
470        ),
471        None => (
472            "uncompensated".to_string(),
473            format!(
474                r#"<data-type:parameter data-type:name="{}"/>"#,
475                xml_escape(parameter.as_str())
476            ),
477        ),
478    }
479}
480
481fn transform_id_for<'a>(doc: &'a FlowGateDocument, target: &TransformKind) -> Option<&'a str> {
482    doc.transforms
483        .iter()
484        .find_map(|(id, t)| if t == target { Some(id.as_str()) } else { None })
485}
486
487fn xml_escape(value: &str) -> String {
488    value
489        .replace('&', "&amp;")
490        .replace('<', "&lt;")
491        .replace('>', "&gt;")
492        .replace('"', "&quot;")
493        .replace('\'', "&apos;")
494}