diff --git a/.coveragerc b/.coveragerc index 481f6cb74..1b8dfd227 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,7 +11,7 @@ source = [report] omit = geojson_modelica_translator/cli.py - + # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..f4c08060c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ + + +### Expected Behavior + +### Actual Behavior + +### Steps to Reproduce + +### Version Information + + + +Modelica Buildings Library: `7.0.0 or GithubBranch/SHA (e.g., issue1437_district_heating_cooling /SHA_XYZ)` +TEASER: `URBANopt/0.6.9` +Python: `3.6.5` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..e47cd976a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +#### Any background context you want to provide? +#### What does this PR accomplish? +#### How should this be manually tested? +#### What are the relevant tickets? +#### Screenshots (if appropriate) diff --git a/.gitignore b/.gitignore index e6f301d64..419405798 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ downloads/ eggs/ .eggs/ lib/ +!geojson_modelica_translator/modelica/lib lib64/ parts/ sdist/ @@ -93,6 +94,11 @@ ENV/ # Test outputs tests/output +tests/output_ets tests/modelica/output tests/model_connectors/output +tests/tests_13buildings_modelica/ +tests/tests_13buildings_rc4/ /geojson_modelica_translator/modelica/buildingslibrary/ +# TODO: this file shoud not be writtent out, but it is... fix this eventually +/geojson_modelica_translator/modelica/ets_cooling_indirect_templated.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..914479af2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +exclude: | + (?x)( + ^docs/conf.py| + ^tests/modelica/| + ^modelica_builder/modelica_parser/ + ) + + +repos: +- repo: git://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer +# - id: requirements-txt-fixer + - id: flake8 + args: ['--max-line-length=140'] # default of Black + - id: mixed-line-ending + + +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.4 + hooks: + - id: isort + args: ['-m 3'] # vertical hanging diff --git a/.travis.yml b/.travis.yml index e85a8c212..ae899d9d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,9 @@ env: - TOX_ENV=python - TOX_ENV=flake8 - TOX_ENV=docs +before_script: + - git clone --single-branch --branch issue1437_district_heating_cooling https://github.com/lbl-srg/modelica-buildings.git + - export MODELICAPATH=$(pwd)/modelica-buildings script: - tox -e $TOX_ENV after_success: diff --git a/AUTHORS.rst b/AUTHORS.rst index 05fe77e17..228762ef8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,3 +2,4 @@ Contributors ============ * Nicholas Long +* Yanfei Li diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b07e93320..c983903d5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,11 @@ Change Log ========== -Version 0.1 ------------ +Version 0.1 (Unreleased) +------------------------ This is the initial release of the package and includes the following functionality: -* Create a ROM/RC model using Modelica 3.2.x, Modelica Buildings Library 7.0 and TEASER 0.6.8 -* Create a Spawn-based models which loads an IDF file - +* Initial implementation of a ModelicaRunner to call a Docker container to run the model. +* Create a ROM/RC model using Modelica 3.2.x, Modelica Buildings Library 7.0 and TEASER 0.7.2. +* Create a Spawn-based models which loads an IDF file. diff --git a/LICENSE b/LICENSE index 22dc67fc2..1f9f84834 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -URBANopt, Copyright (c) 2019, Alliance for Sustainable Energy, LLC, and other contributors. +URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted diff --git a/Makefile b/Makefile index 3e5d83211..e3f11e3e6 100644 --- a/Makefile +++ b/Makefile @@ -3,4 +3,3 @@ init: test: py.test - diff --git a/README.rst b/README.rst index d93f51829..82bf31743 100644 --- a/README.rst +++ b/README.rst @@ -20,14 +20,33 @@ The GeoJSON / Modelica Translator is still in early alpha-phase development and If installing this package for development then you must run the `setup.py build` command in order to install the MBL in the right location. +currently you need python3 and pip3 to install/build the packages. + .. code-block:: bash pip install -r requirements.txt + # or pip install . python setup.py build - py.test + python setup.py test The py.test tests should all pass assuming the libraries are installed correctly on your development computer. Also, there will be a set of Modelica models that are created and persisted into the `tests/output` folder. +Developers +********** + +This project used `pre-commit `_ to ensure code consistency. To enable pre-commit, run the following from the command line. + +.. code-block:: bash + + pip install pre-commit + pre-commit install + +To run pre-commit against the files without calling git commit, then run the following. This is useful when cleaning up the repo before committing. + +.. code-block:: bash + + pre-commit run --all-files + Modules ******* @@ -53,7 +72,7 @@ The Simulation Mapper Class can operate at mulitple levels: 2. The Load Model Connection -- input: geojson+, output: multiple files related to building load models (spawn, rom, csv) 3. The Translation to Modelica -- input: custom format, output: .mo (example inputs: geojson+, system design parameters). The translators are implicit to the load model connectors as each load model requires different paramters to calculate the loads. -In some cases, the Level 3 case (transalation to Modelica) is a blackbox method (e.g. TEASER) which prevents a simulation mapper class from existing at that level. +In some cases, the Level 3 case (translation to Modelica) is a blackbox method (e.g. TEASER) which prevents a simulation mapper class from existing at that level. Adjacency Matrix ++++++++++++++++ @@ -70,12 +89,13 @@ will automatically run the models without having to follow the steps below. * Clone https://github.com/lbl-srg/docker-ubuntu-jmodelica and follow the set up instructions. * Copy jmodelica.py (from docker-ubuntu-jmodelica) to root of project where you will simulate (e.g., geojson-modelica-translator/tests/model_connectors/output) -* Pull https://github.com/lbl-srg/modelica-buildings/tree/issue1442_loadCoupling +* Pull https://github.com/lbl-srg/modelica-buildings/tree/issue1437_district_heating_cooling * **Make sure you have git-lfs installed**. You may need to checkout out the library again after install lfs. + * Please make sure you are in the issue1437_district_heating_cooling branch. * Mac: `brew install git-lfs; git lfs install` * Ubuntu: `sudo apt install git-lfs; git lfs install` * Add the Buildings Library path to your MODELICAPATH environment variable (e.g., export MODELICAPATH=${MODELICAPATH}:/home//github/modelica-buildings). -* Example simulation: +* Example simulation: * `jm_ipython.sh jmodelica.py spawn_two_building.Loads.B5a6b99ec37f4de7f94020090.building` * `jm_ipython.sh jmodelica.py spawn_two_building/Loads/B5a6b99ec37f4de7f94020090/building.mo` * Visualize the results by inspecting the resulting mat file using BuildingsPy. @@ -139,7 +159,6 @@ Todos * handle weather in Teaser * Create a Script directory in the modelica_path class -* EBNF parsing * Validate remaining schema objects * AHU example * runnable example diff --git a/docs/conf.py b/docs/conf.py index f6dfb5d9e..6eb9e28ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,38 +11,38 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'GeoJSON Modelica Translator' -copyright = u'2019, Alliance for Sustainable Energy, LLC' +project = u"GeoJSON Modelica Translator" +copyright = u"2019, Alliance for Sustainable Energy, LLC" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -50,74 +50,74 @@ # # TODO: grab version from setup.py # The short X.Y version. -version = 'v0.0.1' +version = "v0.0.1" # The full version, including alpha/beta/rc tags. -release = 'v0.0.1' +release = "v0.0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -126,47 +126,47 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'sampledoc' +htmlhelp_basename = "sampledoc" # -- Options for LaTeX output -------------------------------------------------- @@ -174,10 +174,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -185,29 +183,34 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'GeoJSON Modelica Translator.tex', u'GeoJSON Modelica Translator Documentation', - u'Nicholas Long', 'manual'), + ( + "index", + "GeoJSON Modelica Translator.tex", + u"GeoJSON Modelica Translator Documentation", + u"Nicholas Long", + "manual", + ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -215,12 +218,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'GeoJSON Modelica Translator', u'GeoJSON Modelica Translator Documentation', - [u'Nicholas Long'], 1) + ( + "index", + "GeoJSON Modelica Translator", + u"GeoJSON Modelica Translator Documentation", + [u"Nicholas Long"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -229,16 +237,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'GeoJSON Modelica Translator', u'GeoJSON Modelica Translator Documentation', - u'Nicholas Long', 'GeoJSON Modelica Translator', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "GeoJSON Modelica Translator", + u"GeoJSON Modelica Translator Documentation", + u"Nicholas Long", + "GeoJSON Modelica Translator", + "One line description of project.", + "Miscellaneous", + ) ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/index.rst b/docs/index.rst index 4ee513ee5..057428500 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,4 +17,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/geojson_modelica_translator/cli.py b/geojson_modelica_translator/cli.py index 12b8139d3..f363b05dd 100644 --- a/geojson_modelica_translator/cli.py +++ b/geojson_modelica_translator/cli.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -32,14 +32,12 @@ import logging import sys - # Work in progress + def parse_args(args): parser = argparse.ArgumentParser(description="Parser") - parser.set_defaults(log_level=logging.WARNING, - extensions=[], - command=run) + parser.set_defaults(log_level=logging.WARNING, extensions=[], command=run) opts = vars(parser.parse_args(args)) return opts @@ -51,7 +49,7 @@ def main(args): args ([str]): command line arguments """ opts = parse_args(args) - opts['command'](opts) + opts["command"](opts) def run(): @@ -59,5 +57,5 @@ def run(): main(sys.argv[1:]) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/geojson_modelica_translator/geojson/data/schemas/building_properties.json b/geojson_modelica_translator/geojson/data/schemas/building_properties.json index 3c936d1b5..15abbe51f 100644 --- a/geojson_modelica_translator/geojson/data/schemas/building_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/building_properties.json @@ -353,4 +353,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/district_system_properties.json b/geojson_modelica_translator/geojson/data/schemas/district_system_properties.json index 0fb110213..b0abfff6b 100644 --- a/geojson_modelica_translator/geojson/data/schemas/district_system_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/district_system_properties.json @@ -131,4 +131,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/electrical_connector_properties.json b/geojson_modelica_translator/geojson/data/schemas/electrical_connector_properties.json index c2dbcbe52..f3e80c936 100644 --- a/geojson_modelica_translator/geojson/data/schemas/electrical_connector_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/electrical_connector_properties.json @@ -72,4 +72,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/electrical_junction_properties.json b/geojson_modelica_translator/geojson/data/schemas/electrical_junction_properties.json index d507c22f2..a3f72d0b6 100644 --- a/geojson_modelica_translator/geojson/data/schemas/electrical_junction_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/electrical_junction_properties.json @@ -58,4 +58,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/region_properties.json b/geojson_modelica_translator/geojson/data/schemas/region_properties.json index f07e74c51..4aea02211 100644 --- a/geojson_modelica_translator/geojson/data/schemas/region_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/region_properties.json @@ -88,4 +88,4 @@ "region_type" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/site_properties.json b/geojson_modelica_translator/geojson/data/schemas/site_properties.json index 48a9c0911..db44a6bee 100644 --- a/geojson_modelica_translator/geojson/data/schemas/site_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/site_properties.json @@ -75,4 +75,4 @@ "longitude" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/thermal_connector_properties.json b/geojson_modelica_translator/geojson/data/schemas/thermal_connector_properties.json index 56e2fff9f..0c87bb4bc 100644 --- a/geojson_modelica_translator/geojson/data/schemas/thermal_connector_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/thermal_connector_properties.json @@ -100,4 +100,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/data/schemas/thermal_junction_properties.json b/geojson_modelica_translator/geojson/data/schemas/thermal_junction_properties.json index 318ec952b..f496d6c30 100644 --- a/geojson_modelica_translator/geojson/data/schemas/thermal_junction_properties.json +++ b/geojson_modelica_translator/geojson/data/schemas/thermal_junction_properties.json @@ -76,4 +76,4 @@ ] } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/geojson/schemas.py b/geojson_modelica_translator/geojson/schemas.py index 9f832c0d1..5424ef9ad 100644 --- a/geojson_modelica_translator/geojson/schemas.py +++ b/geojson_modelica_translator/geojson/schemas.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -42,18 +42,20 @@ class Schemas(object): def __init__(self): """Load in the schemas""" self.schemas = { - 'building': None, - 'district_system': None, - 'electrical_connector': None, - 'electrical_junction': None, - 'region': None, - 'site': None, - 'thermal_connector': None, - 'thermal_junction': None, + "building": None, + "district_system": None, + "electrical_connector": None, + "electrical_junction": None, + "region": None, + "site": None, + "thermal_connector": None, + "thermal_junction": None, } for s in self.schemas.keys(): - path = os.path.join(os.path.dirname(__file__), "data/schemas/%s_properties.json" % s) + path = os.path.join( + os.path.dirname(__file__), "data/schemas/%s_properties.json" % s + ) self.schemas[s] = json.load(open(path, "r")) def retrieve(self, name): @@ -61,7 +63,7 @@ def retrieve(self, name): if self.schemas.get(name): return self.schemas[name] else: - raise Exception('Schema for %s does not exist' % name) + raise Exception("Schema for %s does not exist" % name) def validate(self, name, instance): """ diff --git a/geojson_modelica_translator/geojson/urbanopt_geojson.py b/geojson_modelica_translator/geojson/urbanopt_geojson.py index 0fa312d41..446697c10 100644 --- a/geojson_modelica_translator/geojson/urbanopt_geojson.py +++ b/geojson_modelica_translator/geojson/urbanopt_geojson.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -31,6 +31,7 @@ import logging import os from collections import defaultdict + import geojson from geojson_modelica_translator.geojson.schemas import Schemas @@ -45,8 +46,8 @@ class UrbanOptBuilding(object): def __init__(self, feature): self.feature = feature - self.id = feature.get('properties', {}).get('id', 'NO ID') - self.dirname = f'B{self.id}' + self.id = feature.get("properties", {}).get("id", "NO ID") + self.dirname = f"B{self.id}" class UrbanOptGeoJson(object): @@ -59,7 +60,7 @@ def __init__(self, filename): if os.path.exists(filename): self.data = geojson.load(open(filename)) else: - raise Exception(f'URBANopt GeoJSON file does not exist: {filename}') + raise Exception(f"URBANopt GeoJSON file does not exist: {filename}") # load the shemas self.schemas = Schemas() @@ -67,7 +68,7 @@ def __init__(self, filename): # break up the file based on the various features self.buildings = [] for f in self.data.features: - if f['properties']['type'] == 'Building': + if f["properties"]["type"] == "Building": self.buildings.append(UrbanOptBuilding(f)) def validate(self): @@ -77,18 +78,15 @@ def validate(self): :return: dict of lists, errors for each of the types """ validations = defaultdict(dict) - validations['building'] = [] + validations["building"] = [] status = True # go through building properties for validation for b in self.buildings: - val_res = self.schemas.validate('building', b.feature.properties) + val_res = self.schemas.validate("building", b.feature.properties) if len(val_res) > 0: status = False - res = { - "id": b.feature.properties['id'], - "errors": val_res, - } - validations['building'].append(res) + res = {"id": b.feature.properties["id"], "errors": val_res} + validations["building"].append(res) return status, validations diff --git a/geojson_modelica_translator/geojson_modelica_translator.py b/geojson_modelica_translator/geojson_modelica_translator.py index dc86f882e..c7276d678 100644 --- a/geojson_modelica_translator/geojson_modelica_translator.py +++ b/geojson_modelica_translator/geojson_modelica_translator.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -30,11 +30,12 @@ import logging import os -import shutil -from geojson_modelica_translator.geojson.urbanopt_geojson import UrbanOptGeoJson +from geojson_modelica_translator.geojson.urbanopt_geojson import ( + UrbanOptGeoJson +) from geojson_modelica_translator.modelica.input_parser import PackageParser -from geojson_modelica_translator.utils import ModelicaPath +from geojson_modelica_translator.scaffold import Scaffold _log = logging.getLogger(__name__) @@ -48,11 +49,7 @@ def __init__(self): self.buildings = [] # directory name member variables. These are set in the scaffold_directory method - self.loads_path = None - self.substations_path = None - self.plants_path = None - self.districts_path = None - + self.scaffold = None self.system_parameters = None @classmethod @@ -71,7 +68,7 @@ def from_geojson(cls, filename): klass.buildings = json.buildings return klass else: - raise Exception(f'GeoJSON file does not exist: {filename}') + raise Exception(f"GeoJSON file does not exist: {filename}") def set_system_parameters(self, sys_params): """ @@ -81,26 +78,19 @@ def set_system_parameters(self, sys_params): """ self.system_parameters = sys_params - def scaffold_directory(self, root_dir, overwrite=False): + def scaffold_directory(self, root_dir, project_name, overwrite=False): """ Scaffold out the initial directory and set various helper directories - :param root_dir: string, absolute path where to save results - :return: bool, did the directory get scaffolded + :param root_dir: string, absolute path where the project will be scaffolded. + :param project_name: string, name of the project that is being created + :return: string, path to the scaffold directory """ - # TODO: need to be careful with this. If we are mixing load models, then we need to not remove the entire path - if os.path.exists(root_dir): - if overwrite: - raise Exception("Directory already exists and overwrite is false for %s" % root_dir) - else: - shutil.rmtree(root_dir) - - self.loads_path = ModelicaPath('Loads', root_dir=root_dir) - self.substations_path = ModelicaPath('Substations', root_dir=root_dir) - self.plants_path = ModelicaPath('Plants', root_dir=root_dir) - self.districts_path = ModelicaPath('Districts', root_dir=root_dir) - - def to_modelica(self, project_name, save_dir, model_connector_str='TeaserConnector'): + self.scaffold = Scaffold(root_dir, project_name) + self.scaffold.create() + return self.scaffold.project_path + + def to_modelica(self, project_name, save_dir, model_connector_str="TeaserConnector"): """ Convert the data in the GeoJSON to modelica based-objects @@ -108,29 +98,33 @@ def to_modelica(self, project_name, save_dir, model_connector_str='TeaserConnect {save_dir}/{project_name} :param model_connector_str: str, which model_connector to use """ - prj_dir = f'{save_dir}/{project_name}' - self.scaffold_directory(prj_dir) + self.scaffold_directory(save_dir, project_name) - # TODO: Handle other connectors -- create map based on model_connector_str + # TODO: Create a map to load the required model_connectors import geojson_modelica_translator.model_connectors.teaser + import geojson_modelica_translator.model_connectors.spawn mc_klass = getattr(geojson_modelica_translator.model_connectors.teaser, model_connector_str) + model_connector = mc_klass(self.system_parameters) - _log.info('Exporting to Modelica') + _log.info("Exporting to Modelica") for building in self.buildings: - _log.info(f'Adding building to model connector: {mc_klass.__class__}') + # print("Jing2: ", building.feature.properties["type"]) + _log.info(f"Adding building to model connector: {mc_klass.__class__}") model_connector.add_building(building) - _log.info(f'Translating building to model {building}') - model_connector.to_modelica(project_name, self.loads_path.files_dir, keep_original_models=False) + _log.info(f"Translating building to model {building}") + model_connector.to_modelica(self.scaffold, keep_original_models=False) # add in Substations + # TODO: YL, where are the substations/ETSs? + # add in Districts # add in Plants # now add in the top level package. - pp = PackageParser.new_from_template(prj_dir, project_name, ['Loads']) + pp = PackageParser.new_from_template(self.scaffold.project_path, project_name, ["Loads"]) pp.save() # TODO: BuildingModelClass diff --git a/geojson_modelica_translator/model_connectors/base.py b/geojson_modelica_translator/model_connectors/base.py index 3405b09d6..f9bd1980d 100644 --- a/geojson_modelica_translator/model_connectors/base.py +++ b/geojson_modelica_translator/model_connectors/base.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. diff --git a/geojson_modelica_translator/model_connectors/ets_template.py b/geojson_modelica_translator/model_connectors/ets_template.py new file mode 100644 index 000000000..08d4883f8 --- /dev/null +++ b/geojson_modelica_translator/model_connectors/ets_template.py @@ -0,0 +1,209 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import json +import os + +from jinja2 import Environment, FileSystemLoader + + +class ETSTemplate: + """This class will template the ETS modelica model.""" + + def __init__(self, thermal_junction_properties_geojson, system_parameters_geojson, ets_from_building_modelica): + """ + thermal_junction_properties_geojson contains the ETS at brief and at higher level; + system_parameters_geojson contains the ETS with details ; + ets_from_building_modelica contains the modelica model of ETS ; + """ + super().__init__() + + self.thermal_junction_properties_geojson = thermal_junction_properties_geojson + self.thermal_junction_properties_geojson = self.thermal_junction_properties_geojson.replace("\\", "/") + + self.system_parameters_geojson = system_parameters_geojson + if "\\" in self.system_parameters_geojson: + self.system_parameters_geojson = self.system_parameters_geojson.replace("\\", "/") + + self.ets_from_building_modelica = ets_from_building_modelica + if "\\" in self.ets_from_building_modelica: + self.ets_from_building_modelica = self.ets_from_building_modelica.replace("\\", "/") + + # get the path of modelica-buildings library + # temporarily copied here to reduce repo size + directory_up_one_level = os.path.abspath(os.path.join(__file__, "../..")) + self.directory_modelica_building = os.path.join( + directory_up_one_level + "/modelica/CoolingIndirect.mo" + ) + if "\\" in self.directory_modelica_building: + self.directory_modelica_building = self.directory_modelica_building.replace("\\", "/") + + # go up two levels of directory, to get the path of tests folder for ets + # TODO: we shouldn't be writing to the test directory in this file, only in tests. + directory_up_two_levels = os.path.abspath(os.path.join(__file__, "../../..")) + self.directory_ets_templated = os.path.join( + directory_up_two_levels + "/tests/output_ets" + ) + if "\\" in self.directory_ets_templated: + self.directory_ets_templated = self.directory_ets_templated.replace("\\", "/") + + if not os.path.isdir(self.directory_ets_templated): + os.mkdir(self.directory_ets_templated) + else: + pass + + # here comes the Jinja2 function: Environment() + # it loads all the "*.mot" files into an environment by Jinja2 + self.template_env = Environment( + loader=FileSystemLoader( + searchpath=os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates") + ) + ) + + def check_ets_thermal_junction(self): + """check if ETS info are in thermal-junction-geojson file""" + with open(self.thermal_junction_properties_geojson, "r") as f: + data = json.load(f) + + ets_general = False + for key, value in data.items(): + if key == "definitions": + # three levels down to get the ETS signal + junctions = data["definitions"]["ThermalJunctionType"]["enum"] + if "ETS" in junctions: + ets_general = True + else: + pass + + return ets_general + + def check_ets_system_parameters(self): + """check detailed parameters of ETS""" + with open(self.system_parameters_geojson, "r") as f: + data = json.load(f) + + ets_parameters = False + # four levels down to get the ets model description + # ets_overall = data["definitions"]["building_def"]["properties"]["ets"] + # three levels down to get the parameters + ets_parameters = data["definitions"]["ets_parameters"]["properties"] + # print ("est_parameters are: ", type(ets_parameters) ) + return ets_parameters + + def check_ets_from_building_modelica(self): + """check if ETS-indirectCooling are in modelica building library""" + ets_modelica_available = os.path.isfile(self.ets_from_building_modelica) + + return ets_modelica_available + + def to_modelica(self): + """convert ETS json to modelica""" + # Here come the Jinja2 function: get_template(), which reads into templated ets model. + # CoolingIndirect.mot was manually created as a starting point, by adding stuff following Jinja2 syntax. + # it has all the necessary parameters which need to be changed through templating. + ets_template = self.template_env.get_template("CoolingIndirect.mot") + + # TODO: Seems like the ets_data below should allow defaults from + # the system parameters JSON file, correct? + # ets model parameters are from the schema.json file, default values only. + ets_data = self.check_ets_system_parameters() + + # Here comes the Jina2 function: render() + file_data = ets_template.render(ets_data=ets_data) + + # write templated ETS back to modelica file , to the tests folder for Dymola test + path_ets_templated = os.path.join(self.directory_ets_templated, "ets_cooling_indirect_templated.mo") + + if os.path.exists(path_ets_templated): + os.remove(path_ets_templated) + with open(path_ets_templated, "w") as f: + f.write(file_data) + + # write templated ETS back to building-modelica folder for Dymola test + path_writtenback = os.path.join(os.path.abspath(os.path.join(__file__, "../..")) + "/modelica/") + + if os.path.exists(os.path.join(path_writtenback, "ets_cooling_indirect_templated.mo")): + os.remove(os.path.join(path_writtenback, "ets_cooling_indirect_templated.mo")) + with open(os.path.join(path_writtenback, "ets_cooling_indirect_templated.mo"), "w") as f: + f.write(file_data) + + return file_data + + def templated_ets_openloops_dymola(self): + """after we creating the templated ets, we need to test it in Dymola under open loops. + Here we refactor the example file: CoolingIndirectOpenLoops, + to test our templated ets model. + """ + file = open( + os.path.join(os.getcwd(), "geojson_modelica_translator") + "/modelica/CoolingIndirectOpenLoops.mo", "r", + ) + cooling_indirect_filename = "/CoolingIndirectOpenLoops_Templated.mo" + + # if the modelica example file is existed, delete it first + path_openloops = os.path.join(os.path.abspath(os.path.join(__file__, "../..")) + "/modelica/") + + if os.path.exists(path_openloops + cooling_indirect_filename): + os.remove(path_openloops + cooling_indirect_filename) + + # create the modelica example file for Dymola test + # TODO: Replace this with the ModelicaFile Class -- + # extend ModelicaFile class if does not support. + # Theoretically it is doable using extend clause from Modelica. + # But we need to change the original ETS model first, in order to extend. + # This is Michael Wetter suggested approach. + # if so, we don't need to template modelica models, but we need to connect the modelica components + repl_dict = {} + from_str = "model CoolingIndirectOpenLoops" + to_str = "model CoolingIndirectOpenLoops_Templated\n" + repl_dict[from_str] = to_str + from_str = ( + "Buildings.Applications.DHC.EnergyTransferStations.CoolingIndirect coo(" + ) + to_str = "Buildings.Applications.DHC.EnergyTransferStations.ets_cooling_indirect_templated coo(" + repl_dict[from_str] = to_str + from_str = "end CoolingIndirectOpenLoops;" + to_str = "end CoolingIndirectOpenLoops_Templated;" + repl_dict[from_str] = to_str + + with open(path_openloops + cooling_indirect_filename, "w") as examplefile: + for f in file: + fx = f + for from_str, to_str in repl_dict.items(): + # TODO: f.string() causes errors, check code + if fx.strip() == from_str.strip(): + fx = f.replace(from_str, to_str) + examplefile.write(fx) + + return examplefile + + def connect(self): + """connect ETS-modelica to building-modelica (specifically TEASER modelica). + This function will be modified in future""" + pass diff --git a/geojson_modelica_translator/model_connectors/spawn.py b/geojson_modelica_translator/model_connectors/spawn.py index a017146c3..1ac0d63ea 100644 --- a/geojson_modelica_translator/model_connectors/spawn.py +++ b/geojson_modelica_translator/model_connectors/spawn.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -31,11 +31,11 @@ import os import shutil -from jinja2 import FileSystemLoader, Environment - -from geojson_modelica_translator.model_connectors.base import Base as model_connector_base +from geojson_modelica_translator.model_connectors.base import \ + Base as model_connector_base from geojson_modelica_translator.modelica.input_parser import PackageParser from geojson_modelica_translator.utils import ModelicaPath +from jinja2 import Environment, FileSystemLoader class SpawnConnector(model_connector_base): @@ -43,7 +43,11 @@ def __init__(self, system_parameters): super().__init__(system_parameters) self.template_env = Environment( - loader=FileSystemLoader(searchpath=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')) + loader=FileSystemLoader( + searchpath=os.path.join( + os.path.dirname(os.path.abspath(__file__)), "templates" + ) + ) ) def add_building(self, urbanopt_building, mapper=None): @@ -56,41 +60,30 @@ def add_building(self, urbanopt_building, mapper=None): # TODO: Need to convert units, these should exist on the urbanopt_building object # TODO: Abstract out the GeoJSON functionality if mapper is None: - self.buildings.append({ - "area": urbanopt_building.feature.properties['floor_area'] * 0.092936, # ft2 -> m2 - "building_id": urbanopt_building.feature.properties['id'], - "building_type": urbanopt_building.feature.properties['building_type'], - "floor_height": urbanopt_building.feature.properties['height'] * 0.3048, # ft -> m - "num_stories": urbanopt_building.feature.properties['number_of_stories_above_ground'], - "num_stories_below_grade": urbanopt_building.feature.properties['number_of_stories'] - - urbanopt_building.feature.properties['number_of_stories_above_ground'], - "year_built": urbanopt_building.feature.properties['year_built'] - }) - - def lookup_building_type(self, building_type): - """ - This needs to be updated to decide which building to reference if no IDF is included. - - :param building_type: - :return: - """ - if 'office' in building_type.lower(): - return 'office' - else: - # TODO: define these mappings 'office', 'institute', 'institute4', institute8' - return 'office' + number_stories = urbanopt_building.feature.properties["number_of_stories"] + number_stories_above_ground = urbanopt_building.feature.properties["number_of_stories_above_ground"] + self.buildings.append( + { + "area": urbanopt_building.feature.properties["floor_area"] * 0.092936, # ft2 -> m2 + "building_id": urbanopt_building.feature.properties["id"], + "building_type": urbanopt_building.feature.properties["building_type"], + "floor_height": urbanopt_building.feature.properties["height"] * 0.3048, # ft -> m + "num_stories": urbanopt_building.feature.properties["number_of_stories_above_ground"], + "num_stories_below_grade": number_stories - number_stories_above_ground, + "year_built": urbanopt_building.feature.properties["year_built"], + } + ) - def to_modelica(self, project_name, root_building_dir): + def to_modelica(self, scaffold): """ Create spawn models based on the data in the buildings and geojsons - :param project_name: str, Name of the overall project that will be set in the modelica file - :param root_building_dir: str, root directory where building model will be exported + :param scaffold: Scaffold object, Scaffold of the entire directory of the project. """ curdir = os.getcwd() - loads_root_path = os.path.join(root_building_dir, project_name, 'Loads') - spawn_fmu_template = self.template_env.get_template('spawn_fmu.mot') - spawn_mos_template = self.template_env.get_template('RunSpawnBuilding.mot') + spawn_coupling_template = self.template_env.get_template("spawn_coupling.mot") + spawn_building_template = self.template_env.get_template("spawn_building.mot") + spawn_mos_template = self.template_env.get_template("RunSpawnBuilding.most") building_names = [] try: for building in self.buildings: @@ -99,21 +92,25 @@ def to_modelica(self, project_name, root_building_dir): # Path for building data building_names.append(f"B{building['building_id']}") - b_modelica_path = ModelicaPath(f"B{building['building_id']}", loads_root_path, True) + b_modelica_path = ModelicaPath( + f"B{building['building_id']}", scaffold.loads_path.files_dir, True + ) # grab the data from the system_parameter file for this building id # TODO: create method in system_parameter class to make this easier and respect the defaults idf_filename = self.system_parameters.get_param_by_building_id( - building['building_id'], 'load_model_parameters.spawn.idf_filename' + building["building_id"], "load_model_parameters.spawn.idf_filename" ) epw_filename = self.system_parameters.get_param_by_building_id( - building['building_id'], 'load_model_parameters.spawn.epw_filename' + building["building_id"], "load_model_parameters.spawn.epw_filename" ) mos_weather_filename = self.system_parameters.get_param_by_building_id( - building['building_id'], 'load_model_parameters.spawn.mos_weather_filename' + building["building_id"], + "load_model_parameters.spawn.mos_weather_filename", ) thermal_zones = self.system_parameters.get_param_by_building_id( - building['building_id'], 'load_model_parameters.spawn.thermal_zone_names' + building["building_id"], + "load_model_parameters.spawn.thermal_zone_names", ) # construct th dict to pass into the template @@ -122,76 +119,92 @@ def to_modelica(self, project_name, root_building_dir): "idf": { "idf_filename": idf_filename, "filename": os.path.basename(idf_filename), - "path": os.path.dirname(idf_filename) + "path": os.path.dirname(idf_filename), }, "epw": { "epw_filename": epw_filename, "filename": os.path.basename(epw_filename), - "path": os.path.dirname(epw_filename) + "path": os.path.dirname(epw_filename), }, "mos_weather": { "mos_weather_filename": mos_weather_filename, "filename": os.path.basename(mos_weather_filename), - "path": os.path.dirname(mos_weather_filename) + "path": os.path.dirname(mos_weather_filename), }, - "thermal_zones": [] + "thermal_zones": [], + "thermal_zones_count": len(thermal_zones), + } for tz in thermal_zones: # TODO: method for creating nice zone names for modelica - template_data['thermal_zones'].append({ - "modelica_object_name": f"zn{tz}", - "spawn_object_name": tz - }) + template_data["thermal_zones"].append( + {"modelica_object_name": f"zn{tz}", "spawn_object_name": tz} + ) # copy over the resource files for this building # TODO: move some of this over to a validation step - if os.path.exists(template_data['idf']['idf_filename']): + if os.path.exists(template_data["idf"]["idf_filename"]): shutil.copy( - template_data['idf']['idf_filename'], - os.path.join(b_modelica_path.resources_dir, template_data['idf']['filename']) + template_data["idf"]["idf_filename"], + os.path.join( + b_modelica_path.resources_dir, + template_data["idf"]["filename"], + ), ) else: - raise Exception(f"Missing IDF file for Spawn: {template_data['idf']['idf_filename']}") - - if os.path.exists(template_data['epw']['epw_filename']): - shutil.copy( - template_data['epw']['epw_filename'], - os.path.join(b_modelica_path.resources_dir, template_data['epw']['filename']) + raise Exception( + f"Missing IDF file for Spawn: {template_data['idf']['idf_filename']}" ) + + if os.path.exists(template_data["epw"]["epw_filename"]): + shutil.copy(template_data["epw"]["epw_filename"], + os.path.join(b_modelica_path.resources_dir, template_data["epw"]["filename"])) else: - raise Exception(f"Missing epw file for Spawn: {template_data['epw']['epw_filename']}") + raise Exception(f"Missing EPW file for Spawn: {template_data['epw']['epw_filename']}") - if os.path.exists(template_data['mos_weather']['mos_weather_filename']): + if os.path.exists(template_data["mos_weather"]["mos_weather_filename"]): shutil.copy( - template_data['mos_weather']['mos_weather_filename'], - os.path.join(b_modelica_path.resources_dir, template_data['mos_weather']['filename']) + template_data["mos_weather"]["mos_weather_filename"], + os.path.join(b_modelica_path.resources_dir, template_data["mos_weather"]["filename"]) ) else: raise Exception( - f"Missing mos weather file for Spawn: {template_data['mos_weather']['mos_weather_filename']}") + f"Missing MOS weather file for Spawn: {template_data['mos_weather']['mos_weather_filename']}") - file_data = spawn_fmu_template.render( - project_name=project_name, + # Run the templating + file_data = spawn_building_template.render( + project_name=scaffold.project_name, model_name=f"B{building['building_id']}", data=template_data, ) - with open(os.path.join(os.path.join(b_modelica_path.files_dir, 'building.mo')), 'w') as f: + with open(os.path.join(os.path.join(b_modelica_path.files_dir, "building.mo")), "w") as f: f.write(file_data) - file_data = spawn_mos_template.render( - project_name=project_name, + full_model_name = os.path.join( + scaffold.project_name, + scaffold.loads_path.files_relative_dir, + f"B{building['building_id']}", + "coupling").replace(os.path.sep, '.') + + file_data = spawn_mos_template.render(full_model_name=full_model_name) + with open(os.path.join(os.path.join(b_modelica_path.scripts_dir, "RunSpawnBuilding.mos")), "w") as f: + f.write(file_data) + + file_data = spawn_coupling_template.render( + project_name=scaffold.project_name, model_name=f"B{building['building_id']}", data=template_data, ) - with open(os.path.join(os.path.join(b_modelica_path.resources_dir, 'RunSpawnBuilding.mos')), 'w') as f: + with open(os.path.join(os.path.join(b_modelica_path.files_dir, "coupling.mo")), "w") as f: f.write(file_data) + finally: os.chdir(curdir) # run post process to create the remaining project files for this building - self.post_process(project_name, root_building_dir, building_names) + self.post_process(scaffold, building_names) - def post_process(self, project_name, root_building_dir, building_names): + def post_process(self, scaffold, building_names): """ Cleanup the export of Spawn files into a format suitable for the district-based analysis. This includes the following: @@ -199,27 +212,27 @@ def post_process(self, project_name, root_building_dir, building_names): * Add a Loads project * Add a project level project - :param project_name: string, name of the project which will be used to set the package.mo file - :param root_building_dir: string, where the project will be ultimately saved + :param scaffold: Scaffold object, Scaffold of the entire directory of the project. :param building_names: list, names of the buildings that need to be cleaned up after export :return: None """ - loads_root_path = os.path.join(root_building_dir, project_name, 'Loads') for b in building_names: - b_modelica_path = os.path.join(loads_root_path, b) + b_modelica_path = os.path.join(scaffold.loads_path.files_dir, b) new_package = PackageParser.new_from_template( - b_modelica_path, b, ['building'], within=f"{project_name}.Loads" + b_modelica_path, b, ["building", "coupling"], within=f"{scaffold.project_name}.Loads" ) new_package.save() # now create the Loads level package. This (for now) will create the package without considering any existing # files in the Loads directory. package = PackageParser.new_from_template( - loads_root_path, 'Loads', building_names, within=f'{project_name}' + scaffold.loads_path.files_dir, "Loads", building_names, within=f"{scaffold.project_name}" ) package.save() # now create the Package level package. This really needs to happen at the GeoJSON to modelica stage, but - # do it here for now for testing. - pp = PackageParser.new_from_template(os.path.join(root_building_dir, project_name), project_name, ['Loads']) + # do it here for now to aid in testing. + pp = PackageParser.new_from_template( + scaffold.project_path, scaffold.project_name, ["Loads"] + ) pp.save() diff --git a/geojson_modelica_translator/model_connectors/teaser.py b/geojson_modelica_translator/model_connectors/teaser.py index 01d226de0..80c3ca8bc 100644 --- a/geojson_modelica_translator/model_connectors/teaser.py +++ b/geojson_modelica_translator/model_connectors/teaser.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -32,11 +32,14 @@ import os import shutil -from teaser.project import Project - -from geojson_modelica_translator.model_connectors.base import Base as model_connector_base -from geojson_modelica_translator.modelica.input_parser import InputParser, PackageParser +from geojson_modelica_translator.model_connectors.base import \ + Base as model_connector_base +from geojson_modelica_translator.modelica.input_parser import ( + InputParser, + PackageParser +) from geojson_modelica_translator.utils import ModelicaPath, copytree +from teaser.project import Project class TeaserConnector(model_connector_base): @@ -51,32 +54,38 @@ def add_building(self, urbanopt_building, mapper=None): """ # TODO: Need to convert units, these should exist on the urbanopt_building object # TODO: Abstract out the GeoJSON functionality + # note-1(Yanfei): any building/district geojson file needs to have the following properties. + # note-2(Yanfei): there is a need to clean the building/district geojson file, before making into modelica if mapper is None: - self.buildings.append({ - "area": urbanopt_building.feature.properties['floor_area'] * 0.092936, # ft2 -> m2 - "building_id": urbanopt_building.feature.properties['id'], - "building_type": urbanopt_building.feature.properties['building_type'], - "floor_height": urbanopt_building.feature.properties['height'] * 0.3048, # ft -> m - "num_stories": urbanopt_building.feature.properties['number_of_stories_above_ground'], - "num_stories_below_grade": urbanopt_building.feature.properties['number_of_stories'] - - urbanopt_building.feature.properties['number_of_stories_above_ground'], - "year_built": urbanopt_building.feature.properties['year_built'] - }) + number_stories = urbanopt_building.feature.properties["number_of_stories"] + # print("Jing: ", urbanopt_building.feature.properties.keys()) + number_stories_above_ground = urbanopt_building.feature.properties["number_of_stories_above_ground"] + self.buildings.append( + { + "area": urbanopt_building.feature.properties["floor_area"] * 0.092936, # ft2 -> m2 + "building_id": urbanopt_building.feature.properties["id"], + "building_type": urbanopt_building.feature.properties["building_type"], + "floor_height": urbanopt_building.feature.properties["floor_height"], # ft -> m + "num_stories": urbanopt_building.feature.properties["number_of_stories"], + "num_stories_below_grade": number_stories - number_stories_above_ground, + "year_built": urbanopt_building.feature.properties["year_built"], + + } + ) def lookup_building_type(self, building_type): - if 'office' in building_type.lower(): - return 'office' + if "office" in building_type.lower(): + return "office" else: # TODO: define these mappings 'office', 'institute', 'institute4', institute8' - return 'office' + return "office" - def to_modelica(self, project_name, root_building_dir, keep_original_models=False): + def to_modelica(self, scaffold, keep_original_models=False): """ Save the TEASER representation of the buildings to the filesystem. The path will - be root_building_dir. + be scaffold.loads_path.files_dir. - :param project_name: str, Name of the overall project that will be set in the modelica file - :param root_building_dir: str, root directory where building model will be exported + :param scaffold: Scaffold object, contains all the paths of the project :param keep_original_models: boolean, whether or not to remove the models after exporting from Teaser """ @@ -86,39 +95,37 @@ def to_modelica(self, project_name, root_building_dir, keep_original_models=Fals try: prj = Project(load_data=True) for building in self.buildings: - building_name = building['building_id'] + building_name = building["building_id"] prj.add_non_residential( - method='bmvbs', - usage=self.lookup_building_type(building['building_type']), + method="bmvbs", + usage=self.lookup_building_type(building["building_type"]), name=building_name, - year_of_construction=building['year_built'], - number_of_floors=building['num_stories'], - height_of_floors=building['floor_height'], - net_leased_area=building['area'], + year_of_construction=building["year_built"], + number_of_floors=building["num_stories"], + height_of_floors=building["floor_height"], + net_leased_area=building["area"], office_layout=1, window_layout=1, with_ahu=False, - construction_type="heavy" + construction_type="heavy", ) building_names.append(building_name) - prj.used_library_calc = 'IBPSA' + prj.used_library_calc = "IBPSA" prj.number_of_elements_calc = self.system_parameters.get_param( - 'buildings.default.load_model_parameters.rc.order', default=2) + "buildings.default.load_model_parameters.rc.order", default=2 + ) prj.merge_windows_calc = False # calculate the properties of all the buildings and export to the Buildings library prj.calc_all_buildings() - prj.export_ibpsa( - library="Buildings", - path=os.path.join(curdir, root_building_dir) - ) + prj.export_ibpsa(library="Buildings", path=os.path.join(curdir, scaffold.loads_path.files_dir)) finally: os.chdir(curdir) - self.post_process(project_name, root_building_dir, building_names, keep_original_models=keep_original_models) + self.post_process(scaffold, building_names, keep_original_models=keep_original_models) - def post_process(self, project_name, root_building_dir, building_names, keep_original_models=False): + def post_process(self, scaffold, building_names, keep_original_models=False): """ Cleanup the export of the TEASER files into a format suitable for the district-based analysis. This includes the following: @@ -140,112 +147,126 @@ def post_process(self, project_name, root_building_dir, building_names, keep_ori string_replace_list = [] # create a new modelica based path for the buildings # TODO: make this work at the toplevel, somehow. - b_modelica_path = ModelicaPath(f'B{b}', root_building_dir, True) + b_modelica_path = ModelicaPath(f"B{b}", scaffold.loads_path.files_dir, True) # copy over the entire model to the new location - copytree(os.path.join(root_building_dir, f'Project/B{b}/B{b}_Models'), b_modelica_path.files_dir) + copytree( + os.path.join(scaffold.loads_path.files_dir, f"Project/B{b}/B{b}_Models"), + b_modelica_path.files_dir, + ) # read in the package to apply the changes as they other files are processed # TODO: these should be linked, so a rename method should act across the model and the package.order - package = PackageParser(os.path.join(root_building_dir, f'B{b}')) + package = PackageParser(os.path.join(scaffold.loads_path.files_dir, f"B{b}")) # move the internal gains files to a new resources folder - mat_files = glob.glob(os.path.join(root_building_dir, f'B{b}/*.mat')) + mat_files = glob.glob(os.path.join(scaffold.loads_path.files_dir, f"B{b}/*.txt")) for f in mat_files: - new_file_name = os.path.basename(f).replace(f'B{b}', '') - os.rename(f, f'{b_modelica_path.resources_dir}/{new_file_name}') + new_file_name = os.path.basename(f).replace(f"B{b}", "") + os.rename(f, f"{b_modelica_path.resources_dir}/{new_file_name}") string_replace_list.append( ( - f'Project/B{b}/B{b}_Models/{os.path.basename(f)}', - f'Loads/{b_modelica_path.resources_relative_dir}/{new_file_name}' + f"Project/B{b}/B{b}_Models/{os.path.basename(f)}", + f"Loads/{b_modelica_path.resources_relative_dir}/{new_file_name}", ) ) # process each of the building models - mo_files = glob.glob(os.path.join(root_building_dir, f'B{b}/*.mo')) + mo_files = glob.glob(os.path.join(scaffold.loads_path.files_dir, f"B{b}/*.mo")) for f in mo_files: - # ignore the package.mo file - if os.path.basename(f) == 'package.mo': + if os.path.basename(f) == "package.mo": continue mofile = InputParser(f) # previous paths and replace with the new one. # Make sure to update the names of any resources as well. - mofile.replace_within_string(f'{project_name}.Loads.B{b}') + mofile.replace_within_string(f"{scaffold.project_name}.Loads.B{b}") # remove ReaderTMY3 - mofile.remove_object('ReaderTMY3') + mofile.remove_object("ReaderTMY3") # updating path to internal loads for s in string_replace_list: - mofile.replace_model_string( - 'Modelica.Blocks.Sources.CombiTimeTable', 'internalGains', s[0], s[1] - ) + mofile.replace_model_string("Modelica.Blocks.Sources.CombiTimeTable", "internalGains", s[0], s[1]) # add heat port data = [ - 'annotation (Placement(transformation(extent={{-10,90},{10,110}}), iconTransformation(extent={{-10,90},{10,110}})));' # noqa + "annotation (Placement(transformation(extent={{-10,90},{10,110}}), " + "iconTransformation(extent={{-10,90},{10,110}})));" ] - mofile.add_model_object('Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a', 'port_a', data) + mofile.add_model_object( + "Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a", "port_a", data, + ) # add TAir output # TODO: read in the object by name -- parse the parenthetic content instance = 'TAir(\n quantity="ThermodynamicTemperature", unit="K", displayUnit="degC")' data = [ '"Room air temperature"', - 'annotation (Placement(transformation(extent={{100,-10},{120,10}})));' + "annotation (Placement(transformation(extent={{100,-10},{120,10}})));", ] - mofile.add_model_object('Buildings.Controls.OBC.CDL.Interfaces.RealOutput', instance, data) + mofile.add_model_object( + "Buildings.Controls.OBC.CDL.Interfaces.RealOutput", instance, data + ) - # update the weaBus connectors + # All existing weaDat.weaBus connections need to be updated to simply weaBus mofile.replace_connect_string('weaDat.weaBus', None, 'weaBus', None, True) + # Now remove the redundant weaBus -> weaBus connection + mofile.remove_connect_string('weaBus', 'weaBus') # add new port connections - if self.system_parameters.get_param('buildings.default.load_model_parameters.rc.order', default=2) == 1: # noqa - data = 'annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))' - mofile.add_connect('port_a', 'thermalZoneOneElement.intGainsConv', data) - - data = 'annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))' - mofile.add_connect('thermalZoneOneElement.TAir', 'TAir', data) - elif self.system_parameters.get_param('buildings.default.load_model_parameters.rc.order', default=2) == 2: # noqa - data = 'annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))' - mofile.add_connect('port_a', 'thermalZoneTwoElements.intGainsConv', data) - - data = 'annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))' - mofile.add_connect('thermalZoneTwoElements.TAir', 'TAir', data) - elif self.system_parameters.get_param('buildings.default.load_model_parameters.rc.order', default=2) == 4: # noqa - data = 'annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))' - mofile.add_connect('port_a', 'thermalZoneFourElements.intGainsConv', data) - - data = 'annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))' - mofile.add_connect('thermalZoneFourElements.TAir', 'TAir', data) + if self.system_parameters.get_param( + "buildings.default.load_model_parameters.rc.order", default=2) == 1: + data = "annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))" + mofile.add_connect("port_a", "thermalZoneOneElement.intGainsConv", data) + + data = "annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))" + mofile.add_connect("thermalZoneOneElement.TAir", "TAir", data) + elif self.system_parameters.get_param( + "buildings.default.load_model_parameters.rc.order", default=2) == 2: + data = "annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))" + mofile.add_connect("port_a", "thermalZoneTwoElements.intGainsConv", data) + + data = "annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))" + mofile.add_connect("thermalZoneTwoElements.TAir", "TAir", data) + elif self.system_parameters.get_param( + "buildings.default.load_model_parameters.rc.order", default=2) == 4: + data = "annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))" + mofile.add_connect("port_a", "thermalZoneFourElements.intGainsConv", data) + + data = "annotation (Line(points={{93,32},{98,32},{98,0},{110,0}}, color={0,0,127}))" + mofile.add_connect("thermalZoneFourElements.TAir", "TAir", data) # change the name of the modelica model to remove the building id, update in package too! - new_model_name = mofile.model['name'].split("_")[1] - package.rename_model(mofile.model['name'], new_model_name) - mofile.model['name'] = new_model_name + new_model_name = mofile.model["name"].split("_")[1] + package.rename_model(mofile.model["name"], new_model_name) + mofile.model["name"] = new_model_name # Save as the new filename (without building ID) - new_filename = os.path.join(root_building_dir, f'B{b}/{os.path.basename(f).split("_")[1]}') + new_filename = os.path.join( + scaffold.loads_path.files_dir, f'B{b}/{os.path.basename(f).split("_")[1]}' + ) mofile.save_as(new_filename) os.remove(f) # save the updated package.mo and package.order. new_package = PackageParser.new_from_template( - package.path, f'B{b}', package.order, within=f'{project_name}.Loads' + package.path, f"B{b}", package.order, within=f"{scaffold.project_name}.Loads" ) new_package.save() # remaining clean up tasks across the entire exported project if not keep_original_models: - shutil.rmtree(os.path.join(root_building_dir, 'Project')) + shutil.rmtree(os.path.join(scaffold.loads_path.files_dir, "Project")) # now create the Loads level package. This (for now) will create the package without considering any existing # files in the Loads directory. # add in the silly 'B' before the building names package = PackageParser.new_from_template( - root_building_dir, 'Loads', ['B' + b for b in building_names], within=f'{project_name}' + scaffold.loads_path.files_dir, + "Loads", ["B" + b for b in building_names], + within=f"{scaffold.project_name}" ) package.save() diff --git a/geojson_modelica_translator/model_connectors/templates/CoolingIndirect.mot b/geojson_modelica_translator/model_connectors/templates/CoolingIndirect.mot new file mode 100644 index 000000000..d6a8614de --- /dev/null +++ b/geojson_modelica_translator/model_connectors/templates/CoolingIndirect.mot @@ -0,0 +1,369 @@ + +within Buildings.Applications.DHC.EnergyTransferStations; + +model {{ets_data["ModelName"]}} + +{% raw %} + "Indirect cooling energy transfer station for district energy systems" + extends Buildings.Fluid.Interfaces.PartialFourPort( + redeclare final package Medium1 = Medium, + redeclare final package Medium2 = Medium); + + replaceable package Medium = + Modelica.Media.Interfaces.PartialMedium "Medium in the component"; +{% endraw %} + + {% for mflow_district in ets_data["NominalFlow_District"]%} + // mass flow rates + parameter Modelica.SIunits.MassFlowRate m1_flow_nominal( + final min=0, + start={{mflow_district}}) + "Nominal mass flow rate of primary (district) district cooling side"; + {% endfor %} + + {% for mflow_building in ets_data["NominalFlow_Building"]%} + parameter Modelica.SIunits.MassFlowRate m2_flow_nominal( + final min=0, + start=0.5) + "Nominal mass flow rate of secondary (building) district cooling side"; + {% endfor %} + + {% for dp in ets_data["PressureDrop_Valve"]%} + // Primary supply control valve + parameter Modelica.SIunits.PressureDifference dpValve_nominal( + final min=0, + final displayUnit="Pa")={{dp}} + "Nominal pressure drop of fully open control valve"; + {% endfor %} + + {% for dp in ets_data["PressureDrop_HX_Primary"]%} + // Heat exchanger + parameter Modelica.SIunits.PressureDifference dp1_nominal( + final min=0, + start={{dp}}, + final displayUnit="Pa") + "Nominal pressure difference on primary side" + annotation(Dialog(group="Heat exchanger")); + {% endfor %} + + {% for dp in ets_data["PressureDrop_HX_Secondary"]%} + parameter Modelica.SIunits.PressureDifference dp2_nominal( + final min=0, + start={{dp}}, + final displayUnit="Pa") + "Nominal pressure difference on secondary side" + annotation(Dialog(group="Heat exchanger")); + {% endfor %} + + {% raw %} + parameter Boolean use_Q_flow_nominal=true + "Set to true to specify Q_flow_nominal and temperatures, or to false to specify effectiveness" + annotation(Dialog(group="Heat exchanger")); + {% endraw %} + + {% for Q_flow in ets_data['Q_Flow_Nominal'] %} + parameter Modelica.SIunits.HeatFlowRate Q_flow_nominal( + final min=0, + start={{Q_flow}}) + "Nominal heat transfer" + {% raw %}annotation(Dialog(group="Heat exchanger"));{% endraw %} + {% endfor %} + + + {% for supply_water_temp_district in ets_data['SWT_District'] %} + parameter Modelica.SIunits.Temperature T_a1_nominal( + min=0+273, + max=100+273.15, + start={{supply_water_temp_district}}+273.15, + final displayUnit="K") + "Nominal temperature at port a1" + annotation(Dialog(group="Heat exchanger")); + {% endfor %} + + {% for supply_water_temp_building in ets_data['SWT_Building'] %} + parameter Modelica.SIunits.Temperature T_a2_nominal( + min=0+273, + max=100+273.15, + start={{supply_water_temp_building}}+273.15, + final displayUnit="K") + "Nominal temperature at port a2" + annotation(Dialog(group="Heat exchanger")); + {% endfor %} + + {% for efficiency in ets_data['Eta_Efficiency'] %} + parameter Modelica.SIunits.Efficiency eta( + final min=0, + final max=1)={{efficiency}} + "Constant effectiveness" + annotation(Dialog(group="Heat exchanger")); + {% endfor %} + + {%raw%} + // Controller parameters + parameter Modelica.Blocks.Types.SimpleController controllerType= + Modelica.Blocks.Types.SimpleController.PI + "Type of controller" + annotation(Dialog(tab="Controller")); + parameter Real k(final min=0, final unit="1") = 1 + "Gain of controller" + annotation(Dialog(tab="Controller")); + parameter Modelica.SIunits.Time Ti( + min=Modelica.Constants.small)=120 + "Time constant of integrator block" + annotation (Dialog(tab="Controller", enable= + controllerType == Modelica.Blocks.Types.SimpleController.PI or + controllerType == Modelica.Blocks.Types.SimpleController.PID)); + parameter Modelica.SIunits.Time Td(final min=0)=0.1 + "Time constant of derivative block" + annotation (Dialog(tab="Controller", enable= + controllerType == Modelica.Blocks.Types.SimpleController.PD or + controllerType == Modelica.Blocks.Types.SimpleController.PID)); + parameter Real yMax(start=1)=1 + "Upper limit of output" + annotation(Dialog(tab="Controller")); + parameter Real yMin=0 + "Lower limit of output" + annotation(Dialog(tab="Controller")); + parameter Real wp(final min=0) = 1 + "Set-point weight for Proportional block (0..1)" + annotation(Dialog(tab="Controller")); + parameter Real wd(final min=0) = 0 + "Set-point weight for Derivative block (0..1)" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real Ni(min=100*Modelica.Constants.eps) = 0.9 + "Ni*Ti is time constant of anti-windup compensation" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PI or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real Nd(min=100*Modelica.Constants.eps) = 10 + "The higher Nd, the more ideal the derivative block" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Modelica.Blocks.Types.InitPID initType= + Modelica.Blocks.Types.InitPID.DoNotUse_InitialIntegratorState + "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)" + annotation(Evaluate=true, Dialog(group="Initialization", tab="Controller")); + parameter Real xi_start=0 + "Initial or guess value value for integrator output (= integrator state)" + annotation (Dialog(group="Initialization", tab="Controller", + enable=controllerType==Modelica.Blocks.Types.SimpleController.PI or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real xd_start=0 + "Initial or guess value for state of derivative block" + annotation (Dialog(group="Initialization", tab="Controller", + enable=controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real yCon_start=0 + "Initial value of output from the controller" + annotation(Dialog(group="Initialization", tab="Controller", + enable=initType == Modelica.Blocks.Types.InitPID.InitialOutput)); + parameter Boolean reverseAction = true + "Set to true for throttling the water flow rate through a cooling coil controller" + annotation(Dialog(tab="Controller")); + + Modelica.Blocks.Interfaces.RealInput TSet( + final quantity="ThermodynamicTemperature", + final unit="K") + "Setpoint temperature" + annotation (Placement(transformation(extent={{-140,-20},{-100,20}}))); + + Modelica.Blocks.Interfaces.RealOutput Q_flow( + final quantity="Power", + final unit="W", + final displayUnit="kW") + "Measured power demand at the ETS" + annotation (Placement(transformation(extent={{100,140},{120,160}}))); + + Modelica.Blocks.Interfaces.RealOutput Q( + final quantity="Energy", + final unit="J", + final displayUnit="kWh") + "Measured energy consumption at the ETS" + annotation (Placement(transformation(extent={{100,100},{120,120}}))); + + Buildings.Fluid.HeatExchangers.PlateHeatExchangerEffectivenessNTU hex( + redeclare final package Medium1 = Medium, + redeclare final package Medium2 = Medium, + final m1_flow_nominal=m1_flow_nominal, + final m2_flow_nominal=m2_flow_nominal, + final dp1_nominal=dp1_nominal, + final dp2_nominal=dp2_nominal, + final configuration=Buildings.Fluid.Types.HeatExchangerConfiguration.CounterFlow, + final use_Q_flow_nominal=true, + final Q_flow_nominal=Q_flow_nominal, + final T_a1_nominal=T_a1_nominal, + final T_a2_nominal=T_a2_nominal) "Indirect cooling heat exchanger" + annotation (Placement(transformation(extent={{20,-10},{40,10}}))); + + Buildings.Controls.Continuous.LimPID con( + final controllerType=Modelica.Blocks.Types.SimpleController.PID, + final k=k, + final Td=Td, + final yMax=yMax, + final yMin=yMin, + final Ti=Ti, + final wp=wp, + final wd=wd, + final Ni=Ni, + final Nd=Nd, + final initType=Modelica.Blocks.Types.InitPID.InitialOutput, + final xi_start=xi_start, + final xd_start=xd_start, + final y_start=yCon_start, + final reverseAction=reverseAction) "Controller" + annotation (Placement(transformation(extent={{-90,-10},{-70,10}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort senTDisSup( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal) + "District-side (primary) supply temperature sensor" + annotation (Placement(transformation(extent={{-90,50},{-70,70}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort senTDisRet( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal) + "District-side (primary) return temperature sensor" + annotation (Placement(transformation(extent={{70,50},{90,70}}))); + + Modelica.Blocks.Continuous.Integrator int(k=1) "Integration" + annotation (Placement(transformation(extent={{60,120},{80,100}}))); + + Buildings.Fluid.Sensors.MassFlowRate senMasFlo( + redeclare package Medium = Medium) + annotation (Placement(transformation(extent={{-60,50},{-40,70}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiRet( + redeclare final package Medium = Medium, + final m_flow_nominal=m2_flow_nominal) + "Building-side (secondary) return temperature" + annotation (Placement(transformation(extent={{-70,-70},{-90,-50}}))); + + Buildings.Fluid.Actuators.Valves.TwoWayQuickOpening val( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal, + final dpValve_nominal=dpValve_nominal, + riseTime(displayUnit="s") = 60, + y_start=0) "District-side (primary) control valve" + annotation (Placement(transformation(extent={{-30,70},{-10,50}}))); + + Modelica.Blocks.Math.Gain cp(final k=cp_default) + "Specifc heat multiplier to calculate heat flow rate" + annotation (Placement(transformation(extent={{20,100},{40,120}}))); + + Modelica.Blocks.Math.Product pro "Product" + annotation (Placement(transformation(extent={{-20,100},{0,120}}))); + + Modelica.Blocks.Math.Add dTDis(k1=-1, k2=+1) + "Temperatur difference on the district side" + annotation (Placement(transformation(extent={{-60,106},{-40,126}}))); + +protected + final parameter Medium.ThermodynamicState sta_default = Medium.setState_pTX( + T=Medium.T_default, + p=Medium.p_default, + X=Medium.X_default) "Medium state at default properties"; + final parameter Modelica.SIunits.SpecificHeatCapacity cp_default= + Medium.specificHeatCapacityCp(sta_default) + "Specific heat capacity of the fluid"; + +equation + + connect(hex.port_a2, port_a2) annotation (Line(points={{40,-6},{60,-6},{60,-60}, + {100,-60}}, color={0,127,255})); + connect(hex.port_b1, senTDisRet.port_a) annotation (Line(points={{40,6},{60,6}, + {60,60},{70,60}}, color={0,127,255})); + connect(val.port_b, hex.port_a1) annotation (Line(points={{-10,60},{0,60},{0,6}, + {20,6}}, color={0,127,255})); + connect(senMasFlo.port_b, val.port_a) + annotation (Line(points={{-40,60},{-30,60}}, color={0,127,255})); + connect(port_a1, senTDisSup.port_a) + annotation (Line(points={{-100,60},{-90,60}}, color={0,127,255})); + connect(senTDisSup.port_b, senMasFlo.port_a) + annotation (Line(points={{-70,60},{-60,60}}, color={0,127,255})); + connect(port_b2, TBuiRet.port_b) + annotation (Line(points={{-100,-60},{-90,-60}}, color={0,127,255})); + connect(senTDisRet.port_b, port_b1) + annotation (Line(points={{90,60},{100,60}}, color={0,127,255})); + connect(TSet, con.u_s) + annotation (Line(points={{-120,0},{-106,0},{-106,0},{-92,0}}, + color={0,0,127})); + connect(con.u_m, TBuiRet.T) + annotation (Line(points={{-80,-12},{-80,-49}}, color={0,0,127})); + connect(con.y, val.y) + annotation (Line(points={{-69,0},{-20,0},{-20,48}}, color={0,0,127})); + connect(TBuiRet.port_a, hex.port_b2) annotation (Line(points={{-70,-60},{0,-60}, + {0,-6},{20,-6}}, color={0,127,255})); + connect(pro.y, cp.u) + annotation (Line(points={{1,110},{18,110}}, color={0,0,127})); + connect(senMasFlo.m_flow, pro.u2) + annotation (Line(points={{-50,71},{-50,104},{-22,104}}, color={0,0,127})); + connect(senTDisSup.T, dTDis.u1) + annotation (Line(points={{-80,71},{-80,122},{-62,122}}, color={0,0,127})); + connect(senTDisRet.T, dTDis.u2) annotation (Line(points={{80,71},{80,80},{-70, + 80},{-70,110},{-62,110}}, color={0,0,127})); + connect(dTDis.y, pro.u1) + annotation (Line(points={{-39,116},{-22,116}}, color={0,0,127})); + connect(cp.y, int.u) + annotation (Line(points={{41,110},{58,110}}, color={0,0,127})); + connect(int.y, Q) + annotation (Line(points={{81,110},{110,110}}, color={0,0,127})); + connect(Q_flow, cp.y) annotation (Line(points={{110,150},{50,150},{50,110},{41, + 110}}, color={0,0,127})); + +annotation (defaultComponentName="coo", + Icon(coordinateSystem(preserveAspectRatio=false), graphics={ + Rectangle( + extent={{-100,-56},{100,-64}}, + fillColor={0,0,0}, + fillPattern=FillPattern.Solid, + pattern=LinePattern.None), + Rectangle( + extent={{-100,64},{100,56}}, + fillColor={0,0,0}, + fillPattern=FillPattern.Solid, + pattern=LinePattern.None), + Rectangle( + extent={{-80,80},{80,-80}}, + lineColor={175,175,175}, + fillColor={35,138,255}, + fillPattern=FillPattern.Solid), + Text( + extent={{-52,40},{54,-40}}, + lineColor={0,0,0}, + fillColor={35,138,255}, + fillPattern=FillPattern.Solid, + textStyle={TextStyle.Bold}, + textString="ETS")}), Diagram( + coordinateSystem(preserveAspectRatio=false, extent={{-100,-100},{100,160}})), + Documentation(info=" +

