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('&', "&")
490 .replace('<', "<")
491 .replace('>', ">")
492 .replace('"', """)
493 .replace('\'', "'")
494}