Services#

Services are helper variables outside the DAE variable list. Services are most often used for storing intermediate constants but can be used for special operations to work around restrictions in the symbolic framework. Services are value providers, meaning each service has an attribute v for storing service values. The base class of services is :py:mod`BaseService`, and the supported services are listed as follows.

BaseService([name, tex_name, unit, info, vtype])

Base class for Service.

OperationService([name, tex_name, info])

Base class for a type of Service which performs specific operations.

Class

Description

ConstService

Internal service for constant values.

VarService

Variable service updated at each iteration before equations.

ExtService

External service for retrieving values from value providers.

PostInitService

Constant service evaluated after TDS initialization

NumReduce

The service type for reducing linear 2-D arrays into 1-D arrays

NumRepeat

The service type for repeating a 1-D array to linear 2-D arrays

IdxRepeat

The service type for repeating a 1-D list to linear 2-D list

EventFlag

Service type for flagging changes in inputs as an event

VarHold

Hold input value when a hold signal is active

ExtendedEvent

Extend an event signal for a given period of time

DataSelect

Select optional str data if provided or use the fallback

NumSelect

Select optional numerical data if provided

DeviceFinder

Finds or creates devices linked to the given devices

BackRef

Collects idx-es for the backward references

RefFlatten

Converts BackRef list of lists into a 1-D list

InitChecker

Checks initial values against typical values

FlagValue

Flags values that equals the given value

Replace

Replace values that returns True for the given lambda func

Internal Constants#

The most commonly used service is ConstService. It is used to store an array of constants, whose value is evaluated from a provided symbolic string. They are only evaluated once in the model initialization phase, ahead of variable initialization. ConstService comes handy when one wants to calculate intermediate constants from parameters.

For example, a turbine governor has a NumParam R for the droop. ConstService allows to calculate the inverse of the droop, the gain, and use it in equations. The snippet from a turbine governor's __init__() may look like

self.R = NumParam()
self.G = ConstService(v_str='u/R')

where u is the online status parameter. The model can thus use G in subsequent variable or equation strings.

class andes.core.service.ConstService(v_str: str | None = None, v_numeric: Callable | None = None, vtype: type | None = None, name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None)[source]

A type of Service that stays constant once initialized.