+Indirect cooling energy transfer station (ETS) model that controls +the building chilled water supply temperature by modulating a +primary control valve on the district supply side. The design is +based on a typical district cooling ETS described in ASHRAE's + +District Cooling Guide. +As shown in the figure below, the building pumping design (constant, +variable) is specified on the building side, not within the ETS. +

+

+\"DHC.ETS.CoolingIndirect\"/ +

+

Reference

+

+American Society of Heating, Refrigeration and Air-Conditioning +Engineers. (2013). Chapter 5: End User Interface. In +District Cooling Guide. 1st Edition. +

+", revisions=" +
    +
  • +November 1, 2019, by Kathryn Hinkelman:
    +First implementation.
  • +
  • 12/15/2020, Yanfei Li, Templating ETS model.
  • +
+"));{% endraw %} +end {{ets_data["ModelName"]}}; diff --git a/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.most b/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.most new file mode 100644 index 000000000..9b673c3b3 --- /dev/null +++ b/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.most @@ -0,0 +1,133 @@ +old_hidden_avoid_double_computation=Hidden.AvoidDoubleComputation; +Hidden.AvoidDoubleComputation=true; +simulateModel("{{full_model_name}}", + method="cvode", + tolerance=1e-6, + numberOfIntervals=500, + stopTime=604800.0, + resultFile="CouplingSpawnZ6"); +Hidden.AvoidDoubleComputation=old_hidden_avoid_double_computation; +createPlot( + id=1, + position={10, 20, 670, 900}, + y={ + "bui.maxTSet[1].y", + "bui.minTSet[1].y", + "bui.znAttic.TAir", + "bui.znCore_ZN.TAir", + "bui.znPerimeter_ZN_1.TAir", + "bui.znPerimeter_ZN_2.TAir", + "bui.znPerimeter_ZN_3.TAir", + "bui.znPerimeter_ZN_4.TAir" + }, + autoscale=true, + grid=true +); +createPlot( + id=1, + y={ + "bui.terUni[1].QActHea_flow", + "bui.terUni[2].QActHea_flow", + "bui.terUni[3].QActHea_flow", + "bui.terUni[4].QActHea_flow", + "bui.terUni[5].QActHea_flow", + "bui.terUni[6].QActHea_flow" + }, + grid=true, + subPlot=2 +); +createPlot( + id=1, + y={ + "bui.terUni[1].QActCoo_flow", + "bui.terUni[2].QActCoo_flow", + "bui.terUni[3].QActCoo_flow", + "bui.terUni[4].QActCoo_flow", + "bui.terUni[5].QActCoo_flow", + "bui.terUni[6].QActCoo_flow" + }, + grid=true, + subPlot=3 +); +createPlot( + id=2, + position={700, 20, 670, 900}, + y={ + "supHeaWat.T_in", + "bui.terUni[1].T_aHeaWat_nominal", + "bui.terUni[2].T_aHeaWat_nominal", + "bui.terUni[3].T_aHeaWat_nominal", + "bui.terUni[4].T_aHeaWat_nominal", + "bui.terUni[5].T_aHeaWat_nominal", + "bui.terUni[6].T_aHeaWat_nominal" + }, + autoscale=true, + grid=true +); +createPlot( + id=2, + y={ + "bui.disFloHea.mAct_flow[1].y", + "bui.disFloHea.mAct_flow[2].y", + "bui.disFloHea.mAct_flow[3].y", + "bui.disFloHea.mAct_flow[4].y", + "bui.disFloHea.mAct_flow[5].y", + "bui.disFloHea.mAct_flow[6].y" + }, + grid=true, + subPlot=2 +); +createPlot( + id=2, + y={ + "bui.terUni[1].QActHea_flow", + "bui.terUni[2].QActHea_flow", + "bui.terUni[3].QActHea_flow", + "bui.terUni[4].QActHea_flow", + "bui.terUni[5].QActHea_flow", + "bui.terUni[6].QActHea_flow" + }, + grid=true, + subPlot=3 +); +createPlot( + id=3, + position={1400, 20, 670, 900}, + y={ + "supChiWat.T_in", + "bui.terUni[1].T_aChiWat_nominal", + "bui.terUni[2].T_aChiWat_nominal", + "bui.terUni[3].T_aChiWat_nominal", + "bui.terUni[4].T_aChiWat_nominal", + "bui.terUni[5].T_aChiWat_nominal", + "bui.terUni[6].T_aChiWat_nominal" + }, + autoscale=true, + grid=true +); +createPlot( + id=3, + y={ + "bui.disFloCoo.mAct_flow[1].y", + "bui.disFloCoo.mAct_flow[2].y", + "bui.disFloCoo.mAct_flow[3].y", + "bui.disFloCoo.mAct_flow[4].y", + "bui.disFloCoo.mAct_flow[5].y", + "bui.disFloCoo.mAct_flow[6].y" + }, + grid=true, + subPlot=2 +); +createPlot( + id=3, + y={ + "bui.terUni[1].QActCoo_flow", + "bui.terUni[2].QActCoo_flow", + "bui.terUni[3].QActCoo_flow", + "bui.terUni[4].QActCoo_flow", + "bui.terUni[5].QActCoo_flow", + "bui.terUni[6].QActCoo_flow" + }, + grid=true, + subPlot=3 +); diff --git a/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.mot b/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.mot deleted file mode 100644 index 4b30f98d0..000000000 --- a/geojson_modelica_translator/model_connectors/templates/RunSpawnBuilding.mot +++ /dev/null @@ -1,2 +0,0 @@ -simulateModel("{{project_name}}.Loads.{{model_name}}",stopTime=604800, method="Cvode", tolerance=1e-06, resultFile="{{model_name}}"); -createPlot(id=1, position={55, 50, 1783, 995}, y={% raw %}{{% endraw %}{% for zone in data['thermal_zones']%}"{{zone['modelica_object_name']}}.TAir", {% endfor %} {% raw %} "weaDat.weaBus.TDryBul", "datRea.y[1]", "datRea.y[2]", "datRea.y[3]", "datRea.y[4]", "datRea.y[5]", "datRea.y[6]", "datRea.y[7]"}, range={0.0, 620000.0, -30.0, 25.0}, grid=true, colors={{28,108,200}, {238,46,47}, {0,140,72}, {217,67,180}, {0,0,0}, {162,29,33}, {244,125,35}, {102,44,145}, {28,108,200}, {238,46,47}, {0,140,72}, {217,67,180}, {0,0,0}, {162,29,33}}, patterns={LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Solid, LinePattern.Dash, LinePattern.Dash, LinePattern.Dash, LinePattern.Dash, LinePattern.Dash, LinePattern.Dash});{% endraw %} diff --git a/geojson_modelica_translator/model_connectors/templates/spawn_building.mot b/geojson_modelica_translator/model_connectors/templates/spawn_building.mot new file mode 100644 index 000000000..6e2d208d8 --- /dev/null +++ b/geojson_modelica_translator/model_connectors/templates/spawn_building.mot @@ -0,0 +1,230 @@ +within {{project_name}}.Loads.{{model_name}}; +model building + "n-zone EnergyPlus building model based on URBANopt GeoJSON export, with distribution pumps" + +extends Buildings.Applications.DHC.Loads.BaseClasses.PartialBuilding( + redeclare package Medium = MediumW, + have_eleHea=false, + have_eleCoo=false, + have_pum=true, + have_weaBus=false); + + replaceable package MediumW = Buildings.Media.Water + "Source side medium"; + replaceable package MediumA = Buildings.Media.Air + "Load side medium"; + parameter Real facSca = 1 + "Scaling factor to be applied to each extensive quantity"; + parameter Integer nZon={{data['thermal_zones_count']}} + "Number of thermal zones"; + // TODO: Verify that the paths work in JModelica, Dymola, and in py.tests. + parameter String idfPat= + "modelica://{{project_name}}/Loads/{{data['load_resources_path']}}/{{data['idf']['filename']}}" + "Path of the IDF file"; + parameter String weaPat= + "modelica://{{project_name}}/Loads/{{data['load_resources_path']}}/{{data['mos_weather']['filename']}}" + "Path of the weather file"; + // TODO: Minimum and Maximum TSet: make a function of the outdoor air temperature, type of building,occupancy schedule or woking/idle days? + {% raw %} + Buildings.Controls.OBC.CDL.Continuous.Sources.Constant minTSet[nZon]( + k=fill(20+273.15, nZon)) + "Minimum temperature setpoint" + annotation (Placement(transformation(extent={{-220,60},{-200,80}}))); + Buildings.Controls.OBC.CDL.Continuous.Sources.Constant maxTSet[nZon]( + k=fill(24+273.15, nZon)) + "Maximum temperature setpoint" + annotation (Placement(transformation(extent={{-220,20},{-200,40}}))); + Modelica.Blocks.Sources.Constant qConGai_flow(k=0) "Convective heat gain" + annotation (Placement(transformation(extent={{-66,128},{-46,148}}))); + Modelica.Blocks.Sources.Constant qRadGai_flow(k=0) "Radiative heat gain" + annotation (Placement(transformation(extent={{-66,168},{-46,188}}))); + Modelica.Blocks.Routing.Multiplex3 multiplex3_1 + annotation (Placement(transformation(extent={{-20,128},{0,148}}))); + Modelica.Blocks.Sources.Constant qLatGai_flow(k=0) "Latent heat gain" + annotation (Placement(transformation(extent={{-66,88},{-46,108}}))); + {% endraw %} + + // TODO: apply a dynamic layout + {% for zone in data['thermal_zones'] %} + Buildings.ThermalZones.EnergyPlus.ThermalZone {{zone['modelica_object_name']}}( + redeclare package Medium = MediumA, + nPorts=2, + zoneName="{{zone['spawn_object_name']}}") "Thermal zone" + {% raw %} annotation (Placement(transformation(extent={{40,-20},{80,20}}))); {% endraw %} + {% endfor %} + + {% raw %} + + inner Buildings.ThermalZones.EnergyPlus.Building building( + idfName=Modelica.Utilities.Files.loadResource(idfPat), + weaName=Modelica.Utilities.Files.loadResource(weaPat), + showWeatherData=false) + "Building outer component" + annotation (Placement(transformation(extent={{40,60},{60,80}}))); + + Buildings.Controls.OBC.CDL.Continuous.MultiSum mulSum(nin=nZon) + annotation (Placement(transformation(extent={{260,110},{280,130}}))); + Buildings.Controls.OBC.CDL.Continuous.MultiSum mulSum3(nin=2) + annotation (Placement(transformation(extent={{260,70},{280,90}}))); + + Buildings.Applications.DHC.Loads.Examples.BaseClasses.FanCoil4Pipe terUni[nZon]( + redeclare package Medium1 = MediumW, + redeclare package Medium2 = MediumA, + each facSca=facSca, + QHea_flow_nominal={50000,10000,10000,10000,10000,10000}, + QCoo_flow_nominal={-10000,-10000,-10000,-10000,-10000,-10000}, + each T_aLoaHea_nominal=293.15, + each T_aLoaCoo_nominal=297.15, + each T_bHeaWat_nominal=308.15, + each T_bChiWat_nominal=285.15, + each T_aHeaWat_nominal=313.15, + each T_aChiWat_nominal=280.15, + each mLoaHea_flow_nominal=5, + each mLoaCoo_flow_nominal=5) "Terminal unit" + annotation (Placement(transformation(extent={{-140,-2},{-120,20}}))); + + Buildings.Applications.DHC.Loads.BaseClasses.FlowDistribution disFloHea( + redeclare package Medium = MediumW, + m_flow_nominal=sum(terUni.mHeaWat_flow_nominal .* terUni.facSca), + have_pum=true, + dp_nominal=100000, + nPorts_a1=nZon, + nPorts_b1=nZon) + "Heating water distribution system" + annotation (Placement(transformation(extent={{-236,-188},{-216,-168}}))); + Buildings.Applications.DHC.Loads.BaseClasses.FlowDistribution disFloCoo( + redeclare package Medium = MediumW, + m_flow_nominal=sum(terUni.mChiWat_flow_nominal .* terUni.facSca), + typDis=Buildings.Applications.DHC.Loads.Types.DistributionType.ChilledWater, + dp_nominal=100000, + have_pum=true, + nPorts_a1=nZon, + nPorts_b1=nZon) + "Chilled water distribution system" + annotation (Placement(transformation(extent={{-160,-230},{-140,-210}}))); + {% endraw %} + +equation +{% raw %} + connect(qRadGai_flow.y,multiplex3_1.u1[1]) annotation (Line( + points={{-45,178},{-26,178},{-26,145},{-22,145}}, + color={0,0,127}, + smooth=Smooth.None)); + connect(qConGai_flow.y,multiplex3_1.u2[1]) annotation (Line( + points={{-45,138},{-22,138}}, + smooth=Smooth.None)); + connect(qLatGai_flow.y, multiplex3_1.u3[1]) annotation (Line( + points={{-22,131},{-26,131},{-26,98},{-45,98}}, + color={0,0,127})); + connect(disFloHea.port_a,ports_a[1]) annotation (Line( + points={{-300,0},{-280,0},{-280,-178},{-236,-178}}, + color={0,127,255})); + connect(disFloHea.port_b, ports_b[1]) annotation (Line( + points={{-216,-178},{260,-178},{260,0},{300,0}}, + color={0,127,255})); + connect(disFloCoo.port_a, ports_a[2]) annotation (Line( + points={{-300,0},{-280,0},{-280,-220},{-160,-220}}, + color={0,127,255})); + connect(disFloCoo.port_b, ports_b[2]) annotation (Line( + points={{-140,-220},{280,-220},{280,0},{300,0}}, + color={0,127,255})); + connect(mulSum.y, PFan) annotation (Line( + points={{282,120},{302,120},{302,120},{320,120}}, + color={0,0,127})); + connect(PPum, mulSum3.y) annotation (Line( + points={{320,80},{302,80},{302,80},{282,80}}, + color={0,0,127})); + connect(disFloHea.PPum, mulSum3.u[1]) annotation (Line( + points={{-215,-186},{220.5,-186},{220.5,81},{258,81}}, + color={0,0,127})); + connect(disFloCoo.PPum, mulSum3.u[2]) annotation (Line( + points={{-139,-228},{224,-228},{224,79},{258,79}}, + color={0,0,127})); + connect(disFloHea.QActTot_flow, QHea_flow) annotation (Line( + points={{-215,-184},{-2,-184},{-2,-182},{212,-182},{212,280},{320,280}}, + color={0,0,127})); + connect(disFloCoo.QActTot_flow, QCoo_flow) annotation (Line( + points={{-139,-226},{28,-226},{28,-224},{216,-224},{216,240},{320,240}}, + color={0,0,127})); +{% endraw %} + +for i in 1:nZon loop + connect(terUni[i].PFan, mulSum.u[i]) + {% raw %}annotation (Line(points={{-119.167,9},{-100,9},{-100,220},{220,220},{220,118.333},{258,118.333}},{% endraw %} + color={0,0,127})); + connect(disFloCoo.ports_a1[i], terUni[i].port_bChiWat) + {% raw %}annotation (Line(points={{-140,-214},{-38,-214},{-38,1.66667},{-120,1.66667}},{% endraw %} + color={0,127,255})); + connect(disFloCoo.ports_b1[i], terUni[i].port_aChiWat) + {% raw %}annotation (Line(points={{-160,-214},{-260,-214},{-260,1.66667},{-140,1.66667}},{% endraw %} + color={0,127,255})); + connect(disFloHea.ports_a1[i], terUni[i].port_bHeaWat) + {% raw %}annotation (Line(points={{-220,-164},{-40,-164},{-40,-0.166667},{-120,-0.166667}},{% endraw %} + color={0,127,255})); + connect(disFloHea.ports_b1[i], terUni[i].port_aHeaWat) + {% raw %}annotation (Line(points={{-240,-164},{-260,-164},{-260,-0.166667},{-140,-0.166667}},{% endraw %} + color={0,127,255})); + connect(terUni[i].mReqChiWat_flow, disFloCoo.mReq_flow[i]) + {% raw %}annotation (Line(points={{-119.167,3.5},{-104,3.5},{-104,-80},{-180,-80},{-180,-224},{-161,-224}},{% endraw %} + color={0,0,127})); + connect(terUni[i].mReqHeaWat_flow, disFloHea.mReq_flow[i]) + {% raw %}annotation (Line(points={{-119.167,5.33333},{-100,5.33333},{-100,-90.5},{-241,-90.5},{-241,-174}},{% endraw %} + color={0,0,127})); + connect(terUni[i].TSetHea, minTSet[i].y) + {% raw %}annotation (Line(points={{-140.833,14.5},{-160,14.5},{-160,70},{-198,70}},{% endraw %} + color={0,0,127})); + connect(terUni[i].TSetCoo, maxTSet[i].y) + {% raw %}annotation (Line(points={{-140.833,12.6667},{-164,12.6667},{-164,30},{-198,30}},{% endraw %} + color={0,0,127})); +end for; + +//----------------Depending on number of thermal zones----------------- +{% for zone in data['thermal_zones'] %} + connect(multiplex3_1.y, {{zone['modelica_object_name']}}.qGai_flow) + {% raw %}annotation (Line(points={{1,138},{20,138},{20,30},{22,30}}, color={0,0,127}));{% endraw %} + + connect ({{zone['modelica_object_name']}}.ports[1], terUni[{{loop.index}}].port_aLoa) + {% raw %} annotation(Line(points={{42,-119.2},{-8,-119.2},{-8,18.1667},{-120,18.1667}},{% endraw %} + color={0,127,255})); + + + connect ({{zone['modelica_object_name']}}.ports[2], terUni[{{loop.index}}].port_bLoa) + {% raw %} annotation (Line(points={{-140,18.1667},{-20,18.1667},{-20,-119.2},{46,-119.2}},{% endraw %} + color={0,127,255})); + + + connect ({{zone['modelica_object_name']}}.TAir, terUni[{{loop.index}}].TSen) + {% raw %}annotation (Line(points={{81,13.8},{80,13.8},{80,160},{-152,160},{-152,10.8333},{-140.833,10.8333}},{% endraw %} + color={0,0,127})); +{% endfor %} + +{% raw %} +annotation ( +Diagram(coordinateSystem(extent={{-300,-300},{300,300}})), +Icon(coordinateSystem(extent={{-100,-100},{100,100}}), graphics={ + Bitmap(extent={{-40,-24},{48,22}}, + fileName="modelica://Buildings/Resources/Images/ThermalZones/EnergyPlus/EnergyPlusLogo.png")}), +Documentation( info=" +

