1use std::collections::HashMap;
2
3use flow_gate_core::{
4 gate::{
5 BooleanGate, BooleanOp, BooleanOperand, EllipsoidDimension, EllipsoidGate, GateKind,
6 PolygonDimension, PolygonGate, RectangleDimension, RectangleGate,
7 },
8 transform::{
9 FASinhTransform, HyperlogTransform, LinearTransform, LogarithmicTransform, LogicleParams,
10 LogicleTransform,
11 },
12 FlowGateError, GateId, GateRegistry, ParameterName, TransformKind,
13};
14use indexmap::IndexMap;
15use quick_xml::{
16 events::{BytesStart, Event},
17 name::{Namespace, ResolveResult},
18 NsReader,
19};
20
21use crate::{
22 make_fcs_binding_name, make_ratio_binding_name,
23 namespace::{parse_bool_attr, NS_DATATYPE, NS_GATING, NS_TRANSFORMS},
24 FlowGateDocument, RatioTransformSpec, SpectrumMatrixSpec,
25};
26
27#[derive(Default)]
28pub struct FlowGateParser {
29 transforms: HashMap<String, TransformKind>,
30 ratio_transforms: HashMap<String, RatioTransformSpec>,
31 spectrum_matrices: HashMap<String, SpectrumMatrixSpec>,
32 gates: IndexMap<GateId, GateKind>,
33}
34
35impl FlowGateParser {
36 pub fn parse_str(xml: &str) -> Result<FlowGateDocument, FlowGateError> {
37 parse_document(xml)
38 }
39}
40
41#[derive(Debug, Clone)]
42enum DimensionSelector {
43 Fcs(String),
44 New(String),
45}
46
47fn parse_boolean_op_block(
48 reader: &mut NsReader<&[u8]>,
49 source: &str,
50 operator_local: &[u8],
51 current_gate_id: &GateId,
52 existing_gates: &IndexMap<GateId, GateKind>,
53) -> Result<(BooleanOp, Vec<BooleanOperand>), FlowGateError> {
54 let op = match operator_local {
55 b"and" => BooleanOp::And,
56 b"or" => BooleanOp::Or,
57 b"not" => BooleanOp::Not,
58 _ => {
59 return Err(FlowGateError::InvalidGate(format!(
60 "Unsupported Boolean operator '{}'",
61 String::from_utf8_lossy(operator_local)
62 )))
63 }
64 };
65
66 let mut operands = Vec::new();
67 let mut buf = Vec::new();
68 loop {
69 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
70 Ok(v) => v,
71 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
72 };
73 match event {
74 Event::Start(e) => {
75 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"gateReference") {
76 let attrs = attrs_map(reader, source, &e)?;
77 let gate_id = GateId::from(
78 required_attr(&attrs, "ref", &[NS_GATING, ""], "gateReference")?
79 .to_string(),
80 );
81 if !existing_gates.contains_key(&gate_id) {
82 return Err(FlowGateError::UnknownGateReference(
83 current_gate_id.clone(),
84 gate_id,
85 ));
86 }
87 let complement = parse_bool_attr(
88 optional_attr(&attrs, "use-as-complement", &[NS_GATING, ""])
89 .or_else(|| optional_attr(&attrs, "complement", &[NS_GATING, ""])),
90 false,
91 );
92 operands.push(BooleanOperand {
93 gate_id,
94 complement,
95 });
96 skip_element(reader, source, &e)?;
97 } else {
98 skip_element(reader, source, &e)?;
99 }
100 }
101 Event::Empty(e) => {
102 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"gateReference") {
103 let attrs = attrs_map(reader, source, &e)?;
104 let gate_id = GateId::from(
105 required_attr(&attrs, "ref", &[NS_GATING, ""], "gateReference")?
106 .to_string(),
107 );
108 if !existing_gates.contains_key(&gate_id) {
109 return Err(FlowGateError::UnknownGateReference(
110 current_gate_id.clone(),
111 gate_id,
112 ));
113 }
114 let complement = parse_bool_attr(
115 optional_attr(&attrs, "use-as-complement", &[NS_GATING, ""])
116 .or_else(|| optional_attr(&attrs, "complement", &[NS_GATING, ""])),
117 false,
118 );
119 operands.push(BooleanOperand {
120 gate_id,
121 complement,
122 });
123 }
124 }
125 Event::End(e)
126 if ns_is(&ns, NS_GATING) && e.name().local_name().as_ref() == operator_local =>
127 {
128 break;
129 }
130 Event::Eof => {
131 return Err(xml_err(
132 reader,
133 source,
134 "Unexpected EOF while reading Boolean operator block",
135 ));
136 }
137 _ => {}
138 }
139 buf.clear();
140 }
141
142 Ok((op, operands))
143}
144
145fn parse_vertex(reader: &mut NsReader<&[u8]>, source: &str) -> Result<(f64, f64), FlowGateError> {
146 let coords = parse_coordinates_block(reader, source, NS_GATING, b"vertex")?;
147 if coords.len() != 2 {
148 return Err(FlowGateError::InvalidGate(format!(
149 "Polygon vertex must contain exactly 2 coordinates, found {}",
150 coords.len()
151 )));
152 }
153 Ok((coords[0], coords[1]))
154}
155
156fn parse_coordinates_block(
157 reader: &mut NsReader<&[u8]>,
158 source: &str,
159 end_ns: &str,
160 end_local: &[u8],
161) -> Result<Vec<f64>, FlowGateError> {
162 let mut values = Vec::new();
163 let mut buf = Vec::new();
164
165 loop {
166 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
167 Ok(v) => v,
168 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
169 };
170 match event {
171 Event::Start(e) => {
172 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate") {
173 let attrs = attrs_map(reader, source, &e)?;
174 values.push(parse_required_f64_attr(
175 &attrs,
176 "value",
177 &[NS_DATATYPE, ""],
178 "coordinate",
179 )?);
180 skip_element(reader, source, &e)?;
181 } else {
182 skip_element(reader, source, &e)?;
183 }
184 }
185 Event::Empty(e) => {
186 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate") {
187 let attrs = attrs_map(reader, source, &e)?;
188 values.push(parse_required_f64_attr(
189 &attrs,
190 "value",
191 &[NS_DATATYPE, ""],
192 "coordinate",
193 )?);
194 }
195 }
196 Event::Text(t) => {
197 let text = t
198 .unescape()
199 .map_err(|e| xml_err(reader, source, format!("XML text decode error: {e}")))?;
200 for token in text.split_whitespace() {
201 if let Ok(v) = token.parse::<f64>() {
202 values.push(v);
203 }
204 }
205 }
206 Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
207 break;
208 }
209 Event::Eof => {
210 return Err(xml_err(
211 reader,
212 source,
213 "Unexpected EOF while reading coordinate block",
214 ));
215 }
216 _ => {}
217 }
218 buf.clear();
219 }
220 Ok(values)
221}
222
223fn parse_covariance_block(
224 reader: &mut NsReader<&[u8]>,
225 source: &str,
226) -> Result<Vec<f64>, FlowGateError> {
227 let mut values = Vec::new();
228 let mut buf = Vec::new();
229 loop {
230 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
231 Ok(v) => v,
232 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
233 };
234 match event {
235 Event::Start(e) => {
236 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"row") {
237 values.extend(parse_covariance_row(reader, source)?);
238 } else {
239 skip_element(reader, source, &e)?;
240 }
241 }
242 Event::End(e)
243 if element_is(
244 &ns,
245 e.name().local_name().as_ref(),
246 NS_GATING,
247 b"covarianceMatrix",
248 ) =>
249 {
250 break;
251 }
252 Event::Eof => {
253 return Err(xml_err(
254 reader,
255 source,
256 "Unexpected EOF while reading covarianceMatrix",
257 ));
258 }
259 _ => {}
260 }
261 buf.clear();
262 }
263 Ok(values)
264}
265
266fn parse_covariance_row(
267 reader: &mut NsReader<&[u8]>,
268 source: &str,
269) -> Result<Vec<f64>, FlowGateError> {
270 let mut values = Vec::new();
271 let mut buf = Vec::new();
272 loop {
273 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
274 Ok(v) => v,
275 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
276 };
277 match event {
278 Event::Start(e) => {
279 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"entry")
280 || element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate")
281 {
282 let attrs = attrs_map(reader, source, &e)?;
283 values.push(parse_required_f64_attr(
284 &attrs,
285 "value",
286 &[NS_DATATYPE, "", NS_GATING],
287 "entry",
288 )?);
289 skip_element(reader, source, &e)?;
290 } else {
291 skip_element(reader, source, &e)?;
292 }
293 }
294 Event::Empty(e) => {
295 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"entry")
296 || element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate")
297 {
298 let attrs = attrs_map(reader, source, &e)?;
299 values.push(parse_required_f64_attr(
300 &attrs,
301 "value",
302 &[NS_DATATYPE, "", NS_GATING],
303 "entry",
304 )?);
305 }
306 }
307 Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"row") => {
308 break;
309 }
310 Event::Eof => {
311 return Err(xml_err(
312 reader,
313 source,
314 "Unexpected EOF while reading covariance row",
315 ));
316 }
317 _ => {}
318 }
319 buf.clear();
320 }
321 Ok(values)
322}
323
324fn parse_transform_kind(
325 reader: &NsReader<&[u8]>,
326 source: &str,
327 local: &[u8],
328 start: &BytesStart<'_>,
329) -> Result<TransformKind, FlowGateError> {
330 let attrs = attrs_map(reader, source, start)?;
331 match local {
332 b"logicle" => {
333 let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "logicle")?;
334 let w = parse_required_f64_attr(&attrs, "W", &[NS_TRANSFORMS, ""], "logicle")?;
335 let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "logicle")?;
336 let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "logicle")?;
337 let params = LogicleParams { t, w, m, a }
338 .validate()
339 .map_err(FlowGateError::InvalidTransformParam)?;
340 Ok(TransformKind::Logicle(LogicleTransform { params }))
341 }
342 b"fasinh" => {
343 let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "fasinh")?;
344 let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "fasinh")?;
345 let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "fasinh")?;
346 Ok(TransformKind::FASinh(FASinhTransform::new(t, m, a)?))
347 }
348 b"log" | b"flog" => {
349 let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "log")?;
350 let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "log")?;
351 Ok(TransformKind::Logarithmic(LogarithmicTransform::new(t, m)?))
352 }
353 b"lin" | b"flin" => {
354 let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "lin")?;
355 let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "lin")?;
356 Ok(TransformKind::Linear(LinearTransform::new(t, a)?))
357 }
358 b"hyperlog" => {
359 let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "hyperlog")?;
360 let w = parse_required_f64_attr(&attrs, "W", &[NS_TRANSFORMS, ""], "hyperlog")?;
361 let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "hyperlog")?;
362 let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "hyperlog")?;
363 Ok(TransformKind::Hyperlog(HyperlogTransform::new(t, w, m, a)?))
364 }
365 _ => Err(FlowGateError::InvalidGate(format!(
366 "Unsupported transform element '{}'",
367 String::from_utf8_lossy(local)
368 ))),
369 }
370}
371
372fn parse_transform_ref(
373 attrs: &[AttrRecord],
374 transform_map: &HashMap<String, TransformKind>,
375) -> Result<Option<TransformKind>, FlowGateError> {
376 let candidates = [
377 "transformation-ref",
378 "transformation_ref",
379 "transformationRef",
380 "transformation",
381 "transformRef",
382 "transform_ref",
383 ];
384 for name in candidates {
385 if let Some(value) = optional_attr(attrs, name, &[NS_GATING, ""]) {
386 let transform = transform_map.get(value).copied().ok_or_else(|| {
387 FlowGateError::InvalidGate(format!("Unknown transform reference '{value}'"))
388 })?;
389 return Ok(Some(transform));
390 }
391 }
392 Ok(None)
393}
394
395fn attrs_map(
396 reader: &NsReader<&[u8]>,
397 source: &str,
398 start: &BytesStart<'_>,
399) -> Result<Vec<AttrRecord>, FlowGateError> {
400 let mut out = Vec::new();
401 for attr in start.attributes().with_checks(false) {
402 let attr =
403 attr.map_err(|e| xml_err(reader, source, format!("Invalid XML attribute: {e}")))?;
404 let (resolved_ns, local_name) = reader.resolve_attribute(attr.key);
405 let ns_uri = match resolved_ns {
406 ResolveResult::Bound(Namespace(ns)) => Some(String::from_utf8_lossy(ns).to_string()),
407 ResolveResult::Unbound => None,
408 ResolveResult::Unknown(prefix) => {
409 return Err(FlowGateError::XmlParse(format!(
410 "Unknown XML namespace prefix '{}' in attribute name",
411 String::from_utf8_lossy(&prefix)
412 )))
413 }
414 };
415 let value = attr
416 .decode_and_unescape_value(reader.decoder())
417 .map_err(|e| xml_err(reader, source, format!("XML attribute decode error: {e}")))?
418 .into_owned();
419 out.push(AttrRecord {
420 ns_uri,
421 local: String::from_utf8_lossy(local_name.as_ref()).to_string(),
422 value,
423 });
424 }
425 Ok(out)
426}
427
428fn required_attr<'a>(
429 attrs: &'a [AttrRecord],
430 local: &str,
431 allowed_ns: &[&str],
432 element: &str,
433) -> Result<&'a str, FlowGateError> {
434 optional_attr(attrs, local, allowed_ns)
435 .ok_or_else(|| FlowGateError::MissingAttribute(local.to_string(), element.to_string()))
436}
437
438fn optional_attr<'a>(attrs: &'a [AttrRecord], local: &str, allowed_ns: &[&str]) -> Option<&'a str> {
439 for ns in allowed_ns {
440 let ns_opt = if ns.is_empty() { None } else { Some(*ns) };
441 if let Some(hit) = attrs
442 .iter()
443 .find(|a| a.local == local && a.ns_uri.as_deref() == ns_opt)
444 {
445 return Some(hit.value.as_str());
446 }
447 }
448 None
449}
450
451fn parse_required_f64_attr(
452 attrs: &[AttrRecord],
453 local: &str,
454 allowed_ns: &[&str],
455 element: &str,
456) -> Result<f64, FlowGateError> {
457 let raw = required_attr(attrs, local, allowed_ns, element)?;
458 raw.parse::<f64>()
459 .map_err(|_| FlowGateError::InvalidFloat(raw.to_string(), local.to_string()))
460}
461
462fn parse_optional_f64_attr(
463 attrs: &[AttrRecord],
464 local: &str,
465 allowed_ns: &[&str],
466 element: &str,
467) -> Result<Option<f64>, FlowGateError> {
468 match optional_attr(attrs, local, allowed_ns) {
469 Some(raw) if raw.trim().is_empty() => Ok(None),
470 Some(raw) => raw.parse::<f64>().map(Some).map_err(|_| {
471 FlowGateError::InvalidFloat(raw.to_string(), format!("{element}:{local}"))
472 }),
473 None => Ok(None),
474 }
475}
476
477fn skip_element(
478 reader: &mut NsReader<&[u8]>,
479 source: &str,
480 start: &BytesStart<'_>,
481) -> Result<(), FlowGateError> {
482 let mut buf = Vec::new();
483 reader
484 .read_to_end_into(start.name(), &mut buf)
485 .map_err(|e| xml_err(reader, source, format!("XML skip error: {e}")))?;
486 Ok(())
487}
488
489fn element_is(
490 resolved_ns: &ResolveResult<'_>,
491 local: &[u8],
492 expected_ns: &str,
493 expected_local: &[u8],
494) -> bool {
495 ns_is(resolved_ns, expected_ns) && local == expected_local
496}
497
498fn ns_is(resolved_ns: &ResolveResult<'_>, expected_ns: &str) -> bool {
499 matches!(resolved_ns, ResolveResult::Bound(Namespace(ns)) if *ns == expected_ns.as_bytes())
500}
501
502fn xml_err(reader: &NsReader<&[u8]>, source: &str, message: impl AsRef<str>) -> FlowGateError {
503 let pos = reader.error_position() as usize;
504 let (line, col) = byte_pos_to_line_col(source, pos);
505 FlowGateError::XmlParse(format!(
506 "{} (line {}, column {})",
507 message.as_ref(),
508 line,
509 col
510 ))
511}
512
513fn byte_pos_to_line_col(source: &str, pos: usize) -> (usize, usize) {
514 let mut line = 1usize;
515 let mut col = 1usize;
516 for &b in source.as_bytes().iter().take(pos.min(source.len())) {
517 if b == b'\n' {
518 line += 1;
519 col = 1;
520 } else {
521 col += 1;
522 }
523 }
524 (line, col)
525}
526
527#[derive(Debug, Clone)]
528struct AttrRecord {
529 ns_uri: Option<String>,
530 local: String,
531 value: String,
532}
533
534fn is_gate_element(local: &[u8]) -> bool {
535 matches!(
536 local,
537 b"RectangleGate" | b"PolygonGate" | b"EllipsoidGate" | b"BooleanGate" | b"QuadrantGate"
538 )
539}
540
541fn parse_dimension_selector(
542 reader: &NsReader<&[u8]>,
543 source: &str,
544 start: &BytesStart<'_>,
545) -> Result<DimensionSelector, FlowGateError> {
546 let local = start.local_name();
547 let local = local.as_ref();
548 if !matches!(local, b"parameter" | b"fcs-dimension" | b"new-dimension") {
549 return Err(xml_err(
550 reader,
551 source,
552 format!(
553 "Unsupported dimension element '{}'",
554 String::from_utf8_lossy(local)
555 ),
556 ));
557 }
558
559 let attrs = attrs_map(reader, source, start)?;
560 if local == b"new-dimension" {
561 let ratio_id = required_attr(
562 &attrs,
563 "transformation-ref",
564 &[NS_DATATYPE, ""],
565 "new-dimension",
566 )?;
567 Ok(DimensionSelector::New(ratio_id.to_string()))
568 } else {
569 let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
570 Ok(DimensionSelector::Fcs(name.to_string()))
571 }
572}
573
574fn dimension_selector_to_parameter(
575 compensation_ref: &str,
576 selector: DimensionSelector,
577) -> ParameterName {
578 match selector {
579 DimensionSelector::Fcs(name) => make_fcs_binding_name(compensation_ref, &name),
580 DimensionSelector::New(ratio_id) => make_ratio_binding_name(compensation_ref, &ratio_id),
581 }
582}
583
584fn parse_dimension_ref(
585 reader: &mut NsReader<&[u8]>,
586 source: &str,
587 attrs: &[AttrRecord],
588 context: &str,
589 end_ns: &str,
590 end_local: &[u8],
591) -> Result<ParameterName, FlowGateError> {
592 let compensation_ref = optional_attr(attrs, "compensation-ref", &[NS_GATING, ""])
593 .unwrap_or("uncompensated")
594 .to_string();
595
596 let mut selector: Option<DimensionSelector> = None;
597 let mut buf = Vec::new();
598
599 loop {
600 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
601 Ok(v) => v,
602 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
603 };
604 match event {
605 Event::Start(e) => {
606 if ns_is(&ns, NS_DATATYPE)
607 && matches!(
608 e.local_name().as_ref(),
609 b"parameter" | b"fcs-dimension" | b"new-dimension"
610 )
611 {
612 if selector.is_some() {
613 return Err(FlowGateError::InvalidGate(format!(
614 "{context} contains multiple dimension selectors"
615 )));
616 }
617 selector = Some(parse_dimension_selector(reader, source, &e)?);
618 skip_element(reader, source, &e)?;
619 } else {
620 skip_element(reader, source, &e)?;
621 }
622 }
623 Event::Empty(e) => {
624 if ns_is(&ns, NS_DATATYPE)
625 && matches!(
626 e.local_name().as_ref(),
627 b"parameter" | b"fcs-dimension" | b"new-dimension"
628 )
629 {
630 if selector.is_some() {
631 return Err(FlowGateError::InvalidGate(format!(
632 "{context} contains multiple dimension selectors"
633 )));
634 }
635 selector = Some(parse_dimension_selector(reader, source, &e)?);
636 }
637 }
638 Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
639 break;
640 }
641 Event::Eof => {
642 return Err(xml_err(
643 reader,
644 source,
645 format!("Unexpected EOF while reading {context}"),
646 ));
647 }
648 _ => {}
649 }
650 buf.clear();
651 }
652
653 let selector = selector.ok_or_else(|| {
654 FlowGateError::InvalidGate(format!("{context} is missing fcs-dimension/new-dimension"))
655 })?;
656 Ok(dimension_selector_to_parameter(&compensation_ref, selector))
657}
658
659fn parse_value_content(reader: &mut NsReader<&[u8]>, source: &str) -> Result<f64, FlowGateError> {
660 let mut value: Option<f64> = None;
661 let mut buf = Vec::new();
662
663 loop {
664 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
665 Ok(v) => v,
666 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
667 };
668 match event {
669 Event::Start(e) => {
670 let attrs = attrs_map(reader, source, &e)?;
671 if let Some(v) = parse_optional_f64_attr(
672 &attrs,
673 "value",
674 &[NS_DATATYPE, "", NS_GATING, NS_TRANSFORMS],
675 "value",
676 )? {
677 value = Some(v);
678 }
679 skip_element(reader, source, &e)?;
680 }
681 Event::Empty(e) => {
682 let attrs = attrs_map(reader, source, &e)?;
683 if let Some(v) = parse_optional_f64_attr(
684 &attrs,
685 "value",
686 &[NS_DATATYPE, "", NS_GATING, NS_TRANSFORMS],
687 "value",
688 )? {
689 value = Some(v);
690 }
691 }
692 Event::Text(t) => {
693 let text = t
694 .unescape()
695 .map_err(|e| xml_err(reader, source, format!("XML text decode error: {e}")))?;
696 let trimmed = text.trim();
697 if !trimmed.is_empty() {
698 let parsed = trimmed.parse::<f64>().map_err(|_| {
699 FlowGateError::InvalidFloat(trimmed.to_string(), "value".to_string())
700 })?;
701 value = Some(parsed);
702 }
703 }
704 Event::End(e)
705 if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"value") =>
706 {
707 break;
708 }
709 Event::Eof => {
710 return Err(xml_err(
711 reader,
712 source,
713 "Unexpected EOF while reading <value>",
714 ));
715 }
716 _ => {}
717 }
718 buf.clear();
719 }
720
721 value.ok_or_else(|| FlowGateError::InvalidGate("Missing numeric value in <value>".to_string()))
722}
723
724fn parse_fcs_dimension_list(
725 reader: &mut NsReader<&[u8]>,
726 source: &str,
727 end_ns: &str,
728 end_local: &[u8],
729) -> Result<Vec<ParameterName>, FlowGateError> {
730 let mut out = Vec::new();
731 let mut buf = Vec::new();
732 loop {
733 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
734 Ok(v) => v,
735 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
736 };
737 match event {
738 Event::Start(e) => {
739 if ns_is(&ns, NS_DATATYPE)
740 && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
741 {
742 let attrs = attrs_map(reader, source, &e)?;
743 let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
744 out.push(ParameterName::from(name.to_string()));
745 skip_element(reader, source, &e)?;
746 } else {
747 skip_element(reader, source, &e)?;
748 }
749 }
750 Event::Empty(e) => {
751 if ns_is(&ns, NS_DATATYPE)
752 && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
753 {
754 let attrs = attrs_map(reader, source, &e)?;
755 let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
756 out.push(ParameterName::from(name.to_string()));
757 }
758 }
759 Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
760 break;
761 }
762 Event::Eof => {
763 return Err(xml_err(
764 reader,
765 source,
766 "Unexpected EOF while reading fcs-dimension list",
767 ));
768 }
769 _ => {}
770 }
771 buf.clear();
772 }
773 Ok(out)
774}
775
776fn parse_spectrum_row(
777 reader: &mut NsReader<&[u8]>,
778 source: &str,
779) -> Result<Vec<f64>, FlowGateError> {
780 let mut values = Vec::new();
781 let mut buf = Vec::new();
782 loop {
783 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
784 Ok(v) => v,
785 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
786 };
787 match event {
788 Event::Start(e) => {
789 if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"coefficient") {
790 let attrs = attrs_map(reader, source, &e)?;
791 values.push(parse_required_f64_attr(
792 &attrs,
793 "value",
794 &[NS_TRANSFORMS, "", NS_DATATYPE],
795 "coefficient",
796 )?);
797 skip_element(reader, source, &e)?;
798 } else {
799 skip_element(reader, source, &e)?;
800 }
801 }
802 Event::Empty(e) => {
803 if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"coefficient") {
804 let attrs = attrs_map(reader, source, &e)?;
805 values.push(parse_required_f64_attr(
806 &attrs,
807 "value",
808 &[NS_TRANSFORMS, "", NS_DATATYPE],
809 "coefficient",
810 )?);
811 }
812 }
813 Event::End(e)
814 if element_is(
815 &ns,
816 e.name().local_name().as_ref(),
817 NS_TRANSFORMS,
818 b"spectrum",
819 ) =>
820 {
821 break;
822 }
823 Event::Eof => {
824 return Err(xml_err(
825 reader,
826 source,
827 "Unexpected EOF while reading <spectrum>",
828 ));
829 }
830 _ => {}
831 }
832 buf.clear();
833 }
834 Ok(values)
835}
836
837fn parse_quadrant_definition(
838 reader: &mut NsReader<&[u8]>,
839 source: &str,
840 start: &BytesStart<'_>,
841) -> Result<(GateId, Vec<(String, f64)>), FlowGateError> {
842 let attrs = attrs_map(reader, source, start)?;
843 let id = GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], "Quadrant")?.to_string());
844 let mut positions: Vec<(String, f64)> = Vec::new();
845 let mut buf = Vec::new();
846 loop {
847 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
848 Ok(v) => v,
849 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
850 };
851 match event {
852 Event::Start(e) => {
853 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"position") {
854 let pos_attrs = attrs_map(reader, source, &e)?;
855 let divider_ref =
856 required_attr(&pos_attrs, "divider_ref", &[NS_GATING, ""], "position")?
857 .to_string();
858 let location = parse_required_f64_attr(
859 &pos_attrs,
860 "location",
861 &[NS_GATING, ""],
862 "position",
863 )?;
864 positions.push((divider_ref, location));
865 skip_element(reader, source, &e)?;
866 } else {
867 skip_element(reader, source, &e)?;
868 }
869 }
870 Event::Empty(e) => {
871 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"position") {
872 let pos_attrs = attrs_map(reader, source, &e)?;
873 let divider_ref =
874 required_attr(&pos_attrs, "divider_ref", &[NS_GATING, ""], "position")?
875 .to_string();
876 let location = parse_required_f64_attr(
877 &pos_attrs,
878 "location",
879 &[NS_GATING, ""],
880 "position",
881 )?;
882 positions.push((divider_ref, location));
883 }
884 }
885 Event::End(e)
886 if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"Quadrant") =>
887 {
888 break;
889 }
890 Event::Eof => {
891 return Err(xml_err(
892 reader,
893 source,
894 "Unexpected EOF while reading <Quadrant>",
895 ));
896 }
897 _ => {}
898 }
899 buf.clear();
900 }
901
902 if positions.is_empty() {
903 return Err(FlowGateError::InvalidGate(format!(
904 "Quadrant '{}' does not define any positions",
905 id
906 )));
907 }
908
909 Ok((id, positions))
910}
911
912fn interval_for_location(
913 values: &[f64],
914 location: f64,
915) -> Result<(Option<f64>, Option<f64>), FlowGateError> {
916 if !location.is_finite() {
917 return Err(FlowGateError::InvalidGate(
918 "Quadrant position location must be finite".to_string(),
919 ));
920 }
921 if values.is_empty() {
922 return Err(FlowGateError::InvalidGate(
923 "Quadrant divider must define at least one value".to_string(),
924 ));
925 }
926
927 let mut sorted: Vec<f64> = values.to_vec();
928 if sorted.iter().any(|v| !v.is_finite()) {
929 return Err(FlowGateError::InvalidGate(
930 "Quadrant divider values must be finite".to_string(),
931 ));
932 }
933 sorted.sort_by(|a, b| a.total_cmp(b));
934 sorted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
935
936 if location < sorted[0] {
937 return Ok((None, Some(sorted[0])));
938 }
939 for pair in sorted.windows(2) {
940 let lo = pair[0];
941 let hi = pair[1];
942 if location >= lo && location < hi {
943 return Ok((Some(lo), Some(hi)));
944 }
945 }
946 Ok((
947 Some(*sorted.last().ok_or_else(|| {
948 FlowGateError::InvalidGate(
949 "interval_for_location produced no valid values after filtering".to_string(),
950 )
951 })?),
952 None,
953 ))
954}
955
956pub fn parse_document(xml: &str) -> Result<FlowGateDocument, FlowGateError> {
957 let mut reader = NsReader::from_str(xml);
958 reader.config_mut().trim_text(true);
959 reader.config_mut().expand_empty_elements = true;
960
961 let mut parser = FlowGateParser::default();
962 let mut buf = Vec::new();
963
964 loop {
965 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
966 Ok(v) => v,
967 Err(e) => return Err(xml_err(&reader, xml, format!("XML read error: {e}"))),
968 };
969 match event {
970 Event::Start(e) => {
971 let local_name = e.local_name();
972 let local = local_name.as_ref();
973 if element_is(&ns, local, NS_TRANSFORMS, b"transformation") {
974 parser.parse_transformation_block(&mut reader, xml, &e)?;
975 } else if element_is(&ns, local, NS_TRANSFORMS, b"spectrumMatrix") {
976 parser.parse_spectrum_matrix_block(&mut reader, xml, &e)?;
977 } else if element_is(&ns, local, NS_GATING, b"Gate") {
978 parser.parse_gate_block(&mut reader, xml, &e)?;
979 } else if ns_is(&ns, NS_GATING) && is_gate_element(local) {
980 parser.parse_standalone_gate(&mut reader, xml, &e)?;
981 } else if element_is(&ns, local, NS_GATING, b"GatingML")
982 || element_is(&ns, local, NS_GATING, b"Gating-ML")
983 {
984 } else {
986 skip_element(&mut reader, xml, &e)?;
987 }
988 }
989 Event::Empty(_) => {}
990 Event::Eof => break,
991 _ => {}
992 }
993 buf.clear();
994 }
995
996 let gate_registry = GateRegistry::new(parser.gates)?;
997 Ok(FlowGateDocument {
998 transforms: parser.transforms,
999 ratio_transforms: parser.ratio_transforms,
1000 spectrum_matrices: parser.spectrum_matrices,
1001 gate_registry,
1002 source_xml: Some(xml.to_string()),
1003 })
1004}
1005
1006impl FlowGateParser {
1007 fn parse_transformation_block(
1008 &mut self,
1009 reader: &mut NsReader<&[u8]>,
1010 source: &str,
1011 start: &BytesStart<'_>,
1012 ) -> Result<(), FlowGateError> {
1013 let attrs = attrs_map(reader, source, start)?;
1014 let id = required_attr(&attrs, "id", &[NS_TRANSFORMS, ""], "transformation")?.to_string();
1015 let mut selected_scale: Option<TransformKind> = None;
1016 let mut selected_ratio: Option<RatioTransformSpec> = None;
1017
1018 let mut buf = Vec::new();
1019 loop {
1020 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1021 Ok(v) => v,
1022 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1023 };
1024 match event {
1025 Event::Start(e) if ns_is(&ns, NS_TRANSFORMS) => {
1026 if e.local_name().as_ref() == b"fratio" {
1027 if selected_scale.is_some() || selected_ratio.is_some() {
1028 return Err(FlowGateError::InvalidGate(format!(
1029 "Transformation '{id}' contains multiple payload elements"
1030 )));
1031 }
1032 selected_ratio = Some(self.parse_ratio_transform(reader, source, &e, &id)?);
1033 } else {
1034 if selected_scale.is_some() || selected_ratio.is_some() {
1035 return Err(FlowGateError::InvalidGate(format!(
1036 "Transformation '{id}' contains multiple payload elements"
1037 )));
1038 }
1039 selected_scale = Some(parse_transform_kind(
1040 reader,
1041 source,
1042 e.local_name().as_ref(),
1043 &e,
1044 )?);
1045 skip_element(reader, source, &e)?;
1046 }
1047 }
1048 Event::Empty(e) => {
1049 if ns_is(&ns, NS_TRANSFORMS) {
1050 if e.local_name().as_ref() == b"fratio" {
1051 return Err(FlowGateError::InvalidGate(
1052 "fratio requires two fcs-dimension sub-elements".to_string(),
1053 ));
1054 }
1055 if selected_scale.is_some() || selected_ratio.is_some() {
1056 return Err(FlowGateError::InvalidGate(format!(
1057 "Transformation '{id}' contains multiple payload elements"
1058 )));
1059 }
1060 selected_scale = Some(parse_transform_kind(
1061 reader,
1062 source,
1063 e.local_name().as_ref(),
1064 &e,
1065 )?);
1066 }
1067 }
1068 Event::End(e)
1069 if ns_is(&ns, NS_TRANSFORMS)
1070 && e.name().local_name().as_ref() == b"transformation" =>
1071 {
1072 break;
1073 }
1074 Event::Eof => {
1075 return Err(xml_err(
1076 reader,
1077 source,
1078 "Unexpected EOF while reading <transformation>",
1079 ));
1080 }
1081 Event::Start(e) => {
1082 skip_element(reader, source, &e)?;
1083 }
1084 _ => {}
1085 }
1086 buf.clear();
1087 }
1088
1089 match (selected_scale, selected_ratio) {
1090 (Some(scale), None) => {
1091 self.transforms.insert(id, scale);
1092 Ok(())
1093 }
1094 (None, Some(ratio)) => {
1095 self.ratio_transforms.insert(id, ratio);
1096 Ok(())
1097 }
1098 (None, None) => Err(xml_err(
1099 reader,
1100 source,
1101 "Transformation block does not contain a supported transform element",
1102 )),
1103 (Some(_), Some(_)) => Err(FlowGateError::InvalidGate(
1104 "Transformation cannot contain both scale and ratio payloads".to_string(),
1105 )),
1106 }
1107 }
1108
1109 fn parse_ratio_transform(
1110 &self,
1111 reader: &mut NsReader<&[u8]>,
1112 source: &str,
1113 start: &BytesStart<'_>,
1114 id: &str,
1115 ) -> Result<RatioTransformSpec, FlowGateError> {
1116 let attrs = attrs_map(reader, source, start)?;
1117 let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "fratio")?;
1118 let b = parse_required_f64_attr(&attrs, "B", &[NS_TRANSFORMS, ""], "fratio")?;
1119 let c = parse_required_f64_attr(&attrs, "C", &[NS_TRANSFORMS, ""], "fratio")?;
1120 let mut dims: Vec<ParameterName> = Vec::new();
1121
1122 let mut buf = Vec::new();
1123 loop {
1124 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1125 Ok(v) => v,
1126 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1127 };
1128 match event {
1129 Event::Start(e) => {
1130 if ns_is(&ns, NS_DATATYPE)
1131 && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
1132 {
1133 let param_attrs = attrs_map(reader, source, &e)?;
1134 let name = required_attr(
1135 ¶m_attrs,
1136 "name",
1137 &[NS_DATATYPE, ""],
1138 "fcs-dimension",
1139 )?;
1140 dims.push(ParameterName::from(name.to_string()));
1141 skip_element(reader, source, &e)?;
1142 } else {
1143 skip_element(reader, source, &e)?;
1144 }
1145 }
1146 Event::Empty(e) => {
1147 if ns_is(&ns, NS_DATATYPE)
1148 && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
1149 {
1150 let param_attrs = attrs_map(reader, source, &e)?;
1151 let name = required_attr(
1152 ¶m_attrs,
1153 "name",
1154 &[NS_DATATYPE, ""],
1155 "fcs-dimension",
1156 )?;
1157 dims.push(ParameterName::from(name.to_string()));
1158 }
1159 }
1160 Event::End(e)
1161 if ns_is(&ns, NS_TRANSFORMS) && e.name().local_name().as_ref() == b"fratio" =>
1162 {
1163 break;
1164 }
1165 Event::Eof => {
1166 return Err(xml_err(
1167 reader,
1168 source,
1169 "Unexpected EOF while reading fratio transformation",
1170 ));
1171 }
1172 _ => {}
1173 }
1174 buf.clear();
1175 }
1176
1177 if dims.len() != 2 {
1178 return Err(FlowGateError::InvalidGate(format!(
1179 "fratio transformation '{id}' requires exactly 2 dimensions, found {}",
1180 dims.len()
1181 )));
1182 }
1183
1184 Ok(RatioTransformSpec {
1185 id: id.to_string(),
1186 numerator: dims.remove(0),
1187 denominator: dims.remove(0),
1188 a,
1189 b,
1190 c,
1191 })
1192 }
1193
1194 fn parse_spectrum_matrix_block(
1195 &mut self,
1196 reader: &mut NsReader<&[u8]>,
1197 source: &str,
1198 start: &BytesStart<'_>,
1199 ) -> Result<(), FlowGateError> {
1200 let attrs = attrs_map(reader, source, start)?;
1201 let id = required_attr(&attrs, "id", &[NS_TRANSFORMS, ""], "spectrumMatrix")?.to_string();
1202 let matrix_inverted_already = parse_bool_attr(
1203 optional_attr(&attrs, "matrix-inverted-already", &[NS_TRANSFORMS, ""]),
1204 false,
1205 );
1206 let mut fluorochromes: Vec<ParameterName> = Vec::new();
1207 let mut detectors: Vec<ParameterName> = Vec::new();
1208 let mut rows: Vec<Vec<f64>> = Vec::new();
1209
1210 let mut buf = Vec::new();
1211 loop {
1212 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1213 Ok(v) => v,
1214 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1215 };
1216 match event {
1217 Event::Start(e) => {
1218 if element_is(
1219 &ns,
1220 e.local_name().as_ref(),
1221 NS_TRANSFORMS,
1222 b"fluorochromes",
1223 ) {
1224 fluorochromes = parse_fcs_dimension_list(
1225 reader,
1226 source,
1227 NS_TRANSFORMS,
1228 b"fluorochromes",
1229 )?;
1230 } else if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"detectors")
1231 {
1232 detectors =
1233 parse_fcs_dimension_list(reader, source, NS_TRANSFORMS, b"detectors")?;
1234 } else if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"spectrum") {
1235 rows.push(parse_spectrum_row(reader, source)?);
1236 } else {
1237 skip_element(reader, source, &e)?;
1238 }
1239 }
1240 Event::End(e)
1241 if element_is(
1242 &ns,
1243 e.name().local_name().as_ref(),
1244 NS_TRANSFORMS,
1245 b"spectrumMatrix",
1246 ) =>
1247 {
1248 break;
1249 }
1250 Event::Eof => {
1251 return Err(xml_err(
1252 reader,
1253 source,
1254 "Unexpected EOF while reading spectrumMatrix",
1255 ));
1256 }
1257 _ => {}
1258 }
1259 buf.clear();
1260 }
1261
1262 if fluorochromes.is_empty() || detectors.is_empty() {
1263 return Err(FlowGateError::InvalidGate(format!(
1264 "spectrumMatrix '{id}' must define fluorochromes and detectors"
1265 )));
1266 }
1267 if rows.len() != fluorochromes.len() {
1268 return Err(FlowGateError::InvalidGate(format!(
1269 "spectrumMatrix '{id}' has {} rows but {} fluorochromes",
1270 rows.len(),
1271 fluorochromes.len()
1272 )));
1273 }
1274 for row in &rows {
1275 if row.len() != detectors.len() {
1276 return Err(FlowGateError::InvalidGate(format!(
1277 "spectrumMatrix '{id}' row has {} coefficients but {} detectors",
1278 row.len(),
1279 detectors.len()
1280 )));
1281 }
1282 }
1283
1284 let mut coefficients = Vec::with_capacity(fluorochromes.len() * detectors.len());
1285 for row in rows {
1286 coefficients.extend(row);
1287 }
1288
1289 self.spectrum_matrices.insert(
1290 id.clone(),
1291 SpectrumMatrixSpec {
1292 id,
1293 fluorochromes,
1294 detectors,
1295 coefficients,
1296 matrix_inverted_already,
1297 },
1298 );
1299 Ok(())
1300 }
1301
1302 fn parse_gate_block(
1303 &mut self,
1304 reader: &mut NsReader<&[u8]>,
1305 source: &str,
1306 start: &BytesStart<'_>,
1307 ) -> Result<(), FlowGateError> {
1308 let attrs = attrs_map(reader, source, start)?;
1309 let id = GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], "Gate")?.to_string());
1310 let parent_id = optional_attr(&attrs, "parent_id", &[NS_GATING, ""])
1311 .map(|s| GateId::from(s.to_string()));
1312 let mut parsed_gate: Option<GateKind> = None;
1313
1314 let mut buf = Vec::new();
1315 loop {
1316 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1317 Ok(v) => v,
1318 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1319 };
1320 match event {
1321 Event::Start(e) => {
1322 if ns_is(&ns, NS_GATING) {
1323 parsed_gate = Some(match e.local_name().as_ref() {
1324 b"RectangleGate" => {
1325 GateKind::Rectangle(Box::new(self.parse_rectangle_gate(
1326 reader,
1327 source,
1328 id.clone(),
1329 parent_id.clone(),
1330 )?))
1331 }
1332 b"PolygonGate" => GateKind::Polygon(self.parse_polygon_gate(
1333 reader,
1334 source,
1335 id.clone(),
1336 parent_id.clone(),
1337 )?),
1338 b"EllipsoidGate" => GateKind::Ellipsoid(self.parse_ellipsoid_gate(
1339 reader,
1340 source,
1341 id.clone(),
1342 parent_id.clone(),
1343 )?),
1344 b"BooleanGate" => GateKind::Boolean(self.parse_boolean_gate(
1345 reader,
1346 source,
1347 id.clone(),
1348 parent_id.clone(),
1349 )?),
1350 _ => {
1351 skip_element(reader, source, &e)?;
1352 continue;
1353 }
1354 });
1355 } else {
1356 skip_element(reader, source, &e)?;
1357 }
1358 }
1359 Event::End(e)
1360 if ns_is(&ns, NS_GATING) && e.name().local_name().as_ref() == b"Gate" =>
1361 {
1362 break;
1363 }
1364 Event::Eof => {
1365 return Err(xml_err(
1366 reader,
1367 source,
1368 "Unexpected EOF while reading <Gate>",
1369 ));
1370 }
1371 _ => {}
1372 }
1373 buf.clear();
1374 }
1375
1376 let gate = parsed_gate.ok_or_else(|| {
1377 FlowGateError::InvalidGate("Gate block does not contain a supported gate".to_string())
1378 })?;
1379 self.gates.insert(id, gate);
1380 Ok(())
1381 }
1382
1383 fn parse_standalone_gate(
1384 &mut self,
1385 reader: &mut NsReader<&[u8]>,
1386 source: &str,
1387 start: &BytesStart<'_>,
1388 ) -> Result<(), FlowGateError> {
1389 let attrs = attrs_map(reader, source, start)?;
1390 let gate_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string();
1391 let id =
1392 GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], &gate_name)?.to_string());
1393 let parent_id = optional_attr(&attrs, "parent_id", &[NS_GATING, ""])
1394 .map(|s| GateId::from(s.to_string()));
1395
1396 match start.local_name().as_ref() {
1397 b"RectangleGate" => {
1398 let gate = self.parse_rectangle_gate(reader, source, id.clone(), parent_id)?;
1399 self.gates.insert(id, GateKind::Rectangle(Box::new(gate)));
1400 }
1401 b"PolygonGate" => {
1402 let gate = self.parse_polygon_gate(reader, source, id.clone(), parent_id)?;
1403 self.gates.insert(id, GateKind::Polygon(gate));
1404 }
1405 b"EllipsoidGate" => {
1406 let gate = self.parse_ellipsoid_gate(reader, source, id.clone(), parent_id)?;
1407 self.gates.insert(id, GateKind::Ellipsoid(gate));
1408 }
1409 b"BooleanGate" => {
1410 let gate = self.parse_boolean_gate(reader, source, id.clone(), parent_id)?;
1411 self.gates.insert(id, GateKind::Boolean(gate));
1412 }
1413 b"QuadrantGate" => {
1414 for (qid, gate) in self.parse_quadrant_gate(reader, source, parent_id)? {
1415 self.gates.insert(qid, gate);
1416 }
1417 }
1418 _ => {
1419 skip_element(reader, source, start)?;
1420 }
1421 }
1422
1423 Ok(())
1424 }
1425
1426 fn parse_quadrant_gate(
1427 &self,
1428 reader: &mut NsReader<&[u8]>,
1429 source: &str,
1430 parent_id: Option<GateId>,
1431 ) -> Result<Vec<(GateId, GateKind)>, FlowGateError> {
1432 let mut dividers: HashMap<String, RectangleDimension> = HashMap::new();
1433 let mut divider_values: HashMap<String, Vec<f64>> = HashMap::new();
1434 let mut quadrants: Vec<(GateId, Vec<(String, f64)>)> = Vec::new();
1435 let mut buf = Vec::new();
1436
1437 loop {
1438 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1439 Ok(v) => v,
1440 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1441 };
1442 match event {
1443 Event::Start(e) => {
1444 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"divider") {
1445 let (divider_id, dim, values) =
1446 self.parse_quadrant_divider(reader, source, &e)?;
1447 dividers.insert(divider_id.clone(), dim);
1448 divider_values.insert(divider_id, values);
1449 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"Quadrant") {
1450 quadrants.push(parse_quadrant_definition(reader, source, &e)?);
1451 } else {
1452 skip_element(reader, source, &e)?;
1453 }
1454 }
1455 Event::End(e)
1456 if element_is(
1457 &ns,
1458 e.name().local_name().as_ref(),
1459 NS_GATING,
1460 b"QuadrantGate",
1461 ) =>
1462 {
1463 break;
1464 }
1465 Event::Eof => {
1466 return Err(xml_err(
1467 reader,
1468 source,
1469 "Unexpected EOF while reading QuadrantGate",
1470 ));
1471 }
1472 _ => {}
1473 }
1474 buf.clear();
1475 }
1476
1477 let mut out = Vec::new();
1478 for (qid, positions) in quadrants {
1479 let mut dims = Vec::with_capacity(positions.len());
1480 for (divider_id, location) in positions {
1481 let base_dim = dividers.get(÷r_id).ok_or_else(|| {
1482 FlowGateError::InvalidGate(format!(
1483 "Quadrant '{}' references unknown divider '{}'",
1484 qid, divider_id
1485 ))
1486 })?;
1487 let values = divider_values.get(÷r_id).ok_or_else(|| {
1488 FlowGateError::InvalidGate(format!("Divider '{}' has no values", divider_id))
1489 })?;
1490 let (min, max) = interval_for_location(values, location)?;
1491 dims.push(RectangleDimension {
1492 parameter: base_dim.parameter.clone(),
1493 transform: base_dim.transform,
1494 min,
1495 max,
1496 });
1497 }
1498 let gate = RectangleGate::new(qid.clone(), parent_id.clone(), dims)?;
1499 out.push((qid, GateKind::Rectangle(Box::new(gate))));
1500 }
1501
1502 Ok(out)
1503 }
1504
1505 fn parse_quadrant_divider(
1506 &self,
1507 reader: &mut NsReader<&[u8]>,
1508 source: &str,
1509 start: &BytesStart<'_>,
1510 ) -> Result<(String, RectangleDimension, Vec<f64>), FlowGateError> {
1511 let attrs = attrs_map(reader, source, start)?;
1512 let divider_id = required_attr(&attrs, "id", &[NS_GATING, ""], "divider")?.to_string();
1513 let transform = parse_transform_ref(&attrs, &self.transforms)?;
1514 let compensation_ref = optional_attr(&attrs, "compensation-ref", &[NS_GATING, ""])
1515 .unwrap_or("uncompensated")
1516 .to_string();
1517 let mut selector: Option<DimensionSelector> = None;
1518 let mut values = Vec::<f64>::new();
1519 let mut buf = Vec::new();
1520 loop {
1521 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1522 Ok(v) => v,
1523 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1524 };
1525 match event {
1526 Event::Start(e) => {
1527 if ns_is(&ns, NS_DATATYPE)
1528 && matches!(
1529 e.local_name().as_ref(),
1530 b"parameter" | b"fcs-dimension" | b"new-dimension"
1531 )
1532 {
1533 selector = Some(parse_dimension_selector(reader, source, &e)?);
1534 skip_element(reader, source, &e)?;
1535 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"value") {
1536 values.push(parse_value_content(reader, source)?);
1537 } else {
1538 skip_element(reader, source, &e)?;
1539 }
1540 }
1541 Event::Empty(e) => {
1542 if ns_is(&ns, NS_DATATYPE)
1543 && matches!(
1544 e.local_name().as_ref(),
1545 b"parameter" | b"fcs-dimension" | b"new-dimension"
1546 )
1547 {
1548 selector = Some(parse_dimension_selector(reader, source, &e)?);
1549 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"value") {
1550 let value_attrs = attrs_map(reader, source, &e)?;
1551 values.push(parse_required_f64_attr(
1552 &value_attrs,
1553 "value",
1554 &[NS_DATATYPE, "", NS_GATING],
1555 "value",
1556 )?);
1557 }
1558 }
1559 Event::End(e)
1560 if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"divider") =>
1561 {
1562 break;
1563 }
1564 Event::Eof => {
1565 return Err(xml_err(
1566 reader,
1567 source,
1568 "Unexpected EOF while reading divider",
1569 ));
1570 }
1571 _ => {}
1572 }
1573 buf.clear();
1574 }
1575 let selector = selector.ok_or_else(|| {
1576 FlowGateError::InvalidGate(format!(
1577 "Divider '{divider_id}' is missing dimension reference"
1578 ))
1579 })?;
1580 let parameter = dimension_selector_to_parameter(&compensation_ref, selector);
1581 Ok((
1582 divider_id,
1583 RectangleDimension {
1584 parameter,
1585 transform,
1586 min: None,
1587 max: None,
1588 },
1589 values,
1590 ))
1591 }
1592
1593 fn parse_rectangle_gate(
1594 &self,
1595 reader: &mut NsReader<&[u8]>,
1596 source: &str,
1597 id: GateId,
1598 parent_id: Option<GateId>,
1599 ) -> Result<RectangleGate, FlowGateError> {
1600 let mut dimensions = Vec::new();
1601 let mut buf = Vec::new();
1602 loop {
1603 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1604 Ok(v) => v,
1605 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1606 };
1607 match event {
1608 Event::Start(e) => {
1609 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1610 dimensions.push(self.parse_rectangle_dimension(reader, source, &e)?);
1611 } else {
1612 skip_element(reader, source, &e)?;
1613 }
1614 }
1615 Event::End(e)
1616 if element_is(
1617 &ns,
1618 e.name().local_name().as_ref(),
1619 NS_GATING,
1620 b"RectangleGate",
1621 ) =>
1622 {
1623 break;
1624 }
1625 Event::Eof => {
1626 return Err(xml_err(
1627 reader,
1628 source,
1629 "Unexpected EOF while reading RectangleGate",
1630 ));
1631 }
1632 _ => {}
1633 }
1634 buf.clear();
1635 }
1636 RectangleGate::new(id, parent_id, dimensions)
1637 }
1638
1639 fn parse_rectangle_dimension(
1640 &self,
1641 reader: &mut NsReader<&[u8]>,
1642 source: &str,
1643 start: &BytesStart<'_>,
1644 ) -> Result<RectangleDimension, FlowGateError> {
1645 let attrs = attrs_map(reader, source, start)?;
1646 let min = parse_optional_f64_attr(&attrs, "min", &[NS_GATING, ""], "dimension")?;
1647 let max = parse_optional_f64_attr(&attrs, "max", &[NS_GATING, ""], "dimension")?;
1648 let transform = parse_transform_ref(&attrs, &self.transforms)?;
1649 let parameter = parse_dimension_ref(
1650 reader,
1651 source,
1652 &attrs,
1653 "rectangle dimension",
1654 NS_GATING,
1655 b"dimension",
1656 )?;
1657 Ok(RectangleDimension {
1658 parameter,
1659 transform,
1660 min,
1661 max,
1662 })
1663 }
1664
1665 fn parse_polygon_gate(
1666 &self,
1667 reader: &mut NsReader<&[u8]>,
1668 source: &str,
1669 id: GateId,
1670 parent_id: Option<GateId>,
1671 ) -> Result<PolygonGate, FlowGateError> {
1672 let mut dimensions = Vec::new();
1673 let mut vertices = Vec::new();
1674 let mut buf = Vec::new();
1675
1676 loop {
1677 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1678 Ok(v) => v,
1679 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1680 };
1681 match event {
1682 Event::Start(e) => {
1683 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1684 dimensions.push(self.parse_polygon_dimension(reader, source, &e)?);
1685 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"vertex") {
1686 vertices.push(parse_vertex(reader, source)?);
1687 } else {
1688 skip_element(reader, source, &e)?;
1689 }
1690 }
1691 Event::End(e)
1692 if element_is(
1693 &ns,
1694 e.name().local_name().as_ref(),
1695 NS_GATING,
1696 b"PolygonGate",
1697 ) =>
1698 {
1699 break;
1700 }
1701 Event::Eof => {
1702 return Err(xml_err(
1703 reader,
1704 source,
1705 "Unexpected EOF while reading PolygonGate",
1706 ));
1707 }
1708 _ => {}
1709 }
1710 buf.clear();
1711 }
1712
1713 if dimensions.len() != 2 {
1714 return Err(FlowGateError::InvalidGate(format!(
1715 "PolygonGate requires exactly 2 dimensions, found {}",
1716 dimensions.len()
1717 )));
1718 }
1719
1720 PolygonGate::new(
1721 id,
1722 parent_id,
1723 dimensions.remove(0),
1724 dimensions.remove(0),
1725 vertices,
1726 )
1727 }
1728
1729 fn parse_polygon_dimension(
1730 &self,
1731 reader: &mut NsReader<&[u8]>,
1732 source: &str,
1733 start: &BytesStart<'_>,
1734 ) -> Result<PolygonDimension, FlowGateError> {
1735 let attrs = attrs_map(reader, source, start)?;
1736 let transform = parse_transform_ref(&attrs, &self.transforms)?;
1737 let parameter = parse_dimension_ref(
1738 reader,
1739 source,
1740 &attrs,
1741 "polygon dimension",
1742 NS_GATING,
1743 b"dimension",
1744 )?;
1745 Ok(PolygonDimension {
1746 parameter,
1747 transform,
1748 })
1749 }
1750
1751 fn parse_ellipsoid_gate(
1752 &self,
1753 reader: &mut NsReader<&[u8]>,
1754 source: &str,
1755 id: GateId,
1756 parent_id: Option<GateId>,
1757 ) -> Result<EllipsoidGate, FlowGateError> {
1758 let mut dimensions = Vec::new();
1759 let mut mean = Vec::new();
1760 let mut covariance_upper = Vec::new();
1761 let mut distance_sq: Option<f64> = None;
1762 let mut buf = Vec::new();
1763
1764 loop {
1765 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1766 Ok(v) => v,
1767 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1768 };
1769 match event {
1770 Event::Start(e) => {
1771 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1772 dimensions.push(self.parse_ellipsoid_dimension(reader, source, &e)?);
1773 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"mean") {
1774 mean = parse_coordinates_block(reader, source, NS_GATING, b"mean")?;
1775 } else if element_is(
1776 &ns,
1777 e.local_name().as_ref(),
1778 NS_GATING,
1779 b"covarianceMatrix",
1780 ) {
1781 covariance_upper = parse_covariance_block(reader, source)?;
1782 } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"distanceSquare")
1783 {
1784 let attrs = attrs_map(reader, source, &e)?;
1785 distance_sq = Some(parse_required_f64_attr(
1786 &attrs,
1787 "value",
1788 &[NS_DATATYPE, ""],
1789 "distanceSquare",
1790 )?);
1791 skip_element(reader, source, &e)?;
1792 } else {
1793 skip_element(reader, source, &e)?;
1794 }
1795 }
1796 Event::Empty(e)
1797 if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"distanceSquare") =>
1798 {
1799 let attrs = attrs_map(reader, source, &e)?;
1800 distance_sq = Some(parse_required_f64_attr(
1801 &attrs,
1802 "value",
1803 &[NS_DATATYPE, ""],
1804 "distanceSquare",
1805 )?);
1806 }
1807 Event::End(e)
1808 if element_is(
1809 &ns,
1810 e.name().local_name().as_ref(),
1811 NS_GATING,
1812 b"EllipsoidGate",
1813 ) =>
1814 {
1815 break;
1816 }
1817 Event::Eof => {
1818 return Err(xml_err(
1819 reader,
1820 source,
1821 "Unexpected EOF while reading EllipsoidGate",
1822 ));
1823 }
1824 _ => {}
1825 }
1826 buf.clear();
1827 }
1828
1829 let distance_sq = distance_sq.ok_or_else(|| {
1830 FlowGateError::MissingAttribute("value".to_string(), "distanceSquare".to_string())
1831 })?;
1832 let n = dimensions.len();
1833 if covariance_upper.len() == n * n {
1834 EllipsoidGate::new_general_covariance(
1835 id,
1836 parent_id,
1837 dimensions,
1838 mean,
1839 &covariance_upper,
1840 distance_sq,
1841 )
1842 } else {
1843 EllipsoidGate::new(
1844 id,
1845 parent_id,
1846 dimensions,
1847 mean,
1848 &covariance_upper,
1849 distance_sq,
1850 )
1851 }
1852 }
1853
1854 fn parse_ellipsoid_dimension(
1855 &self,
1856 reader: &mut NsReader<&[u8]>,
1857 source: &str,
1858 start: &BytesStart<'_>,
1859 ) -> Result<EllipsoidDimension, FlowGateError> {
1860 let attrs = attrs_map(reader, source, start)?;
1861 let transform = parse_transform_ref(&attrs, &self.transforms)?;
1862 let parameter = parse_dimension_ref(
1863 reader,
1864 source,
1865 &attrs,
1866 "ellipsoid dimension",
1867 NS_GATING,
1868 b"dimension",
1869 )?;
1870 Ok(EllipsoidDimension {
1871 parameter,
1872 transform,
1873 })
1874 }
1875
1876 fn parse_boolean_gate(
1877 &self,
1878 reader: &mut NsReader<&[u8]>,
1879 source: &str,
1880 current_gate_id: GateId,
1881 parent_id: Option<GateId>,
1882 ) -> Result<BooleanGate, FlowGateError> {
1883 let mut op: Option<BooleanOp> = None;
1884 let mut operands: Vec<BooleanOperand> = Vec::new();
1885 let mut buf = Vec::new();
1886
1887 loop {
1888 let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1889 Ok(v) => v,
1890 Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1891 };
1892 match event {
1893 Event::Start(e) => {
1894 if ns_is(&ns, NS_GATING)
1895 && matches!(e.local_name().as_ref(), b"and" | b"or" | b"not")
1896 {
1897 let (parsed_op, parsed_operands) = parse_boolean_op_block(
1898 reader,
1899 source,
1900 e.local_name().as_ref(),
1901 ¤t_gate_id,
1902 &self.gates,
1903 )?;
1904 op = Some(parsed_op);
1905 operands = parsed_operands;
1906 } else {
1907 skip_element(reader, source, &e)?;
1908 }
1909 }
1910 Event::End(e)
1911 if element_is(
1912 &ns,
1913 e.name().local_name().as_ref(),
1914 NS_GATING,
1915 b"BooleanGate",
1916 ) =>
1917 {
1918 break;
1919 }
1920 Event::Eof => {
1921 return Err(xml_err(
1922 reader,
1923 source,
1924 "Unexpected EOF while reading BooleanGate",
1925 ));
1926 }
1927 _ => {}
1928 }
1929 buf.clear();
1930 }
1931
1932 let op = op.ok_or_else(|| {
1933 FlowGateError::InvalidGate("BooleanGate missing required boolean operator".to_string())
1934 })?;
1935 BooleanGate::new(current_gate_id, parent_id, op, operands)
1936 }
1937}