Source code for trnsystor.trnsysmodel

"""TrnsysModel module."""
import collections
import copy
import itertools

import networkx as nx
from bs4 import BeautifulSoup, Tag
from path import Path

from trnsystor.anchorpoint import AnchorPoint
from trnsystor.collections.cycle import CycleCollection
from trnsystor.collections.derivatives import DerivativesCollection
from trnsystor.collections.externalfile import ExternalFileCollection
from trnsystor.collections.initialinputvalues import InitialInputValuesCollection
from trnsystor.collections.input import InputCollection
from trnsystor.collections.output import OutputCollection
from trnsystor.collections.parameter import ParameterCollection
from trnsystor.collections.specialcards import SpecialCardsCollection
from trnsystor.component import Component
from trnsystor.externalfile import ExternalFile
from trnsystor.specialcard import SpecialCard
from trnsystor.studio import StudioHeader
from trnsystor.typecycle import TypeCycle
from trnsystor.typevariable import Derivative, Input, Output, Parameter, TypeVariable


[docs]class MetaData(object): """General information that is associated with a :class:`TrnsysModel`.""" def __init__( self, object=None, author=None, organization=None, editor=None, creationDate=None, modifictionDate=None, mode=None, validation=None, icon=None, type=None, maxInstance=None, keywords=None, details=None, comment=None, variables=None, plugin=None, variablesComment=None, cycles=None, source=None, externalFiles=None, compileCommand=None, model=None, specialCards=None, **kwargs, ): """Initialize object with arguments. This information is contained in the General Tab of the Proforma. Args: object (str): A generic name describing the component model. author (str): The name of the person who wrote the model. organization (str): The name of organization with which the Author is affiliated. editor (str): Often, the person creating the Simulation Studio Proforma is not the original author and so the name of the Editor may also be important. creationDate (str): This is the date of when the model was first written. modifictionDate (str): This is the date when the Proforma was mostly recently revised. mode (int): 1-Detailed, 2-Simplified, 3-Empirical, 4- Conventional validation (int): Determine the type of validation that was performed on this model. This can be 1-qualitative, 2-numerical, 3-analytical, 4-experimental and 5-‘in assembly’ meaning that it was verified as part of a larger system which was verified. icon (Path): Path to the icon. type (int): The type number. maxInstance (int): The maximum number of instances this type can be used. keywords (str): keywords associated with this model. details (str): The detailed description contains an explanation of the model including a mathematical description of the model comment (str): The text entered here will appear as a comment in the TRNSYS input file. This allows to attach important information about the component to all its users, including users who prefer to edit the input file with a text editor. This text should be short, to avoid overloading the input file. variables (dict, optional): a list of :class:`TypeVariable`. plugin (Path): The plug-in path contains the path to the an external application which will be executed to modify component properties instead of the classical properties window. variablesComment (str): #todo What is this? cycles (list, optional): List of TypeCycle. source (Path): Path of the source code. externalFiles (trnsystor.external_file.ExternalFileCollection): A class handling ExternalFiles for this object. compileCommand (str): Command used to recompile this type. model (Path): Path of the xml or tmf file. specialCards (list of SpecialCards): List of SpecialCards. **kwargs: Other keyword arguments passed to the constructor. """ self.compileCommand = compileCommand self.object = object self.author = author self.organization = organization self.editor = editor self.creationDate = creationDate self.modifictionDate = modifictionDate self.mode = mode self.validation = validation self.icon = icon self.type = type self.maxInstance = maxInstance self.keywords = keywords self.details = details self.comment = comment self.variablesComment = variablesComment self.plugin = plugin self.cycles = cycles self.source = source self.model = model self.variables = variables self.external_files = externalFiles self.special_cards = specialCards self.check_extra_tags(kwargs)
[docs] @classmethod def from_tag(cls, tag, **kwargs): """Create a TrnsysModel from an xml tag. Args: tag (Tag): The XML tag with its attributes and contents. **kwargs: """ meta_args = { child.name: child.__copy__() for child in tag.children if isinstance(child, Tag) } meta_args.update(kwargs) return cls(**{attr: meta_args[attr] for attr in meta_args})
[docs] def check_extra_tags(self, kwargs): """Detect extra tags in the proforma and warn. Args: kwargs (dict): dictionary of extra keyword-arguments that would be passed to the constructor. """ if kwargs: msg = ( "Unknown tags have been detected in this proforma: {}.\nIf " "you wish to continue, the behavior of the object might be " "affected. Please contact the package developers or submit " "an issue.\n Do you wish to continue anyways?".format( ", ".join(kwargs.keys()) ) ) shall = input("%s (y/N) " % msg).lower() == "y" if not shall: raise NotImplementedError()
def __getitem__(self, item): """Get item. self[item].""" return getattr(self, item)
[docs] @classmethod def from_xml(cls, xml, **kwargs): """Initialize MetaData from xml file.""" xml_file = Path(xml) with open(xml_file) as xml: soup = BeautifulSoup(xml, "xml") my_objects = soup.findAll("TrnsysModel") for trnsystype in my_objects: kwargs.pop("name", None) meta = cls.from_tag(trnsystype, **kwargs) return meta
[docs]class TrnsysModel(Component): """TrnsysModel class.""" def __init__(self, meta, name): """Initialize object. Alone, this __init__ method does not do much. See the :func:`from_xml` class method for the official constructor of this class. Args: meta (MetaData): A class containing the model's metadata. name (str): A user-defined name for this model. """ super().__init__(name=name, meta=meta) def __str__(self): """Return repr(self).""" return f"[{self.unit_number}]Type{self.type_number}: {self.name}" def __repr__(self): """Return repr(self).""" return f"[{self.unit_number}]Type{self.type_number}: {self.name}"
[docs] @classmethod def from_xml(cls, xml, **kwargs): """Class method to create a :class:`TrnsysModel` from an xml string. Examples: Simply pass the xml path to the constructor. >>> from trnsystor import TrnsysModel >>> fan1 = TrnsysModel.from_xml("Tests/input_files/Type146.xml") Args: xml (str or Path): The path of the xml file. **kwargs: Returns: TrnsysType: The TRNSYS model. """ xml_file = Path(xml) with open(xml_file) as xml: all_types = [] soup = BeautifulSoup(xml, "xml") my_objects = soup.findAll("TrnsysModel") for trnsystype in my_objects: t = cls._from_tag(trnsystype, **kwargs) t._meta.model = xml_file t.studio = StudioHeader.from_component(t) all_types.append(t) return all_types[0]
[docs] def copy(self, invalidate_connections=True): """Copy object. The new object has a new unit_number. The new object is translated by 50 pts to the right on the canvas. Args: invalidate_connections (bool): If True, connections to other models will be reset. """ new = copy.deepcopy(self) new._unit = next(new.INIT_UNIT_NUMBER) new.UNIT_GRAPH.add_node(new) if invalidate_connections: new.invalidate_connections() from shapely.affinity import translate pt = translate(self.centroid, 50, 0) new.set_canvas_position(pt) return new
@property def derivatives(self) -> DerivativesCollection: """Return derivatives of self.""" return self._get_derivatives() @property def special_cards(self) -> SpecialCardsCollection: """Return special cards of self.""" return self._get_special_cards() @property def initial_input_values(self) -> InitialInputValuesCollection: """Return initial input values of self.""" return self._get_initial_input_values() @property def parameters(self) -> ParameterCollection: """Return parameters of self.""" return self._get_parameters() @property def external_files(self) -> ExternalFileCollection: """Return external files of self.""" return self._get_external_files() @property def anchor_points(self) -> dict: """Return the 8-AnchorPoints as a dict. The anchor point location ('top-left', etc.) is the key. """ return AnchorPoint(self).anchor_points @property def reverse_anchor_points(self): """Reverse anchor points.""" return AnchorPoint(self).reverse_anchor_points @classmethod def _from_tag(cls, tag, **kwargs): """Class method to create a :class:`TrnsysModel` from a tag. Args: tag (Tag): The XML tag with its attributes and contents. **kwargs: Returns: TrnsysModel: The TRNSYS model. """ name = kwargs.pop("name", tag.find("object").text) meta = MetaData.from_tag(tag, **kwargs) model = cls(meta, name) type_vars = [ TypeVariable.from_tag(tag, model=model) for tag in tag.find("variables") if isinstance(tag, Tag) ] type_cycles = CycleCollection( TypeCycle.from_tag(tag) for tag in tag.find("cycles").children if isinstance(tag, Tag) ) special_cards = ( [ SpecialCard.from_tag(tag) for tag in tag.find("specialCards").children if isinstance(tag, Tag) ] if tag.find("specialCards") else None ) model._meta.variables = {id(var): var for var in type_vars} model._meta.cycles = type_cycles model._meta.special_cards = ( {id(var): var for var in special_cards} if special_cards else None ) file_vars = ( [ ExternalFile.from_tag(tag) for tag in tag.find("externalFiles").children if isinstance(tag, Tag) ] if tag.find("externalFiles") else None ) model._meta.external_files = ( {id(var): var for var in file_vars} if file_vars else None ) model._get_inputs() model._get_outputs() model._get_parameters() model._get_external_files() return model def _get_initial_input_values(self): """Get initial input values.""" try: self._resolve_cycles("input", Input) input_dict = self._get_ordered_filtered_types(Input, "variables") # filter out cyclebases input_dict = { k: v for k, v in input_dict.items() if v._iscyclebase is False } return InitialInputValuesCollection.from_dict(input_dict) except TypeError: return InitialInputValuesCollection() def _get_inputs(self): """Get inputs. Sorts by order number and resolves cycles each time it is called. """ try: self._resolve_cycles("input", Input) input_dict = self._get_ordered_filtered_types(Input, "variables") # filter out cyclebases input_dict = { k: v for k, v in input_dict.items() if v._iscyclebase is False } return InputCollection.from_dict(input_dict) except TypeError: return InputCollection() def _get_outputs(self): """Sorts by order number and resolves cycles each time it is called.""" # output_dict = self._get_ordered_filtered_types(Output) try: self._resolve_cycles("output", Output) output_dict = self._get_ordered_filtered_types(Output, "variables") # filter out cyclebases output_dict = { k: v for k, v in output_dict.items() if v._iscyclebase is False } return OutputCollection.from_dict(output_dict) except TypeError: return OutputCollection() def _get_parameters(self): """Sorts by order number and resolves cycles each time it is called.""" self._resolve_cycles("parameter", Parameter) param_dict = self._get_ordered_filtered_types(Parameter, "variables") # filter out cyclebases param_dict = {k: v for k, v in param_dict.items() if v._iscyclebase is False} return ParameterCollection.from_dict(param_dict) def _get_derivatives(self): self._resolve_cycles("derivative", Derivative) deriv_dict = self._get_ordered_filtered_types(Derivative, "variables") # filter out cyclebases deriv_dict = {k: v for k, v in deriv_dict.items() if v._iscyclebase is False} return DerivativesCollection.from_dict(deriv_dict) def _get_special_cards(self): if self._meta.special_cards: special_cards_list = list( self._meta["special_cards"][attr] for attr in self._get_filtered_types(SpecialCard, "special_cards") ) return SpecialCardsCollection(special_cards_list) else: return SpecialCardsCollection() # return empty collection def _get_external_files(self): if self._meta.external_files: ext_files_dict = dict( (attr, self._meta["external_files"][attr]) for attr in self._get_filtered_types(ExternalFile, "external_files") ) return ExternalFileCollection.from_dict(ext_files_dict) else: return ExternalFileCollection() # return empty collection def _get_ordered_filtered_types(self, class_name, store): """Return an ordered dict of :class:`TypeVariable`. Filtered by *class_name* and ordered by their order number attribute. Args: class_name: Name of TypeVariable to filer: Choices are :class:`Input`, :class:`Output`, :class:`Parameter`, :class:`Derivative`. store (str): Attribute name from :class:`MetaData`. Typically, this is the "variables" attribute. """ return collections.OrderedDict( (attr, self._meta[store][attr]) for attr in sorted( self._get_filtered_types(class_name, store), key=lambda key: self._meta[store][key].order, ) ) def _get_filtered_types(self, class_name, store): """Return a filter of TypeVariables from the self._meta[store] by *class_name*. Args: class_name: Name of TypeVariable to filer: Choices are :class:`Input`, :class:`Output`, :class:`Parameter`, :class:`Derivative`. store (str): Attribute name from :class:`MetaData`. Typically, this is the "variables" attribute. """ return filter( lambda kv: isinstance(self._meta[store][kv], class_name), self._meta[store] ) def _resolve_cycles(self, type_, class_): """Cycle resolver. Proformas can contain parameters, inputs and outputs that have a variable number of entries. This will deal with their creation each time the linked parameters are changed. """ output_dict = self._get_ordered_filtered_types(class_, "variables") cycles = { str(id(attr)): attr for attr in self._meta.cycles if attr.role == type_ } # repeat cycle variables n times cycle: TypeCycle for _, cycle in cycles.items(): idxs = cycle.idxs # get list of variables that are not cycles items = [ output_dict.get(id(key)) for key in [ [i for i in output_dict.values() if not i._iscycle][i] for i in idxs ] ] if cycle.is_question: n_times = [] for cycle in cycle.cycles: existing = next( ( key for key, value in output_dict.items() if value.name == cycle.question ), None, ) if not existing: name = cycle.question question_var: TypeVariable = class_( val=cycle.default, name=name, role=cycle.role, unit="-", type=int, dimension="any", min=int(cycle.minSize), max=int(cycle.maxSize), order=9999999, default=cycle.default, model=self, ) question_var._is_question = True self._meta.variables.update({id(question_var): question_var}) output_dict.update({id(question_var): question_var}) n_times.append(question_var.value.m) else: n_times.append(output_dict[existing].value.m) else: n_times = [ list( filter( lambda elem: elem[1].name == cycle.paramName, self._meta.variables.items(), ) )[0][1].value.m for cycle in cycle.cycles ] item: TypeVariable mydict = { key: self._meta.variables.pop(key) for key in dict( filter( lambda kv: kv[1].role == type_ and kv[1]._iscycle, self._meta.variables.items(), ) ) } # pop output_dict items [ output_dict.pop(key) for key in dict( filter( lambda kv: kv[1].role == type_ and kv[1]._iscycle, self._meta.variables.items(), ) ) ] # make sure to cycle through all possible items items_list = list( zip(items, itertools.cycle(n_times)) if len(items) > len(n_times) else zip(itertools.cycle(items), n_times) ) for item, n_time in items_list: item._iscyclebase = True basename = item.name item_base = self._meta.variables.get(id(item)) for n, _ in enumerate(range(int(n_time)), start=1): existing = next( ( key for key, value in mydict.items() if value.name == basename + "-{}".format(n) ), None, ) item = mydict.get(existing, item_base.copy()) item._iscyclebase = False # return it back to False if item._iscycle: self._meta.variables.update({id(item): item}) else: item.name = basename + "-{}".format(n) item.order += 1 if n_time > 1 else 0 item._iscycle = True self._meta.variables.update({id(item): item}) def _to_deck(self): """Return deck representation of self.""" unit_type = f"UNIT {self.unit_number} TYPE {self.type_number} {self.name}\n" studio = self.studio params = self.parameters inputs = self.inputs initial_input_values = self.initial_input_values special_cards = self.special_cards derivatives = self.derivatives externals = self.external_files return ( str(unit_type) + str(studio) + str(params) + str(inputs) + str(initial_input_values) + str(special_cards) + str(derivatives) + str(externals) )
[docs] def update_meta(self, new_meta): """Update self with new :class:`MetaData`.""" for attr in self._meta.__dict__: if hasattr(new_meta, attr): setattr(self._meta, attr, getattr(new_meta, attr)) tag = new_meta.variables type_vars = [ TypeVariable.from_tag(tag, model=self) for tag in tag if isinstance(tag, Tag) ] tag = new_meta.cycles type_cycles = CycleCollection( TypeCycle.from_tag(tag) for tag in tag if isinstance(tag, Tag) ) tag = new_meta.special_cards or {} special_cards = [ SpecialCard.from_tag(tag) for tag in tag if isinstance(tag, Tag) ] self._meta.variables = {id(var): var for var in type_vars} self._meta.cycles = type_cycles self._meta.special_cards = {id(var): var for var in special_cards} tag = new_meta.external_files if tag: self._meta.external_files = ExternalFileCollection.from_dict( { id(ext): ext for ext in { ExternalFile.from_tag(tag) for tag in tag if isinstance(tag, Tag) } } )
[docs] def plot(self): """Plot the model.""" import matplotlib.pyplot as plt G = nx.DiGraph() G.add_edges_from(("type", output.name) for output in self.outputs.values()) G.add_edges_from((input.name, "type") for input in self.inputs.values()) pos = nx.drawing.planar_layout(G, center=(50, 50)) ax = nx.draw_networkx( G, pos, with_labels=True, arrows=True, width=4, ) nx.draw_networkx_edge_labels( G, pos, edge_labels={ ("type", output.name): output.name for output in self.outputs.values() }, ax=ax, ) plt.show() return ax