+ This is a simplified building model based on EnergyPlus + building envelope model. + It was generated from translating a GeoJSON model specified within URBANopt UI. + The heating and cooling loads are computed with a four-pipe + fan coil unit model derived from + + Buildings.Applications.DHC.Loads.BaseClasses.PartialTerminalUnit + and connected to the room model by means of fluid ports. +

+", revisions=" +
  • +March 12, 2020: Nicholas Long
    +Updated implemention to handle template needed for GeoJSON to Modelica. +
  • +
  • +February 21, 2020, by Antoine Gautier:
    +First implementation. +
  • +
+")); +{% endraw %} +end building; diff --git a/geojson_modelica_translator/model_connectors/templates/spawn_coupling.mot b/geojson_modelica_translator/model_connectors/templates/spawn_coupling.mot new file mode 100644 index 000000000..8ee21614c --- /dev/null +++ b/geojson_modelica_translator/model_connectors/templates/spawn_coupling.mot @@ -0,0 +1,128 @@ +within {{project_name}}.Loads.{{model_name}}; +model coupling + "FMU Template for Spawn" + extends Modelica.Icons.Example; + + replaceable package MediumW = Buildings.Media.Water + "Source side medium"; + replaceable package MediumA = Buildings.Media.Air + "Load side medium"; + +{% raw %} + // Do not fully qualify the path to building as {{project_name}}.Loads... That does + // not work in JModelica. + building bui( + nPorts_b=2, + nPorts_a=2) "Building spawn model" + annotation (Placement(transformation(extent={{40,-40},{60,-20}}))); + Buildings.Fluid.Sources.Boundary_pT sinHeaWat( + redeclare package Medium = MediumW, + nPorts=1) + "Sink for heating water" + annotation (Placement(transformation( + extent={{10,-10},{-10,10}}, + rotation=0, + origin={134,20}))); + Buildings.Fluid.Sources.Boundary_pT sinChiWat( + redeclare package Medium = MediumW, nPorts=1) + "Sink for chilled water" + annotation (Placement(transformation( + extent={{10,-10},{-10,10}}, + rotation=0, + origin={134,-40}))); + Modelica.Blocks.Sources.RealExpression THeaWatSup(y=max(bui.terUni.T_aHeaWat_nominal)) + "Heating water supply temperature" + annotation (Placement(transformation(extent={{-110,10},{-90,30}}))); + Modelica.Blocks.Sources.RealExpression TChiWatSup(y=min(bui.terUni.T_aChiWat_nominal)) + "Chilled water supply temperature" + annotation (Placement(transformation(extent={{-110,-50},{-90,-30}}))); + Buildings.Fluid.Sources.Boundary_pT supHeaWat( + redeclare package Medium = MediumW, + use_T_in=true, + nPorts=1) "Heating water supply" annotation (Placement(transformation( + extent={{-10,-10},{10,10}}, + rotation=0, + origin={-40,20}))); + Buildings.Fluid.Sources.Boundary_pT supChiWat( + redeclare package Medium = MediumW, + use_T_in=true, + nPorts=1) "Chilled water supply" annotation (Placement(transformation( + extent={{-10,-10},{10,10}}, + rotation=0, + origin={-40,-40}))); +{% endraw %} +equation +{% raw %} + connect(bui.ports_b[1],sinHeaWat. ports[1]) annotation (Line(points={{80,-50}, + {104,-50},{104,20},{124,20}},color={0,127,255})); + connect(bui.ports_b[2],sinChiWat. ports[1]) annotation (Line(points={{80,-46}, + {104,-46},{104,-40},{124,-40}}, color={0,127,255})); + connect(supHeaWat.T_in,THeaWatSup. y) annotation (Line(points={{-52,24},{-70,24}, + {-70,20},{-89,20}},color={0,0,127})); + connect(supHeaWat.ports[1], bui.ports_a[1]) annotation (Line(points={{-30,20}, + {-10,20},{-10,-50},{20,-50}},color={0,127,255})); + connect(TChiWatSup.y,supChiWat. T_in) annotation (Line(points={{-89,-40},{-70, + -40},{-70,-36},{-52,-36}}, color={0,0,127})); + connect(supChiWat.ports[1], bui.ports_a[2]) annotation (Line(points={{-30,-40}, + {-10,-40},{-10,-46},{20,-46}},color={0,127,255})); +{% endraw %} + + // TODO: determine how to handle the "lines" +{% raw %} + annotation (Diagram( + coordinateSystem(preserveAspectRatio=false, extent={{-120,-100},{160,60}}), + graphics={Text( + extent={{-46,36},{86,10}}, + lineColor={28,108,200}, + textString="")}), +{% endraw %} + __Dymola_Commands(file="modelica://{{project_name}}/Loads/Resources/Scripts/{{model_name}}/Dymola/RunSpawnBuilding.mos" +{% raw %} + "Simulate and plot"), + experiment( + StopTime=604800, + Tolerance=1e-06), + Documentation(info= +" +