ConstService are usually constants calculated from parameters. They are only evaluated once in the initialization phase before variables are initialized. Therefore, uninitialized variables must not be used in v_str`.

ConstService are evaluated in sequence after getting external variables and parameters and before initializing internal variables.

Parameters:
namestr

Name of the ConstService

v_strstr

An equation string to calculate the variable value.

v_numericCallable, optional

A callable which returns the value of the ConstService

v_type: type, optional, default to float

Type of element in the value array in float or complex

Attributes:
varray-like or a scalar

ConstService value

class andes.core.service.VarService(v_str: str | None = None, v_numeric: Callable | None = None, vtype: type | None = None, name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None, sequential: bool | None = True)[source]

Variable service that gets updated in each step/iteration before computing the residual equations. As a results, variable values from the k-th step are used to compute a VarService that will be used to compute the residual for the (k+1)-th step.

This class is useful when one has non-differentiable algebraic equations, which make use of abs(), re and im. Instead of creating Algeb, one can put the equation in VarService, which will be updated before solving algebraic equations.

Parameters:
sequentialbool, optional, default to True

True if this VarService depends on previously defined VarService and should be evaluated in sequence. False if this VarService only uses known variables.

Warning

VarService is not solved with other algebraic equations, meaning that there is one step "delay" between the algebraic variables and VarService. Use an algebraic variable whenever possible.

Examples

In ESST3A model, the voltage and current sensors (vd + jvq), (Id + jIq) estimate the sensed VE using equation

\[VE = | K_{PC}*(v_d + 1j v_q) + 1j (K_I + K_{PC}*X_L)*(I_d + 1j I_q)|\]

One can use VarService to implement this equation

self.VE = VarService(
    tex_name='V_E', info='VE', v_str='Abs(KPC*(vd + 1j*vq) + 1j*(KI +
    KPC*XL)*(Id + 1j*Iq))', )
class andes.core.service.PostInitService(v_str: str | None = None, v_numeric: Callable | None = None, vtype: type | None = None, name: str | None = None, tex_name: str | None = None, info: str | None = None, unit: str | None = None)[source]

Constant service that gets stored once after init.

This service is useful when one need to store initialization values stored in variables.

Examples

In ESST3A model, the vf variable is initialized followed by other variables. One can store the initial vf into vf0 so that equation vf - vf0 = 0 will hold.

self.vref0 = PostInitService(info='Initial reference voltage input',
                             tex_name='V_{ref0}', v_str='vref', )

Since all ConstService are evaluated before equation evaluation, without using PostInitService, one will need to create lots of ConstService to store values in the initialization path towards vf0, in order to correctly initialize vf.

External Constants#

Service constants whose value is retrieved from an external model or group. Using ExtService is similar to using external variables. The values of ExtService will be retrieved once during the initialization phase before ConstService evaluation.

For example, a synchronous generator needs to retrieve the p and q values from static generators for initialization. ExtService is used for this purpose. In the __init__() of a synchronous generator model, one can define the following to retrieve StaticGen.p as p0:

self.p0 = ExtService(src='p',
                     model='StaticGen',
                     indexer=self.gen,
                     tex_name='P_0')
class andes.core.service.ExtService(model: str, src: str, indexer: BaseParam | BaseService, attr: str = 'v', allow_none: bool = False, default=0, name: str = None, tex_name: str = None, vtype=None, info: str = None)[source]

Service constants whose value is from an external model or group.

Parameters:
srcstr

Variable or parameter name in the source model or group

modelstr

A model name or a group name

indexerIdxParam or BaseParam

An "Indexer" instance whose v field contains the idx of devices in the model or group.

Examples

A synchronous generator needs to retrieve the p and q values from static generators for initialization. ExtService is used for this purpose.

In a synchronous generator, one can define the following to retrieve StaticGen.p as p0:

class GENCLSModel(Model):
    def __init__(...):
        ...
        self.p0 = ExtService(src='p',
                             model='StaticGen',
                             indexer=self.gen,
                             tex_name='P_0')

Shape Manipulators#

This section is for advanced model developer.

All generated equations operate on 1-dimensional arrays and can use algebraic calculations only. In some cases, one model would use BackRef to retrieve 2-dimensional indices and will use such indices to retrieve variable addresses. The retrieved addresses usually has a different length of the referencing model and cannot be used directly for calculation. Shape manipulator services can be used in such case.

NumReduce is a helper Service type which reduces a linearly stored 2-D ExtParam into 1-D Service. NumRepeat is a helper Service type which repeats a 1-D value into linearly stored 2-D value based on the shape from a BackRef.

class andes.core.service.BackRef(**kwargs)[source]

A special type of reference collector.

BackRef is used for collecting device indices of other models referencing the parent model of the BackRef. The v``field will be a list of lists, each containing the `idx of other models referencing each device of the parent model.

BackRef can be passed as indexer for params and vars, or shape for NumReduce and NumRepeat. See examples for illustration.

See also

andes.core.service.NumReduce

A more complete example using BackRef to build the COI model

Examples

A Bus device has an IdxParam of area, storing the idx of area to which the bus device belongs. In Bus.__init__(), one has

self.area = IdxParam(model='Area')

Suppose Bus has the following data

idx

area

Vn

1

1

110

2

2

220

3

1

345

4

1

500

The Area model wants to collect the indices of Bus devices which points to the corresponding Area device. In Area.__init__, one defines

self.Bus = BackRef()

where the member attribute name Bus needs to match exactly model name that Area wants to collect idx for. Similarly, one can define self.ACTopology = BackRef() to collect devices in the ACTopology group that references Area.

The collection of idx happens in andes.system.System._collect_ref_param(). It has to be noted that the specific Area entry must exist to collect model idx-dx referencing it. For example, if Area has the following data

idx 1

Then, only Bus 1, 3, and 4 will be collected into self.Bus.v, namely, self.Bus.v == [ [1, 3, 4] ].

If Area has data

idx 1 2

