#!/usr/bin/env python
################################################################################
# Copyright 2015 Brecht Baeten
# This file is part of mpcpy.
#
# mpcpy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# mpcpy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with mpcpy. If not, see <http://www.gnu.org/licenses/>.
################################################################################
import numpy as np
[docs]class Emulator(object):
"""
Base class for defining an emulator object
"""
def __init__(self, input_keys, parameters=None, initial_conditions=None):
"""
Initializes the emulator object
:code:`self.inputs` and :code:`self.res` attributes must be defined
Parameters
----------
input_keys : list of strings
list of strings of inputs. This is done so that not all data
from the boundary conditions and control signals have to be
transferred to the simulation. Only the boundary conditions and
control signals in the :code:`inputs` attribute are interpolated and
passed to the :code:`__call__` method.
parameters : dict
A dictionary of parameters used by the emulator.
initial_conditions : dict
A dictionary of initial conditions of the system under
consideration.
"""
self.inputs = input_keys
self.initial_conditions = {}
if not initial_conditions is None:
# set only the last value for each key
for key in initial_conditions:
try:
self.initial_conditions[key] = initial_conditions[key][-1]
except:
self.initial_conditions[key] = initial_conditions[key]
self.parameters = {}
if not parameters is None:
self.parameters = parameters
self.res = {}
[docs] def initialize(self):
"""
Redefine in a child class
This method is called once before the start of the MPC and by default
clears the results dictionary and then add the initial conditions to it
at time 0.
"""
self.res = {
'time': np.array([0.])
}
for key in self.initial_conditions:
self.res[key] = np.array([self.initial_conditions[key]])
[docs] def simulate(self, starttime, stoptime, input):
"""
Redefine in a child class
This method runs the simulation and should return a dictionary with
results for times between the :code:`starttime` and :code:`stoptime`
given the inputs.
Parameters
----------
starttime : number
time to start the simulation
stoptime : number
time to stop the simulation
input : dict
dictionary with values for the inputs for the simulation, 'time'
and all values in self.inputs must be keys
Returns
-------
dict
dictionary with the simulation results
"""
return {}
[docs] def __call__(self, time, input):
"""
Simulates the system and updated the results dictionary, calls the
:code:`simulate`method.
Parameters
----------
time : numpy array
Times at which the results are requested.
input : dict
Dictionary with values for the inputs of the model, time must be a
part of it.
Returns
-------
dict
Dictionary with the complete simulation results.
Examples
--------
>>> em = Emulator(['u1'])
>>> t = np.arange(0., 3600.1, 600.)
>>> u1 = 5.*np.ones_like(t)
>>> em(t, ['time': t, 'u1': u1])
"""
res = self.simulate(time[0], time[-1], input)
# adding the inputs to the result
for key in input.keys():
# make sure not to do double adding
if not key in res.keys():
if key in self.res:
# append the result
if len(input[key]) == 1:
self.res[key] = input[key]
else:
self.res[key] = np.append(self.res[key][:-1], np.interp(time, input['time'], input[key]))
else:
self.res[key] = np.interp(time, input['time'], input[key])
# interpolate results to the input points in time
for key in res.keys():
if key in self.res:
# append the result
if len(res[key]) == 1:
self.res[key] = res[key]
else:
if key == 'time':
self.res[key] = np.append(self.res[key][:-1], time)
else:
self.res[key] = np.append(self.res[key][:-1], np.interp(time, res['time'], res[key]))
else:
if len(res[key]) == 1:
self.res[key] = res[key]
else:
self.res[key] = np.interp(time, res['time'], res[key])
return self.res
def set_initial_conditions(self, ini):
print('Warning: Depreciated,'
'set the initial conditions during the object creation with the "initial_conditions" keyword parameter.')
# set only the last value for each key
for key in ini:
try:
self.initial_conditions[key] = ini[key][-1]
except:
self.initial_conditions[key] = ini[key]
def set_parameters(self, par):
print('Warning: Depreciated,'
'set the initial conditions during the object creation with the "parameters" keyword parameter.')
self.parameters = par
class DympyEmulator(Emulator):
"""
A class defining an emulator object using dympy for the simulation
"""
def __init__(self, dymola, inputs, initializationtime=1, **kwargs):
"""
Initialize a dympy object for use as an MPC emulation
Parameters
----------
dymola : dympy.Dymola
a dympy object with an opened and compiled dymola model
inputs : list of strings
a list of strings of the variable names of the inputs
initializationtime : number
time to run the initialization simulation
**kwargs :
arguments which can be passed on to dympy
"""
self.inputs = inputs
self.initializationtime = initializationtime
self.dymola = dymola
self.initial_conditions = {}
self.parameters = {}
self.res = {}
# check for additional dymola arguments
self.simulation_args = {}
for key in kwargs:
if key in ['OutputInterval', 'NumberOfIntervals', 'Tolerance', 'FixedStepSize', 'Algorithm']:
self.simulation_args[key] = kwargs[key]
def initialize(self):
"""
initializes the dympy model, by simulating it for
:code:`self.initializationtime`. Afterwards the :code:`res` attribute is
populated
Also, the keys in parameters and initialconditions are checked. If they
do not appear in the dympy results, they are removed and the model is
reinitialized.
"""
self.dymola.set_parameters(self.initial_conditions)
self.dymola.set_parameters(self.parameters)
# clear the result dict
self.res = {}
# simulate the model for a very short time to get the initial states in the res dict
self.dymola.simulate(StartTime=0, StopTime=self.initializationtime)
res = self.dymola.get_result()
# remove the initial conditions and parameters which are not in the results file
redosim = False
keystoremove = []
for key in self.initial_conditions:
if not key in res:
keystoremove.append(key)
redosim = True
for key in keystoremove:
del self.initial_conditions[key]
keystoremove = []
for key in self.parameters:
if not key in res:
keystoremove.append(key)
redosim = True
for key in keystoremove:
del self.parameters[key]
# redo the simulation if required
if redosim:
self.dymola.set_parameters(self.initial_conditions)
self.dymola.set_parameters(self.parameters)
self.dymola.simulate(StartTime=0, StopTime=self.initializationtime)
res = self.dymola.get_result()
for key in res:
self.res[key] = np.array([res[key][0]])
def simulate(self, starttime, stoptime, input):
"""
"""
self.dymola.write_dsu(input)
try:
self.dymola.dsfinal2dsin()
except:
pass
try:
self.dymola.simulate(StartTime=starttime, StopTime=stoptime, **self.simulation_args)
except:
print('Ignoring error during simulation at time {}'.format(starttime));
try:
res = self.dymola.get_result()
except:
print('Ignoring error while loading dymola res file at time {}'.format(input['time'][0]))
return res
def interp_averaged(t, tp, yp):
y = np.zeros_like(t)
for i in range(len(t)-1):
y[i] = np.mean(yp[np.where((tp >= t[i]) & (tp < t[i+1]))])
y[-1] = np.interp(t[-1], tp, yp)
return y