+This example illustrates the use of + +Buildings.Applications.DHC.Loads.BaseClasses.PartialBuilding, + +Buildings.Applications.DHC.Loads.BaseClasses.PartialTerminalUnit +and + +Buildings.Applications.DHC.Loads.BaseClasses.FlowDistribution +in a configuration with: +

+
    +
  • +six-zone building model based on EnergyPlus envelope model (from +GeoJSON export), +
  • +
  • +secondary pumps. +
  • +
+

+Simulation with Dymola requires minimum version 2020x and setting +Hidden.AvoidDoubleComputation=true, see + +Buildings.ThermalZones.EnergyPlus.UsersGuide. +

+", +revisions= +" +
    +
  • +March 21, 2020, by Nicholas Long:
    +GeoJson-Modelica translator template first implementation. +
  • +
  • +February 21, 2020, by Antoine Gautier:
    +Model first implementation. +
  • +
+")); +{% endraw %} +end coupling; diff --git a/geojson_modelica_translator/model_connectors/templates/spawn_fmu.mot b/geojson_modelica_translator/model_connectors/templates/spawn_fmu.mot index 37c870083..6b3654127 100644 --- a/geojson_modelica_translator/model_connectors/templates/spawn_fmu.mot +++ b/geojson_modelica_translator/model_connectors/templates/spawn_fmu.mot @@ -20,13 +20,13 @@ model building Modelica.Blocks.Sources.Constant qLatGai_flow(k=0) "Latent heat gain" annotation (Placement(transformation(extent={{-74,-50},{-54,-30}})));{% endraw %} {% for zone in data['thermal_zones'] %} - Buildings.Experimental.EnergyPlus.ThermalZone {{zone['modelica_object_name']}}( + Buildings.ThermalZones.EnergyPlus.ThermalZone {{zone['modelica_object_name']}}( redeclare package Medium = Medium, idfName=idfName, weaName=weaName, energyDynamics=Modelica.Fluid.Types.Dynamics.FixedInitial, usePrecompiledFMU=false, - fmuName = Modelica.Utilities.Files.loadResource("modelica://Buildings/Resources/src/EnergyPlus/FMUs/Zones1.fmu"), + fmuName = Modelica.Utilities.Files.loadResource("modelica://Buildings/Resources/src/ThermalZones/EnergyPlus/FMUs/Zones1.fmu"), zoneName="{{zone['spawn_object_name']}}") "Thermal zone" {% raw %} annotation (Placement(transformation(extent={{20,40},{60,80}}))); {% endraw %} {% endfor %} @@ -59,7 +59,7 @@ First implementation. "), - __Dymola_Commands(file="modelica://Buildings/Resources/Scripts/Dymola/Experimental/EnergyPlus/Validation/RefBldgSmallOffice.mos" + __Dymola_Commands(file="modelica://Buildings/Resources/Scripts/Dymola/ThermalZones/EnergyPlus/Validation/RefBldgSmallOffice.mos" "Simulate and plot"), experiment( StopTime=604800, diff --git a/geojson_modelica_translator/modelica/CoolingIndirect.mo b/geojson_modelica_translator/modelica/CoolingIndirect.mo new file mode 100644 index 000000000..39fbf4606 --- /dev/null +++ b/geojson_modelica_translator/modelica/CoolingIndirect.mo @@ -0,0 +1,334 @@ +within Buildings.Applications.DHC.EnergyTransferStations; +model CoolingIndirect + "Indirect cooling energy transfer station for district energy systems" + extends Buildings.Fluid.Interfaces.PartialFourPort( + redeclare final package Medium1 = Medium, + redeclare final package Medium2 = Medium); + + replaceable package Medium = + Modelica.Media.Interfaces.PartialMedium "Medium in the component"; + + // mass flow rates + parameter Modelica.SIunits.MassFlowRate m1_flow_nominal( + final min=0, + start=0.5) + "Nominal mass flow rate of primary (district) district cooling side"; + parameter Modelica.SIunits.MassFlowRate m2_flow_nominal( + final min=0, + start=0.5) + "Nominal mass flow rate of secondary (building) district cooling side"; + + // Primary supply control valve + parameter Modelica.SIunits.PressureDifference dpValve_nominal( + final min=0, + final displayUnit="Pa")=6000 + "Nominal pressure drop of fully open control valve"; + + // Heat exchanger + parameter Modelica.SIunits.PressureDifference dp1_nominal( + final min=0, + start=500, + final displayUnit="Pa") + "Nominal pressure difference on primary side" + annotation(Dialog(group="Heat exchanger")); + parameter Modelica.SIunits.PressureDifference dp2_nominal( + final min=0, + start=500, + final displayUnit="Pa") + "Nominal pressure difference on secondary side" + annotation(Dialog(group="Heat exchanger")); + parameter Boolean use_Q_flow_nominal=true + "Set to true to specify Q_flow_nominal and temperatures, or to false to specify effectiveness" + annotation(Dialog(group="Heat exchanger")); + parameter Modelica.SIunits.HeatFlowRate Q_flow_nominal( + final min=0, + start=10000) + "Nominal heat transfer" + annotation(Dialog(group="Heat exchanger")); + parameter Modelica.SIunits.Temperature T_a1_nominal( + min=0+273, + max=100+273.15, + start=5+273.15, + final displayUnit="K") + "Nominal temperature at port a1" + annotation(Dialog(group="Heat exchanger")); + parameter Modelica.SIunits.Temperature T_a2_nominal( + min=0+273, + max=100+273.15, + start=7+273.15, + final displayUnit="K") + "Nominal temperature at port a2" + annotation(Dialog(group="Heat exchanger")); + parameter Modelica.SIunits.Efficiency eta( + final min=0, + final max=1)=0.8 + "Constant effectiveness" + annotation(Dialog(group="Heat exchanger")); + + // Controller parameters + parameter Modelica.Blocks.Types.SimpleController controllerType= + Modelica.Blocks.Types.SimpleController.PI + "Type of controller" + annotation(Dialog(tab="Controller")); + parameter Real k(final min=0, final unit="1") = 1 + "Gain of controller" + annotation(Dialog(tab="Controller")); + parameter Modelica.SIunits.Time Ti( + min=Modelica.Constants.small)=120 + "Time constant of integrator block" + annotation (Dialog(tab="Controller", enable= + controllerType == Modelica.Blocks.Types.SimpleController.PI or + controllerType == Modelica.Blocks.Types.SimpleController.PID)); + parameter Modelica.SIunits.Time Td(final min=0)=0.1 + "Time constant of derivative block" + annotation (Dialog(tab="Controller", enable= + controllerType == Modelica.Blocks.Types.SimpleController.PD or + controllerType == Modelica.Blocks.Types.SimpleController.PID)); + parameter Real yMax(start=1)=1 + "Upper limit of output" + annotation(Dialog(tab="Controller")); + parameter Real yMin=0 + "Lower limit of output" + annotation(Dialog(tab="Controller")); + parameter Real wp(final min=0) = 1 + "Set-point weight for Proportional block (0..1)" + annotation(Dialog(tab="Controller")); + parameter Real wd(final min=0) = 0 + "Set-point weight for Derivative block (0..1)" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real Ni(min=100*Modelica.Constants.eps) = 0.9 + "Ni*Ti is time constant of anti-windup compensation" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PI or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real Nd(min=100*Modelica.Constants.eps) = 10 + "The higher Nd, the more ideal the derivative block" + annotation(Dialog(tab="Controller", enable= + controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Modelica.Blocks.Types.InitPID initType= + Modelica.Blocks.Types.InitPID.DoNotUse_InitialIntegratorState + "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)" + annotation(Evaluate=true, Dialog(group="Initialization", tab="Controller")); + parameter Real xi_start=0 + "Initial or guess value value for integrator output (= integrator state)" + annotation (Dialog(group="Initialization", tab="Controller", + enable=controllerType==Modelica.Blocks.Types.SimpleController.PI or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real xd_start=0 + "Initial or guess value for state of derivative block" + annotation (Dialog(group="Initialization", tab="Controller", + enable=controllerType==Modelica.Blocks.Types.SimpleController.PD or + controllerType==Modelica.Blocks.Types.SimpleController.PID)); + parameter Real yCon_start=0 + "Initial value of output from the controller" + annotation(Dialog(group="Initialization", tab="Controller", + enable=initType == Modelica.Blocks.Types.InitPID.InitialOutput)); + parameter Boolean reverseAction = true + "Set to true for throttling the water flow rate through a cooling coil controller" + annotation(Dialog(tab="Controller")); + + Modelica.Blocks.Interfaces.RealInput TSet( + final quantity="ThermodynamicTemperature", + final unit="K") + "Setpoint temperature" + annotation (Placement(transformation(extent={{-140,-20},{-100,20}}))); + + Modelica.Blocks.Interfaces.RealOutput Q_flow( + final quantity="Power", + final unit="W", + final displayUnit="kW") + "Measured power demand at the ETS" + annotation (Placement(transformation(extent={{100,140},{120,160}}))); + + Modelica.Blocks.Interfaces.RealOutput Q( + final quantity="Energy", + final unit="J", + final displayUnit="kWh") + "Measured energy consumption at the ETS" + annotation (Placement(transformation(extent={{100,100},{120,120}}))); + + Buildings.Fluid.HeatExchangers.PlateHeatExchangerEffectivenessNTU hex( + redeclare final package Medium1 = Medium, + redeclare final package Medium2 = Medium, + final m1_flow_nominal=m1_flow_nominal, + final m2_flow_nominal=m2_flow_nominal, + final dp1_nominal=dp1_nominal, + final dp2_nominal=dp2_nominal, + final configuration=Buildings.Fluid.Types.HeatExchangerConfiguration.CounterFlow, + final use_Q_flow_nominal=true, + final Q_flow_nominal=Q_flow_nominal, + final T_a1_nominal=T_a1_nominal, + final T_a2_nominal=T_a2_nominal) "Indirect cooling heat exchanger" + annotation (Placement(transformation(extent={{20,-10},{40,10}}))); + + Buildings.Controls.Continuous.LimPID con( + final controllerType=Modelica.Blocks.Types.SimpleController.PID, + final k=k, + final Td=Td, + final yMax=yMax, + final yMin=yMin, + final Ti=Ti, + final wp=wp, + final wd=wd, + final Ni=Ni, + final Nd=Nd, + final initType=Modelica.Blocks.Types.InitPID.InitialOutput, + final xi_start=xi_start, + final xd_start=xd_start, + final y_start=yCon_start, + final reverseAction=reverseAction) "Controller" + annotation (Placement(transformation(extent={{-90,-10},{-70,10}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort senTDisSup( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal) + "District-side (primary) supply temperature sensor" + annotation (Placement(transformation(extent={{-90,50},{-70,70}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort senTDisRet( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal) + "District-side (primary) return temperature sensor" + annotation (Placement(transformation(extent={{70,50},{90,70}}))); + + Modelica.Blocks.Continuous.Integrator int(k=1) "Integration" + annotation (Placement(transformation(extent={{60,120},{80,100}}))); + + Buildings.Fluid.Sensors.MassFlowRate senMasFlo( + redeclare package Medium = Medium) + annotation (Placement(transformation(extent={{-60,50},{-40,70}}))); + + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiRet( + redeclare final package Medium = Medium, + final m_flow_nominal=m2_flow_nominal) + "Building-side (secondary) return temperature" + annotation (Placement(transformation(extent={{-70,-70},{-90,-50}}))); + + Buildings.Fluid.Actuators.Valves.TwoWayQuickOpening val( + redeclare final package Medium = Medium, + final m_flow_nominal=m1_flow_nominal, + final dpValve_nominal=dpValve_nominal, + riseTime(displayUnit="s") = 60, + y_start=0) "District-side (primary) control valve" + annotation (Placement(transformation(extent={{-30,70},{-10,50}}))); + + Modelica.Blocks.Math.Gain cp(final k=cp_default) + "Specifc heat multiplier to calculate heat flow rate" + annotation (Placement(transformation(extent={{20,100},{40,120}}))); + + Modelica.Blocks.Math.Product pro "Product" + annotation (Placement(transformation(extent={{-20,100},{0,120}}))); + + Modelica.Blocks.Math.Add dTDis(k1=-1, k2=+1) + "Temperatur difference on the district side" + annotation (Placement(transformation(extent={{-60,106},{-40,126}}))); + +protected + final parameter Medium.ThermodynamicState sta_default = Medium.setState_pTX( + T=Medium.T_default, + p=Medium.p_default, + X=Medium.X_default) "Medium state at default properties"; + final parameter Modelica.SIunits.SpecificHeatCapacity cp_default= + Medium.specificHeatCapacityCp(sta_default) + "Specific heat capacity of the fluid"; + +equation + + connect(hex.port_a2, port_a2) annotation (Line(points={{40,-6},{60,-6},{60,-60}, + {100,-60}}, color={0,127,255})); + connect(hex.port_b1, senTDisRet.port_a) annotation (Line(points={{40,6},{60,6}, + {60,60},{70,60}}, color={0,127,255})); + connect(val.port_b, hex.port_a1) annotation (Line(points={{-10,60},{0,60},{0,6}, + {20,6}}, color={0,127,255})); + connect(senMasFlo.port_b, val.port_a) + annotation (Line(points={{-40,60},{-30,60}}, color={0,127,255})); + connect(port_a1, senTDisSup.port_a) + annotation (Line(points={{-100,60},{-90,60}}, color={0,127,255})); + connect(senTDisSup.port_b, senMasFlo.port_a) + annotation (Line(points={{-70,60},{-60,60}}, color={0,127,255})); + connect(port_b2, TBuiRet.port_b) + annotation (Line(points={{-100,-60},{-90,-60}}, color={0,127,255})); + connect(senTDisRet.port_b, port_b1) + annotation (Line(points={{90,60},{100,60}}, color={0,127,255})); + connect(TSet, con.u_s) + annotation (Line(points={{-120,0},{-106,0},{-106,0},{-92,0}}, + color={0,0,127})); + connect(con.u_m, TBuiRet.T) + annotation (Line(points={{-80,-12},{-80,-49}}, color={0,0,127})); + connect(con.y, val.y) + annotation (Line(points={{-69,0},{-20,0},{-20,48}}, color={0,0,127})); + connect(TBuiRet.port_a, hex.port_b2) annotation (Line(points={{-70,-60},{0,-60}, + {0,-6},{20,-6}}, color={0,127,255})); + connect(pro.y, cp.u) + annotation (Line(points={{1,110},{18,110}}, color={0,0,127})); + connect(senMasFlo.m_flow, pro.u2) + annotation (Line(points={{-50,71},{-50,104},{-22,104}}, color={0,0,127})); + connect(senTDisSup.T, dTDis.u1) + annotation (Line(points={{-80,71},{-80,122},{-62,122}}, color={0,0,127})); + connect(senTDisRet.T, dTDis.u2) annotation (Line(points={{80,71},{80,80},{-70, + 80},{-70,110},{-62,110}}, color={0,0,127})); + connect(dTDis.y, pro.u1) + annotation (Line(points={{-39,116},{-22,116}}, color={0,0,127})); + connect(cp.y, int.u) + annotation (Line(points={{41,110},{58,110}}, color={0,0,127})); + connect(int.y, Q) + annotation (Line(points={{81,110},{110,110}}, color={0,0,127})); + connect(Q_flow, cp.y) annotation (Line(points={{110,150},{50,150},{50,110},{41, + 110}}, color={0,0,127})); + +annotation (defaultComponentName="coo", + Icon(coordinateSystem(preserveAspectRatio=false), graphics={ + Rectangle( + extent={{-100,-56},{100,-64}}, + fillColor={0,0,0}, + fillPattern=FillPattern.Solid, + pattern=LinePattern.None), + Rectangle( + extent={{-100,64},{100,56}}, + fillColor={0,0,0}, + fillPattern=FillPattern.Solid, + pattern=LinePattern.None), + Rectangle( + extent={{-80,80},{80,-80}}, + lineColor={175,175,175}, + fillColor={35,138,255}, + fillPattern=FillPattern.Solid), + Text( + extent={{-52,40},{54,-40}}, + lineColor={0,0,0}, + fillColor={35,138,255}, + fillPattern=FillPattern.Solid, + textStyle={TextStyle.Bold}, + textString="ETS")}), Diagram( + coordinateSystem(preserveAspectRatio=false, extent={{-100,-100},{100,160}})), + Documentation(info=" +

+Indirect cooling energy transfer station (ETS) model that controls +the building chilled water supply temperature by modulating a +primary control valve on the district supply side. The design is +based on a typical district cooling ETS described in ASHRAE's + +District Cooling Guide. +As shown in the figure below, the building pumping design (constant, +variable) is specified on the building side, not within the ETS. +

+

+\"DHC.ETS.CoolingIndirect\"/ +

+

Reference

+

+American Society of Heating, Refrigeration and Air-Conditioning +Engineers. (2013). Chapter 5: End User Interface. In +District Cooling Guide. 1st Edition. +

+", revisions=" +
    +
  • +November 1, 2019, by Kathryn Hinkelman:
    +First implementation.
  • +
+")); +end CoolingIndirect; diff --git a/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops.mo b/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops.mo new file mode 100644 index 000000000..1527b2e08 --- /dev/null +++ b/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops.mo @@ -0,0 +1,179 @@ +within Buildings.Applications.DHC.EnergyTransferStations.Examples; +model CoolingIndirectOpenLoops + "Example model for indirect cooling energy transfer station with open loops on the building and district sides" + extends Modelica.Icons.Example; + + package Medium = Buildings.Media.Water; + + parameter Modelica.SIunits.MassFlowRate m1_flow_nominal = 0.5 + "Nominal mass flow rate of primary (district) district cooling side"; + parameter Modelica.SIunits.MassFlowRate m2_flow_nominal = 0.5 + "Nominal mass flow rate of secondary (building) district cooling side"; + + Modelica.Blocks.Sources.Constant TSetCHWS(k=273.15 + 7) + "Setpoint temperature for building chilled water supply" + annotation (Placement(transformation(extent={{-120,-30},{-100,-10}}))); + Buildings.Fluid.Sources.Boundary_pT sinDis( + redeclare package Medium = Medium, + p=300000, + T=287.15, + nPorts=1) + "District-side (primary) sink" + annotation (Placement(transformation(extent={{80,40},{60,60}}))); + Buildings.Fluid.Sources.Boundary_pT souDis( + redeclare package Medium = Medium, + p(displayUnit="Pa") = 300000 + 800, + use_T_in=true, + T=278.15, + nPorts=1) + "District (primary) source" + annotation (Placement(transformation(extent={{-90,40},{-70,60}}))); + Buildings.Fluid.Sources.Boundary_pT sinBui( + redeclare package Medium = Medium, + use_T_in=false, + T=280.15, + nPorts=1) + "Building (secondary) sink (chilled water supply)" + annotation (Placement(transformation(extent={{-120,-100},{-100,-80}}))); + Buildings.Fluid.Sources.Boundary_pT souBui( + redeclare package Medium = Medium, + use_T_in=true, + T=289.15, + nPorts=1) + "Building (secondary) source (chilled water return)" + annotation (Placement(transformation(extent={{80,-100},{60,-80}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TDisSup( + redeclare package Medium = Medium, + m_flow_nominal=m1_flow_nominal, + T_start=278.15) + "District-side (primary) supply temperature sensor" + annotation (Placement(transformation(extent={{-40,40},{-20,60}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TDisRet( + redeclare package Medium = Medium, + m_flow_nominal=m1_flow_nominal, + T_start=287.15) + "District-side (primary) return temperature sensor" + annotation (Placement(transformation(extent={{20,40},{40,60}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiRet( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + T_start=289.15) + "Building-side (secondary) return temperature sensor" + annotation (Placement(transformation(extent={{40,-100},{20,-80}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiSup( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + T_start=280.15) + "Building-side (secondary) supply temperature sensor" + annotation (Placement(transformation(extent={{-50,-100},{-70,-80}}))); + Buildings.Applications.DHC.EnergyTransferStations.CoolingIndirect coo( + redeclare package Medium = Medium, + m1_flow_nominal=m1_flow_nominal, + m2_flow_nominal=m2_flow_nominal, + dp1_nominal = 500, + dp2_nominal = 500, + Q_flow_nominal=18514, + T_a1_nominal = 278.15, + T_a2_nominal = 289.15, + controllerType=Modelica.Blocks.Types.SimpleController.PI, + k=0.1, + Ti=40, + yMax=1, + yMin=0, + initType=Modelica.Blocks.Types.InitPID.InitialOutput, + yCon_start=0, + reverseAction=true) + "Indirect cooling ETS" + annotation (Placement(transformation(extent={{-10,-30},{10,-10}}))); + Buildings.Fluid.Movers.FlowControlled_m_flow pumBui( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + inputType=Buildings.Fluid.Types.InputType.Constant, + nominalValuesDefineDefaultPressureCurve=true, + dp_nominal=6000) + "Building-side (secondary) pump" + annotation (Placement(transformation(extent={{-20,-100},{-40,-80}}))); + Modelica.Blocks.Sources.Trapezoid tra( + amplitude=1.5, + rising(displayUnit="h") = 10800, + width(displayUnit="h") = 10800, + falling(displayUnit="h") = 10800, + period(displayUnit="h") = 43200, + offset=273 + 3.5) + "District supply temperature trapezoid signal" + annotation (Placement(transformation(extent={{-120,44},{-100,64}}))); + Modelica.Blocks.Sources.RealExpression TBuiRetSig( + y=(273.15 + 16) + 2*sin(time*2*3.14/86400)) + "Sinusoidal signal for return temperature on building (secondary) side" + annotation (Placement(transformation(extent={{120,-96},{100,-76}}))); + Modelica.Blocks.Math.Add TApp(k2=-1) "Calculate approach temperature" + annotation (Placement(transformation(extent={{0,90},{20,110}}))); + Modelica.Blocks.Math.Add dTDis(k1=-1) + "Calculate change in district temperature" + annotation (Placement(transformation(extent={{60,70},{80,90}}))); + Modelica.Blocks.Math.Add dTBui(k1=-1) + "Calculate change in building temperature" + annotation (Placement(transformation(extent={{88,-50},{108,-30}}))); +equation + connect(coo.port_b2, pumBui.port_a) annotation (Line(points={{-10,-26},{-16,-26}, + {-16,-90},{-20,-90}}, color={0,127,255})); + connect(tra.y, souDis.T_in) + annotation (Line(points={{-99,54},{-92,54}}, color={0,0,127})); + connect(TBuiRetSig.y, souBui.T_in) + annotation (Line(points={{99,-86},{82,-86}}, color={0,0,127})); + connect(TSetCHWS.y, coo.TSet) + annotation (Line(points={{-99,-20},{-12,-20}}, + color={0,0,127})); + connect(souDis.ports[1], TDisSup.port_a) + annotation (Line(points={{-70,50},{-40,50}}, color={0,127,255})); + connect(TDisSup.port_b, coo.port_a1) annotation (Line(points={{-20,50},{-16,50}, + {-16,-14},{-10,-14}}, color={0,127,255})); + connect(coo.port_b1, TDisRet.port_a) annotation (Line(points={{10,-14},{16,-14}, + {16,50},{20,50}}, color={0,127,255})); + connect(TDisRet.port_b, sinDis.ports[1]) + annotation (Line(points={{40,50},{60,50}}, color={0,127,255})); + connect(TBuiSup.T, TApp.u1) + annotation (Line(points={{-60,-79},{-60,106},{-2,106}}, color={0,0,127})); + connect(TDisSup.T, TApp.u2) + annotation (Line(points={{-30,61},{-30,94},{-2,94}}, color={0,0,127})); + connect(TDisRet.T, dTDis.u2) + annotation (Line(points={{30,61},{30,74},{58,74}}, color={0,0,127})); + connect(dTDis.u1, TDisSup.T) + annotation (Line(points={{58,86},{-30,86},{-30,61}}, color={0,0,127})); + connect(TBuiRet.T, dTBui.u2) + annotation (Line(points={{30,-79},{30,-46},{86,-46}}, color={0,0,127})); + connect(TBuiSup.T, dTBui.u1) + annotation (Line(points={{-60,-79},{-60,-34},{86,-34}}, color={0,0,127})); + connect(pumBui.port_b, TBuiSup.port_a) + annotation (Line(points={{-40,-90},{-50,-90}}, color={0,127,255})); + connect(TBuiSup.port_b, sinBui.ports[1]) + annotation (Line(points={{-70,-90},{-100,-90}}, color={0,127,255})); + connect(coo.port_a2, TBuiRet.port_b) annotation (Line(points={{10,-26},{16, + -26},{16,-90},{20,-90}}, color={0,127,255})); + connect(TBuiRet.port_a, souBui.ports[1]) + annotation (Line(points={{40,-90},{60,-90}}, color={0,127,255})); + annotation (Icon(coordinateSystem(preserveAspectRatio=false, + extent={{-100,-100},{100,100}})), + Diagram(coordinateSystem(preserveAspectRatio=false, + extent={{-140,-120},{140,120}})), + __Dymola_Commands(file= + "modelica://Buildings/Resources/Scripts/Dymola/Applications/DHC/EnergyTransferStations/Examples/CoolingIndirectOpenLoops.mos" + "Simulate and plot"), + experiment( + StartTime=0, + StopTime=86400, + Tolerance=1e-06), + Documentation(info=" +

This model provides an example for the indirect cooling energy transfer station model. +Both the district and building chilled water loops are open. The district supply temperature +is modulating, while the modulating building return temperature mimics a theoretically +variable cooling load at the building.

+", revisions=" +
    +
  • +November 1, 2019, by Kathryn Hinkelman:
    +First implementation. +
  • +
