Discrete Components#
The discrete component library contains a special type of block for modeling discontinuities in power system devices. Such discontinuities can be device-level physical constraints or algorithmic limits imposed on controllers.
Background#
The base class for discrete components is andes.core.discrete.Discrete.
The uniqueness of discrete components is the way they work. Discrete components take inputs, criteria, and export a set of flags with component-defined meanings. These exported flags can be used in algebraic or differential equations to build piece-wise equations.
For example, Limiter takes a v-provider as input and two v-providers as the
upper and lower bounds. It exports three flags: zi (within bound), zl (below
lower bound), and zu (above upper bound).
Update Timing#
It is important to note when flags are updated. Discrete subclasses can use three methods to check and update values and equations:
Method |
When Called |
Purpose |
|---|---|---|
|
Before equation evaluation |
Variable-based flags (Limiter) |
|
After equation update |
Equation-based flags (AntiWindup) |
|
After solving |
State pegging (AntiWindup) |
In the current implementation:
check_varupdates flags for variable-based discrete components (such asLimiter)check_equpdates flags for equation-involved discrete components (such asAntiWindup)set_varis currently only used byAntiWindupto store the pegged states
Discrete Types#
|
Base discrete class. |
|
Base limiter class. |
|
A limiter that sorts inputs based on the absolute or relative amount of limit violations. |
|
Hard limiter for algebraic or differential variable. |
|
Rate limiter for a differential variable. |
|
Anti-windup limiter. |
|
Anti-windup limiter with rate limits |
|
Less than (<) comparison function that tests if |
|
Selection between two variables using the provided reduce function. |
|
Switcher based on an input parameter. |
|
The basic deadband type. |
|
Deadband with flags for directions of return. |
|
Compute the average value of a BaseVar over a period of time or a number of simulation steps. |
|
The delay class. |
|
Compute the derivative of a variable using numerical differentiation. |
|
Sample and hold |
|
Class for adjusting switchable shunts. |
Limiters#
- class andes.core.discrete.Limiter(u, lower, upper, enable=True, name: str = None, tex_name: str = None, info: str = None, min_iter: int = 2, err_tol: float = 0.01, allow_adjust: bool = True, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, equal=True, no_warn=False, zu=0.0, zl=0.0, zi=1.0)[source]
Base limiter class.
This class compares values and sets limit values. Exported flags are zi, zl and zu.
- Parameters:
- uBaseVar
Input Variable instance
- lowerBaseParam
Parameter instance for the lower limit
- upperBaseParam
Parameter instance for the upper limit
- no_lowerbool
True to only use the upper limit
- no_upperbool
True to only use the lower limit
- sign_lower: 1 or -1
Sign to be multiplied to the lower limit
- sign_upper: bool
Sign to be multiplied to the upper limit
- equalbool
True to include equal signs in comparison (>= or <=).
- no_warnbool
Disable initial limit warnings
- zu0 or 1
Default value for zu if not enabled
- zl0 or 1
Default value for zl if not enabled
- zi0 or 1
Default value for zi if not enabled
- Attributes:
- zlarray-like
Flags of elements violating the lower limit; A array of zeros and/or ones.
- ziarray-like
Flags for within the limits
- zuarray-like
Flags for violating the upper limit
Notes
If not enabled, the default flags are
zu = zl = 0,zi = 1.
Example usage:
from andes.core.discrete import Limiter
self.lim = Limiter(
u=self.x, # Input variable
lower=self.Vmin, # Lower bound
upper=self.Vmax # Upper bound
)
# Flags exported:
# lim.zi = 1 when lower <= x <= upper (within)
# lim.zl = 1 when x < lower (below)
# lim.zu = 1 when x > upper (above)
# Use in equation for piecewise behavior
self.y = Algeb(e_str='x*lim_zi + upper*lim_zu + lower*lim_zl - y')
See the code in models/static/pq.py for an example of voltage-based PQ-to-Z conversion.
- class andes.core.discrete.SortedLimiter(u, lower, upper, n_select: int = 5, name=None, tex_name=None, enable=True, abs_violation=True, min_iter: int = 2, err_tol: float = 0.01, allow_adjust: bool = True, zu=0.0, zl=0.0, zi=1.0, ql=0.0, qu=0.0)[source]
A limiter that sorts inputs based on the absolute or relative amount of limit violations.
- Parameters:
- n_selectint
the number of violations to be flagged, for each of over-limit and under-limit cases. If n_select == 1, at most one over-limit and one under-limit inputs will be flagged. If n_select is zero, heuristics will be used.
- abs_violationbool
True to use the absolute violation. False if the relative violation abs(violation/limit) is used for sorting. Since most variables are in per unit, absolute violation is recommended.
- class andes.core.discrete.HardLimiter(u, lower, upper, enable=True, name: str = None, tex_name: str = None, info: str = None, min_iter: int = 2, err_tol: float = 0.01, allow_adjust: bool = True, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, equal=True, no_warn=False, zu=0.0, zl=0.0, zi=1.0)[source]
Hard limiter for algebraic or differential variable. This class is an alias of Limiter.
- class andes.core.discrete.RateLimiter(u, lower, upper, enable=True, no_lower=False, no_upper=False, lower_cond=1, upper_cond=1, name=None, tex_name=None, info=None)[source]
Rate limiter for a differential variable.
RateLimiter does not export any variable. It directly modifies the differential equation value.
Warning
RateLimiter cannot be applied to a state variable that already undergoes an AntiWindup limiter. Use AntiWindupRate for a rate-limited anti-windup limiter.
Notes
RateLimiter inherits from Discrete to avoid internal naming conflicts with Limiter.
- class andes.core.discrete.AntiWindup(u, lower, upper, enable=True, no_warn=False, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, name=None, tex_name=None, info=None, state=None, allow_adjust: bool = True)[source]
Anti-windup limiter.
Anti-windup limiter prevents the wind-up effect of a differential variable. The derivative of the differential variable is reset if it continues to increase in the same direction after exceeding the limits. During the derivative return, the limiter will be inactive
if x > xmax and x dot > 0: x = xmax and x dot = 0 if x < xmin and x dot < 0: x = xmin and x dot = 0
This class takes one more optional parameter for specifying the equation.
- Parameters:
- stateState, ExtState
A State (or ExtState) whose equation value will be checked and, when condition satisfies, will be reset by the anti-windup-limiter.
- class andes.core.discrete.AntiWindupRate(u, lower, upper, rate_lower, rate_upper, no_lower=False, no_upper=False, rate_no_lower=False, rate_no_upper=False, rate_lower_cond=None, rate_upper_cond=None, enable=True, name=None, tex_name=None, info=None, allow_adjust: bool = True)[source]
Anti-windup limiter with rate limits
Comparers#
- class andes.core.discrete.LessThan(u, bound, equal=False, enable=True, name=None, tex_name=None, info: str = None, cache: bool = False, z0=0, z1=1)[source]
Less than (<) comparison function that tests if
u < bound.Exports two flags: z1 and z0. For elements satisfying the less-than condition, the corresponding z1 = 1. z0 is the element-wise negation of z1.
Notes
The default z0 and z1, if not enabled, can be set through the constructor. By default, the model will not adjust the limit.
- class andes.core.discrete.Selector(*args, fun, tex_name=None, info=None)[source]
Selection between two variables using the provided reduce function.
The reduce function should take the given number of arguments. An example function is np.maximum.reduce which can be used to select the maximum.
Names are in s0, s1.
Warning
A potential bug when more than two inputs are provided, and values in different inputs are equal. Only two inputs are allowed.
Deprecated since version 1.5.9: Use of this class for comparison-based output is discouraged. Instead, use LessThan and Limiter to construct piesewise equations.
See the new implementation of
HVGateandLVGate.See also
numpy.ufunc.reduceNumPy reduce function
Notes
A common pitfall is the 0-based indexing in the Selector flags. Note that exported flags start from 0. Namely, s0 corresponds to the first variable provided for the Selector constructor.
Examples
Example 1: select the largest value between v0 and v1 and put it into vmax.
After the definitions of v0 and v1, define the algebraic variable vmax for the largest value, and a selector vs
self.vmax = Algeb(v_str='maximum(v0, v1)', tex_name='v_{max}', e_str='vs_s0 * v0 + vs_s1 * v1 - vmax') self.vs = Selector(self.v0, self.v1, fun=np.maximum.reduce)
The initial value of vmax is calculated by
maximum(v0, v1), which is the element-wise maximum in SymPy and will be generated intonp.maximum(v0, v1). The equation of vmax is to select the values based on vs_s0 and vs_s1.
- class andes.core.discrete.Switcher(u, options: list | tuple, info: str = None, name: str = None, tex_name: str = None, cache=True)[source]
Switcher based on an input parameter.
The switch class takes one v-provider, compares the input with each value in the option list, and exports one flag array for each option. The flags are 0-indexed.
Exported flags are named with _s0, _s1, ..., with a total number of len(options). See the examples section.
Notes
Switches needs to be distinguished from Selector.
Switcher is for generating flags indicating option selection based on an input parameter. Selector is for generating flags at run time based on variable values and a selection function.
Examples
The IEEEST model takes an input for selecting the signal. Options are 1 through 6. One can construct
self.IC = NumParam(info='input code 1-6') # input code self.SW = Switcher(u=self.IC, options=[0, 1, 2, 3, 4, 5, 6])
If the IC values from the data file ends up being
self.IC.v = np.array([1, 2, 2, 4, 6])
Then, the exported flag arrays will be
{'IC_s0': np.array([0, 0, 0, 0, 0]), 'IC_s1': np.array([1, 0, 0, 0, 0]), 'IC_s2': np.array([0, 1, 1, 0, 0]), 'IC_s3': np.array([0, 0, 0, 0, 0]), 'IC_s4': np.array([0, 0, 0, 1, 0]), 'IC_s5': np.array([0, 0, 0, 0, 0]), 'IC_s6': np.array([0, 0, 0, 0, 1]) }
where IC_s0 is used for padding so that following flags align with the options.
Example of Switcher for multi-mode selection:
from andes.core.discrete import Switcher
self.sw = Switcher(
u=self.mode, # Selection input
options=[0, 1, 2]
)
# sw.s0 = 1 when mode == 0
# sw.s1 = 1 when mode == 1
# sw.s2 = 1 when mode == 2
# Use in equation
self.y = Algeb(e_str='K1*x*sw_s0 + K2*x*sw_s1 + K3*x*sw_s2 - y')
Deadband#
- class andes.core.discrete.DeadBand(u, center, lower, upper, enable=True, equal=False, zu=0.0, zl=0.0, zi=1.0, name=None, tex_name=None, info=None)[source]
The basic deadband type.
- Parameters:
- uNumParam
The pre-deadband input variable
- centerNumParam
Neutral value of the output
- lowerNumParam
Lower bound
- upperNumParam
Upper bound
- enablebool
Enabled if True; Disabled and works as a pass-through if False.
Notes
Input changes within a deadband will incur no output changes. This component computes and exports three flags.
- Three flags computed from the current input:
zl: True if the input is below the lower threshold
zi: True if the input is within the deadband
zu: True if is above the lower threshold
Initial condition:
All three flags are initialized to zero. All flags are updated during check_var when enabled. If the deadband component is not enabled, all of them will remain zero.
Examples
Exported deadband flags need to be used in the algebraic equation corresponding to the post-deadband variable. Assume the pre-deadband input variable is var_in and the post-deadband variable is var_out. First, define a deadband instance db in the model using
self.db = DeadBand(u=self.var_in, center=self.dbc, lower=self.dbl, upper=self.dbu)
To implement a no-memory deadband whose output returns to center when the input is within the band, the equation for var can be written as
var_out.e_str = 'var_in * (1 - db_zi) + \ (dbc * db_zi) - var_out'
- class andes.core.discrete.DeadBandRT(u, center, lower, upper, enable=True)[source]
Deadband with flags for directions of return.
- Parameters:
- uNumParam
The pre-deadband input variable
- centerNumParam
Neutral value of the output
- lowerNumParam
Lower bound
- upperNumParam
Upper bound
- enablebool
Enabled if True; Disabled and works as a pass-through if False.
Notes
Input changes within a deadband will incur no output changes. This component computes and exports five flags. The additional two flags on top of DeadBand indicate the direction of return:
zur: True if the input is/has been within the deadband and was returned from the upper threshold
zlr: True if the input is/has been within the deadband and was returned from the lower threshold
Initial condition:
All five flags are initialized to zero. All flags are updated during check_var when enabled. If the deadband component is not enabled, all of them will remain zero.
Examples
To implement a deadband whose output is pegged at the nearest deadband bounds, the equation for var can be provided as
var_out.e_str = 'var_in * (1 - db_zi) + \ dbl * db_zlr + \ dbu * db_zur - var_out'
Example:
from andes.core.discrete import DeadBand
self.db = DeadBand(
u=self.error,
center=0,
lower=-0.01,
upper=0.01
)
# db.zi = 1 when within dead band
# db.zl = 1 when below
# db.zu = 1 when above
# Zero output in dead band
self.y = Algeb(e_str='error * (1 - db_zi) - y')
Others#
- class andes.core.discrete.Average(u, mode='step', delay=0, name=None, tex_name=None, info=None)[source]
Compute the average value of a BaseVar over a period of time or a number of simulation steps.
Average is based on the memory implemented in the Delay class. The same modes as in Delay are supported.
The output of the Average class is named
<INSTANCE_NAME>_v, where <INSTANCE_NAME> is the instance name of Average.
- class andes.core.discrete.Delay(u, mode='step', delay=0, name=None, tex_name=None, info=None)[source]
The delay class.
Delay allows to impose a predefined and fixed "delay" (in either steps or seconds) for an input variable. The amount of delay must be a scalar and has to be given when instantiating the Delay class when defining the model.
Delay implements an internal memorize to store past variable values.
The default delay mode is step but can be set to time. In the time mode, the value at the
current time - delaywill be interpolated based on the two nearest times and values.Delay can be applied to a state or an algebraic variable. The exported variable is named
<INSTANCE_NAME>_v, where <INSTANCE_NAME> is the name of the Delay instance.
- class andes.core.discrete.Derivative(u, name=None, tex_name=None, info=None)[source]
Compute the derivative of a variable using numerical differentiation.
Derivative is based on the storage implemented in the Delay class. The delay is set to 1 step so that the current and the previous step are used.
A simple first order derivative is computed using
u(t) - u(t-1) / tstep, wheretstepis the current step size.Derivative is intended to be used for algebraic variables because of discontinuity. It can be applied to a state variable, but one should instead implement the right-hand side equation of the state variable in an algebraic equation to obtain the accurate derivative.
Alternatively, the washout filter (
andes.core.block.Washout) can be used to implement a numerically stable derivative.The output of the Derivative class is named
<INSTANCE_NAME>_vjust likeDelay.
- class andes.core.discrete.Sampling(u, interval=1.0, offset=0.0, name=None, tex_name=None, info=None)[source]
Sample and hold
Sample an input variable periodically at the given time interval and hold the value until the next sample time.
For example, this class can be used to implement a 4-second sampling of the AGC signal.
The output of Sampling is named
<INSTANCE_NAME>_v, where <INSTANCE_NAME> is the Sampling instance name.
- class andes.core.discrete.ShuntAdjust(*, v, lower, upper, bsw, gsw, dt, u, enable=True, min_iter=2, err_tol=0.01, name=None, tex_name=None, info=None, no_warn=False)[source]
Class for adjusting switchable shunts.
- Parameters:
- vBaseVar
Voltage measurement
- lowerBaseParam
Lower voltage bound
- upperBaseParam
Upper voltage bound
- bswSwBlock
SwBlock instance for susceptance
- gswSwBlock
SwBlock instance for conductance
- dtNumParam
Delay time
- uNumParam
Connection status
- min_iterint
Minimum iteration number to enable shunt switching
- err_tolfloat
Minimum iteration tolerance to enable switching
Using Flags in Equations#
Piecewise Linear#
# Saturated output
self.y = Algeb(
e_str='x * lim_zi + upper * lim_zu + lower * lim_zl - y'
)
Conditional Behavior#
# Different gains based on mode
self.y = Algeb(
e_str='K1 * x * sw_s0 + K2 * x * sw_s1 - y'
)
Dead Band Application#
# Zero output in dead band
self.y = Algeb(
e_str='error * (1 - db_zi) - y'
)
Naming Convention#
Discrete component flags use the component name as prefix:
self.lim = Limiter(...)
# Access in equations as: lim_zi, lim_zl, lim_zu
self.VL = Limiter(...)
# Access as: VL_zi, VL_zl, VL_zu
Common Patterns#
Voltage-Based Load Shedding#
self.v_low = LessThan(u=self.v, bound=0.9)
self.P = Algeb(e_str='P0 * (1 - v_low_z) - P')
Governor with Anti-Windup#
self.lim = AntiWindup(u=self.gate, lower=0, upper=1)
self.gate = State(e_str='...') # Gate position
Multi-Mode Controller#
self.mode = Switcher(u=self.ctrl_mode, options=[0, 1, 2])
self.output = Algeb(
e_str='out1*mode_s0 + out2*mode_s1 + out3*mode_s2 - output'
)
See Also#
Blocks - Transfer function blocks (many use discrete internally)
Services - Service components
DAE Formulation - Handling discontinuities in DAE