Then, self.Bus.v will end up with [ [1, 3, 4], [2] ].

class andes.core.service.NumReduce(u, ref: BackRef, fun: Callable, name=None, tex_name=None, info=None, cache=True)[source]

A helper Service type which reduces a linearly stored 2-D ExtParam into 1-D Service.

NumReduce works with ExtParam whose v field is a list of lists. A reduce function which takes an array-like and returns a scalar need to be supplied. NumReduce calls the reduce function on each of the lists and return all the scalars in an array.

Parameters:
uExtParam

Input ExtParam whose v contains linearly stored 2-dimensional values

refBackRef

The BackRef whose 2-dimensional shapes are used for indexing

funCallable

The callable for converting a 1-D array-like to a scalar

Examples

Suppose one wants to calculate the mean value of the Vn in one Area. In the Area class, one defines

class AreaModel(...):
    def __init__(...):
        ...
        # backward reference from `Bus`
        self.Bus = BackRef()

        # collect the Vn in an 1-D array
        self.Vn = ExtParam(model='Bus',
                           src='Vn',
                           indexer=self.Bus)

        self.Vn_mean = NumReduce(u=self.Vn,
                                 fun=np.mean,
                                 ref=self.Bus)

Suppose we define two areas, 1 and 2, the Bus data looks like

idx

area

Vn

1

1

110

2

2

220

3

1

345

4

1

500

Then, self.Bus.v is a list of two lists [ [1, 3, 4], [2] ]. self.Vn.v will be retrieved and linearly stored as [110, 345, 500, 220]. Based on the shape from self.Bus, numpy.mean() will be called on [110, 345, 500] and [220] respectively. Thus, self.Vn_mean.v will become [318.33, 220].

class andes.core.service.NumRepeat(u, ref, **kwargs)[source]

A helper Service type which repeats a v-provider's value based on the shape from a BackRef

Examples

NumRepeat was originally designed for computing the inertia-weighted average rotor speed (center of inertia speed). COI speed is computed with

\[\omega_{COI} = \frac{ \sum{M_i * \omega_i} } {\sum{M_i}}\]

The numerator can be calculated with a mix of BackRef, ExtParam and ExtState. The denominator needs to be calculated with NumReduce and Service Repeat. That is, use NumReduce to calculate the sum, and use NumRepeat to repeat the summed value for each device.

In the COI class, one would have

class COIModel(...):
    def __init__(...):
        ...
        self.SynGen = BackRef()
        self.SynGenIdx = RefFlatten(ref=self.SynGen)
        self.M = ExtParam(model='SynGen',
                          src='M',
                          indexer=self.SynGenIdx)

        self.wgen = ExtState(model='SynGen',
                             src='omega',
                             indexer=self.SynGenIdx)

        self.Mt = NumReduce(u=self.M,
                                 fun=np.sum,
                                 ref=self.SynGen)

        self.Mtr = NumRepeat(u=self.Mt,
                               ref=self.SynGen)

        self.pidx = IdxRepeat(u=self.idx,ref=self.SynGen)

Finally, one would define the center of inertia speed as

self.wcoi = Algeb(v_str='1', e_str='-wcoi')

self.wcoi_sub = ExtAlgeb(model='COI',
                         src='wcoi',
                         e_str='M * wgen / Mtr',
                         v_str='M / Mtr',
                         indexer=self.pidx,
                         )

It is very worth noting that the implementation uses a trick to separate the average weighted sum into n sub-equations, each calculating the \((M_i * \omega_i) / (\sum{M_i})\). Since all the variables are preserved in the sub-equation, the derivatives can be calculated correctly.

class andes.core.service.IdxRepeat(u, ref, **kwargs)[source]

Helper class to repeat IdxParam.

This class has the same functionality as andes.core.service.NumRepeat but only operates on IdxParam, DataParam or NumParam.

class andes.core.service.RefFlatten(ref, **kwargs)[source]

A service type for flattening andes.core.service.BackRef into a 1-D list.

Examples

This class is used when one wants to pass BackRef values as indexer.

andes.models.coi.COI collects referencing andes.models.group.SynGen with

