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 nameBusNomVolt— nominal voltage in kVBusName_NomVolt— combined name and voltage labelAreaNum— area number the bus belongs toZoneNum— 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 keysBusName_NomVolt— bus labelGenMWSetPoint,GenMWMax,GenMWMin— MW output and limitsGenMvrSetPoint,GenMVRMax,GenMVRMin— Mvar setpoint and limitsGenVoltSet— 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 keysBusName_NomVolt— bus labelLoadSMW— constant-power MWLoadSMVR— constant-power MvarLoadStatus— “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.