-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move Modelica methods over from GMT (#68)
* remove tox and add poetry * run pre-commit * add benchmark package * add building of doc in test * update dependendencies * precommit * move modelica methods from gmt --------- Co-authored-by: Nathan Moore <nathan.moore@nrel.gov>
- Loading branch information
Showing
38 changed files
with
65,149 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. | ||
# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md | ||
|
||
import re | ||
from pathlib import Path | ||
from typing import Any, Union | ||
|
||
|
||
class ModelicaMOS(object): | ||
def __init__(self, filename: str, data: str = None): | ||
"""Read in a .mos file if it exists into a data object | ||
The format is CSV with additional header info, so for | ||
now read the data as a string. | ||
Args: | ||
filename (str): Name of the file to import, typically a full path | ||
Raises: | ||
FileNotFoundError: File does not exist | ||
Exception: The file extension needs to be MOS | ||
""" | ||
# allow reading in the data directly for testing purposes, for the | ||
# most part. | ||
if not data: | ||
if Path(filename).exists(): | ||
self.filename = Path(filename) | ||
else: | ||
raise FileNotFoundError(f"{filename} does not exist") | ||
|
||
if self.filename.suffix.lower() != ".mos": | ||
raise Exception(f"{filename} is not a .mos file") | ||
|
||
self.data = self.filename.read_text() | ||
else: | ||
self.data = data | ||
|
||
def retrieve_header_variable_value(self, key: str, cast_type: type = str) -> Any: | ||
"""Retrieve a value from a variable in the header | ||
Args: | ||
key (str): Key to retrieve | ||
cast_type (type, optional): Type to cast the value to. Defaults to str. | ||
Returns: | ||
str: Value of the key | ||
""" | ||
# check if the peak water heating load is zero, otherwise just skip | ||
key_re = rf'#(\s?{key}\s?)=\s?(-?\b\d[\d,.]*\b)(.*\s)' | ||
match = re.search(key_re, self.data) | ||
if match: | ||
# the first group is the variable name, the second is the value | ||
value = match.group(2).strip() | ||
if value and cast_type: | ||
try: | ||
if cast_type != str: | ||
value = value.replace(',', '') | ||
return cast_type(value) | ||
else: | ||
return value | ||
except ValueError: | ||
raise Exception(f"Unable to cast {value} to {cast_type}") | ||
else: | ||
return value | ||
else: | ||
return None | ||
|
||
def replace_header_variable_value(self, key: str, new_value: Any) -> bool: | ||
"""Replace the variable value in the header vars | ||
Args: | ||
key (str): Key of the variable value to replace | ||
new_value (Any): new value | ||
Returns: | ||
bool: Always True (for now) | ||
""" | ||
key_re = rf'#(\s?{key}\s?)=\s?(-?\b\d[\d,.]*\b)(.*\s)' | ||
|
||
# verify that group3 exists, otherwise the key doesn't exist | ||
match = re.search(key_re, self.data) | ||
if match: | ||
if match.group(3): | ||
# if there are units, then put the unit back in | ||
self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.data) | ||
else: | ||
# no g3, therefore, just replace the var and value. | ||
self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}", self.data) | ||
|
||
# replace the value, this requires recreating the variable and value | ||
self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.data) | ||
return True | ||
|
||
def save(self): | ||
"""Save the file back to disk""" | ||
self.filename.write_text(self.data) | ||
|
||
def save_as(self, filename: Union[str, Path]): | ||
"""Save the file to a new filename""" | ||
Path(filename).write_text(self.data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
""" | ||
**************************************************************************************************** | ||
:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC. | ||
All rights reserved. | ||
**************************************************************************************************** | ||
""" | ||
import logging | ||
import os | ||
import time | ||
from pathlib import Path | ||
from typing import Union | ||
|
||
from modelica_builder.model import Model | ||
from modelica_builder.package_parser import PackageParser | ||
|
||
_log = logging.getLogger(__name__) | ||
|
||
|
||
class ModelicaFileObject: | ||
"""Class for storing a Modelica file object. Example is a '.mo' file that is | ||
lazily parsed into the AST using Modelica-Builder or a '.mos' file that reads in the | ||
header's values using the ModelicaMOS class.""" | ||
|
||
# enumerations for different file types | ||
FILE_TYPE_PACKAGE = 0 | ||
FILE_TYPE_MODEL = 1 | ||
FILE_TYPE_SCRIPT = 2 | ||
FILE_TYPE_TEXT = 3 | ||
|
||
def __init__(self, file_path): | ||
self.file_path = Path(file_path) | ||
self.object = None | ||
self.file_contents = None | ||
self.file_type = None | ||
|
||
# depending on the file type, parse the object when it is first accessed. | ||
if self.file_path.is_dir(): | ||
self.file_contents = None | ||
elif self.file_path.name == 'package.mo': | ||
# this parses both the .mo and .order files, so we | ||
# need to skip over the .order file. The PackageParser is | ||
# a directory, not the file itself. | ||
self.object = PackageParser(self.file_path.parent) | ||
self.file_type = self.FILE_TYPE_PACKAGE | ||
elif self.file_path.name == 'package.order': | ||
pass | ||
elif self.file_path.suffix == '.mo': | ||
self.file_type = self.FILE_TYPE_MODEL | ||
self._parse_mo_file() | ||
elif self.file_path.suffix == '.mos': | ||
self.file_type = self.FILE_TYPE_SCRIPT | ||
self.file_contents = self.file_path.read_text() | ||
elif self.file_path.suffix == '.txt': | ||
self.file_type = self.FILE_TYPE_TEXT | ||
self.file_contents = self.file_path.read_text() | ||
else: | ||
# not sure what to do with this | ||
_log.warning(f"Unknown file type {self.file_path}") | ||
|
||
def exists(self): | ||
self.file_path.exists() | ||
|
||
def _parse_mo_file(self): | ||
"""method to parse the mo file into a Modelica AST""" | ||
# time the loading of the file | ||
start = time.time() | ||
self.object = Model(self.file_path) | ||
end = time.time() | ||
|
||
_log.debug(f"Took {end - start} seconds to load {self.file_path.name}") | ||
|
||
@property | ||
def name(self): | ||
"""method to get the name of the file""" | ||
return self.file_path.name | ||
|
||
|
||
class ModelicaProject: | ||
"""Class for storing all the files in a Modelica project. This class should organically | ||
grow as more requirements are needed. | ||
The current functionality includes: | ||
* Load in a package.mo file and store all the related files in memory space.""" | ||
|
||
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', ] | ||
# skip some specific files that software may create that are not needed to be | ||
# sent around with the modelica project. | ||
self.file_names_to_skip = ['analysis_variables.csv', 'analysis_name.txt', '.DS_Store', ] | ||
self.file_data = {} | ||
|
||
self._load_data() | ||
|
||
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.name in self.file_names_to_skip and file_path.is_file(): | ||
# skip files that have the file_names_to_skip name | ||
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, 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: | ||
print(f"Unknown file {file_path}") | ||
|
||
# now sort the file_data by the keys | ||
self.file_data = {key: self.file_data[key] for key in sorted(self.file_data)} | ||
|
||
# validate the data, extend as needed. | ||
if self.file_data.get('package.mo', None) is None: | ||
raise Exception('ModelicaPackage does not contain a /package.mo file') | ||
|
||
def pretty_print_tree(self) -> None: | ||
"""Pretty print all the items in the directory structure | ||
""" | ||
# Print a couple lines, just because | ||
print() | ||
for key, obj in self.file_data.items(): | ||
# find how many indents we need based on the number of path separators | ||
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 | ||
Args: | ||
new_package_name (str): Name of the new package, which will also be the directory name | ||
output_dir (Path, optional): Where to persist the new directory and package. Defaults to existing. | ||
""" | ||
if output_dir is None: | ||
output_dir = self.root_directory | ||
output_dir = output_dir / new_package_name | ||
|
||
# in the root package, rename the modelica package (there is not within statement) | ||
self.file_data['package.mo'].object.rename_package(new_package_name) | ||
|
||
# go through each of the package.mo files first and update the within statements | ||
for path, file in self.file_data.items(): | ||
if path == 'package.mo': | ||
# this file is handled above, so just skip | ||
continue | ||
|
||
if file.file_type == ModelicaFileObject.FILE_TYPE_PACKAGE: | ||
# this is a package, so update the within statement | ||
file.object.update_within_statement(new_package_name, element_index=0) | ||
|
||
elif file.file_type == ModelicaFileObject.FILE_TYPE_MODEL: | ||
new_within_statement = f"{new_package_name}.{str(Path(path).parent).replace(os.path.sep, '.')}" | ||
file.object.set_within_statement(new_within_statement) | ||
|
||
# there are a few very specific methods that exist when reading in weather files or | ||
# load files. I am not sure how to abstract out this logic at the moment. | ||
|
||
# IDF names - find the existing value and replace if found | ||
idf_name = file.object.get_parameter_value('String', 'idfName') | ||
if idf_name: | ||
# replace the previous model name with the new name | ||
idf_name = idf_name.replace(self.root_directory.name, new_package_name) | ||
file.object.update_parameter('String', 'idfName', idf_name) | ||
|
||
epw_name = file.object.get_parameter_value('String', 'epwName') | ||
if epw_name: | ||
# replace the previous model name with the new name | ||
epw_name = epw_name.replace(self.root_directory.name, new_package_name) | ||
file.object.update_parameter('String', 'epwName', epw_name) | ||
|
||
weather_filename = file.object.get_parameter_value('String', 'weaName') | ||
if weather_filename: | ||
# replace the previous model name with the new name | ||
weather_filename = weather_filename.replace(self.root_directory.name, new_package_name) | ||
file.object.update_parameter('String', 'weaName', weather_filename) | ||
|
||
filename = file.object.get_parameter_value('String', 'filNam') | ||
if filename: | ||
# replace the previous model name with the new name | ||
filename = filename.replace(self.root_directory.name, new_package_name) | ||
file.object.update_parameter('String', 'filNam', filename) | ||
|
||
# now persist all the files to the new location | ||
if not output_dir.exists(): | ||
output_dir.mkdir(parents=True, exist_ok=True) | ||
|
||
for path, file in self.file_data.items(): | ||
# create the new path | ||
new_path = output_dir / path | ||
if file.file_path.is_dir(): | ||
# this is a directory, so just create it | ||
new_path.mkdir(parents=True, exist_ok=True) | ||
|
||
elif file.file_type == ModelicaFileObject.FILE_TYPE_PACKAGE: | ||
file.object.save_as(new_path.parent) | ||
elif file.file_type == ModelicaFileObject.FILE_TYPE_MODEL: | ||
file.object.save_as(new_path) | ||
elif file.file_type == ModelicaFileObject.FILE_TYPE_SCRIPT: | ||
# just save the file as it is text (mos-based file) | ||
open(new_path, 'w').write(file.file_contents) | ||
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(f"Unknown file type, not including in .save_as, {file.file_path}") |
Oops, something went wrong.