Source code for pycsamt.emtf.xml.serializer

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0

"""EMTF scientific objects -> deterministic XML element trees.

The serializer is intentionally separate from filesystem I/O.  It translates
format-neutral :class:`pycsamt.emtf.EMTF` objects into the EMTF XML vocabulary
without routing through EDI or exposing ``ElementTree`` nodes to the scientific
model.
"""

from __future__ import annotations

from datetime import datetime
import re
from typing import Any, Iterable
import warnings
import xml.etree.ElementTree as ET

import numpy as np

from ...api.property import PyCSAMTObject
from ...metadata import ChannelMeta, Person
from ..document import EMTF
from ..estimates import StatisticalEstimate
from ..transfer import TransferFunction
from .constants import EMTF_ROOT, ESTIMATE_CODES

__all__ = [
    "EMTFXMLSerializationError",
    "EMTFXMLWriteWarning",
    "EMTFXMLSerializer",
]


_XML_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")

_ESTIMATE_DEFAULTS = {
    "VAR": {
        "name": "VAR",
        "type": "real",
        "description": "Variance",
        "intention": "error estimate",
        "tag": "variance",
        "external_url": None,
    },
    "INVSIGCOV": {
        "name": "INVSIGCOV",
        "type": "complex",
        "description": "Inverse Coherent Signal Power Matrix (S)",
        "intention": "signal power estimate",
        "tag": "inverse_signal_covariance",
        "external_url": None,
    },
    "RESIDCOV": {
        "name": "RESIDCOV",
        "type": "complex",
        "description": "Residual Covariance (N)",
        "intention": "error estimate",
        "tag": "residual_covariance",
        "external_url": None,
    },
}