self.SynGen = BackRef(info='SynGen idx lists', export=False)

After collecting BackRefs, self.SynGen.v will become a two-level list of indices, where the first level correspond to each COI and the second level correspond to generators of the COI.

Convert self.SynGen into 1-d as self.SynGenIdx, which can be passed as indexer for retrieving other parameters and variables

self.SynGenIdx = RefFlatten(ref=self.SynGen)

self.M = ExtParam(model='SynGen', src='M',
                  indexer=self.SynGenIdx, export=False,
                  )

Value Manipulation#

class andes.core.service.Replace(old_val, flt, new_val, name=None, tex_name=None, info=None, cache=True)[source]

Replace parameters with new values if the function returns True

class andes.core.service.ParamCalc(param1, param2, func, name=None, tex_name=None, info=None, cache=True)[source]

Parameter calculation service.

Useful to create parameters calculated instantly from existing ones.

Idx and References#

class andes.core.service.DeviceFinder(u, link, idx_name: str, default_model: str, auto_find: bool | None = True, auto_add: bool | None = True, name: str | None = None, tex_name: str | None = None, info: str | None = None)[source]

Service for finding idx of devices which are linked to the given devices.

The auto_find parameter controls if the device idx should be automatically looked up. The auto_add parameter controls if the device will be automatically added. The two parameters are not exclusive. One can skip finding the device but automatically adding it.

If auto_find is True and the idx is None, DeviceFinder will look up for the device. If not found and auto_add is True, DevFinder will then automatically add the devices. The idx of the devices that are found or added will be stored to the DeviceFinder instance, so that DeviceFinder can be used like any IdxParam.

Adding new devices are called at the beginning of andes.system.System.setup().

Examples

The IEEEST stabilizer takes an optional parameter busf of the type IdxParam for specifying the connected bus frequency measurement device, which is needed for mode 6. To avoid reimplementing BusFreq within IEEEST, one can do

self.busfreq = DeviceFinder(self.busf,
                            link=self.buss, idx_name='bus',
                            default_model='BusFreq')

where self.busf is for the optional parameter for the idx of bus frequency estimation devices (e.g., BusFreq), self.buss is for the idx of buses that self.busf devices should measure, and idx_name is the name of the BusFreq parameter through which the indices of measured buses are given.

For each None or invalid values in self.busf, a BusFreq device will be created with its bus set to the corresponding value in self.buss. That is, BusFreq.[idx_name].v = [link].

At the end, the DeviceFinder instance will contain the list of BusFreq that are are connected to self.buss, respectively.

In the case of any valid value in self.busf, that is, the value is an existing BusFreq device, DeviceFinder will return it as is without checking if the BusFreq device actually measures the bus specified by self.buss. It allows to use the measurement at a different location, but the user have to perform the data consistency check.

class andes.core.service.BackRef(**kwargs)[source]

A special type of reference collector.

