Skip to content

Commit 97b7777

Browse files
Correct bugs when classifying variables per module
1 parent c9df8f7 commit 97b7777

File tree

11 files changed

+534
-91
lines changed

11 files changed

+534
-91
lines changed

docs/advanced_usage.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,17 @@ In a Vensim model with three separate views (e.g. `view_1`, `view_2` and `view_3
6868
| ├── _subscripts_many_views_model.json
6969
| ├── many_views_model.py
7070
71-
71+
The variables in each file will be sorted alphabetically, using their Python name.
7272

7373
.. note ::
7474
Often, modelers wish to organise views further. To that end, a common practice is to include a particular character in the View name to indicate that what comes after it is the name of the subview. For instance, we could name one view as `ENERGY.Supply` and another one as `ENERGY.Demand`.
7575
In that particular case, setting the `subview_sep` kwarg equal to `["."]`, as in the code below, would name the translated views as `demand.py` and `supply.py` and place them inside the `ENERGY` folder::
7676
7777
read_vensim("many_views_model.mdl", split_views=True, subview_sep=["."])
7878
79+
.. note ::
80+
If a variable appears as a `workbench variable` in more than one view, it will be added only to the module corresponding to the first view and a warning message will be printed. If a variable does not appear as a workbench variable in any view, it will be added to the main model file printing a warning message.
81+
7982
If macros are present, they will be self-contained in files named after the macro itself. The macro inner variables will be placed inside the module that corresponds with the view in which they were defined.
8083

8184

docs/whats_new.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
What's New
22
==========
3+
v3.6.0 (2022/08/31)
4+
-------------------
5+
6+
New Features
7+
~~~~~~~~~~~~
8+
- Include warning messages when a variable is defined in more than one view, when a control variable appears in a view or when a variable doesn't appear in any view as a `workbench variable` (:issue:`357`).
9+
- Force variables in a module to be saved alphabetically for being able to compare differences between versions (only for the models that are split by views).
10+
11+
Breaking changes
12+
~~~~~~~~~~~~~~~~
13+
14+
Deprecations
15+
~~~~~~~~~~~~
16+
17+
Bug fixes
18+
~~~~~~~~~
19+
- Classify control variables in the main file always (:issue:`357`).
20+
21+
Documentation
22+
~~~~~~~~~~~~~
23+
24+
Performance
25+
~~~~~~~~~~~
26+
27+
Internal Changes
28+
~~~~~~~~~~~~~~~~
29+
- Include :py:class:`pysd.translators.structures.abstract_model.AbstractControlElement` child of :py:class:`pysd.translators.structures.abstract_model.AbstractElement` to differentiate the control variables.
30+
31+
332
v3.5.2 (2022/08/15)
433
-------------------
534

@@ -14,7 +43,7 @@ Deprecations
1443

1544
Bug fixes
1645
~~~~~~~~~
17-
- Make sketch's _font_size_ optional.
46+
- Make sketch's `font_size` optional.
1847

1948
Documentation
2049
~~~~~~~~~~~~~

pysd/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.5.2"
1+
__version__ = "3.6.0"

pysd/builders/python/namespace.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,24 @@ def make_python_identifier(self, string: str, prefix: str = None,
173173
self.cleanspace[clean_s] = identifier
174174

175175
return identifier
176+
177+
def get_original_name(self, identifier):
178+
"""
179+
Search for the original name of a variable's Python identifier.
180+
181+
Parameters
182+
----------
183+
identifier: str
184+
It should be a value in the namespace.
185+
186+
Rerturns
187+
--------
188+
original_name: str
189+
The original name of the variable.
190+
191+
"""
192+
for key, value in self.namespace.items():
193+
if value == identifier:
194+
return key
195+
196+
raise ValueError(f"'{identifier}' not found in the namespace.")

pysd/builders/python/python_model_builder.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
the user is only required to create a ModelBuilder object using the
66
AbstractModel and call the `build_model` method.
77
"""
8+
from warnings import warn
89
import textwrap
910
import black
1011
import json
1112
from pathlib import Path
1213
from typing import Union
1314

1415
from pysd.translators.structures.abstract_model import\
15-
AbstractComponent, AbstractElement, AbstractModel, AbstractSection
16+
AbstractComponent, AbstractElement, AbstractControlElement,\
17+
AbstractModel, AbstractSection
1618

1719
from . import python_expressions_builder as vs
1820
from .namespace import NamespaceManager
@@ -142,34 +144,88 @@ def _process_views_tree(self, view_name: str,
142144
view_content = {
143145
self.namespace.cleanspace[var] for var in view_content
144146
}
145-
# Get subview elements
146-
subview_elems = [
147-
element for element in self.elements_remaining
148-
if element.identifier in view_content
147+
148+
# Get subview elements (ordered)
149+
subview_elems = sorted(
150+
[
151+
element for element in self.elements_remaining
152+
if element.identifier in view_content
153+
and not element.control_var
154+
],
155+
key=lambda x: x.identifier)
156+
157+
# Get the names of the elements and include their
158+
# information in the elements_added dictionary
159+
subview_elems_names = [
160+
element.identifier for element in subview_elems
149161
]
162+
view_path = ".".join(view_name.parts[1:])
163+
self.elements_added.update({
164+
var: view_path for var in subview_elems_names
165+
})
166+
167+
if len(view_content) != len(subview_elems_names):
168+
# Some elements from the view where not added
169+
for var in view_content.difference(subview_elems_names):
170+
original_name = self.namespace.get_original_name(var)
171+
if var in self.elements_added:
172+
# Element already added in another view
173+
warn(
174+
f"Variable '{original_name}' is declared as "
175+
f"a workbench variable in '{view_path}' but "
176+
"it has been already added in "
177+
f"'{self.elements_added[var]}'."
178+
)
179+
else:
180+
# Element is a control variable
181+
warn(
182+
f"Control variable '{original_name}' is "
183+
"declared as a workbench variable in "
184+
f"'{view_path}'. As it is a control "
185+
"variable, this declaration will be ignored "
186+
"and added to the main module only."
187+
)
188+
150189
# Remove elements from remaining ones
151190
[
152191
self.elements_remaining.remove(element)
153192
for element in subview_elems
154193
]
155-
# Build the module
156-
self._build_separate_module(subview_elems, view_name, wdir)
157-
return sorted(view_content)
194+
195+
if subview_elems:
196+
# Build the module (only when they are variables)
197+
self._build_separate_module(subview_elems, view_name, wdir)
198+
199+
return list(subview_elems_names)
158200
else:
159201
# The current view has subviews
160-
wdir = wdir.joinpath(view_name)
161-
wdir.mkdir(exist_ok=True)
162-
return {
163-
subview_name:
164-
self._process_views_tree(subview_name, subview_content, wdir)
202+
(wdir / view_name).mkdir(exist_ok=True)
203+
subviews = {
204+
subview_name: self._process_views_tree(
205+
view_name / subview_name, subview_content, wdir)
165206
for subview_name, subview_content in view_content.items()
166207
}
208+
# Avoid includying empty views to the dictionary
209+
return {
210+
subview_name: subview_content
211+
for subview_name, subview_content in subviews.items()
212+
if subview_content
213+
}
167214

168215
def _build_modular(self, elements_per_view: dict) -> None:
169216
""" Build modular section """
170217
self.elements_remaining = self.elements.copy()
218+
self.elements_added = {}
171219
elements_per_view = self._process_views_tree(
172-
"modules_" + self.model_name, elements_per_view, self.root)
220+
Path("modules_" + self.model_name), elements_per_view, self.root)
221+
222+
for element in self.elements_remaining:
223+
if not element.control_var:
224+
warn(
225+
f"Variable '{element.name}' is not declared as a "
226+
"workbench variable in any view. It will be added to "
227+
"the main module."
228+
)
173229
# Building main file using the build function
174230
self._build_main_module(self.elements_remaining)
175231

@@ -211,15 +267,15 @@ def _build_separate_module(self, elements: list, module_name: str,
211267
Translated using PySD version %(version)s
212268
"""
213269
''' % {
214-
"module_name": module_name,
270+
"module_name": ".".join(module_name.parts[1:]),
215271
"version": __version__,
216272
})
217273
funcs = self._generate_functions(elements)
218274
text += funcs
219275
text = black.format_file_contents(
220276
text, fast=True, mode=black.FileMode())
221277

222-
outfile_name = module_dir.joinpath(module_name + ".py")
278+
outfile_name = module_dir / module_name.with_suffix(".py")
223279

224280
with outfile_name.open("w", encoding="UTF-8") as out:
225281
out.write(text)
@@ -478,6 +534,7 @@ class ElementBuilder:
478534
def __init__(self, abstract_element: AbstractElement,
479535
section: SectionBuilder):
480536
self.__dict__ = abstract_element.__dict__.copy()
537+
self.control_var = isinstance(abstract_element, AbstractControlElement)
481538
# Set element type and subtype to None
482539
self.type = None
483540
self.subtype = None

pysd/translators/structures/abstract_model.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,37 @@ def _str_child(self, depth, indent) -> str: # pragma: no cover
237237
]).replace("\n", "\n" + indent)
238238

239239

240+
@dataclass
241+
class AbstractControlElement(AbstractElement):
242+
"""
243+
Dataclass for a control element. This class is a child of
244+
AbstractElement and has the same attributes.
245+
246+
Parameters
247+
----------
248+
name: str
249+
The name of the element.
250+
components: list
251+
The list of AbstractComponents that define this element.
252+
units: str (optional)
253+
The units of the element. '' by default.
254+
limits: tuple (optional)
255+
The limits of the element. (None, None) by default.
256+
units: str (optional)
257+
The documentation of the element. '' by default.
258+
259+
"""
260+
name: str
261+
components: List[AbstractComponent]
262+
units: str = ""
263+
limits: tuple = (None, None)
264+
documentation: str = ""
265+
266+
def __str__(self) -> str: # pragma: no cover
267+
return "AbstractControlElement:\t%s (%s, %s)\n%s\n" % (
268+
self.name, self.units, self.limits, self.documentation)
269+
270+
240271
@dataclass
241272
class AbstractSubscriptRange:
242273
"""

pysd/translators/vensim/vensim_section.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from pathlib import Path
1111
import parsimonious
1212

13-
from ..structures.abstract_model import AbstractElement, AbstractSection
13+
from ..structures.abstract_model import\
14+
AbstractElement, AbstractControlElement, AbstractSection
1415

1516
from . import vensim_utils as vu
1617
from .vensim_element import Element, SubscriptRange, Component
@@ -160,15 +161,21 @@ def get_abstract_section(self) -> AbstractSection:
160161

161162
def _merge_components(self) -> List[AbstractElement]:
162163
"""Merge model components by their name."""
164+
control_vars = ["initial_time", "final_time", "time_step", "saveper"]
163165
merged = {}
164166
for component in self.components:
165167
# get a safe name to merge (case and white/underscore sensitivity)
166168
name = component.name.lower().replace(" ", "_")
167169
if name not in merged:
168170
# create new element if it is the first component
169-
merged[name] = AbstractElement(
170-
name=component.name,
171-
components=[])
171+
if name in control_vars:
172+
merged[name] = AbstractControlElement(
173+
name=component.name,
174+
components=[])
175+
else:
176+
merged[name] = AbstractElement(
177+
name=component.name,
178+
components=[])
172179

173180
if component.units:
174181
# add units to element data

pysd/translators/xmile/xmile_element.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
import numpy as np
2121

2222
from ..structures.abstract_model import\
23-
AbstractElement, AbstractLookup, AbstractComponent, AbstractSubscriptRange
23+
AbstractElement, AbstractControlElement,\
24+
AbstractLookup, AbstractComponent, AbstractSubscriptRange
2425

2526
from ..structures.abstract_expressions import AbstractSyntax
2627

@@ -467,11 +468,15 @@ def get_abstract_element(self) -> AbstractElement:
467468
with the Abstract Syntax Tree of the expression.
468469
469470
"""
470-
ae = self._get_empty_abstract_element()
471-
ae.components.append(AbstractComponent(
472-
subscripts=([], []),
473-
ast=self.ast))
474-
return ae
471+
return AbstractControlElement(
472+
name=self.name,
473+
units=self.units,
474+
limits=self.limits,
475+
documentation=self.documentation,
476+
components=[
477+
AbstractComponent(subscripts=([], []), ast=self.ast)
478+
]
479+
)
475480

476481

477482
class SubscriptRange():

0 commit comments

Comments
 (0)