Variables#

DAE Variables, or variables for short, are unknowns to be solved using numerical or analytical methods. A variable stores values, equation values, and addresses in the DAE array.

Background#

Variables are both v-providers and e-providers (see Atomic Types). Each variable has member attributes v and e for variable values and equation values, respectively. The initial value of v is set by the initialization routine, and the initial value of e is set to zero.

During power flow calculation or time domain simulation:

  • v is not directly modifiable by models but rather updated after solving non-linear equations

  • e is updated by the models and summed up before solving equations

Each variable also stores addresses in its member attribute a. The addresses are 0-based indices into the numerical DAE array (f or g, based on variable type).

Equations associated with state variables take the form:

\[\mathbf{M} \dot{x} = \mathbf{f}(x, y)\]

where \(\mathbf{x}\) are the differential variables, \(\mathbf{y}\) are the algebraic variables, \(\mathbf{M}\) is the mass matrix, and \(\mathbf{f}\) is the right-hand side of differential equations.

Equations associated with algebraic variables take the form:

\[0 = \mathbf{g}(x, y)\]

where \(\mathbf{g}\) is the equation right-hand side.

Variable Types#

BaseVar([name, tex_name, info, unit, v_str, ...])

Base variable class.

State([name, tex_name, info, unit, v_str, ...])

Differential variable class, an alias of the BaseVar.

Algeb([name, tex_name, info, unit, v_str, ...])

Algebraic variable class, an alias of andes.core.var.BaseVar.

ExtVar(model, src[, indexer, allow_none, ...])

Algebraic variable that links to an external model.

ExtState(model, src[, indexer, allow_none, ...])

External state variable type.

ExtAlgeb(model, src[, indexer, allow_none, ...])

External algebraic variable type.

AliasState(var, **kwargs)

Alias state variable.

AliasAlgeb(var, **kwargs)

Alias algebraic variable.

Observable([e_str, name, tex_name, info, ...])

A variable computed by explicit assignment rather than solved in the DAE system.

State#

State variables are described by differential equations and can only change continuously. Use State for dynamics that evolve over time.

from andes.core.var import State

class Generator(Model):
    def __init__(self):
        # Rotor angle
        self.delta = State(
            v_str='delta0',           # Initial value
            e_str='omega - 1',        # d(delta)/dt = omega - 1
            info='Rotor angle',
            tex_name=r'\delta'
        )

        # Rotor speed
        self.omega = State(
            v_str='1.0',
            e_str='(Tm - Te - D*(omega-1)) / M',
            info='Rotor speed',
            tex_name=r'\omega'
        )
class andes.core.var.State(name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None, v_str: str | float | None = None, v_iter: str | None = None, e_str: str | None = None, discrete: Discrete | None = None, t_const: BaseParam | DummyValue | BaseService | None = None, check_init: bool | None = True, v_setter: bool | None = False, e_setter: bool | None = False, addressable: bool | None = True, export: bool | None = True, diag_eps: float | None = 0.0, deps: List | None = None)[source]

Differential variable class, an alias of the BaseVar.

Parameters:
t_constBaseParam, DummyValue

Left-hand time constant for the differential equation. They will be collected to array dae.Tf. Time constants will not be used when evaluating the right-hand side specified in e_str but will be applied to the left-hand side.

check_initbool

True to check if the equation right-hand-side is zero initially. Disabling the checking can be used for integrators when the initial input may not be zero.

Attributes:
e_codestr

Equation code string, equals string literal f

v_codestr

Variable code string, equals string literal x

Examples

To implement the swing equation

\[M \dot {\omega} = \tau_m - \tau_e - D(\omega - 1)\]

Do the following in the __init__() of a model class:

self.omega = State(e_str = 'tm - te - D * (omega - 1)',
                   t_const = self.M,
                   ...
                   )

Note that self.M, the inertia parameter is given through t_const and is not part of e_str.

Algeb#

Algebraic variables satisfy instantaneous constraints and can be discontinuous. Use Algeb for power balance equations, output calculations, and constraints.

