7878]
7979
8080
81+ def _filter_unread_data (unread_data : dict [str , str | None ]) -> dict [str , str | None ]:
82+ """Filter out empty, null, or whitespace-only values from unread data."""
83+ return {
84+ key : value
85+ for key , value in unread_data .items ()
86+ if not (
87+ (value == "" )
88+ or (value is None )
89+ or (isinstance (value , str ) and value .strip () == "" )
90+ )
91+ }
92+
93+
8194class RegionType (Enum ):
8295 POLYGON = "POLYGON"
8396 RECTANGLE = "RECTANGLE"
@@ -87,14 +100,27 @@ class RegionType(Enum):
87100def create_metadata (root_element : StrictXmlElement , file_path : str ) -> Metadata :
88101 equipment_serial_number = None
89102 model_number = None
103+ all_custom_info = {}
90104
91105 tube = root_element .recursive_find_or_none (["experiment" , "specimen" , "tube" ])
92106 if tube :
93107 equipment_serial_number = tube .find_or_none ("data_instrument_serial_number" )
94108 model_number = tube .find_or_none ("data_instrument_name" )
109+ tube .mark_read ("name" )
95110
96111 software_version = root_element .get_attr_or_none ("version" )
97112
113+ # Collect unread data from root element
114+ root_unread = root_element .get_unread (
115+ skip = {
116+ "version" ,
117+ "release_version" ,
118+ }
119+ )
120+ if root_unread :
121+ filtered_root_unread = _filter_unread_data (root_unread )
122+ all_custom_info .update (filtered_root_unread )
123+
98124 return Metadata (
99125 file_name = Path (file_path ).name ,
100126 unc_path = file_path ,
@@ -107,6 +133,7 @@ def create_metadata(root_element: StrictXmlElement, file_path: str) -> Metadata:
107133 software_name = constants .SOFTWARE_NAME ,
108134 software_version = software_version ,
109135 asm_file_identifier = Path (file_path ).with_suffix (".json" ).name ,
136+ custom_info = all_custom_info if all_custom_info else None ,
110137 )
111138
112139
@@ -134,6 +161,7 @@ def _extract_statistics_from_calculations(
134161 for calc_schedule in statistics_element .findall ("calculation_schedule" ):
135162 gate = calc_schedule .get_attr_or_none ("gate" )
136163 if gate != gate_name :
164+ calc_schedule .get_unread ()
137165 continue
138166
139167 parameter = calc_schedule .get_attr_or_none ("parameter" )
@@ -208,31 +236,43 @@ def _create_data_regions(tube: StrictXmlElement) -> list[DataRegion]:
208236 # First pass: collect gate information
209237 for gate in gates_element .findall ("gate" ):
210238 # Skip All Events full name (no region)
211- if gate .get_attr_or_none ("type" ) == "EventSource_Classifier" :
239+ gate_type = gate .get_attr_or_none ("type" )
240+ fullname = gate .get_attr_or_none ("fullname" )
241+
242+ if gate_type == "EventSource_Classifier" :
212243 continue
213244
214245 region = gate .find_or_none ("region" )
215246 if not region :
216247 continue
217248
218- fullname = gate .get_attr_or_none ("fullname" )
219249 if not fullname :
220250 continue
221251
222252 region_name = region .get_attr_or_none ("name" )
223253 if region_name :
224254 fullname_to_region [fullname ] = region_name
225255
256+ region .mark_read (
257+ {
258+ "type" ,
259+ "xparm" ,
260+ "yparm" ,
261+ }
262+ )
263+
226264 # Second pass: create data regions
227265 for gate in gates_element .findall ("gate" ):
228- if gate .get_attr_or_none ("type" ) == "EventSource_Classifier" :
266+ gate_type = gate .get_attr_or_none ("type" )
267+ fullname = gate .get_attr_or_none ("fullname" )
268+
269+ if gate_type == "EventSource_Classifier" :
229270 continue
230271
231272 region = gate .find_or_none ("region" )
232273 if not region :
233274 continue
234275
235- fullname = gate .get_attr_or_none ("fullname" )
236276 if not fullname :
237277 continue
238278
@@ -348,6 +388,20 @@ def _build_population_tree(full_name: str) -> Population | None:
348388 region = _gate .find_or_none ("region" )
349389 region_id = region .get_attr_or_none ("name" ) if region else None
350390
391+ if region :
392+ region .mark_read (
393+ {
394+ "type" ,
395+ "xparm" ,
396+ "yparm" ,
397+ }
398+ )
399+ _gate .mark_read (
400+ {
401+ "type" ,
402+ }
403+ )
404+
351405 sub_populations = []
352406 if full_name in gate_to_children :
353407 for child_fullname in gate_to_children [full_name ]:
@@ -378,11 +432,16 @@ def _build_population_tree(full_name: str) -> Population | None:
378432 )
379433
380434 # Find the root population (All Events) and build the tree
381- root_gates = [
382- gate
383- for gate in gates_element .findall ("gate" )
384- if gate .get_attr_or_none ("type" ) == "EventSource_Classifier"
385- ]
435+ root_gates = []
436+ for gate in gates_element .findall ("gate" ):
437+ gate_type = gate .get_attr_or_none ("type" )
438+ if gate_type == "EventSource_Classifier" :
439+ root_gates .append (gate )
440+ gate .mark_read (
441+ {
442+ "fullname" ,
443+ }
444+ )
386445
387446 if not root_gates :
388447 return []
@@ -416,6 +475,8 @@ def _create_compensation_matrix_groups(
416475 name = param .get_attr_or_none ("name" )
417476 if name and name not in EXCLUDED_CHANNELS :
418477 parameters .append (param )
478+ else :
479+ param .get_unread ()
419480
420481 if not parameters :
421482 return None
@@ -450,13 +511,22 @@ def _create_compensation_matrix_groups(
450511 compensation_value = coef_value ,
451512 )
452513 )
514+ param .mark_read (
515+ "type" ,
516+ )
453517
454518 result .append (
455519 CompensationMatrixGroup (
456520 dimension_identifier = dimension_id ,
457521 compensation_matrices = matrices if matrices else None ,
458522 )
459523 )
524+ instrument_settings .mark_read (
525+ {
526+ "name" ,
527+ "template" ,
528+ }
529+ )
460530
461531 return result if result else None
462532
@@ -477,10 +547,24 @@ def _process_tube(
477547
478548 processed_data_id = random_uuid_str ()
479549
550+ # Extract custom info from keywords
551+ custom_info = {}
552+ keywords_element = tube .find_or_none ("keywords" )
553+ if keywords_element :
554+ for keyword in keywords_element .findall ("keyword" ):
555+ keyword_name = keyword .get_attr_or_none ("name" )
556+ keyword .mark_read ("type" )
557+
558+ if keyword_name :
559+ value_element = keyword .find_or_none ("value" )
560+ if value_element :
561+ keyword_value = value_element .get_text_or_none ()
562+ if keyword_value :
563+ custom_info [keyword_name ] = keyword_value
564+
480565 # Create populations and data regions
481566 populations = _create_populations (tube )
482567 data_regions = _create_data_regions (tube )
483-
484568 measurement = Measurement (
485569 measurement_identifier = random_uuid_str (),
486570 sample_identifier = tube_name ,
@@ -490,6 +574,7 @@ def _process_tube(
490574 processed_data_identifier = processed_data_id ,
491575 populations = populations ,
492576 data_regions = data_regions ,
577+ custom_info = custom_info if custom_info else None ,
493578 )
494579
495580 return [measurement ]
@@ -511,6 +596,7 @@ def create_measurement_groups(root_element: StrictXmlElement) -> list[Measuremen
511596 if not experiment :
512597 msg = "No experiment found in the XML file."
513598 raise AllotropeParsingError (msg )
599+
514600 measurement_time = experiment .recursive_find_or_none (["specimen" , "tube" , "date" ])
515601 experiment_identifier = experiment .get_attr_or_none ("name" )
516602 analyst = experiment .recursive_find_or_none (["owner_name" ])
0 commit comments