Skip to content

Commit

Permalink
Add Dymola runner (#577)
Browse files Browse the repository at this point in the history
* use filNam for teaser loads

* fix types

* add modelica project class with save_as method

* fix types

* add method to run modelica models in dymola

* rename cspell dictionary

* move modelica tests to class

* Update tests/base_test_case.py

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>

* skip tmp folder in ModelicaProject

* fix windows-based test

* add get_model method to modelica project

* update docstring

* relative path option

* add relative path setting for system parameter generation

* precommit

* do not dymola in tests

* Update geojson_modelica_translator/modelica/modelica_project.py

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>

* Update geojson_modelica_translator/modelica/modelica_project.py

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>

* Update geojson_modelica_translator/modelica/modelica_project.py

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>

* Update geojson_modelica_translator/system_parameters/system_parameters.py

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>

* pre-commit pep8 fix

---------

Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>
  • Loading branch information
nllong and vtnate authored Aug 18, 2023
1 parent 5316190 commit 43977df
Show file tree
Hide file tree
Showing 17 changed files with 416 additions and 105 deletions.
30 changes: 30 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version": "0.2",
"language": "en",
"words": [
"autoload",
"buildingspy",
"Combi",
"cvrmsd",
"dassl",
"dymola",
"GDHC",
"levelname",
"libfortran",
"linecount",
"mfrt",
"MODELICAPATH",
"mofile",
"openstudio",
"oversizing",
"redeclarations",
"Reparse",
"setpoint",
"timeseries",
"timestep",
"urbanopt"
],
"flagWords": [
"hte"
]
}
12 changes: 0 additions & 12 deletions .cspell/custom-dictionary-workspace.txt

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ jobs:
run: |
if [ '${{ matrix.test_env }}' == 'python' ]; then
if [ '${{ matrix.os }}' == 'windows-latest' ]; then
poetry run pytest --doctest-modules -v -m 'not simulation and not compilation' ./tests
poetry run pytest --doctest-modules -v -m 'not simulation and not compilation and not dymola' ./tests
else
poetry run pytest --doctest-modules -v --cov-report term-missing --cov . ./tests
poetry run pytest --doctest-modules -v -m 'not dymola' --cov-report term-missing --cov . ./tests
fi
fi
- name: Run pre-commit
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The project is motivated by the need to easily evaluate district energy systems.
Getting Started
---------------

It is possible to test the GeoJSON to Modelica Translator (GMT) by simpling installing the Python package and running the
It is possible to test the GeoJSON to Modelica Translator (GMT) by simply installing the Python package and running the
command line interface (CLI) with results from and URBANopt SDK set of results. However, to fully leverage the
functionality of this package (e.g., running simulations), then you must also install the Modelica Buildings
library (MBL) and Docker. Instructions for installing and configuring the MBL and Docker are available
Expand Down Expand Up @@ -55,7 +55,7 @@ More example projects are available in an accompanying
Architecture Overview
---------------------

The GMT is designed to enable "easy" swapping of building loads, district systems, and newtork topologies. Some
The GMT is designed to enable "easy" swapping of building loads, district systems, and network topologies. Some
of these functionalities are more developed than others, for instance swapping building loads between Spawn and
RC models (using TEASER) is fleshed out; however, swapping between a first and fifth generation heating system has
yet to be fully implemented.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def build_from_template(self, output_dir: Path, project_name: str) -> None:
# 1: grab all of the time series files and place them in the proper location
for building in self.system_parameters.get_param("$.buildings[?load_model=time_series]"):
building_load_file = Path(building['load_model_parameters']['time_series']['filepath'])

files_to_copy.append({
"orig_file": building_load_file,
"geojson_id": building['geojson_id'],
Expand All @@ -52,7 +51,7 @@ def build_from_template(self, output_dir: Path, project_name: str) -> None:
})

# 2: Copy the files to the appropriate location and ensure uniqueness by putting into a unique directory
# (since openstudio creates all files with modelica.mos)
# (since OpenStudio creates all files with modelica.mos)
total_heating_load = 0
total_cooling_load = 0
total_swh_load = 0
Expand Down
60 changes: 55 additions & 5 deletions geojson_modelica_translator/modelica/modelica_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import time
from pathlib import Path
from typing import Union

from modelica_builder.model import Model

Expand Down Expand Up @@ -81,6 +82,7 @@ class ModelicaProject:
def __init__(self, package_file):
self.root_directory = Path(package_file).parent
self.file_types = ['.mo', '.txt', '.mos', '.order']
self.file_types_to_skip = ['.log', '.mat']
self.file_data = {}

self._load_data()
Expand All @@ -89,13 +91,24 @@ def _load_data(self) -> None:
"""method to load all of the files into a data structure for processing"""
# walk the tree and add in all the files
for file_path in self.root_directory.rglob('*'):
if file_path.suffix in self.file_types_to_skip and file_path.is_file():
# skip files that have the file_types_to_skip suffix
continue

