Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include tags in the /measurements and /inputs APIs #361

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# )

repos:
- repo: git://github.com/pre-commit/pre-commit-hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.3
hooks:
- id: trailing-whitespace
Expand Down
17 changes: 17 additions & 0 deletions data/data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ def save_data_and_jsons(self, fmu_path):
self.days_path = os.path.join(models_dir, 'days.json')
# Find the config.json path
self.config_path = os.path.join(models_dir, 'config.json')
# Find the tags.json path
self.tags_path = os.path.join(models_dir, 'tags.json')

if os.path.exists(resources_dir):
# Find all files within Resources folder
Expand Down Expand Up @@ -252,6 +254,13 @@ def save_data_and_jsons(self, fmu_path):
else:
warnings.warn('No config.json found for this test case')

# Write a copy of tags.json to the fmu resources folder
if os.path.exists(self.tags_path):
self.z_fmu.write(self.tags_path,
os.path.join('resources', 'tags.json'))
else:
warnings.warn('No tags.json found for this test case')

# Close the fmu
self.z_fmu.close()

Expand Down Expand Up @@ -384,6 +393,14 @@ def load_data_and_jsons(self):
# Load config json
json_str = z_fmu.open('resources/config.json').read()
self.case.config_json = json.loads(json_str)
# Load tags json
# This is currently not a required file
try:
json_str = z_fmu.open('resources/tags.json').read()
except:
# If there is no tags file then create an empty dictionary
json_str = '{}'
self.case.tags_json = json.loads(json_str)

# Find the test case data files
files = []
Expand Down
4 changes: 4 additions & 0 deletions docs/DesReqGui/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,7 @@

# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

# Configure bibtex
extensions = ['sphinxcontrib.bibtex']
bibtex_bibfiles = ['references.bib']
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 52 additions & 1 deletion docs/DesReqGui/source/testcasedev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ following structure:
| | |--resources // Resources directory
| | | |--kpis.json // JSON mapping outputs to KPI calculations
| | | |--days.json // JSON mapping time period names to day number for simulation
| | | |--tags.json // JSON definition of point semantic model tags
| | | |--config.json // BOPTEST configuration file and sets defaults
| | | |--weather.mos // Weather data for model simulation in Modelica
| | | |--weather.csv // Weather data for forecasting
Expand Down Expand Up @@ -243,7 +244,7 @@ Test Case Configuration and JSON Mapping
-----------------------------------------
In order to assign particular configuration and default values
for a test case upon loading in BOPTEST, a configuration JSON saved as
:code:`config.json` will have the structure::
:code:`config.json` will have the structure:

::

Expand All @@ -260,6 +261,56 @@ for a test case upon loading in BOPTEST, a configuration JSON saved as
}


Semantic Tags and JSON Mapping
------------------------------
In order to capture semantic model tags associated with a test case, a JSON saved
as :code:`tags.json` will have the structure:

::

{
"<point_name>" : // Name of input or measurement point
{"<tag_name>" : <value> // Tag name and value pair
...
},
...
}

