Creating Objects

The same bracket-write syntax used for modifying existing data can also create new objects in PowerWorld. When you assign a DataFrame containing primary keys that don’t match any existing objects, ESA++ automatically falls back to a row-by-row insertion path that creates them.

This is the primary way to programmatically add buses, generators, loads, branches, and any other PowerWorld object type from Python.

from esapp import PowerWorld
from esapp.components import *

pw = PowerWorld("path/to/case.pwb")

Prerequisites

PowerWorld must be in EDIT mode before you can add new objects. Call pw.edit_mode() to enter it, and pw.run_mode() when you’re done.

The DataFrame you write should include all identifier fields for the object type — that’s the union of primary keys and secondary keys. You can inspect these with Bus.identifiers(), Gen.identifiers(), etc. Primary keys uniquely identify the object; secondary keys provide the additional context PowerWorld needs to fully define it (nominal voltage, area, zone, limits, etc.).

In [ ]:
# Identifier fields needed for creation
print("Bus identifiers:", Bus.identifiers())
print("Gen identifiers:", Gen.identifiers())
print("Load identifiers:", Load.identifiers())

Creating Buses

Buses have a single primary key (BusNum) but several secondary identifier fields that PowerWorld uses to fully define the bus. The full set of bus identifiers is:

  • BusNum — bus number (primary key)

  • BusName — bus name

  • BusNomVolt — nominal voltage in kV

  • BusName_NomVolt — combined name and voltage label

  • AreaNum — area number the bus belongs to

  • ZoneNum — zone number the bus belongs to

Include all of these when creating buses so PowerWorld has the complete definition.

In [3]:
# Record the original bus count
n_before = pw.n_bus

# Build a DataFrame with all identifier fields
new_buses = pd.DataFrame({
    "BusNum":         [90001, 90002, 90003],
    "BusName":        ["NewBus_138", "NewBus_230", "NewBus_500"],
    "BusNomVolt":     [138.0, 230.0, 500.0],
    "BusName_NomVolt": ["NewBus_138 138.00", "NewBus_230 230.00", "NewBus_500 500.00"],
    "AreaNum":        [1, 1, 1],
    "ZoneNum":        [1, 1, 1],
})

# Enter edit mode, create the buses, return to run mode
pw.edit_mode()
pw[Bus] = new_buses
pw.run_mode()

# Verify they exist
assert pw.n_bus == n_before + 3
created = pw[Bus, ["BusName", "BusNomVolt", "AreaNum", "ZoneNum"]]
created[created["BusNum"].isin([90001, 90002, 90003])]
Out[3]:
AreaNum BusName BusNomVolt BusNum ZoneNum
37 1 NewBus_138 138.0 90001 1
38 1 NewBus_230 230.0 90002 1
39 1 NewBus_500 500.0 90003 1

Creating Generators

Generators have a compound primary key (BusNum + GenID) and a larger set of secondary identifiers that define their operating characteristics. The bus must already exist. The full set of generator identifiers includes:

  • BusNum, GenID — primary keys

  • BusName_NomVolt — bus label

  • GenMWSetPoint, GenMWMax, GenMWMin — MW output and limits

  • GenMvrSetPoint, GenMVRMax, GenMVRMin — Mvar setpoint and limits

  • GenVoltSet — voltage setpoint (pu)

  • GenStatus — “Open” or “Closed”

  • GenAGCAble, GenAVRAble — AGC/AVR availability (“YES”/”NO”)

In [4]:
n_gen_before = pw.n_gen

new_gens = pd.DataFrame({
    "BusNum":         [90001, 90002],
    "GenID":          ["1", "1"],
    "BusName_NomVolt": ["NewBus_138 138.00", "NewBus_230 230.00"],
    "GenMWSetPoint":  [100.0, 250.0],
    "GenMWMax":       [200.0, 500.0],
    "GenMWMin":       [0.0, 50.0],
    "GenMvrSetPoint": [0.0, 0.0],
    "GenMVRMax":      [100.0, 200.0],
    "GenMVRMin":      [-50.0, -100.0],
    "GenVoltSet":     [1.0, 1.0],
    "GenStatus":      ["Closed", "Closed"],
    "GenAGCAble":     ["YES", "YES"],
    "GenAVRAble":     ["YES", "YES"],
})

pw.edit_mode()
pw[Gen] = new_gens
pw.run_mode()

assert pw.n_gen == n_gen_before + 2
gens = pw[Gen, ["GenMWSetPoint", "GenMWMax", "GenStatus"]]
gens[gens["BusNum"].isin([90001, 90002])]
Out[4]:
BusNum GenID GenMWMax GenMWSetPoint GenStatus
45 90001 1 200.0 100.0 Closed
46 90002 1 500.0 250.0 Closed

Creating Loads

Loads follow the same pattern with primary keys BusNum and LoadID. Their secondary identifiers define the constant-power MW and Mvar components and status:

  • BusNum, LoadID — primary keys

  • BusName_NomVolt — bus label

  • LoadSMW — constant-power MW

  • LoadSMVR — constant-power Mvar

  • LoadStatus — “Open” or “Closed”

In [5]:
new_loads = pd.DataFrame({
    "BusNum":         [90002, 90003],
    "LoadID":         ["1", "1"],
    "BusName_NomVolt": ["NewBus_230 230.00", "NewBus_500 500.00"],
    "LoadSMW":        [75.0, 150.0],
    "LoadSMVR":       [20.0, 40.0],
    "LoadStatus":     ["Closed", "Closed"],
})

pw.edit_mode()
pw[Load] = new_loads
pw.run_mode()

loads = pw[Load, ["LoadSMW", "LoadSMVR", "LoadStatus"]]
loads[loads["BusNum"].isin([90002, 90003])]
Out[5]:
BusNum LoadID LoadSMVR LoadSMW LoadStatus
27 90002 1 20.000000 75.0 Closed
28 90003 1 40.000001 150.0 Closed

How It Works

Under the hood, pw[ObjectType] = df first tries the fast batch-update path (ChangeParametersMultipleElementRect). If PowerWorld reports that some objects were “not found”, ESA++ checks that all primary key columns are present in the DataFrame and falls back to a row-by-row path (ChangeParametersMultipleElement) that creates missing objects.

This means the same syntax works for both updating and creating:

  • If the objects already exist, their fields are updated.

  • If they don’t exist, they’re created with the values you provided.

  • A mixed DataFrame (some rows exist, some don’t) also works — existing rows are updated and new rows are created.

If primary keys are missing from the DataFrame, ESA++ raises a ValueError immediately rather than silently failing.

Tips

  • Always enter edit mode before creating objects and return to run mode afterward. Forgetting this is the most common cause of creation failures.

  • Include all identifier fields when creating objects. Use ComponentType.identifiers() to see the full set of primary and secondary key fields. PowerWorld needs these to fully define the object — omitting them may cause unexpected defaults or creation failures.

  • The broadcast syntax does not create objects. Only the DataFrame assignment path (pw[Type] = df) can create new objects. The field-broadcast path (pw[Type, "Field"] = value) only modifies existing ones.