from andes.core.var import Algeb

class Generator(Model):
    def __init__(self):
        # Electrical power output
        self.Pe = Algeb(
            v_str='p0',
            e_str='vd*Id + vq*Iq - Pe',
            info='Electrical power',
            tex_name='P_e'
        )
class andes.core.var.Algeb(name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None, v_str: str | float | None = None, v_iter: str | None = None, e_str: str | None = None, discrete: Discrete | None = None, v_setter: bool | None = False, e_setter: bool | None = False, v_str_add: bool | None = False, addressable: bool | None = True, export: bool | None = True, diag_eps: float | None = 0.0, deps: List | None = None, is_output: bool | None = False)[source]

Algebraic variable class, an alias of andes.core.var.BaseVar.

Note that residual equations corresponding to algebraic variables are given in an implicit form.

Attributes:
e_codestr

Equation code string, equals string literal g

v_codestr

Variable code string, equals string literal y

Examples

When an algebraic variable y and the equation y = x + z shall be defined, use

e_str = 'x + z - y'

because it expresses the equation x + z - y = 0. It is a common mistake to use e_str = 'x + z', which will result in a singular Jacobian matrix because d(x + z) / d(y) is zero.

External Variables#

Some models have "public" variables that can be accessed by other models. For example, a Bus defines v for voltage magnitude. Each device attached to a particular bus needs to access the value and impose reactive power injection.

External variables link with existing variables from another model or group using ExtAlgeb or ExtState.

ExtAlgeb#

from andes.core.var import ExtAlgeb

class PQ(Model):
    def __init__(self):
        # Access bus voltage
        self.v = ExtAlgeb(
            src='v',              # Source variable name
            model='Bus',          # Source model
            indexer=self.bus,     # IdxParam for lookup
            e_str='-q',           # Inject reactive power
        )
class andes.core.var.ExtAlgeb(model: str, src: str, indexer: List | ndarray | BaseParam | BaseService | None = None, allow_none: bool | None = False, name: str | None = None, tex_name: str | None = None, ename: str | None = None, tex_ename: str | None = None, info: str | None = None, unit: str | None = None, v_str: str | float | None = None, v_iter: str | None = None, e_str: str | None = None, v_setter: bool | None = False, e_setter: bool | None = False, addressable: bool | None = True, export: bool | None = True, diag_eps: float | None = 0.0, is_input: bool | None = False)[source]

External algebraic variable type.

ExtState#

from andes.core.var import ExtState

class Exciter(Model):
    def __init__(self):
        # Access generator field voltage
        self.vf = ExtState(
            src='vf',
            model='GENROU',
            indexer=self.syn,
            e_str='Efd - vf0',
        )
class andes.core.var.ExtState(model: str, src: str, indexer: List | ndarray | BaseParam | BaseService | None = None, allow_none: bool | None = False, name: str | None = None, tex_name: str | None = None, ename: str | None = None, tex_ename: str | None = None, info: str | None = None, unit: str | None = None, v_str: str | float | None = None, v_iter: str | None = None, e_str: str | None = None, v_setter: bool | None = False, e_setter: bool | None = False, addressable: bool | None = True, export: bool | None = True, diag_eps: float | None = 0.0, is_input: bool | None = False)[source]

External state variable type.

Warning

ExtState is not allowed to set t_const, as it may conflict with the source State variable.

Only in rare cases should one set e_str for ExtState. The t_const of the source State variable is used.

Value and Equation Strings#

The most important feature of the symbolic framework is allowing equations to be defined using strings. There are three types of strings for a variable:

v_str: Explicit Initialization#

Equation string for explicit initialization in the form of v = v_str(x, y). The expression evaluates directly into the initial value.

self.omega = State(v_str='1.0')  # Start at 1.0 pu
self.Pe = Algeb(v_str='p0')      # Start at initial power

v_iter: Implicit Initialization#