if file_path.suffix in self.file_types and file_path.is_file():
# only store the relative path that is in the package
rel_path = file_path.relative_to(self.root_directory)
self.file_data[str(rel_path)] = ModelicaFileObject(file_path)
elif file_path.is_dir():
# this is a directory, just add in
# a temp object for now to keep the path known
# this is a directory, add in an empty ModelicaFileObject
# to keep track of the directory.
#
# however, we ignore if there is a tmp directory or the parent dir is
# tmp. Maybe we need to support more than 2 levels here.
if 'tmp' in file_path.parts:
_log.warning(f"Found a tmp directory, skipping {file_path}")
continue

rel_path = file_path.relative_to(self.root_directory)
self.file_data[str(rel_path)] = ModelicaFileObject(file_path)
else:
Expand All @@ -108,8 +121,6 @@ def _load_data(self) -> None:
if self.file_data.get('package.mo', None) is None:
raise Exception('ModelicaPackage does not contain a /package.mo file')

self.pretty_print_tree()

def pretty_print_tree(self) -> None:
"""Pretty print all the items in the directory structure
"""
Expand All @@ -120,6 +131,42 @@ def pretty_print_tree(self) -> None:
indent = key.count(os.path.sep)
print(" " * indent + f"{os.path.sep} {key.replace(os.path.sep, f' {os.path.sep} ')}")

def get_model(self, model_name: Union[Path, str]) -> Model:
"""Return the model object based on the based string name. The model
name should be in the format that Modelica prefers which is period(.)
delimited.
Args:
model_name (str): Name of the model to return, in the form of . delimited
Raises:
Exception: Various exceptions if the model is not found or the file type is incorrect
Returns:
Model: The Modelica Builder model object
"""
# check if the last 3 characters are .mo. The path should originally be
# a period delimited path.
model_name = str(model_name)
if model_name.endswith('.mo'):
raise Exception(f"Model name should not have the .mo extension: {model_name} ")

# convert the model_name to the path format
model_name = Path(model_name.replace('.', os.path.sep))

# now add on the extension
model_name = model_name.with_suffix('.mo')

if self.file_data.get(str(model_name)) is None:
raise Exception(f"ModelicaPackage does not contain a {model_name} model")
else:
# verify that the type of file is model
model = self.file_data[str(model_name)]
if model.file_type != ModelicaFileObject.FILE_TYPE_MODEL:
raise Exception(f"Model is a package file, not a model: {model_name}")

return self.file_data[str(model_name)].object