+")); +end CoolingIndirectOpenLoops; diff --git a/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops_Templated.mo b/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops_Templated.mo new file mode 100644 index 000000000..5eb407b47 --- /dev/null +++ b/geojson_modelica_translator/modelica/CoolingIndirectOpenLoops_Templated.mo @@ -0,0 +1,180 @@ +within Buildings.Applications.DHC.EnergyTransferStations.Examples; +model CoolingIndirectOpenLoops_Templated + + "Example model for indirect cooling energy transfer station with open loops on the building and district sides" + extends Modelica.Icons.Example; + + package Medium = Buildings.Media.Water; + + parameter Modelica.SIunits.MassFlowRate m1_flow_nominal = 0.5 + "Nominal mass flow rate of primary (district) district cooling side"; + parameter Modelica.SIunits.MassFlowRate m2_flow_nominal = 0.5 + "Nominal mass flow rate of secondary (building) district cooling side"; + + Modelica.Blocks.Sources.Constant TSetCHWS(k=273.15 + 7) + "Setpoint temperature for building chilled water supply" + annotation (Placement(transformation(extent={{-120,-30},{-100,-10}}))); + Buildings.Fluid.Sources.Boundary_pT sinDis( + redeclare package Medium = Medium, + p=300000, + T=287.15, + nPorts=1) + "District-side (primary) sink" + annotation (Placement(transformation(extent={{80,40},{60,60}}))); + Buildings.Fluid.Sources.Boundary_pT souDis( + redeclare package Medium = Medium, + p(displayUnit="Pa") = 300000 + 800, + use_T_in=true, + T=278.15, + nPorts=1) + "District (primary) source" + annotation (Placement(transformation(extent={{-90,40},{-70,60}}))); + Buildings.Fluid.Sources.Boundary_pT sinBui( + redeclare package Medium = Medium, + use_T_in=false, + T=280.15, + nPorts=1) + "Building (secondary) sink (chilled water supply)" + annotation (Placement(transformation(extent={{-120,-100},{-100,-80}}))); + Buildings.Fluid.Sources.Boundary_pT souBui( + redeclare package Medium = Medium, + use_T_in=true, + T=289.15, + nPorts=1) + "Building (secondary) source (chilled water return)" + annotation (Placement(transformation(extent={{80,-100},{60,-80}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TDisSup( + redeclare package Medium = Medium, + m_flow_nominal=m1_flow_nominal, + T_start=278.15) + "District-side (primary) supply temperature sensor" + annotation (Placement(transformation(extent={{-40,40},{-20,60}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TDisRet( + redeclare package Medium = Medium, + m_flow_nominal=m1_flow_nominal, + T_start=287.15) + "District-side (primary) return temperature sensor" + annotation (Placement(transformation(extent={{20,40},{40,60}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiRet( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + T_start=289.15) + "Building-side (secondary) return temperature sensor" + annotation (Placement(transformation(extent={{40,-100},{20,-80}}))); + Buildings.Fluid.Sensors.TemperatureTwoPort TBuiSup( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + T_start=280.15) + "Building-side (secondary) supply temperature sensor" + annotation (Placement(transformation(extent={{-50,-100},{-70,-80}}))); + Buildings.Applications.DHC.EnergyTransferStations.ets_cooling_indirect_templated coo( + redeclare package Medium = Medium, + m1_flow_nominal=m1_flow_nominal, + m2_flow_nominal=m2_flow_nominal, + dp1_nominal = 500, + dp2_nominal = 500, + Q_flow_nominal=18514, + T_a1_nominal = 278.15, + T_a2_nominal = 289.15, + controllerType=Modelica.Blocks.Types.SimpleController.PI, + k=0.1, + Ti=40, + yMax=1, + yMin=0, + initType=Modelica.Blocks.Types.InitPID.InitialOutput, + yCon_start=0, + reverseAction=true) + "Indirect cooling ETS" + annotation (Placement(transformation(extent={{-10,-30},{10,-10}}))); + Buildings.Fluid.Movers.FlowControlled_m_flow pumBui( + redeclare package Medium = Medium, + m_flow_nominal=m2_flow_nominal, + inputType=Buildings.Fluid.Types.InputType.Constant, + nominalValuesDefineDefaultPressureCurve=true, + dp_nominal=6000) + "Building-side (secondary) pump" + annotation (Placement(transformation(extent={{-20,-100},{-40,-80}}))); + Modelica.Blocks.Sources.Trapezoid tra( + amplitude=1.5, + rising(displayUnit="h") = 10800, + width(displayUnit="h") = 10800, + falling(displayUnit="h") = 10800, + period(displayUnit="h") = 43200, + offset=273 + 3.5) + "District supply temperature trapezoid signal" + annotation (Placement(transformation(extent={{-120,44},{-100,64}}))); + Modelica.Blocks.Sources.RealExpression TBuiRetSig( + y=(273.15 + 16) + 2*sin(time*2*3.14/86400)) + "Sinusoidal signal for return temperature on building (secondary) side" + annotation (Placement(transformation(extent={{120,-96},{100,-76}}))); + Modelica.Blocks.Math.Add TApp(k2=-1) "Calculate approach temperature" + annotation (Placement(transformation(extent={{0,90},{20,110}}))); + Modelica.Blocks.Math.Add dTDis(k1=-1) + "Calculate change in district temperature" + annotation (Placement(transformation(extent={{60,70},{80,90}}))); + Modelica.Blocks.Math.Add dTBui(k1=-1) + "Calculate change in building temperature" + annotation (Placement(transformation(extent={{88,-50},{108,-30}}))); +equation + connect(coo.port_b2, pumBui.port_a) annotation (Line(points={{-10,-26},{-16,-26}, + {-16,-90},{-20,-90}}, color={0,127,255})); + connect(tra.y, souDis.T_in) + annotation (Line(points={{-99,54},{-92,54}}, color={0,0,127})); + connect(TBuiRetSig.y, souBui.T_in) + annotation (Line(points={{99,-86},{82,-86}}, color={0,0,127})); + connect(TSetCHWS.y, coo.TSet) + annotation (Line(points={{-99,-20},{-12,-20}}, + color={0,0,127})); + connect(souDis.ports[1], TDisSup.port_a) + annotation (Line(points={{-70,50},{-40,50}}, color={0,127,255})); + connect(TDisSup.port_b, coo.port_a1) annotation (Line(points={{-20,50},{-16,50}, + {-16,-14},{-10,-14}}, color={0,127,255})); + connect(coo.port_b1, TDisRet.port_a) annotation (Line(points={{10,-14},{16,-14}, + {16,50},{20,50}}, color={0,127,255})); + connect(TDisRet.port_b, sinDis.ports[1]) + annotation (Line(points={{40,50},{60,50}}, color={0,127,255})); + connect(TBuiSup.T, TApp.u1) + annotation (Line(points={{-60,-79},{-60,106},{-2,106}}, color={0,0,127})); + connect(TDisSup.T, TApp.u2) + annotation (Line(points={{-30,61},{-30,94},{-2,94}}, color={0,0,127})); + connect(TDisRet.T, dTDis.u2) + annotation (Line(points={{30,61},{30,74},{58,74}}, color={0,0,127})); + connect(dTDis.u1, TDisSup.T) + annotation (Line(points={{58,86},{-30,86},{-30,61}}, color={0,0,127})); + connect(TBuiRet.T, dTBui.u2) + annotation (Line(points={{30,-79},{30,-46},{86,-46}}, color={0,0,127})); + connect(TBuiSup.T, dTBui.u1) + annotation (Line(points={{-60,-79},{-60,-34},{86,-34}}, color={0,0,127})); + connect(pumBui.port_b, TBuiSup.port_a) + annotation (Line(points={{-40,-90},{-50,-90}}, color={0,127,255})); + connect(TBuiSup.port_b, sinBui.ports[1]) + annotation (Line(points={{-70,-90},{-100,-90}}, color={0,127,255})); + connect(coo.port_a2, TBuiRet.port_b) annotation (Line(points={{10,-26},{16, + -26},{16,-90},{20,-90}}, color={0,127,255})); + connect(TBuiRet.port_a, souBui.ports[1]) + annotation (Line(points={{40,-90},{60,-90}}, color={0,127,255})); + annotation (Icon(coordinateSystem(preserveAspectRatio=false, + extent={{-100,-100},{100,100}})), + Diagram(coordinateSystem(preserveAspectRatio=false, + extent={{-140,-120},{140,120}})), + __Dymola_Commands(file= + "modelica://Buildings/Resources/Scripts/Dymola/Applications/DHC/EnergyTransferStations/Examples/CoolingIndirectOpenLoops.mos" + "Simulate and plot"), + experiment( + StartTime=0, + StopTime=86400, + Tolerance=1e-06), + Documentation(info=" +

This model provides an example for the indirect cooling energy transfer station model. +Both the district and building chilled water loops are open. The district supply temperature +is modulating, while the modulating building return temperature mimics a theoretically +variable cooling load at the building.

+", revisions=" +
    +
  • +November 1, 2019, by Kathryn Hinkelman:
    +First implementation. +
  • +
