
Energy Systems
- 52 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
energy-systems is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- energy-systems
- AI & Agent Building
- AI-coding skill
Energy Systems by the numbers
- 52 all-time installs (skills.sh)
- Ranked #7,086 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill energy-systemsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Energy Systems
Identity
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Energy Systems & Grid Modeling
Patterns
Power Flow Analysis
Description
AC and DC power flow for grid state analysis
Use When
Modeling voltage, power flows, and system state
Implementation
import pandapower as pp import numpy as np
def create_power_system(): """Create a simple power system model.""" net = pp.create_empty_network()
Create buses
bus_slack = pp.create_bus(net, vn_kv=110, name="Slack Bus") bus_pv = pp.create_bus(net, vn_kv=110, name="Generator Bus") bus_load = pp.create_bus(net, vn_kv=110, name="Load Bus")
External grid (slack bus)
pp.create_ext_grid(net, bus=bus_slack, vm_pu=1.02)
Generator
pp.create_gen(net, bus=bus_pv, p_mw=50, vm_pu=1.01)
Load
pp.create_load(net, bus=bus_load, p_mw=80, q_mvar=20)
Lines
pp.create_line_from_parameters( net, from_bus=bus_slack, to_bus=bus_pv, length_km=50, r_ohm_per_km=0.1, x_ohm_per_km=0.4, c_nf_per_km=10, max_i_ka=0.5 ) pp.create_line_from_parameters( net, from_bus=bus_pv, to_bus=bus_load, length_km=30, r_ohm_per_km=0.1, x_ohm_per_km=0.4, c_nf_per_km=10, max_i_ka=0.5 )
return net
def run_power_flow(net, algorithm='nr'): """Run power flow analysis.""" try: pp.runpp(net, algorithm=algorithm, numba=True)
results = { 'converged': net.converged, 'bus_voltages': net.res_bus[['vm_pu', 'va_degree']].to_dict(), 'line_loading': net.res_line['loading_percent'].to_dict(), 'losses_mw': net.res_line['pl_mw'].sum(), 'generation_mw': net.res_gen['p_mw'].sum() }
Check for violations
results['voltage_violations'] = net.res_bus[ (net.res_bus['vm_pu'] < 0.95) | (net.res_bus['vm_pu'] > 1.05) ].index.tolist()
results['line_overloads'] = net.res_line[ net.res_line['loading_percent'] > 100 ].index.tolist()
return results
except pp.LoadflowNotConverged: return {'converged': False, 'error': 'Power flow did not converge'}
Usage
net = create_power_system() results = run_power_flow(net)
if results['converged']: print(f"System losses: {results['losses_mw']:.2f} MW") print(f"Voltage violations: {results['voltage_violations']}") else: print("Power flow did not converge - check system configuration")
Storage Dispatch
Description
Battery storage dispatch optimization
Use When
Optimizing charge/discharge for arbitrage, peak shaving, or services
Implementation
import numpy as np from scipy.optimize import minimize
class BatteryStorage: def __init__(self, capacity_mwh, power_mw, efficiency=0.90): self.capacity_mwh = capacity_mwh self.power_mw = power_mw self.efficiency_oneway = np.sqrt(efficiency) # Symmetric self.soc_min = 0.1 # 10% minimum state of charge self.soc_max = 0.9 # 90% maximum
def simulate_dispatch(self, dispatch_mw, hours=1): """Simulate one timestep of dispatch."""
Positive = discharge, negative = charge
energy_change = dispatch_mw * hours
if dispatch_mw > 0: # Discharging actual_output = min(dispatch_mw, self.power_mw) energy_used = actual_output / self.efficiency_oneway return actual_output, -energy_used
else: # Charging actual_input = max(dispatch_mw, -self.power_mw) energy_stored = actual_input * self.efficiency_oneway return actual_input, -energy_stored
def optimize_arbitrage(prices, battery, initial_soc=0.5): """Optimize storage dispatch for price arbitrage.""" n_hours = len(prices)
def objective(dispatch):
Maximize revenue (minimize negative revenue)
return -np.sum(dispatch * prices)
def soc_constraint(dispatch):
Track state of charge through time
soc = initial_soc * battery.capacity_mwh socs = [soc]
for d in dispatch: if d > 0: # Discharge soc -= d / battery.efficiency_oneway else: # Charge soc -= d * battery.efficiency_oneway
socs.append(soc)
return np.array(socs)
Constraints
constraints = [
SOC bounds
{'type': 'ineq', 'fun': lambda x: soc_constraint(x)[1:] - battery.soc_min battery.capacity_mwh}, {'type': 'ineq', 'fun': lambda x: battery.soc_max battery.capacity_mwh - soc_constraint(x)[1:]}, ]
Power limits
bounds = [(-battery.power_mw, battery.power_mw)] * n_hours
result = minimize( objective, x0=np.zeros(n_hours), bounds=bounds, constraints=constraints, method='SLSQP' )
return { 'dispatch_mw': result.x, 'revenue': -result.fun, 'soc_profile': soc_constraint(result.x) / battery.capacity_mwh }
Usage
prices = np.array([30, 25, 22, 20, 22, 35, 60, 80, 70, 55, 45, 40, 38, 35, 33, 35, 45, 75, 90, 70, 50, 40, 35, 30])
battery = BatteryStorage(capacity_mwh=100, power_mw=25, efficiency=0.90) result = optimize_arbitrage(prices, battery)
print(f"Daily revenue: ${result['revenue']:.2f}")
Demand Response
Description
Load management and demand response programs
Use When
Modeling flexible loads and DR programs
Implementation
import numpy as np from dataclasses import dataclass from typing import List, Callable
@dataclass class FlexibleLoad: name: str base_load_mw: np.ndarray # Hourly baseline flexibility_up_mw: np.ndarray # Can increase by flexibility_down_mw: np.ndarray # Can decrease by response_time_hours: float # Lead time required duration_limit_hours: float # Max continuous curtailment cost_per_mwh: float # Incentive or penalty
class DemandResponseProgram: def __init__(self, loads: List[FlexibleLoad]): self.loads = loads self.curtailment_history = {}
def calculate_available_dr(self, hour: int) -> dict: """Calculate available DR capacity at given hour.""" available_up = 0 available_down = 0
for load in self.loads:
Check duration limits
recent_curtailment = self._get_recent_curtailment(load.name, hour)
if recent_curtailment < load.duration_limit_hours: available_down += load.flexibility_down_mw[hour]
available_up += load.flexibility_up_mw[hour]
return { 'increase_mw': available_up, 'decrease_mw': available_down, 'hour': hour }
def dispatch_dr(self, hour: int, target_reduction_mw: float) -> dict: """Dispatch DR to achieve target reduction.""" dispatched = [] remaining = target_reduction_mw
Sort by cost (cheapest first)
sorted_loads = sorted(self.loads, key=lambda x: x.cost_per_mwh)
for load in sorted_loads: if remaining <= 0: break
available = load.flexibility_down_mw[hour] curtail = min(available, remaining)
if curtail > 0: dispatched.append({ 'load': load.name, 'curtailment_mw': curtail, 'cost': curtail * load.cost_per_mwh }) remaining -= curtail
return { 'target_mw': target_reduction_mw, 'achieved_mw': target_reduction_mw - remaining, 'dispatched': dispatched, 'total_cost': sum(d['cost'] for d in dispatched) }
def _get_recent_curtailment(self, load_name: str, hour: int) -> float:
Track consecutive curtailment hours
history = self.curtailment_history.get(load_name, []) return sum(1 for h in history if hour - h <= 4)
Economic Dispatch
Description
Generator dispatch optimization
Use When
Minimizing generation cost while meeting demand
Implementation
import numpy as np from scipy.optimize import minimize, LinearConstraint
@dataclass class Generator: name: str p_min_mw: float p_max_mw: float cost_a: float # $/MWh^2 (quadratic term) cost_b: float # $/MWh (linear term) cost_c: float # $ (no-load cost) ramp_rate_mw_per_hour: float
def cost(self, p_mw: float) -> float: """Quadratic cost function.""" if p_mw < self.p_min_mw or p_mw > self.p_max_mw: return float('inf') return self.cost_a p_mw2 + self.cost_b p_mw + self.cost_c
def marginal_cost(self, p_mw: float) -> float: """Marginal cost at given output.""" return 2 self.cost_a p_mw + self.cost_b
def economic_dispatch(generators: List[Generator], demand_mw: float) -> dict: """Solve economic dispatch using quadratic programming.""" n = len(generators)
def total_cost(p): return sum(g.cost(p[i]) for i, g in enumerate(generators))
Demand balance constraint
def demand_constraint(p): return np.sum(p) - demand_mw
constraints = [ {'type': 'eq', 'fun': demand_constraint} ]
Generator limits
bounds = [(g.p_min_mw, g.p_max_mw) for g in generators]
Initial guess (proportional to capacity)
total_cap = sum(g.p_max_mw for g in generators) x0 = [demand_mw * g.p_max_mw / total_cap for g in generators]
result = minimize( total_cost, x0=x0, bounds=bounds, constraints=constraints, method='SLSQP' )
if result.success: dispatch = result.x lmp = generators[0].marginal_cost(dispatch[0]) # System LMP
return { 'dispatch_mw': {g.name: dispatch[i] for i, g in enumerate(generators)}, 'total_cost': result.fun, 'lmp_dollar_per_mwh': lmp, 'demand_met': True } else: return {'demand_met': False, 'error': result.message}
Usage
generators = [ Generator("Coal", 100, 500, 0.002, 20, 500, 50), Generator("CCGT", 50, 300, 0.003, 35, 300, 100), Generator("Peaker", 0, 100, 0.01, 80, 100, 200), ]
result = economic_dispatch(generators, demand_mw=600) print(f"System LMP: ${result['lmp_dollar_per_mwh']:.2f}/MWh")
Reliability Metrics
Description
Grid reliability and adequacy metrics
Use When
Assessing system reliability and resource adequacy
Implementation
import numpy as np from scipy import stats
def calculate_lolp(capacity_mw: np.ndarray, demand_mw: np.ndarray, for_rates: np.ndarray) -> dict: """ Calculate Loss of Load Probability. capacity_mw: Available capacity per unit demand_mw: Hourly demand profile for_rates: Forced outage rates per unit """ n_hours = len(demand_mw) n_units = len(capacity_mw)
lol_hours = 0 total_unserved_mwh = 0
Monte Carlo simulation
n_simulations = 1000
for _ in range(n_simulations): for hour in range(n_hours):
Simulate outages
available = np.where( np.random.random(n_units) > for_rates, capacity_mw, 0 )
total_available = available.sum()
if total_available < demand_mw[hour]: lol_hours += 1 total_unserved_mwh += demand_mw[hour] - total_available
lole = lol_hours / n_simulations # Loss of Load Expectation eue = total_unserved_mwh / n_simulations # Expected Unserved Energy
return { 'lole_hours_per_year': lole, 'eue_mwh_per_year': eue, 'lolp': lol_hours / (n_simulations * n_hours) }
def calculate_reserve_margin(capacity_mw: float, peak_demand_mw: float) -> dict: """Calculate reserve margin.""" reserve_mw = capacity_mw - peak_demand_mw reserve_margin = (capacity_mw - peak_demand_mw) / peak_demand_mw
return { 'reserve_mw': reserve_mw, 'reserve_margin_pct': reserve_margin * 100, 'adequate': reserve_margin >= 0.15 # Typical 15% target }
Anti-Patterns
---
Pattern
DC power flow for voltage studies
Why
DC approximation ignores reactive power and voltage magnitudes
Instead
Use AC power flow for voltage and VAR analysis
---
Pattern
Ignoring ramp rate constraints
Why
Generators can't change output instantaneously
Instead
Include ramp limits in unit commitment/dispatch
---
Pattern
100% efficiency for storage
Why
Round-trip efficiency is 85-95%, not 100%
Instead
Model charging and discharging losses separately
---
Pattern
Single contingency analysis only
Why
N-1 is minimum; critical paths need N-2
Instead
Perform N-1-1 for critical infrastructure
---
Pattern
Static load models
Why
Loads vary with voltage and frequency
Instead
Use ZIP model (constant Z, I, P components)
---
Pattern
Ignoring transmission losses
Why
Losses are 2-6% of generation, non-trivial
Instead
Include loss factors in dispatch optimization
Energy Systems - Sharp Edges
Using DC Power Flow for Voltage Analysis
Id
dc-for-voltage
Severity
high
Summary
DC approximation ignores reactive power and voltage magnitudes
Symptoms
- Voltage violations not detected
- VAR support needs missed
- Capacitor/reactor sizing wrong
- Transformer tap settings ineffective
Why
DC power flow assumes:
- All voltages = 1.0 per unit
- Angle differences small (sin θ ≈ θ)
- No reactive power flows
- No losses
Good for: Fast contingency screening, market dispatch Bad for: Voltage stability, VAR planning, loss calculation
Errors can be 10-20% for heavily loaded lines. Voltage collapse scenarios completely missed.
Gotcha
DC power flow for transmission planning
import pandapower as pp
net = create_network() pp.rundcpp(net) # DC approximation
Check for overloads
overloaded = net.res_line[net.res_line['loading_percent'] > 100]
Looks fine! But wait...
What about voltage at remote bus?
DC flow says 1.0 pu everywhere
Reality: could be 0.92 pu, needing VAR support
Solution
1. Use AC power flow for planning studies
import pandapower as pp
net = create_network()
AC power flow - captures voltage and reactive power
pp.runpp(net, algorithm='nr', numba=True)
if not net.converged: print("Power flow did not converge - check model") return
2. Check voltage violations
voltage_violations = net.res_bus[ (net.res_bus['vm_pu'] < 0.95) | (net.res_bus['vm_pu'] > 1.05) ]
3. Check reactive power margins
for i, gen in net.gen.iterrows(): q_actual = net.res_gen.loc[i, 'q_mvar'] q_max = gen['max_q_mvar'] q_min = gen['min_q_mvar']
if q_actual > 0.9 q_max or q_actual < 0.9 q_min: print(f"Gen {i} near reactive power limit")
4. Use DC only for screening, then AC for detailed
def contingency_screening(net, contingencies):
DC screening (fast)
dc_overloads = [] for c in contingencies: net_temp = net.deepcopy() apply_contingency(net_temp, c) pp.rundcpp(net_temp) if has_overload(net_temp): dc_overloads.append(c)
AC analysis (accurate) on flagged cases
ac_violations = [] for c in dc_overloads: net_temp = net.deepcopy() apply_contingency(net_temp, c) pp.runpp(net_temp) if has_violations(net_temp): ac_violations.append(c)
return ac_violations
Battery Dispatch Without Degradation Modeling
Id
storage-degradation-ignored
Severity
high
Summary
Cycle life and capacity fade not in optimization
Symptoms
- Battery degrades faster than expected
- Warranty violated by excessive cycling
- Replacement costs underestimated
- NPV of storage project negative
Why
Li-ion battery degradation depends on:
- Cycle depth (deeper = more degradation)
- Temperature (higher = faster fade)
- State of charge (high SOC = faster calendar aging)
- C-rate (faster charging = more stress)
Ignoring degradation leads to:
- Aggressive cycling that kills battery early
- Underestimated levelized storage cost
- Warranty issues (cycle count limits)
Gotcha
Arbitrage optimization
def optimize_arbitrage(prices, battery):
Maximize revenue
dispatch = solve_lp(prices, battery.power, battery.capacity) cycles_per_day = calculate_cycles(dispatch)
Result: 2 full cycles per day = 730 cycles/year
annual_revenue = sum(dispatch * prices)
Looks great! $50k/year revenue!
But battery rated for 3000 cycles over 10 years
At 730/year, battery dead in 4 years
Replacement cost: $200k
Actually losing money
Solution
1. Include degradation cost in optimization
class BatteryWithDegradation: def __init__(self, capacity_mwh, power_mw, efficiency=0.90, replacement_cost=200000, cycle_life=3000): self.capacity = capacity_mwh self.power = power_mw self.efficiency = efficiency self.replacement_cost = replacement_cost self.cycle_life = cycle_life
Cost per kWh throughput
self.degradation_cost = replacement_cost / (cycle_life capacity_mwh 1000)
def cost_per_cycle(self, depth_of_discharge): """Non-linear degradation - deeper cycles cost more."""
Empirical: DoD^1.5 relationship
return self.degradation_cost depth_of_discharge * 1.5
2. Include in dispatch optimization
def optimize_with_degradation(prices, battery): n = len(prices)
def objective(dispatch): revenue = np.sum(dispatch * prices)
Calculate cycle degradation cost
energy_throughput = np.sum(np.abs(dispatch)) equivalent_cycles = energy_throughput / (2 battery.capacity) degradation_cost = equivalent_cycles battery.replacement_cost / battery.cycle_life
return -(revenue - degradation_cost) # Maximize net value
Solve with degradation in objective
result = minimize(objective, ...) return result
3. Enforce SOC limits to reduce calendar aging
Keeping SOC between 20-80% reduces stress
4. Temperature-aware dispatch
Limit power in high ambient temperatures
Dispatch Without Ramp Rate Constraints
Id
ramp-constraints-ignored
Severity
high
Summary
Generators can't change output instantaneously
Symptoms
- Infeasible schedules
- Frequency deviations
- ACE violations
- Operator manual interventions
Why
Generator ramp rates (typical):
- Coal: 1-3% of capacity per minute
- CCGT: 5-8% per minute
- Simple cycle GT: 10-15% per minute
- Hydro: 50%+ per minute
Ignoring ramp rates leads to:
- Scheduled output physically impossible
- Need for expensive balancing reserves
- Reliability standard violations
Gotcha
5-minute dispatch
demand = [100, 150, 200, 180, 120] # MW per period
Simple economic dispatch
for t, d in enumerate(demand): dispatch[t] = economic_dispatch(generators, d)
Result:
t=0: Coal=100
t=1: Coal=100, CCGT=50 (CCGT ramped 50 MW in 5 min - impossible!)
CCGT can only ramp 15 MW in 5 minutes
Solution
1. Include ramp constraints in optimization
import pyomo.environ as pyo
def unit_commitment_with_ramps(generators, demand, timesteps): model = pyo.ConcreteModel()
Sets
model.G = pyo.Set(initialize=range(len(generators))) model.T = pyo.Set(initialize=range(timesteps))
Variables
model.p = pyo.Var(model.G, model.T, domain=pyo.NonNegativeReals) model.u = pyo.Var(model.G, model.T, domain=pyo.Binary) # On/off
Ramp constraints
def ramp_up_rule(m, g, t): if t == 0: return pyo.Constraint.Skip return m.p[g,t] - m.p[g,t-1] <= generators[g].ramp_up_mw
def ramp_down_rule(m, g, t): if t == 0: return pyo.Constraint.Skip return m.p[g,t-1] - m.p[g,t] <= generators[g].ramp_down_mw
model.ramp_up = pyo.Constraint(model.G, model.T, rule=ramp_up_rule) model.ramp_down = pyo.Constraint(model.G, model.T, rule=ramp_down_rule)
Min up/down time constraints
... (additional constraints)
return model
2. Look-ahead dispatch
Consider ramp needs for future periods
3. Include startup/shutdown trajectories
Generators have specific ramp profiles during transitions
N-1 Only for Critical Infrastructure
Id
single-contingency-only
Severity
high
Summary
Single contingency analysis misses cascading failures
Symptoms
- Cascading outages
- Multiple element failures cause blackouts
- NERC Category D events
- Wide-area disturbances
Why
N-1 contingency standard:
- System must survive loss of any single element
- Standard for normal planning
But critical systems need N-1-1:
- Loss of element, then loss of another before restoration
- Common mode failures (same corridor, same weather)
- Protection system failures
2003 Northeast blackout: N-1-1 would have flagged issues.
Gotcha
Standard N-1 contingency analysis
def n1_analysis(net, elements): violations = [] for element in elements: net_temp = net.deepcopy() trip_element(net_temp, element)
if not is_secure(net_temp): violations.append(element)
return violations
Result: No violations! System is N-1 secure.
But what if line A trips, then line B (parallel path)?
Both overload, cascade begins...
Solution
1. N-1-1 for critical infrastructure
def n1_1_analysis(net, elements, critical_elements): violations = []
for first in critical_elements: net_temp = net.deepcopy() trip_element(net_temp, first)
System must be secure after first contingency
if not is_secure(net_temp): violations.append((first, None)) continue
Now check second contingency
for second in elements: if second == first: continue
net_temp2 = net_temp.deepcopy() trip_element(net_temp2, second)
if not is_secure(net_temp2): violations.append((first, second))
return violations
2. Common mode contingencies
Elements in same corridor, subject to same weather
def identify_common_mode(net): corridors = group_by_corridor(net.line) weather_zones = group_by_weather_zone(net)
common_mode_groups = [] for corridor, lines in corridors.items(): if len(lines) > 1: common_mode_groups.append(lines)
return common_mode_groups
3. Protection system failures
Analyze breaker failure contingencies
4. Extreme event analysis
Beyond N-1-1 for rare but high-impact events
Using Static Load Models for Dynamic Studies
Id
static-load-models
Severity
medium
Summary
Loads change with voltage and frequency
Symptoms
- Simulation doesn't match field events
- Voltage recovery too fast or slow
- Frequency response inaccurate
- Motor stalling not captured
Why
Real loads are voltage/frequency dependent:
- Resistive loads: P ∝ V²
- Motor loads: Complex P-V characteristic, can stall
- Electronic loads: Constant power (bad for voltage stability)
ZIP model: P = P0 (ZV² + IV + P1) where Z + I + P = 1
WECC composite load model includes:
- Motor dynamics
- Electronic load fraction
- Distributed generation
Gotcha
Dynamic simulation
pp.create_load(net, bus=1, p_mw=100, q_mvar=30)
Fault simulation
apply_fault(net, bus=2, duration=0.1) simulate_dynamics(net)
Result shows voltage recovers nicely
But in reality, with 40% motor load:
- Motors decelerate during fault
- Draw high current on recovery
- Possible motor stalling → delayed recovery
Solution
1. Use ZIP load model
def create_zip_load(net, bus, p_mw, q_mvar, zip_p=(0.3, 0.4, 0.3), # Constant Z, I, P fractions zip_q=(0.3, 0.4, 0.3)): """Create voltage-dependent load."""
During simulation, P scales with voltage
P = P0 (zV² + i*V + p)
pass # Depends on simulation tool
2. Include motor models for industrial loads
class MotorLoad: def __init__(self, p_mw, power_factor=0.85): self.p_mw = p_mw self.pf = power_factor self.stall_voltage = 0.7 # Stalls below 70%
def current(self, voltage_pu): if voltage_pu < self.stall_voltage: return self.locked_rotor_current() return self.normal_current(voltage_pu)
3. Use composite load models for bulk system
WECC CLM, EPRI LOADSYN
4. Validate against actual events
Compare simulation to recorded disturbances
Energy Systems - Validations
DC Power Flow for Voltage Analysis
Id
dc-power-flow-voltage
Severity
warning
Type
regex
Pattern
- rundcpp.voltage|voltage.rundcpp
- dc_power_flow.*volt(?!age_angle)
Message
DC power flow ignores voltage magnitudes. Use AC for voltage studies.
Fix Action
Use: pp.runpp(net) instead of pp.rundcpp(net)
Applies To
- */.py
Battery Storage Without Efficiency
Id
storage-no-efficiency
Severity
warning
Type
regex
Pattern
- energy_out\s=\senergy_in(?!.*eff)
- soc.charge.discharge(?!.*loss|eff)
Message
Battery round-trip efficiency is 85-95%, not 100%.
Fix Action
Apply: energy_out = energy_in * sqrt(round_trip_eff) for each direction
Applies To
- */.py
Battery Optimization Without Degradation
Id
storage-no-degradation
Severity
info
Type
regex
Pattern
- arbitrage|dispatch.optim(?!.degrad|cycle|life)
- maximize.revenue.storage(?!.*wear)
Message
Include degradation cost in storage optimization.
Fix Action
Add degradation cost = cycles * (replacement_cost / cycle_life)
Applies To
- */.py
Dispatch Without Ramp Constraints
Id
no-ramp-constraints
Severity
warning
Type
regex
Pattern
- dispatch\[t\].=(?!.ramp)
- power\[t\].=.demand(?!.*ramp|limit)
Message
Include generator ramp rate constraints in dispatch.
Fix Action
Add: abs(p[t] - p[t-1]) <= ramp_rate * timestep
Applies To
- */.py
Unit Commitment Without Min Up/Down Time
Id
no-min-up-down
Severity
info
Type
regex
Pattern
- unit_commitment(?!.*min_up|min_down)
- on_off.var(?!.up_time|down_time)
Message
Include minimum up/down time constraints for thermal units.
Fix Action
Add: sum(u[t-min_up:t]) >= min_up * (u[t] - u[t-1])
Applies To
- */.py
Only N-1 Contingency Analysis
Id
single-contingency
Severity
info
Type
regex
Pattern
- contingency.for.element(?!.*n_1_1|n-1-1|second)
- n_minus_1(?!.*n_minus_2|cascade)
Message
Consider N-1-1 for critical infrastructure.
Fix Action
After N-1, check second contingency before system is restored.
Applies To
- */.py
Static Load in Dynamic Simulation
Id
constant-load
Severity
info
Type
regex
Pattern
- dynamic.sim.p_mw\s=\s\d+(?!.*zip|composite)
- transient.load.const
Message
Use voltage-dependent load models for dynamic studies.
Fix Action
Apply ZIP model: P = P0 (zV² + i*V + p)
Applies To
- */.py
Power Flow Without Balance Verification
Id
no-power-balance-check
Severity
warning
Type
regex
Pattern
- runpp|runpf(?!.*balance|verify|check)
Message
Verify power balance after power flow simulation.
Fix Action
Check: abs(sum(gen) - sum(load) - losses) < tolerance
Applies To
- */.py
Mixing Per-Unit and Physical Units
Id
per-unit-mismatch
Severity
warning
Type
regex
Pattern
- pu.\.mw|mw.\.pu(?!.*base)
- v_pu.\+.kv|kv.\+.v_pu
Message
Don't mix per-unit and physical units without base conversion.
Fix Action
Convert: MW = pu S_base; kV = pu V_base
Applies To
- */.py
Dispatch Ignoring Transmission Losses
Id
ignore-losses
Severity
info
Type
regex
Pattern
- demand.=.sum.gen(?!.loss)
- balance.gen.load(?!.*loss)
Message
Include transmission losses (2-6% of generation).
Fix Action
Add: sum(gen) = sum(load) + losses
Applies To
- */.py
Generator Without Reactive Power Limits
Id
no-reactive-limits
Severity
info
Type
regex
Pattern
- create_gen.p_mw(?!.q_max|q_min)
- gen.reactive(?!.limit|cap)
Message
Define generator reactive power limits for voltage analysis.
Fix Action
Add: max_q_mvar and min_q_mvar to generator definition
Applies To
- */.py
Large Disturbance Without Frequency Response
Id
frequency-ignored
Severity
info
Type
regex
Pattern
- trip.gen.(?!.*frequency|inertia|governor)
- contingency.generator(?!.freq)
Message
Model frequency response for generator trip studies.
Fix Action
Include inertia constant H, governor droop, load damping.
Applies To
- */.py