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

check_var

Before equation evaluation

Variable-based flags (Limiter)

check_eq

After equation update

Equation-based flags (AntiWindup)

set_var

After solving

State pegging (AntiWindup)

In the current implementation:

  • check_var updates flags for variable-based discrete components (such as Limiter)

  • check_eq updates flags for equation-involved discrete components (such as AntiWindup)

  • set_var is currently only used by AntiWindup to store the pegged states

Discrete Types#

Discrete([name, tex_name, info, no_warn, ...])

Base discrete class.

Limiter(u, lower, upper[, enable, name, ...])

Base limiter class.

SortedLimiter(u, lower, upper[, n_select, ...])

A limiter that sorts inputs based on the absolute or relative amount of limit violations.

HardLimiter(u, lower, upper[, enable, name, ...])

Hard limiter for algebraic or differential variable.

RateLimiter(u, lower, upper[, enable, ...])

Rate limiter for a differential variable.

AntiWindup(u, lower, upper[, enable, ...])

Anti-windup limiter.

AntiWindupRate(u, lower, upper, rate_lower, ...)

Anti-windup limiter with rate limits

LessThan(u, bound[, equal, enable, name, ...])

Less than (<) comparison function that tests if u < bound.

Selector(*args, fun[, tex_name, info])

Selection between two variables using the provided reduce function.

Switcher(u, options[, info, name, tex_name, ...])

Switcher based on an input parameter.

DeadBand(u, center, lower, upper[, enable, ...])

The basic deadband type.

DeadBandRT(u, center, lower, upper[, enable])

Deadband with flags for directions of return.

Average(u[, mode, delay, name, tex_name, info])

Compute the average value of a BaseVar over a period of time or a number of simulation steps.

Delay(u[, mode, delay, name, tex_name, info])

The delay class.

Derivative(u[, name, tex_name, info])

Compute the derivative of a variable using numerical differentiation.

Sampling(u[, interval, offset, name, ...])

Sample and hold

ShuntAdjust(*, v, lower, upper, bsw, gsw, dt, u)

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 HVGate and LVGate.

See also

numpy.ufunc.reduce

NumPy 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 into np.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 - delay will 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, where tstep is 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>_v just like Delay.

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