"""
The Section class allows parsing a model section into Elements. The
final result can be exported to an AbstractSection class in order to
build a model in other language. A section could be either the main model
(without the macros), or a macro definition (not supported yet for Xmile).
"""
from typing import List, Union
from lxml import etree
from pathlib import Path
from ..structures.abstract_model import AbstractSection
from .xmile_element import ControlElement, SubscriptRange, Aux, Flow, Gf, Stock
[docs]
class Section():
"""
Section object allows parsing the elements of that section.
Parameters
----------
name: str
Section name. '__main__' for the main section or the macro name.
path: pathlib.Path
Section path. It should be the model name for main section and
the clean macro name for a macro.
section_type: str ('main' or 'macro')
The section type.
params: list
List of params that takes the section. In the case of main
section it will be an empty list.
returns: list
List of variables that returns the section. In the case of main
section it will be an empty list.
content_root: etree._Element
Section parsed tree content.
namespace: str
The namespace of the section given after parsing its content
with etree.
split: bool
If split is True the created section will split the variables
depending on the views_dict.
views_dict: dict
The dictionary of the views. Giving the variables classified at
any level in order to split them by files.
"""
_control_vars = ["initial_time", "final_time", "time_step", "saveper"]
def __init__(self, name: str, path: Path, section_type: str,
params: List[str], returns: List[str],
content_root: etree._Element, namespace: str, split: bool,
views_dict: Union[dict, None]):
self.name = name
self.path = path
self.type = section_type
self.params = params
self.returns = returns
self.content = content_root
self.ns = {"ns": namespace}
self.split = split
self.views_dict = views_dict
self.elements = None
self.behaviors = {}
def __str__(self): # pragma: no cover
return "\nSection: %s\n" % self.name
@property
def _verbose(self) -> str: # pragma: no cover
"""Get section information."""
text = self.__str__()
if self.elements:
for element in self.elements:
text += element._verbose
else:
text += self.content
return text
@property
def verbose(self): # pragma: no cover
"""Print section information to standard output."""
print(self._verbose)
[docs]
def parse(self, parse_all: bool = True) -> None:
"""
Parse section object. The subscripts of the section will be added
to self subscripts. The variables defined as Flows, Auxiliary, Gf,
and Stock will be converted in XmileElements. The control variables,
if the section is __main__, will be converted to a ControlElement.
Parameters
----------
parse_all: bool (optional)
If True then the created VensimElement objects will be
automatically parsed. Otherwise, this objects will only be
added to self.elements but not parser. Default is True.
"""
# parse subscripts and components
self.subscripts = self._parse_subscripts()
self.components = self._parse_components()
if self.name == "__main__":
# parse control variables
self.components += self._parse_control_vars()
# Parse behavior section
self.behaviors.update(self._parse_behavior())
if parse_all:
[component.parse(self.behaviors) for component in self.components]
# define elements for printting information
self.elements = self.subscripts + self.components
def _parse_behavior(self) -> List[SubscriptRange]:
"""Parse the behavior the section."""
behaviors = {
'non_negative_stock': False,
'non_negative_flow': False
}
parsed_bhs = self.content.xpath("ns:behavior", namespaces=self.ns)
if not parsed_bhs:
return behaviors
if parsed_bhs[0].xpath('ns:non_negative', namespaces=self.ns):
behaviors['non_negative_stock'] = True
behaviors['non_negative_flow'] = True
return behaviors
bhs_stock = parsed_bhs[0].xpath('ns:stock', namespaces=self.ns)
bhs_flow = parsed_bhs[0].xpath('ns:flow', namespaces=self.ns)
if bhs_stock and bhs_stock[0].xpath('ns:non_negative', namespaces=self.ns):
behaviors['non_negative_stock'] = True
if bhs_flow and bhs_flow[0].xpath('ns:non_negative', namespaces=self.ns):
behaviors['non_negative_flow'] = True
return behaviors
def _parse_subscripts(self) -> List[SubscriptRange]:
"""Parse the subscripts of the section."""
subscripts = [
SubscriptRange(
node.attrib["name"],
[
sub.attrib["name"]
for sub in node.xpath("ns:elem", namespaces=self.ns)
],
[]) # no subscript mapping implemented
for node
in self.content.xpath("ns:dimensions/ns:dim", namespaces=self.ns)
]
self.subscripts_dict = {
subr.name: subr.definition for subr in subscripts}
return subscripts
def _parse_components(self) -> List[Union[Flow, Aux, Gf, Stock]]:
"""
Parse model components. Four groups defined:
Aux: auxiliary variables
Flow: flows
Gf: lookups
Stock: integs
"""
# Add auxiliary variables
components = [
Aux(node, self.ns, self.subscripts_dict)
for node in self.content.xpath(
"ns:model/ns:variables/ns:aux",
namespaces=self.ns)
if node.attrib["name"].lower().replace(" ", "_")
not in self._control_vars]
# Add flows
components += [
Flow(node, self.ns, self.subscripts_dict)
for node in self.content.xpath(
"ns:model/ns:variables/ns:flow",
namespaces=self.ns)
if node.attrib["name"].lower().replace(" ", "_")
not in self._control_vars]
# Add lookups
components += [
Gf(node, self.ns, self.subscripts_dict)
for node in self.content.xpath(
"ns:model/ns:variables/ns:gf",
namespaces=self.ns)
]
# Add stocks
components += [
Stock(node, self.ns, self.subscripts_dict)
for node in self.content.xpath(
"ns:model/ns:variables/ns:stock",
namespaces=self.ns)
]
return components
def _parse_control_vars(self) -> List[ControlElement]:
"""Parse control vars and rename them with Vensim standard."""
# Read the start time of simulation
node = self.content.xpath('ns:sim_specs', namespaces=self.ns)[0]
time_units = node.attrib['time_units']\
if 'time_units' in node.attrib else ""
control_vars = []
# initial time of the simulation
control_vars.append(ControlElement(
name="INITIAL TIME",
units=time_units,
documentation="The initial time for the simulation.",
eqn=node.xpath("ns:start", namespaces=self.ns)[0].text
))
# final time of the simulation
control_vars.append(ControlElement(
name="FINAL TIME",
units=time_units,
documentation="The final time for the simulation.",
eqn=node.xpath("ns:stop", namespaces=self.ns)[0].text
))
# time step of simulation
dt_node = node.xpath("ns:dt", namespaces=self.ns)[0]
dt_eqn = "1/(" + dt_node.text + ")" if "reciprocal" in dt_node.attrib\
and dt_node.attrib["reciprocal"].lower() == "true"\
else dt_node.text
control_vars.append(ControlElement(
name="TIME STEP",
units=time_units,
documentation="The time step for the simulation.",
eqn=dt_eqn
))
# saving time of the simulation = time step
control_vars.append(ControlElement(
name="SAVEPER",
units=time_units,
documentation="The save time step for the simulation.",
eqn="time_step"
))
return control_vars
[docs]
def get_abstract_section(self) -> AbstractSection:
"""
Get Abstract Section used for building. This, method should be
called after parsing the section (self.parse). This method is
automatically called by Model's get_abstract_model and
automatically generates the AbstractSubscript ranges and merge
the components in elements calling also the get_abstract_components
method from each model component.
Returns
-------
AbstractSection: AbstractSection
Abstract Section object that can be used for building the model
in another language.
"""
return AbstractSection(
name=self.name,
path=self.path,
type=self.type,
params=self.params,
returns=self.returns,
subscripts=[
subs_range.get_abstract_subscript_range()
for subs_range in self.subscripts
],
elements=[
element.get_abstract_element()
for element in self.components
],
constraints=[],
test_inputs=[],
split=self.split,
views_dict=self.views_dict
)