[docs] class EMTFXMLSerializationError(ValueError): """Raised when an EMTF object cannot be serialized safely."""
[docs] class EMTFXMLWriteWarning(UserWarning): """Warning emitted when permissive XML writing omits unsafe content."""
def _warn(message: str) -> None: warnings.warn(message, EMTFXMLWriteWarning, stacklevel=3) def _value_text(value: Any) -> str: if isinstance(value, datetime): return value.isoformat() return str(value) def _append_text( parent: ET.Element, tag: str, value: Any, *, allow_empty: bool = False, attrs: dict[str, Any] | None = None, ) -> ET.Element | None: if value is None: return None raw = _value_text(value) if not raw and not allow_empty: return None node = ET.SubElement(parent, tag) for key, attr_value in (attrs or {}).items(): if attr_value is not None: node.set(str(key), _value_text(attr_value)) if raw: node.text = raw return node def _apply_mapping(node: ET.Element, value: Any) -> None: """Reverse the reader's ``element_to_mapping`` representation.""" if isinstance(value, dict): attributes = value.get("@attributes", {}) if isinstance(attributes, dict): for key, attr_value in attributes.items(): if attr_value is not None: node.set(str(key), _value_text(attr_value)) if "#text" in value and value["#text"] is not None: node.text = _value_text(value["#text"]) for key, child_value in value.items(): if key in {"@attributes", "#text"}: continue values = ( child_value if isinstance(child_value, list) else [child_value] ) for item in values: child_node = ET.SubElement(node, str(key)) _apply_mapping(child_node, item) return if value is not None: node.text = _value_text(value) def _append_mapping(parent: ET.Element, tag: str, value: Any) -> ET.Element: node = ET.SubElement(parent, tag) _apply_mapping(node, value) return node
[docs] class EMTFXMLSerializer(PyCSAMTObject): """Build a deterministic EMTF XML element tree from an :class:`EMTF`. Parameters ---------- strict : bool, default=True Reject unsupported or scientifically ambiguous content. Permissive mode warns and omits only the problematic component. precision : int, default=17 Significant decimal digits used for floating-point response data. Seventeen digits are sufficient to round-trip IEEE float64 values. """ def __init__(self, *, strict: bool = True, precision: int = 17) -> None: self.strict = bool(strict) self.precision = int(precision) if not 6 <= self.precision <= 20: raise ValueError("precision must be between 6 and 20")
[docs] def to_element(self, document: EMTF) -> ET.Element: """Return the root ``<EM_TF>`` element for *document*.""" if not isinstance(document, EMTF): raise TypeError("document must be a pycsamt.emtf.EMTF") root = ET.Element(EMTF_ROOT) self._append_header(root, document) self._append_provenance(root, document) self._append_copyright(root, document) self._append_site(root, document) self._append_field_notes(root, document) self._append_processing(root, document) data_specs = self._data_type_specs(document) estimate_specs = self._estimate_specs(document) self._append_estimate_declarations(root, estimate_specs) self._append_data_type_declarations(root, data_specs) self._append_site_layout(root, document) self._append_data(root, document, data_specs) self._append_period_range(root, document) return root
# ------------------------------------------------------------------ # Header / metadata # ------------------------------------------------------------------ def _append_header(self, root: ET.Element, document: EMTF) -> None: _append_text(root, "Description", document.description) _append_text(root, "ProductId", document.product_id) _append_text(root, "SubType", document.subtype) metadata = document.metadata or {} if "notes" in metadata: _append_mapping(root, "Notes", metadata["notes"]) if document.tags: _append_text(root, "Tags", ",".join(document.tags)) for key, tag in ( ("externalurl", "ExternalUrl"), ("primarydata", "PrimaryData"), ("attachment", "Attachment"), ("gridorigin", "GridOrigin"), ): if key not in metadata: continue raw = metadata[key] values = raw if isinstance(raw, list) else [raw] for value in values: _append_mapping(root, tag, value) def _append_provenance(self, root: ET.Element, document: EMTF) -> None: provenance = document.provenance if provenance is None: return node = ET.SubElement(root, "Provenance") _append_text(node, "CreateTime", provenance.create_time) _append_text( node, "CreatingApplication", provenance.creating_application, ) self._append_person(node, "Creator", provenance.creator) self._append_person(node, "Submitter", provenance.submitter) @staticmethod def _append_person( parent: ET.Element, tag: str, person: Person | None, ) -> None: if person is None: return if not any( ( person.name, person.email, person.organization, person.organization_url, ) ): return node = ET.SubElement(parent, tag) _append_text(node, "Name", person.name) _append_text(node, "Email", person.email) _append_text(node, "Org", person.organization) _append_text(node, "OrgUrl", person.organization_url) def _append_copyright(self, root: ET.Element, document: EMTF) -> None: info = document.copyright orientation = document.orientation rotation_info = ( orientation.rotation_info if orientation is not None else None ) if info is None and rotation_info is None: return if info is None: # Rotation history is scientifically useful but must not force the # creation of an otherwise fabricated Copyright metadata block. _append_text(root, "RotationInfo", rotation_info) return node = ET.SubElement(root, "Copyright") if info is not None: reference = info.reference citation = ET.SubElement(node, "Citation") _append_text(citation, "Title", reference.title) _append_text(citation, "Authors", reference.author) _append_text(citation, "Year", reference.year) survey_doi = reference.extra.get("survey_doi") _append_text(citation, "SurveyDOI", survey_doi) doi = reference.doi or reference.extra.get("doi") _append_text(citation, "DOI", doi) extras = reference.extra _append_text( node, "SelectedPublications", extras.get("selectedpublications"), ) _append_text( node, "Acknowledgement", extras.get("acknowledgement"), ) _append_text(node, "ReleaseStatus", info.release_status) _append_text(node, "ConditionsOfUse", info.conditions_of_use) if rotation_info: _append_text(node, "RotationInfo", rotation_info) if info is not None: _append_text( node, "AdditionalInfo", info.reference.extra.get("additionalinfo"), ) def _append_site(self, root: ET.Element, document: EMTF) -> None: site = document.site orientation = document.orientation quality = document.quality if site is None and orientation is None and quality is None: return node = ET.SubElement(root, "Site") if site is not None: _append_text(node, "Project", site.project) _append_text(node, "Survey", site.survey) _append_text(node, "YearCollected", site.year_collected) _append_text(node, "Country", site.country) _append_text(node, "Id", site.site_id) _append_text(node, "Name", site.name) self._append_location(node, site.location) if orientation is not None and orientation.mode is not None: attrs: dict[str, Any] = {} if orientation.is_orthogonal: attrs["angle_to_geographic_north"] = self._format_float( orientation.angle_to_geographic_north ) _append_text( node, "Orientation", orientation.mode, attrs=attrs, ) if site is not None: _append_text(node, "AcquiredBy", site.acquired_by) _append_text(node, "Start", site.start) _append_text(node, "End", site.end) run_list = site.extra.get("run_list") if isinstance(run_list, (tuple, list)): run_list = " ".join(str(item) for item in run_list) _append_text(node, "RunList", run_list) self._append_quality(node, quality) if site is not None: comments = site.extra.get("comments", []) if not isinstance(comments, list): comments = [comments] for comment in comments: _append_text(node, "Comments", comment) def _append_location(self, parent: ET.Element, location: Any) -> None: if location is None: return attrs: dict[str, str] = {} if location.datum: attrs["datum"] = str(location.datum) node = ET.SubElement(parent, "Location", attrs) _append_text( node, "Latitude", self._format_float(location.latitude), ) _append_text( node, "Longitude", self._format_float(location.longitude), ) if location.elevation is not None: _append_text( node, "Elevation", self._format_float(location.elevation), attrs={"units": location.elevation_units}, ) if location.declination is not None: attrs = {} if location.declination_epoch is not None: attrs["epoch"] = self._format_float( location.declination_epoch ) _append_text( node, "Declination", self._format_float(location.declination), attrs=attrs, ) def _append_quality(self, parent: ET.Element, quality: Any) -> None: if quality is None: return if any( value is not None for value in ( quality.rating, quality.good_from_period, quality.good_to_period, ) ) or quality.comments: node = ET.SubElement(parent, "DataQualityNotes") _append_text(node, "Rating", quality.rating) _append_text( node, "GoodFromPeriod", self._format_float(quality.good_from_period), ) _append_text( node, "GoodToPeriod", self._format_float(quality.good_to_period), ) for comment in quality.comments: attrs = {"author": comment.author} if comment.author else {} _append_text(node, "Comments", comment.text, attrs=attrs) if quality.warning_flag is not None or quality.warnings: node = ET.SubElement(parent, "DataQualityWarnings") _append_text(node, "Flag", quality.warning_flag) for comment in quality.warnings: attrs = {"author": comment.author} if comment.author else {} _append_text(node, "Comments", comment.text, attrs=attrs) def _append_field_notes(self, root: ET.Element, document: EMTF) -> None: for key, raw in document.field_notes.items(): values = raw if isinstance(raw, list) else [raw] for value in values: node = ET.SubElement(root, "FieldNotes") _apply_mapping(node, value) if "run" not in node.attrib and key and not key.startswith( "run_" ): node.set("run", str(key)) def _append_processing(self, root: ET.Element, document: EMTF) -> None: processing = document.processing if processing is None: return node = ET.SubElement(root, "ProcessingInfo") _append_text( node, "SignConvention", self._xml_sign_convention(processing.sign_convention), ) remote = processing.remote_reference if remote is not None and remote.reference_type is not None: ET.SubElement( node, "RemoteRef", {"type": str(remote.reference_type)}, ) remote_info = processing.extra.get("remote_info") if remote_info is not None: _append_mapping(node, "RemoteInfo", remote_info) elif remote is not None and remote.site is not None: info = ET.SubElement(node, "RemoteInfo") site = ET.SubElement(info, "Site") _append_text(site, "Id", remote.site) _append_text(node, "ProcessedBy", processing.processed_by) _append_text( node, "ProcessDate", processing.extra.get("process_date"), ) if processing.software is not None: software = ET.SubElement(node, "ProcessingSoftware") _append_text(software, "Name", processing.software.name) _append_text(software, "LastMod", processing.software.release) author = processing.software.author _append_text( software, "Author", author.name if author is not None else None, ) _append_text(node, "ProcessingTag", processing.processing_tag) @staticmethod def _xml_sign_convention(value: str | None) -> str | None: if value == "exp(+i ω t)": return r"exp(+ i\omega t)" if value == "exp(-i ω t)": return r"exp(- i\omega t)" return value # ------------------------------------------------------------------ # Declarations # ------------------------------------------------------------------ def _data_type_specs(self, document: EMTF) -> list[dict[str, Any]]: stored = document.metadata.get("xml_data_types", []) stored_specs = [ dict(item) for item in stored if isinstance(item, dict) ] by_tag = { str(item.get("tag", "")).lower(): item for item in stored_specs if item.get("tag") } by_name = { str(item.get("name", "")).upper(): item for item in stored_specs if item.get("name") } result = list(stored_specs) known_keys = { ( str(item.get("name", "")).upper(), str(item.get("tag", "")).lower(), ) for item in result } for tf in document.transfer_functions.values(): xml_name = str(tf.attrs.get("xml_name") or "").upper() source = by_tag.get(tf.name.lower()) or by_name.get(xml_name) spec = ( dict(source) if source is not None else self._spec_from_tf(tf) ) key = ( str(spec.get("name", "")).upper(), str(spec.get("tag", "")).lower(), ) if key not in known_keys: result.append(spec) known_keys.add(key) return result def _spec_from_tf(self, tf: TransferFunction) -> dict[str, Any]: definition = tf.definition xml_name = tf.attrs.get("xml_name") if not xml_name and definition is not None: xml_name = definition.name if not xml_name: candidate = tf.name.upper() if _XML_NAME.match(candidate): xml_name = candidate if not xml_name or not _XML_NAME.match(str(xml_name)): self._problem( f"transfer function {tf.name!r} has no safe EMTF XML code" ) xml_name = "UNKNOWN" data_kind = tf.attrs.get("xml_data_kind") if not data_kind: if definition is not None: data_kind = definition.data_kind else: data_kind = "complex" if np.iscomplexobj(tf.data) else "real" return { "name": str(xml_name).upper(), "tag": tf.name, "data_kind": str(data_kind).lower(), "input_kind": tf.attrs.get("xml_input_kind") or (definition.input_kind if definition is not None else None), "output_kind": tf.attrs.get("xml_output_kind") or (definition.output_kind if definition is not None else None), "units": tf.units if tf.units is not None else (definition.units if definition is not None else None), "intention": tf.attrs.get("xml_intention") or (definition.intention if definition is not None else "primary"), "description": definition.description if definition else "", "derived_from": definition.derived_from if definition else None, "see_also": definition.see_also if definition else (), "external_url": None, } def _estimate_specs(self, document: EMTF) -> list[dict[str, Any]]: stored = document.metadata.get("xml_statistical_estimates", []) result = [dict(item) for item in stored if isinstance(item, dict)] known = { str(item.get("name", "")).upper() for item in result if item.get("name") } for tf in document.transfer_functions.values(): for estimate in tf.estimates.values(): code = estimate.name.upper() if code not in ESTIMATE_CODES: self._problem( "EMTF XML writer does not yet define a safe mapping " f"for statistical estimate {code!r} on {tf.name!r}" ) continue if code not in known: result.append(dict(_ESTIMATE_DEFAULTS[code])) known.add(code) return result def _append_estimate_declarations( self, root: ET.Element, specs: list[dict[str, Any]], ) -> None: if not specs: return parent = ET.SubElement(root, "StatisticalEstimates") for spec in specs: name = spec.get("name") if not name: continue attrs = {"name": str(name)} kind = spec.get("type") or spec.get("data_kind") if kind: attrs["type"] = str(kind) node = ET.SubElement(parent, "Estimate", attrs) _append_text(node, "Description", spec.get("description")) _append_text(node, "Intention", spec.get("intention")) _append_text(node, "Tag", spec.get("tag")) _append_text(node, "ExternalUrl", spec.get("external_url")) def _append_data_type_declarations( self, root: ET.Element, specs: list[dict[str, Any]], ) -> None: if not specs: return parent = ET.SubElement(root, "DataTypes") for spec in specs: name = spec.get("name") tag = spec.get("tag") if not name or not tag: self._problem("EMTF DataType declaration needs name and tag") continue attrs: dict[str, str] = {"name": str(name)} data_kind = spec.get("data_kind") or spec.get("type") if data_kind: attrs["type"] = str(data_kind) if spec.get("output_kind"): attrs["output"] = str(spec["output_kind"]) elif spec.get("output"): attrs["output"] = str(spec["output"]) if spec.get("input_kind"): attrs["input"] = str(spec["input_kind"]) elif spec.get("input"): attrs["input"] = str(spec["input"]) if spec.get("units"): attrs["units"] = str(spec["units"]) node = ET.SubElement(parent, "DataType", attrs) _append_text(node, "Description", spec.get("description")) intention = str(spec.get("intention") or "primary") if intention in {"primary", "derived"}: intention = f"{intention} data type" _append_text(node, "Intention", intention) _append_text(node, "Tag", tag) _append_text(node, "DerivedFrom", spec.get("derived_from")) see_also = spec.get("see_also") if isinstance(see_also, (tuple, list)): see_also = ",".join(str(item) for item in see_also if item) _append_text(node, "SeeAlso", see_also) _append_text(node, "ExternalUrl", spec.get("external_url")) # ------------------------------------------------------------------ # Site layout # ------------------------------------------------------------------ def _append_site_layout(self, root: ET.Element, document: EMTF) -> None: layout = document.site_layout if layout is None: return parent = ET.SubElement(root, "SiteLayout") self._append_channel_group( parent, "InputChannels", layout.input_channels, units=layout.input_units, reference=layout.input_reference, ) self._append_channel_group( parent, "OutputChannels", layout.output_channels, units=layout.output_units, reference=layout.output_reference, ) def _append_channel_group( self, parent: ET.Element, tag: str, channels: Iterable[ChannelMeta], *, units: str | None, reference: str | None, ) -> None: channel_list = list(channels) if not channel_list: return attrs: dict[str, str] = {} if reference: attrs["ref"] = reference if units: attrs["units"] = units node = ET.SubElement(parent, tag, attrs) for channel in channel_list: tag_name = ( "Electric" if channel.is_electric else "Magnetic" if channel.is_magnetic else "Channel" ) values: dict[str, Any] = { "name": channel.name, "orientation": channel.orientation, "tilt": channel.tilt, "x": channel.x, "y": channel.y, "z": channel.z, "x2": channel.x2, "y2": channel.y2, "z2": channel.z2, "units": channel.units, "ref": channel.reference, "id": channel.sensor_id, } values.update(channel.extra) attrs = { key: self._format_float(value) if isinstance(value, (float, int, np.floating, np.integer)) else str(value) for key, value in values.items() if value is not None } ET.SubElement(node, tag_name, attrs) # ------------------------------------------------------------------ # Data matrices # ------------------------------------------------------------------ def _append_data( self, root: ET.Element, document: EMTF, specs: list[dict[str, Any]], ) -> None: if document.is_empty(): return periods = document.periods if periods is None: self._problem( "cannot serialize transfer-function data without periods" ) return periods = np.asarray(periods, dtype=float) if periods.size != document.n_periods: self._problem("document period count is inconsistent") return spec_by_tag = { str(item.get("tag", "")).lower(): item for item in specs if item.get("tag") } parent = ET.SubElement(root, "Data", {"count": str(periods.size)}) for iper, period in enumerate(periods): pnode = ET.SubElement( parent, "Period", {"value": self._format_period_number(period), "units": "secs"}, ) for tf in document.transfer_functions.values(): spec = spec_by_tag.get(tf.name.lower()) if spec is None: spec = self._spec_from_tf(tf) self._append_tf_period(pnode, tf, spec, iper) def _append_tf_period( self, parent: ET.Element, tf: TransferFunction, spec: dict[str, Any], iper: int, ) -> None: code = str(spec.get("name") or "").upper() if not code or not _XML_NAME.match(code): self._problem(f"invalid XML data-type name {code!r}") return data_kind = str(spec.get("data_kind") or "complex").lower() self._append_matrix( parent, code, tf.data[iper], rows=tf.output_channels, cols=tf.input_channels, complex_=data_kind == "complex", units=tf.units or spec.get("units"), component_code=code, scalar=tf.n_output == 1 and tf.n_input == 1 and not tf.output_channels and not tf.input_channels, declared_size=(tf.n_output, tf.n_input), ) for estimate in tf.estimates.values(): est_code = estimate.name.upper() if est_code not in ESTIMATE_CODES: # The declaration path already issued a diagnostic. Avoid a # second warning in permissive mode. continue matrix = self._estimate_matrix(tf, estimate, est_code, iper) if matrix is None: continue if est_code == "VAR": rows = tf.output_channels cols = tf.input_channels complex_ = False scalar = not rows and not cols elif est_code == "INVSIGCOV": rows = tf.input_channels cols = tf.input_channels complex_ = True scalar = False else: rows = tf.output_channels cols = tf.output_channels complex_ = True scalar = False self._append_matrix( parent, f"{code}.{est_code}", matrix, rows=rows, cols=cols, complex_=complex_, units=estimate.units, component_code=code, scalar=scalar, # FCU v4.1 writes the parent TF size for covariance blocks. # The value channel attributes remain the source of truth. declared_size=(tf.n_output, tf.n_input), ) def _estimate_matrix( self, tf: TransferFunction, estimate: StatisticalEstimate, code: str, iper: int, ) -> np.ndarray | None: data = np.asarray(estimate.data) if data.ndim != 3: self._problem( f"estimate {code} on {tf.name!r} must be a 3-D array" ) return None if code == "VAR": expected = (tf.n_periods, tf.n_output, tf.n_input) elif code == "INVSIGCOV": expected = (tf.n_periods, tf.n_input, tf.n_input) else: expected = (tf.n_periods, tf.n_output, tf.n_output) if data.shape != expected: self._problem( f"estimate {code} on {tf.name!r} has shape {data.shape}; " f"expected {expected}" ) return None if code == "VAR" and np.iscomplexobj(data): imag = np.imag(data) if np.any(np.isfinite(imag) & (imag != 0.0)): self._problem( f"variance estimate on {tf.name!r} contains non-zero " "imaginary values" ) return None data = np.real(data) return np.asarray(data[iper]) def _append_matrix( self, parent: ET.Element, tag: str, matrix: np.ndarray, *, rows: tuple[str, ...], cols: tuple[str, ...], complex_: bool, units: str | None, component_code: str, scalar: bool, declared_size: tuple[int, int], ) -> None: matrix = np.asarray(matrix) values: list[tuple[int, int, complex | float]] = [] for row in range(matrix.shape[0]): for col in range(matrix.shape[1]): raw = matrix[row, col] if not self._is_finite(raw, complex_=complex_): if self._is_missing(raw): continue self._problem( f"{tag} contains a non-finite value at " f"[{row}, {col}]" ) continue values.append((row, col, raw)) if not values: return attrs = { "type": "complex" if complex_ else "real", "size": f"{declared_size[0]} {declared_size[1]}", } if units: attrs["units"] = str(units) node = ET.SubElement(parent, tag, attrs) for row, col, raw in values: value = ET.SubElement(node, "value") if scalar: value.set("name", component_code) else: out_name = rows[row] in_name = cols[col] if ".INVSIGCOV" not in tag and ".RESIDCOV" not in tag: value.set( "name", self._component_name( component_code, output=out_name, input_=in_name, ), ) value.set("output", out_name) value.set("input", in_name) value.text = self._format_numeric(raw, complex_=complex_) @staticmethod def _component_name(code: str, *, output: str, input_: str) -> str: def axis(name: str) -> str: lowered = str(name).strip().lower() for candidate in ("x", "y", "z"): if lowered.endswith(candidate): return candidate return lowered upper = code.upper() if upper in {"T", "TI"} and output.lower().endswith("z"): return f"{upper}{axis(input_)}" return f"{upper}{axis(output)}{axis(input_)}" # ------------------------------------------------------------------ # Period range / formatting # ------------------------------------------------------------------ def _append_period_range(self, root: ET.Element, document: EMTF) -> None: if document.periods is not None and len(document.periods): periods = np.asarray(document.periods, dtype=float) ET.SubElement( root, "PeriodRange", { "min": self._format_period_number( float(np.min(periods)) ), "max": self._format_period_number( float(np.max(periods)) ), }, ) return stored = document.metadata.get("period_range") if isinstance(stored, dict) and stored: ET.SubElement( root, "PeriodRange", {str(key): str(value) for key, value in stored.items()}, ) def _format_data_number(self, value: float) -> str: return format(float(value), f".{self.precision}g") @staticmethod def _format_period_number(value: float) -> str: # Periods are positive coordinate values rather than noisy matrix # coefficients. Fifteen significant digits avoid binary display # artefacts while preserving practical float64 period precision. return format(float(value), ".15g") @staticmethod def _format_float(value: Any | None) -> str | None: if value is None: return None return format(float(value), ".15g") def _format_numeric( self, value: complex | float, *, complex_: bool, ) -> str: if complex_: number = complex(value) return ( f"{self._format_data_number(number.real)} " f"{self._format_data_number(number.imag)}" ) return self._format_data_number(float(np.real(value))) @staticmethod def _is_missing(value: Any) -> bool: try: if np.iscomplexobj(value): return bool( np.isnan(np.real(value)) or np.isnan(np.imag(value)) ) return bool(np.isnan(value)) except TypeError: return False @staticmethod def _is_finite(value: Any, *, complex_: bool) -> bool: if complex_: number = complex(value) return bool(np.isfinite(number.real) and np.isfinite(number.imag)) try: return bool(np.isfinite(float(np.real(value)))) except (TypeError, ValueError): return False def _problem(self, message: str) -> None: if self.strict: raise EMTFXMLSerializationError(message) _warn(message)