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.

WarningExperimental feature

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

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)

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 equation section;
  • every equation must have exactly x or der(x) on the left-hand side: write der(x) = (u - x)/T, not T*der(x) = u - x;
  • der() may only be applied to a declared state, never to an output - make it a protected state and monitor it as s:<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.

Back to top