import sys
import json
from pathlib import Path
with open(r"..\..\settings_local.json") as settings_file:
SETTINGS = json.load(settings_file)
sys.path.append(SETTINGS["local path to PowerFactory application"])
import powerfactory
from powfacpy.applications.modelica import ModelicaModel, ModelicaModelSpec, ModelicaVariable
app = powerfactory.GetApplication()
app.Show()
app.ActivateProject(r"powfacpy\39_bus_new_england_copy_to_run_tests")
pfmdl = ModelicaModel(app)Importing Modelica Models
PowerFactory has no .mo file import. A Modelica model type (TypMdl) is a decomposed representation - separate tabular declarations for inputs / outputs / parameters / states / internal variables plus text fields for the equations - and it only supports a subset of Modelica 3.5. Entering all of that through the GUI is tedious.
powfacpy.applications.modelica lets you describe the model once (as a .mo file or a ModelicaModelSpec) and create the compiled TypMdl from a script. This tutorial imports the stepwise under-frequency / RoCoF load-shedding controller shipped in pf_models/modelica/LoadSheddingController.mo.
This is a best-effort bridge, not a general Modelica compiler. PowerFactory’s TypMdl only supports a subset of Modelica 3.5, and the .mo parser and the array / for-loop flattener handle the constructs typical of controllers and relays - not arbitrary Modelica. Expect to simplify a model’s source to fit the subset (see the rules at the end), or to assemble the ModelicaModelSpec by hand. For full standard Modelica, compile the .mo to an FMU (OpenModelica / Dymola) and load that into a TypMdl instead. The API may still change.
1 Setup
2 The Modelica model
The controller is written as standard Modelica (it also compiles with OpenModelica / Dymola), but it is written to fit PowerFactory’s subset - see the notes at the end.
MO_FILE = Path(r"..\..\pf_models\modelica\LoadSheddingController.mo")
print(MO_FILE.read_text()[:1600], "...")model LoadSheddingController
"Stepwise under-frequency / RoCoF load-shedding controller for one PowerFactory ElmLod.
One instance per load. Latching only (no automatic load restoration). Each stage
sheds a fraction of THIS load's instantaneous power. Intended composite-model wiring:
f <-- bus electrical frequency n:fehz [Hz]
pLoad <-- load active power m:Psum:bus1 [MW]
qLoad <-- load reactive power m:Qsum:bus1 [Mvar]
dpext --> ElmLod.Pext [MW]
dqext --> ElmLod.Qext [Mvar]
Sign convention: ElmLod.Pext is assumed to be added to the load (Pext > 0 means
more load). If your PowerFactory version uses the opposite convention, negate
dpext / dqext.
This is standard Modelica but also written to fit PowerFactory's Modelica subset
(Hybrid method): der() only on protected state variables, `der(x) = (u-x)/T` form,
every state assigned once in `initial equation`, no parameter start values. Import
it with `powfacpy.applications.modelica.ModelicaModel.create_model_type(
ModelicaModelSpec.from_modelica('.../LoadSheddingController.mo'))`, which unrolls
the `for` loops / arrays before creating the TypMdl.
"
// ------------------------------------------------------------------
// Under-frequency stages (definite time)
// ------------------------------------------------------------------
parameter Integer nStage = 4 "number of under-frequency stages";
parameter Real fLevel[nStage] = {49.0, 48.8, ...
3 Parse it into a spec
ModelicaModelSpec.from_modelica is a best-effort parser that splits the source into the parts a TypMdl needs.
spec = ModelicaModelSpec.from_modelica(MO_FILE)
print("name: ", spec.name)
print("method:", spec.method)
print("inputs: ", [v.name for v in spec.inputs])
print("outputs:", [v.name for v in spec.outputs])
print("states: ", [f"{v.name}{'['+v.size+']' if v.size else ''}" for v in spec.states])
print("params: ", [f"{v.name}{'['+v.size+']' if v.size else ''}" for v in spec.parameters])name: LoadSheddingController
method: hybrid
inputs: ['f', 'pLoad', 'qLoad']
outputs: ['dpext', 'dqext', 'rocof']
states: ['kShed', 'fFilt', 'pMeasF', 'qMeasF', 'fTmr[nStage]', 'rTmr[nRstage]']
params: ['nStage', 'fLevel[nStage]', 'fDelay[nStage]', 'fFrac[nStage]', 'useRocof', 'nRstage', 'rLevel[nRstage]', 'rDelay[nRstage]', 'rFrac[nRstage]', 'fRocofArm', 'Trocof', 'enable', 'fNom', 'Tmeas', 'Tbrk', 'kShedMax']
The parser recognises fTmr as a state (because der(fTmr[i]) appears in the equations) and the other protected variables as internal variables.
You can also build the spec directly, without a .mo file:
manual_spec = ModelicaModelSpec(
name="Gain",
inputs=[ModelicaVariable("u", unit="p.u.")],
outputs=[ModelicaVariable("y", unit="p.u.")],
parameters=[ModelicaVariable("K", default="1.0", description="gain")],
equations=["y = K*u;"],
)
print(manual_spec.to_modelica())model Gain
parameter Real K = 1.0 "gain";
input Real u(unit="p.u.");
output Real y(unit="p.u.");
equation
y = K*u;
end Gain;
4 Flatten arrays and for loops
PowerFactory’s support for arrays and for loops inside the equation fields is limited, so spec.flattened() unrolls them: array variables x[n] become scalars x_1 ... x_n, for i in 1:n loop ... end for; is expanded, and sum(x) becomes (x_1 + ... + x_n). Array sizes are resolved from Integer parameters with an integer default (nStage = 4).
flat = spec.flattened()
print("parameters after flattening:")
print(" ", [v.name for v in flat.parameters])
print("\nunrolled under-frequency stage equations:")
for eq in flat.equations:
if eq.startswith(("fArm_", "fTrip", "fCon_", "when fArm", "when not fArm")):
print(" ", eq)
print("\naggregation:")
for eq in flat.equations:
if eq.startswith(("kTarget", "der(kShed)", "dpext", "dqext")):
print(" ", eq)parameters after flattening:
['nStage', 'fLevel_1', 'fLevel_2', 'fLevel_3', 'fLevel_4', 'fDelay_1', 'fDelay_2', 'fDelay_3', 'fDelay_4', 'fFrac_1', 'fFrac_2', 'fFrac_3', 'fFrac_4', 'useRocof', 'nRstage', 'rLevel_1', 'rDelay_1', 'rFrac_1', 'fRocofArm', 'Trocof', 'enable', 'fNom', 'Tmeas', 'Tbrk', 'kShedMax']
unrolled under-frequency stage equations:
fArm_1 = enable and (f < fLevel_1);
when not fArm_1 then
when fArm_1 and (fTmr_1 >= fDelay_1) then
fTrip_1 = true;
fCon_1 = if fTrip_1 then fFrac_1 else 0;
fArm_2 = enable and (f < fLevel_2);
when not fArm_2 then
when fArm_2 and (fTmr_2 >= fDelay_2) then
fTrip_2 = true;
fCon_2 = if fTrip_2 then fFrac_2 else 0;
fArm_3 = enable and (f < fLevel_3);
when not fArm_3 then
when fArm_3 and (fTmr_3 >= fDelay_3) then
fTrip_3 = true;
fCon_3 = if fTrip_3 then fFrac_3 else 0;
fArm_4 = enable and (f < fLevel_4);
when not fArm_4 then
when fArm_4 and (fTmr_4 >= fDelay_4) then
fTrip_4 = true;
fCon_4 = if fTrip_4 then fFrac_4 else 0;
aggregation:
kTarget = min(kShedMax, (fCon_1 + fCon_2 + fCon_3 + fCon_4) + (rCon_1));
der(kShed) = (kTarget - kShed) / Tbrk;
dpext = -kShed / den * pMeasF;
dqext = -kShed / den * qMeasF;
5 Create and compile the TypMdl
create_model_type flattens the spec (unless told otherwise), creates the TypMdl in the project’s dynamic-models library folder (app.GetProjectFolder("blk") - its display name is ‘Dynamic Models’, ‘User Defined Models’, … depending on the project), sets every declaration column and equation, then runs Check() and Compile(). A failed compile raises PFModelicaCompilationError carrying the PowerFactory output-window messages.
try:
app.Hide()
model_type = pfmdl.create_model_type(spec)
finally:
app.Show()
print("class: ", model_type.GetClassName())
print("modMethod: ", model_type.GetAttribute("modMethod"), "(0 = Hybrid)")
print("compiled: ", bool(model_type.GetAttribute("modelType")))
print("binary: ", model_type.GetAttribute("cfilePath"))
print("parameters: ", model_type.GetAttribute("paramName"))class: TypMdl
modMethod: 0 (0 = Hybrid)
compiled: True
binary: C:\Users\seberlein\AppData\Local\DIgSILENT\PowerFactory 2025 SP5\Workspace.Muwy05o\db\User Models\seberlein\hBpJmjg7\LoadSheddingController.pfmu
parameters: ['nStage', 'fLevel_1', 'fLevel_2', 'fLevel_3', 'fLevel_4', 'fDelay_1', 'fDelay_2', 'fDelay_3', 'fDelay_4', 'fFrac_1', 'fFrac_2', 'fFrac_3', 'fFrac_4', 'useRocof', 'nRstage', 'rLevel_1', 'rDelay_1', 'rFrac_1', 'fRocofArm', 'Trocof', 'enable', 'fNom', 'Tmeas', 'Tbrk', 'kShedMax']
The four under-frequency stages of the source array fLevel[nStage] are now the scalar parameters fLevel_1 ... fLevel_4, each with its own default from the {49.0, 48.8, 48.6, 48.4} literal.
5.1 Read it back
roundtrip = pfmdl.read_model_type(model_type)
print("inputs: ", [v.name for v in roundtrip.inputs])
print("param defaults:", {v.name: v.default for v in roundtrip.parameters if v.name.startswith("fLevel")})inputs: ['f', 'pLoad', 'qLoad']
param defaults: {'fLevel_1': '49.0', 'fLevel_2': '48.8', 'fLevel_3': '48.6', 'fLevel_4': '48.4'}
6 Create a model instance
try:
app.Hide()
load = pfmdl.act_prj.get_unique_obj(
r"Network Model\Network Data\Grid\Load 23"
)
model = pfmdl.create_model(
model_type, parent_folder=load.GetParent(), name="Load 23 shedding"
)
# Not wired into a composite model yet (see below), so its inputs are
# unconnected - take it out of service to avoid breaking dynamic
# simulations elsewhere in this project until it is actually wired up.
model.outserv = 1
finally:
app.Show()
print(model, "->", model.GetAttribute("typ_id"))<l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Network Model.IntPrjfolder\Network Data.IntPrjfolder\Grid.ElmNet\Load 23 shedding.ElmMdl</l3> -> <l3>\seberlein.IntUser\powfacpy\39_bus_new_england_copy_to_run_tests.IntPrj\Library.IntPrjfolder\User Defined Models.IntPrjfolder\LoadSheddingController.TypMdl</l3>
7 Wiring it into a simulation
The ElmMdl still has to be placed in a composite model (ElmComp) whose frame (BlkDef) connects:
| controller signal | source / target |
|---|---|
f |
bus electrical frequency n:fehz of the load’s terminal |
pLoad |
load active power m:Psum:bus1 |
qLoad |
load reactive power m:Qsum:bus1 |
dpext |
ElmLod.Pext |
dqext |
ElmLod.Qext |
Building that frame is currently a GUI step (see the Study Cases / Dynamic Simulation tutorials for working with composite models). One known limitation: setting scalar parameter values on the ElmMdl from a script only works after PowerFactory has populated the instance’s parameter list (open the model dialog once); create_model warns for any value it could not set.
8 PowerFactory Modelica subset
Things a Hybrid model must obey (the shipped .mo already does, while staying valid standard Modelica):
- a state’s start value must be a literal constant, not a parameter - initialise states in the
initial equationsection; - every equation must have exactly
xorder(x)on the left-hand side: writeder(x) = (u - x)/T, notT*der(x) = u - x; der()may only be applied to a declared state, never to an output - make it aprotectedstate and monitor it ass:<name>;- every state must be assigned exactly once in
initial equation.
reinit(), when, pre(), if / for (once unrolled), min / max and latching booleans (a Boolean written only inside a when) all work.