# [ANDES] (C)2015-2022 Hantao Cui
#
# This program 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.
#
# File name: block.py
# Last modified: 8/16/20, 7:28 PM
from collections import OrderedDict
from typing import Iterable, List, Optional, Tuple, Union
from andes.core.common import JacTriplet, ModelFlags, dummify
from andes.core.discrete import (AntiWindup, AntiWindupRate, DeadBand,
HardLimiter, LessThan, RateLimiter,)
from andes.core.service import EventFlag
from andes.core.observable import Observable
from andes.core.var import Algeb, State
[docs]class Block:
r"""
Base class for control blocks.
Blocks are meant to be instantiated as Model attributes to provide
pre-defined equation sets. Subclasses must overload the `__init__` method to
take custom inputs. Subclasses of Block must overload the `define` method to
provide initialization and equation strings. Exported variables, services
and blocks must be constructed into a dictionary ``self.vars`` at the end of
the constructor.
Blocks can be nested. A block can have blocks but itself as attributes and
therefore reuse equations. When a block has sub-blocks, the outer block must
be constructed with a``name``.
Nested block works in the following way: the parent block modifies the
sub-block's ``name`` attribute by prepending the parent block's name at the
construction phase. The parent block then exports the sub-block as a whole.
When the parent Model class picks up the block, it will recursively import
the variables in the block and the sub-blocks correctly. See the example
section for details.
Parameters
----------
name : str, optional
Block name
tex_name : str, optional
Block LaTeX name
info : str, optional
Block description.
namespace : str, local or parent
Namespace of the exported elements. If 'local', the block name will be
prepended by the parent. If 'parent', the original element name will be
used when exporting.
Warnings
--------
It is a good practice to avoid more than one level of nesting, to avoid
multi-underscore variable names.
Examples
--------
Example for two-level nested blocks. Suppose we have the following hierarchy
.. code-block
SomeModel instance M
|
LeadLag A exports (x, y)
|
Lag B exports (x, y)
SomeModel instance M contains an instance of LeadLag block named A, which
contains a Lag instance named B. Both A and B exports two variables ``x``
and ``y``.
In the code for SomeModel, the following code is used to instantiate LeadLag
.. code-block:: python
class SomeModel:
def __init__(...)
... self.A = LeadLag(name='A',
u=self.foo1, T1=self.foo2, T2=self.foo3)
To use Lag in the LeadLag code, the following lines are found in the
constructor of LeadLag
.. code-block:: python
class LeadLag:
def __init__(name, ...)
... self.B = Lag(u=self.y, K=self.K, T=self.T)
# register `self.B` with the name `A` self.vars = {..., 'B':
self.B}
When instantiating any block instance, its ``__setattr__`` function assigns
names to exported variables and blocks. For the LeadLag instance with the
name ``A``, its member attribute ``B`` is assigned the name ``A_B`` by
convention. That is, ``A_B`` will be set to `B.name`.
When A is picked up by ``SomeModel.__setattr__``, B is captured from A's
exports with the name ``A_B``. Recursively, B's variables are exported,
Recall that `B.name` is now ``A_B``, following the naming rule (parent
block's name + variable name), B's internal variables become ``A_B_x`` and
``A_B_y``.
Again, the LeadLag instance name (``A.name`` in this example) must be given
when instantiating in `SomeModel`'s constructor to ensure correct name
propagation. If there is more than one level of nesting, other than the
terminal-level block, all names of the parent blocks must be provided at
instantiation.
In such a way, B's ``define()`` needs no modification since the naming rule is
the same. For example, B's internal y is always ``{self.name}_y``, although
the nested B has gotten a new name ``A_B``.
"""
[docs] def __init__(self,
name: Optional[str] = None,
tex_name: Optional[str] = None,
info: Optional[str] = None,
namespace: str = 'local'):
self.name = name
self.tex_name = tex_name if tex_name else name
self.info = info
if namespace not in ('local', 'parent'):
raise ValueError(f'Argument namespace must be `local` or `parent`, got {namespace}')
self.namespace = namespace
self.owner = None
self.vars = OrderedDict()
self.triplets = JacTriplet()
self.flags = ModelFlags() # f_num, g_num, j_num and j_setup can be set
def __setattr__(self, key, value):
"""
Set attribute of the block.
This function handles sub-blocks by prepending self.name to child block's name.
"""
if isinstance(value, Block):
if self.name is None:
raise ValueError("Must specify `name` for %s instance "
"because it contains a sub-block named <%s>." % (self.class_name, key))
if not value.owner:
value.__dict__['owner'] = self
if value.namespace == 'local':
prepend = self.name + '_'
tex_prepend = self.tex_name + r'\ '
else:
prepend = ''
tex_prepend = ''
if not value.name:
value.__dict__['name'] = prepend + key
else:
value.__dict__['name'] = prepend + value.name
if not value.tex_name:
value.__dict__['tex_name'] = tex_prepend + key
else:
value.__dict__['tex_name'] = f'{tex_prepend}{{{value.tex_name}}}'
self.__dict__[key] = value
def j_reset(self):
"""
Helper function to clear the lists holding the numerical Jacobians.
This function should be only called once at the beginning of
``j_setup`` in blocks.
"""
self.triplets.clear_ijv()
[docs] def define(self):
"""
Function for setting the initialization and equation strings for
internal variables. This method must be implemented by subclasses.
The equations should be written with the "final" variable names. Let's
say the block instance is named `blk` (kept at ``self.name`` of the
block), and an internal variable `v` is defined. The internal variable
will be captured as ``blk_v`` by the parent model. Therefore, all
equations should use ``{self.name}_v`` to represent variable ``v``,
where ``{self.name}`` is the name of the block at run time.
On the other hand, the names of externally provided parameters or
variables are obtained by directly accessing the ``name`` attribute. For
example, if ``self.T`` is a parameter provided through the block
constructor, ``{self.T.name}`` should be used in the equation.
Examples
--------
An internal variable ``v`` has a trivial equation ``T = v``, where T is
a parameter provided to the block constructor.
In the model, one has
.. code-block :: python
class SomeModel():
def __init__(...)
self.input = Algeb()
self.T = Param()
self.blk = ExampleBlock(u=self.input, T=self.T)
In the ExampleBlock function, the internal variable is defined in the
constructor as
.. code-block :: python
class ExampleBlock():
def __init__(...):
self.v = Algeb()
self.vars = {'v', self.v}
In the ``define``, the equation is provided as
.. code-block :: python
def define(self):
self.v.v_str = '{self.T.name}'
self.v.e_str = '{self.T.name} - {self.name}_v'
In the parent model, ``v`` from the block will be captured as ``blk_v``,
and the equation will evaluate into
.. code-block :: python
self.blk_v.v_str = 'T'
self.blk_v.e_str = 'T - blk_v'
See Also
--------
PIController.define : Equations for the PI Controller block
"""
raise NotImplementedError(f'define() method not implemented in {self.class_name}')
def export(self):
"""
Method for exporting instances defined in this class in a dictionary.
This method calls the ``define`` method first and returns ``self.vars``.
Returns
-------
dict
Keys are the (last section of the) variable name, and the values are the attribute instance.
"""
self.define()
return self.vars
def g_numeric(self, **kwargs):
"""
Function call to update algebraic equation values.
This function should modify the ``e`` value of block ``Algeb`` and ``ExtAlgeb`` in place.
"""
pass
def f_numeric(self, **kwargs):
"""
Function call to update differential equation values.
This function should modify the ``e`` value of block ``State`` and ``ExtState`` in place.
"""
pass
def j_setup(self):
"""
One-time Jacobian sparsity pattern and constant value setup.
Called once by ``store_sparse_pattern`` during system setup.
Use ``triplets.append_ijv`` with the ``'c'`` suffix (e.g., ``'gyc'``)
for constant Jacobians, or without the suffix for variable entries
whose values will be updated per iteration by ``j_numeric``.
Requires ``flags.j_setup = True``.
"""
pass
def j_numeric(self, **kwargs):
"""
Per-iteration numerical Jacobian update.
Called every Newton iteration by ``j_update``, parallel to
``g_numeric`` / ``f_numeric``. Update variable Jacobian triplet
values **in-place**.
Requires ``flags.j_num = True``.
"""
pass
@property
def class_name(self):
"""Return the class name."""
return self.__class__.__name__
@staticmethod
def enforce_tex_name(fields):
"""
Enforce tex_name is not None
"""
if not isinstance(fields, Iterable):
fields = [fields]
for field in fields:
if field.tex_name is None:
raise NameError(f'tex_name for <{field.name}> cannot be None')
def __repr__(self):
return f'{self.__class__.__name__}: {self.owner.__class__.__name__}.{self.name}'
[docs]class PIController(Block):
"""
Proportional Integral Controller.
The controller takes an error signal as the input.
It takes an optional `ref` signal, which will be subtracted from the input.
Parameters
----------
u : BaseVar
The input variable instance
kp : BaseParam
The proportional gain parameter instance
ki : [type]
The integral gain parameter instance
"""
[docs] def __init__(self, u, kp, ki, ref=0.0, x0=0.0, name=None, tex_name=None, info=None):
Block.__init__(self, name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.kp = dummify(kp)
self.ki = dummify(ki)
self.ref = dummify(ref)
self.x0 = dummify(x0)
self.xi = State(info="Integrator output", tex_name='xi')
self.y = Algeb(info="PI output", tex_name='y')
self.vars = OrderedDict([('xi', self.xi), ('y', self.y)])
[docs] def define(self):
r"""
Define equations for the PI Controller.
Notes
-----
One state variable ``xi`` and one algebraic variable ``y`` are added.
Equations implemented are
.. math ::
\dot{x_i} &= k_i * (u - ref) \\
y &= x_i + k_p * (u - ref)
"""
self.xi.v_str = f'{self.x0.name}'
self.xi.e_str = f'{self.ki.name} * ({self.u.name} - {self.ref.name})'
self.y.v_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + {self.x0.name}'
self.y.e_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + ' \
f'{self.name}_xi - {self.name}_y'
[docs]class PIDController(PIController):
r"""
Proportional Integral Derivative Controller.
::
┌────────────────────┐
│ ki skd │
u -> │kp + ─── + ─────── │ -> y
│ s 1 + sTd │
└────────────────────┘
The controller takes an error signal as the input.
It takes an optional ``ref`` signal, which will be subtracted from the input.
The name is suggessted to be specified the same as the instance name.
This block assembles a ``PIController`` and a ``Washout``.
Parameters
----------
u : BaseVar
The input variable instance
kp : BaseParam
The proportional gain parameter instance
ki : BaseParam
The integral gain parameter instance
kd : BaseParam
The derivative gain parameter instance
Td : BaseParam
The derivative time constant parameter instance
x0 : BaseParam
The initial value of the integrator
"""
[docs] def __init__(self, u, kp, ki, kd, Td, name, ref=0.0, x0=0.0, tex_name=None, info=None):
PIController.__init__(self, u=u, kp=kp, ki=ki, ref=ref, x0=x0,
name=name, tex_name=tex_name, info=info,
)
self.kd = dummify(kd)
self.Td = dummify(Td)
self.uin = Algeb(info="PID input", tex_name='uin')
self.PIC = PIController(info="PIC", tex_name="PIC",
u=self.uin, kp=self.kp, ki=self.ki, x0=x0)
self.WO = Washout(info='Washout', tex_name='WO',
u=self.uin, K=self.kd, T=self.kd)
self.y = Algeb(info="PID output", tex_name='y')
self.vars = OrderedDict([('uin', self.uin), ('PIC', self.PIC),
('WO', self.WO), ('y', self.y)])
[docs] def define(self):
r"""
Define equations for the PID Controller.
Notes
-----
One PIController ``PIC``, one Washout ``xd``, and one algebraic variable ``y`` are added.
Equations implemented are
.. math ::
\dot{x_i} &= k_i * (u - ref) \\
xd &= Washout(u - ref) \\
y &= x_i + k_p * (u - ref) + xd
"""
self.uin.v_str = f'({self.u.name} - {self.ref.name})'
self.uin.e_str = f'({self.u.name} - {self.ref.name}) - {self.name}_uin'
self.y.v_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + {self.x0.name}'
self.y.e_str = f'{self.name}_PIC_y + {self.name}_WO_y - {self.name}_y'
[docs]class PIAWHardLimit(PIController):
"""
PI controller with anti-windup limiter on the integrator and
hard limit on the output.
Limits ``lower`` and ``upper`` are on the final output,
and ``aw_lower`` ``aw_upper`` are on the integrator.
"""
[docs] def __init__(self, u, kp, ki, aw_lower, aw_upper, lower, upper, no_lower=False, no_upper=False,
ref=0.0, x0=0.0, name=None, tex_name=None, info=None):
PIController.__init__(self, u=u, kp=kp, ki=ki, ref=ref, x0=x0,
name=name, tex_name=tex_name, info=info,
)
self.lower = dummify(lower)
self.upper = dummify(upper)
self.aw_lower = dummify(aw_lower)
self.aw_upper = dummify(aw_upper)
self.no_lower = no_lower
self.no_upper = no_upper
self.aw = AntiWindup(u=self.xi, lower=self.aw_lower, upper=self.aw_upper,
no_lower=no_lower, no_upper=no_upper, tex_name='aw'
)
self.yul = Algeb("PI unlimited output", tex_name='y^{ul}',
discrete=self.aw,
)
self.hl = HardLimiter(u=self.yul, lower=self.lower, upper=self.upper,
no_lower=no_lower, no_upper=no_upper, tex_name='hl',
)
# the sequence affect the initialization order
self.vars = OrderedDict([('xi', self.xi), ('aw', self.aw),
('yul', self.yul), ('hl', self.hl),
('y', self.y)])
self.y.discrete = self.hl
[docs] def define(self):
PIController.define(self)
self.yul.v_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + {self.x0.name}'
self.yul.e_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + ' \
f'{self.name}_xi - {self.name}_yul'
# overwrite existing `y` equations
self.y.v_str = f'{self.name}_yul * {self.name}_hl_zi + ' \
f'{self.lower.name} * {self.name}_hl_zl + ' \
f'{self.upper.name} * {self.name}_hl_zu'
self.y.e_str = self.y.v_str + f' - {self.name}_y'
[docs]class PIDAWHardLimit(PIAWHardLimit):
r"""
PID controller with anti-windup limiter on the integrator and
hard limit on the output.
::
upper
/¯¯¯¯¯¯
┌────────────────────┐
│ ki skd │
u -> │kp + ─── + ─────── │ -> y
│ s 1 + sTd │
└────────────────────┘
______/
lower
The controller takes an error signal as the input.
Limits ``lower`` and ``upper`` are on the final output,
and ``aw_lower`` ``aw_upper`` are on the integrator.
The name is suggessted to be specified the same as the instance name.
Parameters
----------
u : BaseVar
The input variable instance
kp : BaseParam
The proportional gain parameter instance
ki : BaseParam
The integral gain parameter instance
kd : BaseParam
The derivative gain parameter instance
Td : BaseParam
The derivative time constant parameter instance
"""
[docs] def __init__(self, u, kp, ki, kd, Td, aw_lower, aw_upper, lower, upper, name, no_lower=False, no_upper=False,
ref=0.0, x0=0.0, tex_name=None, info=None):
PIAWHardLimit.__init__(self, u=u, kp=kp, ki=ki, aw_lower=aw_lower,
aw_upper=aw_upper, lower=lower, upper=upper,
no_lower=no_lower, no_upper=no_upper,
ref=ref, x0=x0, name=name, tex_name=tex_name,
info=info)
self.kd = dummify(kd)
self.Td = dummify(Td)
self.uin = Algeb(info="PID input", tex_name='uin')
self.WO = Washout(info='Washout', tex_name='WO',
u=self.uin, K=self.kd, T=self.kd)
# the sequence affect the initialization order
self.vars = OrderedDict([('xi', self.xi), ('uin', self.uin),
('WO', self.WO),
('aw', self.aw), ('yul', self.yul),
('hl', self.hl), ('y', self.y)])
[docs] def define(self):
PIAWHardLimit.define(self)
self.uin.v_str = f'({self.u.name} - {self.ref.name})'
self.uin.e_str = f'({self.u.name} - {self.ref.name}) - {self.name}_uin'
# overwrite existing `yul` equations
self.yul.e_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + ' \
f'{self.name}_xi + {self.name}_WO_y - {self.name}_yul'
[docs]class PITrackAW(Block):
"""
PI with tracking anti-windup limiter
"""
[docs] def __init__(self, u, kp, ki, ks, lower, upper, no_lower=False, no_upper=False,
ref=0.0, x0=0.0, name=None, tex_name=None, info=None):
Block.__init__(self, name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.kp = dummify(kp)
self.ki = dummify(ki)
self.ks = dummify(ks)
self.lower = dummify(lower)
self.upper = dummify(upper)
self.ref = dummify(ref)
self.x0 = dummify(x0)
self.no_lower = no_lower
self.no_upper = no_upper
self.xi = State(info="Integrator output", tex_name='xi')
self.ys = Algeb(info="PI summation before limit", tex_name='ys')
self.lim = HardLimiter(u=self.ys, lower=self.lower, upper=self.upper,
no_lower=no_lower, no_upper=no_upper, tex_name='lim')
self.y = Algeb(info="PI output", discrete=self.lim, tex_name='y')
self.vars = OrderedDict([('xi', self.xi),
('ys', self.ys),
('lim', self.lim),
('y', self.y)])
[docs] def define(self):
self.xi.v_str = f'{self.x0.name}'
self.ys.v_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + {self.x0.name}'
self.y.v_str = f'{self.name}_ys * {self.name}_lim_zi + ' \
f'{self.lower.name} * {self.name}_lim_zl + ' \
f'{self.upper.name} * {self.name}_lim_zu'
self.xi.e_str = f'{self.ki.name} * ({self.u.name} - {self.ref.name} -' \
f' {self.ks.name} * ({self.name}_ys - {self.name}_y))'
self.ys.e_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + ' \
f'{self.name}_xi - {self.name}_ys'
self.y.e_str = self.y.v_str + f' - {self.name}_y'
[docs]class PIDTrackAW(PITrackAW):
"""
PID with tracking anti-windup limiter
"""
[docs] def __init__(self, u, kp, ki, kd, Td, ks, lower, upper, no_lower=False, no_upper=False,
ref=0.0, x0=0.0, name=None, tex_name=None, info=None):
PITrackAW.__init__(self, u=u, kp=kp, ki=ki, ks=ks,
lower=lower, upper=upper,
no_lower=no_lower, no_upper=no_upper,
ref=ref, x0=x0, name=name, tex_name=tex_name,
info=info)
self.kd = dummify(kd)
self.Td = dummify(Td)
self.uin = Algeb(info="PID input", tex_name='uin')
self.WO = Washout(info='Washout', tex_name='WO',
u=self.uin, K=self.kd, T=self.Td)
self.vars = OrderedDict([('uin', self.uin),
('xi', self.xi),
('WO', self.WO),
('ys', self.ys),
('lim', self.lim),
('y', self.y)])
[docs] def define(self):
PITrackAW.define(self)
self.uin.v_str = f'({self.u.name} - {self.ref.name})'
self.uin.e_str = f'({self.u.name} - {self.ref.name}) - {self.name}_uin'
# overwrite existing `y` equations
self.ys.e_str = f'{self.kp.name} * ({self.u.name} - {self.ref.name}) + ' \
f'{self.name}_xi + {self.name}_WO_y - {self.name}_ys'
[docs]class PITrackAWFreeze(PITrackAW):
"""
PI controller with tracking anti-windup limiter and state freeze.
"""
[docs] def __init__(self, u, kp, ki, ks, lower, upper, freeze, no_lower=False, no_upper=False,
ref=0.0, x0=0.0, name=None, tex_name=None, info=None):
PITrackAW.__init__(self, u, kp, ki, ks, lower, upper, no_lower=no_lower, no_upper=no_upper,
ref=ref, x0=x0, name=name, tex_name=tex_name, info=info)
self.freeze = dummify(freeze)
self.flag = EventFlag(u=self.freeze, tex_name='z^{flag}')
self.vars['flag'] = self.flag
self.ys.diag_eps = True
self.y.diag_eps = True
[docs] def define(self):
PITrackAW.define(self)
self.xi.e_str = f'(1-{self.freeze.name}) * {self.ki.name} * ' \
f'({self.u.name} - {self.ref.name} -' \
f' {self.ks.name} * ({self.name}_ys - {self.name}_y))'
self.ys.e_str = f'(1-{self.freeze.name}) * ' \
f'({self.kp.name} * ({self.u.name} - {self.ref.name}) + {self.name}_xi - {self.name}_ys)'
self.y.e_str = f'(1 - {self.freeze.name}) * ' \
f'({self.name}_ys * {self.name}_lim_zi +' \
f' {self.lower.name} * {self.name}_lim_zl +' \
f' {self.upper.name} * {self.name}_lim_zu - {self.name}_y)'
[docs]class PIFreeze(PIController):
"""
PI controller with state freeze.
Freezes state when the corresponding `freeze == 1`.
Notes
-----
Tested in `experimental.TestPITrackAW.PIFreeze`.
"""
[docs] def __init__(self, u, kp, ki, freeze, ref=0.0, x0=0.0, name=None,
tex_name=None, info=None):
PIController.__init__(self, u=u, kp=kp, ki=ki, ref=ref, x0=x0,
name=name, tex_name=tex_name, info=info)
self.freeze = dummify(freeze)
self.flag = EventFlag(u=self.freeze, tex_name='z^{flag}')
self.vars['flag'] = self.flag
self.y.diag_eps = True
[docs] def define(self):
r"""
Notes
-----
One state variable ``xi`` and one algebraic variable ``y`` are added.
Equations implemented are
.. math ::
\dot{x_i} &= k_i * (u - ref) \\
y &= (1-freeze) * (x_i + k_p * (u - ref)) + freeze * y
"""
PIController.define(self)
# state freeze does not affect initial values
self.xi.e_str = f'(1 - {self.freeze.name}) * {self.ki.name} * ' \
f'({self.u.name} - {self.ref.name})'
# Freeze and unfreeze should invoke a Jacobian update
# In general, any equation switching should invoke a Jacobian update
self.y.e_str = f'(1 - {self.freeze.name}) * ' \
f'({self.kp.name}*({self.u.name}-{self.ref.name}) + {self.name}_xi - {self.name}_y)'
class PIControllerNumeric(Block):
"""
A PI Controller implemented with numerical function calls.
`ref` must not be a variable.
"""
def __init__(self, u, kp, ki, ref=0.0, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = u
self.ref = dummify(ref)
self.kp = dummify(kp)
self.ki = dummify(ki)
self.xi = State(info="Integrator value")
self.y = Algeb(info="PI output")
self.vars = {'xi': self.xi, 'y': self.y}
self.flags.update({'f_num': True, 'g_num': True, 'j_setup': True})
def g_numeric(self, **kwargs):
self.y.e = self.kp.v * (self.u.v - self.ref.v) + self.xi.v - self.y.v
def f_numeric(self, **kwargs):
self.xi.e = self.ki.v * (self.u.v - self.ref.v)
def j_setup(self):
self.j_reset()
self.triplets.append_ijv('fyc', self.xi.id, self.u.id, self.ki.v)
self.triplets.append_ijv('gyc', self.y.id, self.u.id, self.kp.v)
self.triplets.append_ijv('gxc', self.y.id, self.xi.id, 1.0)
self.triplets.append_ijv('gyc', self.y.id, self.y.id, -1.0)
def define(self):
"""Skip the symbolic definition"""
pass
[docs]class Gain(Block):
r"""
Gain block.
::
┌───┐
u -> │ K │ -> y
└───┘
Exports an algebraic output `y`.
"""
[docs] def __init__(self, u, K, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.K = dummify(K)
self.enforce_tex_name((self.K,))
self.y = Observable(info='Gain output', tex_name='y')
self.vars = {'y': self.y}
[docs] def define(self):
r"""
Implemented equation and the initial condition are
.. math ::
y = K u \\
y^{(0)} = K u^{(0)}
"""
self.y.e_str = f'{self.K.name} * {self.u.name}'
[docs]class Integrator(Block):
r"""
Integrator block.
::
┌──────┐
u -> │ K/sT │ -> y
└──────┘
Exports a differential variable `y`.
The initial output needs to be specified through `y0`.
"""
[docs] def __init__(self, u, T, K, y0, check_init=True,
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.K = dummify(K)
self.T = dummify(T)
self.y0 = dummify(y0)
self.check_init = check_init
self.enforce_tex_name((self.K, self.T))
self.y = State(info='Integrator output', tex_name='y', t_const=self.T,
check_init=check_init)
self.vars = {'y': self.y}
[docs] def define(self):
r"""
Implemented equation and the initial condition are
.. math ::
\dot{y} = K u \\
y^{(0)} = 0
"""
self.y.v_str = f'{self.y0.name}'
self.y.e_str = f'{self.K.name} * ({self.u.name})'
[docs]class IntegratorAntiWindup(Block):
r"""
Integrator block with anti-windup limiter.
::
upper
/¯¯¯¯¯
┌──────┐
u -> │ K/sT │ -> y
└──────┘
_____/
lower
Exports a differential variable `y` and an AntiWindup `lim`.
The initial output must be specified through `y0`.
"""
[docs] def __init__(self, u, T, K, y0, lower, upper, name=None, tex_name=None, info=None, no_warn=False):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.y0 = dummify(y0)
self.lower = dummify(lower)
self.upper = dummify(upper)
self.enforce_tex_name((self.K, self.T))
self.no_warn = no_warn
self.y = State(info='AW Integrator output', tex_name='y', t_const=self.T)
self.lim = AntiWindup(u=self.y, lower=self.lower, upper=self.upper, tex_name='lim',
info='Limiter in integrator', no_warn=self.no_warn,
)
self.y.discrete = self.lim
self.vars = {'y': self.y, 'lim': self.lim}
[docs] def define(self):
r"""
Implemented equation and the initial condition are
.. math ::
\dot{y} = K u \\
y^{(0)} = 0
"""
self.y.v_str = f'{self.y0.name}'
self.y.e_str = f'{self.K.name} * ({self.u.name})'
[docs]class Washout(Block):
r"""
Washout filter (high pass) block.
::
┌────────┐
│ sK │
u -> │ ────── │ -> y
│ 1 + sT │
└────────┘
Exports state `x` (symbol `x'`) and output algebraic variable `y`.
"""
[docs] def __init__(self, u, T, K, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.enforce_tex_name((self.K, self.T))
self.x = State(info='State in washout filter', tex_name="x'", t_const=self.T)
self.y = Algeb(info='Output of washout filter', tex_name=r'y', diag_eps=True)
self.vars.update({'x': self.x, 'y': self.y})
[docs] def define(self):
r"""
Notes
-----
Equations and initial values:
.. math ::
T \dot{x'} &= (u - x') \\
T y &= K (u - x') \\
x'^{(0)} &= u \\
y^{(0)} &= 0
"""
self.x.v_str = f'{self.u.name}'
self.y.v_str = '0'
self.x.e_str = f'({self.u.name} - {self.name}_x)'
self.y.e_str = f'{self.K.name} * ({self.u.name} - {self.name}_x) - {self.T.name} * {self.name}_y'
[docs]class WashoutOrLag(Washout):
"""
Washout with the capability to convert to Lag when K = 0.
Can be enabled with `zero_out`. Need to provide `name` to construct.
Exports state `x` (symbol `x'`), output algebraic variable `y`, and a LessThan block `LT`.
Parameters
----------
zero_out : bool, optional
If True, ``sT`` will become 1, and the washout will become a low-pass filter.
If False, functions as a regular Washout.
"""
[docs] def __init__(self, u, T, K, name=None, zero_out=True, tex_name=None, info=None):
super().__init__(u, T, K, name=name, tex_name=tex_name, info=info)
self.zero_out = zero_out
self.LT = LessThan(K,
dummify(0),
equal=True,
enable=zero_out,
tex_name='LT',
cache=True,
z0=1, z1=0)
self.vars.update({'LT': self.LT})
[docs] def define(self):
r"""
Notes
-----
Equations and initial values:
.. math ::
T \dot{x'} &= (u - x') \\
T y = z_0 K (u - x') + z_1 T x \\
x'^{(0)} &= u \\
y^{(0)} &= 0
where ``z_0`` is a flag array for the greater-than-zero elements, and ``z_1`` is that for the
less-than or equal-to zero elements.
"""
super().define()
self.y.v_str = f'{self.name}_LT_z0 * 0 + {self.name}_LT_z1 * {self.name}_x'
self.y.e_str = f'{self.name}_LT_z0 * {self.K.name} * ({self.u.name} - {self.name}_x) + ' \
f'{self.name}_LT_z1 * {self.T.name} * {self.name}_x - ' \
f'{self.T.name} * {self.name}_y'
[docs]class Lag(Block):
r"""
Lag (low pass filter) transfer function.
::
┌────────┐
│ K │
u -> │ ────── │ -> y
│ D + sT │
└────────┘
Exports one state variable `y` as the output.
Parameters
----------
K
Gain
T
Time constant
D
Constant
u
Input variable
"""
[docs] def __init__(self, u, T, K, D=1, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.D = dummify(D)
self.enforce_tex_name((self.K, self.T, self.D))
self.y = State(info='State in lag transfer function', tex_name="y",
t_const=self.T)
self.vars = {'y': self.y}
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (Ku - Dy) \\
y^{(0)} &= Ku / D
"""
self.y.v_str = f'{self.u.name} * {self.K.name} / {self.D.name}'
self.y.e_str = f'({self.K.name} * {self.u.name} - {self.D.name} * {self.name}_y)'
[docs]class LagFreeze(Lag):
"""
Lag with an input to freeze the state.
During the period when the freeze signal is 1, the LagFreeze output will be frozen.
"""
[docs] def __init__(self, u, T, K, freeze, D=1, name=None, tex_name=None, info=None):
Lag.__init__(self, u, T, K, D=1, name=name, tex_name=tex_name, info=info)
self.freeze = dummify(freeze)
self.flag = EventFlag(u=self.freeze, tex_name='z^{flag}')
self.vars['flag'] = self.flag
self.y.diag_eps = True
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (1 - freeze) * (Ku - y) \\
y^{(0)} &= K u
"""
Lag.define(self)
self.y.e_str = f'(1 - {self.freeze.name})* ({self.K.name} * {self.u.name} - {self.name}_y)'
[docs]class LagAntiWindup(Block):
r"""
Lag (low pass filter) transfer function block with an anti-windup limiter.
::
upper
/¯¯¯¯¯¯
┌────────┐
│ K │
u -> │ ────── │ -> y
│ D + sT │
└────────┘
______/
lower
Exports one state variable `y` as the output and one AntiWindup instance `lim`.
Parameters
----------
K
Gain
T
Time constant
D
Constant
u
Input variable
"""
[docs] def __init__(self, u, T, K, lower, upper, D=1,
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.D = dummify(D)
self.lower = dummify(lower)
self.upper = dummify(upper)
self.enforce_tex_name((self.T, self.K, self.D))
self.y = State(info='State in lag TF', tex_name="y",
t_const=self.T)
self.lim = AntiWindup(u=self.y, lower=self.lower, upper=self.upper, tex_name='lim',
info='Limiter in Lag')
self.y.discrete = self.lim
self.vars = {'y': self.y, 'lim': self.lim}
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (Ku - Dy) \\
y^{(0)} &= K u / D
"""
self.y.v_str = f'{self.u.name} * {self.K.name} / {self.D.name}'
self.y.e_str = f'{self.K.name} * {self.u.name} - {self.D.name} * {self.name}_y'
[docs]class LagAWFreeze(LagAntiWindup):
"""
Lag with anti-windup limiter and state freeze.
Note that the output `y` is a state variable.
"""
[docs] def __init__(self, u, T, K, lower, upper, freeze, D=1,
name=None, tex_name=None, info=None):
LagAntiWindup.__init__(self, u, T, K, lower, upper, D=D,
name=name, tex_name=tex_name, info=info)
self.freeze = dummify(freeze)
self.flag = EventFlag(u=self.freeze, tex_name='z^{flag}')
self.vars['flag'] = self.flag
self.y.diag_eps = True
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (1 - freeze) (Ku - y) \\
y^{(0)} &= K u
``y`` undergoes an anti-windup limiter.
"""
LagAntiWindup.define(self)
self.y.e_str = f'(1 - {self.freeze.name}) * ({self.K.name} * {self.u.name} - {self.name}_y)'
[docs]class LagRate(Block):
r"""
Lag (low pass filter) transfer function block with a rate limiter.
::
/ rate_upper
┌────────┐
│ K │
u -> │ ────── │ -> y
│ D + sT │
└────────┘
rate_lower /
Exports one state variable `y` as the output and one AntiWindupRate instance `lim`.
Parameters
----------
K
Gain
T
Time constant
D
Constant
u
Input variable
"""
[docs] def __init__(self, u, T, K,
rate_lower, rate_upper, D=1,
rate_no_lower=False, rate_no_upper=False,
rate_lower_cond=None, rate_upper_cond=None,
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.D = dummify(D)
self.rate_lower, self.rate_upper = rate_lower, rate_upper
self.rate_no_lower, self.rate_no_upper = rate_no_lower, rate_no_upper
self.rate_lower_cond, self.rate_upper_cond = rate_lower_cond, rate_upper_cond
self.enforce_tex_name((self.T, self.K, self.D))
self.y = State(info='State in lag TF', tex_name="y",
t_const=self.T)
self.lim = RateLimiter(u=self.y, lower=rate_lower, upper=rate_upper,
no_lower=rate_no_lower, no_upper=rate_no_upper,
lower_cond=rate_lower_cond, upper_cond=rate_upper_cond,
tex_name='lim',
info='Rate limiter in Lag')
self.vars = {'y': self.y, 'lim': self.lim}
# TODO: check if the rate is correct when `t_const` is not 1
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (Ku - y) \\
y^{(0)} &= K u
"""
self.y.v_str = f'{self.u.name} * {self.K.name}'
self.y.e_str = f'{self.K.name} * {self.u.name} - {self.name}_y'
[docs]class LagAntiWindupRate(Block):
r"""
Lag (low pass filter) transfer function block with a rate limiter and an
anti-windup limiter.
::
upper
rate_upper /¯¯¯¯¯¯
┌────────┐
│ K │
u -> │ ────── │ -> y
│ D + sT │
└────────┘
______/ rate_lower
lower
Exports one state variable `y` as the output and one AntiWindupRate instance `lim`.
Parameters
----------
K
Gain
T
Time constant
D
Constant
u
Input variable
"""
[docs] def __init__(self, u, T, K,
lower, upper, rate_lower, rate_upper, D=1,
no_lower=False, no_upper=False, rate_no_lower=False, rate_no_upper=False,
rate_lower_cond=None, rate_upper_cond=None,
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T = dummify(T)
self.K = dummify(K)
self.D = dummify(D)
self.lower = dummify(lower)
self.upper = dummify(upper)
# register in itself in case of queries
self.no_lower, self.no_upper = no_lower, no_upper
self.rate_lower, self.rate_upper = rate_lower, rate_upper
self.rate_no_lower, self.rate_no_upper = rate_no_lower, rate_no_upper
self.rate_lower_cond, self.rate_upper_cond = rate_lower_cond, rate_upper_cond
self.enforce_tex_name((self.T, self.K, self.D))
self.y = State(info='State in lag TF', tex_name="y",
t_const=self.T,
)
self.lim = AntiWindupRate(u=self.y, lower=self.lower, upper=self.upper,
rate_lower=rate_lower, rate_upper=rate_upper,
no_lower=no_lower, no_upper=no_upper,
rate_no_lower=rate_no_lower, rate_no_upper=rate_no_upper,
rate_lower_cond=rate_lower_cond, rate_upper_cond=rate_upper_cond,
tex_name='lim',
info='Limiter in Lag',
)
self.y.discrete = self.lim
self.vars = {'y': self.y, 'lim': self.lim}
[docs] def define(self):
r"""
Notes
-----
Equations and initial values are
.. math ::
T \dot{y} &= (Ku - Dy) \\
y^{(0)} &= K u / D
"""
self.y.v_str = f'{self.u.name} * {self.K.name} / {self.D.name}'
self.y.e_str = f'{self.K.name} * {self.u.name} - {self.D.name} * {self.name}_y'
[docs]class Lag2ndOrd(Block):
r"""
Second order lag transfer function (low-pass filter).
::
┌──────────────────┐
│ K │
u -> │ ──────────────── │ -> y
│ 1 + sT1 + s^2 T2 │
└──────────────────┘
Exports one two state variables (`x`, `y`), where `y` is the output.
Parameters
----------
u
Input
K
Gain
T1
First order time constant
T2
Second order time constant
"""
[docs] def __init__(self, u, K, T1, T2, name=None, tex_name=None, info=None):
super(Lag2ndOrd, self).__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.K = dummify(K)
self.T1 = dummify(T1)
self.T2 = dummify(T2)
self.enforce_tex_name((self.K, self.T1, self.T2))
self.x = State(info='State in 2nd order LPF', tex_name="x'", t_const=self.T2)
self.y = State(info='Output of 2nd order LPF', tex_name='y')
self.vars = {'x': self.x, 'y': self.y}
[docs] def define(self):
r"""
Notes
-----
Implemented equations and initial values are
.. math ::
T_2 \dot{x} &= Ku - y - T_1 x \\
\dot{y} &= x \\
x^{(0)} &= 0 \\
y^{(0)} &= K u
"""
self.x.v_str = 0
self.x.e_str = f'{self.u.name} * {self.K.name} - ' \
f'{self.name}_y - ' \
f'{self.T1.name} * {self.name}_x'
self.y.v_str = f'{self.u.name} * {self.K.name}'
self.y.e_str = f'{self.name}_x'
[docs]class LeadLag(Block):
r"""
Lead-Lag transfer function block in series implementation.
::
┌───────────┐
│ 1 + sT1 │
u -> │ K ─────── │ -> y
│ 1 + sT2 │
└───────────┘
Exports two variables: internal state ``x`` and output algebraic
variable ``y``.
Implementation
--------------
The equations are written in the "multiply by T2" form to avoid
division by T2::
T2 * x_dot = u - x
T2 * y = K*T1*(u - x) + K*T2*x + E2
This makes T2=0 safe for the state equation (it becomes the
algebraic constraint ``0 = u - x``), but causes the ``y``
self-coupling (coefficient ``-T2``) to vanish. When T2=0 the
algebraic equation for ``y`` is degenerate and ``y`` is
unconstrained.
The ``zero_out`` mechanism detects T2 <= 0 at run time via a
cached ``LessThan`` flag (``LT_z1``) and adds the bypass term
``E2 = (y - K*u)`` so that ``y = K*u`` is enforced. This
corresponds to evaluating the transfer function at its DC gain.
Parameters
----------
u : BaseVar or BaseParam
Input signal.
T1 : BaseParam
Numerator (lead) time constant.
T2 : BaseParam
Denominator (lag) time constant. Also used as the ``t_const``
of the internal state ``x``.
K : BaseParam or numeric, optional
Static gain (default 1).
zero_out : bool, optional
If ``True`` (default), bypass the block as ``y = K * u`` when
T2 <= 0. Set to ``False`` only if T2 > 0 is guaranteed by
the data.
"""
[docs] def __init__(self, u, T1, T2, K=1, zero_out=True, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T1 = dummify(T1)
self.T2 = dummify(T2)
self.K = dummify(K)
self.zero_out = zero_out
self.enforce_tex_name((self.T1, self.T2))
self.x = State(info='State in lead-lag', tex_name="x'", t_const=self.T2)
self.y = Algeb(info='Output of lead-lag', tex_name=r'y', diag_eps=True)
self.vars = {'x': self.x, 'y': self.y}
if self.zero_out is True:
self.LT = LessThan(T2, dummify(0), equal=True, enable=zero_out, tex_name='LT',
cache=True, z0=1, z1=0)
self.x.discrete = (self.LT,)
self.vars['LT'] = self.LT
[docs] def define(self):
r"""
Notes
-----
Implemented equations and initial values
.. math ::
T_2 \dot{x'} &= (u - x') \\
T_2 y &= K T_1 (u - x') + K T_2 x' + E_2 \, , \text{where} \\
E_2 = &
\left\{\begin{matrix}
(y - K u) &\text{ if } T_2 \leq 0 \& zero\_out=True \\
0& \text{ otherwise }
\end{matrix}\right. \\
x'^{(0)} & = u\\
y^{(0)} & = Ku\\
"""
self.x.v_str = f'{self.u.name}'
self.y.v_str = f'{self.u.name}'
self.x.e_str = f'({self.u.name} - {self.name}_x)'
self.y.e_str = f'{self.K.name} * {self.T1.name} * ({self.u.name} - {self.name}_x) + ' \
f'{self.K.name} * {self.name}_x * {self.T2.name} - ' \
f'{self.name}_y * {self.T2.name}'
# when T2<=0, bypass as pure gain: 0 = y - K*u
if self.zero_out is True:
self.y.e_str += f'+ {self.name}_LT_z1 * ' \
f'({self.name}_y - {self.K.name} * {self.u.name})'
[docs]class LeadLag2ndOrd(Block):
r"""
Second-order lead-lag transfer function block.
::
┌──────────────────┐
│ 1 + sT3 + s^2 T4 │
u -> │ ──────────────── │ -> y
│ 1 + sT1 + s^2 T2 │
└──────────────────┘
Exports two internal states (``x1`` and ``x2``) and output algebraic
variable ``y``.
Implementation
--------------
State-space realization with two states::
T2 * x1_dot = u - x2 - T1*x1
x2_dot = x1
T2 * y = T2*x2 + T2*T3*x1 + T4*(u - x2 - T1*x1) + E2
All equations are multiplied by T2 so that T2=0 converts the
``x1`` equation into an algebraic constraint ``0 = u - x2 - T1*x1``
rather than a division-by-zero. However, the ``y`` self-coupling
(coefficient ``-T2``) also vanishes, leaving ``y`` unconstrained.
The ``zero_out`` mechanism detects T2 <= 0 via a cached
``LessThan`` flag (``LT_z1``) and adds ``E2 = (y - u)`` to
enforce the DC-gain bypass ``y = u``.
Only T2 is checked because it is the common factor of the
denominator polynomial that multiplies the ``y`` equation. When
T2=0 the denominator reduces to ``1 + sT1``, which is at most
first-order — the second-order realization is degenerate and
bypass is the correct behavior. Earlier versions checked all
four time constants, which failed to activate the bypass when
the numerator constants (T3, T4) were nonzero (e.g. IEEEST
with A3=A4=0, A5!=0, A6!=0).
Parameters
----------
u : BaseVar or BaseParam
Input signal.
T1 : BaseParam
Denominator first-order time constant.
T2 : BaseParam
Denominator second-order time constant. Also used as the
``t_const`` of the internal state ``x1``.
T3 : BaseParam
Numerator first-order time constant.
T4 : BaseParam
Numerator second-order time constant.
zero_out : bool, optional
If ``True``, bypass the block as ``y = u`` when T2 <= 0.
Default is ``False``.
"""
[docs] def __init__(self, u, T1, T2, T3, T4, zero_out=False, name=None, tex_name=None, info=None):
super(LeadLag2ndOrd, self).__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T1 = dummify(T1)
self.T2 = dummify(T2)
self.T3 = dummify(T3)
self.T4 = dummify(T4)
self.zero_out = zero_out
self.enforce_tex_name((self.T1, self.T2, self.T3, self.T4))
self.x1 = State(info='State #1 in 2nd order lead-lag', tex_name="x'", t_const=self.T2)
self.x2 = State(info='State #2 in 2nd order lead-lag', tex_name="x''")
self.y = Algeb(info='Output of 2nd order lead-lag', tex_name='y', diag_eps=True)
self.vars = {'x1': self.x1, 'x2': self.x2, 'y': self.y}
if self.zero_out is True:
self.LT = LessThan(T2, dummify(0), equal=True, enable=zero_out, tex_name='LT',
cache=True, z0=1, z1=0)
self.x2.discrete = (self.LT,)
self.vars['LT'] = self.LT
[docs] def define(self):
r"""
Notes
-----
Implemented equations and initial values are
.. math ::
T_2 \dot{x}_1 &= u - x_2 - T_1 x_1 \\
\dot{x}_2 &= x_1 \\
T_2 y &= T_2 x_2 + T_2 T_3 x_1 + T_4 (u - x_2 - T_1 x_1) + E_2 \, , \text{ where} \\
E_2 = &
\left\{\begin{matrix}
(y - u) &\text{ if } T_2 \leq 0 \& zero\_out=True \\
0& \text{ otherwise }
\end{matrix}\right. \\
x_1^{(0)} &= 0 \\
x_2^{(0)} &= y^{(0)} = u
"""
self.x1.e_str = f'{self.u.name} - {self.name}_x2 - {self.T1.name} * {self.name}_x1'
self.x2.e_str = f'{self.name}_x1'
self.y.e_str = f'{self.T2.name} * {self.name}_x2 + ' \
f'{self.T2.name} * {self.T3.name} * {self.name}_x1 + ' \
f'{self.T4.name} * ({self.u.name} - {self.name}_x2 - {self.T1.name} * {self.name}_x1) - ' \
f'{self.T2.name} * {self.name}_y'
self.x1.v_str = 0
self.x2.v_str = f'{self.u.name}'
self.y.v_str = f'{self.u.name}'
# when T2<=0, bypass block: 0 = y - u
if self.zero_out is True:
self.y.e_str += f'+ {self.name}_LT_z1 * ' \
f'({self.name}_y - {self.u.name})'
[docs]class LeadLagLimit(Block):
r"""
Lead-Lag transfer function block with hard limiter (series implementation).
::
┌─────────┐ upper
│ 1 + sT1 │ /¯¯¯¯¯
u -> │ ─────── │ -> ynl / -> y
│ 1 + sT2 │ _____/
└─────────┘ lower
Exports four variables: state `x`, output before hard limiter `ynl`, output `y`, and AntiWindup `lim`.
"""
[docs] def __init__(self, u, T1, T2, lower, upper,
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.T1 = dummify(T1)
self.T2 = dummify(T2)
self.lower = lower
self.upper = upper
self.enforce_tex_name((self.T1, self.T2))
self.x = State(info='State in lead-lag TF', tex_name="x'", t_const=self.T2)
self.ynl = Algeb(info='Output of lead-lag TF before limiter', tex_name=r'y_{nl}')
self.y = Observable(info='Output of lead-lag TF after limiter', tex_name=r'y')
self.lim = AntiWindup(u=self.ynl, lower=self.lower, upper=self.upper)
self.ynl.discrete = self.lim
self.vars = {'x': self.x, 'ynl': self.ynl, 'y': self.y, 'lim': self.lim}
[docs] def define(self):
r"""
Notes
-----
Implemented control block equations (without limiter) and initial values
.. math ::
T_2 \dot{x'} &= (u - x') \\
T_2 y &= T_1 (u - x') + T_2 x' \\
x'^{(0)} &= y^{(0)} = u
"""
self.x.v_str = f'{self.u.name}'
self.ynl.v_str = f'{self.u.name}'
self.x.e_str = f'({self.u.name} - {self.name}_x)'
self.ynl.e_str = f'{self.T1.name} * ({self.u.name} - {self.name}_x) + ' \
f'{self.name}_x * {self.T2.name} - ' \
f'{self.name}_ynl * {self.T2.name}'
self.y.e_str = f'{self.name}_ynl * {self.name}_lim_zi + ' \
f'{self.lower.name} * {self.name}_lim_zl + ' \
f'{self.upper.name} * {self.name}_lim_zu'
[docs]class HVGate(Block):
"""
High Value Gate. Outputs the maximum of two inputs.
::
┌─────────┐
u1 -> │ HV Gate │
│ │ -> y
u2 -> │ (MAX) │
└─────────┘
"""
[docs] def __init__(self, u1, u2, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u1 = dummify(u1)
self.u2 = dummify(u2)
self.enforce_tex_name((u1, u2))
self.lt = LessThan(self.u1, self.u2)
self.y = Observable(info='HVGate output', tex_name='y', discrete=self.lt)
self.vars = {'y': self.y, 'lt': self.lt}
def define(self):
"""
Implemented equations and initial conditions
.. math ::
0 = s_0^{sl} u_1 + s_1^{sl} u_2 - y
y_0 = maximum(u_1, u_2)
Notes
-----
In the implementation, one should not use ::
self.y.v_str = f'maximum({self.u1.name}, {self.u2.name})',
because SymPy processes this equation to `{self.u1.name}`.
Not sure if this is a bug or intended.
"""
self.y.e_str = f'{self.name}_lt_z0*{self.u1.name} + {self.name}_lt_z1*{self.u2.name}'
[docs]class LVGate(Block):
"""
Low Value Gate. Outputs the minimum of the two inputs.
::
┌─────────┐
u1 -> │ LV Gate |
│ | -> y
u2 -> │ (MIN) |
└─────────┘
"""
[docs] def __init__(self, u1, u2, name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u1 = dummify(u1)
self.u2 = dummify(u2)
self.enforce_tex_name((u1, u2))
self.lt = LessThan(self.u1, self.u2)
self.y = Observable(info='LVGate output', tex_name='y', discrete=self.lt)
self.vars = {'y': self.y, 'lt': self.lt}
def define(self):
"""
Implemented equations and initial conditions
.. math ::
0 = s_0^{sl} u_1 + s_1^{sl} u_2 - y
y_0 = minimum(u_1, u_2)
Notes
-----
Same problem as `HVGate` as `minimum` does not sympify correctly.
"""
self.y.e_str = f'{self.name}_lt_z1*{self.u1.name} + {self.name}_lt_z0*{self.u2.name}'
[docs]class GainLimiter(Block):
"""
Gain followed by a limiter and another gain.
Exports the limited output `y`, unlimited output `x`, and HardLimiter `lim`.
::
┌─────┐ upper ┌─────┐
│ │ /¯¯¯¯¯ │ │
u -> │ K │ -> x / -> │ R │ -> y
│ │ _____/ │ │
└─────┘ lower └─────┘
Parameters
----------
u : str, BaseVar
Input variable, or an equation string for constructing an anonymous variable
K : str, BaseParam, BaseService
Initial gain for `u` before limiter
R : str, BaseParam, BaseService
Post limiter gain
"""
[docs] def __init__(self, u, K, R, lower, upper, no_lower=False, no_upper=False,
sign_lower=1, sign_upper=1, allow_adjust=True,
name=None, tex_name=None, info=None):
Block.__init__(self, name=name, tex_name=tex_name, info=info)
self.u = dummify(u)
self.K = dummify(K)
self.R = dummify(R)
self.upper = dummify(upper)
self.lower = dummify(lower)
self.sign_lower, self.sign_upper = sign_lower, sign_upper
if (no_upper and no_lower) is True:
raise ValueError("no_upper or no_lower cannot both be True")
self.no_lower = no_lower
self.no_upper = no_upper
self.x = Algeb(info='Value before limiter', tex_name='x')
self.lim = HardLimiter(u=self.x, lower=self.lower, upper=self.upper,
no_upper=no_upper, no_lower=no_lower,
sign_lower=sign_lower, sign_upper=sign_upper,
allow_adjust=allow_adjust,
tex_name='lim')
self.y = Observable(info='Output after limiter and post gain', tex_name='y', discrete=self.lim)
self.vars = {'lim': self.lim, 'x': self.x, 'y': self.y}
[docs] def define(self):
"""
TODO: write docstring
"""
self.x.v_str = f'{self.K.name} * ({self.u.name})'
self.x.e_str = f'{self.K.name} * ({self.u.name}) - {self.name}_x'
self.y.e_str = f'{self.name}_x * {self.name}_lim_zi * {self.R.name}'
if not self.no_upper:
self.y.e_str += f' + {self.name}_lim_zu*{self.upper.name} * {self.R.name}* {self.lim.sign_upper.name}'
if not self.no_lower:
self.y.e_str += f' + {self.name}_lim_zl*{self.lower.name} * {self.R.name}* {self.lim.sign_lower.name}'
[docs]class Piecewise(Block):
"""
Piecewise block. Outputs an algebraic variable `y`.
This block takes a list of `N` points, `[x0, x1, ...x_{n-1}]` to define N+1 ranges,
namely (-inf, x0), (x0, x1), ..., (x_{n-1}, +inf).
and a list of `N+1` function strings `[fun0, ..., fun_n]`.
Inputs that fall within each range applies the corresponding function.
The first range (-inf, x0) applies `fun_0`, and
the last range (x_{n-1}, +inf) applies the last function `fun_n`.
The function returns zero if no condition is met.
.. note::
Piecewise.y must remain ``Algeb`` (not ``Observable``) because:
(1) model authors may set ``v_iter`` on the output for coupled
iterative initialization (e.g., EXAC1), and (2) zero-valued
branches cause ``ComplexInfinity`` when the expression is
substituted into denominators during code generation.
Parameters
----------
points : list, tuple
A list of piecewise points. Need to be provided in the constructor function.
funs : list, tuple
A list of strings for the piecewise functions. Need to be provided in the overloaded `define` function.
"""
[docs] def __init__(self, u, points: Union[List, Tuple], funs: Union[List, Tuple],
name=None, tex_name=None, info=None):
super().__init__(name=name, tex_name=tex_name, info=info)
self.u = u
self.points = points
self.funs = funs
self.y = Algeb(info='Output of piecewise', tex_name='y')
self.vars = {'y': self.y}
[docs] def define(self):
"""
Build the equation string for the piecewise equations.
``self.funs`` needs to be provided with the function strings corresponding to each range.
"""
args = []
i = 0
for i in range(len(self.points)):
args.append(f'({self.funs[i]}, {self.u.name} <= {self.points[i]})')
args.append(f'({self.funs[i + 1]}, {self.u.name} > {self.points[-1]})')
args_comma = ', '.join(args) + ', (0, True)'
pw_fun = f'Piecewise({args_comma}, evaluate=False)'
self.y.v_str = pw_fun
self.y.e_str = f'{pw_fun} - {self.name}_y'
[docs]class DeadBand1(Block):
"""
Deadband type 1 (linear, non-step).
.. note::
DeadBand1.y must remain ``Algeb`` (not ``Observable``) because
other models (e.g., DGPRCT1) reference it via ``ExtAlgeb``.
Parameters
----------
center
Default value when within the deadband. If the input is an error signal,
center should be set to zero.
gain
Gain multiplied to DeadBand discrete block's output.
Notes
-----
Block diagram ::
| /
______|__/___ -> Gain -> DeadBand1_y
/ |
/ |
"""
[docs] def __init__(self, u, center, lower, upper, gain=1.0, enable=True,
name=None, tex_name=None, info=None, namespace='local'):
Block.__init__(self, name=name, tex_name=tex_name, info=info, namespace=namespace)
self.u = dummify(u)
self.center = dummify(center)
self.lower = dummify(lower)
self.upper = dummify(upper)
self.gain = dummify(gain)
self.enable = enable
self.db = DeadBand(u=u, center=center, lower=lower, upper=upper,
enable=enable, tex_name='db')
self.y = Algeb(info='Deadband type 1 output', tex_name='y', discrete=self.db)
self.vars = {'db': self.db, 'y': self.y}
def define(self):
"""
Notes
-----
Implemented equation:
.. math ::
0 = center + z_u * (u - upper) + z_l * (u - lower) - y
"""
db_expr = f'{self.gain.name} * ({self.center.name} + ' \
f'{self.name}_db_zu * ({self.u.name} - {self.upper.name}) +' \
f'{self.name}_db_zl * ({self.u.name} - {self.lower.name}))'
self.y.v_str = db_expr
self.y.e_str = f'{db_expr} - {self.name}_y'