Source code for exosim.tasks.load.load_options

import os
import xml.etree.ElementTree as ET
from collections import OrderedDict

import yaml
from astropy import units as u
from astropy.table import Table

from exosim.tasks.task import Task
from exosim.utils.run_config import RunConfig


[docs] class LoadOptions(Task): """ Reads the XML file with payload parameters and returns an object with attributes related to the input data. Attributes ---------- config_path : str Configuration path. Returns ------- dict Parsed XML input file. Raises ------ IOError If the indicated file is not found or the format is not supported. Examples -------- >>> load_options = LoadOptions() >>> options = load_options(filename="path/to/file.xml") """ def __init__(self): super().__init__() # Explicitly call the parent class constructor self.add_task_param("filename", "Input option file name") self.add_task_param("config_path", "On-run setting for ConfigPath", None)
[docs] def execute(self): self._filename = self.get_task_param("filename") config_path_param = self.get_task_param("config_path") self.config_path = config_path_param or "__ConfigPath__" # Track if config_path was explicitly set (not default) self._config_path_from_param = config_path_param is not None RunConfig.config_file_list.append(os.path.abspath(self._filename)) self._check_format() root = self._get_root() options_dict = self._parse_data(root) self._finalise(options_dict) options_dict = self._substitute_keywords(options_dict) options_dict = _clean_dict(options_dict) self.set_output(options_dict)
def _parse_data(self, root): """Parse XML or YAML data structure into a unified dictionary format. Parameters ---------- root : ElementTree.Element or dict Root element from XML or dict from YAML Returns ------- dict Parsed data in unified format """ if isinstance(root, dict): # YAML input return self._parse_yaml(root) # XML input return self._parse_xml(root) def _parse_yaml(self, data): """Parse YAML dict to match the XML format. Parameters ---------- data : dict YAML data structure Returns ------- dict Converted data structure """ result = {} for key, value in data.items(): if isinstance(value, dict): child_dict = self._parse_yaml(value) if "value" in value and "unit" in value: unit_name = value["unit"] val = value["value"] if isinstance(val, str): val = self._convert_value(val) if unit_name == "dimensionless": unit_name = "" if unit_name: val = val * u.Unit(unit_name) child_dict["value"] = val result[key] = child_dict else: result[key] = {"value": value} if isinstance(value, str): result[key]["value"] = self._convert_value(value) return result def _check_format(self): if not ( self._filename.endswith(".xml") or self._filename.endswith(".yaml") or self._filename.endswith(".yml") ): self.error("Wrong input format: Expected an XML or YAML file.") raise OSError("Wrong input format: Expected an XML or YAML file.") def _get_root(self): try: self.debug(f"Input option file found: {self._filename}") if self._filename.endswith(".yaml") or self._filename.endswith(".yml"): with open(self._filename) as f: return yaml.safe_load(f) else: return ET.parse(self._filename).getroot() except FileNotFoundError: self.error(f"No input option file found: {self._filename}") raise except (yaml.YAMLError, ET.ParseError) as e: self.error(f"Error parsing {self._filename}: {e!s}") raise OSError(f"Error parsing {self._filename}: {e!s}") from e def _parse_xml(self, root): root_dict = {} for child in root: child_dict = self._parse_xml(child) child_dict.update(child.attrib) value = self._compact_string(child.text) if value: value = self._convert_value(value) if "unit" in child_dict: unit_name = child_dict.pop("unit") if unit_name == "dimensionless": unit_name = "" value = value * u.Unit(unit_name) if isinstance(value, str) and "__ConfigPath__" in value: value = value.replace("__ConfigPath__", self.config_path) child_dict["value"] = value # Only update config_path from XML if it wasn't explicitly set as parameter if child.tag == "ConfigPath" and not self._config_path_from_param: self.config_path = value if child.tag in root_dict: existing_attr = root_dict[child.tag] if isinstance(existing_attr, OrderedDict): existing_attr[value] = child_dict else: dtmp = OrderedDict( [ (existing_attr.get("value"), existing_attr), (value, child_dict), ] ) root_dict[child.tag] = dtmp else: root_dict[child.tag] = child_dict if "datafile" in root_dict: datafile = root_dict["datafile"]["value"].replace( "__ConfigPath__", self.config_path ) if not os.path.exists(datafile): self.error(f"Datafile not found: {datafile}") raise FileNotFoundError(f"Datafile not found: {datafile}") try: root_dict["data"] = self._read_data_table(datafile) except OSError: self.error(f"Cannot read input file: {datafile}") raise OSError("Error reading the input data file.") from None return root_dict def _substitute_keywords(self, root_dict): """ Substitutes keywords in the root_dict with their corresponding values. Parameters ---------- root_dict : dict The dictionary parsed from the XML. Returns ------- dict Updated dictionary with substituted keywords. """ # Extract keywords (elements starting with '__') keywords = { key.strip("_"): value["value"] for key, value in root_dict.items() if isinstance(value, dict) and "value" in value and key.startswith("__") } def substitute_value(value): if isinstance(value, str): for keyword, replacement in keywords.items(): value = value.replace(f"__{keyword}__", replacement) return value def recursive_substitute(d): for key, value in d.items(): if isinstance(value, dict): recursive_substitute(value) elif isinstance(value, str): d[key] = substitute_value(value) recursive_substitute(root_dict) return root_dict def _finalise(self, dictionary): xml_entry = dictionary.pop("config", None) if xml_entry: xmlfile = xml_entry["value"].replace("__ConfigPath__", self.config_path) xmlfile = os.path.expanduser(xmlfile) if not os.path.exists(xmlfile): self.error(f"Referenced config file not found: {xmlfile}") raise FileNotFoundError(f"Referenced config file not found: {xmlfile}") sub_system_dict = LoadOptions()( filename=xmlfile, config_path=self.config_path ) dictionary.update(sub_system_dict) for item in dictionary.values(): if isinstance(item, dict): self._finalise(item) def _compact_string(self, string): return string.replace("\n", "").strip() if string else "" def _convert_value(self, value): try: return int(value) except ValueError: try: return float(value) except ValueError: return self._convert_boolean(value) def _convert_boolean(self, value): if value == "True": return True if value == "False": return False return value def _read_data_table(self, datafile): data_type = os.path.splitext(datafile)[1] try: data = Table.read( os.path.expanduser(datafile), fill_values=[("#N/A", "0"), ("N/A", "0"), ("", "0")], format="ascii" + data_type, ) except Exception as exc: raise Exception(f"{datafile} caused the exception") from exc for col in data.columns: if hasattr(data[col], "fill_value"): data[col].fill_value = 0.0 return data
def _clean_dict(input_dict): """ Cleans an input dictionary by removing the "value" notation and comments. It can be applied recursively. Parameters ---------- input_dict : dict The dictionary to clean. Returns ------- dict Cleaned dictionary. """ input_dict.pop("comment", None) for key in list(input_dict.keys()): if isinstance(input_dict[key], dict): keys_list = list(input_dict[key].keys()) if keys_list == ["unit", "value"] or keys_list == ["value"]: input_dict[key] = input_dict[key]["value"] else: _clean_dict(input_dict[key]) return input_dict