BackRef is used for collecting device indices of other models referencing the parent model of the BackRef. The v``field will be a list of lists, each containing the `idx of other models referencing each device of the parent model.

BackRef can be passed as indexer for params and vars, or shape for NumReduce and NumRepeat. See examples for illustration.

See also

andes.core.service.NumReduce

A more complete example using BackRef to build the COI model

Examples

A Bus device has an IdxParam of area, storing the idx of area to which the bus device belongs. In Bus.__init__(), one has

self.area = IdxParam(model='Area')

Suppose Bus has the following data

idx

area

Vn

1

1

110

2

2

220

3

1

345

4

1

500

The Area model wants to collect the indices of Bus devices which points to the corresponding Area device. In Area.__init__, one defines

self.Bus = BackRef()

where the member attribute name Bus needs to match exactly model name that Area wants to collect idx for. Similarly, one can define self.ACTopology = BackRef() to collect devices in the ACTopology group that references Area.

The collection of idx happens in andes.system.System._collect_ref_param(). It has to be noted that the specific Area entry must exist to collect model idx-dx referencing it. For example, if Area has the following data

idx 1

Then, only Bus 1, 3, and 4 will be collected into self.Bus.v, namely, self.Bus.v == [ [1, 3, 4] ].

If Area has data

idx 1 2

Then, self.Bus.v will end up with [ [1, 3, 4], [2] ].

class andes.core.service.RefFlatten(ref, **kwargs)[source]

A service type for flattening andes.core.service.BackRef into a 1-D list.

Examples

This class is used when one wants to pass BackRef values as indexer.

andes.models.coi.COI collects referencing andes.models.group.SynGen with

self.SynGen = BackRef(info='SynGen idx lists', export=False)

After collecting BackRefs, self.SynGen.v will become a two-level list of indices, where the first level correspond to each COI and the second level correspond to generators of the COI.

Convert self.SynGen into 1-d as self.SynGenIdx, which can be passed as indexer for retrieving other parameters and variables

self.SynGenIdx = RefFlatten(ref=self.SynGen)

self.M = ExtParam(model='SynGen', src='M',
                  indexer=self.SynGenIdx, export=False,
                  )

Events#

class andes.core.service.EventFlag(u, vtype: type | None = None, name: str | None = None, tex_name=None, info=None)[source]

Service to flag events when the input value changes. The typical input is a v-provider with binary values.

Implemented by providing self.check(**kwargs) as v_numeric. EventFlag.v stores the values of the input variable in the most recent iteration/step.

After the evaluation of self.check(), self.v will be updated.

class andes.core.service.ExtendedEvent(u, t_ext: int | float | BaseParam | BaseService = 0.0, trig: str = 'rise', enable=True, v_disabled=0, extend_only=False, vtype: type | None = None, name: str | None = None, tex_name=None, info=None)[source]

Service for indicating an event for an extended, predefined period of time following the event disappearance.

The triggering of an event, whether the rise or fall edge, is specified through trig. For example, if trig = rise, the change of the input from 0 to 1 will be considered as an input, whereas the subsequent change back to 0 will be considered as the event end.

ExtendedEvent.v stores the flags whether the extended time has completed. Outputs will become 1 once the event starts and return to 0 when the extended time ends.

Parameters:
uv-provider

Triggering signal where the values are 0 or 1.

trigstr in ("rise", "fall")

Triggering edge for the beginning of an event. rise by default.

enablebool or v-provider

If disabled, the output will be v_disabled

extend_onlybool

Only output during the extended period, not the event period.

Warning

The performance of this class needs to be optimized.

class andes.core.service.VarHold(u, hold, vtype=None, name=None, tex_name=None, info=None)[source]

Service for holding the input when the hold signal is on.

Parameters:
holdv-provider, binary

Hold signal array with length equal to the input. For elements that are 1, the corresponding inputs are held until the hold signal returns to 0.

Flags#

class andes.core.service.FlagCondition(u, func, flag=1, name=None, tex_name=None, info=None, cache=True)[source]

Class for flagging values based on a condition function.

By default, values whose condition function output equal that equal to True/1 will be flagged as 1. 0 otherwise.

Parameters:
u

Input parameter

func

A condition function that returns True or False.

flag1 by default, only 0 or 1 is accepted.

The flag for the inputs whose condition output is True.

Warning

This class is not ready.

FlagCondition can only be applied to BaseParam with cache=True. Applying to Service will fail unless cache is False (at a performance cost).

class andes.core.service.FlagGreaterThan(u, value=0.0, flag=1, equal=False, name=None, tex_name=None, info=None, cache=True)[source]

Service for flagging parameters > or >= the given value element-wise.

Parameters that satisfy the comparison (u > or >= value) will flagged as flag (1 by default).

class andes.core.service.FlagLessThan(u, value=0.0, flag=1, equal=False, name=None, tex_name=None, info=None, cache=True)[source]

Service for flagging parameters < or <= the given value element-wise.

Parameters that satisfy the comparison (u < or <= value) will flagged as flag (1 by default).

class andes.core.service.FlagValue(u, value, flag=0, name=None, tex_name=None, info=None, cache=True)[source]

Class for flagging values that equal to the given value.

By default, values that equal to value will be flagged as 0. Non-matching values will be flagged as 1.

Parameters:
u

Input parameter

value

Value to flag. Can be None, string, or a number.

flag0 by default, only 0 or 1 is accepted.

The flag for the matched ones

Warning

FlagNotNone can only be applied to BaseParam with cache=True. Applying to Service will fail unless cache is False (at a performance cost).

Data Select#

class andes.core.service.DataSelect(optional, fallback, name: str | None = None, tex_name: str | None = None, info: str | None = None)[source]

Class for selecting values for optional DataParam or NumParam.

This service is a v-provider that uses optional DataParam when available. Otherwise, use the fallback value.

DataParam will be tested for None, and NumParam will be tested with np.isnan().

Notes

An use case of DataSelect is remote bus. One can do

self.buss = DataSelect(option=self.busr, fallback=self.bus)

Then, pass self.buss instead of self.bus as indexer to retrieve voltages.

Another use case is to allow an optional turbine rating. One can do

self.Tn = NumParam(default=None)
self.Sg = ExtParam(...)
self.Sn = DataSelect(Tn, Sg)
class andes.core.service.NumSelect(optional, fallback, name: str | None = None, tex_name: str | None = None, info: str | None = None, ignore_cond: ~typing.Callable | None = functools.partial(<ufunc 'equal'>, 0))[source]

Class for selecting values for optional NumParam.

NumSelect works with internal and external parameters.

Any values equal to np.nan will always be ignored. If one needs to ignore values based on additional conditions, pass it through ignore_cond. For example, to ignore zero values, use ignore_cond = partial(np.equal, 0).

Examples

One use case is to allow an optional turbine rating. One can do

self.Tn = NumParam(default=None) self.Sg = ExtParam(...) self.Sn =
DataSelect(Tn, Sg)

Miscellaneous#

class andes.core.service.InitChecker(u, lower=None, upper=None, equal=None, not_equal=None, enable=True, error_out=False, **kwargs)[source]

Class for checking init values against known typical values.

Instances will be stored in Model.services_post and Model.services_icheck, which will be checked in Model.post_init_check() after initialization.

Parameters:
u

v-provider to be checked

lowerfloat, BaseParam, BaseVar, BaseService

lower bound

upperfloat, BaseParam, BaseVar, BaseService

upper bound

equalfloat, BaseParam, BaseVar, BaseService

values that the value from v_str should equal

not_equalfloat, BaseParam, BaseVar, BaseService

values that should not equal

enablebool

True to enable checking

Examples

Let's say generator excitation voltages are known to be in the range of 1.6 - 3.0 per unit. One can add the following instance to GENBase

self._vfc = InitChecker(u=self.vf,
                        info='vf range',
                        lower=1.8,
                        upper=3.0,
                        )

lower and upper can also take v-providers instead of float values.

One can also pass float values from Config to make it adjustable as in our implementation of GENBase._vfc.

class andes.core.service.ApplyFunc(u, func, name=None, tex_name=None, info=None, cache=True)[source]

Class for applying a numerical function on a parameter..

Parameters:
u

Input parameter

func

A condition function that returns True or False.

Warning

This class is not ready.

class andes.core.service.CurrentSign(bus, bus1, bus2, name=None, tex_name=None, info=None)[source]

Service for computing the sign of the current flowing through a series device.

With a given line connecting bus1 and bus2, one can compute the current flow using (v1*exp(1j*a1) - v2*exp(1j*a2)) / (r + jx) whose value is the outflow on bus1.

CurrentSign can be used to compute the sign to be multiplied depending on the observing bus. For each value in bus, the sign will be +1 if it appears in bus1 or -1 otherwise.

bus1          bus2
 *------>>-----*
bus(+)        bus(-)
class andes.core.service.RandomService(func=<built-in method rand of numpy.random.mtrand.RandomState object>, **kwargs)[source]

A service type for generating random numbers.

Parameters:
namestr

Name

funcCallable

A callable for generating the random variable.

Warning

The value will be randomized every time it is accessed. Do not use it if the value needs to be stable for each simulation step.

class andes.core.service.SwBlock(*, init, ns, blocks, ext_sel=None, name=None, tex_name=None, info=None)[source]

Service type for switched shunt blocks.