+")); +end CoolingIndirectOpenLoops_Templated; diff --git a/geojson_modelica_translator/modelica/input_parser.py b/geojson_modelica_translator/modelica/input_parser.py index 5de861a22..993ef831c 100644 --- a/geojson_modelica_translator/modelica/input_parser.py +++ b/geojson_modelica_translator/modelica/input_parser.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -30,7 +30,7 @@ import os -from jinja2 import FileSystemLoader, Environment +from jinja2 import Environment, FileSystemLoader class PackageParser(object): @@ -52,7 +52,11 @@ class method. self.load() self.template_env = Environment( - loader=FileSystemLoader(searchpath=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')) + loader=FileSystemLoader( + searchpath=os.path.join( + os.path.dirname(os.path.abspath(__file__)), "templates" + ) + ) ) @classmethod @@ -68,37 +72,37 @@ def new_from_template(cls, path, name, order, within=None): """ klass = PackageParser(path) if within: - template = klass.template_env.get_template('package.mot') + template = klass.template_env.get_template("package.mot") else: - template = klass.template_env.get_template('package_base.mot') + template = klass.template_env.get_template("package_base.mot") klass.package_data = template.render(within=within, name=name, order=order) - klass.order_data = '\n'.join(order) - klass.order_data += '\n' # trailing line + klass.order_data = "\n".join(order) + klass.order_data += "\n" # trailing line return klass def load(self): """ Load the package.mo and package.mo data from the member variable path """ - filename = os.path.join(self.path, 'package.mo') + filename = os.path.join(self.path, "package.mo") if os.path.exists(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: self.package_data = f.read() - filename = os.path.join(self.path, 'package.order') + filename = os.path.join(self.path, "package.order") if os.path.exists(filename): - with open(filename, 'r') as f: + with open(filename, "r") as f: self.order_data = f.read() def save(self): """ Save the updated files to the same location """ - with open(os.path.join(os.path.join(self.path, 'package.mo')), 'w') as f: + with open(os.path.join(os.path.join(self.path, "package.mo")), "w") as f: f.write(self.package_data) - with open(os.path.join(os.path.join(self.path, 'package.order')), 'w') as f: + with open(os.path.join(os.path.join(self.path, "package.order")), "w") as f: f.write(self.order_data) @property @@ -108,7 +112,7 @@ def order(self): :return: list, list of the loaded models in the package.order file """ - return self.order_data.split('\n') + return self.order_data.split("\n") def rename_model(self, old_model, new_model): """ @@ -127,7 +131,7 @@ class InputParser(object): def __init__(self, modelica_filename): if not os.path.exists(modelica_filename): - raise Exception(f'Modelica file does not exist: {modelica_filename}') + raise Exception(f"Modelica file does not exist: {modelica_filename}") self.modelica_filename = modelica_filename self.init_vars() @@ -135,72 +139,88 @@ def __init__(self, modelica_filename): def init_vars(self): self.within = None - self.model = {'name': None, 'comment': None, 'objects': []} + self.model = {"name": None, "comment": None, "objects": []} self.connections = [] self.equations = [] def parse_mo(self): # eventually move this over to use token-based assessment of the files. Here is a list of some of the tokens # TODO: strip all spacing and reconstruct on export - tokens = ['within', 'block', 'algorithm', 'model', 'equation', 'protected', 'package', - 'extends', 'initial equation', 'end'] + tokens = [ + "within", + "block", + "algorithm", + "model", + "equation", + "protected", + "package", + "extends", + "initial equation", + "end", + ] current_block = None - obj_data = '' - connect_data = '' - with open(self.modelica_filename, 'r') as f: + obj_data = "" + connect_data = "" + with open(self.modelica_filename, "r") as f: for index, line in enumerate(f.readlines()): - if line == '\n': + if line == "\n": # Skip blank lines (for now? continue - elif line.startswith('within'): + elif line.startswith("within"): # these lines typically only have a single line, so just persist it if not self.within: # remove the line feed and the trailing semicolon - self.within = line.split(' ')[1].rstrip().replace(';', '') + self.within = line.split(" ")[1].rstrip().replace(";", "") else: raise Exception("More than one 'within' lines found") continue - elif line.startswith('model'): + elif line.startswith("model"): # get the model name and save - self.model['name'] = line.split(' ')[1].rstrip() - current_block = 'model' + self.model["name"] = line.split(" ")[1].rstrip() + current_block = "model" continue - elif line.startswith('equation'): - current_block = 'equation' + elif line.startswith("equation"): + current_block = "equation" continue - elif line.startswith('end'): - current_block = 'end' + elif line.startswith("end"): + current_block = "end" else: # check if any other tokens are triggered and throw a 'not-supported' message for t in tokens: if line.startswith(t): raise Exception( - f"Found other token '{t}' in '{self.modelica_filename}' that is not supported... cannot continue") # noqa + f"Found other token '{t}' in '{self.modelica_filename}' that is not supported... \ + cannot continue" + ) # now store data that is in between these other blocks - if current_block == 'model': + if current_block == "model": # grab the lines that are comments: - if not obj_data and line.strip().startswith('"') and line.strip().endswith('"'): - self.model['comment'] = line.rstrip() + if ( + not obj_data + and line.strip().startswith('"') + and line.strip().endswith('"') + ): + self.model["comment"] = line.rstrip() continue # determine if this is a new object or a new object (look for ';') obj_data += line - if line.endswith(';\n'): - self.model['objects'].append(obj_data) - obj_data = '' - elif current_block == 'equation': - if line.strip().startswith('connect'): + if line.endswith(";\n"): + self.model["objects"].append(obj_data) + obj_data = "" + elif current_block == "equation": + if line.strip().startswith("connect"): connect_data += line - elif connect_data and line.endswith(';\n'): + elif connect_data and line.endswith(";\n"): connect_data += line self.connections.append(connect_data) - connect_data = '' + connect_data = "" elif connect_data: connect_data += line else: self.equations.append(line) - elif current_block == 'end': + elif current_block == "end": pass else: # there is nothing to do here @@ -221,7 +241,7 @@ def save_as(self, new_filename): :param new_filename: :return: """ - with open(new_filename, 'w') as f: + with open(new_filename, "w") as f: f.write(self.serialize()) def remove_object(self, obj_name): @@ -233,7 +253,7 @@ def remove_object(self, obj_name): """ index, obj = self.find_model_object(obj_name) if index is not None: - del self.model['objects'][index] + del self.model["objects"][index] def replace_within_string(self, new_string): """ @@ -249,9 +269,9 @@ def find_model_object(self, obj_name): :param obj_name: string, name (including the instance) :return: list, index and string of object """ - for index, o in enumerate(self.model['objects']): + for index, o in enumerate(self.model["objects"]): if obj_name in o: - return index, self.model['objects'][index] + return index, self.model["objects"][index] return None, None @@ -274,9 +294,11 @@ def replace_model_string(self, model_name, model_instance, old_string, new_strin :param old_string: string, name of the old string to replace :param new_string: string, the new string """ - index, _model = self.find_model_object(f'{model_name} {model_instance}') + index, _model = self.find_model_object(f"{model_name} {model_instance}") if index is not None: - self.model['objects'][index] = self.model['objects'][index].replace(old_string, new_string) + self.model["objects"][index] = self.model["objects"][index].replace( + old_string, new_string + ) def add_model_object(self, model_name, model_instance, data): """ @@ -286,10 +308,10 @@ def add_model_object(self, model_name, model_instance, data): :param model_instance: string :param data: list of strings """ - str = f' {model_name} {model_instance}\n' + str = f" {model_name} {model_instance}\n" for d in data: - str += f' {d}\n' - self.model['objects'].append(str) + str += f" {d}\n" + self.model["objects"].append(str) def add_connect(self, a, b, annotation): """ @@ -299,7 +321,7 @@ def add_connect(self, a, b, annotation): :param b: string, port b :param annotation: string, description """ - self.connections.append(f' connect({a}, {b})\n {annotation};\n') + self.connections.append(f" connect({a}, {b})\n {annotation};\n") def find_connect(self, port_a, port_b): """ @@ -312,12 +334,12 @@ def find_connect(self, port_a, port_b): """ for index, c in enumerate(self.connections): if not port_a: - raise Exception('Unable to replace string in connect if unknown port A') + raise Exception("Unable to replace string in connect if unknown port A") if not port_b: - if f'({port_a}, ' in c: + if f"({port_a}, " in c: return index, c if port_a and port_b: - if f'({port_a}, {port_b})' in c: + if f"({port_a}, {port_b})" in c: return index, c return None, None @@ -346,19 +368,31 @@ def replace_connect_string(self, a, b, new_a, new_b, replace_all=False): else: index, c = self.find_connect(a, b) + def remove_connect_string(self, a, b): + """ + Remove a connection string that matches the a, b. + + :param a: string, existing port a + :param b: string, existing port b + """ + # find the connection that matches a, b + index, c = self.find_connect(a, b) + if index: + del self.connections[index] + def serialize(self): """ Serialize the modelica object to a string with line feeds :return: string """ - str = f'within {self.within};\n' + str = f"within {self.within};\n" str += f"model {self.model['name']}\n" str += f"{self.model['comment']}\n\n" - for o in self.model['objects']: + for o in self.model["objects"]: for l in o: str += l - str += 'equation\n' + str += "equation\n" for c in self.connections: str += c for e in self.equations: diff --git a/geojson_modelica_translator/modelica/lib/__init__.py b/geojson_modelica_translator/modelica/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/geojson_modelica_translator/modelica/lib/runner/__init__.py b/geojson_modelica_translator/modelica/lib/runner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/geojson_modelica_translator/modelica/lib/runner/jm_ipython.sh b/geojson_modelica_translator/modelica/lib/runner/jm_ipython.sh new file mode 100755 index 000000000..00aa2808c --- /dev/null +++ b/geojson_modelica_translator/modelica/lib/runner/jm_ipython.sh @@ -0,0 +1,105 @@ +#!/bin/bash +################################################# +# Shell script that simulates JModelica using +# a docker image of JModelica. +# +# The main purpose of this script is to export +# MODELICAPATH and PYTHONPATH with their values +# updated for the docker, and to mount the +# required directories. +################################################# +set -e +IMG_NAME=ubuntu-1804_jmodelica_trunk +DOCKER_USERNAME=michaelwetter + +# Function declarations +function create_mount_command() +{ + local pat="$1" + # Each entry in pat will be a mounted read-only volume + local mnt_cmd="" + for ele in ${pat//:/ }; do + mnt_cmd="${mnt_cmd} -v ${ele}:/mnt${ele}:ro" + done + + # On Darwin, the exported temporary folder needs to be /private/var/folders, not /var/folders + # see https://askubuntu.com/questions/600018/how-to-display-the-paths-in-path-separately + if [ `uname` == "Darwin" ]; then + mnt_cmd=`echo ${mnt_cmd} | sed -e 's| /var/folders/| /private/var/folders/|g'` + fi + echo "${mnt_cmd}" +} + +function update_path_variable() +{ + # Prepend /mnt/ in front of each entry of a PATH variable in which the arguments are + # separated by a colon ":" + # This allows for example to create the new MODELICAPATH + local pat="$1" + local new_pat=`(set -f; IFS=:; printf "/mnt%s:" ${pat})` + # Cut the trailing ':' + new_pat=${new_pat%?} + echo "${new_pat}" +} + +# Export the MODELICAPATH +if [ -z ${MODELICAPATH+x} ]; then + MODELICAPATH=`pwd` +else + # Add the current directory to the front of the Modelica path. + # This will export the directory to the docker, and also set + # it in the MODELICAPATH so that JModelica finds it. + MODELICAPATH=`pwd`:${MODELICAPATH} +fi + +# Create the command to mount all directories in read-only mode +# a) for MODELICAPATH +MOD_MOUNT=`create_mount_command ${MODELICAPATH}` +# b) for PYTHONPATH +PYT_MOUNT=`create_mount_command ${PYTHONPATH}` + +# Prepend /mnt/ in front of each entry, which will then be used as the MODELICAPATH +DOCKER_MODELICAPATH=`update_path_variable ${MODELICAPATH}` +DOCKER_PYTHONPATH=`update_path_variable ${PYTHONPATH}` + +# If the current directory is part of the argument list, +# replace it with . as the docker may have a different file structure +cur_dir=`pwd` +bas_nam=`basename ${cur_dir}` +arg_lis=`echo $@ | sed -e "s|${cur_dir}|.|g"` + +# Set variable for shared directory +sha_dir=`dirname ${cur_dir}` + +# Check if the python script should be run interactively (if -i is specified) +while [ $# -ne 0 ] +do + arg="$1" + case "$arg" in + -i) + interactive=true + DOCKER_INTERACTIVE=-t + ;; + esac + shift +done + +# --user=${UID} \ + +docker run \ + --user=${UID} \ + -i \ + $DOCKER_INTERACTIVE \ + --detach=false \ + ${MOD_MOUNT} \ + ${PYT_MOUNT} \ + -v ${sha_dir}:/mnt/shared \ + -e DISPLAY=${DISPLAY} \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + --rm \ + ${DOCKER_USERNAME}/${IMG_NAME} /bin/bash -c \ + "export MODELICAPATH=${DOCKER_MODELICAPATH}:/usr/local/JModelica/ThirdParty/MSL && \ + export PYTHONPATH=${DOCKER_PYTHONPATH} && \ + cd /mnt/shared/${bas_nam} && \ + /usr/local/JModelica/bin/jm_ipython.sh ${arg_lis}" +exit $? diff --git a/geojson_modelica_translator/modelica/lib/runner/jmodelica.py b/geojson_modelica_translator/modelica/lib/runner/jmodelica.py new file mode 100644 index 000000000..6e2ac84d4 --- /dev/null +++ b/geojson_modelica_translator/modelica/lib/runner/jmodelica.py @@ -0,0 +1,123 @@ +########################################################################## +# Script to simulate Modelica models with JModelica. +# +########################################################################## +# Import the function for compilation of models and the load_fmu method + +import os +import shutil +import sys + +import pymodelica +from pyfmi import load_fmu +from pymodelica import compile_fmu + +# import matplotlib.pyplot as plt + +debug_solver = False +model = "Buildings.Controls.OBC.CDL.Continuous.Validation.LimPID" +# Overwrite model with command line argument if specified +if len(sys.argv) > 1: + # If the argument is a file, then parse it to a model name + if os.path.isfile(sys.argv[1]): + model = sys.argv[1].replace(os.path.sep, '.')[:-3] + else: + model = sys.argv[1] + + +print("*** Compiling {}".format(model)) +# Increase memory +pymodelica.environ['JVM_ARGS'] = '-Xmx4096m' + + +sys.stdout.flush() + +###################################################################### +# Compile fmu +fmu_name = compile_fmu(model, + version="2.0", + compiler_log_level='warning', + compiler_options={"generate_html_diagnostics": False, + "nle_solver_tol_factor": 1e-2}) + +###################################################################### +# Load model +mod = load_fmu(fmu_name, log_level=3) + +###################################################################### +# Retrieve and set solver options +x_nominal = mod.nominal_continuous_states +opts = mod.simulate_options() # Retrieve the default options + +opts['solver'] = 'CVode' +opts['ncp'] = 5000 + +if opts['solver'].lower() == 'cvode': + # Set user-specified tolerance if it is smaller than the tolerance in the .mo file + rtol = 1.0e-6 + x_nominal = mod.nominal_continuous_states + + if len(x_nominal) > 0: + atol = rtol * x_nominal + else: + atol = rtol + + opts['CVode_options'] = { + 'external_event_detection': False, + 'maxh': (mod.get_default_experiment_stop_time() - mod.get_default_experiment_stop_time()) / float(opts['ncp']), + 'iter': 'Newton', + 'discr': 'BDF', + 'rtol': rtol, + 'atol': atol, + 'store_event_points': True + } + +if debug_solver: + opts["logging"] = True # <- Turn on solver debug logging +mod.set("_log_level", 6) + +###################################################################### +# Simulate +res = mod.simulate(options=opts) +# logging.error(traceback.format_exc()) + +# plt.plot(res['time'], res['x1']) +# plt.plot(res['time'], res['x2']) +# plt.xlabel('time in [s]') +# plt.ylabel('line2.y') +# plt.grid() +# plt.show() +# plt.savefig("plot.pdf") + +###################################################################### +# Copy style sheets. +# This is a hack to get the css and js files to render the html diagnostics. +htm_dir = os.path.splitext(os.path.basename(fmu_name))[0] + "_html_diagnostics" +if os.path.exists(htm_dir): + for fil in ["scripts.js", "style.css", "zepto.min.js"]: + src = os.path.join(".jmodelica_html", fil) + if os.path.exists(src): + des = os.path.join(htm_dir, fil) + shutil.copyfile(src, des) + +###################################################################### +# Get debugging information +if debug_solver: + # Load the debug information + from pyfmi.debug import CVodeDebugInformation + debug = CVodeDebugInformation(model.replace(".", "_") + "_debug.txt") + + # Below are options to plot the order, error and step-size evolution. + # The error methos also take a threshold and a region if you want to + # limit the plot to a certain interval. + + # Plot order evolution + debug.plot_order() + + # Plot error evolution + debug.plot_error() # Note see also the arguments to the method + + # Plot the used step-size + debug.plot_step_size() + + # See also debug? diff --git a/geojson_modelica_translator/modelica/lib/runner/license.txt b/geojson_modelica_translator/modelica/lib/runner/license.txt new file mode 100644 index 000000000..14c934007 --- /dev/null +++ b/geojson_modelica_translator/modelica/lib/runner/license.txt @@ -0,0 +1,43 @@ +Copyright (c) 2018, The Regents of the University of California, Department +of Energy contract-operators of the Lawrence Berkeley National Laboratory. +All rights reserved. + +1. Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + (1) Redistributions of source code must retain the copyright notice, this + list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + (3) Neither the name of the University of California, Lawrence Berkeley + National Laboratory, U.S. Dept. of Energy nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +2. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +3. You are under no obligation whatsoever to provide any bug fixes, patches, +or upgrades to the features, functionality or performance of the source code +("Enhancements") to anyone; however, if you choose to make your Enhancements +available either publicly, or directly to Lawrence Berkeley National +Laboratory, without imposing a separate written license agreement for such +Enhancements, then you hereby grant the following license: a non-exclusive, +royalty-free perpetual license to install, use, modify, prepare derivative +works, incorporate into other computer software, distribute, and sublicense +such enhancements or derivative works thereof, in binary and source code form. + +NOTE: This license corresponds to the "revised BSD" or "3-clause BSD" license +and includes the following modification: Paragraph 3. has been added by the +University of California. diff --git a/geojson_modelica_translator/modelica/modelica_runner.py b/geojson_modelica_translator/modelica/modelica_runner.py new file mode 100644 index 000000000..6a3b2b69a --- /dev/null +++ b/geojson_modelica_translator/modelica/modelica_runner.py @@ -0,0 +1,126 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import os +import shutil +import subprocess + + +class ModelicaRunner(object): + """ + Class to run Modelica models. This is a very simple implementation of what needs to be + a full CLI to run Modelica easily. At the moment, this probably only works on Linux/Mac + and perhaps in Windows with Docker. + + # TODO: test in windows + # Document how to install Docker + """ + + def __init__(self, modelica_lib_path=None): + """ + Initialize the runner with data needed for simulation + + :param modelica_lib_path: string, Path to the MBL to run against + """ + # check if the user has defined a MODELICAPATH, is so, then use that. + if os.environ.get('MODELICAPATH', None): + print('Using predefined MODELICAPATH') + self.modelica_lib_path = os.environ['MODELICAPATH'] + else: + self.modelica_lib_path = modelica_lib_path + local_path = os.path.dirname(os.path.abspath(__file__)) + self.jmodelica_py_path = os.path.join(local_path, 'lib', 'runner', 'jmodelica.py') + self.jm_ipython_path = os.path.join(local_path, 'lib', 'runner', 'jm_ipython.sh') + + # Verify that docker is up and running + r = subprocess.call(['docker', 'ps'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.docker_configured = r == 0 + + def run_in_docker(self, file_to_run, run_path=None): + """ + Run the Modelica project in a docker-based environment. Results are saved into the path of the + file that was selected to run. + + stdout.log will store both stdout and stderr of the simulations + + :param file_to_run: string, name of the file (could be directory?) to simulate + """ + if not self.docker_configured: + raise Exception('Docker not configured on host computer, unable to run') + + if not os.path.exists(file_to_run): + raise Exception(f'File not found to run {file_to_run}') + + if not os.path.isfile(file_to_run): + raise Exception(f'Expecting to run a file, not a folder in {file_to_run}') + + if not run_path: + run_path = os.path.dirname(file_to_run) + + new_jm_ipython = os.path.join(run_path, os.path.basename(self.jm_ipython_path)) + shutil.copyfile(self.jm_ipython_path, new_jm_ipython) + os.chmod(new_jm_ipython, 0o775) + shutil.copyfile(self.jmodelica_py_path, os.path.join(run_path, os.path.basename(self.jmodelica_py_path))) + curdir = os.getcwd() + os.chdir(run_path) + stdout_log = open('stdout.log', 'w') + try: + # get the relative difference between the file to run and the path which everything is running in. + # make sure to simulate at a directory above the project directory! + + # Use slashes for now, but can make these periods `.replace(os.sep, '.')` but must strip off + # the .mo extension on the model to run + run_model = os.path.relpath(file_to_run, run_path) + # TODO: Create a logger to show more information such as the actual run command being executed. + p = subprocess.Popen( + ['./jm_ipython.sh', 'jmodelica.py', run_model], + stdout=stdout_log, + stderr=subprocess.STDOUT, + cwd=run_path + ) + exitcode = p.wait() + finally: + os.chdir(curdir) + stdout_log.close() + + return exitcode + + def cleanup_path(self, path): + """ + Clean up the files in the path that was presumably used to run the simulation + """ + remove_files = [ + 'jm_ipython.sh', + 'jmodelica.py', + ] + + for f in remove_files: + if os.path.exists(os.path.join(path, f)): + os.remove(os.path.join(path, f)) diff --git a/geojson_modelica_translator/scaffold.py b/geojson_modelica_translator/scaffold.py new file mode 100644 index 000000000..a876130e8 --- /dev/null +++ b/geojson_modelica_translator/scaffold.py @@ -0,0 +1,95 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import logging +import os +import shutil + +from geojson_modelica_translator.utils import ModelicaPath + +_log = logging.getLogger(__name__) + + +class Scaffold(object): + """Scaffold to hold the entire directory structure for the project. The purpose of this class is to + allow a developer/user to easily access the various paths of the project without having to + manually strip/replace strings/filenames/paths/etc. + + The project structure where an URBANopt-Modelica analysis will occur follows a well + defined structure and includes multiple levels of nested directories, data files, and scripts. + + Presently, the scaffold stops at the loads, substation, plant, districts, scripts path and does not + create a list of all of the submodels (yet). + """ + + def __init__(self, root_dir, project_name, overwrite=False): + """Initialize the scaffold. This will clear out the directory if it already exists, so use this + with caution. + + :param root_dir: Directory where to create the scaffold + :param project_name: Name of the project to create (should contain no spaces) + :param overwrite: boolean, overwrite the project if it already exists? + """ + self.root_dir = root_dir + self.project_name = project_name + self.loads_path = None + self.substations_path = None + self.plants_path = None + self.districts_path = None + self.scripts_path = None + self.overwrite = overwrite + + # clear out the project path + self.project_path = os.path.join(self.root_dir, self.project_name) + print(self.project_path) + if os.path.exists(self.project_path): + if not self.overwrite: + raise Exception("Directory already exists and overwrite is false for %s" % self.project_path) + else: + shutil.rmtree(self.project_path) + + def create(self): + """run the scaffolding""" + + # leverage the ModelicaPath function + self.loads_path = ModelicaPath("Loads", root_dir=self.project_path, overwrite=self.overwrite) + self.substations_path = ModelicaPath("Substations", root_dir=self.project_path, overwrite=self.overwrite) + self.plants_path = ModelicaPath("Plants", root_dir=self.project_path, overwrite=self.overwrite) + self.districts_path = ModelicaPath("Districts", root_dir=self.project_path, overwrite=self.overwrite) + + def clear_or_create_path(self, path, overwrite=False): + if os.path.exists(path): + if not overwrite: + raise Exception("Directory already exists and overwrite is false for %s" % path) + else: + shutil.rmtree(path) + os.makedirs(path, exist_ok=True) + + return path diff --git a/geojson_modelica_translator/system_parameters/schema.json b/geojson_modelica_translator/system_parameters/schema.json index a2139e9bb..1fd8d29f3 100755 --- a/geojson_modelica_translator/system_parameters/schema.json +++ b/geojson_modelica_translator/system_parameters/schema.json @@ -154,11 +154,11 @@ }, "ets": { "title": "ets", - "description": "ETS connection information", + "description": "energy transfer station model, one side is connected with district water loops, and the other side is connected with building water loops", "type": "object", "properties": { "system": { - "description": "Type of ETS system that the building is connected to.", + "description": "Indirect cooling ETS. The energy transfer is implemented through nondirect contact of water loops. ", "type": "string", "enum": [ "Booster Heater", @@ -283,6 +283,22 @@ } }, "additionalProperties": false + }, + "ets_parameters": { + "description": "Parameters associated with ETS models", + "type": "object", + "properties": { + "ModelName": "ets_cooling_indirect_templated", + "Q_Flow_Nominal": [8000], + "Eta_Efficiency": [0.666], + "NominalFlow_District": [0.666], + "NominalFlow_Building": [0.666], + "PressureDrop_Valve": [888], + "PressureDrop_HX_Secondary": [999], + "PressureDrop_HX_Primary": [999], + "SWT_District": [5], + "SWT_Building": [7] + } } } -} \ No newline at end of file +} diff --git a/geojson_modelica_translator/system_parameters/system_parameters.py b/geojson_modelica_translator/system_parameters/system_parameters.py index 5ff8e5d28..e04d76865 100644 --- a/geojson_modelica_translator/system_parameters/system_parameters.py +++ b/geojson_modelica_translator/system_parameters/system_parameters.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -46,14 +46,16 @@ def __init__(self, filename=None): :param filename: string, (optional) path to file to load """ # load the schema for validation - self.schema = json.load(open(os.path.join(os.path.dirname(__file__), 'schema.json'), 'r')) + self.schema = json.load(open(os.path.join(os.path.dirname(__file__), "schema.json"), "r")) self.data = {} if filename: if os.path.exists(filename): - self.data = json.load(open(filename, 'r')) + self.data = json.load(open(filename, "r")) else: - raise Exception(f"System design parameters file does not exist: {filename}") + raise Exception( + f"System design parameters file does not exist: {filename}" + ) errors = self.validate() if len(errors) != 0: @@ -90,9 +92,9 @@ def get_param(self, path, data=None, default=None): if data is None: data = self.data - paths = path.split('.') + paths = path.split(".") check_path = paths.pop(0) - if check_path == '': + if check_path == "": # no path passed return default else: @@ -103,7 +105,7 @@ def get_param(self, path, data=None, default=None): if len(paths) == 0: return value else: - return self.get_param('.'.join(paths), data=value, default=default) + return self.get_param(".".join(paths), data=value, default=default) def get_param_by_building_id(self, building_id, path, default=None): """ @@ -117,8 +119,8 @@ def get_param_by_building_id(self, building_id, path, default=None): """ # TODO: return the default value if the building ID is not defined - for b in self.data.get('buildings', {}).get('custom', {}): - if b.get('geojson_id', None) == building_id: + for b in self.data.get("buildings", {}).get("custom", {}): + if b.get("geojson_id", None) == building_id: # print(f"Building found for {building_id}") return self.get_param(path, b, default=default) diff --git a/geojson_modelica_translator/utils.py b/geojson_modelica_translator/utils.py index 428c50b4f..cabb759a6 100644 --- a/geojson_modelica_translator/utils.py +++ b/geojson_modelica_translator/utils.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -51,7 +51,7 @@ def copytree(src, dst, symlinks=False, ignore=None): class ModelicaPath(object): """ Class for storing Modelica paths. This allows the path to point to - the model directory and the resources directory. + the model directory, resources, and scripts directory. """ def __init__(self, name, root_dir, overwrite=False): @@ -62,17 +62,20 @@ def __init__(self, name, root_dir, overwrite=False): """ self.name = name self.root_dir = root_dir + self.overwrite = overwrite # create the directories if root_dir is not None: check_path = os.path.join(self.files_dir) - self.clear_path(check_path, overwrite=overwrite) + self.clear_or_create_path(check_path) check_path = os.path.join(self.resources_dir) - self.clear_path(check_path, overwrite=overwrite) + self.clear_or_create_path(check_path) + check_path = os.path.join(self.scripts_dir) + self.clear_or_create_path(check_path) - def clear_path(self, path, overwrite=False): + def clear_or_create_path(self, path): if os.path.exists(path): - if overwrite: + if not self.overwrite: raise Exception("Directory already exists and overwrite is false for %s" % path) else: shutil.rmtree(path) @@ -87,7 +90,7 @@ def files_dir(self): :return: string, path to where files (models) are stored, without trailing slash """ if self.root_dir is None: - return self.name + return self.files_relative_dir else: return os.path.join(self.root_dir, self.name) @@ -96,9 +99,24 @@ def resources_relative_dir(self): """ Return the relative resource directory instead of the full path. This is useful when replacing strings within modelica files which are relative to the package. - :return: + + :return: string, relative resource's data path """ - return os.path.join('Resources', 'Data', self.name) + return os.path.join("Resources", "Data", self.name) + + @property + def scripts_relative_dir(self, platform='Dymola'): + """Return the scripts directory that is in the resources directory. This only returns the + relative directory and is useful when replacing string values within Modelica files. + + :return: string, relative scripts path + """ + return os.path.join("Resources", "Scripts", self.name, platform) + + @property + def files_relative_dir(self): + """Return the path to the files relative to the project name.""" + return os.path.join(self.name) @property def resources_dir(self): @@ -112,3 +130,16 @@ def resources_dir(self): return self.resources_relative_dir else: return os.path.join(self.root_dir, self.resources_relative_dir) + + @property + def scripts_dir(self): + """ + Return the path to the scripts directory (in the resources dir) for the specified ModelicaPath. + This path does not include the trailing slash. + + :return: string, path to where scripts are stored, without trailing slash. + """ + if self.root_dir is None: + return self.scripts_relative_dir + else: + return os.path.join(self.root_dir, self.scripts_relative_dir) diff --git a/management/update_licenses.py b/management/update_licenses.py index da3c84368..7e3a082d0 100644 --- a/management/update_licenses.py +++ b/management/update_licenses.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -37,7 +37,7 @@ PYTHON_REGEX = re.compile(r'^""".\*{100}.*:copyright.*\*{100}."""$', re.MULTILINE | re.DOTALL) PYTHON_LICENSE = '''""" **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -65,12 +65,11 @@ **************************************************************************************************** """''' -EXCLUDE_FILES = ["__init__.py", ] +EXCLUDE_FILES = ["__init__.py"] PATHS = [ - {"glob": 'geojson_modelica_translator/**/*.py', "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX}, - {"glob": 'management/**/*.py', "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX}, - {"glob": 'tests/**/*.py', "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX}, - + {"glob": "geojson_modelica_translator/**/*.py", "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX, }, + {"glob": "management/**/*.py", "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX}, + {"glob": "tests/**/*.py", "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX}, # single files # { "glob": 'bin/resources/**/file.py', "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX }, ] @@ -79,7 +78,7 @@ class UpdateLicenses(distutils.cmd.Command): """Custom comand for updating the GeoJSON schemas on which this project depends.""" - description = 'Update the license/copyright headers' + description = "Update the license/copyright headers" user_options = [] @@ -97,27 +96,27 @@ def check_and_update_license(self, filename): :param filename: str, path of the file to update :return: None """ - s = open(filename, 'r').read() + s = open(filename, "r").read() if PYTHON_REGEX.search(s): - print('License already exists, updating') + print("License already exists, updating") content = re.sub(PYTHON_REGEX, PYTHON_LICENSE, s) - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(content) f.close() else: - print('Adding license') - with open(filename, 'r+') as f: + print("Adding license") + with open(filename, "r+") as f: content = f.read() f.seek(0, 0) - f.write(PYTHON_LICENSE.rstrip('\r\n') + '\n\n\n' + content) + f.write(PYTHON_LICENSE.rstrip("\r\n") + "\n\n\n" + content) f.close() def run(self): for p in PATHS: - gl = glob.glob(p['glob'], recursive=True) + gl = glob.glob(p["glob"], recursive=True) for g in gl: if os.path.basename(g) in EXCLUDE_FILES: - print(f'Skipping file {g}') + print(f"Skipping file {g}") else: - print(f'Checking license in file {g}') + print(f"Checking license in file {g}") self.check_and_update_license(g) diff --git a/management/update_schemas.py b/management/update_schemas.py index b79b5eaa4..4c1653801 100644 --- a/management/update_schemas.py +++ b/management/update_schemas.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -38,26 +38,23 @@ class UpdateSchemas(distutils.cmd.Command): """Custom comand for updating the GeoJSON schemas on which this project depends.""" - description = 'download updated GeoJSON schemas' + description = "download updated GeoJSON schemas" - user_options = [ - ('baseurl=', 'u', 'base URL from which to download the schemas'), - ] + user_options = [("baseurl=", "u", "base URL from which to download the schemas")] def initialize_options(self): self.files_to_download = [ - 'building_properties.json', - 'district_system_properties.json', - 'electrical_connector_properties.json', - 'electrical_junction_properties.json', - 'region_properties.json', - 'site_properties.json', - 'thermal_connector_properties.json', - 'thermal_junction_properties.json', - + "building_properties.json", + "district_system_properties.json", + "electrical_connector_properties.json", + "electrical_junction_properties.json", + "region_properties.json", + "site_properties.json", + "thermal_connector_properties.json", + "thermal_junction_properties.json", ] # For now the branch is 'schema' but will need to be moved to develop after it is merged. - self.baseurl = 'https://raw.githubusercontent.com/urbanopt/urbanopt-geojson-gem/schema/lib/urbanopt/geojson/schema/' # noqa + self.baseurl = "https://raw.githubusercontent.com/urbanopt/urbanopt-geojson-gem/schema/lib/urbanopt/geojson/schema/" # noqa def finalize_options(self): if self.baseurl is None: @@ -65,10 +62,10 @@ def finalize_options(self): def run(self): for f in self.files_to_download: - self.announce('Downloading schema: %s' % str(f), level=distutils.log.INFO) - response = requests.get('%s/%s' % (self.baseurl, f)) - save_path = 'geojson_modelica_translator/geojson/data/schemas/%s' % f + self.announce("Downloading schema: %s" % str(f), level=distutils.log.INFO) + response = requests.get("%s/%s" % (self.baseurl, f)) + save_path = "geojson_modelica_translator/geojson/data/schemas/%s" % f # if os.path.exists(save_path): # os.remove(save_path) - with open(save_path, 'w') as outf: + with open(save_path, "w") as outf: json.dump(response.json(), outf, indent=2) diff --git a/requirements.txt b/requirements.txt index c9d492e4e..65fa5e5ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,30 +2,30 @@ # Remember to also add them in setup.py # core libraries -geojson==2.4.1 +geojson==2.5.0 jsonschema==3.0.1 requests==2.22.0 # dependent projects -## Require the MBL. Note this is a large dependency, but needed to assemble the models for running -BuildingsPy==1.7.0 -#-e git+https://github.com/lbl-srg/modelica-buildings.git@issue1442_loadCoupling#egg=buildings +BuildingsPy==2.0.0 +# There may be a need to require the MBL. Note this is a large dependency, but needed to assemble the models for running +#-e git+https://github.com/lbl-srg/modelica-buildings.git@issue1437_district_heating_cooling #egg=buildings -# Use TEASER from URBANopt for now to support MBL 7.0 -#teaser==0.6.8 +# Use the core teaser library. Note: if using the github checkout, then it will be saved in the ./src directory if python is system python. +teaser==0.7.2 #-e file:../TEASER-urbanopt#egg=teaser --e git+https://github.com/urbanopt/TEASER.git@mbl-7.0#egg=teaser +#-e git+https://github.com/RWTH-EBC/TEASER.git@0.7.2#egg=teaser # Test and documentation nose==1.3.7 -sphinx==2.1.2 +sphinx==2.4.2 sphinx_rtd_theme==0.4.3 -pytest==5.0.1 -pytest-cov==2.7.1 -python-coveralls==2.9.2 -autopep8==1.4.4 -flake8==3.7.8 -tox==3.13.2 +pytest==5.3.5 +pytest-cov==2.8.1 +python-coveralls==2.9.3 +autopep8==1.5 +flake8==3.7.9 +tox==3.14.5 # debugging and testing, not used at the moment in the main portion of the code (i.e. not needed in setup.cfg) scipy diff --git a/setup.cfg b/setup.cfg index 6ee70d475..1937e4b34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,13 @@ addopts = --cov geojson_modelica_translator --cov-report term-missing --verbose -s +norecursedirs = + dist + build + modelica-buildings + .tox + src +testpaths = tests [flake8] # Some sane defaults for the code style checker flake8 @@ -14,6 +21,7 @@ exclude = .tox .eggs build + modelica-buildings dist docs/conf.py diff --git a/setup.py b/setup.py index 0b68b61bd..7ff1c0eed 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -35,70 +35,63 @@ from io import BytesIO from zipfile import ZipFile -from requests import get -from setuptools import setup, find_packages +from setuptools import find_packages, setup from management.update_licenses import UpdateLicenses from management.update_schemas import UpdateSchemas +from requests import get -with open('README.rst') as f: +with open("README.rst") as f: readme = f.read() -with open('LICENSE') as f: +with open("LICENSE") as f: license = f.read() setup( - name='GeoJSON Modelica Translator', - version='0.1.0', - description='Package for converting GeoJSON to Modelica models for Urban Scale Analyses.', + name="GeoJSON Modelica Translator", + version="0.1.0", + description="Package for converting GeoJSON to Modelica models for Urban Scale Analyses.", long_description=readme, - author='Nicholas Long', - author_email='nicholas.long@nrel.gov', - url='https://github.com/urbanopt/geojson_modelica_translator', + author="Nicholas Long", + author_email="nicholas.long@nrel.gov", + url="https://github.com/urbanopt/geojson_modelica_translator", license=license, - packages=find_packages(exclude=('tests', 'docs')), - cmdclass={ - 'update_schemas': UpdateSchemas, - 'update_licenses': UpdateLicenses, - }, - install_requires=[ - 'geojson==2.4.1', - 'jsonschema==3.0.1', - 'requests==2.22.0', - ], + packages=find_packages(exclude=("tests", "docs")), + cmdclass={"update_schemas": UpdateSchemas, "update_licenses": UpdateLicenses}, + install_requires=["geojson==2.4.1", "jsonschema==3.0.1", "requests==2.22.0"], classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ], ) # install portions of the Modelica Buildings Library for grabbing files as needed (e.g. MOS files, examples, etc) -libs_to_extract = ['Buildings/Applications/DHC'] -save_path = 'geojson_modelica_translator/modelica/buildingslibrary' -tmp_save_path = 'geojson_modelica_translator/modelica/tmp_buildingslibrary' -repo_name = 'modelica-buildings' +libs_to_extract = ["Buildings/Applications/DHC"] +save_path = "geojson_modelica_translator/modelica/buildingslibrary" +tmp_save_path = "geojson_modelica_translator/modelica/tmp_buildingslibrary" +repo_name = "modelica-buildings" if os.path.exists(save_path): shutil.rmtree(save_path) if os.path.exists(tmp_save_path): shutil.rmtree(tmp_save_path) -mbl_archive_name = 'issue1442_loadCoupling' -r = get(f'https://github.com/lbl-srg/{repo_name}/archive/{mbl_archive_name}.zip') +mbl_archive_name = "issue1437_district_heating_cooling" +r = get(f"https://github.com/lbl-srg/{repo_name}/archive/{mbl_archive_name}.zip") with ZipFile(BytesIO(r.content)) as zip: files = zip.namelist() for file in files: # check if this needs to be extracted by looking into the libs_to_extract list for lib_to_extract in libs_to_extract: # make the path system independent when searching - if os.path.join(lib_to_extract.replace('/', os.path.sep)) in file: - print(f'extracting ... {file}') + if os.path.join(lib_to_extract.replace("/", os.path.sep)) in file: + print(f"extracting ... {file}") zip.extract(file, path=tmp_save_path) # Move the whole directory -shutil.move(os.path.join(tmp_save_path, f'{repo_name}-{mbl_archive_name}'), save_path) +shutil.move(os.path.join(tmp_save_path, f"{repo_name}-{mbl_archive_name}"), save_path) if os.path.exists(tmp_save_path): shutil.rmtree(tmp_save_path) diff --git a/tests/context.py b/tests/context.py index c74fe5fe6..be3b48f91 100644 --- a/tests/context.py +++ b/tests/context.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -31,8 +31,9 @@ # Helper file to allow for easy setup of test files. This allows for running tests in PyCharm and # the command line using py.test. -import sys import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import sys import geojson_modelica_translator # noqa - Do not remove this line. + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/geojson/data/example_geojson_13buildings.json b/tests/geojson/data/example_geojson_13buildings.json new file mode 100644 index 000000000..5d4817641 --- /dev/null +++ b/tests/geojson/data/example_geojson_13buildings.json @@ -0,0 +1,660 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + -78.84948467732347, + 42.81677154451123 + ], + "type": "Point" + }, + "properties": { + "begin_date": "2017-01-01T07:00:00.000Z", + "cec_climate_zone": null, + "climate_zone": "6A", + "default_template": "90.1-2013", + "end_date": "2017-13-31T07:00:00.000Z", + "id": "53340c2c-ab20-40db-aba1-11ac607c52a7", + "import_surrounding_buildings_as_shading": null, + "name": "Site Origin", + "surface_elevation": null, + "tariff_filename": null, + "timesteps_per_hour": 1, + "type": "Site Origin", + "weather_filename": "USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.epw" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.84650338745196, + 42.81331301863236 + ], + [ + -78.84652443964629, + 42.81463974371101 + ], + [ + -78.84680142363833, + 42.815293654042534 + ], + [ + -78.84744455124724, + 42.81514110006128 + ], + [ + -78.84728610028628, + 42.81478165791734 + ], + [ + -78.84786797764677, + 42.814643631760134 + ], + [ + -78.84721106637106, + 42.813153418927016 + ], + [ + -78.84650338745196, + 42.81331301863236 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "id": "1", + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "building_type": "Mixed use", + "floor_area": 752184, + "floor_height": 12, + "year_built": 1980, + + "footprint_area": 188046, + "mixed_type_1": "Office", + "mixed_type_1_percentage": 50, + "mixed_type_2": "Food service", + "mixed_type_2_percentage": 50, + "mixed_type_3": "Strip shopping mall", + "mixed_type_3_percentage": 0, + "mixed_type_4": "Lodging", + "mixed_type_4_percentage": 0, + "name": "Mixed_use 1", + + "type": "Building" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.8500120420453, + 42.81812185529549 + ], + [ + -78.85038975191084, + 42.81803226424341 + ], + [ + -78.850630729414, + 42.81857888627522 + ], + [ + -78.85025301954843, + 42.81866847653532 + ], + [ + -78.8500120420453, + 42.81812185529549 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Food service", + "floor_area": 22313, + "footprint_area": 22313, + "id": "2", + "name": "Restaurant 1", + "number_of_stories": 1, + "number_of_stories_above_ground": 1, + "year_built": 1980, + "floor_height": 12, + "type": "Building" + }, + "type": "Feature" + }, + { + "type": "Feature", + "properties": { + "id": "3", + "name": "Restaurant 10", + "type": "Building", + "footprint_area": 41877, + "floor_area": 125631, + "floor_height": 12, + "year_built": 1980, + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "building_type": "Food service" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -78.84962224800356, + 42.81329273502644 + ], + [ + -78.84929833482822, + 42.81337083838241 + ], + [ + -78.84983265832118, + 42.814563298664666 + ], + [ + -78.85015657149653, + 42.81448519681467 + ], + [ + -78.84962224800356, + 42.81329273502644 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "id": "4", + "name": "Restaurant 12", + "type": "Building", + "footprint_area": 10541, + "floor_area": 31623, + "floor_height": 12, + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "year_built": 1980, + "building_type": "Food service" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -78.84907318596754, + 42.81342719667407 + ], + [ + -78.84862090048105, + 42.81353625345659 + ], + [ + -78.84871721918239, + 42.813751210926796 + ], + [ + -78.84916950466888, + 42.81364215452331 + ], + [ + -78.84907318596754, + 42.81342719667407 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "id": "5", + "name": "Restaurant 14", + "type": "Building", + "footprint_area": 8804, + "floor_area": 8804, + "floor_height": 12, + "number_of_stories": 1, + "number_of_stories_above_ground": 1, + "year_built": 1980, + "building_type": "Food service" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -78.84809175426629, + 42.81367038997507 + ], + [ + -78.84848670778973, + 42.81357515750889 + ], + [ + -78.84857883872144, + 42.81378076888831 + ], + [ + -78.84818388519801, + 42.81387600103781 + ], + [ + -78.84809175426629, + 42.81367038997507 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": { + "id": "6", + "name": "Restaurant 15", + "type": "Building", + "footprint_area": 10689, + "floor_area": 10689, + "floor_height": 12, + "number_of_stories": 1, + "number_of_stories_above_ground": 1, + "year_built": 1980, + "building_type": "Food service" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -78.84846106738529, + 42.814495803077364 + ], + [ + -78.8486903952376, + 42.81444050756261 + ], + [ + -78.8484977578349, + 42.81401059666683 + ], + [ + -78.84826842998261, + 42.81406589256599 + ], + [ + -78.84846106738529, + 42.814495803077364 + ] + ] + ] + } + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.84733878006863, + 42.816466983030836 + ], + [ + -78.84854275129324, + 42.81617669028003 + ], + [ + -78.848356395545, + 42.81576080994094 + ], + [ + -78.84715242432038, + 42.81605110464406 + ], + [ + -78.84733878006863, + 42.816466983030836 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Office", + "id": "7", + "detailed_model_filename": "7.osm", + "number_of_stories": 6, + "number_of_stories_above_ground": 6, + "floor_area": 10689, + "floor_height": 12, + "year_built": 1980, + "name": "Office 1", + "type": "Building" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.84973966335251, + 42.8154441454509 + ], + [ + -78.85049562542395, + 42.81525669280299 + ], + [ + -78.85078257620685, + 42.81588131780643 + ], + [ + -78.8505086568277, + 42.81594736368234 + ], + [ + -78.85041233812638, + 42.815732413845666 + ], + [ + -78.84991755499783, + 42.81585689105046 + ], + [ + -78.84973966335251, + 42.8154441454509 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Outpatient health care", + "id": "8", + "name": "Hospital 1", + "detailed_model_filename": "8.osm", + "number_of_stories": 10, + "number_of_stories_above_ground": 10, + "year_built": 1980, + "floor_area": 10689, + "floor_height": 12, + "type": "Building" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.85083627755732, + 42.81600678613279 + ], + [ + -78.85056039001891, + 42.816076133580566 + ], + [ + -78.85072568130569, + 42.816450649528036 + ], + [ + -78.84940134236577, + 42.81677160705479 + ], + [ + -78.84958014898304, + 42.81716858994267 + ], + [ + -78.8507262115271, + 42.816890840117026 + ], + [ + -78.8508565789851, + 42.81719595796099 + ], + [ + -78.85132137101688, + 42.81708331517635 + ], + [ + -78.85083627755732, + 42.81600678613279 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Inpatient health care", + "id": "9", + "detailed_model_filename": "9.osm", + "name": "Hospital 2", + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "year_built": 1980, + "floor_area": 10689, + "floor_height": 12, + "type": "Building" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.85115264550463, + 42.81786093060211 + ], + [ + -78.85163483958878, + 42.81774467026972 + ], + [ + -78.85246596719499, + 42.819583261120755 + ], + [ + -78.85082390085432, + 42.819979162017745 + ], + [ + -78.85060552295334, + 42.81947573727234 + ], + [ + -78.85174564783776, + 42.81920483484765 + ], + [ + -78.85115264550463, + 42.81786093060211 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Mixed use", + "floor_area": 1278384, + "floor_height": 12, + "footprint_area": 159798, + "id": "10", + "mixed_type_1": "Strip shopping mall", + "mixed_type_1_percentage": 25, + "mixed_type_2": "Food service", + "mixed_type_2_percentage": 25, + "mixed_type_3": "Office", + "mixed_type_3_percentage": 15, + "mixed_type_4": "Lodging", + "mixed_type_4_percentage": 35, + "name": "Mixed use 2", + "number_of_stories": 8, + "number_of_stories_above_ground": 8, + "year_built": 1980, + "type": "Building" + }, + "type": "Feature" + }, + { + "type": "Feature", + "properties": { + "id": "11", + "name": "Restaurant 13", + "type": "Building", + "footprint_area": 10837, + "floor_area": 32511, + "floor_height": 12, + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "year_built": 1980, + "building_type": "Food service" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -78.84961163640645, + 42.81460851835703 + ], + [ + -78.84914661048371, + 42.81472064501696 + ], + [ + -78.84905029178236, + 42.81450569091638 + ], + [ + -78.84951531770513, + 42.81439356386673 + ], + [ + -78.84961163640645, + 42.81460851835703 + ] + ] + ] + } + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.84768338040897, + 42.817161656757065 + ], + [ + -78.8482630702579, + 42.8170218879136 + ], + [ + -78.84915297130291, + 42.81900776764229 + ], + [ + -78.84857328145401, + 42.81914753199706 + ], + [ + -78.84768338040897, + 42.817161656757065 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Strip shopping mall", + "floor_area": 374409, + "floor_height": 12, + "footprint_area": 124803, + "id": "12", + "name": "Mall 1", + "number_of_stories": 3, + "number_of_stories_above_ground": 3, + "year_built": 1980, + "type": "Building" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + -78.8494955083645, + 42.819748790984335 + ], + [ + -78.84891089471263, + 42.81989327725856 + ], + [ + -78.8491389243777, + 42.82038967009544 + ], + [ + -78.84972353802956, + 42.82024518498119 + ], + [ + -78.8494955083645, + 42.819748790984335 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "building_type": "Lodging", + "floor_area": 316160, + "floor_height": 12, + "footprint_area": 31616, + "id": "13", + "name": "Hotel 1", + "number_of_stories": 10, + "number_of_stories_above_ground": 10, + "year_built": 1980, + "type": "Building" + }, + "type": "Feature" + } + ], + "mappers": [], + "project": { + "begin_date": "2017-01-01T07:00:00.000Z", + "cec_climate_zone": null, + "climate_zone": "6A", + "default_template": "90.1-2013", + "end_date": "2017-12-31T07:00:00.000Z", + "id": "7c33a001-bccb-413e-ac87-67558b5d4b07", + "import_surrounding_buildings_as_shading": null, + "name": "New Project", + "surface_elevation": null, + "tariff_filename": null, + "timesteps_per_hour": 1, + "weather_filename": "USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.epw" + }, + "scenarios": [ + { + "feature_mappings": [], + "id": "72301739-c6c3-4dd7-bf1a-f37c8eff40db", + "name": "New Scenario" + } + ], + "type": "FeatureCollection" +} diff --git a/tests/geojson/data/geojson_1.json b/tests/geojson/data/geojson_1.json index afee07d12..877011621 100644 --- a/tests/geojson/data/geojson_1.json +++ b/tests/geojson/data/geojson_1.json @@ -42,7 +42,7 @@ "created_at": "2018-01-26T21:13:16.655Z", "building_type": "Office", "number_of_stories": 3, - "height": 9, + "floor_height": 9, "number_of_stories_above_ground": 3, "building_status": "Proposed", "include_in_energy_analysis": true, @@ -91,7 +91,7 @@ "created_at": "2018-01-31T20:35:04.192Z", "building_type": "Retail other than mall", "number_of_stories": 1, - "height": 3, + "floor_height": 3, "number_of_stories_above_ground": 1, "floor_area": 24567, "building_status": "Proposed", @@ -139,7 +139,7 @@ "updated_at": "2018-01-31T20:42:06.981Z", "created_at": "2018-01-31T20:41:11.756Z", "number_of_stories": 4, - "height": 12, + "floor_height": 12, "floor_area": 34448, "number_of_stories_above_ground": 4, "building_status": "Proposed", diff --git a/tests/geojson/data/nrel_campus.json b/tests/geojson/data/nrel_campus.json index ddf45e79b..2e01a3522 100644 --- a/tests/geojson/data/nrel_campus.json +++ b/tests/geojson/data/nrel_campus.json @@ -52,7 +52,7 @@ "footprint_perimeter": 228, "updated_at": "2017-09-01T21:17:07.590Z", "created_at": "2017-09-01T21:16:27.788Z", - "height": 6, + "floor_height": 6, "geometryType": "Polygon" } }, @@ -107,7 +107,7 @@ "footprint_perimeter": 245, "updated_at": "2017-09-01T21:17:07.507Z", "created_at": "2017-09-01T21:16:27.649Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -162,7 +162,7 @@ "footprint_perimeter": 83, "updated_at": "2017-09-01T21:17:07.607Z", "created_at": "2017-09-01T21:16:27.805Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -217,7 +217,7 @@ "footprint_perimeter": 151, "updated_at": "2017-09-01T21:17:07.585Z", "created_at": "2017-09-01T21:16:27.782Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -272,7 +272,7 @@ "footprint_perimeter": 192, "updated_at": "2017-09-01T21:17:07.612Z", "created_at": "2017-09-01T21:16:27.817Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -327,7 +327,7 @@ "footprint_perimeter": 101, "updated_at": "2017-09-01T21:17:07.618Z", "created_at": "2017-09-01T21:16:27.829Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -394,7 +394,7 @@ "footprint_perimeter": 447, "updated_at": "2017-09-01T21:17:07.518Z", "created_at": "2017-09-01T21:16:27.662Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -457,7 +457,7 @@ "footprint_perimeter": 464, "updated_at": "2017-09-01T21:17:07.524Z", "created_at": "2017-09-01T21:16:27.668Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -512,7 +512,7 @@ "footprint_perimeter": 187, "updated_at": "2017-09-01T21:17:07.529Z", "created_at": "2017-09-01T21:16:27.674Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -603,7 +603,7 @@ "footprint_perimeter": 459, "updated_at": "2017-09-01T21:17:07.513Z", "created_at": "2017-09-01T21:16:27.656Z", - "height": 6, + "floor_height": 6, "geometryType": "Polygon" } }, @@ -658,7 +658,7 @@ "footprint_perimeter": 65, "updated_at": "2017-09-01T21:17:07.573Z", "created_at": "2017-09-01T21:16:27.764Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -713,7 +713,7 @@ "footprint_perimeter": 84, "updated_at": "2017-09-01T21:17:07.602Z", "created_at": "2017-09-01T21:16:27.799Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -768,7 +768,7 @@ "footprint_perimeter": 61, "updated_at": "2017-09-01T21:17:07.623Z", "created_at": "2017-09-01T21:16:27.835Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -823,7 +823,7 @@ "footprint_perimeter": 61, "updated_at": "2017-09-01T21:17:07.629Z", "created_at": "2017-09-01T21:16:27.842Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -878,7 +878,7 @@ "footprint_perimeter": 74, "updated_at": "2017-09-01T21:17:07.635Z", "created_at": "2017-09-01T21:16:27.850Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -933,7 +933,7 @@ "footprint_perimeter": 201, "updated_at": "2017-09-01T21:17:07.502Z", "created_at": "2017-09-01T21:16:27.643Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -1052,7 +1052,7 @@ "footprint_perimeter": 2987, "updated_at": "2017-09-01T21:17:07.496Z", "created_at": "2017-09-01T21:16:27.637Z", - "height": 15, + "floor_height": 15, "geometryType": "Polygon" } }, @@ -1139,7 +1139,7 @@ "footprint_perimeter": 519, "updated_at": "2017-09-01T21:17:07.484Z", "created_at": "2017-09-01T21:16:27.622Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -1242,7 +1242,7 @@ "footprint_perimeter": 1304, "updated_at": "2017-09-01T21:17:07.490Z", "created_at": "2017-09-01T21:16:27.629Z", - "height": 6, + "floor_height": 6, "geometryType": "Polygon" } }, @@ -1381,7 +1381,7 @@ "footprint_perimeter": 1562, "updated_at": "2017-09-01T21:17:07.478Z", "created_at": "2017-09-01T21:16:27.616Z", - "height": 6, + "floor_height": 6, "geometryType": "Polygon" } }, @@ -1436,7 +1436,7 @@ "footprint_perimeter": 211, "updated_at": "2017-09-01T21:17:07.579Z", "created_at": "2017-09-01T21:16:27.776Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -1503,7 +1503,7 @@ "footprint_perimeter": 1422, "updated_at": "2017-09-01T21:17:07.640Z", "created_at": "2017-09-01T21:16:27.866Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -1662,7 +1662,7 @@ "footprint_perimeter": 1318, "updated_at": "2017-09-01T21:17:07.472Z", "created_at": "2017-09-01T21:16:27.607Z", - "height": 9, + "floor_height": 9, "geometryType": "Polygon" } }, @@ -1813,7 +1813,7 @@ "footprint_perimeter": 3325, "updated_at": "2017-09-01T21:17:07.465Z", "created_at": "2017-09-01T21:16:27.599Z", - "height": 12, + "floor_height": 12, "geometryType": "Polygon" } }, @@ -1940,7 +1940,7 @@ "footprint_perimeter": 2194, "updated_at": "2017-09-01T21:17:07.447Z", "created_at": "2017-09-01T21:16:27.579Z", - "height": 9, + "floor_height": 9, "geometryType": "Polygon" } }, @@ -2019,7 +2019,7 @@ "footprint_perimeter": 432, "updated_at": "2017-09-01T21:17:07.453Z", "created_at": "2017-09-01T21:16:27.586Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2098,7 +2098,7 @@ "footprint_perimeter": 134, "updated_at": "2017-09-01T21:17:07.458Z", "created_at": "2017-09-01T21:16:27.592Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2161,7 +2161,7 @@ "footprint_perimeter": 758, "updated_at": "2017-09-01T21:17:07.535Z", "created_at": "2017-09-01T21:16:27.687Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2216,7 +2216,7 @@ "footprint_perimeter": 65, "updated_at": "2017-09-01T21:17:07.552Z", "created_at": "2017-09-01T21:16:27.707Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2271,7 +2271,7 @@ "footprint_perimeter": 288, "updated_at": "2017-09-01T21:17:07.557Z", "created_at": "2017-09-01T21:16:27.746Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2326,7 +2326,7 @@ "footprint_perimeter": 478, "updated_at": "2017-09-01T21:17:07.546Z", "created_at": "2017-09-01T21:16:27.700Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2381,7 +2381,7 @@ "footprint_perimeter": 286, "updated_at": "2017-09-01T21:17:07.563Z", "created_at": "2017-09-01T21:16:27.753Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2500,7 +2500,7 @@ "footprint_perimeter": 1467, "updated_at": "2017-09-01T21:17:07.541Z", "created_at": "2017-09-01T21:16:27.693Z", - "height": 6, + "floor_height": 6, "geometryType": "Polygon" } }, @@ -2555,7 +2555,7 @@ "footprint_perimeter": 97, "updated_at": "2017-09-01T21:17:07.568Z", "created_at": "2017-09-01T21:16:27.759Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, @@ -2610,7 +2610,7 @@ "footprint_perimeter": 96, "updated_at": "2017-09-01T21:17:07.596Z", "created_at": "2017-09-01T21:16:27.793Z", - "height": 3, + "floor_height": 3, "geometryType": "Polygon" } }, diff --git a/tests/geojson/test_geojson.py b/tests/geojson/test_geojson.py index d8c9f8972..31677680e 100644 --- a/tests/geojson/test_geojson.py +++ b/tests/geojson/test_geojson.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -31,32 +31,34 @@ import os import unittest -from ..context import geojson_modelica_translator # noqa - Do not remove this line -from geojson_modelica_translator.geojson.urbanopt_geojson import UrbanOptGeoJson +from geojson_modelica_translator.geojson.urbanopt_geojson import ( + UrbanOptGeoJson +) class GeoJSONTest(unittest.TestCase): - def test_load_geojson(self): - filename = os.path.abspath('tests/geojson/data/geojson_1.json') + filename = os.path.abspath("tests/geojson/data/geojson_1.json") json = UrbanOptGeoJson(filename) self.assertIsNotNone(json.data) self.assertEqual(len(json.data.features), 4) def test_missing_file(self): - fn = 'non-existent-path' + fn = "non-existent-path" with self.assertRaises(Exception) as exc: UrbanOptGeoJson(fn) - self.assertEqual(f'URBANopt GeoJSON file does not exist: {fn}', str(exc.exception)) + self.assertEqual( + f"URBANopt GeoJSON file does not exist: {fn}", str(exc.exception) + ) def test_validate(self): - filename = os.path.abspath('tests/geojson/data/geojson_1.json') + filename = os.path.abspath("tests/geojson/data/geojson_1.json") json = UrbanOptGeoJson(filename) valid, results = json.validate() self.assertFalse(valid) - self.assertEqual(len(results['building']), 3) - self.assertEqual(results['building'][0]['id'], '5a6b99ec37f4de7f94020090') + self.assertEqual(len(results["building"]), 3) + self.assertEqual(results["building"][0]["id"], "5a6b99ec37f4de7f94020090") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/geojson/test_schemas.py b/tests/geojson/test_schemas.py index b8bbbe5a8..5266fc6ab 100644 --- a/tests/geojson/test_schemas.py +++ b/tests/geojson/test_schemas.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -28,8 +28,6 @@ **************************************************************************************************** """ -from ..context import geojson_modelica_translator # noqa - Do not remove this line - import unittest from geojson_modelica_translator.geojson.schemas import Schemas @@ -38,18 +36,18 @@ class SchemasTest(unittest.TestCase): def test_load_schemas(self): s = Schemas() - data = s.retrieve('building') - self.assertEqual(data['title'], 'Building object') + data = s.retrieve("building") + self.assertEqual(data["title"], "Building object") def test_invalid_retrieve(self): s = Schemas() with self.assertRaises(Exception) as context: - s.retrieve('judicate') - self.assertEqual('Schema for judicate does not exist', str(context.exception)) + s.retrieve("judicate") + self.assertEqual("Schema for judicate does not exist", str(context.exception)) def test_validate_schema(self): s = Schemas() - s.retrieve('building') + s.retrieve("building") # verify that the schema can validate an instance with simple parameters instance = { @@ -64,17 +62,17 @@ def test_validate_schema(self): "number_of_stories_above_ground": 3, "building_status": "Proposed", "floor_area": 51177, - "year_built": 2010 + "year_built": 2010, } - res = s.validate('building', instance) + res = s.validate("building", instance) self.assertEqual(len(res), 0) # bad system_type - instance['type'] = 'MagicBuilding' - res = s.validate('building', instance) + instance["type"] = "MagicBuilding" + res = s.validate("building", instance) self.assertIn("'MagicBuilding' is not one of ['Building']", res[0]) self.assertEqual(len(res), 1) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/model_connectors/data/RefBldgSmallOfficeNew2004_Chicago.idf b/tests/model_connectors/data/RefBldgSmallOfficeNew2004_Chicago.idf index a9e959582..84b1effcf 100644 --- a/tests/model_connectors/data/RefBldgSmallOfficeNew2004_Chicago.idf +++ b/tests/model_connectors/data/RefBldgSmallOfficeNew2004_Chicago.idf @@ -4834,4 +4834,3 @@ 0, !- Nuclear High Level Emission Factor {g/MJ} , !- Nuclear High Level Emission Factor Schedule Name 0; !- Nuclear Low Level Emission Factor {m3/MJ} - diff --git a/tests/model_connectors/data/jm_ipython.sh b/tests/model_connectors/data/jm_ipython.sh new file mode 100644 index 000000000..00aa2808c --- /dev/null +++ b/tests/model_connectors/data/jm_ipython.sh @@ -0,0 +1,105 @@ +#!/bin/bash +################################################# +# Shell script that simulates JModelica using +# a docker image of JModelica. +# +# The main purpose of this script is to export +# MODELICAPATH and PYTHONPATH with their values +# updated for the docker, and to mount the +# required directories. +################################################# +set -e +IMG_NAME=ubuntu-1804_jmodelica_trunk +DOCKER_USERNAME=michaelwetter + +# Function declarations +function create_mount_command() +{ + local pat="$1" + # Each entry in pat will be a mounted read-only volume + local mnt_cmd="" + for ele in ${pat//:/ }; do + mnt_cmd="${mnt_cmd} -v ${ele}:/mnt${ele}:ro" + done + + # On Darwin, the exported temporary folder needs to be /private/var/folders, not /var/folders + # see https://askubuntu.com/questions/600018/how-to-display-the-paths-in-path-separately + if [ `uname` == "Darwin" ]; then + mnt_cmd=`echo ${mnt_cmd} | sed -e 's| /var/folders/| /private/var/folders/|g'` + fi + echo "${mnt_cmd}" +} + +function update_path_variable() +{ + # Prepend /mnt/ in front of each entry of a PATH variable in which the arguments are + # separated by a colon ":" + # This allows for example to create the new MODELICAPATH + local pat="$1" + local new_pat=`(set -f; IFS=:; printf "/mnt%s:" ${pat})` + # Cut the trailing ':' + new_pat=${new_pat%?} + echo "${new_pat}" +} + +# Export the MODELICAPATH +if [ -z ${MODELICAPATH+x} ]; then + MODELICAPATH=`pwd` +else + # Add the current directory to the front of the Modelica path. + # This will export the directory to the docker, and also set + # it in the MODELICAPATH so that JModelica finds it. + MODELICAPATH=`pwd`:${MODELICAPATH} +fi + +# Create the command to mount all directories in read-only mode +# a) for MODELICAPATH +MOD_MOUNT=`create_mount_command ${MODELICAPATH}` +# b) for PYTHONPATH +PYT_MOUNT=`create_mount_command ${PYTHONPATH}` + +# Prepend /mnt/ in front of each entry, which will then be used as the MODELICAPATH +DOCKER_MODELICAPATH=`update_path_variable ${MODELICAPATH}` +DOCKER_PYTHONPATH=`update_path_variable ${PYTHONPATH}` + +# If the current directory is part of the argument list, +# replace it with . as the docker may have a different file structure +cur_dir=`pwd` +bas_nam=`basename ${cur_dir}` +arg_lis=`echo $@ | sed -e "s|${cur_dir}|.|g"` + +# Set variable for shared directory +sha_dir=`dirname ${cur_dir}` + +# Check if the python script should be run interactively (if -i is specified) +while [ $# -ne 0 ] +do + arg="$1" + case "$arg" in + -i) + interactive=true + DOCKER_INTERACTIVE=-t + ;; + esac + shift +done + +# --user=${UID} \ + +docker run \ + --user=${UID} \ + -i \ + $DOCKER_INTERACTIVE \ + --detach=false \ + ${MOD_MOUNT} \ + ${PYT_MOUNT} \ + -v ${sha_dir}:/mnt/shared \ + -e DISPLAY=${DISPLAY} \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ + --rm \ + ${DOCKER_USERNAME}/${IMG_NAME} /bin/bash -c \ + "export MODELICAPATH=${DOCKER_MODELICAPATH}:/usr/local/JModelica/ThirdParty/MSL && \ + export PYTHONPATH=${DOCKER_PYTHONPATH} && \ + cd /mnt/shared/${bas_nam} && \ + /usr/local/JModelica/bin/jm_ipython.sh ${arg_lis}" +exit $? diff --git a/tests/model_connectors/data/jmodelica.py b/tests/model_connectors/data/jmodelica.py new file mode 100644 index 000000000..7066c43bb --- /dev/null +++ b/tests/model_connectors/data/jmodelica.py @@ -0,0 +1,132 @@ +########################################################################## +# Script to simulate Modelica models with JModelica. +# +########################################################################## +# Import the function for compilation of models and the load_fmu method + +import os +import shutil +import sys + +import pymodelica +from pyfmi import load_fmu +from pymodelica import compile_fmu + +# import matplotlib.pyplot as plt + +debug_solver = False +model = "Buildings.Controls.OBC.CDL.Continuous.Validation.LimPID" +# Overwrite model with command line argument if specified +if len(sys.argv) > 1: + # If the argument is a file, then parse it to a model name + if os.path.isfile(sys.argv[1]): + model = sys.argv[1].replace(os.path.sep, ".")[:-3] + else: + model = sys.argv[1] + + +print("*** Compiling {}".format(model)) +# Increase memory +pymodelica.environ["JVM_ARGS"] = "-Xmx4096m" + + +sys.stdout.flush() + +###################################################################### +# Compile fmu +fmu_name = compile_fmu( + model, + version="2.0", + compiler_log_level="warning", + compiler_options={ + "generate_html_diagnostics": False, + "nle_solver_tol_factor": 1e-2, + }, +) + +###################################################################### +# Load model +mod = load_fmu(fmu_name, log_level=3) + +###################################################################### +# Retrieve and set solver options +x_nominal = mod.nominal_continuous_states +opts = mod.simulate_options() # Retrieve the default options + +opts["solver"] = "CVode" +opts["ncp"] = 5000 + +if opts["solver"].lower() == "cvode": + # Set user-specified tolerance if it is smaller than the tolerance in the .mo file + rtol = 1.0e-6 + x_nominal = mod.nominal_continuous_states + + if len(x_nominal) > 0: + atol = rtol * x_nominal + else: + atol = rtol + + opts["CVode_options"] = { + "external_event_detection": False, + "maxh": ( + mod.get_default_experiment_stop_time() + - mod.get_default_experiment_stop_time() + ) + / float(opts["ncp"]), + "iter": "Newton", + "discr": "BDF", + "rtol": rtol, + "atol": atol, + "store_event_points": True, + } + +if debug_solver: + opts["logging"] = True # <- Turn on solver debug logging +mod.set("_log_level", 6) + +###################################################################### +# Simulate +res = mod.simulate(options=opts) +# logging.error(traceback.format_exc()) + +# plt.plot(res['time'], res['x1']) +# plt.plot(res['time'], res['x2']) +# plt.xlabel('time in [s]') +# plt.ylabel('line2.y') +# plt.grid() +# plt.show() +# plt.savefig("plot.pdf") + +###################################################################### +# Copy style sheets. +# This is a hack to get the css and js files to render the html diagnostics. +htm_dir = os.path.splitext(os.path.basename(fmu_name))[0] + "_html_diagnostics" +if os.path.exists(htm_dir): + for fil in ["scripts.js", "style.css", "zepto.min.js"]: + src = os.path.join(".jmodelica_html", fil) + if os.path.exists(src): + des = os.path.join(htm_dir, fil) + shutil.copyfile(src, des) + +###################################################################### +# Get debugging information +if debug_solver: + # Load the debug information + from pyfmi.debug import CVodeDebugInformation + + debug = CVodeDebugInformation(model.replace(".", "_") + "_debug.txt") + + # Below are options to plot the order, error and step-size evolution. + # The error methos also take a threshold and a region if you want to + # limit the plot to a certain interval. + + # Plot order evolution + debug.plot_order() + + # Plot error evolution + debug.plot_error() # Note see also the arguments to the method + + # Plot the used step-size + debug.plot_step_size() + + # See also debug? diff --git a/tests/model_connectors/data/spawn_system_params_ex2.json b/tests/model_connectors/data/spawn_system_params_ex2.json index d85811297..b2abf057e 100644 --- a/tests/model_connectors/data/spawn_system_params_ex2.json +++ b/tests/model_connectors/data/spawn_system_params_ex2.json @@ -45,5 +45,3 @@ ] } } - - diff --git a/tests/model_connectors/test_ets.py b/tests/model_connectors/test_ets.py new file mode 100644 index 000000000..a7f9d54fa --- /dev/null +++ b/tests/model_connectors/test_ets.py @@ -0,0 +1,76 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import os +import unittest + +from geojson_modelica_translator.model_connectors.ets_template import ( + ETSTemplate +) + + +class ETSModelConnectorSingleBuildingTest(unittest.TestCase): + def setUp(self): # the first method/member must be setUp + base_folder = os.path.join(os.getcwd(), "geojson_modelica_translator") + dest_path = "/geojson/data/schemas/thermal_junction_properties.json" + self.thermal_junction_properties_geojson = base_folder + dest_path + dest_path = "/system_parameters/schema.json" + self.system_parameters_geojson = base_folder + dest_path + dest_path = "/modelica/CoolingIndirect.mo" + self.ets_from_building_modelica = base_folder + dest_path + + self.ets = ETSTemplate( + self.thermal_junction_properties_geojson, + self.system_parameters_geojson, + self.ets_from_building_modelica, + ) + self.assertIsNotNone(self.ets) + + return self.ets + + def test_ets_thermal_junction(self): + ets_general = self.ets.check_ets_thermal_junction() + self.assertTrue(ets_general) + + def test_ets_system_parameters(self): + self.assertIsNotNone(self.ets.check_ets_system_parameters()) + + def test_ets_from_building_modelica(self): + self.assertTrue(self.ets.check_ets_from_building_modelica()) + + def test_ets_to_modelica(self): + self.assertIsNotNone(self.ets.to_modelica()) + + def test_ets_in_dymola(self): + self.assertIsNotNone(self.ets.templated_ets_openloops_dymola()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/model_connectors/test_spawn.py b/tests/model_connectors/test_spawn.py index 4f38a95ba..2e5d87f39 100644 --- a/tests/model_connectors/test_spawn.py +++ b/tests/model_connectors/test_spawn.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -29,59 +29,92 @@ """ import os +import shutil import unittest +from pathlib import Path -from ..context import geojson_modelica_translator # noqa - Do not remove this line -from geojson_modelica_translator.system_parameters.system_parameters import SystemParameters -from geojson_modelica_translator.geojson_modelica_translator import GeoJsonModelicaTranslator +from geojson_modelica_translator.geojson_modelica_translator import ( + GeoJsonModelicaTranslator +) from geojson_modelica_translator.model_connectors.spawn import SpawnConnector +from geojson_modelica_translator.modelica.modelica_runner import ModelicaRunner +from geojson_modelica_translator.system_parameters.system_parameters import ( + SystemParameters +) class SpawnModelConnectorSingleBuildingTest(unittest.TestCase): def setUp(self): - prj_dir = 'tests/model_connectors/output/spawn_single' + root_dir, project_name = "tests/model_connectors/output", "spawn_single" + + if os.path.exists(os.path.join(root_dir, project_name)): + shutil.rmtree(os.path.join(root_dir, project_name)) # load in the example geojson with a single offie building - filename = os.path.abspath('tests/model_connectors/data/spawn_geojson_ex1.json') - gj = GeoJsonModelicaTranslator.from_geojson(filename) - gj.scaffold_directory(prj_dir) # use the GeoJson translator to scaffold out the directory + filename = os.path.abspath("tests/model_connectors/data/spawn_geojson_ex1.json") + self.gj = GeoJsonModelicaTranslator.from_geojson(filename) + self.gj.scaffold_directory(root_dir, project_name) # use the GeoJson translator to scaffold out the directory # load system parameter data - filename = os.path.abspath('tests/model_connectors/data/spawn_system_params_ex1.json') + filename = os.path.abspath("tests/model_connectors/data/spawn_system_params_ex1.json") sys_params = SystemParameters(filename) # now test the spawn connector (independent of the larger geojson translator self.spawn = SpawnConnector(sys_params) - for b in gj.buildings: + for b in self.gj.buildings: self.spawn.add_building(b) def test_spawn_init(self): self.assertIsNotNone(self.spawn) - self.assertEqual(self.spawn.system_parameters.get_param('buildings.custom')[0]['load_model'], 'Spawn') + self.assertEqual(self.spawn.system_parameters.get_param("buildings.custom")[0]["load_model"], "Spawn", ) def test_spawn_to_modelica(self): - self.spawn.to_modelica('spawn_single', 'tests/model_connectors/output') + self.spawn.to_modelica(self.gj.scaffold) + + def test_spawn_to_modelica_and_run(self): + self.spawn.to_modelica(self.gj.scaffold) + + # make sure the model can run using the ModelicaRunner class + mr = ModelicaRunner() + file_to_run = os.path.abspath( + os.path.join(self.gj.scaffold.loads_path.files_dir, 'B5a6b99ec37f4de7f94020090', 'coupling.mo'), + ) + run_path = Path(os.path.abspath(self.gj.scaffold.project_path)).parent + exitcode = mr.run_in_docker(file_to_run, run_path=run_path) + self.assertEqual(0, exitcode) class SpawnModelConnectorTwoBuildingTest(unittest.TestCase): def setUp(self): - prj_dir = 'tests/model_connectors/output/spawn_two_building' + root_dir, project_name = "tests/model_connectors/output", "spawn_two_building" + + if os.path.exists(os.path.join(root_dir, project_name)): + shutil.rmtree(os.path.join(root_dir, project_name)) # load in the example geojson with a single offie building - filename = os.path.abspath('tests/model_connectors/data/spawn_geojson_ex2.json') - gj = GeoJsonModelicaTranslator.from_geojson(filename) - gj.scaffold_directory(prj_dir) # use the GeoJson translator to scaffold out the directory + filename = os.path.abspath("tests/model_connectors/data/spawn_geojson_ex2.json") + self.gj = GeoJsonModelicaTranslator.from_geojson(filename) + self.gj.scaffold_directory(root_dir, project_name) # use the GeoJson translator to scaffold out the directory # load system parameter data - filename = os.path.abspath('tests/model_connectors/data/spawn_system_params_ex2.json') + filename = os.path.abspath("tests/model_connectors/data/spawn_system_params_ex2.json") sys_params = SystemParameters(filename) # now test the spawn connector (independent of the larger geojson translator self.spawn = SpawnConnector(sys_params) - for b in gj.buildings: + for b in self.gj.buildings: self.spawn.add_building(b) - def test_spawn_to_modelica(self): - self.spawn.to_modelica('spawn_two_building', 'tests/model_connectors/output') + def test_spawn_to_modelica_and_run(self): + self.spawn.to_modelica(self.gj.scaffold) + + # make sure the model can run using the ModelicaRunner class + mr = ModelicaRunner() + file_to_run = os.path.abspath( + os.path.join(self.gj.scaffold.loads_path.files_dir, 'B5a6b99ec37f4de7f94021950', 'coupling.mo') + ) + run_path = Path(os.path.abspath(self.gj.scaffold.project_path)).parent + exitcode = mr.run_in_docker(file_to_run, run_path=run_path) + self.assertEqual(0, exitcode) diff --git a/tests/modelica/data/BouncingBall.mo b/tests/modelica/data/BouncingBall.mo new file mode 100644 index 000000000..f30b5824c --- /dev/null +++ b/tests/modelica/data/BouncingBall.mo @@ -0,0 +1,23 @@ +model BouncingBall + parameter Real e=0.7 "coefficient of restitution"; + parameter Real g=9.81 "gravity acceleration"; + Real h(fixed=true, start=1) "height of ball"; + Real v(fixed=true) "velocity of ball"; + Boolean flying(fixed=true, start=true) "true, if ball is flying"; + Boolean impact; + Real v_new(fixed=true); + Integer foo; + +equation + impact = h <= 0.0; + foo = if impact then 1 else 2; + der(v) = if flying then -g else 0; + der(h) = v; + + when {h <= 0.0 and v <= 0.0,impact} then + v_new = if edge(impact) then -e*pre(v) else 0; + flying = v_new > 0; + reinit(v, v_new); + end when; + +end BouncingBall; diff --git a/tests/modelica/data/package.mo b/tests/modelica/data/package.mo index 510b48b53..03228d116 100644 --- a/tests/modelica/data/package.mo +++ b/tests/modelica/data/package.mo @@ -1,5 +1,5 @@ within Project.B5a6b99ec37f4de7f94020090; package B5a6b99ec37f4de7f94020090_Models extends Modelica.Icons.Package; - + end B5a6b99ec37f4de7f94020090_Models; diff --git a/tests/modelica/test_input_parser.py b/tests/modelica/test_input_parser.py index 77c4c95ca..7dca779ab 100644 --- a/tests/modelica/test_input_parser.py +++ b/tests/modelica/test_input_parser.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -28,8 +28,6 @@ **************************************************************************************************** """ -from ..context import geojson_modelica_translator # noqa - Do not remove this line - import filecmp import os import unittest @@ -39,55 +37,62 @@ class InputParserTest(unittest.TestCase): def setUp(self): - self.results_path = os.path.abspath('tests/modelica/output') + self.results_path = os.path.abspath("tests/modelica/output") if not os.path.exists(self.results_path): os.mkdir(self.results_path) def test_missing_file(self): - fn = 'non-existent-path' + fn = "non-existent-path" with self.assertRaises(Exception) as exc: InputParser(fn) - self.assertEqual(f'Modelica file does not exist: {fn}', str(exc.exception)) + self.assertEqual(f"Modelica file does not exist: {fn}", str(exc.exception)) def test_roundtrip(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_1.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_1.mo") f = InputParser(filename) f.save_as(new_filename) self.assertTrue(filecmp.cmp(filename, new_filename)) def test_remove_object(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_2.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_2.mo") f1 = InputParser(filename) - f1.remove_object('ReaderTMY3') + f1.remove_object("ReaderTMY3") f1.save_as(new_filename) self.assertFalse(filecmp.cmp(filename, new_filename)) f1.reload() f2 = InputParser(new_filename) - self.assertGreater(len(f1.model['objects']), len(f2.model['objects'])) + self.assertGreater(len(f1.model["objects"]), len(f2.model["objects"])) # verify that it exists in f1 but not in f2 - self.assertGreaterEqual(f1.find_model_object('ReaderTMY3')[0], 0) - self.assertIsNone(f2.find_model_object('ReaderTMY3')[0]) + self.assertGreaterEqual(f1.find_model_object("ReaderTMY3")[0], 0) + self.assertIsNone(f2.find_model_object("ReaderTMY3")[0]) def test_gsub_field(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_3.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_3.mo") f1 = InputParser(filename) # This example is actually updating an annotation object, not a model, but leave it here for now. - f1.replace_model_string('Modelica.Blocks.Sources.CombiTimeTable', 'internalGains', 'Internals', 'NotInternals') + f1.replace_model_string( + "Modelica.Blocks.Sources.CombiTimeTable", + "internalGains", + "Internals", + "NotInternals", + ) f1.save_as(new_filename) f2 = InputParser(new_filename) - index, model = f2.find_model_object('Modelica.Blocks.Sources.CombiTimeTable internalGains') + index, model = f2.find_model_object( + "Modelica.Blocks.Sources.CombiTimeTable internalGains" + ) self.assertFalse(filecmp.cmp(filename, new_filename)) # the 5th index is the rotation... non-ideal look up - self.assertTrue('NotInternals' in model) + self.assertTrue("NotInternals" in model) def test_rename_filename(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_4.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_4.mo") f1 = InputParser(filename) # This example is actually updating an annotation object, not a model, but leave it here for now. f1.replace_model_string( @@ -99,54 +104,88 @@ def test_rename_filename(self): f1.save_as(new_filename) f2 = InputParser(new_filename) - index, model = f2.find_model_object('Modelica.Blocks.Sources.CombiTimeTable internalGains') + index, model = f2.find_model_object( + "Modelica.Blocks.Sources.CombiTimeTable internalGains" + ) self.assertFalse(filecmp.cmp(filename, new_filename)) # the 5th index is the rotation... non-ideal look up - self.assertTrue('a/new/path.mat' in model) + self.assertTrue("a/new/path.mat" in model) def test_add_model_obj(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_5.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_5.mo") f1 = InputParser(filename) data = [ 'annotation (Placement(transformation(extent={{-10,90},{10,110}}), iconTransformation(extent={{-10,90},{10,110}})));' # noqa ] - f1.add_model_object('Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a', 'port_a', data) + f1.add_model_object( + "Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a", "port_a", data + ) f1.save_as(new_filename) # verify in the new file that the new model object exists f2 = InputParser(new_filename) - index, model = f2.find_model_object('Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a port_a') + index, model = f2.find_model_object( + "Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a port_a" + ) self.assertFalse(filecmp.cmp(filename, new_filename)) self.assertGreaterEqual(index, 0) def test_gsub_connect(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_6.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_6.mo") f1 = InputParser(filename) - f1.add_connect('port_a', 'thermalZoneTwoElements.intGainsConv', - 'annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))') + f1.add_connect( + "port_a", + "thermalZoneTwoElements.intGainsConv", + "annotation (Line(points={{0,100},{96,100},{96,20},{92,20}}, color={191,0,0}))", + ) f1.save_as(new_filename) # verify in the new file that the new model object exists f2 = InputParser(new_filename) self.assertFalse(filecmp.cmp(filename, new_filename)) - index, c = f2.find_connect('port_a', 'thermalZoneTwoElements.intGainsConv') + index, c = f2.find_connect("port_a", "thermalZoneTwoElements.intGainsConv") self.assertGreaterEqual(index, 0) def test_rename_connection(self): - filename = os.path.abspath('tests/modelica/data/test_1.mo') - new_filename = os.path.abspath('tests/modelica/output/test_1_output_7.mo') + filename = os.path.abspath("tests/modelica/data/test_1.mo") + new_filename = os.path.abspath("tests/modelica/output/test_1_output_7.mo") f1 = InputParser(filename) # connect(weaDat.weaBus, HDifTil[3].weaBus) - f1.replace_connect_string('eqAirTemp.TEqAir', 'prescribedTemperature.T', 'NothingOfImportance', None) - f1.replace_connect_string('weaDat.weaBus', None, 'weaBus', None, True) + f1.replace_connect_string( + "eqAirTemp.TEqAir", "prescribedTemperature.T", "NothingOfImportance", None + ) + f1.replace_connect_string("weaDat.weaBus", None, "weaBus", None, True) + f1.save_as(new_filename) + + f2 = InputParser(new_filename) + self.assertFalse(filecmp.cmp(filename, new_filename)) + index, c = f2.find_connect('weaBus', 'weaBus') + # there should exist the new connection + self.assertGreaterEqual(index, 0) + + # the old one should not exist + index, c = f2.find_connect('weaDat.weaBus', 'weaBus') + self.assertIsNone(index) + + def test_remove_connection(self): + filename = os.path.abspath('tests/modelica/data/test_1.mo') + new_filename = os.path.abspath('tests/modelica/output/test_1_output_8.mo') + f1 = InputParser(filename) + f1.remove_connect_string('weaDat.weaBus', 'weaBus') f1.save_as(new_filename) + f2 = InputParser(new_filename) + self.assertFalse(filecmp.cmp(filename, new_filename)) + index, c = f2.find_connect('weaDat.weaBus', 'weaBus') + # the connection should no longer exist + self.assertIsNone(index) + def update_connection(self): pass -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/modelica/test_modelica_runner.py b/tests/modelica/test_modelica_runner.py new file mode 100644 index 000000000..25672ccc1 --- /dev/null +++ b/tests/modelica/test_modelica_runner.py @@ -0,0 +1,86 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import os +import shutil +import unittest + +from geojson_modelica_translator.modelica.modelica_runner import ModelicaRunner + + +class ModelicaRunnerTest(unittest.TestCase): + # create a run directory and copy in a project to test run + def setUp(self): + self.run_path = os.path.abspath('tests/modelica/output/simdir') + if not os.path.exists(self.run_path): + os.mkdir(self.run_path) + # copy in the test modelica file (teaser model) + shutil.copyfile(os.path.abspath('tests/modelica/data/BouncingBall.mo'), + os.path.join(self.run_path, 'BouncingBall.mo')) + + def test_run_setup(self): + prev_mod_path = os.environ.get('MODELICAPATH', None) + try: + os.environ['MODELICAPATH'] = 'A_PATH/to_something' + mr = ModelicaRunner() + self.assertEqual(mr.modelica_lib_path, 'A_PATH/to_something') + finally: + if prev_mod_path: + os.environ['MODELICAPATH'] = prev_mod_path + print(mr.jmodelica_py_path) + self.assertTrue(os.path.exists(mr.jmodelica_py_path)) + self.assertTrue(os.path.exists(mr.jm_ipython_path)) + + def test_docker_enabled(self): + mr = ModelicaRunner() + self.assertTrue(mr.docker_configured, 'Docker is not running, unable to run all tests') + + def test_run_in_docker_errors(self): + mr = ModelicaRunner() + file_to_run = os.path.join(self.run_path, 'no_file.mo') + with self.assertRaises(Exception) as exc: + mr.run_in_docker(file_to_run) + self.assertEqual(f'File not found to run {file_to_run}', str(exc.exception)) + + file_to_run = os.path.join(self.run_path) + with self.assertRaises(Exception) as exc: + mr.run_in_docker(file_to_run) + self.assertEqual(f'Expecting to run a file, not a folder in {file_to_run}', str(exc.exception)) + + def test_run_in_docker(self): + mr = ModelicaRunner() + mr.run_in_docker(os.path.join(self.run_path, 'BouncingBall.mo')) + mr.cleanup_path(self.run_path) + self.assertTrue(os.path.exists(os.path.join(self.run_path, 'stdout.log'))) + self.assertTrue(os.path.exists(os.path.join(self.run_path, 'BouncingBall_result.mat'))) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/system_parameters/test_system_parameters.py b/tests/system_parameters/test_system_parameters.py index a44724c1e..047798ba4 100644 --- a/tests/system_parameters/test_system_parameters.py +++ b/tests/system_parameters/test_system_parameters.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -31,99 +31,95 @@ import os import unittest -from geojson_modelica_translator.system_parameters.system_parameters import SystemParameters +from geojson_modelica_translator.system_parameters.system_parameters import ( + SystemParameters +) class GeoJSONTest(unittest.TestCase): - def test_load_system_parameters_1(self): - filename = os.path.abspath('tests/system_parameters/data/system_params_1.json') + filename = os.path.abspath("tests/system_parameters/data/system_params_1.json") sdp = SystemParameters(filename) - self.assertEqual(sdp.data['buildings']['default']['load_model_parameters']['rc']['order'], 2) + self.assertEqual( + sdp.data["buildings"]["default"]["load_model_parameters"]["rc"]["order"], 2 + ) def test_load_system_parameters_2(self): - filename = os.path.abspath('tests/system_parameters/data/system_params_2.json') + filename = os.path.abspath("tests/system_parameters/data/system_params_2.json") sdp = SystemParameters(filename) self.assertIsNotNone(sdp) def test_missing_file(self): - fn = 'non-existent-path' + fn = "non-existent-path" with self.assertRaises(Exception) as exc: SystemParameters(fn) - self.assertEqual(f'System design parameters file does not exist: {fn}', str(exc.exception)) + self.assertEqual( + f"System design parameters file does not exist: {fn}", str(exc.exception) + ) def test_errors(self): data = { "buildings": { "default": { "load_model": "ROM/RC", - "load_model_parameters": { - "rc": { - "order": 6 - } - } + "load_model_parameters": {"rc": {"order": 6}}, } } } with self.assertRaises(Exception) as exc: SystemParameters.loadd(data) - self.assertRegex(str(exc.exception), 'Invalid system parameter file.*') + self.assertRegex(str(exc.exception), "Invalid system parameter file.*") sp = SystemParameters.loadd(data, validate_on_load=False) - self.assertEqual(sp.validate()[0], '6 is not one of [1, 2, 3, 4]') + self.assertEqual(sp.validate()[0], "6 is not one of [1, 2, 3, 4]") def test_get_param(self): data = { "buildings": { "default": { "load_model": "ROM/RC", - "load_model_parameters": { - "rc": { - "order": 4 - } - } + "load_model_parameters": {"rc": {"order": 4}}, } } } sp = SystemParameters.loadd(data) - value = sp.get_param('buildings.default.load_model_parameters.rc.order') + value = sp.get_param("buildings.default.load_model_parameters.rc.order") self.assertEqual(value, 4) - value = sp.get_param('buildings.default.load_model') - self.assertEqual(value, 'ROM/RC') + value = sp.get_param("buildings.default.load_model") + self.assertEqual(value, "ROM/RC") - value = sp.get_param('buildings.default') - self.assertDictEqual(value, {'load_model': 'ROM/RC', 'load_model_parameters': {"rc": {"order": 4}}}) + value = sp.get_param("buildings.default") + self.assertDictEqual( + value, + {"load_model": "ROM/RC", "load_model_parameters": {"rc": {"order": 4}}}, + ) - value = sp.get_param('') + value = sp.get_param("") self.assertIsNone(value) - value = sp.get_param('not.a.real.path') + value = sp.get_param("not.a.real.path") self.assertIsNone(value) def test_get_param_with_default(self): - data = { - "buildings": { - "default": { - "load_model": "Spawn" - } - } - } + data = {"buildings": {"default": {"load_model": "Spawn"}}} sp = SystemParameters.loadd(data) - value = sp.get_param('buildings.default.load_model_parameters.rc.order', default=2) + value = sp.get_param( + "buildings.default.load_model_parameters.rc.order", default=2 + ) self.assertEqual(value, 2) - value = sp.get_param('not.a.real.path', default=2) + value = sp.get_param("not.a.real.path", default=2) self.assertEqual(value, 2) def test_get_param_with_building_id(self): - filename = os.path.abspath('tests/system_parameters/data/system_params_1.json') + filename = os.path.abspath("tests/system_parameters/data/system_params_1.json") sdp = SystemParameters(filename) - value = sdp.get_param_by_building_id('abcd1234', 'ets.system') - self.assertEqual(value, 'Booster Heater') + value = sdp.get_param_by_building_id("abcd1234", "ets.system") + self.assertEqual(value, "Booster Heater") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py new file mode 100644 index 000000000..dad9d7fa8 --- /dev/null +++ b/tests/test_scaffold.py @@ -0,0 +1,49 @@ +""" +**************************************************************************************************** +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions +and the following disclaimer in the documentation and/or other materials provided with the +distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**************************************************************************************************** +""" + +import os +import unittest + +from geojson_modelica_translator.scaffold import Scaffold + + +class ScaffoldTest(unittest.TestCase): + def test_scaffold(self): + root_dir = os.path.abspath(os.path.join("tests", "output")) + scaffold = Scaffold(root_dir, "scaffold_01", overwrite=True) + scaffold.create() + # self.assertEqual(mp.resources_dir, os.path.join("Resources", "Data", "Loads")) + self.assertTrue( + os.path.exists(os.path.join(root_dir, "scaffold_01", "Resources", "Scripts", "Loads", "Dymola")) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_translator.py b/tests/test_translator.py index 592f2d636..4f21e7a81 100644 --- a/tests/test_translator.py +++ b/tests/test_translator.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -28,14 +28,20 @@ **************************************************************************************************** """ +import itertools import os import shutil import unittest -import itertools -from .context import geojson_modelica_translator # noqa - Do not remove this line -from geojson_modelica_translator.geojson_modelica_translator import GeoJsonModelicaTranslator -from geojson_modelica_translator.system_parameters.system_parameters import SystemParameters +from geojson_modelica_translator.geojson_modelica_translator import ( + GeoJsonModelicaTranslator +) +# from geojson_modelica_translator.modelica.modelica_runner import ModelicaRunner +from geojson_modelica_translator.system_parameters.system_parameters import ( + SystemParameters +) + +# from pathlib import Path class GeoJSONTranslatorTest(unittest.TestCase): @@ -44,89 +50,187 @@ def test_init(self): self.assertIsNotNone(gj) def test_from_geojson(self): - filename = os.path.abspath('tests/geojson/data/geojson_1.json') + filename = os.path.abspath("tests/geojson/data/geojson_1.json") gj = GeoJsonModelicaTranslator.from_geojson(filename) self.assertEqual(len(gj.buildings), 3) def test_missing_geojson(self): - fn = 'non-existent-path' + fn = "non-existent-path" with self.assertRaises(Exception) as exc: GeoJsonModelicaTranslator.from_geojson(fn) - self.assertEqual(f'GeoJSON file does not exist: {fn}', str(exc.exception)) + self.assertEqual(f"GeoJSON file does not exist: {fn}", str(exc.exception)) def test_to_modelica_defaults(self): - self.results_path = os.path.abspath('tests/output/geojson_1') + self.results_path = os.path.abspath("tests/output/geojson_1") if os.path.exists(self.results_path): shutil.rmtree(self.results_path) - filename = os.path.abspath('tests/geojson/data/geojson_1.json') + filename = os.path.abspath("tests/geojson/data/geojson_1.json") gj = GeoJsonModelicaTranslator.from_geojson(filename) sys_params = SystemParameters() gj.set_system_parameters(sys_params) - gj.to_modelica('geojson_1', 'tests/output') + gj.to_modelica("geojson_1", "tests/output") # setup what we are going to check - model_names = ['Floor', 'ICT', 'Meeting', 'Office', 'package', 'Restroom', 'Storage'] - building_paths = [os.path.join(gj.loads_path.files_dir, b.dirname) for b in gj.buildings] - path_checks = [f'{os.path.sep.join(r)}.mo' for r in itertools.product(building_paths, model_names)] + model_names = [ + "Floor", + "ICT", + "Meeting", + "Office", + "package", + "Restroom", + "Storage", + ] + building_paths = [ + os.path.join(gj.scaffold.loads_path.files_dir, b.dirname) for b in gj.buildings + ] + path_checks = [ + f"{os.path.sep.join(r)}.mo" + for r in itertools.product(building_paths, model_names) + ] for p in path_checks: - # print(p) - self.assertTrue(os.path.exists(p)) - - # look for resource files - resource_names = ['InternalGains_Floor', 'InternalGains_ICT', 'InternalGains_Meeting', 'InternalGains_Office', - 'InternalGains_Restroom', 'InternalGains_Storage'] - resource_paths = [os.path.join(gj.loads_path.files_dir, 'Resources', 'Data', b.dirname) for b in gj.buildings] - path_checks = [f'{os.path.sep.join(r)}.mat' for r in itertools.product(resource_paths, resource_names)] - - for p in path_checks: - self.assertTrue(os.path.exists(p)) + self.assertTrue(os.path.exists(p), f"Path not found {p}") + + # go through the generated buildings and ensure that the resources are created + resource_names = [ + "InternalGains_Floor", + "InternalGains_ICT", + "InternalGains_Meeting", + "InternalGains_Office", + "InternalGains_Restroom", + "InternalGains_Storage", + ] + for b in gj.buildings: + for resource_name in resource_names: + # TEASER 0.7.2 used .txt for schedule files + path = os.path.join( + gj.scaffold.loads_path.files_dir, + "Resources", + "Data", + b.dirname, + f"{resource_name}.txt", + ) + self.assertTrue(os.path.exists(path), f"Path not found: {path}") + + # verify that the models run in JModelica -- this is broken! + # mr = ModelicaRunner() + # file_to_run = os.path.abspath( + # os.path.join(gj.scaffold.loads_path.files_dir, 'B5a6b99ec37f4de7f94020090', 'Office.mo') + # ) + # run_path = Path(os.path.abspath(gj.scaffold.project_path)).parent + # exitcode = mr.run_in_docker(file_to_run, run_path=run_path) + # self.assertEqual(0, exitcode) def test_to_modelica_rc_order_4(self): - self.results_path = os.path.abspath('tests/output/rc_order_4') + self.results_path = os.path.abspath("tests/output/rc_order_4") if os.path.exists(self.results_path): shutil.rmtree(self.results_path) - filename = os.path.abspath('tests/geojson/data/geojson_1.json') + filename = os.path.abspath("tests/geojson/data/geojson_1.json") gj = GeoJsonModelicaTranslator.from_geojson(filename) sys_params = SystemParameters.loadd( - { - "buildings": { - "default": { - "load_model_parameters": { - "rc": { - "order": 4 - } - } - } - } - } + {"buildings": {"default": {"load_model_parameters": {"rc": {"order": 4}}}}} ) self.assertEqual(len(sys_params.validate()), 0) gj.set_system_parameters(sys_params) - gj.to_modelica('rc_order_4', 'tests/output') + gj.to_modelica("rc_order_4", "tests/output") # setup what we are going to check - model_names = ['Floor', 'ICT', 'Meeting', 'Office', 'package', 'Restroom', 'Storage'] - building_paths = [os.path.join(gj.loads_path.files_dir, b.dirname) for b in gj.buildings] - path_checks = [f'{os.path.sep.join(r)}.mo' for r in itertools.product(building_paths, model_names)] + model_names = [ + "Floor", + "ICT", + "Meeting", + "Office", + "package", + "Restroom", + "Storage", + ] + building_paths = [ + os.path.join(gj.scaffold.loads_path.files_dir, b.dirname) for b in gj.buildings + ] + path_checks = [ + f"{os.path.sep.join(r)}.mo" + for r in itertools.product(building_paths, model_names) + ] for p in path_checks: - # print(p) - self.assertTrue(os.path.exists(p)) + self.assertTrue(os.path.exists(p), f"Path not found: {p}") + + resource_names = [ + "InternalGains_Floor", + "InternalGains_ICT", + "InternalGains_Meeting", + "InternalGains_Office", + "InternalGains_Restroom", + "InternalGains_Storage", + ] + for b in gj.buildings: + for resource_name in resource_names: + # TEASER 0.7.2 used .txt for schedule files + path = os.path.join( + gj.scaffold.loads_path.files_dir, + "Resources", + "Data", + b.dirname, + f"{resource_name}.txt", + ) + self.assertTrue(os.path.exists(path), f"Path not found: {path}") + + # # make sure the model can run using the ModelicaRunner class + # mr = ModelicaRunner() + # file_to_run = os.path.abspath( + # f'{self.results_path}/Loads/B5a6b99ec37f4de7f94020090/Office.mo' + # ) + # exitcode = mr.run_in_docker(file_to_run) + # self.assertEqual(0, exitcode) + + +class GeoJSONUrbanOptExampleFileTranslatorTest(unittest.TestCase): + def test_init(self): + gj = GeoJSONTranslatorTest() + self.assertIsNotNone(gj) - # look for resource files - resource_names = ['InternalGains_Floor', 'InternalGains_ICT', 'InternalGains_Meeting', 'InternalGains_Office', - 'InternalGains_Restroom', 'InternalGains_Storage'] - resource_paths = [os.path.join(gj.loads_path.files_dir, 'Resources', 'Data', b.dirname) for b in gj.buildings] - path_checks = [f'{os.path.sep.join(r)}.mat' for r in itertools.product(resource_paths, resource_names)] + def test_from_geojson(self): + filename = os.path.abspath("tests/geojson/data/example_geojson_13buildings.json") + gj = GeoJsonModelicaTranslator.from_geojson(filename) - for p in path_checks: - self.assertTrue(os.path.exists(p)) + self.assertEqual(len(gj.buildings), 13) + def test_to_modelica_defaults(self): + self.results_path = os.path.abspath("tests/output/geojson_urbanopt") + if os.path.exists(self.results_path): + shutil.rmtree(self.results_path) -if __name__ == '__main__': + filename = os.path.abspath("tests/geojson/data/example_geojson_13buildings.json") + gj = GeoJsonModelicaTranslator.from_geojson(filename) + sys_params = SystemParameters() + gj.set_system_parameters(sys_params) + gj.to_modelica("geojson_urbanopt", "tests/output") + + # go through the generated buildings and ensure that the resources are created + resource_names = [ + "InternalGains_Floor", + "InternalGains_ICT", + "InternalGains_Meeting", + "InternalGains_Office", + "InternalGains_Restroom", + "InternalGains_Storage", + ] + for b in gj.buildings: + for resource_name in resource_names: + # TEASER 0.7.2 used .txt for schedule files + path = os.path.join( + gj.scaffold.loads_path.files_dir, + "Resources", + "Data", + b.dirname, + f"{resource_name}.txt", + ) + self.assertTrue(os.path.exists(path), f"Path not found: {path}") + + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index 2aca12cb0..8bc8d487d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ """ **************************************************************************************************** -:copyright (c) 2019 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. +:copyright (c) 2019-2020 URBANopt, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved. @@ -28,8 +28,6 @@ **************************************************************************************************** """ -from .context import geojson_modelica_translator # noqa - Do not remove this line - import os import unittest @@ -38,16 +36,16 @@ class ModelicaPathTest(unittest.TestCase): def test_properties(self): - mp = ModelicaPath('Loads', root_dir=None) - self.assertEqual(mp.files_dir, 'Loads') - self.assertEqual(mp.resources_dir, os.path.join('Resources', 'Data', 'Loads')) + mp = ModelicaPath("Loads", root_dir=None) + self.assertEqual(mp.files_dir, "Loads") + self.assertEqual(mp.resources_dir, os.path.join("Resources", "Data", "Loads")) - def test_scaffold(self): - root_dir = os.path.abspath(os.path.join('tests', 'output', 'test_02')) - ModelicaPath('RandomContainer', root_dir) - self.assertTrue(os.path.exists(os.path.join(root_dir, 'RandomContainer'))) - self.assertTrue(os.path.exists(os.path.join(root_dir, 'Resources', 'Data', 'RandomContainer'))) + def test_single_sub_resource(self): + root_dir = os.path.abspath(os.path.join("tests", "output", "modelica_path_01")) + ModelicaPath("RandomContainer", root_dir, overwrite=True) + self.assertTrue(os.path.exists(os.path.join(root_dir, "RandomContainer"))) + self.assertTrue(os.path.exists(os.path.join(root_dir, "Resources", "Data", "RandomContainer"))) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index 8d8c7bcbb..146398c58 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ commands= py.test . -v --cov coveralls --cov-report term-missing passenv= COVERALLS_REPO_TOKEN + MODELICAPATH whitelist_externals= cp