diff --git a/README.md b/README.md index 8aff7e3..93daf59 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,54 @@ element. In this case a list is returned: [Inlet Fluid Temperature - Pipe 1; units=C; value=15.0 °C The temperature of the fluid flowing into the first buried horizontal pipe., Inlet Fluid Flowrate - Pipe 1; units=(kg)/(hr); value=0.0 kg/hr The flowrate of fluid into the first buried horizontal pipe.] +``` + +## Parsing string snippets + +Since version 1.4, it is possible to parse string snippets of TRNSYS components. +Deck.load() and Deck.loads() (similarly to json.load and json.loads for users who are +familiar with json deserializing in python). + +For example, one can load the following string into a Deck object: + +```pythonstub +from trnsystor import Deck +s = r""" +UNIT 3 TYPE 11 Tee Piece +*$UNIT_NAME Tee Piece +*$MODEL district\xmltypes\Type11h.xml +*$POSITION 50.0 50.0 +*$LAYER Main +PARAMETERS 1 +1 ! 1 Tee piece mode +INPUTS 4 +0,0 ! [unconnected] Tee Piece:Temperature at inlet 1 +flowRateDoubled ! double:flowRateDoubled -> Tee Piece:Flow rate at inlet 1 +0,0 ! [unconnected] Tee Piece:Temperature at inlet 2 +0,0 ! [unconnected] Tee Piece:Flow rate at inlet 2 +*** INITIAL INPUT VALUES +20 ! Temperature at inlet 1 +100 ! Flow rate at inlet 1 +20 ! Temperature at inlet 2 +100 ! Flow rate at inlet 2 + +* EQUATIONS "double" +* +EQUATIONS 1 +flowRateDoubled = 2*[1, 2] +*$UNIT_NAME double +*$LAYER Main +*$POSITION 50.0 50.0 +*$UNIT_NUMBER 2 +""" +dck = Deck.loads(s, proforma_root="tests/input_files") +``` +If the same string was in a file, it could be as easily parsed using Deck.load(): + +```pydocstring +>>> from trnsystor import Deck +>>> with open("file.txt", "r") as fp: +>>> dck = Deck.load(fp, proforma_root="tests/input_files") ``` diff --git a/tests/test_xml.py b/tests/test_xml.py index 75fe51d..2b15c0f 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -871,6 +871,44 @@ def test_print(self, deck_file, pvt_deck): def test_save(self, pvt_deck): pvt_deck.to_file("test.dck", None, "w") + @pytest.fixture() + def components_string(self): + yield r""" + UNIT 3 TYPE 11 Tee Piece + *$UNIT_NAME Tee Piece + *$MODEL tests\input_files\Type11h.xml + *$POSITION 50.0 50.0 + *$LAYER Main + PARAMETERS 1 + 1 ! 1 Tee piece mode + INPUTS 4 + 0,0 ! [unconnected] Tee Piece:Temperature at inlet 1 + flowRateDoubled ! double:flowRateDoubled -> Tee Piece:Flow rate at inlet 1 + 0,0 ! [unconnected] Tee Piece:Temperature at inlet 2 + 0,0 ! [unconnected] Tee Piece:Flow rate at inlet 2 + *** INITIAL INPUT VALUES + 20 ! Temperature at inlet 1 + 100 ! Flow rate at inlet 1 + 20 ! Temperature at inlet 2 + 100 ! Flow rate at inlet 2 + + * EQUATIONS "double" + * + EQUATIONS 1 + flowRateDoubled = 2*[1, 2] + *$UNIT_NAME double + *$LAYER Main + *$POSITION 50.0 50.0 + *$UNIT_NUMBER 2 + """ + + @pytest.mark.parametrize("proforma_root", [None, "tests/input_files"]) + def test_load(self, components_string, proforma_root): + from trnsystor import Deck + + dck = Deck.loads(components_string, proforma_root=proforma_root) + assert len(dck.models) == 2 + class TestComponent: def test_unique_hash(self, fan_type): diff --git a/trnsystor/deck.py b/trnsystor/deck.py index 27ad484..539a3b8 100644 --- a/trnsystor/deck.py +++ b/trnsystor/deck.py @@ -2,10 +2,13 @@ import datetime import itertools +import json import logging as lg +import os import re import tempfile from io import StringIO +from typing import Union from pandas import to_datetime from pandas.io.common import _get_filepath_or_buffer, get_handle @@ -146,7 +149,9 @@ def __init__( ) @classmethod - def read_file(cls, file, author=None, date_created=None, proforma_root=None): + def read_file( + cls, file, author=None, date_created=None, proforma_root=None, **kwargs + ): """Returns a Deck from a file. Args: @@ -157,28 +162,18 @@ def read_file(cls, file, author=None, date_created=None, proforma_root=None): datetime.datetime.now(). proforma_root (str): Either the absolute or relative path to the folder where proformas (in xml format) are stored. + **kwargs: Keywords passed to the Deck constructor. """ file = Path(file) with open(file) as dcklines: - cc = ControlCards() - dck = cls( + dck = cls.load( + dcklines, + proforma_root, name=file.basename(), author=author, date_created=date_created, - control_cards=cc, + **kwargs ) - no_whitelines = list(filter(None, (line.rstrip() for line in dcklines))) - with tempfile.TemporaryFile("r+") as dcklines: - dcklines.writelines("\n".join(no_whitelines)) - dcklines.seek(0) - line = dcklines.readline() - # parse whole file once - cls._parse_logic(cc, dck, dcklines, line, proforma_root) - - # parse a second time to complete links - dcklines.seek(0) - line = dcklines.readline() - cls._parse_logic(cc, dck, dcklines, line, proforma_root) # assert missing types # todo: list types that could not be parsed @@ -331,7 +326,67 @@ def _to_string(self): return "\n".join([cc, models, end]) + styles @classmethod - def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): + def load(cls, fp, proforma_root=None, dck=None, **kwargs): + """Deserialize fp as a Deck object. + + Args: + + fp (SupportsRead[Union[str, bytes]]): a ``.read()``-supporting file-like + object containing a Component. + proforma_root (Union[str, os.PathLike]): The path to a directory of xml + proformas. + dck (Deck): Optionally pass a Deck object to act upon it. This is used in Deck.read_file where + **kwargs: Keywords passed to the Deck constructor. + + Returns: + (Deck): A Deck object containing the parsed TrnsysModel objects. + """ + return cls.loads(fp.read(), proforma_root=proforma_root, dck=dck, **kwargs) + + @classmethod + def loads(cls, s, proforma_root=None, dck=None, **kwargs): + """Deserialize ``s`` to a Python object. + + Args: + dck: + s (Union[str, bytes]): a ``str``, ``bytes`` or ``bytearray`` + instance containing a TRNSYS Component. + proforma_root (Union[str, os.PathLike]): The path to a directory of xml + proformas. + + Returns: + (Deck): A Deck object containing the parsed TrnsysModel objects. + """ + # prep model + cc = ControlCards() + if dck is None: + dck = cls(control_cards=cc, name=kwargs.pop("name", "unnamed"), **kwargs) + + # decode string of bytes, bytearray + if isinstance(s, str): + pass + else: + if not isinstance(s, (bytes, bytearray)): + raise TypeError( + f"the DCK object must be str, bytes or bytearray, " + f"not {s.__class__.__name__}" + ) + s = s.decode(json.detect_encoding(s), "surrogatepass") + # Remove empty lines from string + s = os.linesep.join([s.strip() for s in s.splitlines() if s]) + + # First pass + cls._parse_string(cc, dck, proforma_root, s) + + # parse a second time to complete links using previous dck object. + cls._parse_string(cc, dck, proforma_root, s) + return dck + + @classmethod + def _parse_string(cls, cc, dck, proforma_root, s): + # iterate + deck_lines = iter(s.splitlines()) + line = next(deck_lines) if proforma_root is None: proforma_root = Path.getcwd() else: @@ -351,7 +406,7 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): n_cnts = match.group(key) cb = ConstantCollection() for n in range(int(n_cnts)): - line = next(iter(dcklines)) + line = next(deck_lines) cb.update(Constant.from_expression(line)) cc.set_statement(cb) if key == "simulation": @@ -397,7 +452,7 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): k = match.group(key) cc.set_statement(EqSolver(*k.strip().split())) if key == "userconstants": - line = dcklines.readline() + line = next(deck_lines) key, match = dck._parse_line(line) # identify an equation block (EquationCollection) if key == "equations": @@ -405,7 +460,7 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): n_equations = match.group("equations") # read each line of the table until a blank line list_eq = [] - for line in [next(iter(dcklines)) for x in range(int(n_equations))]: + for line in [next(deck_lines) for x in range(int(n_equations))]: # extract number and value if line == "\n": continue @@ -421,8 +476,13 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): print("Empty UserConstants block") # read studio markup if key == "unitnumber": - dck.remove_models(component) unit_number = match.group(key) + try: + model_ = dck.models.iloc[unit_number] + except KeyError: + pass + else: + dck.models.pop(model_) component._unit = int(unit_number) dck.update_models(component) if key == "unitname": @@ -466,7 +526,7 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): n_vars = n_vars * 2 i = 0 while line: - line = dcklines.readline() + line = next(deck_lines) if not line.strip(): line = "\n" else: @@ -499,7 +559,7 @@ def _parse_logic(cls, cc, dck, dcklines, line, proforma_root): # identify u,v unit numbers u, v = match.group(key).strip().split(":") - line = dcklines.readline() + line = next(deck_lines) key, match = dck._parse_line(line) # identify linkstyle attributes @@ -560,14 +620,19 @@ def distance(a, b): if not xml: raise ValueError( f"The proforma {xml_basename} could not be found " - f"at {proforma_root}" + f"at '{proforma_root}'\nnor at '{tmf.dirname()}' as " + f"specified in the input string." ) meta = MetaData.from_xml(xml) if isinstance(component, TrnsysModel): + if component._meta is None: + component._meta = meta component.update_meta(meta) - line = dcklines.readline() - return line + try: + line = next(deck_lines) + except StopIteration: + line = None def return_equation_or_constant(self, name): """Return Equation or Constant for name. diff --git a/trnsystor/trnsysmodel.py b/trnsystor/trnsysmodel.py index 8b5ad5d..2a79d2f 100644 --- a/trnsystor/trnsysmodel.py +++ b/trnsystor/trnsysmodel.py @@ -107,7 +107,7 @@ class handling ExternalFiles for this object. self.organization = organization self.editor = editor self.creationDate = creationDate - self.modifictionDate = modifictionDate + self.modifictionDate = modifictionDate # has a typo in proforma xml self.mode = mode self.validation = validation self.icon = icon