Equation string for implicit initialization in the form of v_iter(x, y) = 0. All v_iter equations are solved numerically using the Newton-Krylov iterative method.

self.Efd = Algeb(v_iter='Vf - Efd')  # Solve: Vf - Efd = 0

e_str: Equation Definition#

Equation string for the differential or algebraic equation:

  • For State: right-hand side of \(\dot{x} = f(x, y)\)

  • For Algeb: residual that should equal zero, \(g(x, y) = 0\)

# Differential: d(omega)/dt = (Tm - Te) / M
self.omega = State(e_str='(Tm - Te) / M')

# Algebraic: 0 = P_gen - P_load
self.P = Algeb(e_str='Pgen - Pload')

Device Status: u vs ue#

Every model has a u parameter (own online status, from input data) and a ue service (effective status, accounting for parent devices). Use ue in e_str to ensure equations are zeroed out when the device or any of its ancestors (bus, generator, exciter, etc.) is offline:

# Correct: equation respects parent status
self.omega = State(e_str='ue * (Tm - Te - D*(omega-1)) / M')

# Wrong: ignores bus/parent offline status
self.omega = State(e_str='u * (Tm - Te - D*(omega-1)) / M')

ue is computed as the product of the device's own u and the effective status of all parents declared with status_parent=True (see Parameters). For example, a governor's ue is u * generator.ue * bus.ue.

Use u in v_str for initialization, since v_str is evaluated before status propagation runs.

Flags for Value Overwriting#

Variables have special flags for handling value initialization and equation values. This is only relevant for public or external variables.

v_setter Flag#

The v_setter flag indicates whether a particular variable instance sets the initial value. If v_setter=False (default), variable values of the same address are added. If v_setter=True, the variable will set the values in the DAE array to its value, overwriting any previous values.

Only one variable at the same address is allowed to have v_setter=True.

e_setter Flag#

The e_setter flag indicates whether the equation associated with a variable sets the equation value rather than adding to it.

A v_setter Example#

A Bus is allowed to default the initial voltage magnitude to 1 and the voltage phase angle to 0. If a PV device is connected to a Bus device, the PV should override the voltage initial value with its voltage set point.

In Bus.__init__():

self.v = Algeb(v_str='1')

In PV.__init__():

self.v0 = Param()
self.bus = IdxParam(model='Bus')

self.v = ExtAlgeb(src='v',
                  model='Bus',
                  indexer=self.bus,
                  v_str='v0',
                  v_setter=True)

An ExtAlgeb is defined to access Bus.v using indexer self.bus. The v_str sets the initial value to v0. During the variable initialization phase for PV, PV.v.v is set to v0.

During value collection into DAE.y by the System class, PV.v, as a final v_setter, will overwrite the voltage magnitude for Bus devices with the indices provided in PV.bus.

Observable#

Observable variables are computed by explicit assignment rather than solved in the DAE system. At code generation time, the expression is substituted into all referencing equations so that Jacobians are computed correctly. Post-solve, values are evaluated and stored in dae.b for recording.

Use Observable instead of Algeb when a variable is a pure explicit function of other variables and does not need to participate in the Newton-Raphson solve. This reduces the DAE system size without sacrificing Jacobian accuracy.

from andes.core.observable import Observable

class MyModel(Model):
    def __init__(self):
        self.vd = Observable(
            e_str='v * cos(delta - a)',
            info='d-axis voltage',
            tex_name='V_d',
        )

Note

Observable variables cannot be referenced via ExtAlgeb. If another model needs to link to the variable externally, keep it as Algeb.

class andes.core.observable.Observable(e_str: str | None = None, name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None, discrete=None)[source]

A variable computed by explicit assignment rather than solved in the DAE system.

At code generation time, the Observable's expression is substituted into all referencing equations so that Jacobians are correctly computed via direct differentiation. Post-solve, values are evaluated and stored in dae.b for recording.

Parameters:
e_strstr, optional

Assignment expression string. Unlike Algeb where e_str means residual (0 = e_str), for Observable it means direct assignment (b = e_str).

