Source code for fastoad.io.xml.variable_io_base

"""
Defines how OpenMDAO variables are serialized to XML using a conversion table
"""
#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
#  FAST is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.

import json
import logging
import re
import warnings
from typing import IO, Union

import numpy as np
from lxml import etree
from lxml.etree import (
    XPathEvalError,
    _Element,
)  # pylint: disable=protected-access  # Useful for type hinting
from openmdao.vectors.vector import Vector

from fastoad._utils.files import make_parent_dir
from fastoad._utils.strings import get_float_list_from_string
from fastoad.io.formatter import IVariableIOFormatter
from fastoad.io.xml.exceptions import (
    FastXPathEvalError,
    FastXmlFormatterDuplicateVariableError,
    FastXpathTranslatorVariableError,
    FastXpathTranslatorXPathError,
)
from fastoad.io.xml.translator import VarXpathTranslator
from fastoad.openmdao.variables import VariableList
from .constants import DEFAULT_IO_ATTRIBUTE, DEFAULT_UNIT_ATTRIBUTE, ROOT_TAG

_LOGGER = logging.getLogger(__name__)  # Logger for this module


[docs]class VariableXmlBaseFormatter(IVariableIOFormatter): """ Customizable formatter for variables User must provide at instantiation a VarXpathTranslator instance that tells how variable names should be converted from/to XPath. Note: XPath are always considered relatively to the root. Therefore, "foo/bar" should be provided to match following XML structure: .. code-block:: xml <root> <foo> <bar> "some value" </bar> </foo> </root> :param translator: the VarXpathTranslator instance """ def __init__(self, translator: VarXpathTranslator): self._translator = translator self.xml_unit_attribute = DEFAULT_UNIT_ATTRIBUTE self.xml_io_attribute = DEFAULT_IO_ATTRIBUTE self.unit_translation = { "²": "**2", "³": "**3", "°": "deg", "°C": "degC", "kt": "kn", "\bin\b": "inch", } """ Used for converting read units in units recognized by OpenMDAO Dict keys can use regular expressions. """
[docs] def set_translator(self, translator: VarXpathTranslator): """ Sets the VarXpathTranslator() instance that rules how OpenMDAO variable are matched to XML Path. :param translator: """ warnings.warn("provide translator at instantiation", DeprecationWarning) self._translator = translator
[docs] def read_variables(self, data_source: Union[str, IO]) -> VariableList: variables = VariableList() parser = etree.XMLParser(remove_blank_text=True, remove_comments=True) tree = etree.parse(data_source, parser) root = tree.getroot() for elem in root.iter(): units = elem.attrib.get(self.xml_unit_attribute, None) is_input = elem.attrib.get(self.xml_io_attribute, None) if units: # Ensures compatibility with OpenMDAO units for legacy_chars, om_chars in self.unit_translation.items(): units = re.sub(legacy_chars, om_chars, units) units = units.replace(legacy_chars, om_chars) value = None if elem.text: value = get_float_list_from_string(elem.text) if value is not None: try: path_tags = [ancestor.tag for ancestor in elem.iterancestors()] path_tags.reverse() path_tags.append(elem.tag) xpath = "/".join(path_tags[1:]) # Do not use root tag name = self._translator.get_variable_name(xpath) except FastXpathTranslatorXPathError as err: _LOGGER.warning( "The xpath %s does not have any variable " "affected in the translator.", err.xpath, ) continue if name not in variables.names(): # Add Variable if is_input is not None: is_input = is_input == "True" variables[name] = {"value": value, "units": units, "is_input": is_input} else: raise FastXmlFormatterDuplicateVariableError( "Variable %s is defined in more than one place in file %s" % (name, data_source) ) return variables
[docs] def write_variables(self, data_source: Union[str, IO], variables: VariableList): root = etree.Element(ROOT_TAG) for variable in variables: try: xpath = self._translator.get_xpath(variable.name) except FastXpathTranslatorVariableError as exc: _LOGGER.warning("No translation found: %s", exc) continue element = self._create_xpath(root, xpath) # Set value, units and io if variable.units: element.attrib[self.xml_unit_attribute] = variable.units if variable.is_input is not None: element.attrib[self.xml_io_attribute] = str(variable.is_input) # Filling value for already created element element.text = str(variable.value) if not isinstance(variable.value, (np.ndarray, Vector, list)): # Here, it should be a float element.text = str(variable.value) elif len(np.squeeze(variable.value).shape) == 0: element.text = str(np.squeeze(variable.value).item()) else: element.text = json.dumps(np.asarray(variable.value).tolist()) if variable.description: element.append(etree.Comment(variable.description)) # Write tree = etree.ElementTree(root) make_parent_dir(data_source) tree.write(data_source, pretty_print=True)
@staticmethod def _create_xpath(root: _Element, xpath: str) -> _Element: """ Creates required XML Path from provided root element :param root: :param xpath: :return: created element """ if xpath.startswith("/"): xpath = xpath[1:] # needed to avoid empty string at first place after split path_components = xpath.split("/") element = root children = [] # Create XML path if needed for path_component in path_components: try: children = element.xpath(path_component) except XPathEvalError: raise FastXPathEvalError('Could not resolve XPath "%s"' % path_component) if not children: # Build path new_element = etree.Element(path_component) element.append(new_element) element = new_element else: # Use existing path # (Unicity of OpenMDAO variables makes that children should not have more that # one element) element = children[0] return element