This JSON may be created manually by a test case developer. Alternatively,
functionality of the signal exchange blocks and parser described in the
previous section will facilitate the generation of the tags JSON. An example
implementation has been completed in a fork of the Modelica Buildings Library
(https://github.com/dhblum/modelica-buildings/tree/issue_boptest_360_haystack_tags)
for tags conforming to Project Haystack
(https://project-haystack.org/). Haystack defines taxonomies which classify
particular types of tags (https://project-haystack.org/doc/index). One of
particular interest is the "marker" type (https://project-haystack.org/doc/appendix/marker),
which can be used to mark particular properties to a semantic object.
A subset of marker types have been implemented as a new Modelica package
in ``Buildings.Utilities.IO.SignalExchange.HaystackTags``. Each marker
type contains an enumerated list of allowable tags representing a subset
of those defined by Haystack. For example, the ``quantity`` marker can take on
tags ``temp``, ``flow``, ``pressure``, ``humidity``, ``power``, or ``concentration``.
New parameters are added to the signal exchange blocks, in a new dialogue tab,
which a test case developer can use to select tags for each type of marker
relevant to either overwrite points or read points. The Figure below
shows an example configuration of a supply air flow sensor point.


.. figure:: images/read-haystack-example.png
:scale: 50 %

Example configuration of tagging parameters for Haystack compliant tags
for a supply air flow sensor point.

Upon configuration of all desired Haystack tags, the ``parser.py`` will
parse the information in each signal exchange block and output the associated
``tags.json`` file. In addition to the tags defined in the signal exchange
blocks, additional tags are added by the parser which are already known
from typical BOPTEST metadata, such as the Haystack tag names "units", "writable",
"sensor", "kind", "dis", "siteRef", and "weather-point".


Data Generation and Collection Module
-------------------------------------

Expand Down
140 changes: 134 additions & 6 deletions parsing/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

from pyfmi import load_fmu
from pyfmi.fmi import FMUException
from pymodelica import compile_fmu
import os
import json
Expand All @@ -34,8 +35,8 @@ def parse_instances(model_path, file_name):
-------
instances : dict
Dictionary of overwrite and read block class instance lists.
{'Overwrite': {input_name : {Unit : unit_name, Description : description, Minimum : min, Maximum : max}},
'Read': {output_name : {Unit : unit_name, Description : description, Minimum : min, Maximum : max}}}
{'Overwrite': {input_name : {Unit : unit_name, Description : description, Minimum : min, Maximum : max, Haystack : {haystack_tags}}},
'Read': {output_name : {Unit : unit_name, Description : description, Minimum : min, Maximum : max, Haystack : {haystack_tags}}}}
signals : dict
{'signal_type' : [output_name]}

Expand Down Expand Up @@ -65,13 +66,36 @@ def parse_instances(model_path, file_name):
description = fmu.get(instance+'.description')[0]
mini = fmu.get_variable_min(instance+'.u')
maxi = fmu.get_variable_max(instance+'.u')
# Parse Haystack
pointFunctionType = fmu.get_variable_declared_type(instance+'.pointFunctionType').items[fmu.get(instance+'.pointFunctionType')[0]][0]
quantity = 'None'
ductSectionType = 'None'
substance = 'None'
equip = 'None'
customMarkers = 'None'
zone = 'None'
writable = 'writable'
# Read
elif 'boptestRead' in var:
label = 'Read'
unit = fmu.get_variable_unit(instance+'.y')
description = fmu.get(instance+'.description')[0]
mini = None
maxi = None
mini = 'None'
maxi = 'None'
# Parse Haystack
pointFunctionType = 'sensor'
quantity = fmu.get_variable_declared_type(instance+'.quantity').items[fmu.get(instance+'.quantity')[0]][0]
ductSectionType = fmu.get_variable_declared_type(instance+'.ductSectionType').items[fmu.get(instance+'.ductSectionType')[0]][0]
substance = fmu.get_variable_declared_type(instance+'.substance').items[fmu.get(instance+'.substance')[0]][0]
equip = fmu.get_variable_declared_type(instance+'.equip').items[fmu.get(instance+'.equip')[0]][0]
customMarkers = fmu.get(instance+'.customMarkers')[0]
try:
fmu.get(instance+'.zone')[0]
zone = 'zone'
except FMUException:
zone = 'None'
writable = 'None'

# KPI
elif 'KPIs' in var:
label = 'kpi'
Expand All @@ -83,6 +107,14 @@ def parse_instances(model_path, file_name):
instances[label][instance]['Description'] = description
instances[label][instance]['Minimum'] = mini
instances[label][instance]['Maximum'] = maxi
instances[label][instance]['Haystack'] = {'pointFunctionType':pointFunctionType,
'quantity':quantity,
'ductSectionType':ductSectionType,
'substance':substance,
'equip':equip,
'zone':zone,
'writable':writable,
'customMarkers':customMarkers}
else:
signal_type = fmu.get_variable_declared_type(var).items[fmu.get(var)[0]][0]
# Split certain signal types for multi-zone
Expand Down Expand Up @@ -190,8 +222,35 @@ def write_wrapper(model_path, file_name, instances):

return fmu_path, wrapped_path

def export_fmu(model_path, file_name):
'''Parse signal exchange blocks and export boptest fmu and kpi json.
def write_full_haystack_dict(instances, site_name):
'''Write haystack dictionary used to create tags json.

Parameters
----------
instances : dict

site_name : str

Returns
-------
haystack_dict

'''

haystack_dict = dict()
# Check for instances of Overwrite and/or Read blocks
len_write_blocks = len(instances['Overwrite'])
len_read_blocks = len(instances['Read'])
# If there are, write and export wrapper model
if len_write_blocks:
haystack_dict = _write_haystack_dict(instances, haystack_dict, site_name, 'Overwrite')
if len_read_blocks:
haystack_dict = _write_haystack_dict(instances, haystack_dict, site_name, 'Read')

return haystack_dict

def export_fmu(model_path, file_name, testcase_name):
'''Parse signal exchange blocks and export boptest fmu, kpi json, and tags json.

Parameters
----------
Expand All @@ -200,13 +259,17 @@ def export_fmu(model_path, file_name):
file_name : list
Path(s) to modelica file and required libraries not on MODELICAPATH.
Passed to file_name parameter of pymodelica.compile_fmu() in JModelica.
testcase_name : str
Name of testcase.

Returns
-------
fmu_path : str
Path to the wrapped modelica model fmu
kpi_path : str
Path to kpi json
tags_path : str
Path to tags json

'''

Expand All @@ -218,6 +281,11 @@ def export_fmu(model_path, file_name):
kpi_path = os.path.join(os.getcwd(), 'kpis.json')
with open(kpi_path, 'w') as f:
json.dump(signals, f)
# Write Tags json
haystack_dict = write_full_haystack_dict(instances, testcase_name)
tags_path = os.path.join(os.getcwd(), 'tags.json')
with open(tags_path, 'w') as f:
json.dump(haystack_dict, f, indent=2)
# Generate test case data
man = Data_Manager()
man.save_data_and_jsons(fmu_path=fmu_path)
Expand Down Expand Up @@ -267,6 +335,66 @@ def _make_var_name(block, style, description='', attribute=''):

return var_name

def _write_haystack_dict(instances, haystack_dict, site_name, sig_exc='Overwrite'):
'''Writes the haystack dictionary.

Parameters
----------
instances : dict
Dictionary of overwrite and read block class instance lists.
From function parse_instances.
haystack_dict : dict
Starting haystack_dict.
site_name : str
Name of site.
sig_exc : str
Signal exchange block type. Options are 'Overwrite' or 'Read'.

Returns
-------
haystack_dict : dict
Modified haystack_dict

'''

markers = ['pointFunctionType',
'quantity',
'ductSectionType',
'substance',
'equip',
'zone',
'writable',
'customMarkers']
for block in instances[sig_exc].keys():
if sig_exc == 'Overwrite':
name = _make_var_name(block,style='input_signal')
elif sig_exc == 'Read':
name = _make_var_name(block,style='output')
else:
raise ValueError('Signal exchange block type {0} unknown.'.format(sig_exc))
haystack_dict[name] = dict()
haystack_dict[name]['siteRef'] = 'r:{0}'.format(site_name)
haystack_dict[name]['dis'] = 's:{0}'.format(instances[sig_exc][block]['Description'])
haystack_dict[name]['unit'] = 's:{0}'.format(instances[sig_exc][block]['Unit'])
haystack_dict[name]['kind'] = 's:Number'
for marker in markers:
m = instances[sig_exc][block]['Haystack'][marker]
if m != 'None':
if marker == 'customMarkers':
if len(m)>0:
m = m[1:-1]
m_list = m.split(',')
for i in m_list:
haystack_dict[name][i] = 'm:'
elif m == 'flow_t':
haystack_dict[name]['flow'] = 'm:'
elif m == 'return_t':
haystack_dict[name]['return'] = 'm:'
else:
haystack_dict[name][m] = 'm:'

return haystack_dict


if __name__ == '__main__':
# Define model
Expand Down
1 change: 1 addition & 0 deletions testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ def _get_var_metadata(self, fmu, var_list, inputs=False):
maxi = None
var_metadata[var] = {'Unit':unit,
'Description':description,
'Tags': self.tags_json.get(var),
'Minimum':mini,
'Maximum':maxi}

Expand Down
14 changes: 13 additions & 1 deletion testcases/bestest_air/models/BESTESTAir/BaseClasses/Case900FF.mo
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@ model Case900FF "Case 600FF, but with high thermal mass"
meanT(
Min=24.5 + 273.15,
Max=25.9 + 273.15,
Mean=25.2 + 273.15)));
Mean=25.2 + 273.15)),
reaTRooAir(
quantity=Buildings.Utilities.IO.SignalExchange.HaystackTags.Quantity.temp,

substance=Buildings.Utilities.IO.SignalExchange.HaystackTags.Substance.air,

customMarkers="{zone}"),
reaCO2RooAir(quantity=Buildings.Utilities.IO.SignalExchange.HaystackTags.Quantity.concentration),

reaPLig(quantity=Buildings.Utilities.IO.SignalExchange.HaystackTags.Quantity.power,
equip=Buildings.Utilities.IO.SignalExchange.HaystackTags.Equip.panel),
reaPPlu(quantity=Buildings.Utilities.IO.SignalExchange.HaystackTags.Quantity.power,
equip=Buildings.Utilities.IO.SignalExchange.HaystackTags.Equip.panel));

parameter Buildings.ThermalZones.Detailed.Validation.BESTEST.Data.ExteriorWallCase900
extWalCase900 "Exterior wall"
Expand Down
Loading