Skip to content

Commit

Permalink
Merge branch 'release/v0.2.27'
Browse files Browse the repository at this point in the history
  • Loading branch information
t-sommer committed Jan 28, 2021
2 parents a2c6236 + 167def6 commit 216bd08
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 116 deletions.
16 changes: 16 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
## v0.2.27 (2021-01-28)

### Enhancements

- Scale icons on High DPI screens (#226)
- Add min and max columns and "Show All" action (#225)
- Update link to FMI 2.0.2 spec (#210)
- Handle missing documentation and model.png in web app (#187)
- Validate XML against schema in validate_fmu() (#223)
- Check for illegal start values (#224)
- Add "Validate FMU" action to GUI (#221)
- Set input derivatives for FMI 2.0 Co-Simulation (#214)
- Add "include" parameter to fmpy.extract() (#208)
- Handle missing "derivative" attribute in validate_fmu() (#206)
- Call SetProcessDpiAwareness(True) on Windows to avoid broken PyQtGraph plots (#201)

## v0.2.26 (2020-11-27)

### Enhancements
Expand Down
10 changes: 6 additions & 4 deletions fmpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ctypes import *
import _ctypes

__version__ = '0.2.26'
__version__ = '0.2.27'

# experimental
plot_library = 'matplotlib' # 'plotly'
Expand Down Expand Up @@ -167,13 +167,13 @@ def fmi_info(filename):
return fmi_version, fmi_types


def extract(filename, unzipdir=None):
def extract(filename, unzipdir=None, include=None):
""" Extract a ZIP archive to a temporary directory
Parameters:
filename filename of the ZIP archive
unzipdir target directory (None: create temporary directory)
include a filter function to select the files to extract
Returns:
unzipdir path to the directory that contains the extracted files
"""
Expand All @@ -200,8 +200,10 @@ def extract(filename, unzipdir=None):
if ':' in name or name.startswith('/'):
raise Exception("Illegal path %s found in %s. The path must not contain a drive or device letter, or a leading slash." % (name, filename))

members = filter(include, zf.namelist()) if include else None

# extract the archive
zf.extractall(unzipdir)
zf.extractall(unzipdir, members=members)

return unzipdir

Expand Down
44 changes: 39 additions & 5 deletions fmpy/gui/MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

pg.setConfigOptions(background='w', foreground='k', antialias=True)

COLLAPSABLE_COLUMNS = ['Value Reference', 'Initial', 'Causality', 'Variability', 'Min', 'Max']


class ClickableLabel(QLabel):
""" A QLabel that shows a pointing hand cursor and emits a *clicked* event when clicked """
Expand Down Expand Up @@ -175,9 +177,7 @@ def __init__(self, parent=None):
self.ui.treeView.setColumnWidth(i, w)
self.ui.tableView.setColumnWidth(i, w)

if n in ['Value Reference', 'Initial', 'Causality', 'Variability']:
self.ui.treeView.setColumnHidden(i, True)
self.ui.tableView.setColumnHidden(i, True)
self.hideAllColumns()

# populate the recent files list
settings = QSettings()
Expand Down Expand Up @@ -238,7 +238,10 @@ def __init__(self, parent=None):
self.actionEditTable = self.contextMenu.addAction("Edit Table", self.editTable)
self.contextMenu.addSeparator()
self.columnsMenu = self.contextMenu.addMenu('Columns')
for column in ['Value Reference', 'Initial', 'Causality', 'Variability']:
action = self.columnsMenu.addAction('Show All')
action.triggered.connect(self.showAllColumns)
self.columnsMenu.addSeparator()
for column in COLLAPSABLE_COLUMNS:
action = self.columnsMenu.addAction(column)
action.setCheckable(True)
action.toggled.connect(lambda show, col=column: self.showColumn(col, show))
Expand All @@ -252,6 +255,7 @@ def __init__(self, parent=None):
self.ui.actionSaveChanges.triggered.connect(self.saveChanges)

# tools menu
self.ui.actionValidateFMU.triggered.connect(self.validateFMU)
self.ui.actionCompilePlatformBinary.triggered.connect(self.compilePlatformBinary)
self.ui.actionCreateJupyterNotebook.triggered.connect(self.createJupyterNotebook)
self.ui.actionCreateCMakeProject.triggered.connect(self.createCMakeProject)
Expand All @@ -261,7 +265,7 @@ def __init__(self, parent=None):
# help menu
self.ui.actionOpenFMI1SpecCS.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://svn.modelica.org/fmi/branches/public/specifications/v1.0/FMI_for_CoSimulation_v1.0.1.pdf')))
self.ui.actionOpenFMI1SpecME.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://svn.modelica.org/fmi/branches/public/specifications/v1.0/FMI_for_ModelExchange_v1.0.1.pdf')))
self.ui.actionOpenFMI2Spec.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://svn.modelica.org/fmi/branches/public/specifications/v2.0/FMI_for_ModelExchange_and_CoSimulation_v2.0.pdf')))
self.ui.actionOpenFMI2Spec.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://github.com/modelica/fmi-standard/releases/download/v2.0.2/FMI-Specification-2.0.2.pdf')))
self.ui.actionOpenTestFMUs.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://github.com/modelica/fmi-cross-check/tree/master/fmus')))
self.ui.actionOpenWebsite.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://github.com/CATIA-Systems/FMPy')))
self.ui.actionShowReleaseNotes.triggered.connect(lambda: QDesktopServices.openUrl(QUrl('https://fmpy.readthedocs.io/en/latest/changelog/')))
Expand Down Expand Up @@ -434,6 +438,8 @@ def load(self, filename):
self.stopTimeLineEdit.setText(str(experiment.stopTime))

# actions
self.ui.actionValidateFMU.setEnabled(True)

can_compile = md.fmiVersion == '2.0' and 'c-code' in platforms
self.ui.actionCompilePlatformBinary.setEnabled(can_compile)
self.ui.actionCreateCMakeProject.setEnabled(can_compile)
Expand Down Expand Up @@ -826,6 +832,14 @@ def showColumn(self, name, show):
self.ui.treeView.setColumnHidden(i, not show)
self.ui.tableView.setColumnHidden(i, not show)

def showAllColumns(self):
for name in COLLAPSABLE_COLUMNS:
self.showColumn(name, True)

def hideAllColumns(self):
for name in COLLAPSABLE_COLUMNS:
self.showColumn(name, False)

def setStatusMessage(self, level, text):

if level in ['debug', 'info', 'warning', 'error']:
Expand Down Expand Up @@ -927,6 +941,26 @@ def removeDuplicates(seq):
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]

def validateFMU(self):

from ..validation import validate_fmu

problems = validate_fmu(self.filename)

if problems:
button = QMessageBox.question(self, "Validation failed", "%d problems have been found. Save validation messages?" % len(problems))
if button == QMessageBox.Yes:
filename, _ = os.path.splitext(self.filename)
filename, _ = QFileDialog.getSaveFileName(parent=self,
caption="Save validation messages",
directory=filename + '_validation.txt',
filter="Text Files (*.txt);;All Files (*.*)")
if filename:
with open(filename, 'w') as f:
f.writelines(problems)
else:
QMessageBox.information(self, "Validation successful", "No problems have been found.")

def addFileAssociation(self):
""" Associate *.fmu with the FMPy GUI """

Expand Down
9 changes: 9 additions & 0 deletions fmpy/gui/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

if __name__ == '__main__':

import os
import sys
import ctypes
import platform
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication
from fmpy.gui.MainWindow import MainWindow

if os.name == 'nt' and int(platform.release()) >= 8:
ctypes.windll.shcore.SetProcessDpiAwareness(True)

QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
Expand Down
18 changes: 15 additions & 3 deletions fmpy/gui/forms/MainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1328,7 +1328,7 @@ QToolButton:checked, QToolButton:hover:pressed {
</property>
<widget class="QMenu" name="menuFMI_Specifications">
<property name="title">
<string>FMI Specifications</string>
<string>FMI &amp;Specifications</string>
</property>
<addaction name="actionOpenFMI2Spec"/>
<addaction name="separator"/>
Expand All @@ -1350,6 +1350,7 @@ QToolButton:checked, QToolButton:hover:pressed {
<addaction name="actionCreateDesktopShortcut"/>
<addaction name="actionAddFileAssociation"/>
<addaction name="separator"/>
<addaction name="actionValidateFMU"/>
<addaction name="actionCompilePlatformBinary"/>
<addaction name="actionCreateJupyterNotebook"/>
<addaction name="actionCreateCMakeProject"/>
Expand Down Expand Up @@ -1759,7 +1760,10 @@ QToolButton::menu-button {
</action>
<action name="actionOpenFMI2Spec">
<property name="text">
<string>FMI 2.0</string>
<string>FMI 2.0.2</string>
</property>
<property name="toolTip">
<string>FMI 2.0.2</string>
</property>
</action>
<action name="actionOpenFMI1SpecCS">
Expand Down Expand Up @@ -1808,7 +1812,7 @@ QToolButton::menu-button {
</action>
<action name="actionOpenTestFMUs">
<property name="text">
<string>Test FMUs</string>
<string>&amp;Test FMUs</string>
</property>
<property name="toolTip">
<string>Test FMUs on fmi-standard.org</string>
Expand Down Expand Up @@ -1920,6 +1924,14 @@ QToolButton::menu-button {
<string>Create &amp;Jupyter Notebook...</string>
</property>
</action>
<action name="actionValidateFMU">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Validate FMU</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
Expand Down
16 changes: 13 additions & 3 deletions fmpy/gui/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ def addChild(self, child):

class VariablesModel(QAbstractItemModel):

COLUMN_NAMES = ['Name', 'Value Reference', 'Initial', 'Causality', 'Variability', 'Start', 'Unit', 'Plot', 'Description']
COLUMN_WIDTHS = [200, 100, 70, 70, 70, 70, 40, 40]
COLUMN_NAMES = ['Name', 'Value Reference', 'Initial', 'Causality', 'Variability', 'Start', 'Min', 'Max', 'Unit', 'Plot', 'Description']
COLUMN_WIDTHS = [200, 100, 70, 70, 70, 70, 70, 70, 40, 40]
variableSelected = pyqtSignal(ScalarVariable, name='variableSelected')
variableDeselected = pyqtSignal(ScalarVariable, name='variableDeselected')

Expand Down Expand Up @@ -63,7 +63,7 @@ def columnData(self, v, column, role):
elif role == Qt.CheckStateRole and column == 'Plot':
return Qt.Checked if v in self.selectedVariables else Qt.Unchecked

elif role == Qt.TextAlignmentRole and column in ['Value Reference', 'Start']:
elif role == Qt.TextAlignmentRole and column in ['Value Reference', 'Start', 'Min', 'Max']:
return Qt.AlignRight | Qt.AlignVCenter

elif role == Qt.FontRole and column == 'Start' and v.name in self.startValues:
Expand All @@ -85,6 +85,16 @@ def columnData(self, v, column, role):
return self.startValues[v.name]
elif v.start is not None:
return str(v.start)
elif column == 'Min':
if v.min is not None:
return v.min
elif v.declaredType is not None and v.declaredType.min is not None:
return v.declaredType.min
elif column == 'Max':
if v.max is not None:
return v.max
elif v.declaredType is not None and v.declaredType.max is not None:
return v.declaredType.max
elif column == 'Unit':
if v.unit is not None:
return v.unit
Expand Down
34 changes: 21 additions & 13 deletions fmpy/model_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,19 @@ def __repr__(self):
return '%s' % self.variable


class ValidationError(Exception):
""" Exception raised for failed validation of the modelDescription.xml
Attributes:
problems list of problems found
"""

def __init__(self, problems):
message = "Failed to validate modelDescription.xml:\n\n- " + '\n- '.join(problems)
self.problems = problems
super(ValidationError, self).__init__(message)


def _copy_attributes(element, object, attributes=None):
""" Copy attributes from an XML element to a Python object """

Expand Down Expand Up @@ -433,10 +446,9 @@ def read_model_description(filename, validate=True, validate_variable_names=Fals
schema = etree.XMLSchema(file=os.path.join(module_dir, 'schema', 'fmi3', 'fmi3ModelDescription.xsd'))

if not schema.validate(root):
message = "Failed to validate modelDescription.xml:"
for entry in schema.error_log:
message += "\n%s (line %d, column %d): %s" % (entry.level_name, entry.line, entry.column, entry.message)
raise Exception(message)
problems = ["%s (line %d, column %d): %s" % (e.level_name, e.line, e.column, e.message)
for e in schema.error_log]
raise ValidationError(problems)

modelDescription = ModelDescription()
_copy_attributes(root, modelDescription, ['fmiVersion', 'guid', 'modelName', 'description', 'generationTool',
Expand Down Expand Up @@ -814,15 +826,11 @@ def read_model_description(filename, validate=True, validate_variable_names=Fals
if variable.derivative is not None:
variable.derivative = modelDescription.modelVariables[int(variable.derivative)]

problems = []

if validate:
problems += validation.validate_model_description(modelDescription,
validate_variable_names=validate_variable_names,
validate_model_structure=validate_model_structure)

if problems:
message = ("Failed to validate model description. %d problems were found:\n\n- " % len(problems)) + '\n- '.join(problems)
raise Exception(message)
problems = validation.validate_model_description(modelDescription,
validate_variable_names=validate_variable_names,
validate_model_structure=validate_model_structure)
if problems:
raise ValidationError(problems)

return modelDescription
Loading

0 comments on commit 216bd08

Please sign in to comment.