Skip to content

Commit

Permalink
Merge branch 'release/v0.2.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
t-sommer committed Apr 11, 2018
2 parents a46c772 + 1416356 commit ff1c6ce
Show file tree
Hide file tree
Showing 22 changed files with 1,568 additions and 388 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/CATIA-Systems/FMPy?branch=master&svg=true)](https://ci.appveyor.com/project/TorstenSommer/fmpy)
[![PyPI version](https://badge.fury.io/py/fmpy.svg)](https://badge.fury.io/py/fmpy)
[![Anaconda-Server Badge](https://anaconda.org/conda-forge/fmpy/badges/version.svg)](https://anaconda.org/conda-forge/fmpy)
[![Documentation Status](https://readthedocs.org/projects/fmpy/badge/?version=latest)](http://fmpy.readthedocs.io/en/latest/?badge=latest)

# FMPy

Expand All @@ -10,8 +11,8 @@ 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 ``NEW``
- compiles source code FMUs ``NEW``
- has a graphical user interface
- compiles source code FMUs

## Installation

Expand All @@ -21,7 +22,7 @@ Several options are available:
- Install with from PyPI: `python -m pip install fmpy[complete]`
- Install the latest development version directly from GitHub: `python -m pip install https://github.com/CATIA-Systems/FMPy/archive/develop.zip`

If you don't have Python on your machine you can install [Anaconda Python 3.6](https://www.anaconda.com/download/).
If you don't have Python on your machine you can install [Anaconda Python](https://www.anaconda.com/download/).

## Start the Graphical User Interface

Expand Down
18 changes: 18 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## v0.2.3 (2018-04-11)

- `NEW` Allow simulation of extracted FMUs and pre-loaded model descriptions
- `NEW` Re-load an FMU in the GUI
- `NEW` Load start values from an FMU
- `NEW` Write changed start values back to the FMU
- `NEW` Table editor for 1-d and 2-d array variables
- `NEW` Handle events in input signals
- `NEW` Plot events
- `NEW` Regular time grid for model-exchange results
- `NEW` Apply start values in the model description
- `NEW` Debug and FMI logging on the command line and in the GUI
- `NEW` Log filtering by type and message in the GUI
- `FIXED` Logger callback for FMI 1.0
- `FIXED` Handling of `None` in setString()
- `FIXED` Handling of time events
- `FIXED` Conversion of Boolean start values

## v0.2.2 (2018-03-13)

- `NEW` FMI 2.0 state serialization functions added
Expand Down
65 changes: 35 additions & 30 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.2'
__version__ = '0.2.3'


# determine the platform
Expand Down Expand Up @@ -50,7 +50,7 @@ def supported_platforms(filename):
""" Get the platforms supported by the FMU without extracting it
Parameters:
filename filename of the FMU
filename filename of the FMU or directory with extracted FMU
Returns:
platforms a list of supported platforms supported by the FMU
Expand All @@ -60,42 +60,47 @@ def supported_platforms(filename):

platforms = []

# open the FMU
with zipfile.ZipFile(filename, 'r') as zf:

# get the supported platforms
names = zf.namelist()

# check for the C-sources
# get the files within the FMU
if os.path.isdir(filename):
names = []
for dirpath, _, filenames in os.walk(filename):
for name in filenames:
abspath = os.path.join(dirpath, name)
names.append(os.path.relpath(abspath, start=filename).replace('\\', '/'))
else:
with zipfile.ZipFile(filename, 'r') as zf:
names = zf.namelist()

# check for the C-sources
for name in names:
head, tail = os.path.split(name)
if head == 'sources' and tail.endswith('.c'):
platforms.append('c-code')
break

# check for *.dylib on Mac
for name in names:
head, tail = os.path.split(name)
if head == 'binaries/darwin64' and tail.endswith('.dylib'):
platforms.append('darwin64')
break

# check for *.so on Linux
for platform in ['linux32', 'linux64']:
for name in names:
head, tail = os.path.split(name)
if head == 'sources' and tail.endswith('.c'):
platforms.append('c-code')
if head == 'binaries/' + platform and tail.endswith('.so'):
platforms.append(platform)
break

# check for *.dylib on Mac
# check for *.dll on Windows
for platform in ['win32', 'win64']:
for name in names:
head, tail = os.path.split(name)
if head == 'binaries/darwin64' and tail.endswith('.dylib'):
platforms.append('darwin64')
if head == 'binaries/' + platform and tail.endswith('.dll'):
platforms.append(platform)
break

# check for *.so on Linux
for platform in ['linux32', 'linux64']:
for name in names:
head, tail = os.path.split(name)
if head == 'binaries/' + platform and tail.endswith('.so'):
platforms.append(platform)
break

# check for *.dll on Windows
for platform in ['win32', 'win64']:
for name in names:
head, tail = os.path.split(name)
if head == 'binaries/' + platform and tail.endswith('.dll'):
platforms.append(platform)
break

return platforms


Expand Down
15 changes: 13 additions & 2 deletions fmpy/command_line.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
""" Command line interface for FMPy """

from __future__ import print_function


def main():

Expand Down Expand Up @@ -39,8 +41,10 @@ def main():
parser.add_argument('--stop-time', type=float, help="stop time for the simulation")
parser.add_argument('--show-plot', action='store_true', help="plot the results")
parser.add_argument('--timeout', type=float, help="max. time to wait for the simulation to finish")
parser.add_argument('--fmi-logging', action='store_true', help="enable FMI logging")
parser.add_argument('--start-values', nargs='+', help="name-value pairs of start values")
parser.add_argument('--apply-default-start-values', action='store_true', help="apply the default values from the model description")
parser.add_argument('--debug-logging', action='store_true', help="enable the FMU's debug logging")
parser.add_argument('--fmi-logging', action='store_true', help="enable FMI logging")

args = parser.parse_args()

Expand Down Expand Up @@ -68,6 +72,11 @@ def main():

input = read_csv(args.input_file) if args.input_file else None

if args.fmi_logging:
fmi_call_logger = lambda s: print('[FMI] ' + s)
else:
fmi_call_logger = None

result = simulate_fmu(args.fmu_filename,
validate=True,
start_time=args.start_time,
Expand All @@ -77,10 +86,12 @@ def main():
output_interval=None,
fmi_type=None,
start_values=start_values,
apply_default_start_values=args.apply_default_start_values,
input=input,
output=args.output_variables,
timeout=args.timeout,
fmi_logging=args.fmi_logging)
debug_logging=args.debug_logging,
fmi_call_logger=fmi_call_logger)

if args.output_file:
write_csv(filename=args.output_file, result=result)
Expand Down
10 changes: 7 additions & 3 deletions fmpy/examples/coupled_clutches.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import print_function
from fmpy import simulate_fmu
from fmpy.util import download_test_file
import numpy as np
Expand All @@ -7,6 +8,7 @@ def simulate_coupled_clutches(fmi_version='2.0',
fmi_type='ModelExchange',
output=['outputs[1]', 'outputs[2]', 'outputs[3]', 'outputs[4]'],
solver='CVode',
events=True,
fmi_logging=False,
show_plot=True):

Expand All @@ -20,22 +22,24 @@ def simulate_coupled_clutches(fmi_version='2.0',
print("Simulating CoupledClutches.fmu (FMI %s, %s, %s)..." % (fmi_version, fmi_type, solver))
result = simulate_fmu(
filename='CoupledClutches.fmu',
validate=False,
start_time=0,
stop_time=1.5,
solver=solver,
step_size=1e-2,
output_interval=2e-2,
record_events=events,
start_values={'CoupledClutches1_freqHz': 0.4},
input=input,
output=output,
validate=False,
fmi_logging=fmi_logging)
fmi_call_logger=lambda s: print('[FMI] ' + s) if fmi_logging else None)

if show_plot:
print("Plotting results...")
from fmpy.util import plot_result
plot_result(result=result,
window_title="CoupledClutches.fmu (FMI %s, %s, %s)" % (fmi_version, fmi_type, solver))
window_title="CoupledClutches.fmu (FMI %s, %s, %s)" % (fmi_version, fmi_type, solver),
events=events)

print("Done.")

Expand Down
22 changes: 11 additions & 11 deletions fmpy/fmi1.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,22 @@ def stepFinished(componentEnvironment, status):
class _FMU(object):
""" Base class for all FMUs """

def __init__(self, guid, modelIdentifier, unzipDirectory, instanceName, libraryPath=None, logFMICalls=False):
def __init__(self, guid, modelIdentifier, unzipDirectory, instanceName=None, libraryPath=None, fmiCallLogger=None):
"""
Parameters:
guid the GUI from the modelDescription.xml
modelIdentifier the model identifier from the modelDescription.xml
unzipDirectory folder where the FMU has been extracted
instanceName the name of the FMU instance
libraryPath path to the shared library
logFMICalls whether FMI calls should be logged
fmiCallLogger logger callback that takes a message as input
"""

self.guid = guid
self.modelIdentifier = modelIdentifier
self.unzipDirectory = unzipDirectory
self.instanceName = instanceName if instanceName is not None else self.modelIdentifier
self.logFMICalls = logFMICalls
self.fmiCallLogger = fmiCallLogger

# remember the current working directory
work_dir = os.getcwd()
Expand Down Expand Up @@ -144,7 +144,7 @@ def freeLibrary(self):

def _print_fmi_args(self, fname, argnames, argtypes, args, restype, res):

f = '[FMI] ' + fname + '('
f = fname + '('

l = []

Expand Down Expand Up @@ -202,7 +202,7 @@ def _print_fmi_args(self, fname, argnames, argtypes, args, restype, res):
elif restype == c_void_p:
f += ' -> ' + hex(res)

print(f)
self.fmiCallLogger(f)


class _FMU1(_FMU):
Expand Down Expand Up @@ -264,7 +264,7 @@ def w(*args, **kwargs):

res = f(*args, **kwargs)

if self.logFMICalls:
if self.fmiCallLogger is not None:
self._print_fmi_args('fmi' + name, argnames, argtypes, args, restype, res)

if restype == fmi1Status:
Expand Down Expand Up @@ -367,7 +367,7 @@ def __init__(self, **kwargs):
fmi1Status)

def instantiate(self, mimeType='application/x-fmu-sharedlibrary', timeout=0, visible=fmi1False,
interactive=fmi1False, functions=None, loggingOn=fmi1False):
interactive=fmi1False, functions=None, loggingOn=False):

fmuLocation = pathlib.Path(self.unzipDirectory).as_uri()

Expand All @@ -388,7 +388,7 @@ def instantiate(self, mimeType='application/x-fmu-sharedlibrary', timeout=0, vis
visible,
interactive,
functions,
loggingOn)
fmi1True if loggingOn else fmi1False)

def initialize(self, tStart=0.0, stopTime=None):
stopTimeDefined = fmi1True if stopTime is not None else fmi1False
Expand Down Expand Up @@ -486,11 +486,11 @@ def __init__(self, **kwargs):
[fmi1Component],
None)

def instantiate(self, functions=None, loggingOn=fmi1False):
def instantiate(self, functions=None, loggingOn=False):

if functions is None:
functions = fmi1CallbackFunctions()
functions.logger = fmi1CallbackLoggerTYPE(logger)
functions.logger = fmi1CallbackLoggerTYPE(printLogMessage)
functions.allocateMemory = fmi1CallbackAllocateMemoryTYPE(allocateMemory)
functions.freeMemory = fmi1CallbackFreeMemoryTYPE(freeMemory)
functions.stepFinished = None
Expand All @@ -500,7 +500,7 @@ def instantiate(self, functions=None, loggingOn=fmi1False):
self.component = self.fmi1InstantiateModel(self.instanceName.encode('UTF-8'),
self.guid.encode('UTF-8'),
self.callbacks,
loggingOn)
fmi1True if loggingOn else fmi1False)

def setTime(self, time):
return self.fmi1SetTime(self.component, time)
Expand Down
11 changes: 4 additions & 7 deletions fmpy/fmi2.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def w(*args, **kwargs):

res = f(*args, **kwargs)

if self.logFMICalls:
if self.fmiCallLogger is not None:
self._print_fmi_args(fname, argnames, argtypes, args, restype, res)

if restype == fmi2Status: # status code
Expand All @@ -204,7 +204,6 @@ def instantiate(self, visible=False, callbacks=None, loggingOn=False):

kind = fmi2ModelExchange if isinstance(self, FMU2Model) else fmi2CoSimulation
resourceLocation = pathlib.Path(self.unzipDirectory, 'resources').as_uri()
visible = fmi2True if visible else fmi2False

if callbacks is None:
callbacks = fmi2CallbackFunctions()
Expand All @@ -214,15 +213,13 @@ def instantiate(self, visible=False, callbacks=None, loggingOn=False):

self.callbacks = callbacks

loggingOn = fmi2True if loggingOn else fmi2False

self.component = self.fmi2Instantiate(self.instanceName.encode('utf-8'),
kind,
self.guid.encode('utf-8'),
resourceLocation.encode('utf-8'),
byref(self.callbacks),
visible,
loggingOn)
fmi2True if visible else fmi2False,
fmi2True if loggingOn else fmi2False)

def setupExperiment(self, tolerance=None, startTime=0.0, stopTime=None):

Expand Down Expand Up @@ -291,7 +288,7 @@ def setBoolean(self, vr, value):

def setString(self, vr, value):
vr = (fmi2ValueReference * len(vr))(*vr)
value = map(lambda s: s.encode('utf-8'), value)
value = map(lambda s: s.encode('utf-8') if s is not None else s, value)
value = (fmi2String * len(vr))(*value)
self.fmi2SetString(self.component, vr, len(vr), value)

Expand Down
Loading

0 comments on commit ff1c6ce

Please sign in to comment.