Attributes:
e_codeNone

Observable has no equation residual.

v_codestr

Variable code string, equals string literal b.

Examples

To define an observable vd computed from bus voltage and angle:

self.vd = Observable(e_str='v * cos(delta - a)',
                     info='d-axis voltage',
                     tex_name='V_d')

The symbol vd can then be used in other equations. At code generation time, vd will be replaced by v * cos(delta - a) in those equations.

reset()[source]

Reset the internal numpy arrays and flags.

set_address(addr: ndarray, contiguous=False)[source]

Set the address of this Observable in dae.b.

Overrides BaseVar to ensure e_inplace is always False (no equation array for Observable).

set_arrays(dae, inplace=True, alloc=True)[source]

Set the value array for this Observable.

Only sets self.v (into dae.b). No equation array is allocated since Observable has no residual.

Alias Variables#

Create references to existing variables for convenience:

from andes.core.var import AliasState, AliasAlgeb

# Reference generator speed
self.omega = AliasState(self.GENROU.omega)
class andes.core.var.AliasState(var, **kwargs)[source]

Alias state variable.

Refer to the docs of AliasAlgeb.

class andes.core.var.AliasAlgeb(var, **kwargs)[source]

Alias algebraic variable. Essentially ExtAlgeb that links to a a model's own variable.

AliasAlgeb is useful when the final output of a model is from a block, but the model must provide the final output in a pre-defined name. Using AliasAlgeb, A model can avoid adding an additional variable with a dummy equations.

Like ExtVar, labels of AliasAlgeb will not be saved in the final output. When plotting from file, one need to look up the original variable name.

BaseVar#

The abstract base class defining the variable interface.

class andes.core.var.BaseVar(name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None, v_str: str | float | None = None, v_iter: str | None = None, e_str: str | None = None, discrete: Discrete | None = None, v_setter: bool | None = False, e_setter: bool | None = False, v_str_add: bool | None = False, addressable: bool | None = True, export: bool | None = True, diag_eps: float | None = 0.0, deps: List | None = None, is_output: bool | None = False)[source]

Base variable class.

Derived classes State and Algeb should be used to build model variables.

Parameters:
infostr, optional

Descriptive information

unitstr, optional

Unit

tex_namestr

LaTeX-formatted variable symbol. If is None, the value of name will be used.

discreteDiscrete

Discrete component on which this variable depends. ANDES will call check_var() of the discrete component before initializing this variable.

namestr, optional

Variable name. One should typically assigning the name directly because it will be automatically assigned by the model. The value of name will be the symbol name to be used in expressions.

Attributes:
aarray-like

variable address

varray-like

local-storage of the variable value

earray-like

local-storage of the corresponding equation value

e_strstr

the string/symbolic representation of the equation

v_strstr

explicit initialization equation

v_str_addbool

True if the value of v_str will be added to the variable. Useful when other models access this variable and set part of the initial value

v_iterstr

implicit iterative equation in the form of 0 = v_iter

reset()[source]

Reset the internal numpy arrays and flags.

set_address(addr: ndarray, contiguous=False)[source]

Set the address of internal variables.

Parameters:
addrnp.ndarray

The assigned address for this variable

contiguousbool, optional

If the addresses are contiguous

set_arrays(dae, inplace=True, alloc=True)[source]

Set the equation and values arrays.

Parameters:
daeDAE

Reference to System.dae

Common Patterns#

Power Balance#

# Algebraic equation: 0 = P_gen - P_load - P_line
self.P = Algeb(e_str='Pgen - Pload - Pline')

Swing Equation#

self.delta = State(e_str='omega - 1')
self.omega = State(e_str='(Tm - Te - D*(omega-1)) / M')

External Power Injection#

# Inject power to bus (negative = generation/injection)
self.a = ExtAlgeb(src='a', model='Bus', indexer=self.bus,
                  e_str='-p')
self.v = ExtAlgeb(src='v', model='Bus', indexer=self.bus,
                  e_str='-q')

See Also#