Routine#
Routine refers to scheduling-level model, and it includes two sectinos, namely, Data Section and Model Section.
Data Section#
A simplified code snippet for RTED is shown below as an example.
class RTED:
def __init__(self):
... ...
self.R10 = RParam(info='10-min ramp rate',
name='R10', tex_name=r'R_{10}',
model='StaticGen', src='R10',
unit='p.u./h',)
self.gs = ZonalSum(u=self.zg, zone='Zone',
name='gs', tex_name=r'S_{g}',
info='Sum Gen vars vector in shape of zone',
no_parse=True, sparse=True)
... ...
self.rbu = Constraint(name='rbu',
info='RegUp reserve balance',
e_str = 'gs @ cp.multiply(ug, pru) - dud == 0')
... ...
Routine Parameter#
As discussed in previous section, actual data parameters are stored in the device-level models.
Thus, in routines, parameters are retrieved from target devices given the device
name and the parameter name.
In the example above, R10 is a 10-min ramp rate parameter for the static generator.
The parameter is retrieved from the devices StaticGen with the parameter name R10.
Service#
Services are developed to assit the formulations.
In the example above, ZonalSum is a service to sum the generator variables in a zone.
Later, in the constraint, gs is multiplied to the reserve variable pru.
Model Section#
Descriptive Formulation#
Scheduling routine is the descriptive model of the optimization problem.
Further, to facilitate the routine definition, AMS developed a class
ams.core.param.RParam to pass the model data to multiple routine modeling.
- class ams.core.param.RParam(name: str | None = None, tex_name: str | None = None, info: str | None = None, src: str | None = None, unit: str | None = None, model: str | None = None, v: ndarray | None = None, indexer: str | None = None, imodel: str | None = None, horizon: RParam | None = None, hindexer: str | None = None, expand_dims: int | None = None, no_parse: bool | None = False, nonneg: bool | None = False, nonpos: bool | None = False, cplx: bool | None = False, imag: bool | None = False, symmetric: bool | None = False, diag: bool | None = False, hermitian: bool | None = False, boolean: bool | None = False, integer: bool | None = False, pos: bool | None = False, neg: bool | None = False, sparse: list | None = None)[source]
Class for parameters used in a routine. This class is developed to simplify the routine definition.
RParm is further used to define Parameter in the optimization model.
no_parse is used to skip parsing the RParam in optimization model. It means that the RParam will not be added to the optimization model. This is useful when the RParam contains non-numeric values, or it is not necessary to be added to the optimization model.
- Parameters:
- namestr, optional
Name of this parameter. If not provided, name will be set to the attribute name.
- tex_namestr, optional
LaTeX-formatted parameter name. If not provided, tex_name will be assigned the same as name.
- infostr, optional
A description of this parameter
- srcstr, optional
Source name of the parameter.
- unitstr, optional
Unit of the parameter.
- modelstr, optional
Name of the owner model or group.
- vnp.ndarray, optional
External value of the parameter.
- indexerstr, optional
Primary-axis indexer of the parameter — name of an
IdxParamon the row-owner model whose values get matched againstimodel.get_all_idxes()to sort / position rows.- imodelstr, optional
Name of the owner model or group of the (primary) indexer.
- horizonams.core.param.RParam, optional
Secondary-axis indexer. Mirrors the
ams.opt.var.Var.horizonconvention used for output Vars: when set together withindexer/imodel, the param'svreturns a 2D matrix shaped(imodel.n, horizon.n)built by pivoting the long-format rows onhindexer(secondary) andindexer(primary). Cells with no matching row fall back to the sourceNumParam.default.- hindexerstr, optional
Name of the
IdxParamon the row-owner model that carries the secondary key, matched againsthorizon.v. Required whenhorizonis set; ignored otherwise.- no_parse: bool, optional
True to skip parsing the parameter.
- nonneg: bool, optional
True to set the parameter as non-negative.
- nonpos: bool, optional
True to set the parameter as non-positive.
- cplx: bool, optional
True to set the parameter as complex.
- imag: bool, optional
True to set the parameter as imaginary.
- symmetric: bool, optional
True to set the parameter as symmetric.
- diag: bool, optional
True to set the parameter as diagonal.
- hermitian: bool, optional
True to set the parameter as hermitian.
- boolean: bool, optional
True to set the parameter as boolean.
- integer: bool, optional
True to set the parameter as integer.
- pos: bool, optional
True to set the parameter as positive.
- neg: bool, optional
True to set the parameter as negative.
- sparse: bool, optional
True to set the parameter as sparse.
Examples
Example 1: Define a routine parameter from a source model or group.
In this example, we define the parameter cru from the source model SFRCost with the parameter cru. Note since this parameter comes from model SFRCost, but it is used to multiply on generator output powers, we need to ensure the value is sorted in the same order as generators. gen is the indexer that comes from model SFR itself, and imodel is the indexer model, i.e., the model that has idx as its attribute. Then, we can ensure the value of cru is sorted in the same order as the indexer StaticGen.
>>> self.cru = RParam(info='RegUp reserve coefficient', >>> tex_name=r'c_{r,u}', >>> unit=r'$/(p.u.)', >>> name='cru', >>> src='cru', >>> model='SFRCost', >>> indexer='gen', >>> imodel='StaticGen', >>> )
Example 2: Define a routine parameter with a user-defined value.
In this example, we define the parameter with a user-defined value. TODO: Add example
Numerical Optimization#
Optimization model is the optimization problem. Var, Constraint, and
Objective are the basic building blocks of the optimization model. OModel
is the container of the optimization model.
A summary table is shown below.
|
Base class for variables used in a routine. |
|
Base class for constraints. |
|
Base class for objective functions. |
|
Base class for optimization models. |
Expression Notation in e_str#
Routine constraints, objectives, and expressions are written as Python source
strings in the e_str argument. e_str uses canonical CVXPY syntax:
call cp.multiply(a, b), cp.sum(x), cp.power(x, n) etc. directly.
Before evaluation, ams.core.symprocessor.SymProcessor only
resolves bare symbol names (pg, Cft, rate_a, …) to the
underlying CVXPY Variable / Parameter / sparse-matrix objects of
the host routine. It does not rewrite function names: cp.* calls
must be written explicitly.
Note
Prior to v1.2.3, AMS rewrote mul(...) → cp.multiply(...),
bare sum(...) → cp.sum(...), and a dot b → a * b
automatically. That rewrite layer has been removed — see the v1.2.3
migration table in Release notes. User customizations
(addConstrs(e_str=...) / obj.e_str += '...') that still use
the old vocabulary will raise NameError at eval time.
Multiplication is the easiest place to introduce silent bugs because there are three semantically distinct operations that all read like "multiply":
Notation in |
Meaning |
When to use |
|---|---|---|
|
Matrix / matrix-vector multiply |
Topology matrix times a stacked vector, e.g. |
|
Element-wise multiply |
Per-device scaling, e.g. |
|
Scalar multiply |
A literal number or a 0-D parameter scaling an expression.
CVXPY accepts |
Bare ``a * b`` between two identifiers is not rewritten and will be passed
to CVXPY as-is. CVXPY will then either raise a shape error or, worse,
emit a deprecation warning and silently perform matrix multiplication. Always
write @ or cp.multiply() explicitly.
Other canonical-CVXPY constructs in e_str:
Reductions and atoms —
cp.sum(x),cp.norm(x),cp.pos(x),cp.square(x),cp.power(x, n),cp.vstack(...),cp.hstack(...),cp.maximum(...),cp.minimum(...),cp.quad_form(...),cp.sum_squares(...),cp.diag(...)— call directly.Comparisons
... == 0/... <= 0/... >= 0are embedded in thee_stritself; the LHS-zero authoring convention keepsConstraint.vreporting slack-from-zero uniformly. See Migration: canonical CVXPY in e_str (v1.2.3) for the v1.2.3is_eqretirement.Powers use
**(e.g.vmax**2).
Warning
Routine symbol names (Var, RParam, Service,
Expression, Constraint) may not collide with CVXPY atom
names (sum, multiply, vstack, hstack, power,
norm, pos, neg, square, quad_form,
sum_squares, diag, maximum, minimum, abs,
exp, log, sqrt, inv_pos). The codegen raises an
explanatory error on collision. Without this guard, a routine that
declared a symbol named sum would have its eval-fallback path
silently rewrite a user-appended sum(...) into
self.om.sum(...).
If in doubt, check an existing routine such as ams.routines.dcopf
or ams.routines.rted for canonical patterns.
How e_str becomes a CVXPY problem (codegen)#
The e_str rewrites above are applied once at prep time, not on
every routine init. The first time a routine is instantiated, AMS:
Walks the constructed routine's
constrs/exprs/exprcs/objregistries.Resolves bare symbol names in each
e_str(pg→r.pg, …) via the same_build_symbol_regex/_collect_symbol_namespass used by the eval-fallback helper. Function names (cp.sum,cp.multiply, …) are passed through untouched.Emits a small Python module at
~/.ams/pycode/<routine>.pywith one named callable per opt element — e.g.def _constr_pglb(r): return -r.pg + r.pmine— plus the pre-rendered LaTeX string for documentation.Wires those callables onto the routine's
Constraint/Expression/Objectiveinstances via theire_fnattribute.
Subsequent inits skip step 2-3 and just import. The cache is keyed by
md5 of the routine source file; editing an e_str regenerates
automatically. The whole cache can be refreshed or wiped via
ams prep (see Release notes v1.2.2).
This is analogous to ANDES's andes prep / ~/.andes/pycode/
pipeline. Author-facing API is unchanged — you keep writing
e_str. The codegen is what runs underneath.
Authors who prefer to skip the DSL and write a callable directly may
pass e_fn=callable instead of e_str; the runtime accepts both,
and the codegen leaves manually-set e_fn alone.
Two execution paths: codegen vs eval#
Internally, two execution paths from e_str to a CVXPY object live
side by side:
Codegen path (fast, default for source-defined items). At
init()time,ams.routines.routine.RoutineBase._link_pycode()loads the per-class pycode and wires each item'se_fnfrom a named callable in that module.parse()andevaluate()then just invoke the callable with aams.core.routine_ns.RoutineNSproxy; no regex, noeval.Eval-fallback path (used for items the codegen doesn't cover). At
parse()time,ams.opt._runtime_eval.eval_e_str()resolves bare symbol names (pg→r.pg,Cft→r.Cft, …) via a single regex pass andevals the result against aRoutineNSproxy. Function names (cp.sum,cp.multiply, …) are not rewritten — author canonical CVXPY ine_strand the helper passes them through.
Both paths must produce the same CVXPY object given the same e_str
— the codegen is the AOT-compiled version of the eval-fallback pipeline.
Which path runs is decided per-item by _link_pycode:
Item state |
Path |
Why |
|---|---|---|
In source code, no runtime mutation |
codegen |
The pristine |
Added at runtime via |
eval |
The cache (always generated against a pristine instance — see The pristine-source invariant below) doesn't know about runtime additions. |
|
eval |
The descriptor mutex on |
Authors and users can introspect which path is in effect:
ams.opt.OptzBase.formulation_source— per-item, returns'codegen' | 'eval' | 'manual' | 'pending'.ams.routines.routine.RoutineBase.formulation_summary()— full table.An INFO-level log line
<RoutineName> formulation: codegen=X/Y, …is emitted on everyinit().
The pristine-source invariant#
~/.ams/pycode/<routine>.py is always a faithful representation
of the routine source code, never of any user customization. To
guarantee this, _link_pycode runs codegen against a pristine
routine instance fetched from a per-process singleton
ams.System(no_input=True) (see
ams.prep._get_pristine_system()), never against the user's
self. A pristine = True marker in the generated module's
header acts as a cache-validation tripwire — caches written by older
AMS versions (which codegen'd against the live instance) lack the
marker and are auto-invalidated on next read.
This means a user can do:
sp = ams.load(...)
sp.DCOPF.obj.e_str += '+ extra_term'
sp.DCOPF.run(...) # uses customization
sp0 = ams.load(...) # fresh instance
sp0.DCOPF.run(...) # uses original formulation
without sp's mutation leaking into sp0. See
examples/ex8.ipynb for a working sp1 / sp2 / sp3 walkthrough.
Interoperation with ANDES#
The interoperation with dynamic simulator invovles both file conversion and data exchange.
In AMS, the built-in interface with ANDES is implemented in ams.interface.
File Format Converter#
Power flow data is the bridge between scheduling study and dynamics study, where it defines grid topology and power flow. An AMS case can be converted to an ANDES case, with the option to supply additional dynamic data.
- ams.interface.to_andes(system, addfile=None, setup=False, no_output=False, default_config=True, verify=False, tol=0.001, **kwargs)[source]
Convert the AMS system to an ANDES system.
A preferred dynamic system file to be added has following features: 1. The file contains both power flow and dynamic models. 2. The file can run in ANDES natively. 3. Power flow models are in the same shape as the AMS system. 4. Dynamic models, if any, are in the same shape as the AMS system.
This function is wrapped as the
Systemclass methodto_andes(). Using the file conversionto_andes()will automatically link the AMS system instance to the converted ANDES system instance in the AMS system attributedyn.It should be noted that detailed dynamic simualtion requires extra dynamic models to be added to the ANDES system, which can be passed through the
addfileargument.- Parameters:
- systemSystem
The AMS system to be converted to ANDES format.
- addfilestr, optional
The additional file to be converted to ANDES dynamic mdoels.
- setupbool, optional
Whether to call setup() after the conversion. Default is True.
- no_outputbool, optional
To ANDES system.
- default_configbool, optional
To ANDES system.
- verifybool
If True, the converted ANDES system will be verified with the source AMS system using AC power flow.
- tolfloat
The tolerance of error.
- Returns:
- adsysandes.system.System
The converted ANDES system.
Notes
Power flow models in the addfile will be skipped and only dynamic models will be used.
The addfile format is guessed based on the file extension. Currently only
xlsxis supported.Index in the addfile is automatically adjusted when necessary.
Examples
>>> import ams >>> import andes >>> sp = ams.load(ams.get_case('ieee14/ieee14_uced.xlsx'), setup=True) >>> sa = sp.to_andes(addfile=andes.get_case('ieee14/ieee14_full.xlsx'), ... setup=False, overwrite=True, no_output=True)
Data Exchange in Simulation#
To achieve scheduling-dynamics cosimulation, it requires bi-directional data exchange between
scheduling and dynamics study.
From the perspective of AMS, two functions, send and receive, are developed.
The maping relationship for a specific routine is defined in the routine class as map1 and
map2.
Additionally, a link table for the ANDES case is used for the controller connections.
Module ams.interface.Dynamic, contains the necessary functions and classes for
file conversion and data exchange.
- class ams.interface.Dynamic(amsys=None, adsys=None)[source]
ANDES interface class.
- Parameters:
- amsysAMS.system.System
The AMS system.
- adsysANDES.system.System
The ANDES system.
- Attributes:
- linkpandas.DataFrame
The ANDES system link table.
Notes
Using the file conversion
to_andes()will automatically link the AMS system to the converted ANDES system in the attributedyn.
Examples
>>> import ams >>> import andes >>> sp = ams.load(ams.get_case('ieee14/ieee14_rted.xlsx'), setup=True) >>> sa = sp.to_andes(setup=True, ... addfile=andes.get_case('ieee14/ieee14_wt3.xlsx'), ... overwrite=True, keep=False, no_output=True) >>> sp.RTED.run() >>> sp.RTED.dc2ac() >>> sp.dyn.send() # send RTED results to ANDES system >>> sa.PFlow.run() >>> sp.TDS.run() >>> sp.dyn.receive() # receive TDS results from ANDES system
- receive(adsys=None, routine=None, no_update=False)[source]
Receive ANDES system results to AMS devices.
- Parameters:
- adsysadsys.System.system, optional
The target ANDES dynamic system instance. If not provided, use the linked ANDES system isntance (
sp.dyn.adsys).- routinestr, optional
The routine to be received from ANDES. If None,
recentwill be used.- no_updatebool, optional
True to skip update the AMS routine parameters after sync. Default is False.
- send(adsys=None, routine=None)[source]
Send results of the recent sovled AMS routine (
sp.recent) to the target ANDES system.Note that converged AC conversion DOES NOT guarantee successful dynamic initialization
TDS.init(). Failed initialization is usually caused by limiter violation.- Parameters:
- adsysadsys.System.system, optional
The target ANDES dynamic system instance. If not provided, use the linked ANDES system isntance (
sp.dyn.adsys).- routinestr, optional
The routine to be sent to ANDES. If None,
recentwill be used.
When you use this interface, it automatically picks either the dynamic or static model based on the TDS initialization status. If the TDS is running, it selects the dynamic model; otherwise, it goes for the static model. For more details, check out the full API reference or take a look at the source code.
Note
Check ANDES documentation StaticGen for more details about substituting static generators with dynamic generators.