Scripting & Automation¶
Python console and multi-tab script editor for automating element creation, batch modifications, data extraction, analysis, and simulation. Full access to the GUI data model and any installed Python library.
Console and Script Editor¶
Python Console¶
Interactive REPL at the bottom-left, built on code.InteractiveInterpreter. Startup banner:
Python Console - ESFEX Studio
Available objects: model, state, config
Type help(<object>) for details.
>>>
Console Shortcuts¶
| Key | Action |
|---|---|
Enter |
Execute the current line (or add to multi-line buffer if incomplete) |
Up Arrow |
Navigate to the previous command in history |
Down Arrow |
Navigate to the next command in history |
Ctrl+C |
Cancel the current input line |
Tab |
Insert 4 spaces (indentation) |
Multi-Line Input¶
Incomplete statements (e.g., for loops, if blocks, function definitions) change the prompt from >>> to .... Press Enter on an empty line to execute.
>>> for g in state.generators.values():
... if g.rated_power > 50:
... print(f"{g.name}: {g.rated_power} MW")
...
Solar Farm North: 100 MW
Diesel Plant A: 75 MW
Command History¶
Session-level command history navigable with Up/Down arrow keys. Not persisted between sessions.
Script Editor¶
Bottom-right of the window. Multi-line scripting environment:
- Multiple tabs -- Open several scripts simultaneously. Each tab has its own undo history.
- Syntax highlighting -- Python keywords, builtins, strings, numbers, comments, and decorators are highlighted using the current theme's color scheme.
- Line numbers -- Displayed in the left gutter for easy reference.
- Auto-indentation -- New lines inherit the indentation of the previous line.
Script Editor Actions¶
| Button / Shortcut | Action |
|---|---|
| Run | Execute the current tab's script. Output appears in the console. |
| Open | Load a .py file into a new tab. |
| Save | Save the current tab's content to a .py file. |
| New Tab | Create a blank script tab. |
| Close Tab | Close the current tab (prompts to save if modified). |
Scripts execute in the same namespace as the console. Variables are shared bidirectionally.
Available Objects¶
Four pre-loaded objects in the console/script namespace:
| Variable | Type | Description |
|---|---|---|
model |
GuiModel |
The central data model. Use its methods for all create/update/delete operations. Changes via model methods trigger proper UI updates (map refresh, tree sync). |
state |
GuiSystemState |
The currently active system's data container. Contains dictionaries of nodes, generators, batteries, lines, etc. Updates automatically when you switch systems. |
config |
ESFEXConfig or None |
The loaded configuration object. None if no YAML file has been opened. |
window |
MainWindow |
The editor window instance. Provides access to the map widget, element tree, console, and all UI components. |
The model Object¶
Primary interface for programmatic modifications. Methods ensure the map, element tree, and properties panel all update correctly.
Important: Always use model.update_*() methods. Direct attribute changes on state objects update data but do not refresh UI.
# Correct -- triggers full UI update (map marker, tree label, form)
model.update_generator(gen_id, rated_power=200.0)
# Incorrect -- data changes but UI is stale
state.generators[gen_id].rated_power = 200.0
Key Model Methods¶
| Method | Description |
|---|---|
model.add_node(name, lat, lng) |
Create a new node |
model.add_generator_instance(unit_key, name, gen_type, fuel, node, rated_power, ...) |
Create a new generator |
model.add_battery(name, node, rated_power, capacity, ...) |
Create a new battery |
model.add_line(from_node, to_node, capacity_mw, ...) |
Create a new transmission line |
model.update_generator(gen_id, **kwargs) |
Update generator properties |
model.update_battery(bat_id, **kwargs) |
Update battery properties |
model.update_node(node_idx, **kwargs) |
Update node properties |
model.delete_element(element_type, element_id) |
Delete an element |
The state Object¶
Read access to the active system's data:
| Attribute | Type | Description |
|---|---|---|
state.nodes |
dict[int, GuiNode] |
All nodes, keyed by index |
state.generators |
dict[str, GuiGeneratorInstance] |
All generators, keyed by ID |
state.batteries |
dict[str, GuiBatteryInstance] |
All batteries, keyed by ID |
state.transmission_lines |
dict[str, GuiTransmissionLine] |
All lines, keyed by line_id |
state.transformers |
dict[str, GuiTransformer] |
All transformers |
state.zones |
dict[str, GuiZone] |
All development zones |
state.fuel_entries |
dict[str, GuiFuelEntry] |
All fuel entry points |
state.fuel_storages |
dict[str, GuiFuelStorage] |
All fuel storage facilities |
state.fuel_routes |
dict[str, GuiFuelRoute] |
All fuel transport routes |
state.buses |
dict[str, GuiBus] |
All buses |
state.electrolyzers |
dict[str, GuiElectrolyzer] |
All electrolyzers |
The window Object¶
Access to UI components for advanced scripting:
| Attribute | Type | Description |
|---|---|---|
window.map_widget |
MapWidget |
The Leaflet.js map widget |
window.element_tree |
ElementTreePanel |
The element tree panel |
window.console |
PythonConsole |
The Python console itself |
window.properties_panel |
PropertiesPanel |
The right-side properties panel |
window.script_editor |
ScriptEditor |
The script editor widget |
Example: System Summary¶
print(f"System: {window._current_system_name}")
print(f"Nodes: {len(state.nodes)}")
print(f"Generators: {len(state.generators)}")
print(f"Total generation capacity: {sum(g.rated_power for g in state.generators.values()):.0f} MW")
print(f"Batteries: {len(state.batteries)}")
print(f"Total storage capacity: {sum(b.capacity for b in state.batteries.values()):.0f} MWh")
print(f"Lines: {len(state.transmission_lines)}")
print(f"Buses: {len(state.buses)}")
print(f"Fuel entries: {len(state.fuel_entries)}")
Example: Batch Import Generators from CSV¶
import csv
with open("generators.csv") as f:
reader = csv.DictReader(f)
count = 0
for row in reader:
gen_id = model.add_generator_instance(
unit_key=row["name"].lower().replace(" ", "_"),
name=row["name"],
gen_type=row["type"],
fuel=row["fuel"],
node=int(row["node"]),
rated_power=float(row["rated_power_mw"]),
)
# Set additional properties on the state object
gen = state.generators[gen_id]
gen.invest_cost = float(row.get("invest_cost", 0))
gen.life_time = int(row.get("lifetime", 25))
gen.fuel_cost = float(row.get("fuel_cost", 0))
count += 1
print(f"Imported {count} generators")
Expected CSV format:
name,type,fuel,node,rated_power_mw,invest_cost,lifetime,fuel_cost
Solar Farm A,Renewable,Solar,0,50,1200,25,0
Diesel Plant B,Non-renewable,Diesel,1,20,800,30,65
Wind Farm C,Renewable,Wind,2,80,1500,20,0
Example: Scale All Capacities¶
FACTOR = 1.5
for gen_id, gen in state.generators.items():
model.update_generator(gen_id, rated_power=gen.rated_power * FACTOR)
for bat_id, bat in state.batteries.items():
model.update_battery(bat_id,
rated_power=bat.rated_power * FACTOR,
capacity=bat.capacity * FACTOR)
print(f"All capacities scaled by {FACTOR}x")
Example: Create Grid from Adjacency Matrix¶
import numpy as np
from esfex.visualization.data.gui_model import GeoPoint
# Define nodes: (latitude, longitude, name)
nodes = [
(-21.10, 55.53, "Saint-Denis"),
(-21.15, 55.45, "Le Port"),
(-21.34, 55.47, "Saint-Louis"),
(-21.28, 55.70, "Saint-Benoit"),
]
# Adjacency matrix: capacity in MW (0 = no connection)
adjacency = np.array([
[0, 300, 0, 200],
[300, 0, 250, 0 ],
[0, 250, 0, 150],
[200, 0, 150, 0 ],
])
# Create nodes
for i, (lat, lng, name) in enumerate(nodes):
model.add_node(name=name)
state.nodes[i].coordinate = GeoPoint(lat=lat, lng=lng, label=name)
# Create lines from upper triangle of adjacency matrix
line_count = 0
for i in range(len(nodes)):
for j in range(i + 1, len(nodes)):
if adjacency[i, j] > 0:
model.add_line(from_node=i, to_node=j,
capacity_mw=float(adjacency[i, j]))
line_count += 1
print(f"Created {len(nodes)} nodes and {line_count} lines")
Example: Color Generators by Fuel Type¶
from esfex.visualization.data.gui_model import VisualStyle
COLORS = {
"Solar": "#f39c12", # Orange
"Wind": "#3498db", # Blue
"Diesel": "#7f8c8d", # Gray
"Gas": "#e67e22", # Dark orange
"Hydro": "#1abc9c", # Teal
"Biomass": "#27ae60", # Green
"Nuclear": "#8e44ad", # Purple
"Hydrogen": "#2ecc71", # Light green
}
for gen_id, gen in state.generators.items():
color = COLORS.get(gen.fuel, "#95a5a6")
size = max(8, gen.rated_power * 0.05) # Scale marker with capacity
gen.style = VisualStyle(color=color, size=size)
model.update_generator(gen_id)
print(f"Colored {len(state.generators)} generators")
Example: Export System Data to Excel¶
import pandas as pd
# Generator data
gen_data = [{
"Name": g.name,
"Type": g.gen_type,
"Fuel": g.fuel,
"Node": g.node,
"Rated Power (MW)": g.rated_power,
"Fuel Cost ($/MWh)": g.fuel_cost,
"Investment Cost ($/MW)": g.invest_cost,
"Lifetime (years)": g.life_time,
} for g in state.generators.values()]
# Battery data
bat_data = [{
"Name": b.name,
"Node": b.node,
"Power (MW)": b.rated_power,
"Capacity (MWh)": b.capacity,
"Charge Eff.": b.charge_efficiency,
"Discharge Eff.": b.discharge_efficiency,
} for b in state.batteries.values()]
# Line data
line_data = [{
"Line ID": l.line_id,
"From": l.from_ref.element_id if l.from_ref else "?",
"To": l.to_ref.element_id if l.to_ref else "?",
"Capacity (MW)": l.capacity_mw,
"Voltage (kV)": l.voltage_kv,
} for l in state.transmission_lines.values()]
with pd.ExcelWriter("system_export.xlsx") as writer:
pd.DataFrame(gen_data).to_excel(writer, sheet_name="Generators", index=False)
pd.DataFrame(bat_data).to_excel(writer, sheet_name="Batteries", index=False)
pd.DataFrame(line_data).to_excel(writer, sheet_name="Lines", index=False)
print("Exported to system_export.xlsx")
Example: Find Undersized Lines¶
THRESHOLD_MW = 100 # Flag lines below this capacity
undersized = []
for line_id, line in state.transmission_lines.items():
if line.capacity_mw < THRESHOLD_MW:
undersized.append((line_id, line.capacity_mw))
if undersized:
print(f"Found {len(undersized)} lines below {THRESHOLD_MW} MW:")
for lid, cap in sorted(undersized, key=lambda x: x[1]):
print(f" {lid}: {cap:.0f} MW")
else:
print(f"All lines are at or above {THRESHOLD_MW} MW")
Example: Compute Generation Mix¶
from collections import defaultdict
capacity_by_fuel = defaultdict(float)
for gen in state.generators.values():
capacity_by_fuel[gen.fuel] += gen.rated_power
total = sum(capacity_by_fuel.values())
print(f"\nGeneration Mix ({total:.0f} MW total)")
print("-" * 40)
for fuel, cap in sorted(capacity_by_fuel.items(), key=lambda x: -x[1]):
pct = cap / total * 100 if total > 0 else 0
bar = "#" * int(pct / 2)
print(f" {fuel:15s} {cap:8.1f} MW ({pct:5.1f}%) {bar}")
Example: Run Simulation from the GUI¶
from esfex import load_config
from esfex.runner import Orchestrator
from esfex.visualization.data.serializer import gui_state_to_yaml
import tempfile, os
# Export current state to a temporary YAML
tmp = tempfile.mkdtemp()
yaml_path = os.path.join(tmp, "gui_export.yaml")
gui_state_to_yaml(
model._all_states, config, yaml_path,
global_settings=model._global_settings,
stochastic_scenarios=model._stochastic_scenarios,
)
# Load and run
cfg = load_config(yaml_path)
orch = Orchestrator(cfg, config_path=yaml_path)
results = orch.run(years=5, start_year=2025)
# Display summary
for yr in results:
print(f"Year {yr.year}: Cost=${yr.objective:,.0f} RE={yr.re_penetration:.1%}")
Example: Bulk Update from External Database¶
import sqlite3
conn = sqlite3.connect("plant_database.db")
cursor = conn.execute("""
SELECT name, fuel_cost, efficiency, lifetime
FROM generators
WHERE country = 'Cuba'
""")
updated = 0
for db_name, fuel_cost, efficiency, lifetime in cursor:
# Find matching generator by name
for gen_id, gen in state.generators.items():
if gen.name.lower() == db_name.lower():
model.update_generator(gen_id,
fuel_cost=fuel_cost,
efficiency_rated=efficiency,
life_time=lifetime)
updated += 1
break
conn.close()
print(f"Updated {updated} generators from database")
Example: Inject Variables for Interactive Exploration¶
# In a script: compute something complex
import numpy as np
capacities = np.array([g.rated_power for g in state.generators.values()])
names = [g.name for g in state.generators.values()]
fuels = [g.fuel for g in state.generators.values()]
# Make results available in the console
window.console.update_namespace(
capacities=capacities,
gen_names=names,
gen_fuels=fuels,
)
print("Variables 'capacities', 'gen_names', 'gen_fuels' are now available in the console")
After running, capacities.mean() or capacities.max() are available directly in the console.
Example: Generate a Matplotlib Chart¶
import matplotlib.pyplot as plt
from collections import defaultdict
capacity_by_fuel = defaultdict(float)
for gen in state.generators.values():
capacity_by_fuel[gen.fuel] += gen.rated_power
fuels = list(capacity_by_fuel.keys())
caps = [capacity_by_fuel[f] for f in fuels]
fig, ax = plt.subplots(figsize=(10, 5))
ax.barh(fuels, caps, color=["#f39c12", "#3498db", "#7f8c8d", "#27ae60", "#e67e22"][:len(fuels)])
ax.set_xlabel("Installed Capacity (MW)")
ax.set_title(f"Generation Capacity - {window._current_system_name}")
plt.tight_layout()
plt.savefig("capacity_chart.png", dpi=150)
plt.show()
print("Chart saved to capacity_chart.png")
Tips and Best Practices¶
- Save before running destructive scripts. Bulk deletes, capacity scaling, and network restructuring cannot be undone across script boundaries.
- Use
model.update_*()methods for all modifications. Direct attribute changes onstateobjects will not refresh the UI. - Use
window.console.update_namespace(my_var=data)to store intermediate results for later interactive exploration in the console. - Any installed Python library is available:
numpy,pandas,matplotlib,scipy,networkx,geopandas. stateupdates automatically on system switch. For multi-system scripts, accessmodel._all_statesdirectly.- Print progress for long-running scripts to monitor execution.
- Test on a small subset first before applying to the full system.
Error Handling¶
Exceptions print the full traceback in the console. The editor remains functional.
# The console shows the full traceback:
# Traceback (most recent call last):
# File "<script>", line 5, in <module>
# model.update_generator("nonexistent_id", rated_power=100)
# KeyError: 'nonexistent_id'
For batch modifications, wrap operations in try/except to continue past individual failures:
errors = []
for gen_id, gen in state.generators.items():
try:
model.update_generator(gen_id, rated_power=gen.rated_power * 1.5)
except Exception as e:
errors.append((gen_id, str(e)))
if errors:
print(f"Completed with {len(errors)} errors:")
for gid, msg in errors:
print(f" {gid}: {msg}")
else:
print("All generators updated successfully")
Subprocess Execution¶
External command execution via QProcess integration. Used internally by Run and Sensitivity actions; also available in scripts for invoking external tools.