Skip to content

Commit

Permalink
Merge branch 'release/v0.2.25'
Browse files Browse the repository at this point in the history
  • Loading branch information
t-sommer committed Nov 4, 2020
2 parents e4c530d + 6730971 commit 2d569cc
Show file tree
Hide file tree
Showing 18 changed files with 687 additions and 30 deletions.
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ FMPy is a free Python library to simulate [Functional Mock-up Units (FMUs)](http
- supports FMI 1.0 and 2.0
- supports Co-Simulation and Model Exchange
- runs on Windows, Linux and macOS
- has a graphical user interface
- has a [command line](#simulate-an-fmu-on-the-command-line), [graphical user interface](#start-the-graphical-user-interface), and [web app](#start-the-web-app)
- creates [Jupyter Notebooks](#create-a-jupyter-notebook)
- compiles C code FMUs and generates [CMake](https://cmake.org/) projects for debugging

Try [this Jupyter notebook](https://notebooks.azure.com/t-sommer/projects/CoupledClutches) online!

## Installation

Several options are available:
Expand Down Expand Up @@ -93,6 +92,35 @@ Get more information about the available options
fmpy --help
```

## Create a Jupyter Notebook

To create a [Jupyter](https://jupyter.org/) Notebook open an FMU in the FMPy GUI and select `Tools > Create Jupyter Notebook...` or run

```
fmpy create-jupyter-notebook Rectifier.fmu
```

on the command line and open the notebook in Jupyter with

```
jupyter notebook Rectifier.ipynb
```

![Web App](docs/Rectifier_Notebook.png)

## Start the Web App

The FMPy Web App is built with [Dash](https://plotly.com/dash/) and a great way to share your FMUs with anyone that has a web browser.
To start it run

```
python -m fmpy.webapp Rectfier.fmu
```

on the command line or use `--help` for more options.

![Web App](docs/Rectifier_WebApp.png)

## Advanced Usage

To learn more about how to use FMPy in you own scripts take a look at the
Expand Down
28 changes: 14 additions & 14 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ jobs:
python27:
python.version: '2.7'
python.libraries: 'pathlib'
python36:
python.version: '3.6'
python.libraries: ''
python37:
python.version: '3.7'
python.libraries: 'ipython=7 notebook'
pool:
vmImage: 'ubuntu-16.04'
steps:
Expand All @@ -21,7 +21,7 @@ jobs:

- bash: |
source activate myEnvironment
conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES
conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION cmake dask lark-parser lxml matplotlib numpy plotly pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES
displayName: Install Anaconda packages
- bash: |
Expand Down Expand Up @@ -64,9 +64,9 @@ jobs:
python27:
python.version: '2.7'
python.libraries: 'pathlib'
python36:
python.version: '3.6'
python.libraries: ''
python38:
python.version: '3.7'
python.libraries: 'ipython=7 notebook'
pool:
vmImage: 'macos-10.15'

Expand All @@ -85,7 +85,7 @@ jobs:

- bash: |
source activate myEnvironment
conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES
conda install --yes --quiet --name myEnvironment -c conda-forge python=$PYTHON_VERSION dask lark-parser lxml matplotlib numpy plotly pyqt pyqtgraph pytest-cov requests scipy $PYTHON_LIBRARIES
displayName: Install Anaconda packages
- bash: |
Expand Down Expand Up @@ -128,8 +128,8 @@ jobs:
# lxml broken for Python 2.7
# python27:
# python.version: '2.7'
python36:
python.version: '3.6'
python37:
python.version: '3.7'
pool:
vmImage: 'vs2017-win2016'

Expand All @@ -143,7 +143,7 @@ jobs:

- script: |
call activate myEnvironment
conda install --yes --quiet --name myEnvironment -c conda-forge python=%PYTHON_VERSION% cmake dask lark-parser lxml matplotlib numpy pyqt pyqtgraph pytest-cov pywin32 requests scipy
conda install --yes --quiet --name myEnvironment -c conda-forge python=%PYTHON_VERSION% cmake dask ipython=7 lark-parser lxml matplotlib notebook numpy plotly pyqt pyqtgraph pytest-cov pywin32 requests scipy
displayName: Install Anaconda packages
- script: |
Expand Down Expand Up @@ -203,17 +203,17 @@ jobs:

- task: DownloadPipelineArtifact@2
inputs:
artifact: linux-python-3.6
artifact: linux-python-3.7
downloadPath: linux

- task: DownloadPipelineArtifact@2
inputs:
artifact: macosx-python-3.6
artifact: macosx-python-3.7
downloadPath: macosx

- task: DownloadPipelineArtifact@2
inputs:
artifact: windows-python-3.6
artifact: windows-python-3.7
downloadPath: windows

- bash: |
Expand Down
Binary file added docs/Rectifier_Notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Rectifier_WebApp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## v0.2.25 (2020-11-04)

### Enhancements

- Add Dash based web app
- Add Jupyter Notebook generation
- Don't import NumPy in fmi1.py to allow reuse in projects with minimal dependencies (#184)
- Convert array indices in write_csv() to tuple to avoid FutureWarning

## v0.2.24 (2020-10-14)

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

__version__ = '0.2.24'
__version__ = '0.2.25'

# experimental
plot_library = 'matplotlib' # 'plotly'


# determine the platform
Expand Down
10 changes: 10 additions & 0 deletions fmpy/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def main():
Compile a source code FMU:
fmpy compile Rectifier.fmu
Create a Jupyter Notebook
fmpy create-jupyter-notebook Rectifier.fmu
"""

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
Expand Down Expand Up @@ -103,6 +107,12 @@ def main():

create_cmake_project(args.fmu_filename, project_dir)

elif args.command == 'create-jupyter-notebook':

from fmpy.util import create_jupyter_notebook

create_jupyter_notebook(args.fmu_filename)

elif args.command == 'simulate':

from fmpy import simulate_fmu
Expand Down
3 changes: 1 addition & 2 deletions fmpy/fmi1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import os
import pathlib
import numpy as np
from ctypes import *
from . import free, freeLibrary, platform, sharedLibraryExtension, calloc

Expand Down Expand Up @@ -183,7 +182,7 @@ def _log_fmi_args(self, fname, argnames, argtypes, args, restype, res):
else:
if len(args) > i + 1:
# double pointers are always flowed by the size of the array
arr = np.ctypeslib.as_array(v, (args[i + 1],))
arr = v[:args[i + 1]]
a += '[' + ', '.join(map(str, arr)) + ']'
else:
# except for fmi3DoStep
Expand Down
34 changes: 30 additions & 4 deletions fmpy/gui/MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,20 @@ def __init__(self, parent=None):
self.ui.actionReload.triggered.connect(lambda: self.load(self.filename))
self.ui.actionSaveChanges.triggered.connect(self.saveChanges)

# tools menu
self.ui.actionCompilePlatformBinary.triggered.connect(self.compilePlatformBinary)
self.ui.actionCreateJupyterNotebook.triggered.connect(self.createJupyterNotebook)
self.ui.actionCreateCMakeProject.triggered.connect(self.createCMakeProject)
self.ui.actionAddRemoting.triggered.connect(self.addRemoting)
self.ui.actionAddCoSimulationWrapper.triggered.connect(self.addCoSimulationWrapper)

# 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.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/')))
self.ui.actionCompilePlatformBinary.triggered.connect(self.compilePlatformBinary)
self.ui.actionCreateCMakeProject.triggered.connect(self.createCMakeProject)
self.ui.actionAddRemoting.triggered.connect(self.addRemoting)
self.ui.actionAddCoSimulationWrapper.triggered.connect(self.addCoSimulationWrapper)

# filter menu
self.filterMenu = QMenu()
Expand Down Expand Up @@ -435,6 +438,8 @@ def load(self, filename):
self.ui.actionCompilePlatformBinary.setEnabled(can_compile)
self.ui.actionCreateCMakeProject.setEnabled(can_compile)

self.ui.actionCreateJupyterNotebook.setEnabled(True)

can_add_remoting = md.fmiVersion == '2.0' and 'win32' in platforms and 'win64' not in platforms
self.ui.actionAddRemoting.setEnabled(can_add_remoting)

Expand Down Expand Up @@ -1162,6 +1167,27 @@ def compilePlatformBinary(self):

self.load(self.filename)


def createJupyterNotebook(self):
""" Create a Juypyter Notebook to simulate the FMU """

from fmpy.util import create_jupyter_notebook

filename, ext = os.path.splitext(self.filename)

filename = QFileDialog.getSaveFileName(
parent=self,
directory=filename + '.ipynb',
filter='Jupyter Notebooks (*.ipynb);;All Files (*)'
)

if filename:
try:
create_jupyter_notebook(self.filename, filename[0])
except Exception as e:
QMessageBox.critical(self, "Failed to create Jupyter Notebook", str(e))


def createCMakeProject(self):
""" Create a CMake project from a C code FMU """

Expand Down
9 changes: 9 additions & 0 deletions fmpy/gui/forms/MainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ QToolButton:checked, QToolButton:hover:pressed {
<addaction name="actionAddFileAssociation"/>
<addaction name="separator"/>
<addaction name="actionCompilePlatformBinary"/>
<addaction name="actionCreateJupyterNotebook"/>
<addaction name="actionCreateCMakeProject"/>
<addaction name="actionAddRemoting"/>
<addaction name="actionAddCoSimulationWrapper"/>
Expand Down Expand Up @@ -1911,6 +1912,14 @@ QToolButton::menu-button {
<string>Add Co-Simulation &amp;Wrapper</string>
</property>
</action>
<action name="actionCreateJupyterNotebook">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Create &amp;Jupyter Notebook...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
Expand Down
62 changes: 60 additions & 2 deletions fmpy/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,51 @@
eps = 1e-13


class SimulationResult(np.ndarray):

def __new__(subtype, shape, dtype=float, buffer=None, offset=0, strides=None, order=None, modelDescription=None):
obj = super(SimulationResult, subtype).__new__(subtype, shape, dtype, buffer, offset, strides, order)
obj.modelDescription = modelDescription
return obj

def __array_finalize__(self, obj):
if obj is None:
return
self.modelDescription = getattr(obj, 'modelDescription', None)


def _get_output_variables(model_description, max_variables=5):
""" Create a list of default output variables """

output_variables = []

# output variables
for variable in model_description.modelVariables:
if variable.causality == 'output':
output_variables.append(variable)

if len(output_variables) > 0:
return output_variables

# continuous states
if model_description.derivatives is not None:
output_variables = [derivative.variable.derivative for derivative in model_description.derivatives]

if len(output_variables) > 0:
return output_variables[:max_variables]

# local variables
for variable in model_description.modelVariables:
if variable.variability == 'local':
output_variables.append(variable)

if len(output_variables) > 0:
return output_variables[:max_variables]

# any variable
return model_description.modelVariables[:max_variables]


class Recorder(object):
""" Helper class to record the variables during the simulation """

Expand All @@ -38,14 +83,17 @@ def __init__(self, fmu, modelDescription, variableNames=None, interval=None):
self.constants = {}
self.modelDescription = modelDescription

if variableNames is None:
variableNames = [variable.name for variable in _get_output_variables(modelDescription)]

# collect the variables to record
for sv in modelDescription.modelVariables:

if sv.name == 'time':
continue # "time" is reserved for the simulation time

# collect the variables to record
if (variableNames is not None and sv.name in variableNames) or (variableNames is None and sv.causality == 'output'):
if sv.name in variableNames:
type = sv.type
if type == 'Enumeration':
type = 'Integer' if modelDescription.fmiVersion in {'1.0', '2.0'} else 'Int32'
Expand Down Expand Up @@ -108,7 +156,13 @@ def sample(self, time, force=False):
def result(self):
""" Return a structured NumPy array with the recorded results """

return np.array(self.rows, dtype=np.dtype(self.cols))
arr = np.array(self.rows, dtype=np.dtype(self.cols))

info_arr = arr.view(SimulationResult)

info_arr.modelDescription = self.modelDescription

return info_arr

@property
def lastSampleTime(self):
Expand Down Expand Up @@ -549,12 +603,16 @@ def simulate_fmu(filename,
else:
start_time = 0.0

start_time = float(start_time)

if stop_time is None:
if experiment is not None and experiment.stopTime is not None:
stop_time = experiment.stopTime
else:
stop_time = start_time + 1.0

stop_time = float(stop_time)

if relative_tolerance is None and experiment is not None:
relative_tolerance = experiment.tolerance

Expand Down
Loading

0 comments on commit 2d569cc

Please sign in to comment.