From 7e319a467340ff0e73b6fb0909bb24091047ec2a Mon Sep 17 00:00:00 2001 From: m-bossart Date: Wed, 23 Sep 2026 21:36:51 -0700 Subject: [PATCH 1/5] Store remote and shared voltage control Replace the plain-integer regulated_bus_number, remote_bus_control and rmpct columns with nullable foreign keys to the bus table: remote_regulated_bus_id on generators, storage, synchronous condensers, sources, switched shunts, FACTS devices and converters, the VSC and LCC references on two_terminal_hvdc_lines, and regulated_bus_id with regulated_bus_side and load_drop_compensation on transformer_circuits. Generators also get voltage_setpoint with its unit basis. Same-row CHECKs reject a device naming its own bus and a transformer naming a bus without regulating voltage; triggers enforce the rules that need the arc. Add voltage_control_groups (VoltageDroopControl and ReactivePowerSharing, modeled on plants with the droop characteristic as typed columns) and voltage_control_associations with positive weights, one group per member or converter terminal, and a member-type trigger. Register the new columns, add the Impedance per-unit basis rule, regenerate the sealed registry and the reference DDL, and bump user_version to 2. --- schema/column_conventions.json | 27 +- schema/coverage_decisions.json | 4 - schema/generated_schema.sql | 41 +- schema/schema.sql | 152 ++++++- schema/sql_codegen_map.json | 6 +- schema/triggers.sql | 147 +++++++ schema/unit_registry.sql | 33 +- scripts/generate_unit_registry.py | 5 + test/conftest.py | 36 ++ test/test_schema_integrity.py | 49 +-- test/test_sql_codegen.py | 3 + test/test_unit_registry.py | 6 +- test/test_voltage_control.py | 633 ++++++++++++++++++++++++++++++ 13 files changed, 1057 insertions(+), 85 deletions(-) create mode 100644 test/test_voltage_control.py diff --git a/schema/column_conventions.json b/schema/column_conventions.json index a4872b7c..b1fbabca 100644 --- a/schema/column_conventions.json +++ b/schema/column_conventions.json @@ -67,6 +67,8 @@ {"table": "transformer_circuits", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, {"table": "transformer_circuits", "column": "base_voltage_primary", "quantity_kind": "Voltage", "unit": "kV", "description": "Primary (from) terminal-side base voltage; reference for the circuit's pu impedance"}, {"table": "transformer_circuits", "column": "base_voltage_secondary", "quantity_kind": "Voltage", "unit": "kV", "description": "Secondary (to) terminal-side base voltage"}, + {"table": "transformer_circuits", "column": "load_drop_compensation", "quantity_kind": "Impedance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "base_voltage_ref": "base_voltage_primary", "description": "Load drop compensation impedance (JSON real/imag = PSS/E CR/CX), per-unit on the row's base_power, referenced to base_voltage_primary"}, + {"table": "transformer_circuits", "column": "load_drop_compensation", "quantity_kind": "Impedance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Load drop compensation impedance (JSON real/imag) in ohm"}, {"table": "two_winding_transformers", "column": "magnetizing_shunt.real", "quantity_kind": "Conductance", "unit": "pu", "base_power_ref": "circuit->transformer_circuits.base_power", "base_voltage_ref": "circuit->transformer_circuits.base_voltage_primary", "description": "Magnetizing shunt conductance, per-unit on the circuit's base_power (magnetizing_shunt JSON path)"}, {"table": "two_winding_transformers", "column": "magnetizing_shunt.imag", "quantity_kind": "Susceptance", "unit": "pu", "base_power_ref": "circuit->transformer_circuits.base_power", "base_voltage_ref": "circuit->transformer_circuits.base_voltage_primary", "description": "Magnetizing shunt susceptance, per-unit on the circuit's base_power (magnetizing_shunt JSON path)"}, {"table": "three_winding_transformers", "column": "r_12", "quantity_kind": "Resistance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power_12", "base_voltage_ref": "primary_circuit->transformer_circuits.base_voltage_primary", "description": "Measured primary-secondary resistance, per-unit on base_power_12, referred to the primary winding's voltage base per PSS/E convention"}, @@ -108,6 +110,8 @@ {"table": "thermal_generators", "column": "production_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "COST", "description": "Average-rate cost curve (production_cost column); payload power_units must be NATURAL_UNITS"}, {"table": "thermal_generators", "column": "production_cost", "quantity_kind": "HeatRate", "unit": "MMBtu/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "INCREMENTAL", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "FUEL", "description": "Incremental heat rate of a FuelCurve; the money is carried separately by fuel_cost (production_cost column)"}, {"table": "thermal_generators", "column": "production_cost", "quantity_kind": "HeatRate", "unit": "MMBtu/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "FUEL", "description": "Average heat rate of a FuelCurve; the money is carried separately by fuel_cost (production_cost column)"}, + {"table": "thermal_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology"}, + {"table": "thermal_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "renewable_generators", "column": "rating", "quantity_kind": "ApparentPower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Nameplate rating, per-unit on the row's base_power"}, {"table": "renewable_generators", "column": "rating", "quantity_kind": "ApparentPower", "unit": "MVA", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Nameplate rating in MVA"}, {"table": "renewable_generators", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, @@ -125,6 +129,8 @@ {"table": "renewable_generators", "column": "operation_cost.curtailment_cost", "quantity_kind": "CostPerTime", "unit": "USD/h", "discriminator_column": "operation_cost.curtailment_cost.value_curve.curve_type", "discriminator_value": "INPUT_OUTPUT", "description": "Curtailment input-output cost curve: y is a cost rate at a power level (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, {"table": "renewable_generators", "column": "operation_cost.curtailment_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "operation_cost.curtailment_cost.value_curve.curve_type", "discriminator_value": "INCREMENTAL", "description": "Curtailment incremental (marginal) cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, {"table": "renewable_generators", "column": "operation_cost.curtailment_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "operation_cost.curtailment_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "description": "Curtailment average-rate cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, + {"table": "renewable_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology"}, + {"table": "renewable_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "hydro_generators", "column": "rating", "quantity_kind": "ApparentPower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Nameplate rating, per-unit on the row's base_power"}, {"table": "hydro_generators", "column": "rating", "quantity_kind": "ApparentPower", "unit": "MVA", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Nameplate rating in MVA"}, {"table": "hydro_generators", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, @@ -149,6 +155,8 @@ {"table": "hydro_generators", "column": "production_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "COST", "description": "Average-rate cost curve (production_cost column); payload power_units must be NATURAL_UNITS"}, {"table": "hydro_generators", "column": "production_cost", "quantity_kind": "HeatRate", "unit": "MMBtu/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "INCREMENTAL", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "FUEL", "description": "Incremental heat rate of a FuelCurve; the money is carried separately by fuel_cost (production_cost column)"}, {"table": "hydro_generators", "column": "production_cost", "quantity_kind": "HeatRate", "unit": "MMBtu/MWh", "discriminator_column": "production_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "discriminator_column_2": "production_cost.variable_cost_type", "discriminator_value_2": "FUEL", "description": "Average heat rate of a FuelCurve; the money is carried separately by fuel_cost (production_cost column)"}, + {"table": "hydro_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology"}, + {"table": "hydro_generators", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "storage_units", "column": "rating", "quantity_kind": "ApparentPower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Nameplate rating, per-unit on the row's base_power"}, {"table": "storage_units", "column": "rating", "quantity_kind": "ApparentPower", "unit": "MVA", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Nameplate rating in MVA"}, {"table": "storage_units", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, @@ -184,6 +192,8 @@ {"table": "storage_units", "column": "operation_cost.discharge_variable_cost", "quantity_kind": "CostPerTime", "unit": "USD/h", "discriminator_column": "operation_cost.discharge_variable_cost.value_curve.curve_type", "discriminator_value": "INPUT_OUTPUT", "description": "Discharge input-output cost curve: y is a cost rate at a power level (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, {"table": "storage_units", "column": "operation_cost.discharge_variable_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "operation_cost.discharge_variable_cost.value_curve.curve_type", "discriminator_value": "INCREMENTAL", "description": "Discharge incremental (marginal) cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, {"table": "storage_units", "column": "operation_cost.discharge_variable_cost", "quantity_kind": "CostPerEnergy", "unit": "USD/MWh", "discriminator_column": "operation_cost.discharge_variable_cost.value_curve.curve_type", "discriminator_value": "AVERAGE_RATE", "description": "Discharge average-rate cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS"}, + {"table": "storage_units", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology"}, + {"table": "storage_units", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "hydro_reservoirs", "column": "storage_level_limits", "quantity_kind": "Volume", "unit": "m3", "discriminator_column": "level_data_type", "discriminator_value": "USABLE_VOLUME", "description": "JSON {min, max}"}, {"table": "hydro_reservoirs", "column": "storage_level_limits", "quantity_kind": "Volume", "unit": "m3", "discriminator_column": "level_data_type", "discriminator_value": "TOTAL_VOLUME", "description": "JSON {min, max}"}, {"table": "hydro_reservoirs", "column": "storage_level_limits", "quantity_kind": "Elevation", "unit": "m", "discriminator_column": "level_data_type", "discriminator_value": "HEAD", "description": "JSON {min, max}"}, @@ -225,7 +235,6 @@ {"table": "switched_admittance", "column": "admittance_limits", "quantity_kind": "Susceptance", "unit": "S", "discriminator_column": "admittance_units", "discriminator_value": "NATURAL_UNITS", "description": "Admittance limits (JSON min/max) in siemens"}, {"table": "switched_admittance", "column": "admittance_limits", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "admittance_units", "discriminator_value": "COMPONENT_MVAR", "description": "Admittance limits (JSON min/max) in the PSS/E form: MVAr at 1.0 pu voltage"}, {"table": "switched_admittance", "column": "control_mode", "quantity_kind": "Fraction", "unit": "1", "description": "Switched-shunt control mode"}, - {"table": "switched_admittance", "column": "regulated_bus_number", "quantity_kind": "Fraction", "unit": "1", "description": "Bus whose voltage/quantity this shunt regulates; 0 = local bus"}, {"table": "synchronous_condensers", "column": "reactive_power", "quantity_kind": "ReactivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Initial reactive power setpoint, per-unit on the row's base_power"}, {"table": "synchronous_condensers", "column": "reactive_power", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Initial reactive power setpoint in MVAr"}, {"table": "synchronous_condensers", "column": "rating", "quantity_kind": "ApparentPower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Maximum AC-side output rating, per-unit on the row's base_power"}, @@ -235,6 +244,8 @@ {"table": "synchronous_condensers", "column": "reactive_power_limits", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Reactive power limits, JSON {min, max} in MVAr"}, {"table": "synchronous_condensers", "column": "active_power_losses", "quantity_kind": "ActivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Active power loss incurred by being online, per-unit on the row's base_power"}, {"table": "synchronous_condensers", "column": "active_power_losses", "quantity_kind": "ActivePower", "unit": "MW", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Active power loss incurred by being online in MW"}, + {"table": "synchronous_condensers", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else bus"}, + {"table": "synchronous_condensers", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "sources", "column": "base_voltage", "quantity_kind": "Voltage", "unit": "kV", "description": "Nominal voltage of the source terminal"}, {"table": "sources", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, {"table": "sources", "column": "active_power", "quantity_kind": "ActivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Active power injection setpoint, per-unit on the row's base_power"}, @@ -259,6 +270,8 @@ {"table": "sources", "column": "r_th", "quantity_kind": "Resistance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Thevenin resistance in ohm"}, {"table": "sources", "column": "x_th", "quantity_kind": "Reactance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "base_voltage_ref": "base_voltage", "description": "Thevenin reactance, per-unit on the row's base_power"}, {"table": "sources", "column": "x_th", "quantity_kind": "Reactance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Thevenin reactance in ohm"}, + {"table": "sources", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "remote_regulated_bus_id->balancing_topologies.base_voltage", "description": "Voltage held at the regulated bus, per-unit on that bus's base voltage; the regulated bus is remote_regulated_bus_id when set, else bus"}, + {"table": "sources", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_setpoint_units", "discriminator_value": "NATURAL_UNITS", "description": "Voltage held at the regulated bus in kV"}, {"table": "two_terminal_hvdc_lines", "column": "active_power_flow", "quantity_kind": "ActivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Active power flow through the line, per-unit on the row's base_power"}, {"table": "two_terminal_hvdc_lines", "column": "active_power_flow", "quantity_kind": "ActivePower", "unit": "MW", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Active power flow through the line in MW"}, {"table": "two_terminal_hvdc_lines", "column": "active_power_limits_from", "quantity_kind": "ActivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "From-terminal active power limits, per-unit on the row's base_power"}, @@ -276,7 +289,6 @@ {"table": "facts_control_devices", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "bus->balancing_topologies.base_voltage", "description": "Sending-end voltage setpoint, per-unit on the bus's base voltage"}, {"table": "facts_control_devices", "column": "voltage_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Sending-end voltage setpoint in kV"}, {"table": "facts_control_devices", "column": "shunt_control_type", "quantity_kind": "Fraction", "unit": "1", "description": "Device class selecting the reactive-limit law (SVC vs STATCOM)"}, - {"table": "facts_control_devices", "column": "regulated_bus_number", "quantity_kind": "Fraction", "unit": "1", "description": "Bus whose voltage this device regulates; 0 = local bus"}, {"table": "facts_control_devices", "column": "max_reactive_power", "quantity_kind": "ReactivePower", "unit": "pu", "discriminator_column": "power_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "description": "Independent max reactive power ceiling, per-unit on the row's base_power"}, {"table": "facts_control_devices", "column": "max_reactive_power", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "power_units", "discriminator_value": "NATURAL_UNITS", "description": "Independent max reactive power ceiling in MVAr"}, {"table": "facts_control_devices", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, @@ -288,11 +300,16 @@ {"table": "interconnecting_converters", "column": "ac_setpoint", "quantity_kind": "PowerFactor", "unit": "1", "discriminator_column": "ac_control", "discriminator_value": "AC_REACTIVE_POWER", "description": "AC power factor setpoint"}, {"table": "interconnecting_converters", "column": "ac_setpoint", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "ac_control", "discriminator_value": "AC_VOLTAGE", "discriminator_column_2": "parameter_units", "discriminator_value_2": "COMPONENT_BASE", "base_voltage_ref": "bus->balancing_topologies.base_voltage", "description": "AC voltage setpoint, per-unit"}, {"table": "interconnecting_converters", "column": "ac_setpoint", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "ac_control", "discriminator_value": "AC_VOLTAGE", "discriminator_column_2": "parameter_units", "discriminator_value_2": "NATURAL_UNITS", "description": "AC voltage setpoint in kV"}, - {"table": "interconnecting_converters", "column": "rmpct", "quantity_kind": "Fraction", "unit": "1", "description": "Remote-bus voltage-control percentage compensation"}, {"table": "interconnecting_converters", "column": "power_factor_weighting_fraction", "quantity_kind": "Fraction", "unit": "1", "description": "Reactive/active power-factor weighting fraction"}, - {"table": "interconnecting_converters", "column": "remote_bus_control", "quantity_kind": "Fraction", "unit": "1", "description": "Remote bus whose voltage this converter regulates; null = local bus"}, {"table": "interconnecting_converters", "column": "voltage_limits", "quantity_kind": "Voltage", "unit": "pu", "base_voltage_ref": "dc_bus->balancing_topologies.base_voltage", "description": "JSON {min, max} voltage limits"}, {"table": "interconnecting_converters", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, + {"table": "voltage_control_groups", "column": "reactive_power_limits", "quantity_kind": "ReactivePower", "unit": "MVAr", "description": "Droop controller reactive power range (JSON min/max, PSS/E QMIN/QMAX)"}, + {"table": "voltage_control_groups", "column": "deadband_reactive_power", "quantity_kind": "ReactivePower", "unit": "MVAr", "description": "Droop controller reactive power held inside the voltage deadband (PSS/E QDB)"}, + {"table": "voltage_control_groups", "column": "deadband_voltage_limits", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "regulated_bus_id->balancing_topologies.base_voltage", "description": "Droop controller voltage deadband (JSON min/max, PSS/E VDBLOW/VDBHIGH), per-unit on the regulated bus's base voltage"}, + {"table": "voltage_control_groups", "column": "deadband_voltage_limits", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_units", "discriminator_value": "NATURAL_UNITS", "description": "Droop controller voltage deadband (JSON min/max) in kV"}, + {"table": "voltage_control_groups", "column": "voltage_limits", "quantity_kind": "Voltage", "unit": "pu", "discriminator_column": "voltage_units", "discriminator_value": "COMPONENT_BASE", "base_voltage_ref": "regulated_bus_id->balancing_topologies.base_voltage", "description": "Droop controller voltage range (JSON min/max, PSS/E VLOW/VHIGH), per-unit on the regulated bus's base voltage"}, + {"table": "voltage_control_groups", "column": "voltage_limits", "quantity_kind": "Voltage", "unit": "kV", "discriminator_column": "voltage_units", "discriminator_value": "NATURAL_UNITS", "description": "Droop controller voltage range (JSON min/max) in kV"}, + {"table": "voltage_control_associations", "column": "weight", "quantity_kind": "Dimensionless", "unit": "1", "description": "Relative reactive power share of one voltage control group member (PSS/E RMPCT / 100)"}, {"table": "supply_technologies", "column": "lifetime", "quantity_kind": "CalendarPeriod", "unit": "yr", "description": "Technology lifetime"}, {"table": "supply_technologies", "column": "unit_size", "quantity_kind": "ActivePower", "unit": "MW", "description": "Unit nameplate capacity"}, {"table": "supply_technologies", "column": "capacity_limits", "quantity_kind": "ActivePower", "unit": "MW", "description": "JSON {min, max} capacity limits"}, @@ -364,8 +381,6 @@ {"table": "attributes", "column": "rating_to", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "VSC to-converter rating"}, {"table": "attributes", "column": "reactive_power_from", "quantity_kind": "ReactivePower", "unit": "MVAr", "description": "VSC from-terminal reactive power"}, {"table": "attributes", "column": "reactive_power_to", "quantity_kind": "ReactivePower", "unit": "MVAr", "description": "VSC to-terminal reactive power"}, - {"table": "attributes", "column": "rmpct_from", "quantity_kind": "Fraction", "unit": "1", "description": "VSC from-converter remote-regulation share"}, - {"table": "attributes", "column": "rmpct_to", "quantity_kind": "Fraction", "unit": "1", "description": "VSC to-converter remote-regulation share"}, {"table": "attributes", "column": "power_factor_weighting_fraction_from", "quantity_kind": "Fraction", "unit": "1", "description": "VSC from-converter power-factor weighting"}, {"table": "attributes", "column": "power_factor_weighting_fraction_to", "quantity_kind": "Fraction", "unit": "1", "description": "VSC to-converter power-factor weighting"}, {"table": "attributes", "column": "magnitude", "quantity_kind": "Voltage", "unit": "pu", "description": "Bus voltage magnitude, per-unit on base_voltage"}, diff --git a/schema/coverage_decisions.json b/schema/coverage_decisions.json index 5559f406..0a3f01e4 100644 --- a/schema/coverage_decisions.json +++ b/schema/coverage_decisions.json @@ -278,10 +278,6 @@ "rectifier_tap_step": "attribute", "rectifier_transformer_ratio": "attribute", "rectifier_xc": "attribute", - "remote_bus_control_from": "attribute", - "remote_bus_control_to": "attribute", - "rmpct_from": "attribute", - "rmpct_to": "attribute", "scheduled_dc_voltage": "attribute", "setpoint_voltage_units": "skip", "switch_mode_voltage": "attribute", diff --git a/schema/generated_schema.sql b/schema/generated_schema.sql index c59908d7..9d1acd85 100644 --- a/schema/generated_schema.sql +++ b/schema/generated_schema.sql @@ -22,6 +22,9 @@ CREATE TABLE thermal_generators ( status TEXT NOT NULL CHECK (status IN ('OFFLINE', 'ONLINE', 'STARTUP', 'SHUTDOWN')), commitment_mode TEXT NULL DEFAULT 'COMMITTED' CHECK (commitment_mode IN ('UNCOMMITTED', 'COMMITTED', 'SELF_SCHEDULED', 'RELIABILITY', 'MUST_RUN')), balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) active_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MW) reactive_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) rating REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVA) @@ -44,6 +47,9 @@ CREATE TABLE renewable_generators ( name TEXT NOT NULL UNIQUE, available BOOLEAN NOT NULL, balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) active_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MW) reactive_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) rating REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVA) @@ -64,6 +70,9 @@ CREATE TABLE hydro_generators ( name TEXT NOT NULL UNIQUE, available BOOLEAN NOT NULL, balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) active_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MW) reactive_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) rating REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVA) @@ -95,6 +104,9 @@ CREATE TABLE storage_units ( name TEXT NOT NULL UNIQUE, available BOOLEAN NOT NULL, balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) prime_mover_type TEXT NOT NULL CHECK (prime_mover_type IN ('BA', 'BT', 'CA', 'CC', 'CE', 'CP', 'CS', 'CT', 'ES', 'FC', 'FW', 'GT', 'HA', 'HB', 'HK', 'HY', 'IC', 'PS', 'OT', 'ST', 'PVe', 'WT', 'WS')) REFERENCES prime_mover_types (name), storage_technology_type TEXT NOT NULL CHECK (storage_technology_type IN ('PTES', 'LIB', 'LAB', 'FLWB', 'SIB', 'ZIB', 'HGS', 'LAES', 'OTHER_CHEM', 'OTHER_MECH', 'OTHER_THERM')) REFERENCES storage_technology_types (name), storage_capacity REAL NOT NULL, -- Units: per energy_units (MWH: MWh, MWMIN: MWmin) @@ -238,7 +250,9 @@ CREATE TABLE transformer_circuits ( r REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) x REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) control_objective TEXT NULL DEFAULT 'UNDEFINED' CHECK (control_objective IN ('UNDEFINED', 'VOLTAGE_DISABLED', 'REACTIVE_POWER_FLOW_DISABLED', 'ACTIVE_POWER_FLOW_DISABLED', 'CONTROL_OF_DC_LINE_DISABLED', 'ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED', 'FIXED', 'VOLTAGE', 'REACTIVE_POWER_FLOW', 'ACTIVE_POWER_FLOW', 'CONTROL_OF_DC_LINE', 'ASYMMETRIC_ACTIVE_POWER_FLOW')), - regulated_bus_number INTEGER NULL DEFAULT 0, + regulated_bus_id INTEGER NULL, + regulated_bus_side JSON NULL, + load_drop_compensation JSON NULL DEFAULT '{"imag":0.0,"real":0.0}', -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) control_limits JSON NULL DEFAULT '{"max":1.1,"min":0.9}', -- Units: per control_objective (ACTIVE_POWER_FLOW: rad, ACTIVE_POWER_FLOW_DISABLED: rad, ASYMMETRIC_ACTIVE_POWER_FLOW: rad, ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED: rad, CONTROL_OF_DC_LINE: 1, CONTROL_OF_DC_LINE_DISABLED: 1, FIXED: 1, REACTIVE_POWER_FLOW: 1, REACTIVE_POWER_FLOW_DISABLED: 1, UNDEFINED: 1, VOLTAGE: 1, VOLTAGE_DISABLED: 1) controlled_quantity_limits JSON NULL DEFAULT '{"max":1.1,"min":0.9}', -- Units: per control_objective (ACTIVE_POWER_FLOW: MW, ACTIVE_POWER_FLOW_DISABLED: MW, ASYMMETRIC_ACTIVE_POWER_FLOW: MW, ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED: MW, CONTROL_OF_DC_LINE: MW, CONTROL_OF_DC_LINE_DISABLED: MW, FIXED: pu, REACTIVE_POWER_FLOW: MVAr, REACTIVE_POWER_FLOW_DISABLED: MVAr, UNDEFINED: pu, VOLTAGE: pu, VOLTAGE_DISABLED: pu) number_of_tap_positions INTEGER NULL DEFAULT 33, @@ -286,7 +300,7 @@ CREATE TABLE three_winding_transformers ( -- two_terminal_hvdc_lines: generated from TwoTerminalGenericHVDCLine, TwoTerminalLCCLine, TwoTerminalVSCLine -- Stored via the generic `attributes` table (registered attribute-name --- conventions), not as columns: loss, r, transfer_setpoint, scheduled_dc_voltage, rectifier_bridges, rectifier_delay_angle, rectifier_delay_angle_limits, rectifier_rc, rectifier_xc, rectifier_base_voltage, rectifier_transformer_ratio, rectifier_tap_setting, rectifier_tap_limits, rectifier_tap_step, rectifier_capacitor_reactance, inverter_bridges, inverter_extinction_angle, inverter_extinction_angle_limits, inverter_rc, inverter_xc, inverter_base_voltage, inverter_transformer_ratio, inverter_tap_setting, inverter_tap_limits, inverter_tap_step, inverter_capacitor_reactance, power_mode, switch_mode_voltage, compounding_resistance, min_compounding_voltage, rating, rating_from, rating_to, g, dc_current, rated_dc_voltage, reactive_power_from, reactive_power_to, dc_control_from, dc_control_to, ac_control_from, ac_control_to, dc_setpoint_from, dc_setpoint_to, ac_setpoint_from, ac_setpoint_to, converter_loss_from, converter_loss_to, max_dc_current_from, max_dc_current_to, power_factor_weighting_fraction_from, power_factor_weighting_fraction_to, voltage_limits_from, voltage_limits_to, dc_voltage_droop_from, dc_voltage_droop_to, remote_bus_control_from, remote_bus_control_to, rated_ac_voltage_from, rated_ac_voltage_to, rmpct_from, rmpct_to +-- conventions), not as columns: loss, r, transfer_setpoint, scheduled_dc_voltage, rectifier_bridges, rectifier_delay_angle, rectifier_delay_angle_limits, rectifier_rc, rectifier_xc, rectifier_base_voltage, rectifier_transformer_ratio, rectifier_tap_setting, rectifier_tap_limits, rectifier_tap_step, rectifier_capacitor_reactance, inverter_bridges, inverter_extinction_angle, inverter_extinction_angle_limits, inverter_rc, inverter_xc, inverter_base_voltage, inverter_transformer_ratio, inverter_tap_setting, inverter_tap_limits, inverter_tap_step, inverter_capacitor_reactance, power_mode, switch_mode_voltage, compounding_resistance, min_compounding_voltage, rating, rating_from, rating_to, g, dc_current, rated_dc_voltage, reactive_power_from, reactive_power_to, dc_control_from, dc_control_to, ac_control_from, ac_control_to, dc_setpoint_from, dc_setpoint_to, ac_setpoint_from, ac_setpoint_to, converter_loss_from, converter_loss_to, max_dc_current_from, max_dc_current_to, power_factor_weighting_fraction_from, power_factor_weighting_fraction_to, voltage_limits_from, voltage_limits_to, dc_voltage_droop_from, dc_voltage_droop_to, rated_ac_voltage_from, rated_ac_voltage_to CREATE TABLE two_terminal_hvdc_lines ( id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, name TEXT NOT NULL UNIQUE, @@ -298,7 +312,13 @@ CREATE TABLE two_terminal_hvdc_lines ( reactive_power_limits_from JSON NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) reactive_power_limits_to JSON NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) base_power REAL NOT NULL, -- Units: MVA - power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')) + power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), + rectifier_commutating_bus_id INTEGER NULL, + inverter_commutating_bus_id INTEGER NULL, + rectifier_tap_transformer_id INTEGER NULL, + inverter_tap_transformer_id INTEGER NULL, + remote_regulated_bus_id_from INTEGER NULL, + remote_regulated_bus_id_to INTEGER NULL ); -- tmodel_hvdc_lines: generated from TModelHVDCLine @@ -323,6 +343,9 @@ CREATE TABLE synchronous_condensers ( name TEXT NOT NULL UNIQUE, available BOOLEAN NOT NULL, bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) reactive_power REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) rating REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVA) reactive_power_limits JSON NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) @@ -355,7 +378,7 @@ CREATE TABLE switched_admittance ( solved_admittance REAL NULL, -- Units: per admittance_units (COMPONENT_MVAR: MVAr, NATURAL_UNITS: S) admittance_limits JSON NULL DEFAULT '{"max":1.0,"min":1.0}', -- Units: per admittance_units (COMPONENT_MVAR: MVAr, NATURAL_UNITS: S) control_mode TEXT NULL DEFAULT 'FIXED' CHECK (control_mode IN ('UNDEFINED', 'FIXED', 'DISCRETE_VOLTAGE', 'CONTINUOUS_VOLTAGE', 'DISCRETE_REACTIVE_PLANT', 'DISCRETE_REACTIVE_VSC', 'DISCRETE_ADMITTANCE_REMOTE')), - regulated_bus_number INTEGER NULL DEFAULT 0 -- Units: 1 + remote_regulated_bus_id INTEGER NULL ); -- sources: generated from Source @@ -364,6 +387,9 @@ CREATE TABLE sources ( name TEXT NOT NULL UNIQUE, available BOOLEAN NOT NULL, bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + remote_regulated_bus_id INTEGER NULL, + voltage_setpoint_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (voltage_setpoint_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), + voltage_setpoint REAL NULL DEFAULT 1.0, -- Units: per voltage_setpoint_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) active_power REAL NULL DEFAULT 0.0, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MW) reactive_power REAL NULL DEFAULT 0.0, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) active_power_limits JSON NULL DEFAULT '{"max":0.0,"min":0.0}', -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MW) @@ -401,8 +427,7 @@ CREATE TABLE interconnecting_converters ( dc_setpoint REAL NULL DEFAULT 0.0, -- Units: per dc_control (DC_POWER: MW; DC_VOLTAGE: per parameter_units [COMPONENT_BASE: pu, NATURAL_UNITS: kV]; DC_VOLTAGE_DROOP: per parameter_units [COMPONENT_BASE: pu, NATURAL_UNITS: kV]) ac_setpoint REAL NULL DEFAULT 1.0, -- Units: per ac_control (AC_REACTIVE_POWER: 1; AC_VOLTAGE: per parameter_units [COMPONENT_BASE: pu, NATURAL_UNITS: kV]) dc_voltage_droop REAL NULL DEFAULT 0.0, -- Units: pu - remote_bus_control INTEGER NULL, - rmpct REAL NULL DEFAULT 100.0, -- Units: 1 + remote_regulated_bus_id INTEGER NULL, power_factor_weighting_fraction REAL NULL DEFAULT 1.0, -- Units: 1 voltage_limits JSON NULL DEFAULT '{"max":999.9,"min":0.0}', -- Units: pu dynamic_injector INTEGER NULL @@ -418,10 +443,10 @@ CREATE TABLE facts_control_devices ( parameter_units TEXT NULL DEFAULT 'COMPONENT_BASE' CHECK (parameter_units IN ('NATURAL_UNITS', 'COMPONENT_BASE')), voltage_setpoint REAL NOT NULL, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: kV) max_shunt_current REAL NOT NULL, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVA) - reactive_power_required REAL NOT NULL, -- Units: 1 + reactive_power_required REAL NULL DEFAULT 0.0, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) max_reactive_power REAL NULL DEFAULT 9999.0, -- Units: per power_units (COMPONENT_BASE: pu, NATURAL_UNITS: MVAr) shunt_control_type TEXT NULL DEFAULT 'STATCOM' CHECK (shunt_control_type IN ('SVC', 'STATCOM')), - regulated_bus_number INTEGER NULL DEFAULT 0, -- Units: 1 + remote_regulated_bus_id INTEGER NULL, base_power REAL NOT NULL, -- Units: MVA power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), dynamic_injector INTEGER NULL diff --git a/schema/schema.sql b/schema/schema.sql index 20c7bdea..6958efcf 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -1,6 +1,6 @@ -- Requires SQLite >= 3.45. Test-only: drops every table below, so never run -- against a live dataset. -PRAGMA user_version = 1; -- first released schema version; bump on every schema or registry change +PRAGMA user_version = 2; -- bump on every schema or registry change DROP TABLE IF EXISTS thermal_generators; @@ -84,6 +84,10 @@ DROP TABLE IF EXISTS supplemental_attribute_associations; DROP TABLE IF EXISTS transport_technologies; +DROP TABLE IF EXISTS voltage_control_associations; + +DROP TABLE IF EXISTS voltage_control_groups; + DROP TABLE IF EXISTS combined_cycle_associations; DROP TABLE IF EXISTS plant_associations; @@ -251,8 +255,17 @@ CREATE TABLE transformer_circuits ( 'ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED', 'FIXED', 'VOLTAGE', 'REACTIVE_POWER_FLOW', 'ACTIVE_POWER_FLOW', 'CONTROL_OF_DC_LINE', 'ASYMMETRIC_ACTIVE_POWER_FLOW')), - -- Controlled bus number (sign = regulation side): - regulated_bus_number INTEGER NOT NULL DEFAULT 0, + -- Bus whose voltage the tap holds, named exactly when control_objective regulates + -- voltage. regulated_bus_side says which winding the bus lies beyond when it is not an + -- end of the circuit's arc; for an arc end the side follows from the arc and must be + -- NULL (enforce_transformer_circuits_regulated_bus_side_*). + regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + regulated_bus_side TEXT NULL + CHECK (regulated_bus_side IS NULL OR regulated_bus_side IN ('CONTROLLING_WINDING', 'OPPOSITE_WINDING')), + -- Load drop compensation {"real": CR, "imag": CX}: the impedance times the circuit + -- current that corrects the regulated voltage. + load_drop_compensation TEXT NOT NULL DEFAULT '{"real": 0.0, "imag": 0.0}' + CHECK (json_valid(load_drop_compensation)), -- Units: per parameter_units control_limits TEXT NULL DEFAULT '{"min": 0.9, "max": 1.1}' CHECK (control_limits IS NULL OR json_valid(control_limits)), -- Units: per control_objective (tap ratio 1 / angle rad) controlled_quantity_limits TEXT NULL DEFAULT '{"min": 0.9, "max": 1.1}' @@ -266,7 +279,9 @@ CREATE TABLE transformer_circuits ( base_power REAL NOT NULL CHECK (base_power > 0), -- Units: MVA power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), base_voltage_primary REAL NULL CHECK (base_voltage_primary > 0), -- Units: kV - base_voltage_secondary REAL NULL CHECK (base_voltage_secondary > 0) -- Units: kV + base_voltage_secondary REAL NULL CHECK (base_voltage_secondary > 0), -- Units: kV + CHECK ((regulated_bus_id IS NOT NULL) = (control_objective IN ('VOLTAGE', 'VOLTAGE_DISABLED'))), + CHECK (regulated_bus_side IS NULL OR regulated_bus_id IS NOT NULL) ) strict; -- Two-winding transformer (PSY TwoWindingTransformer); series data lives on @@ -341,6 +356,13 @@ CREATE TABLE thermal_generators ( prime_mover_type TEXT NOT NULL REFERENCES prime_mover_types(name), fuel TEXT NOT NULL DEFAULT 'OTHER' REFERENCES fuels(name), balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), rating REAL NOT NULL CHECK (rating >= 0), -- Units: per power_units base_power REAL NOT NULL CHECK (base_power > 0), power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), @@ -389,6 +411,13 @@ CREATE TABLE renewable_generators ( name TEXT NOT NULL UNIQUE, prime_mover_type TEXT NOT NULL REFERENCES prime_mover_types(name), balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), rating REAL NOT NULL CHECK (rating >= 0), -- Units: per power_units base_power REAL NOT NULL CHECK (base_power > 0), power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), @@ -427,6 +456,13 @@ CREATE TABLE hydro_generators ( name TEXT NOT NULL UNIQUE, prime_mover_type TEXT NOT NULL DEFAULT 'HY' REFERENCES prime_mover_types(name), balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), rating REAL NOT NULL CHECK (rating >= 0), -- Units: per power_units base_power REAL NOT NULL CHECK (base_power > 0), power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), @@ -475,6 +511,13 @@ CREATE TABLE storage_units ( prime_mover_type TEXT NOT NULL REFERENCES prime_mover_types(name), storage_technology_type TEXT NOT NULL REFERENCES storage_technology_types(name), balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), rating REAL NOT NULL CHECK (rating >= 0), -- Units: per power_units base_power REAL NOT NULL CHECK (base_power > 0), power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), @@ -716,6 +759,57 @@ CREATE TABLE combined_cycle_associations ( UNIQUE (plant_id, entity_id, hrsg_index) ) strict; +-- Voltage droop controllers and reactive power sharing groups (PSY VoltageDroopControl +-- and ReactivePowerSharing): supplemental attributes whose members jointly regulate one +-- bus. Modeled on plants: TYPE discriminates the two kinds. The columns after available +-- describe the droop controller's Q-V characteristic, which a sharing group does not +-- have, so they are set exactly on a droop row. Reactive powers are natural MVAr: the +-- attribute has no base_power to per-unitize against. Voltages follow voltage_units +-- (COMPONENT_BASE: pu on the regulated bus's base_voltage; NATURAL_UNITS: kV). +CREATE TABLE voltage_control_groups ( + id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name TEXT NOT NULL UNIQUE, + TYPE TEXT NOT NULL CHECK (TYPE IN ('VoltageDroopControl', 'ReactivePowerSharing')), + available INTEGER NOT NULL DEFAULT 1 CHECK (available IN (0, 1)), + regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + reactive_power_limits TEXT NULL -- {"min": ..., "max": ...} + CHECK (reactive_power_limits IS NULL OR json_valid(reactive_power_limits)), -- Units: MVAr + deadband_reactive_power REAL NULL, -- Units: MVAr + voltage_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), + deadband_voltage_limits TEXT NULL -- {"min": ..., "max": ...} + CHECK (deadband_voltage_limits IS NULL OR json_valid(deadband_voltage_limits)), -- Units: per voltage_units + voltage_limits TEXT NULL -- {"min": ..., "max": ...} + CHECK (voltage_limits IS NULL OR json_valid(voltage_limits)), -- Units: per voltage_units + CHECK ((TYPE = 'VoltageDroopControl') = (regulated_bus_id IS NOT NULL + AND reactive_power_limits IS NOT NULL AND deadband_reactive_power IS NOT NULL + AND deadband_voltage_limits IS NOT NULL AND voltage_limits IS NOT NULL)), + CHECK (TYPE = 'VoltageDroopControl' OR (regulated_bus_id IS NULL + AND reactive_power_limits IS NULL AND deadband_reactive_power IS NULL + AND deadband_voltage_limits IS NULL AND voltage_limits IS NULL)) +) strict; + +-- One row per member of a voltage control group: entity_id is the regulating device, or +-- a two-terminal VSC line with terminal naming the converter. weight is the member's +-- relative reactive power share (PSS/E RMPCT / 100); shares are relative, so a group's +-- rows need not sum to one. Which entities may join, and when terminal is required, is +-- enforced by enforce_voltage_control_member_types_*. Like plant_associations, the +-- relationship is GridDB's own, so it gets a local AUTOINCREMENT surrogate. +CREATE TABLE voltage_control_associations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + control_id INTEGER NOT NULL REFERENCES voltage_control_groups (id) ON DELETE CASCADE, + entity_id INTEGER NOT NULL REFERENCES entities (id) ON DELETE CASCADE, + weight REAL NOT NULL DEFAULT 1.0 CHECK (weight > 0), -- Units: 1 + terminal TEXT NULL CHECK (terminal IS NULL OR terminal IN ('FROM', 'TO')) +) strict; + +-- A device, or one converter of a VSC line, belongs to at most one group of either kind. +CREATE UNIQUE INDEX uq_voltage_control_member + ON voltage_control_associations (entity_id, COALESCE(terminal, '')); + +CREATE INDEX idx_voltage_control_assoc_control + ON voltage_control_associations (control_id, entity_id); + -- Mirrors infrastore's catalog table column-for-column so a row deserializes -- straight into a store, and projects onto the SiennaSchemas wire form. -- owner_category / time_series_type hold the wire string spelling directly @@ -912,8 +1006,10 @@ CREATE TABLE switched_admittance ( CHECK (control_mode IN ('UNDEFINED', 'FIXED', 'DISCRETE_VOLTAGE', 'CONTINUOUS_VOLTAGE', 'DISCRETE_REACTIVE_PLANT', 'DISCRETE_REACTIVE_VSC', 'DISCRETE_ADMITTANCE_REMOTE')), - -- 0 = local bus: - regulated_bus_number INTEGER NOT NULL DEFAULT 0 + -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own + -- bus is rejected. A shared regulation weight lives on voltage_control_associations: + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus) ) strict; -- Synchronous machine for inertia or reactive support (PSY SynchronousCondenser). @@ -924,6 +1020,13 @@ CREATE TABLE synchronous_condensers ( id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, name TEXT NOT NULL UNIQUE, bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), available INTEGER NOT NULL DEFAULT 1 CHECK (available IN (0, 1)), reactive_power REAL NOT NULL DEFAULT 0.0, -- Units: per power_units rating REAL NOT NULL CHECK (rating > 0), -- Units: per power_units @@ -944,6 +1047,13 @@ CREATE TABLE sources ( id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, name TEXT NOT NULL UNIQUE, bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own + -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), + voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units + voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' + CHECK (voltage_setpoint_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), available INTEGER NOT NULL DEFAULT 1 CHECK (available IN (0, 1)), -- Nullable: a source may take the voltage of the bus it connects to instead: base_voltage REAL NULL CHECK (base_voltage IS NULL OR base_voltage > 0), -- Units: kV @@ -1056,7 +1166,22 @@ CREATE TABLE two_terminal_hvdc_lines ( reactive_power_limits_from TEXT NULL CHECK (reactive_power_limits_from IS NULL OR json_valid(reactive_power_limits_from)), -- Units: per power_units reactive_power_limits_to TEXT NULL - CHECK (reactive_power_limits_to IS NULL OR json_valid(reactive_power_limits_to)) -- Units: per power_units + CHECK (reactive_power_limits_to IS NULL OR json_valid(reactive_power_limits_to)), -- Units: per power_units + -- VSC only: the bus each converter's AC voltage control holds. NULL means its own + -- terminal bus, which enforce_two_terminal_hvdc_lines_remote_bus_* forbids naming. + remote_regulated_bus_id_from INTEGER NULL REFERENCES balancing_topologies (id), + remote_regulated_bus_id_to INTEGER NULL REFERENCES balancing_topologies (id), + -- LCC only: each converter's commutating bus and tap transformer, when they are not + -- its terminal bus and an ideal transformer. + rectifier_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + inverter_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + rectifier_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id), + inverter_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id), + CHECK (converter_type = 'VSC' + OR (remote_regulated_bus_id_from IS NULL AND remote_regulated_bus_id_to IS NULL)), + CHECK (converter_type = 'LCC' + OR (rectifier_commutating_bus_id IS NULL AND inverter_commutating_bus_id IS NULL + AND rectifier_tap_transformer_id IS NULL AND inverter_tap_transformer_id IS NULL)) ) strict; -- T-model HVDC line (PSY TModelHVDCLine): a DC-network element whose arc @@ -1096,8 +1221,10 @@ CREATE TABLE facts_control_devices ( max_reactive_power REAL NOT NULL DEFAULT 9999.0 CHECK (max_reactive_power >= 0), -- Units: per power_units shunt_control_type TEXT NOT NULL DEFAULT 'STATCOM' CHECK (shunt_control_type IN ('SVC', 'STATCOM')), - -- 0 = local (sending) bus: - regulated_bus_number INTEGER NOT NULL DEFAULT 0 + -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own + -- bus is rejected. A shared regulation weight lives on voltage_control_associations: + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus) ) strict; -- Interconnecting power converter (PSY InterconnectingConverter), an AC<->DC @@ -1120,9 +1247,10 @@ CREATE TABLE interconnecting_converters ( parameter_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' CHECK (parameter_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), base_power REAL NOT NULL CHECK (base_power > 0), -- Units: MVA power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), - -- Remote-bus voltage control, droop, and power-factor weighting: - remote_bus_control INTEGER NULL CHECK (remote_bus_control IS NULL OR remote_bus_control >= 1), - rmpct REAL NOT NULL DEFAULT 100.0 CHECK (rmpct >= 0), + -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own + -- bus is rejected. A shared regulation weight lives on voltage_control_associations: + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), power_factor_weighting_fraction REAL NOT NULL DEFAULT 1.0 CHECK (power_factor_weighting_fraction >= 0), voltage_limits TEXT NULL DEFAULT '{"min": 0.0, "max": 999.9}' CHECK (voltage_limits IS NULL OR json_valid(voltage_limits)), diff --git a/schema/sql_codegen_map.json b/schema/sql_codegen_map.json index 06b3fea3..dff35ac5 100644 --- a/schema/sql_codegen_map.json +++ b/schema/sql_codegen_map.json @@ -179,12 +179,8 @@ "voltage_limits_to", "dc_voltage_droop_from", "dc_voltage_droop_to", - "remote_bus_control_from", - "remote_bus_control_to", "rated_ac_voltage_from", - "rated_ac_voltage_to", - "rmpct_from", - "rmpct_to" + "rated_ac_voltage_to" ], "skip": [ "parameter_units", diff --git a/schema/triggers.sql b/schema/triggers.sql index c1724fef..c11d3bfa 100644 --- a/schema/triggers.sql +++ b/schema/triggers.sql @@ -2554,3 +2554,150 @@ SELECT ); END; + +-- Voltage control groups: entity supertype boilerplate. +CREATE TRIGGER IF NOT EXISTS check_voltage_control_groups_entity_exists BEFORE +INSERT ON voltage_control_groups + WHEN NOT EXISTS ( + SELECT 1 + FROM entities + WHERE id = NEW.id + AND entity_table = 'voltage_control_groups' + ) BEGIN +SELECT RAISE( + ABORT, + 'voltage_control_groups.id must exist in entities with entity_table voltage_control_groups before insert' + ); +END; + +CREATE TRIGGER IF NOT EXISTS delete_voltage_control_groups_entity +AFTER DELETE ON voltage_control_groups +FOR EACH ROW +BEGIN + DELETE FROM entities WHERE id = OLD.id; +END; + +-- A transformer circuit regulating an end of its own arc derives the side from the arc +-- and stores none; regulating any other bus needs the side to say which winding the bus +-- lies beyond. +CREATE TRIGGER IF NOT EXISTS enforce_transformer_circuits_regulated_bus_side_insert BEFORE +INSERT ON transformer_circuits + WHEN NEW.regulated_bus_id IS NOT NULL + AND (NEW.regulated_bus_side IS NULL) <> EXISTS ( + SELECT 1 + FROM arcs + WHERE id = NEW.arc_id + AND NEW.regulated_bus_id IN (from_id, to_id) + ) +BEGIN +SELECT RAISE( + ABORT, + 'transformer_circuits.regulated_bus_side must be NULL when regulated_bus_id is an end of the circuit''s arc and set when it is not' + ); +END; + +CREATE TRIGGER IF NOT EXISTS enforce_transformer_circuits_regulated_bus_side_update BEFORE +UPDATE OF regulated_bus_id, regulated_bus_side, arc_id ON transformer_circuits + WHEN NEW.regulated_bus_id IS NOT NULL + AND (NEW.regulated_bus_side IS NULL) <> EXISTS ( + SELECT 1 + FROM arcs + WHERE id = NEW.arc_id + AND NEW.regulated_bus_id IN (from_id, to_id) + ) +BEGIN +SELECT RAISE( + ABORT, + 'transformer_circuits.regulated_bus_side must be NULL when regulated_bus_id is an end of the circuit''s arc and set when it is not' + ); +END; + +-- A VSC converter's remote regulated bus is never its own terminal bus: NULL already means +-- own-bus control. +CREATE TRIGGER IF NOT EXISTS enforce_two_terminal_hvdc_lines_remote_bus_insert +AFTER INSERT ON two_terminal_hvdc_lines +BEGIN +SELECT + CASE + WHEN NEW.remote_regulated_bus_id_from = (SELECT from_id FROM arcs WHERE id = NEW.arc_id) + THEN RAISE(ABORT, 'two_terminal_hvdc_lines.remote_regulated_bus_id_from must not be the from converter''s own terminal bus; NULL means own-bus control') + WHEN NEW.remote_regulated_bus_id_to = (SELECT to_id FROM arcs WHERE id = NEW.arc_id) + THEN RAISE(ABORT, 'two_terminal_hvdc_lines.remote_regulated_bus_id_to must not be the to converter''s own terminal bus; NULL means own-bus control') + END; +END; + +CREATE TRIGGER IF NOT EXISTS enforce_two_terminal_hvdc_lines_remote_bus_update +AFTER UPDATE OF remote_regulated_bus_id_from, remote_regulated_bus_id_to, arc_id ON two_terminal_hvdc_lines +BEGIN +SELECT + CASE + WHEN NEW.remote_regulated_bus_id_from = (SELECT from_id FROM arcs WHERE id = NEW.arc_id) + THEN RAISE(ABORT, 'two_terminal_hvdc_lines.remote_regulated_bus_id_from must not be the from converter''s own terminal bus; NULL means own-bus control') + WHEN NEW.remote_regulated_bus_id_to = (SELECT to_id FROM arcs WHERE id = NEW.arc_id) + THEN RAISE(ABORT, 'two_terminal_hvdc_lines.remote_regulated_bus_id_to must not be the to converter''s own terminal bus; NULL means own-bus control') + END; +END; + +-- Voltage control membership. Droop members are generators; sharing members are the +-- devices that regulate voltage (RenewableNonDispatch shares the renewable table but is a +-- fixed injection); terminal names a converter of a two-terminal VSC line and nothing +-- else. The member's table comes from entities, the same source the arc trigger reads. +CREATE TRIGGER IF NOT EXISTS enforce_voltage_control_member_types_insert +AFTER INSERT ON voltage_control_associations +BEGIN +SELECT + CASE + WHEN (SELECT TYPE FROM voltage_control_groups WHERE id = NEW.control_id) = 'VoltageDroopControl' + AND NOT EXISTS ( + SELECT 1 FROM entities + WHERE id = NEW.entity_id + AND entity_table IN ('thermal_generators', 'hydro_generators', 'renewable_generators') + AND entity_type <> 'RenewableNonDispatch' + ) + THEN RAISE(ABORT, 'voltage_control_associations: only generators can be members of a VoltageDroopControl') + WHEN NOT EXISTS ( + SELECT 1 FROM entities + WHERE id = NEW.entity_id + AND entity_table IN ('thermal_generators', 'hydro_generators', 'renewable_generators', + 'storage_units', 'synchronous_condensers', 'sources', 'switched_admittance', + 'facts_control_devices', 'interconnecting_converters', 'two_terminal_hvdc_lines') + AND entity_type <> 'RenewableNonDispatch' + ) + THEN RAISE(ABORT, 'voltage_control_associations: the member must be a voltage regulating device (generator, storage unit, synchronous condenser, source, switched shunt, FACTS device, converter or two-terminal VSC line)') + WHEN (NEW.terminal IS NULL) = COALESCE( + (SELECT converter_type FROM two_terminal_hvdc_lines WHERE id = NEW.entity_id) = 'VSC', + 0 + ) + THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be NULL for any other member') + END; +END; + +CREATE TRIGGER IF NOT EXISTS enforce_voltage_control_member_types_update +AFTER UPDATE OF control_id, entity_id, terminal ON voltage_control_associations +BEGIN +SELECT + CASE + WHEN (SELECT TYPE FROM voltage_control_groups WHERE id = NEW.control_id) = 'VoltageDroopControl' + AND NOT EXISTS ( + SELECT 1 FROM entities + WHERE id = NEW.entity_id + AND entity_table IN ('thermal_generators', 'hydro_generators', 'renewable_generators') + AND entity_type <> 'RenewableNonDispatch' + ) + THEN RAISE(ABORT, 'voltage_control_associations: only generators can be members of a VoltageDroopControl') + WHEN NOT EXISTS ( + SELECT 1 FROM entities + WHERE id = NEW.entity_id + AND entity_table IN ('thermal_generators', 'hydro_generators', 'renewable_generators', + 'storage_units', 'synchronous_condensers', 'sources', 'switched_admittance', + 'facts_control_devices', 'interconnecting_converters', 'two_terminal_hvdc_lines') + AND entity_type <> 'RenewableNonDispatch' + ) + THEN RAISE(ABORT, 'voltage_control_associations: the member must be a voltage regulating device (generator, storage unit, synchronous condenser, source, switched shunt, FACTS device, converter or two-terminal VSC line)') + WHEN (NEW.terminal IS NULL) = COALESCE( + (SELECT converter_type FROM two_terminal_hvdc_lines WHERE id = NEW.entity_id) = 'VSC', + 0 + ) + THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be NULL for any other member') + END; +END; diff --git a/schema/unit_registry.sql b/schema/unit_registry.sql index 47a3bc03..fa506a35 100644 --- a/schema/unit_registry.sql +++ b/schema/unit_registry.sql @@ -92,6 +92,7 @@ INSERT INTO allowed_units (quantity_kind, unit) VALUES ('HeadRate', 'm/s'), ('HeatRate', 'MMBtu/MWh'), ('Impedance', 'ohm'), + ('Impedance', 'pu'), ('Inductance', 'H'), ('Length', 'km'), ('Length', 'm'), @@ -168,8 +169,6 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('attributes', 'rectifier_tap_setting', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'LCC rectifier tap setting'), ('attributes', 'rectifier_tap_step', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'LCC rectifier tap step'), ('attributes', 'rectifier_transformer_ratio', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'LCC rectifier transformer ratio'), - ('attributes', 'rmpct_from', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'VSC from-converter remote-regulation share'), - ('attributes', 'rmpct_to', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'VSC to-converter remote-regulation share'), ('attributes', 'start_fuel_mmbtu_per_mw', 'StartFuelPerCapacity', 'MMBtu/MW', NULL, NULL, NULL, NULL, NULL, NULL, 'Start fuel per unit capacity; mirrors the supply_technologies column'), ('attributes', 'start_time_limits', 'OperationalDuration', 'min', NULL, NULL, NULL, NULL, NULL, NULL, 'ThermalMultiStart hot/warm/cold start time limits'), ('attributes', 'start_types', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'ThermalMultiStart start-type count: hot, warm, cold'), @@ -187,7 +186,6 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('facts_control_devices', 'base_power', 'ApparentPower', 'MVA', NULL, NULL, NULL, NULL, NULL, NULL, 'Base power the row''s per-unit values resolve against'), ('facts_control_devices', 'max_reactive_power', 'ReactivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Independent max reactive power ceiling, per-unit on the row''s base_power'), ('facts_control_devices', 'max_reactive_power', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Independent max reactive power ceiling in MVAr'), - ('facts_control_devices', 'regulated_bus_number', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Bus whose voltage this device regulates; 0 = local bus'), ('facts_control_devices', 'shunt_control_type', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Device class selecting the reactive-limit law (SVC vs STATCOM)'), ('facts_control_devices', 'voltage_setpoint', 'Voltage', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'bus->balancing_topologies.base_voltage', 'Sending-end voltage setpoint, per-unit on the bus''s base voltage'), ('facts_control_devices', 'voltage_setpoint', 'Voltage', 'kV', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Sending-end voltage setpoint in kV'), @@ -220,6 +218,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('hydro_generators', 'reactive_power_limits', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'JSON {min, max} in MVAr'), ('hydro_generators', 'time_limits', 'OperationalDuration', 'min', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {up, down}'), ('hydro_generators', 'travel_time', 'OperationalDuration', 'min', NULL, NULL, NULL, NULL, NULL, NULL, 'Water travel time'), + ('hydro_generators', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology'), + ('hydro_generators', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('hydro_reservoirs', 'evaporative_loss', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Standing loss from evaporation as a fraction of stored volume or energy'), ('hydro_reservoirs', 'head_to_volume_factor', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Head-to-volume conversion curve (FunctionData JSON); coefficients are dimensionless in the natural-unit convention'), ('hydro_reservoirs', 'inflow', 'ActivePower', 'MW', 'level_data_type', 'ENERGY', NULL, NULL, NULL, NULL, 'Inflow rate'), @@ -258,8 +258,6 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('interconnecting_converters', 'dc_setpoint', 'Voltage', 'pu', 'dc_control', 'DC_VOLTAGE_DROOP', 'parameter_units', 'COMPONENT_BASE', NULL, 'dc_bus->balancing_topologies.base_voltage', 'DC droop voltage setpoint, per-unit'), ('interconnecting_converters', 'dc_setpoint', 'Voltage', 'kV', 'dc_control', 'DC_VOLTAGE_DROOP', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, 'DC droop voltage setpoint in kV'), ('interconnecting_converters', 'power_factor_weighting_fraction', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Reactive/active power-factor weighting fraction'), - ('interconnecting_converters', 'remote_bus_control', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Remote bus whose voltage this converter regulates; null = local bus'), - ('interconnecting_converters', 'rmpct', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Remote-bus voltage-control percentage compensation'), ('interconnecting_converters', 'voltage_limits', 'Voltage', 'pu', NULL, NULL, NULL, NULL, NULL, 'dc_bus->balancing_topologies.base_voltage', 'JSON {min, max} voltage limits'), ('loads', 'base_power', 'ApparentPower', 'MVA', NULL, NULL, NULL, NULL, NULL, NULL, 'Base power the row''s per-unit values resolve against'), ('point_to_point_bids', 'max_active_power', 'ActivePower', 'MW', NULL, NULL, NULL, NULL, NULL, NULL, 'MW envelope for the bid'), @@ -287,6 +285,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('renewable_generators', 'reactive_power', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Initial reactive power setpoint in MVAr'), ('renewable_generators', 'reactive_power_limits', 'ReactivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'JSON {min, max}, per-unit on the row''s base_power'), ('renewable_generators', 'reactive_power_limits', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'JSON {min, max} in MVAr'), + ('renewable_generators', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology'), + ('renewable_generators', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('sources', 'active_power', 'ActivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Active power injection setpoint, per-unit on the row''s base_power'), ('sources', 'active_power', 'ActivePower', 'MW', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Active power injection setpoint in MW'), ('sources', 'active_power_limits', 'ActivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Active power limits (JSON min/max), per-unit on the row''s base_power'), @@ -309,6 +309,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('sources', 'reactive_power', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Reactive power injection setpoint in MVAr'), ('sources', 'reactive_power_limits', 'ReactivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Reactive power limits (JSON min/max), per-unit on the row''s base_power'), ('sources', 'reactive_power_limits', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Reactive power limits (JSON min/max) in MVAr'), + ('sources', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else bus'), + ('sources', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('sources', 'x_th', 'Reactance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage', 'Thevenin reactance, per-unit on the row''s base_power'), ('sources', 'x_th', 'Reactance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Thevenin reactance in ohm'), ('storage_technologies', 'capacity_limits_charge', 'ActivePower', 'MW', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {min, max} charge capacity limits'), @@ -365,6 +367,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('storage_units', 'storage_capacity', 'ElectricalEnergy', 'MWmin', 'energy_units', 'MWMIN', NULL, NULL, NULL, NULL, 'Total storage capacity, on the MWmin minutes basis'), ('storage_units', 'storage_level_limits', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {min, max} as ratio [0, 1] of storage_capacity'), ('storage_units', 'storage_target', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'End-of-simulation storage target as ratio of storage_capacity'), + ('storage_units', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology'), + ('storage_units', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('supply_technologies', 'capacity_limits', 'ActivePower', 'MW', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {min, max} capacity limits'), ('supply_technologies', 'capital_costs', 'CostPerCapacity', 'USD/MW', NULL, NULL, NULL, NULL, NULL, NULL, 'Capital cost structure'), ('supply_technologies', 'co2', 'CO2Emissions', 't/MMBtu', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON CO2 emission rates per fuel'), @@ -387,7 +391,6 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('switched_admittance', 'admittance_limits', 'ReactivePower', 'MVAr', 'admittance_units', 'COMPONENT_MVAR', NULL, NULL, NULL, NULL, 'Admittance limits (JSON min/max) in the PSS/E form: MVAr at 1.0 pu voltage'), ('switched_admittance', 'admittance_limits', 'Susceptance', 'S', 'admittance_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Admittance limits (JSON min/max) in siemens'), ('switched_admittance', 'control_mode', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Switched-shunt control mode'), - ('switched_admittance', 'regulated_bus_number', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Bus whose voltage/quantity this shunt regulates; 0 = local bus'), ('switched_admittance', 'solved_admittance', 'ReactivePower', 'MVAr', 'admittance_units', 'COMPONENT_MVAR', NULL, NULL, NULL, NULL, 'Solved-case admittance (PSS/E BINIT) in the PSS/E form: MVAr at 1.0 pu voltage'), ('switched_admittance', 'solved_admittance', 'Susceptance', 'S', 'admittance_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Solved-case admittance (PSS/E BINIT) in siemens'), ('synchronous_condensers', 'active_power_losses', 'ActivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Active power loss incurred by being online, per-unit on the row''s base_power'), @@ -399,6 +402,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('synchronous_condensers', 'reactive_power', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Initial reactive power setpoint in MVAr'), ('synchronous_condensers', 'reactive_power_limits', 'ReactivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Reactive power limits, JSON {min, max}, per-unit on the row''s base_power'), ('synchronous_condensers', 'reactive_power_limits', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Reactive power limits, JSON {min, max} in MVAr'), + ('synchronous_condensers', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else bus'), + ('synchronous_condensers', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('thermal_generators', 'active_power', 'ActivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Initial active power setpoint, per-unit on the row''s base_power'), ('thermal_generators', 'active_power', 'ActivePower', 'MW', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Initial active power setpoint in MW'), ('thermal_generators', 'active_power_limits', 'ActivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'JSON {min, max}, per-unit on the row''s base_power'), @@ -421,6 +426,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('thermal_generators', 'reactive_power_limits', 'ReactivePower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'JSON {min, max}, per-unit on the row''s base_power'), ('thermal_generators', 'reactive_power_limits', 'ReactivePower', 'MVAr', 'power_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'JSON {min, max} in MVAr'), ('thermal_generators', 'time_limits', 'OperationalDuration', 'min', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {up, down}'), + ('thermal_generators', 'voltage_setpoint', 'Voltage', 'pu', 'voltage_setpoint_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'remote_regulated_bus_id->balancing_topologies.base_voltage', 'Voltage held at the regulated bus, per-unit on that bus''s base voltage; the regulated bus is remote_regulated_bus_id when set, else balancing_topology'), + ('thermal_generators', 'voltage_setpoint', 'Voltage', 'kV', 'voltage_setpoint_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Voltage held at the regulated bus in kV'), ('three_winding_transformers', 'base_power_12', 'ApparentPower', 'MVA', NULL, NULL, NULL, NULL, NULL, NULL, 'Base power the primary-secondary pair''s per-unit impedance resolves against'), ('three_winding_transformers', 'base_power_23', 'ApparentPower', 'MVA', NULL, NULL, NULL, NULL, NULL, NULL, 'Base power the secondary-tertiary pair''s per-unit impedance resolves against'), ('three_winding_transformers', 'base_power_31', 'ApparentPower', 'MVA', NULL, NULL, NULL, NULL, NULL, NULL, 'Base power the tertiary-primary pair''s per-unit impedance resolves against'), @@ -471,6 +478,8 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'UNDEFINED', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} controlled-quantity band; no control block'), ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'VOLTAGE', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} pu voltage band (voltage control)'), ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'VOLTAGE_DISABLED', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} pu voltage band (voltage control, disabled)'), + ('transformer_circuits', 'load_drop_compensation', 'Impedance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Load drop compensation impedance (JSON real/imag = PSS/E CR/CX), per-unit on the row''s base_power, referenced to base_voltage_primary'), + ('transformer_circuits', 'load_drop_compensation', 'Impedance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Load drop compensation impedance (JSON real/imag) in ohm'), ('transformer_circuits', 'r', 'Resistance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Circuit resistance, per-unit on the row''s base_power, referenced to base_voltage_primary'), ('transformer_circuits', 'r', 'Resistance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Circuit resistance, in ohm'), ('transformer_circuits', 'rating', 'ApparentPower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Thermal rating, per-unit on the row''s base_power'), @@ -524,7 +533,14 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('virtual_participants', 'operation_cost.decremental_offer_curves', 'CostPerTime', 'USD/h', 'operation_cost.decremental_offer_curves.value_curve.curve_type', 'INPUT_OUTPUT', NULL, NULL, NULL, NULL, 'Buy offer input-output cost curve: y is a cost rate at a power level (operation_cost JSON path); payload power_units must be NATURAL_UNITS'), ('virtual_participants', 'operation_cost.incremental_offer_curves', 'CostPerEnergy', 'USD/MWh', 'operation_cost.incremental_offer_curves.value_curve.curve_type', 'AVERAGE_RATE', NULL, NULL, NULL, NULL, 'Sell offer average-rate cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS'), ('virtual_participants', 'operation_cost.incremental_offer_curves', 'CostPerEnergy', 'USD/MWh', 'operation_cost.incremental_offer_curves.value_curve.curve_type', 'INCREMENTAL', NULL, NULL, NULL, NULL, 'Sell offer incremental (marginal) cost curve (operation_cost JSON path); payload power_units must be NATURAL_UNITS'), - ('virtual_participants', 'operation_cost.incremental_offer_curves', 'CostPerTime', 'USD/h', 'operation_cost.incremental_offer_curves.value_curve.curve_type', 'INPUT_OUTPUT', NULL, NULL, NULL, NULL, 'Sell offer input-output cost curve: y is a cost rate at a power level (operation_cost JSON path); payload power_units must be NATURAL_UNITS'); + ('virtual_participants', 'operation_cost.incremental_offer_curves', 'CostPerTime', 'USD/h', 'operation_cost.incremental_offer_curves.value_curve.curve_type', 'INPUT_OUTPUT', NULL, NULL, NULL, NULL, 'Sell offer input-output cost curve: y is a cost rate at a power level (operation_cost JSON path); payload power_units must be NATURAL_UNITS'), + ('voltage_control_associations', 'weight', 'Dimensionless', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Relative reactive power share of one voltage control group member (PSS/E RMPCT / 100)'), + ('voltage_control_groups', 'deadband_reactive_power', 'ReactivePower', 'MVAr', NULL, NULL, NULL, NULL, NULL, NULL, 'Droop controller reactive power held inside the voltage deadband (PSS/E QDB)'), + ('voltage_control_groups', 'deadband_voltage_limits', 'Voltage', 'pu', 'voltage_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'regulated_bus_id->balancing_topologies.base_voltage', 'Droop controller voltage deadband (JSON min/max, PSS/E VDBLOW/VDBHIGH), per-unit on the regulated bus''s base voltage'), + ('voltage_control_groups', 'deadband_voltage_limits', 'Voltage', 'kV', 'voltage_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Droop controller voltage deadband (JSON min/max) in kV'), + ('voltage_control_groups', 'reactive_power_limits', 'ReactivePower', 'MVAr', NULL, NULL, NULL, NULL, NULL, NULL, 'Droop controller reactive power range (JSON min/max, PSS/E QMIN/QMAX)'), + ('voltage_control_groups', 'voltage_limits', 'Voltage', 'pu', 'voltage_units', 'COMPONENT_BASE', NULL, NULL, NULL, 'regulated_bus_id->balancing_topologies.base_voltage', 'Droop controller voltage range (JSON min/max, PSS/E VLOW/VHIGH), per-unit on the regulated bus''s base voltage'), + ('voltage_control_groups', 'voltage_limits', 'Voltage', 'kV', 'voltage_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Droop controller voltage range (JSON min/max) in kV'); -- 4. Per-quantity-type pu resolution rules INSERT INTO unit_basis_rules (quantity_kind, base_expression, description) VALUES @@ -532,6 +548,7 @@ INSERT INTO unit_basis_rules (quantity_kind, base_expression, description) VALUE ('ActivePowerChangeRate', 'base_power', 'Per-unit active power ramp rate: divide by the row''s base power; the /min in pu/min is a per-minute rate, not a separate time base'), ('ApparentPower', 'base_power', 'Per-unit apparent power: divide by the row''s base power'), ('Conductance', 'base_power/base_voltage^2', 'Per-unit conductance: base admittance is base_power/base_voltage^2'), + ('Impedance', 'base_voltage^2/base_power', 'Per-unit impedance: base impedance is base_voltage^2/base_power'), ('Reactance', 'base_voltage^2/base_power', 'Per-unit reactance: base impedance is base_voltage^2/base_power'), ('ReactivePower', 'base_power', 'Per-unit reactive power: divide by the row''s base power'), ('Resistance', 'base_voltage^2/base_power', 'Per-unit resistance: base impedance is base_voltage^2/base_power'), @@ -547,4 +564,4 @@ INSERT INTO unit_management_metadata (key, value, description) VALUES -- Inserting this row activates the immutability triggers. See the -- module docstring of generate_unit_registry.py for the exact repr. INSERT INTO unit_management_metadata (key, value, description) VALUES - ('unit_conventions_checksum', 'ab9137a875d33a3278f89af3ad2a0074d9aba82477a3c1c1a43ff11114b4096b', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); + ('unit_conventions_checksum', 'acd084e643a828bef2496a5c5cc9c14cd4e8a9078cec096699fddc8e40e073c6', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); diff --git a/scripts/generate_unit_registry.py b/scripts/generate_unit_registry.py index 753733d6..cf539f51 100755 --- a/scripts/generate_unit_registry.py +++ b/scripts/generate_unit_registry.py @@ -63,6 +63,11 @@ "base_expression": "base_voltage^2/base_power", "description": "Per-unit reactance: base impedance is base_voltage^2/base_power", }, + { + "quantity_kind": "Impedance", + "base_expression": "base_voltage^2/base_power", + "description": "Per-unit impedance: base impedance is base_voltage^2/base_power", + }, { "quantity_kind": "Susceptance", "base_expression": "base_power/base_voltage^2", diff --git a/test/conftest.py b/test/conftest.py index b5525779..e49fc9ec 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -56,6 +56,42 @@ def make_entity( ) return entity_id +def make_bus(conn, bus_id, name): + make_entity(conn, bus_id, "balancing_topologies", "ACBus", is_topology=1) + conn.execute( + "INSERT INTO balancing_topologies(id, name) VALUES (?, ?)", (bus_id, name) + ) + return bus_id + + +def make_dc_bus(conn, bus_id, name): + """Create a DC balancing-topology entity plus its row (entity_types.is_dc = 1).""" + make_entity(conn, bus_id, "balancing_topologies", "DCBus", is_topology=1, is_dc=1) + conn.execute( + "INSERT INTO balancing_topologies(id, name) VALUES (?, ?)", (bus_id, name) + ) + return bus_id + + +def make_arc(conn, arc_id, from_id, to_id): + make_entity(conn, arc_id, "arcs", "Arc") + conn.execute( + "INSERT INTO arcs(id, from_id, to_id) VALUES (?, ?, ?)", + (arc_id, from_id, to_id), + ) + return arc_id + + +def make_circuit(conn, circuit_id, arc_id): + make_entity(conn, circuit_id, "transformer_circuits", "TransformerCircuit") + conn.execute( + "INSERT INTO transformer_circuits(id, arc_id, power_units, base_power) " + "VALUES (?, ?, 'COMPONENT_BASE', 100.0)", + (circuit_id, arc_id), + ) + return circuit_id + + # Order matters: schema (tables) -> triggers -> registry seed (+ seal) -> views. SCHEMA_FILES = [ SCHEMA_DIR / "schema.sql", diff --git a/test/test_schema_integrity.py b/test/test_schema_integrity.py index b12f154c..cb75a692 100644 --- a/test/test_schema_integrity.py +++ b/test/test_schema_integrity.py @@ -8,43 +8,15 @@ import pytest -from conftest import SCHEMAS_PATH, load_schemas_json, make_entity - - -def make_bus(conn, bus_id, name): - make_entity(conn, bus_id, "balancing_topologies", "ACBus", is_topology=1) - conn.execute( - "INSERT INTO balancing_topologies(id, name) VALUES (?, ?)", (bus_id, name) - ) - return bus_id - - -def make_dc_bus(conn, bus_id, name): - """Create a DC balancing-topology entity plus its row (entity_types.is_dc = 1).""" - make_entity(conn, bus_id, "balancing_topologies", "DCBus", is_topology=1, is_dc=1) - conn.execute( - "INSERT INTO balancing_topologies(id, name) VALUES (?, ?)", (bus_id, name) - ) - return bus_id - - -def make_arc(conn, arc_id, from_id, to_id): - make_entity(conn, arc_id, "arcs", "Arc") - conn.execute( - "INSERT INTO arcs(id, from_id, to_id) VALUES (?, ?, ?)", - (arc_id, from_id, to_id), - ) - return arc_id - - -def make_circuit(conn, circuit_id, arc_id): - make_entity(conn, circuit_id, "transformer_circuits", "TransformerCircuit") - conn.execute( - "INSERT INTO transformer_circuits(id, arc_id, power_units, base_power) " - "VALUES (?, ?, 'COMPONENT_BASE', 100.0)", - (circuit_id, arc_id), - ) - return circuit_id +from conftest import ( + SCHEMAS_PATH, + load_schemas_json, + make_arc, + make_bus, + make_circuit, + make_dc_bus, + make_entity, +) def _arc_between_new_buses(conn): @@ -271,6 +243,7 @@ def test_transformer_circuit_control_fields_roundtrip(fresh_db): ("three_winding_transformers", "shunt_location", "ThreeWindingTransformerShuntLocation"), ("transformer_circuits", "control_objective", "TransformerControlObjective"), + ("transformer_circuits", "regulated_bus_side", "TransformerRegulatedBusSide"), ] @@ -521,5 +494,3 @@ def test_dc_flag_requires_topology_type(fresh_db): fresh_db.execute( "INSERT INTO entity_types(name, is_topology, is_dc) VALUES ('Bogus', 0, 1)" ) - - diff --git a/test/test_sql_codegen.py b/test/test_sql_codegen.py index 1ef4e9bc..63d5eec2 100644 --- a/test/test_sql_codegen.py +++ b/test/test_sql_codegen.py @@ -126,6 +126,7 @@ def test_branch_parameter_pu_pairs_in_vocabulary(fresh_db): assert pairs == { ("Resistance", "pu"), ("Reactance", "pu"), + ("Impedance", "pu"), ("Susceptance", "pu"), ("Conductance", "pu"), ("Voltage", "pu"), @@ -314,6 +315,8 @@ def test_transformer_circuits_columns_and_units(fresh_db): ("r", "NATURAL_UNITS"): ("Resistance", "ohm"), ("x", "COMPONENT_BASE"): ("Reactance", "pu"), ("x", "NATURAL_UNITS"): ("Reactance", "ohm"), + ("load_drop_compensation", "COMPONENT_BASE"): ("Impedance", "pu"), + ("load_drop_compensation", "NATURAL_UNITS"): ("Impedance", "ohm"), } assert { diff --git a/test/test_unit_registry.py b/test/test_unit_registry.py index f8336102..7297b432 100644 --- a/test/test_unit_registry.py +++ b/test/test_unit_registry.py @@ -22,8 +22,8 @@ # Expected seed row counts (current sealed state). EXPECTED_QUANTITY_TYPES = 41 -EXPECTED_ALLOWED_UNITS = 66 -EXPECTED_UNIT_CONVENTIONS = 405 +EXPECTED_ALLOWED_UNITS = 67 +EXPECTED_UNIT_CONVENTIONS = 420 VERIFY_SCRIPT = SCRIPTS_DIR / "verify_unit_registry.py" REGISTRY_SQL = SCHEMA_DIR / "unit_registry.sql" @@ -1345,7 +1345,7 @@ def test_merged_hvdc_columns_registered(db): ("dc_current", "CurrentFlow/A"), ("rating_from", "ApparentPower/MVA"), ("reactive_power_to", "ReactivePower/MVAr"), - ("rmpct_from", "Fraction/1"), + ("power_factor_weighting_fraction_from", "Fraction/1"), # Unit depends on a basis choice or a sibling control mode. A convention's # discriminator_column names a sibling *column*, which an attributes row # does not have, so these stay unregistered and each row carries its own diff --git a/test/test_voltage_control.py b/test/test_voltage_control.py new file mode 100644 index 00000000..6a04ea82 --- /dev/null +++ b/test/test_voltage_control.py @@ -0,0 +1,633 @@ +"""Remote and shared voltage control: the remote regulated bus references on devices and +transformer circuits, and the voltage control groups with their memberships. + +Rule numbers follow the remote-control design: R1 (a device never names its own bus as +remote), R2 (every referenced bus or transformer exists), R4 (a transformer names a +regulated bus exactly when it regulates voltage and a side exactly when that bus is off +its arc), R7 (one group per member or converter terminal), R9 (positive weights) and R13 +(droop members are generators).""" + +import json +import sqlite3 + +import pytest + +from conftest import make_arc, make_bus, make_circuit, make_entity + +DEVICE_TABLES_WITH_REMOTE_BUS = [ + "thermal_generators", + "renewable_generators", + "hydro_generators", + "storage_units", + "synchronous_condensers", + "sources", + "switched_admittance", + "facts_control_devices", + "interconnecting_converters", +] + +GENERATOR_TABLES = [ + "thermal_generators", + "renewable_generators", + "hydro_generators", + "storage_units", + "synchronous_condensers", + "sources", +] + + +def _columns(conn, table): + return {row[1]: row for row in conn.execute(f"PRAGMA table_info({table})")} + + +def _thermal(conn, gen_id, bus_id, entity_type="ThermalStandard", **extra): + make_entity(conn, gen_id, "thermal_generators", entity_type) + conn.execute("INSERT OR IGNORE INTO prime_mover_types(name) VALUES ('ST')") + conn.execute("INSERT OR IGNORE INTO fuels(name) VALUES ('NATURAL_GAS')") + columns = { + "id": gen_id, + "name": f"gen_{gen_id}", + "prime_mover_type": "ST", + "fuel": "NATURAL_GAS", + "balancing_topology": bus_id, + "rating": 1.0, + "base_power": 100.0, + "power_units": "COMPONENT_BASE", + "active_power_limits": '{"min": 0.0, "max": 1.0}', + "status": "ONLINE", + **extra, + } + conn.execute( + f"INSERT INTO thermal_generators({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return gen_id + + +def _shunt(conn, shunt_id, bus_id, **extra): + make_entity(conn, shunt_id, "switched_admittance", "SwitchedAdmittance") + columns = {"id": shunt_id, "name": f"shunt_{shunt_id}", "bus": bus_id, **extra} + conn.execute( + f"INSERT INTO switched_admittance({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return shunt_id + + +def _facts(conn, facts_id, bus_id, **extra): + make_entity(conn, facts_id, "facts_control_devices", "FACTSControlDevice") + columns = { + "id": facts_id, + "name": f"facts_{facts_id}", + "bus": bus_id, + "voltage_setpoint": 1.0, + "power_units": "COMPONENT_BASE", + "base_power": 100.0, + **extra, + } + conn.execute( + f"INSERT INTO facts_control_devices({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return facts_id + + +def _hvdc_line(conn, line_id, arc_id, converter_type, **extra): + make_entity(conn, line_id, "two_terminal_hvdc_lines", f"TwoTerminal{converter_type}Line") + columns = { + "id": line_id, + "name": f"dc_{line_id}", + "arc_id": arc_id, + "converter_type": converter_type, + "power_units": "COMPONENT_BASE", + "base_power": 100.0, + **extra, + } + conn.execute( + f"INSERT INTO two_terminal_hvdc_lines({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return line_id + + +def _control_circuit(conn, circuit_id, arc_id, **extra): + make_entity(conn, circuit_id, "transformer_circuits", "TransformerCircuit") + columns = { + "id": circuit_id, + "arc_id": arc_id, + "power_units": "COMPONENT_BASE", + "base_power": 100.0, + **extra, + } + conn.execute( + f"INSERT INTO transformer_circuits({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return circuit_id + + +def _two_winding(conn, transformer_id, circuit_id): + make_entity(conn, transformer_id, "two_winding_transformers", "TwoWindingTransformer") + conn.execute( + "INSERT INTO two_winding_transformers(id, name, circuit) VALUES (?, ?, ?)", + (transformer_id, f"xf_{transformer_id}", circuit_id), + ) + return transformer_id + + +def _sharing_group(conn, group_id, name=None): + make_entity(conn, group_id, "voltage_control_groups", "ReactivePowerSharing") + conn.execute( + "INSERT INTO voltage_control_groups(id, name, TYPE) VALUES (?, ?, 'ReactivePowerSharing')", + (group_id, name or f"share_{group_id}"), + ) + return group_id + + +def _droop_group(conn, group_id, bus_id, **extra): + make_entity(conn, group_id, "voltage_control_groups", "VoltageDroopControl") + columns = { + "id": group_id, + "name": f"droop_{group_id}", + "TYPE": "VoltageDroopControl", + "regulated_bus_id": bus_id, + "reactive_power_limits": '{"min": -50.0, "max": 50.0}', + "deadband_reactive_power": 0.0, + "voltage_units": "COMPONENT_BASE", + "deadband_voltage_limits": '{"min": 0.99, "max": 1.01}', + "voltage_limits": '{"min": 0.95, "max": 1.05}', + **extra, + } + conn.execute( + f"INSERT INTO voltage_control_groups({', '.join(columns)}) " + f"VALUES ({', '.join('?' * len(columns))})", + list(columns.values()), + ) + return group_id + + +def _member(conn, group_id, entity_id, weight=1.0, terminal=None): + conn.execute( + "INSERT INTO voltage_control_associations(control_id, entity_id, weight, terminal) " + "VALUES (?, ?, ?, ?)", + (group_id, entity_id, weight, terminal), + ) + + +# Remote regulated bus references on devices + + +@pytest.mark.parametrize("table", DEVICE_TABLES_WITH_REMOTE_BUS) +def test_device_tables_reference_the_remote_regulated_bus(db, table): + """The remote bus is a nullable foreign key to the bus table; the plain bus-number + and percentage columns it replaces are gone.""" + columns = _columns(db, table) + assert "remote_regulated_bus_id" in columns + assert columns["remote_regulated_bus_id"][3] == 0, "must be nullable: null means own bus" + for legacy in ("regulated_bus_number", "remote_bus_control", "rmpct"): + assert legacy not in columns, f"{table}.{legacy} was replaced" + foreign_keys = { + row[3]: row[2] for row in db.execute(f"PRAGMA foreign_key_list({table})") + } + assert foreign_keys.get("remote_regulated_bus_id") == "balancing_topologies" + + +@pytest.mark.parametrize("table", GENERATOR_TABLES) +def test_generator_tables_carry_a_voltage_setpoint(db, table): + columns = _columns(db, table) + assert columns["voltage_setpoint"][4] == "1.0" + assert columns["voltage_setpoint_units"][4] == "'COMPONENT_BASE'" + + +def test_generator_regulates_its_own_bus_by_default(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + _thermal(fresh_db, 2, bus) + row = fresh_db.execute( + "SELECT remote_regulated_bus_id, voltage_setpoint, voltage_setpoint_units " + "FROM thermal_generators WHERE id = 2" + ).fetchone() + assert row == (None, 1.0, "COMPONENT_BASE") + + +def test_generator_remote_bus_round_trips(fresh_db): + own = make_bus(fresh_db, 1, "b1") + remote = make_bus(fresh_db, 2, "b2") + _thermal( + fresh_db, 3, own, remote_regulated_bus_id=remote, voltage_setpoint=138.0, + voltage_setpoint_units="NATURAL_UNITS", + ) + row = fresh_db.execute( + "SELECT remote_regulated_bus_id, voltage_setpoint, voltage_setpoint_units " + "FROM thermal_generators WHERE id = 3" + ).fetchone() + assert row == (remote, 138.0, "NATURAL_UNITS") + + +def test_generator_remote_bus_must_exist(fresh_db): + """R2: the remote bus is a foreign key, not a free integer.""" + bus = make_bus(fresh_db, 1, "b1") + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY"): + _thermal(fresh_db, 2, bus, remote_regulated_bus_id=999) + + +def test_generator_cannot_name_its_own_bus_as_remote(fresh_db): + """R1: null already means the own bus.""" + bus = make_bus(fresh_db, 1, "b1") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _thermal(fresh_db, 2, bus, remote_regulated_bus_id=bus) + + +def test_generator_voltage_setpoint_units_is_a_voltage_basis(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _thermal(fresh_db, 2, bus, voltage_setpoint_units="SYSTEM_BASE") + + +def test_shunt_cannot_name_its_own_bus_as_remote(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + remote = make_bus(fresh_db, 2, "b2") + _shunt(fresh_db, 3, bus, remote_regulated_bus_id=remote) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _shunt(fresh_db, 4, bus, remote_regulated_bus_id=bus) + + +def test_facts_cannot_name_its_own_bus_as_remote(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + remote = make_bus(fresh_db, 2, "b2") + _facts(fresh_db, 3, bus, remote_regulated_bus_id=remote) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _facts(fresh_db, 4, bus, remote_regulated_bus_id=bus) + + +def test_deleting_a_remote_bus_is_refused_while_a_device_regulates_it(fresh_db): + """A device regulating a deleted bus would silently fall back to its own bus, so the + reference blocks the delete instead of being cleared.""" + own = make_bus(fresh_db, 1, "b1") + remote = make_bus(fresh_db, 2, "b2") + _thermal(fresh_db, 3, own, remote_regulated_bus_id=remote) + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY"): + fresh_db.execute("DELETE FROM balancing_topologies WHERE id = ?", (remote,)) + + +# Transformer circuits + + +def test_transformer_circuit_control_columns(db): + columns = _columns(db, "transformer_circuits") + assert "regulated_bus_number" not in columns + assert columns["regulated_bus_id"][3] == 0 + assert columns["regulated_bus_side"][3] == 0 + assert json.loads(columns["load_drop_compensation"][4].strip("'")) == { + "real": 0.0, "imag": 0.0, + } + foreign_keys = { + row[3]: row[2] for row in db.execute("PRAGMA foreign_key_list(transformer_circuits)") + } + assert foreign_keys["regulated_bus_id"] == "balancing_topologies" + + +def test_voltage_objective_requires_a_regulated_bus(fresh_db): + """R4: a voltage-regulating circuit names its bus; a circuit that regulates no voltage + names none.""" + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit(fresh_db, 4, arc, control_objective="VOLTAGE") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit(fresh_db, 5, arc, control_objective="VOLTAGE_DISABLED") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit(fresh_db, 6, arc, control_objective="ACTIVE_POWER_FLOW", regulated_bus_id=1) + _control_circuit(fresh_db, 7, arc, control_objective="VOLTAGE", regulated_bus_id=1) + _control_circuit(fresh_db, 8, arc, control_objective="VOLTAGE_DISABLED", regulated_bus_id=2) + _control_circuit(fresh_db, 9, arc, control_objective="ACTIVE_POWER_FLOW") + + +def test_regulated_bus_side_requires_a_regulated_bus(fresh_db): + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit(fresh_db, 4, arc, regulated_bus_side="OPPOSITE_WINDING") + + +def test_regulated_bus_side_is_an_enum(fresh_db): + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + make_bus(fresh_db, 9, "far") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit( + fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=9, + regulated_bus_side="PRIMARY", + ) + + +def test_own_bus_regulation_carries_no_side(fresh_db): + """R4: when the regulated bus is an end of the circuit's arc the side follows from + the arc, so storing one is a contradiction waiting to happen.""" + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + _control_circuit(fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=2) + with pytest.raises(sqlite3.IntegrityError, match="regulated_bus_side"): + _control_circuit( + fresh_db, 5, arc, control_objective="VOLTAGE", regulated_bus_id=1, + regulated_bus_side="CONTROLLING_WINDING", + ) + + +def test_off_arc_regulation_requires_a_side(fresh_db): + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + far = make_bus(fresh_db, 9, "far") + with pytest.raises(sqlite3.IntegrityError, match="regulated_bus_side"): + _control_circuit(fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=far) + _control_circuit( + fresh_db, 5, arc, control_objective="VOLTAGE", regulated_bus_id=far, + regulated_bus_side="OPPOSITE_WINDING", + ) + + +def test_regulated_bus_side_is_rechecked_on_update(fresh_db): + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + far = make_bus(fresh_db, 9, "far") + _control_circuit(fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=2) + with pytest.raises(sqlite3.IntegrityError, match="regulated_bus_side"): + fresh_db.execute( + "UPDATE transformer_circuits SET regulated_bus_id = ? WHERE id = 4", (far,) + ) + + +def test_load_drop_compensation_is_json(fresh_db): + arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) + _control_circuit( + fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=1, + load_drop_compensation='{"real": 0.01, "imag": 0.02}', + ) + (imag,) = fresh_db.execute( + "SELECT json_extract(load_drop_compensation, '$.imag') FROM transformer_circuits WHERE id = 4" + ).fetchone() + assert imag == 0.02 + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _control_circuit(fresh_db, 5, arc, load_drop_compensation="0.01+0.02j") + + +# Two-terminal HVDC lines + + +def test_vsc_line_remote_buses_are_foreign_keys(db): + foreign_keys = { + row[3]: row[2] for row in db.execute("PRAGMA foreign_key_list(two_terminal_hvdc_lines)") + } + assert foreign_keys["remote_regulated_bus_id_from"] == "balancing_topologies" + assert foreign_keys["remote_regulated_bus_id_to"] == "balancing_topologies" + assert foreign_keys["rectifier_commutating_bus_id"] == "balancing_topologies" + assert foreign_keys["inverter_commutating_bus_id"] == "balancing_topologies" + assert foreign_keys["rectifier_tap_transformer_id"] == "two_winding_transformers" + assert foreign_keys["inverter_tap_transformer_id"] == "two_winding_transformers" + + +def test_vsc_converter_cannot_name_its_own_terminal_bus(fresh_db): + """R1 per terminal: the from converter's own bus is the arc's from bus.""" + b1, b2, b3 = (make_bus(fresh_db, i, f"b{i}") for i in (1, 2, 3)) + arc = make_arc(fresh_db, 4, b1, b2) + _hvdc_line(fresh_db, 5, arc, "VSC", remote_regulated_bus_id_from=b3, remote_regulated_bus_id_to=b1) + with pytest.raises(sqlite3.IntegrityError, match="remote_regulated_bus_id_from"): + _hvdc_line(fresh_db, 6, arc, "VSC", remote_regulated_bus_id_from=b1) + with pytest.raises(sqlite3.IntegrityError, match="remote_regulated_bus_id_to"): + _hvdc_line(fresh_db, 7, arc, "VSC", remote_regulated_bus_id_to=b2) + + +def test_remote_buses_belong_to_vsc_lines_only(fresh_db): + b1, b2, b3 = (make_bus(fresh_db, i, f"b{i}") for i in (1, 2, 3)) + arc = make_arc(fresh_db, 4, b1, b2) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _hvdc_line(fresh_db, 5, arc, "LCC", remote_regulated_bus_id_from=b3) + + +def test_lcc_references_commutating_bus_and_tap_transformer(fresh_db): + b1, b2, b3, b4 = (make_bus(fresh_db, i, f"b{i}") for i in (1, 2, 3, 4)) + dc_arc = make_arc(fresh_db, 5, b1, b2) + tap_arc = make_arc(fresh_db, 6, b3, b4) + transformer = _two_winding(fresh_db, 8, make_circuit(fresh_db, 7, tap_arc)) + _hvdc_line( + fresh_db, 9, dc_arc, "LCC", rectifier_commutating_bus_id=b3, + rectifier_tap_transformer_id=transformer, + ) + row = fresh_db.execute( + "SELECT rectifier_commutating_bus_id, rectifier_tap_transformer_id, " + "inverter_commutating_bus_id, inverter_tap_transformer_id " + "FROM two_terminal_hvdc_lines WHERE id = 9" + ).fetchone() + assert row == (b3, transformer, None, None) + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY"): + _hvdc_line(fresh_db, 10, dc_arc, "LCC", inverter_tap_transformer_id=999) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _hvdc_line(fresh_db, 11, dc_arc, "VSC", rectifier_commutating_bus_id=b3) + + +# Voltage control groups and memberships + + +def test_voltage_control_group_tables_exist(db): + tables = { + row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'") + } + assert {"voltage_control_groups", "voltage_control_associations"} <= tables + + +def test_sharing_group_has_no_droop_parameters(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + _sharing_group(fresh_db, 2) + row = fresh_db.execute( + "SELECT regulated_bus_id, reactive_power_limits, voltage_limits, available " + "FROM voltage_control_groups WHERE id = 2" + ).fetchone() + assert row == (None, None, None, 1) + make_entity(fresh_db, 3, "voltage_control_groups", "ReactivePowerSharing") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + fresh_db.execute( + "INSERT INTO voltage_control_groups(id, name, TYPE, regulated_bus_id) " + "VALUES (3, 'share_3', 'ReactivePowerSharing', ?)", + (bus,), + ) + + +def test_droop_group_carries_its_characteristic(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + _droop_group(fresh_db, 2, bus, voltage_units="NATURAL_UNITS", deadband_reactive_power=5.0) + row = fresh_db.execute( + "SELECT regulated_bus_id, deadband_reactive_power, voltage_units, " + "json_extract(voltage_limits, '$.max') FROM voltage_control_groups WHERE id = 2" + ).fetchone() + assert row == (bus, 5.0, "NATURAL_UNITS", 1.05) + + +def test_droop_group_requires_its_regulated_bus_and_limits(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _droop_group(fresh_db, 2, None) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _droop_group(fresh_db, 3, bus, voltage_limits=None) + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY"): + _droop_group(fresh_db, 4, 999) + + +def test_group_type_is_one_of_the_two_attributes(fresh_db): + make_entity(fresh_db, 2, "voltage_control_groups", "PlantAttribute") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + fresh_db.execute( + "INSERT INTO voltage_control_groups(id, name, TYPE) VALUES (2, 'x', 'PlantAttribute')" + ) + + +def test_member_weight_is_positive(fresh_db): + """R9.""" + bus = make_bus(fresh_db, 1, "b1") + gen = _thermal(fresh_db, 2, bus) + group = _sharing_group(fresh_db, 3) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _member(fresh_db, group, gen, weight=0.0) + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _member(fresh_db, group, gen, weight=-0.5) + _member(fresh_db, group, gen, weight=0.6) + (weight,) = fresh_db.execute( + "SELECT weight FROM voltage_control_associations WHERE entity_id = ?", (gen,) + ).fetchone() + assert weight == 0.6 + + +def test_member_weight_defaults_to_one(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + gen = _thermal(fresh_db, 2, bus) + group = _sharing_group(fresh_db, 3) + fresh_db.execute( + "INSERT INTO voltage_control_associations(control_id, entity_id) VALUES (?, ?)", + (group, gen), + ) + (weight,) = fresh_db.execute( + "SELECT weight FROM voltage_control_associations WHERE entity_id = ?", (gen,) + ).fetchone() + assert weight == 1.0 + + +def test_device_belongs_to_at_most_one_group(fresh_db): + """R7 across both kinds of group.""" + bus = make_bus(fresh_db, 1, "b1") + gen = _thermal(fresh_db, 2, bus) + sharing = _sharing_group(fresh_db, 3) + droop = _droop_group(fresh_db, 4, make_bus(fresh_db, 5, "b5")) + _member(fresh_db, sharing, gen) + with pytest.raises(sqlite3.IntegrityError, match="UNIQUE"): + _member(fresh_db, droop, gen) + + +def test_each_vsc_terminal_belongs_to_at_most_one_group(fresh_db): + """R7 per converter: the two terminals of one line may sit in different groups, but + one terminal joins only once.""" + b1, b2 = make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2") + line = _hvdc_line(fresh_db, 4, make_arc(fresh_db, 3, b1, b2), "VSC") + first, second = _sharing_group(fresh_db, 5), _sharing_group(fresh_db, 6) + _member(fresh_db, first, line, terminal="FROM") + _member(fresh_db, second, line, terminal="TO") + with pytest.raises(sqlite3.IntegrityError, match="UNIQUE"): + _member(fresh_db, second, line, terminal="FROM") + + +def test_terminal_names_a_vsc_converter_only(fresh_db): + b1, b2 = make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2") + gen = _thermal(fresh_db, 3, b1) + line = _hvdc_line(fresh_db, 5, make_arc(fresh_db, 4, b1, b2), "VSC") + lcc = _hvdc_line(fresh_db, 7, make_arc(fresh_db, 6, b2, b1), "LCC") + group = _sharing_group(fresh_db, 8) + with pytest.raises(sqlite3.IntegrityError, match="terminal"): + _member(fresh_db, group, gen, terminal="FROM") + with pytest.raises(sqlite3.IntegrityError, match="terminal"): + _member(fresh_db, group, line) + with pytest.raises(sqlite3.IntegrityError, match="terminal|member"): + _member(fresh_db, group, lcc, terminal="FROM") + with pytest.raises(sqlite3.IntegrityError, match="CHECK"): + _member(fresh_db, group, line, terminal="RECTIFIER") + + +def test_sharing_members_are_voltage_control_devices(fresh_db): + """A load or a bus has no reactive power to share.""" + bus = make_bus(fresh_db, 1, "b1") + make_entity(fresh_db, 2, "loads", "PowerLoad") + group = _sharing_group(fresh_db, 3) + with pytest.raises(sqlite3.IntegrityError, match="member"): + _member(fresh_db, group, 2) + with pytest.raises(sqlite3.IntegrityError, match="member"): + _member(fresh_db, group, bus) + shunt = _shunt(fresh_db, 4, bus) + facts = _facts(fresh_db, 5, bus) + _member(fresh_db, group, shunt, weight=2.0) + _member(fresh_db, group, facts) + + +def test_droop_members_are_generators_only(fresh_db): + """R13.""" + bus = make_bus(fresh_db, 1, "b1") + droop = _droop_group(fresh_db, 2, make_bus(fresh_db, 9, "regulated")) + gen = _thermal(fresh_db, 3, bus) + shunt = _shunt(fresh_db, 4, bus) + facts = _facts(fresh_db, 5, bus) + _member(fresh_db, droop, gen, weight=0.7) + with pytest.raises(sqlite3.IntegrityError, match="generator"): + _member(fresh_db, droop, shunt) + with pytest.raises(sqlite3.IntegrityError, match="generator"): + _member(fresh_db, droop, facts) + + +def test_non_dispatch_renewable_cannot_join_a_droop_controller(fresh_db): + """The renewable table also holds RenewableNonDispatch, a fixed injection with no + voltage control, so membership is judged by the entity type, not the table.""" + bus = make_bus(fresh_db, 1, "b1") + droop = _droop_group(fresh_db, 2, make_bus(fresh_db, 9, "regulated")) + make_entity(fresh_db, 3, "renewable_generators", "RenewableNonDispatch") + make_entity(fresh_db, 4, "renewable_generators", "RenewableDispatch") + with pytest.raises(sqlite3.IntegrityError, match="generator"): + _member(fresh_db, droop, 3) + _member(fresh_db, droop, 4) + + +def test_membership_is_rechecked_on_update(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + droop = _droop_group(fresh_db, 2, make_bus(fresh_db, 9, "regulated")) + gen = _thermal(fresh_db, 3, bus) + shunt = _shunt(fresh_db, 4, bus) + _member(fresh_db, droop, gen) + with pytest.raises(sqlite3.IntegrityError, match="generator"): + fresh_db.execute( + "UPDATE voltage_control_associations SET entity_id = ? WHERE entity_id = ?", + (shunt, gen), + ) + + +def test_memberships_follow_their_group_and_member(fresh_db): + bus = make_bus(fresh_db, 1, "b1") + first = _thermal(fresh_db, 2, bus) + second = _thermal(fresh_db, 3, bus) + group = _sharing_group(fresh_db, 4) + _member(fresh_db, group, first) + _member(fresh_db, group, second) + fresh_db.execute("DELETE FROM thermal_generators WHERE id = ?", (first,)) + remaining = [ + row[0] + for row in fresh_db.execute( + "SELECT entity_id FROM voltage_control_associations ORDER BY entity_id" + ) + ] + assert remaining == [second] + fresh_db.execute("DELETE FROM voltage_control_groups WHERE id = ?", (group,)) + assert fresh_db.execute("SELECT count(*) FROM voltage_control_associations").fetchone() == (0,) + assert fresh_db.execute("SELECT count(*) FROM entities WHERE id = ?", (group,)).fetchone() == (0,) + + +def test_group_row_requires_its_entity(fresh_db): + with pytest.raises(sqlite3.IntegrityError, match="voltage_control_groups"): + fresh_db.execute( + "INSERT INTO voltage_control_groups(id, name, TYPE) VALUES (7, 'orphan', 'ReactivePowerSharing')" + ) + + +def test_schema_version_bumped_for_voltage_control(db): + (version,) = db.execute("PRAGMA user_version").fetchone() + assert version >= 2 From 47d9889fc1f1f0d255ced57c9ee2c7d1489b5a21 Mon Sep 17 00:00:00 2001 From: m-bossart Date: Thu, 24 Sep 2026 13:03:26 -0700 Subject: [PATCH 2/5] Regenerate the unit registry for the Admittance quantity kind The whole-complex Y_increase column is registered as Admittance; the per-part admittance columns keep their scalar kinds. --- schema/column_conventions.json | 2 +- schema/unit_registry.sql | 7 +++++-- test/test_sql_codegen.py | 1 + test/test_unit_registry.py | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/schema/column_conventions.json b/schema/column_conventions.json index b1fbabca..619e6ac6 100644 --- a/schema/column_conventions.json +++ b/schema/column_conventions.json @@ -228,7 +228,7 @@ {"table": "fixed_admittance", "column": "y_g", "quantity_kind": "ActivePower", "unit": "MW", "discriminator_column": "admittance_units", "discriminator_value": "COMPONENT_MVAR", "description": "Shunt conductance in the PSS/E form: MW at 1.0 pu voltage"}, {"table": "fixed_admittance", "column": "y_b", "quantity_kind": "Susceptance", "unit": "S", "discriminator_column": "admittance_units", "discriminator_value": "NATURAL_UNITS", "description": "Shunt susceptance in siemens"}, {"table": "fixed_admittance", "column": "y_b", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "admittance_units", "discriminator_value": "COMPONENT_MVAR", "description": "Shunt susceptance in the PSS/E form: MVAr at 1.0 pu voltage"}, - {"table": "switched_admittance", "column": "Y_increase", "quantity_kind": "Susceptance", "unit": "S", "discriminator_column": "admittance_units", "discriminator_value": "NATURAL_UNITS", "description": "Per-step admittance increment in siemens"}, + {"table": "switched_admittance", "column": "Y_increase", "quantity_kind": "Admittance", "unit": "S", "discriminator_column": "admittance_units", "discriminator_value": "NATURAL_UNITS", "description": "Per-step admittance increment in siemens"}, {"table": "switched_admittance", "column": "Y_increase", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "admittance_units", "discriminator_value": "COMPONENT_MVAR", "description": "Per-step admittance increment in the PSS/E form: MVAr at 1.0 pu voltage"}, {"table": "switched_admittance", "column": "solved_admittance", "quantity_kind": "Susceptance", "unit": "S", "discriminator_column": "admittance_units", "discriminator_value": "NATURAL_UNITS", "description": "Solved-case admittance (PSS/E BINIT) in siemens"}, {"table": "switched_admittance", "column": "solved_admittance", "quantity_kind": "ReactivePower", "unit": "MVAr", "discriminator_column": "admittance_units", "discriminator_value": "COMPONENT_MVAR", "description": "Solved-case admittance (PSS/E BINIT) in the PSS/E form: MVAr at 1.0 pu voltage"}, diff --git a/schema/unit_registry.sql b/schema/unit_registry.sql index fa506a35..bdcb0a2c 100644 --- a/schema/unit_registry.sql +++ b/schema/unit_registry.sql @@ -9,6 +9,7 @@ PRAGMA foreign_keys = ON; INSERT INTO quantity_kinds (name, default_unit, dimension, description) VALUES ('ActivePower', 'MW', '{"L":2,"M":1,"T":-3}', 'Active (real) power'), ('ActivePowerChangeRate', 'MW/min', '{"L":2,"M":1,"T":-4}', 'Rate of change of active power (ramp)'), + ('Admittance', 'S', '{"I":2,"L":-2,"M":-1,"T":3}', 'Admittance (complex conductance + j susceptance)'), ('Angle', 'rad', '{}', 'Angle (allowed units rad and deg)'), ('ApparentPower', 'MVA', '{"L":2,"M":1,"T":-3}', 'Apparent power'), ('CO2Emissions', 't/MMBtu', '{"Btu":-1,"M":1}', 'CO2 emission rate (mass per unit of fuel energy)'), @@ -58,6 +59,8 @@ INSERT INTO allowed_units (quantity_kind, unit) VALUES ('ActivePower', 'pu'), ('ActivePowerChangeRate', 'MW/min'), ('ActivePowerChangeRate', 'pu/min'), + ('Admittance', 'S'), + ('Admittance', 'pu'), ('Angle', 'deg'), ('Angle', 'rad'), ('ApparentPower', 'MVA'), @@ -387,7 +390,7 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('supply_technologies', 'time_limits', 'OperationalDuration', 'min', NULL, NULL, NULL, NULL, NULL, NULL, 'JSON {up, down} time limits'), ('supply_technologies', 'unit_size', 'ActivePower', 'MW', NULL, NULL, NULL, NULL, NULL, NULL, 'Unit nameplate capacity'), ('switched_admittance', 'Y_increase', 'ReactivePower', 'MVAr', 'admittance_units', 'COMPONENT_MVAR', NULL, NULL, NULL, NULL, 'Per-step admittance increment in the PSS/E form: MVAr at 1.0 pu voltage'), - ('switched_admittance', 'Y_increase', 'Susceptance', 'S', 'admittance_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Per-step admittance increment in siemens'), + ('switched_admittance', 'Y_increase', 'Admittance', 'S', 'admittance_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Per-step admittance increment in siemens'), ('switched_admittance', 'admittance_limits', 'ReactivePower', 'MVAr', 'admittance_units', 'COMPONENT_MVAR', NULL, NULL, NULL, NULL, 'Admittance limits (JSON min/max) in the PSS/E form: MVAr at 1.0 pu voltage'), ('switched_admittance', 'admittance_limits', 'Susceptance', 'S', 'admittance_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Admittance limits (JSON min/max) in siemens'), ('switched_admittance', 'control_mode', 'Fraction', '1', NULL, NULL, NULL, NULL, NULL, NULL, 'Switched-shunt control mode'), @@ -564,4 +567,4 @@ INSERT INTO unit_management_metadata (key, value, description) VALUES -- Inserting this row activates the immutability triggers. See the -- module docstring of generate_unit_registry.py for the exact repr. INSERT INTO unit_management_metadata (key, value, description) VALUES - ('unit_conventions_checksum', 'acd084e643a828bef2496a5c5cc9c14cd4e8a9078cec096699fddc8e40e073c6', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); + ('unit_conventions_checksum', 'd9810b38c8fd2836c03b3bb2936e7f652394c8cd7ed5576d34e7073674dcd80f', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); diff --git a/test/test_sql_codegen.py b/test/test_sql_codegen.py index 63d5eec2..40f92af9 100644 --- a/test/test_sql_codegen.py +++ b/test/test_sql_codegen.py @@ -128,6 +128,7 @@ def test_branch_parameter_pu_pairs_in_vocabulary(fresh_db): ("Reactance", "pu"), ("Impedance", "pu"), ("Susceptance", "pu"), + ("Admittance", "pu"), ("Conductance", "pu"), ("Voltage", "pu"), ("ActivePower", "pu"), diff --git a/test/test_unit_registry.py b/test/test_unit_registry.py index 7297b432..84fa4a1f 100644 --- a/test/test_unit_registry.py +++ b/test/test_unit_registry.py @@ -21,8 +21,8 @@ from conftest import SCHEMA_DIR, SCRIPTS_DIR, load_schemas_json, make_entity # Expected seed row counts (current sealed state). -EXPECTED_QUANTITY_TYPES = 41 -EXPECTED_ALLOWED_UNITS = 67 +EXPECTED_QUANTITY_TYPES = 42 +EXPECTED_ALLOWED_UNITS = 69 EXPECTED_UNIT_CONVENTIONS = 420 VERIFY_SCRIPT = SCRIPTS_DIR / "verify_unit_registry.py" @@ -1780,7 +1780,7 @@ def test_switched_admittance_admittance_units_conventions(db): "AND column_name IN ('Y_increase', 'solved_admittance', 'admittance_limits')" ).fetchall() assert set(rows) == { - ("Y_increase", "admittance_units", "NATURAL_UNITS", "Susceptance", "S"), + ("Y_increase", "admittance_units", "NATURAL_UNITS", "Admittance", "S"), ("Y_increase", "admittance_units", "COMPONENT_MVAR", "ReactivePower", "MVAr"), ("solved_admittance", "admittance_units", "NATURAL_UNITS", "Susceptance", "S"), ("solved_admittance", "admittance_units", "COMPONENT_MVAR", "ReactivePower", "MVAr"), From d09ac6b9457c92fa405639c4ec8d5dbde1549d39 Mon Sep 17 00:00:00 2001 From: m-bossart Date: Thu, 24 Sep 2026 13:12:29 -0700 Subject: [PATCH 3/5] Store load drop compensation as two REAL columns load_drop_compensation_r and load_drop_compensation_x replace the JSON blob, registered like the circuit's r and x; registry and reference DDL regenerated. --- schema/column_conventions.json | 6 ++++-- schema/generated_schema.sql | 3 ++- schema/schema.sql | 8 ++++---- schema/unit_registry.sql | 8 +++++--- test/test_sql_codegen.py | 6 ++++-- test/test_unit_registry.py | 2 +- test/test_voltage_control.py | 24 +++++++++++++----------- 7 files changed, 33 insertions(+), 24 deletions(-) diff --git a/schema/column_conventions.json b/schema/column_conventions.json index 619e6ac6..780d5d1a 100644 --- a/schema/column_conventions.json +++ b/schema/column_conventions.json @@ -67,8 +67,10 @@ {"table": "transformer_circuits", "column": "base_power", "quantity_kind": "ApparentPower", "unit": "MVA", "description": "Base power the row's per-unit values resolve against"}, {"table": "transformer_circuits", "column": "base_voltage_primary", "quantity_kind": "Voltage", "unit": "kV", "description": "Primary (from) terminal-side base voltage; reference for the circuit's pu impedance"}, {"table": "transformer_circuits", "column": "base_voltage_secondary", "quantity_kind": "Voltage", "unit": "kV", "description": "Secondary (to) terminal-side base voltage"}, - {"table": "transformer_circuits", "column": "load_drop_compensation", "quantity_kind": "Impedance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "base_voltage_ref": "base_voltage_primary", "description": "Load drop compensation impedance (JSON real/imag = PSS/E CR/CX), per-unit on the row's base_power, referenced to base_voltage_primary"}, - {"table": "transformer_circuits", "column": "load_drop_compensation", "quantity_kind": "Impedance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Load drop compensation impedance (JSON real/imag) in ohm"}, + {"table": "transformer_circuits", "column": "load_drop_compensation_r", "quantity_kind": "Resistance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "base_voltage_ref": "base_voltage_primary", "description": "Resistive part of the load drop compensation impedance (PSS/E CR), per-unit on the row's base_power, referenced to base_voltage_primary"}, + {"table": "transformer_circuits", "column": "load_drop_compensation_r", "quantity_kind": "Resistance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Resistive part of the load drop compensation impedance (PSS/E CR), in ohm"}, + {"table": "transformer_circuits", "column": "load_drop_compensation_x", "quantity_kind": "Reactance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power", "base_voltage_ref": "base_voltage_primary", "description": "Reactive part of the load drop compensation impedance (PSS/E CX), per-unit on the row's base_power, referenced to base_voltage_primary"}, + {"table": "transformer_circuits", "column": "load_drop_compensation_x", "quantity_kind": "Reactance", "unit": "ohm", "discriminator_column": "parameter_units", "discriminator_value": "NATURAL_UNITS", "description": "Reactive part of the load drop compensation impedance (PSS/E CX), in ohm"}, {"table": "two_winding_transformers", "column": "magnetizing_shunt.real", "quantity_kind": "Conductance", "unit": "pu", "base_power_ref": "circuit->transformer_circuits.base_power", "base_voltage_ref": "circuit->transformer_circuits.base_voltage_primary", "description": "Magnetizing shunt conductance, per-unit on the circuit's base_power (magnetizing_shunt JSON path)"}, {"table": "two_winding_transformers", "column": "magnetizing_shunt.imag", "quantity_kind": "Susceptance", "unit": "pu", "base_power_ref": "circuit->transformer_circuits.base_power", "base_voltage_ref": "circuit->transformer_circuits.base_voltage_primary", "description": "Magnetizing shunt susceptance, per-unit on the circuit's base_power (magnetizing_shunt JSON path)"}, {"table": "three_winding_transformers", "column": "r_12", "quantity_kind": "Resistance", "unit": "pu", "discriminator_column": "parameter_units", "discriminator_value": "COMPONENT_BASE", "base_power_ref": "base_power_12", "base_voltage_ref": "primary_circuit->transformer_circuits.base_voltage_primary", "description": "Measured primary-secondary resistance, per-unit on base_power_12, referred to the primary winding's voltage base per PSS/E convention"}, diff --git a/schema/generated_schema.sql b/schema/generated_schema.sql index 9d1acd85..ce23346f 100644 --- a/schema/generated_schema.sql +++ b/schema/generated_schema.sql @@ -252,7 +252,8 @@ CREATE TABLE transformer_circuits ( control_objective TEXT NULL DEFAULT 'UNDEFINED' CHECK (control_objective IN ('UNDEFINED', 'VOLTAGE_DISABLED', 'REACTIVE_POWER_FLOW_DISABLED', 'ACTIVE_POWER_FLOW_DISABLED', 'CONTROL_OF_DC_LINE_DISABLED', 'ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED', 'FIXED', 'VOLTAGE', 'REACTIVE_POWER_FLOW', 'ACTIVE_POWER_FLOW', 'CONTROL_OF_DC_LINE', 'ASYMMETRIC_ACTIVE_POWER_FLOW')), regulated_bus_id INTEGER NULL, regulated_bus_side JSON NULL, - load_drop_compensation JSON NULL DEFAULT '{"imag":0.0,"real":0.0}', -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) + load_drop_compensation_r REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) + load_drop_compensation_x REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) control_limits JSON NULL DEFAULT '{"max":1.1,"min":0.9}', -- Units: per control_objective (ACTIVE_POWER_FLOW: rad, ACTIVE_POWER_FLOW_DISABLED: rad, ASYMMETRIC_ACTIVE_POWER_FLOW: rad, ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED: rad, CONTROL_OF_DC_LINE: 1, CONTROL_OF_DC_LINE_DISABLED: 1, FIXED: 1, REACTIVE_POWER_FLOW: 1, REACTIVE_POWER_FLOW_DISABLED: 1, UNDEFINED: 1, VOLTAGE: 1, VOLTAGE_DISABLED: 1) controlled_quantity_limits JSON NULL DEFAULT '{"max":1.1,"min":0.9}', -- Units: per control_objective (ACTIVE_POWER_FLOW: MW, ACTIVE_POWER_FLOW_DISABLED: MW, ASYMMETRIC_ACTIVE_POWER_FLOW: MW, ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED: MW, CONTROL_OF_DC_LINE: MW, CONTROL_OF_DC_LINE_DISABLED: MW, FIXED: pu, REACTIVE_POWER_FLOW: MVAr, REACTIVE_POWER_FLOW_DISABLED: MVAr, UNDEFINED: pu, VOLTAGE: pu, VOLTAGE_DISABLED: pu) number_of_tap_positions INTEGER NULL DEFAULT 33, diff --git a/schema/schema.sql b/schema/schema.sql index 6958efcf..8a8ae7ba 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -262,10 +262,10 @@ CREATE TABLE transformer_circuits ( regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id), regulated_bus_side TEXT NULL CHECK (regulated_bus_side IS NULL OR regulated_bus_side IN ('CONTROLLING_WINDING', 'OPPOSITE_WINDING')), - -- Load drop compensation {"real": CR, "imag": CX}: the impedance times the circuit - -- current that corrects the regulated voltage. - load_drop_compensation TEXT NOT NULL DEFAULT '{"real": 0.0, "imag": 0.0}' - CHECK (json_valid(load_drop_compensation)), -- Units: per parameter_units + -- Load drop compensation CR + jCX: the impedance times the circuit current that + -- corrects the regulated voltage. + load_drop_compensation_r REAL NOT NULL DEFAULT 0.0, -- Units: per parameter_units + load_drop_compensation_x REAL NOT NULL DEFAULT 0.0, -- Units: per parameter_units control_limits TEXT NULL DEFAULT '{"min": 0.9, "max": 1.1}' CHECK (control_limits IS NULL OR json_valid(control_limits)), -- Units: per control_objective (tap ratio 1 / angle rad) controlled_quantity_limits TEXT NULL DEFAULT '{"min": 0.9, "max": 1.1}' diff --git a/schema/unit_registry.sql b/schema/unit_registry.sql index bdcb0a2c..4cacefcb 100644 --- a/schema/unit_registry.sql +++ b/schema/unit_registry.sql @@ -481,8 +481,10 @@ INSERT INTO unit_conventions (table_name, column_name, quantity_kind, unit, disc ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'UNDEFINED', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} controlled-quantity band; no control block'), ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'VOLTAGE', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} pu voltage band (voltage control)'), ('transformer_circuits', 'controlled_quantity_limits', 'Voltage', 'pu', 'control_objective', 'VOLTAGE_DISABLED', NULL, NULL, NULL, 'base_voltage_primary', 'JSON {min, max} pu voltage band (voltage control, disabled)'), - ('transformer_circuits', 'load_drop_compensation', 'Impedance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Load drop compensation impedance (JSON real/imag = PSS/E CR/CX), per-unit on the row''s base_power, referenced to base_voltage_primary'), - ('transformer_circuits', 'load_drop_compensation', 'Impedance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Load drop compensation impedance (JSON real/imag) in ohm'), + ('transformer_circuits', 'load_drop_compensation_r', 'Resistance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Resistive part of the load drop compensation impedance (PSS/E CR), per-unit on the row''s base_power, referenced to base_voltage_primary'), + ('transformer_circuits', 'load_drop_compensation_r', 'Resistance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Resistive part of the load drop compensation impedance (PSS/E CR), in ohm'), + ('transformer_circuits', 'load_drop_compensation_x', 'Reactance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Reactive part of the load drop compensation impedance (PSS/E CX), per-unit on the row''s base_power, referenced to base_voltage_primary'), + ('transformer_circuits', 'load_drop_compensation_x', 'Reactance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Reactive part of the load drop compensation impedance (PSS/E CX), in ohm'), ('transformer_circuits', 'r', 'Resistance', 'pu', 'parameter_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', 'base_voltage_primary', 'Circuit resistance, per-unit on the row''s base_power, referenced to base_voltage_primary'), ('transformer_circuits', 'r', 'Resistance', 'ohm', 'parameter_units', 'NATURAL_UNITS', NULL, NULL, NULL, NULL, 'Circuit resistance, in ohm'), ('transformer_circuits', 'rating', 'ApparentPower', 'pu', 'power_units', 'COMPONENT_BASE', NULL, NULL, 'base_power', NULL, 'Thermal rating, per-unit on the row''s base_power'), @@ -567,4 +569,4 @@ INSERT INTO unit_management_metadata (key, value, description) VALUES -- Inserting this row activates the immutability triggers. See the -- module docstring of generate_unit_registry.py for the exact repr. INSERT INTO unit_management_metadata (key, value, description) VALUES - ('unit_conventions_checksum', 'd9810b38c8fd2836c03b3bb2936e7f652394c8cd7ed5576d34e7073674dcd80f', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); + ('unit_conventions_checksum', '80ab58bbf50508e6742a1c6a042a11de770f91f7d2038b140b1d6ffb663243c3', 'Registry content fingerprint -- verify with scripts/verify_unit_registry.py'); diff --git a/test/test_sql_codegen.py b/test/test_sql_codegen.py index 40f92af9..0a6eade8 100644 --- a/test/test_sql_codegen.py +++ b/test/test_sql_codegen.py @@ -316,8 +316,10 @@ def test_transformer_circuits_columns_and_units(fresh_db): ("r", "NATURAL_UNITS"): ("Resistance", "ohm"), ("x", "COMPONENT_BASE"): ("Reactance", "pu"), ("x", "NATURAL_UNITS"): ("Reactance", "ohm"), - ("load_drop_compensation", "COMPONENT_BASE"): ("Impedance", "pu"), - ("load_drop_compensation", "NATURAL_UNITS"): ("Impedance", "ohm"), + ("load_drop_compensation_r", "COMPONENT_BASE"): ("Resistance", "pu"), + ("load_drop_compensation_r", "NATURAL_UNITS"): ("Resistance", "ohm"), + ("load_drop_compensation_x", "COMPONENT_BASE"): ("Reactance", "pu"), + ("load_drop_compensation_x", "NATURAL_UNITS"): ("Reactance", "ohm"), } assert { diff --git a/test/test_unit_registry.py b/test/test_unit_registry.py index 84fa4a1f..0a173176 100644 --- a/test/test_unit_registry.py +++ b/test/test_unit_registry.py @@ -23,7 +23,7 @@ # Expected seed row counts (current sealed state). EXPECTED_QUANTITY_TYPES = 42 EXPECTED_ALLOWED_UNITS = 69 -EXPECTED_UNIT_CONVENTIONS = 420 +EXPECTED_UNIT_CONVENTIONS = 422 VERIFY_SCRIPT = SCRIPTS_DIR / "verify_unit_registry.py" REGISTRY_SQL = SCHEMA_DIR / "unit_registry.sql" diff --git a/test/test_voltage_control.py b/test/test_voltage_control.py index 6a04ea82..a7b8357e 100644 --- a/test/test_voltage_control.py +++ b/test/test_voltage_control.py @@ -282,9 +282,8 @@ def test_transformer_circuit_control_columns(db): assert "regulated_bus_number" not in columns assert columns["regulated_bus_id"][3] == 0 assert columns["regulated_bus_side"][3] == 0 - assert json.loads(columns["load_drop_compensation"][4].strip("'")) == { - "real": 0.0, "imag": 0.0, - } + assert columns["load_drop_compensation_r"][4] == "0.0" + assert columns["load_drop_compensation_x"][4] == "0.0" foreign_keys = { row[3]: row[2] for row in db.execute("PRAGMA foreign_key_list(transformer_circuits)") } @@ -355,18 +354,21 @@ def test_regulated_bus_side_is_rechecked_on_update(fresh_db): ) -def test_load_drop_compensation_is_json(fresh_db): +def test_load_drop_compensation_round_trips_as_two_columns(fresh_db): arc = make_arc(fresh_db, 3, make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2")) _control_circuit( fresh_db, 4, arc, control_objective="VOLTAGE", regulated_bus_id=1, - load_drop_compensation='{"real": 0.01, "imag": 0.02}', + load_drop_compensation_r=0.01, load_drop_compensation_x=0.02, ) - (imag,) = fresh_db.execute( - "SELECT json_extract(load_drop_compensation, '$.imag') FROM transformer_circuits WHERE id = 4" - ).fetchone() - assert imag == 0.02 - with pytest.raises(sqlite3.IntegrityError, match="CHECK"): - _control_circuit(fresh_db, 5, arc, load_drop_compensation="0.01+0.02j") + assert fresh_db.execute( + "SELECT load_drop_compensation_r, load_drop_compensation_x " + "FROM transformer_circuits WHERE id = 4" + ).fetchone() == (0.01, 0.02) + _control_circuit(fresh_db, 5, arc, control_objective="VOLTAGE", regulated_bus_id=1) + assert fresh_db.execute( + "SELECT load_drop_compensation_r, load_drop_compensation_x " + "FROM transformer_circuits WHERE id = 5" + ).fetchone() == (0.0, 0.0) # Two-terminal HVDC lines From 5ab767f4180da636b6868a93102a8c80d73f1c59 Mon Sep 17 00:00:00 2001 From: m-bossart Date: Thu, 24 Sep 2026 14:08:23 -0700 Subject: [PATCH 4/5] Clear a device's remote reference when its target is deleted The regulated-bus and tap-transformer references are non-owning, so they follow settlement_point_id with ON DELETE SET NULL; null already means the device regulates its own bus or has no tap transformer. A droop controller's own regulated bus cascades, since the controller exists for that bus. --- schema/schema.sql | 34 +++++++++++++++++----------------- test/test_voltage_control.py | 25 ++++++++++++++++++++----- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/schema/schema.sql b/schema/schema.sql index 8a8ae7ba..1cdb8e70 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -259,7 +259,7 @@ CREATE TABLE transformer_circuits ( -- voltage. regulated_bus_side says which winding the bus lies beyond when it is not an -- end of the circuit's arc; for an arc end the side follows from the arc and must be -- NULL (enforce_transformer_circuits_regulated_bus_side_*). - regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, regulated_bus_side TEXT NULL CHECK (regulated_bus_side IS NULL OR regulated_bus_side IN ('CONTROLLING_WINDING', 'OPPOSITE_WINDING')), -- Load drop compensation CR + jCX: the impedance times the circuit current that @@ -358,7 +358,7 @@ CREATE TABLE thermal_generators ( balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -413,7 +413,7 @@ CREATE TABLE renewable_generators ( balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -458,7 +458,7 @@ CREATE TABLE hydro_generators ( balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -513,7 +513,7 @@ CREATE TABLE storage_units ( balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> balancing_topology), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -771,7 +771,7 @@ CREATE TABLE voltage_control_groups ( name TEXT NOT NULL UNIQUE, TYPE TEXT NOT NULL CHECK (TYPE IN ('VoltageDroopControl', 'ReactivePowerSharing')), available INTEGER NOT NULL DEFAULT 1 CHECK (available IN (0, 1)), - regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id), + regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, reactive_power_limits TEXT NULL -- {"min": ..., "max": ...} CHECK (reactive_power_limits IS NULL OR json_valid(reactive_power_limits)), -- Units: MVAr deadband_reactive_power REAL NULL, -- Units: MVAr @@ -1008,7 +1008,7 @@ CREATE TABLE switched_admittance ( 'DISCRETE_REACTIVE_VSC', 'DISCRETE_ADMITTANCE_REMOTE')), -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own -- bus is rejected. A shared regulation weight lives on voltage_control_associations: - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus) ) strict; @@ -1022,7 +1022,7 @@ CREATE TABLE synchronous_condensers ( bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -1049,7 +1049,7 @@ CREATE TABLE sources ( bus INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, -- Bus whose voltage the unit holds while it regulates voltage. NULL means its own -- bus, so naming the own bus is rejected; the reference blocks deleting that bus. - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), voltage_setpoint REAL NOT NULL DEFAULT 1.0 CHECK (voltage_setpoint > 0), -- Units: per voltage_setpoint_units voltage_setpoint_units TEXT NOT NULL DEFAULT 'COMPONENT_BASE' @@ -1169,14 +1169,14 @@ CREATE TABLE two_terminal_hvdc_lines ( CHECK (reactive_power_limits_to IS NULL OR json_valid(reactive_power_limits_to)), -- Units: per power_units -- VSC only: the bus each converter's AC voltage control holds. NULL means its own -- terminal bus, which enforce_two_terminal_hvdc_lines_remote_bus_* forbids naming. - remote_regulated_bus_id_from INTEGER NULL REFERENCES balancing_topologies (id), - remote_regulated_bus_id_to INTEGER NULL REFERENCES balancing_topologies (id), + remote_regulated_bus_id_from INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, + remote_regulated_bus_id_to INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, -- LCC only: each converter's commutating bus and tap transformer, when they are not -- its terminal bus and an ideal transformer. - rectifier_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id), - inverter_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id), - rectifier_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id), - inverter_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id), + rectifier_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, + inverter_commutating_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, + rectifier_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id) ON DELETE SET NULL, + inverter_tap_transformer_id INTEGER NULL REFERENCES two_winding_transformers (id) ON DELETE SET NULL, CHECK (converter_type = 'VSC' OR (remote_regulated_bus_id_from IS NULL AND remote_regulated_bus_id_to IS NULL)), CHECK (converter_type = 'LCC' @@ -1223,7 +1223,7 @@ CREATE TABLE facts_control_devices ( CHECK (shunt_control_type IN ('SVC', 'STATCOM')), -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own -- bus is rejected. A shared regulation weight lives on voltage_control_associations: - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus) ) strict; @@ -1249,7 +1249,7 @@ CREATE TABLE interconnecting_converters ( power_units TEXT NOT NULL CHECK (power_units IN ('COMPONENT_BASE', 'NATURAL_UNITS')), -- Bus whose voltage the device regulates; NULL means its own bus, so naming the own -- bus is rejected. A shared regulation weight lives on voltage_control_associations: - remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) + remote_regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL CHECK (remote_regulated_bus_id IS NULL OR remote_regulated_bus_id <> bus), power_factor_weighting_fraction REAL NOT NULL DEFAULT 1.0 CHECK (power_factor_weighting_fraction >= 0), voltage_limits TEXT NULL DEFAULT '{"min": 0.0, "max": 999.9}' diff --git a/test/test_voltage_control.py b/test/test_voltage_control.py index a7b8357e..0b3a52e9 100644 --- a/test/test_voltage_control.py +++ b/test/test_voltage_control.py @@ -264,14 +264,29 @@ def test_facts_cannot_name_its_own_bus_as_remote(fresh_db): _facts(fresh_db, 4, bus, remote_regulated_bus_id=bus) -def test_deleting_a_remote_bus_is_refused_while_a_device_regulates_it(fresh_db): - """A device regulating a deleted bus would silently fall back to its own bus, so the - reference blocks the delete instead of being cleared.""" +def test_deleting_a_droop_controllers_bus_removes_the_controller(fresh_db): + """A droop controller exists for its regulated bus the way a device exists on its bus, + so that reference cascades instead of clearing.""" + bus = make_bus(fresh_db, 1, "b1") + _droop_group(fresh_db, 10, bus) + fresh_db.execute("DELETE FROM balancing_topologies WHERE id = ?", (bus,)) + (count,) = fresh_db.execute( + "SELECT COUNT(*) FROM voltage_control_groups WHERE id = 10" + ).fetchone() + assert count == 0 + + +def test_deleting_a_remote_bus_clears_the_reference(fresh_db): + """A remote regulated bus is a non-owning reference like settlement_point_id: deleting + the bus clears it, and null already means the device regulates its own bus.""" own = make_bus(fresh_db, 1, "b1") remote = make_bus(fresh_db, 2, "b2") _thermal(fresh_db, 3, own, remote_regulated_bus_id=remote) - with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY"): - fresh_db.execute("DELETE FROM balancing_topologies WHERE id = ?", (remote,)) + fresh_db.execute("DELETE FROM balancing_topologies WHERE id = ?", (remote,)) + (cleared,) = fresh_db.execute( + "SELECT remote_regulated_bus_id FROM thermal_generators WHERE id = 3" + ).fetchone() + assert cleared is None # Transformer circuits From 9342b2a6caef1304f3dd6a9f9987312912447013 Mon Sep 17 00:00:00 2001 From: m-bossart Date: Thu, 24 Sep 2026 14:30:24 -0700 Subject: [PATCH 5/5] Store the regulated bus side and the member terminal as UNDEFINED, not NULL Both enums gain an UNDEFINED member, following TransformerControlObjective and the other sentinel enums, so the columns are NOT NULL with that default and the membership index needs no COALESCE. --- schema/generated_schema.sql | 2 +- schema/schema.sql | 15 ++++++++------- schema/triggers.sql | 20 ++++++++++---------- test/test_voltage_control.py | 19 +++++++++++++++++-- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/schema/generated_schema.sql b/schema/generated_schema.sql index ce23346f..577a831f 100644 --- a/schema/generated_schema.sql +++ b/schema/generated_schema.sql @@ -251,7 +251,7 @@ CREATE TABLE transformer_circuits ( x REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) control_objective TEXT NULL DEFAULT 'UNDEFINED' CHECK (control_objective IN ('UNDEFINED', 'VOLTAGE_DISABLED', 'REACTIVE_POWER_FLOW_DISABLED', 'ACTIVE_POWER_FLOW_DISABLED', 'CONTROL_OF_DC_LINE_DISABLED', 'ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED', 'FIXED', 'VOLTAGE', 'REACTIVE_POWER_FLOW', 'ACTIVE_POWER_FLOW', 'CONTROL_OF_DC_LINE', 'ASYMMETRIC_ACTIVE_POWER_FLOW')), regulated_bus_id INTEGER NULL, - regulated_bus_side JSON NULL, + regulated_bus_side TEXT NULL DEFAULT 'UNDEFINED' CHECK (regulated_bus_side IN ('UNDEFINED', 'CONTROLLING_WINDING', 'OPPOSITE_WINDING')), load_drop_compensation_r REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) load_drop_compensation_x REAL NULL DEFAULT 0.0, -- Units: per parameter_units (COMPONENT_BASE: pu, NATURAL_UNITS: ohm) control_limits JSON NULL DEFAULT '{"max":1.1,"min":0.9}', -- Units: per control_objective (ACTIVE_POWER_FLOW: rad, ACTIVE_POWER_FLOW_DISABLED: rad, ASYMMETRIC_ACTIVE_POWER_FLOW: rad, ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED: rad, CONTROL_OF_DC_LINE: 1, CONTROL_OF_DC_LINE_DISABLED: 1, FIXED: 1, REACTIVE_POWER_FLOW: 1, REACTIVE_POWER_FLOW_DISABLED: 1, UNDEFINED: 1, VOLTAGE: 1, VOLTAGE_DISABLED: 1) diff --git a/schema/schema.sql b/schema/schema.sql index 1cdb8e70..11ae4f8f 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -258,10 +258,10 @@ CREATE TABLE transformer_circuits ( -- Bus whose voltage the tap holds, named exactly when control_objective regulates -- voltage. regulated_bus_side says which winding the bus lies beyond when it is not an -- end of the circuit's arc; for an arc end the side follows from the arc and must be - -- NULL (enforce_transformer_circuits_regulated_bus_side_*). + -- UNDEFINED (enforce_transformer_circuits_regulated_bus_side_*). regulated_bus_id INTEGER NULL REFERENCES balancing_topologies (id) ON DELETE SET NULL, - regulated_bus_side TEXT NULL - CHECK (regulated_bus_side IS NULL OR regulated_bus_side IN ('CONTROLLING_WINDING', 'OPPOSITE_WINDING')), + regulated_bus_side TEXT NOT NULL DEFAULT 'UNDEFINED' + CHECK (regulated_bus_side IN ('UNDEFINED', 'CONTROLLING_WINDING', 'OPPOSITE_WINDING')), -- Load drop compensation CR + jCX: the impedance times the circuit current that -- corrects the regulated voltage. load_drop_compensation_r REAL NOT NULL DEFAULT 0.0, -- Units: per parameter_units @@ -281,7 +281,7 @@ CREATE TABLE transformer_circuits ( base_voltage_primary REAL NULL CHECK (base_voltage_primary > 0), -- Units: kV base_voltage_secondary REAL NULL CHECK (base_voltage_secondary > 0), -- Units: kV CHECK ((regulated_bus_id IS NOT NULL) = (control_objective IN ('VOLTAGE', 'VOLTAGE_DISABLED'))), - CHECK (regulated_bus_side IS NULL OR regulated_bus_id IS NOT NULL) + CHECK (regulated_bus_side = 'UNDEFINED' OR regulated_bus_id IS NOT NULL) ) strict; -- Two-winding transformer (PSY TwoWindingTransformer); series data lives on @@ -790,7 +790,8 @@ CREATE TABLE voltage_control_groups ( ) strict; -- One row per member of a voltage control group: entity_id is the regulating device, or --- a two-terminal VSC line with terminal naming the converter. weight is the member's +-- a two-terminal VSC line with terminal naming the converter (UNDEFINED for any other +-- member). weight is the member's -- relative reactive power share (PSS/E RMPCT / 100); shares are relative, so a group's -- rows need not sum to one. Which entities may join, and when terminal is required, is -- enforced by enforce_voltage_control_member_types_*. Like plant_associations, the @@ -800,12 +801,12 @@ CREATE TABLE voltage_control_associations ( control_id INTEGER NOT NULL REFERENCES voltage_control_groups (id) ON DELETE CASCADE, entity_id INTEGER NOT NULL REFERENCES entities (id) ON DELETE CASCADE, weight REAL NOT NULL DEFAULT 1.0 CHECK (weight > 0), -- Units: 1 - terminal TEXT NULL CHECK (terminal IS NULL OR terminal IN ('FROM', 'TO')) + terminal TEXT NOT NULL DEFAULT 'UNDEFINED' CHECK (terminal IN ('UNDEFINED', 'FROM', 'TO')) ) strict; -- A device, or one converter of a VSC line, belongs to at most one group of either kind. CREATE UNIQUE INDEX uq_voltage_control_member - ON voltage_control_associations (entity_id, COALESCE(terminal, '')); + ON voltage_control_associations (entity_id, terminal); CREATE INDEX idx_voltage_control_assoc_control ON voltage_control_associations (control_id, entity_id); diff --git a/schema/triggers.sql b/schema/triggers.sql index c11d3bfa..2c6f8ca0 100644 --- a/schema/triggers.sql +++ b/schema/triggers.sql @@ -2583,7 +2583,7 @@ END; CREATE TRIGGER IF NOT EXISTS enforce_transformer_circuits_regulated_bus_side_insert BEFORE INSERT ON transformer_circuits WHEN NEW.regulated_bus_id IS NOT NULL - AND (NEW.regulated_bus_side IS NULL) <> EXISTS ( + AND (NEW.regulated_bus_side = 'UNDEFINED') <> EXISTS ( SELECT 1 FROM arcs WHERE id = NEW.arc_id @@ -2592,14 +2592,14 @@ INSERT ON transformer_circuits BEGIN SELECT RAISE( ABORT, - 'transformer_circuits.regulated_bus_side must be NULL when regulated_bus_id is an end of the circuit''s arc and set when it is not' + 'transformer_circuits.regulated_bus_side must be UNDEFINED when regulated_bus_id is an end of the circuit''s arc and a winding side when it is not' ); END; CREATE TRIGGER IF NOT EXISTS enforce_transformer_circuits_regulated_bus_side_update BEFORE UPDATE OF regulated_bus_id, regulated_bus_side, arc_id ON transformer_circuits WHEN NEW.regulated_bus_id IS NOT NULL - AND (NEW.regulated_bus_side IS NULL) <> EXISTS ( + AND (NEW.regulated_bus_side = 'UNDEFINED') <> EXISTS ( SELECT 1 FROM arcs WHERE id = NEW.arc_id @@ -2608,7 +2608,7 @@ UPDATE OF regulated_bus_id, regulated_bus_side, arc_id ON transformer_circuits BEGIN SELECT RAISE( ABORT, - 'transformer_circuits.regulated_bus_side must be NULL when regulated_bus_id is an end of the circuit''s arc and set when it is not' + 'transformer_circuits.regulated_bus_side must be UNDEFINED when regulated_bus_id is an end of the circuit''s arc and a winding side when it is not' ); END; @@ -2640,8 +2640,8 @@ END; -- Voltage control membership. Droop members are generators; sharing members are the -- devices that regulate voltage (RenewableNonDispatch shares the renewable table but is a --- fixed injection); terminal names a converter of a two-terminal VSC line and nothing --- else. The member's table comes from entities, the same source the arc trigger reads. +-- fixed injection); terminal names a converter of a two-terminal VSC line and is +-- UNDEFINED for anything else. The member's table comes from entities, the same source the arc trigger reads. CREATE TRIGGER IF NOT EXISTS enforce_voltage_control_member_types_insert AFTER INSERT ON voltage_control_associations BEGIN @@ -2664,11 +2664,11 @@ SELECT AND entity_type <> 'RenewableNonDispatch' ) THEN RAISE(ABORT, 'voltage_control_associations: the member must be a voltage regulating device (generator, storage unit, synchronous condenser, source, switched shunt, FACTS device, converter or two-terminal VSC line)') - WHEN (NEW.terminal IS NULL) = COALESCE( + WHEN (NEW.terminal = 'UNDEFINED') = COALESCE( (SELECT converter_type FROM two_terminal_hvdc_lines WHERE id = NEW.entity_id) = 'VSC', 0 ) - THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be NULL for any other member') + THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be UNDEFINED for any other member') END; END; @@ -2694,10 +2694,10 @@ SELECT AND entity_type <> 'RenewableNonDispatch' ) THEN RAISE(ABORT, 'voltage_control_associations: the member must be a voltage regulating device (generator, storage unit, synchronous condenser, source, switched shunt, FACTS device, converter or two-terminal VSC line)') - WHEN (NEW.terminal IS NULL) = COALESCE( + WHEN (NEW.terminal = 'UNDEFINED') = COALESCE( (SELECT converter_type FROM two_terminal_hvdc_lines WHERE id = NEW.entity_id) = 'VSC', 0 ) - THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be NULL for any other member') + THEN RAISE(ABORT, 'voltage_control_associations.terminal names the converter of a two-terminal VSC line member and must be UNDEFINED for any other member') END; END; diff --git a/test/test_voltage_control.py b/test/test_voltage_control.py index 0b3a52e9..8f66b7e3 100644 --- a/test/test_voltage_control.py +++ b/test/test_voltage_control.py @@ -171,7 +171,7 @@ def _droop_group(conn, group_id, bus_id, **extra): return group_id -def _member(conn, group_id, entity_id, weight=1.0, terminal=None): +def _member(conn, group_id, entity_id, weight=1.0, terminal="UNDEFINED"): conn.execute( "INSERT INTO voltage_control_associations(control_id, entity_id, weight, terminal) " "VALUES (?, ?, ?, ?)", @@ -296,7 +296,8 @@ def test_transformer_circuit_control_columns(db): columns = _columns(db, "transformer_circuits") assert "regulated_bus_number" not in columns assert columns["regulated_bus_id"][3] == 0 - assert columns["regulated_bus_side"][3] == 0 + assert columns["regulated_bus_side"][3] == 1 + assert columns["regulated_bus_side"][4] == "'UNDEFINED'" assert columns["load_drop_compensation_r"][4] == "0.0" assert columns["load_drop_compensation_x"][4] == "0.0" foreign_keys = { @@ -548,6 +549,20 @@ def test_each_vsc_terminal_belongs_to_at_most_one_group(fresh_db): _member(fresh_db, second, line, terminal="FROM") +def test_terminal_is_never_null(fresh_db): + """UNDEFINED, not NULL, is how a single-bus member says it names no converter.""" + b1 = make_bus(fresh_db, 1, "b1") + gen = _thermal(fresh_db, 3, b1) + group = _sharing_group(fresh_db, 8) + with pytest.raises(sqlite3.IntegrityError, match="NOT NULL"): + _member(fresh_db, group, gen, terminal=None) + _member(fresh_db, group, gen) + (terminal,) = fresh_db.execute( + "SELECT terminal FROM voltage_control_associations WHERE entity_id = 3" + ).fetchone() + assert terminal == "UNDEFINED" + + def test_terminal_names_a_vsc_converter_only(fresh_db): b1, b2 = make_bus(fresh_db, 1, "b1"), make_bus(fresh_db, 2, "b2") gen = _thermal(fresh_db, 3, b1)