Skip to content

Commit

Permalink
Load a string of TRNSYS components as a python Deck object (#59)
Browse files Browse the repository at this point in the history
* Adds load and loads to Deck class + refactors read_file method

* Adds functionality in readme.md

* Adds component string test

* passed kwargs

* strips whitespaces in input string

* Completes load and loads
  • Loading branch information
Samuel Letellier-Duchesne committed May 26, 2021
1 parent 45d8288 commit 978caef
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 27 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
```

38 changes: 38 additions & 0 deletions tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
117 changes: 91 additions & 26 deletions trnsystor/deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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":
Expand Down Expand Up @@ -397,15 +452,15 @@ 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":
# extract number of line, number of equations
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
Expand All @@ -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":
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion trnsystor/trnsysmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 978caef

Please sign in to comment.