def save_as(self, new_package_name: str, output_dir: Path = None) -> None:
"""method to save the ModelicaProject to a new location which
requires a new path name and updating all of the "within" statements
Expand Down Expand Up @@ -198,5 +245,8 @@ def save_as(self, new_package_name: str, output_dir: Path = None) -> None:
elif file.file_type == ModelicaFileObject.FILE_TYPE_TEXT:
# just save the file as it is text (all other files)
open(new_path, 'w').write(file.file_contents)
elif file.file_path.name == 'package.order':
# this is included in the FILE_TYPE_PACKAGE, so just skip
continue
else:
_log.warn("Unknown file type, not saving")
_log.warn(f"Unknown file type, not including in .save_as, {file.file_path}")
96 changes: 93 additions & 3 deletions geojson_modelica_translator/modelica/modelica_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from typing import Union

from buildingspy.simulate.Dymola import Simulator
from jinja2 import Environment, FileSystemLoader, StrictUndefined

from geojson_modelica_translator.jinja_filters import ALL_CUSTOM_FILTERS
Expand Down Expand Up @@ -46,7 +47,7 @@ def __init__(self, modelica_lib_path=None):
local_path = Path(__file__).parent.resolve()
self.om_docker_path = local_path / 'lib' / 'runner' / 'om_docker.sh'

# Verify that docker is up and running
# Verify that docker is up and running, if needed.
r = subprocess.call(['docker', 'ps'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.docker_configured = r == 0

Expand Down Expand Up @@ -181,7 +182,7 @@ def _subprocess_call_to_docker(self, run_path: Union[str, Path], action: str) ->
cwd=run_path
)
# Uncomment this section and rebuild the container in order to pause the container
# to inpsect the container and test commands.
# to inspect the container and test commands.
# import time
# time.sleep(10000) # wait for the subprocess to start
logger.debug(f"Subprocess command executed, waiting for completion... \nArgs used: {p.args}")
Expand Down Expand Up @@ -221,11 +222,12 @@ def run_in_docker(self, action: str, model_name: str, file_to_load: Union[str, P
model_name (str): The name of the model to be simulated (this is the name within Modelica)
file_to_load (str, Path): The file path or a modelica path to be simulated
run_path (str, optional): location where the Modelica simulation will start. Defaults to None.
kwargs: additional arugments to pass to the runner which can include
kwargs: additional arguments to pass to the runner which can include
project_in_library (bool): whether the project is in a library or not
start_time (float): start time of the simulation
stop_time (float): stop time of the simulation
step_size (float): step size of the simulation
number_of_intervals (int): number of intervals to run the simulation
debug (bool): whether to run in debug mode or not, prevents files from being deleted.
Returns:
Expand Down Expand Up @@ -271,6 +273,88 @@ def run_in_docker(self, action: str, model_name: str, file_to_load: Union[str, P
self.move_results(verified_run_path, results_path, model_name)
return (exitcode == 0, results_path)

def run_in_dymola(self, action: str, model_name: str, file_to_load: Union[str, Path], run_path: Union[str, Path], **kwargs) -> tuple[bool, Union[str, Path]]:
"""If running on Windows or Linux, you can run Dymola (assuming you have a license),
using the BuildingsPy library. This is not supported on Mac.
For using Dymola with the GMT, you need to ensure that MSL v4.0 are loaded correctly and that the
Buildings library is in the MODELICAPATH. I added the MSL openModel via appending it to the Dymola's
/opt/<install>/install/dymola.mos file on Linux.
Args:
action (str): compile (translate) or simulate
model_name (str): Name of the model to translate or simulate
package_path (Union[str, Path]): Name of the package to also load
kwargs: additional arguments to pass to the runner which can include
start_time (float): start time of the simulation
stop_time (float): stop time of the simulation, in seconds
step_size (float): step size of the simulation, in seconds
debug (bool): whether to run in debug mode or not, prevents files from being deleted.
Returns:
tuple[bool, Union[str, Path]]: success status and path to the results directory
"""
run_path = str(run_path)
current_dir = Path.cwd()
try:
os.chdir(run_path)
print(run_path)
if file_to_load is None:
# This occurs when the model is already in a library that is loaded (e.g., MSL, Buildings)
# Dymola does check the MODELICAPATH for any libraries that need to be loaded automatically.
dymola_simulator = Simulator(
modelName=model_name,
outputDirectory=run_path,
)
else:
file_to_load = str(file_to_load)
dymola_simulator = Simulator(
modelName=model_name,
outputDirectory=run_path,
packagePath=file_to_load,
)

# TODO: add in passing of parameters
# dymola_simulator.addParameters({'PI.k': 10.0, 'PI.Ti': 0.1})
dymola_simulator.setSolver("dassl")

start_time = kwargs.get('start_time', 0)
stop_time = kwargs.get('stop_time', 300)
step_size = kwargs.get('step_size', 5)

# calculate the number of intervals based on the step size
number_of_intervals = int((stop_time - start_time) / step_size)

dymola_simulator.setStartTime(start_time)
dymola_simulator.setStopTime(stop_time)
dymola_simulator.setNumberOfIntervals(number_of_intervals)

# Do not show progressbar! -- It will cause an "elapsed time used before assigned" error.
# dymola_simulator.showProgressBar(show=True)

if kwargs.get('debug', False):
dymola_simulator.showGUI(show=True)
dymola_simulator.exitSimulator(False)

# BuildingPy throws an exception if the model errs
try:
if action == 'compile':
dymola_simulator.translate()
# the results of this does not create an FMU, just
# the status of the translation/compilation.
elif action == 'simulate':
dymola_simulator.simulate()
except Exception as e:
logger.error(f"Exception running Dymola: {e}")
return False, run_path

# remove some of the unneeded results
self.cleanup_path(Path(run_path), model_name, debug=kwargs.get('debug', False))
finally:
os.chdir(current_dir)

return True, run_path

def move_results(self, from_path: Path, to_path: Path, model_name: Union[str, None] = None) -> None:
"""This method moves the results of the simulation that are known for now.
This method moves only specific files (stdout.log for now), plus all files and folders beginning
Expand Down Expand Up @@ -313,6 +397,10 @@ def cleanup_path(self, path: Path, model_name: str, **kwargs: dict) -> None:
"""
# list of files to always remove
files_to_remove = [
'dsin.txt',
'dsmodel.c',
'dymosim',
'request.',
f'{model_name}',
f'{model_name}.makefile',
f'{model_name}.libs',
Expand All @@ -325,6 +413,8 @@ def cleanup_path(self, path: Path, model_name: str, **kwargs: dict) -> None:
'om_docker.sh',
'compile_fmu.mos',
'simulate.mos',
'run.mos',
'run_translate.mos',
]

if not kwargs.get('debug', False):
Expand Down
3 changes: 3 additions & 0 deletions geojson_modelica_translator/modelica/package_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ def save_as(self, new_path: Union[str, Path]) -> None:
f.write(self.order_data)
f.write("\n")

# update link to saved path
self.path = new_path

@property
def order(self) -> list[str]:
"""Return the order of the packages from the package.order file
Expand Down
Loading

0 comments on commit 43977df

Please sign in to comment.