diff --git a/distribution copy.sqlite3 b/distribution copy.sqlite3 new file mode 100644 index 00000000..dfc3b196 Binary files /dev/null and b/distribution copy.sqlite3 differ diff --git a/distribution.db b/distribution.db new file mode 100644 index 00000000..dfc3b196 Binary files /dev/null and b/distribution.db differ diff --git a/distribution_schema.sql b/distribution_schema.sql new file mode 100644 index 00000000..6c75a086 --- /dev/null +++ b/distribution_schema.sql @@ -0,0 +1,1288 @@ +-- DISCLAIMER +-- The current version of this schema only works for SQLITE >=3.45 +-- When adding new functionality, think about the following: +-- 1. Simplicity and ease of use over complexity, +-- 2. Clear, concise and strict fields but allow for extensability, +-- 3. User friendly over performance, but consider performance always. +-- WARNING: This script should only be used while testing the schema and should +-- not be applied to existing datasets since it drops all existing information. +-- +-- DESIGN NOTES +-- * Every field from every pydantic model is represented as a native column. +-- * No JSON columns are used. All arrays and nested objects are normalized +-- into dedicated tables linked by foreign keys. +-- * Physical quantities are stored as a REAL value + TEXT unit column pair. +-- * Ordered arrays (phases, tap positions, matrix rows, curve points, etc.) +-- include a position_index INTEGER to preserve ordering. +-- * Polymorphic controller types use a type_discriminator TEXT column with a +-- CHECK constraint listing the allowed subtypes. Subtype-specific fields are +-- nullable and only populated for the relevant subtype. +-- * Component tables mirror the Python pydantic model hierarchy: +-- Topology : feeders -> substations -> buses +-- Equipment : wire/cable catalogs -> branch/transformer/load equipment +-- Components: loads, branches, transformers, DERs, switchgear +-- ----------------------------------------------------------------------------- + +-- ============================================================ +-- DROP TABLES (most-dependent first) +-- ============================================================ +DROP TABLE IF EXISTS time_series_associations; +DROP TABLE IF EXISTS regulator_winding_phases; +DROP TABLE IF EXISTS regulator_winding_buses; +DROP TABLE IF EXISTS regulator_controllers; +DROP TABLE IF EXISTS distribution_regulators; +DROP TABLE IF EXISTS transformer_winding_phases; +DROP TABLE IF EXISTS transformer_winding_buses; +DROP TABLE IF EXISTS distribution_transformers; +DROP TABLE IF EXISTS switch_phase_states; +DROP TABLE IF EXISTS matrix_impedance_switch_phases; +DROP TABLE IF EXISTS matrix_impedance_switches; +DROP TABLE IF EXISTS recloser_phase_states; +DROP TABLE IF EXISTS matrix_impedance_recloser_phases; +DROP TABLE IF EXISTS matrix_impedance_reclosers; +DROP TABLE IF EXISTS recloser_reclose_intervals; +DROP TABLE IF EXISTS recloser_controllers; +DROP TABLE IF EXISTS fuse_phase_states; +DROP TABLE IF EXISTS matrix_impedance_fuse_phases; +DROP TABLE IF EXISTS matrix_impedance_fuses; +DROP TABLE IF EXISTS geometry_branch_thermal_limits; +DROP TABLE IF EXISTS geometry_branch_phases; +DROP TABLE IF EXISTS geometry_branches; +DROP TABLE IF EXISTS sequence_impedance_branch_thermal_limits; +DROP TABLE IF EXISTS sequence_impedance_branch_phases; +DROP TABLE IF EXISTS sequence_impedance_branches; +DROP TABLE IF EXISTS matrix_impedance_branch_thermal_limits; +DROP TABLE IF EXISTS matrix_impedance_branch_phases; +DROP TABLE IF EXISTS matrix_impedance_branches; +DROP TABLE IF EXISTS distribution_voltage_source_phases; +DROP TABLE IF EXISTS distribution_voltage_sources; +DROP TABLE IF EXISTS distribution_capacitor_phases; +DROP TABLE IF EXISTS capacitor_controllers; +DROP TABLE IF EXISTS distribution_capacitors; +DROP TABLE IF EXISTS distribution_battery_phases; +DROP TABLE IF EXISTS distribution_batteries; +DROP TABLE IF EXISTS distribution_solar_phases; +DROP TABLE IF EXISTS distribution_solar; +DROP TABLE IF EXISTS distribution_load_phases; +DROP TABLE IF EXISTS distribution_loads; +DROP TABLE IF EXISTS inverter_controllers; +DROP TABLE IF EXISTS inverter_active_power_controls; +DROP TABLE IF EXISTS inverter_reactive_power_controls; +DROP TABLE IF EXISTS bus_voltage_limits; +DROP TABLE IF EXISTS bus_phases; +DROP TABLE IF EXISTS distribution_buses; +DROP TABLE IF EXISTS substation_feeders; +DROP TABLE IF EXISTS distribution_substations; +DROP TABLE IF EXISTS distribution_feeders; +DROP TABLE IF EXISTS thermal_limit_sets; +DROP TABLE IF EXISTS voltage_limit_sets; +DROP TABLE IF EXISTS voltage_source_phases; +DROP TABLE IF EXISTS voltage_source_equipment; +DROP TABLE IF EXISTS phase_voltage_source_equipment; +DROP TABLE IF EXISTS inverter_equipment; +DROP TABLE IF EXISTS battery_equipment; +DROP TABLE IF EXISTS solar_equipment; +DROP TABLE IF EXISTS capacitor_equipment_phases; +DROP TABLE IF EXISTS capacitor_equipment; +DROP TABLE IF EXISTS phase_capacitor_equipment; +DROP TABLE IF EXISTS load_equipment_phases; +DROP TABLE IF EXISTS load_equipment; +DROP TABLE IF EXISTS phase_load_equipment; +DROP TABLE IF EXISTS winding_tap_positions; +DROP TABLE IF EXISTS transformer_coupling_sequences; +DROP TABLE IF EXISTS winding_equipment; +DROP TABLE IF EXISTS distribution_transformer_equipment; +DROP TABLE IF EXISTS impedance_matrix_entries; +DROP TABLE IF EXISTS matrix_impedance_switch_equipment; +DROP TABLE IF EXISTS switch_controllers; +DROP TABLE IF EXISTS recloser_controller_equipment; +DROP TABLE IF EXISTS matrix_impedance_recloser_equipment; +DROP TABLE IF EXISTS matrix_impedance_fuse_equipment; +DROP TABLE IF EXISTS matrix_impedance_branch_equipment; +DROP TABLE IF EXISTS sequence_impedance_branch_equipment; +DROP TABLE IF EXISTS geometry_branch_conductors; +DROP TABLE IF EXISTS geometry_branch_equipment; +DROP TABLE IF EXISTS concentric_cable_equipment; +DROP TABLE IF EXISTS bare_conductor_equipment; +DROP TABLE IF EXISTS time_current_curve_points; +DROP TABLE IF EXISTS time_current_curves; +DROP TABLE IF EXISTS curve_points; +DROP TABLE IF EXISTS curves; +DROP TABLE IF EXISTS wire_insulation_types; +DROP TABLE IF EXISTS line_types; +DROP TABLE IF EXISTS transformer_mountings; +DROP TABLE IF EXISTS connection_types; +DROP TABLE IF EXISTS voltage_types; +DROP TABLE IF EXISTS limit_types; +DROP TABLE IF EXISTS phases; + +PRAGMA foreign_keys = ON; + +-- ============================================================ +-- REFERENCE / ENUM TABLES +-- ============================================================ + +CREATE TABLE phases (name TEXT PRIMARY KEY); +INSERT INTO phases VALUES ('A'), ('B'), ('C'), ('N'), ('S1'), ('S2'); + +CREATE TABLE voltage_types (name TEXT PRIMARY KEY); +INSERT INTO voltage_types VALUES ('line-to-line'), ('line-to-ground'); + +CREATE TABLE connection_types (name TEXT PRIMARY KEY); +INSERT INTO connection_types VALUES + ('STAR'), ('DELTA'), ('OPEN_DELTA'), ('OPEN_STAR'), ('ZIG_ZAG'); + +CREATE TABLE limit_types (name TEXT PRIMARY KEY); +INSERT INTO limit_types VALUES ('min'), ('max'); + +CREATE TABLE transformer_mountings (name TEXT PRIMARY KEY); +INSERT INTO transformer_mountings VALUES + ('POLE_MOUNT'), ('PAD_MOUNT'), ('UNDERGROUND_VAULT'); + +CREATE TABLE line_types (name TEXT PRIMARY KEY); +INSERT INTO line_types VALUES ('OVERHEAD'), ('UNDERGROUND'); + +-- Relative permittivity values for wire insulation materials. +CREATE TABLE wire_insulation_types ( + name TEXT PRIMARY KEY, + dielectric REAL NOT NULL +); +INSERT INTO wire_insulation_types VALUES + ('AIR', 1.0), ('PVC', 3.18), ('XLPE', 2.3), ('EPR', 2.5), + ('PE', 2.25), ('TEFLON', 2.1), ('SILICONE_RUBBER', 3.5), + ('PAPER', 3.7), ('MICA', 6.0); + +-- ============================================================ +-- CURVE TABLES +-- ============================================================ + +-- Generic (x, y) curve -- used for volt-var, volt-watt, PV power-temp, +-- and inverter efficiency curves. +CREATE TABLE curves ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '' +); + +-- Ordered breakpoints for a Curve. +CREATE TABLE curve_points ( + id INTEGER PRIMARY KEY, + curve_id INTEGER NOT NULL REFERENCES curves (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + x_value REAL NOT NULL, + y_value REAL NOT NULL, + UNIQUE (curve_id, position_index) +); + +-- Time-current curve header -- used for fuse and recloser TCC protection curves. +CREATE TABLE time_current_curves ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '' +); + +-- Ordered breakpoints for a TimeCurrentCurve. +CREATE TABLE time_current_curve_points ( + id INTEGER PRIMARY KEY, + curve_id INTEGER NOT NULL REFERENCES time_current_curves (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + current_value REAL NOT NULL CHECK (current_value >= 0), + current_unit TEXT NOT NULL DEFAULT 'ampere', + time_value REAL NOT NULL CHECK (time_value >= 0), + time_unit TEXT NOT NULL DEFAULT 'second', + UNIQUE (curve_id, position_index) +); + +-- ============================================================ +-- WIRE / CABLE CATALOG +-- ============================================================ + +-- Bare conductor (overhead wire) catalog entry. +CREATE TABLE bare_conductor_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + conductor_diameter REAL NOT NULL CHECK (conductor_diameter > 0), + conductor_diameter_unit TEXT NOT NULL DEFAULT 'meter', + conductor_gmr REAL NOT NULL CHECK (conductor_gmr > 0), + conductor_gmr_unit TEXT NOT NULL DEFAULT 'meter', + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere', + emergency_ampacity REAL NOT NULL CHECK (emergency_ampacity > 0), + emergency_ampacity_unit TEXT NOT NULL DEFAULT 'ampere', + ac_resistance REAL NOT NULL CHECK (ac_resistance > 0), + ac_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + dc_resistance REAL NOT NULL CHECK (dc_resistance > 0), + dc_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter' +); + +-- Concentric (underground) cable catalog entry. +CREATE TABLE concentric_cable_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + strand_diameter REAL NOT NULL CHECK (strand_diameter > 0), + strand_diameter_unit TEXT NOT NULL DEFAULT 'meter', + conductor_diameter REAL NOT NULL CHECK (conductor_diameter > 0), + conductor_diameter_unit TEXT NOT NULL DEFAULT 'meter', + cable_diameter REAL NOT NULL CHECK (cable_diameter > 0), + cable_diameter_unit TEXT NOT NULL DEFAULT 'meter', + insulation_thickness REAL NOT NULL CHECK (insulation_thickness > 0), + insulation_thickness_unit TEXT NOT NULL DEFAULT 'meter', + insulation_diameter REAL NOT NULL CHECK (insulation_diameter > 0), + insulation_diameter_unit TEXT NOT NULL DEFAULT 'meter', + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere', + conductor_gmr REAL NOT NULL CHECK (conductor_gmr > 0), + conductor_gmr_unit TEXT NOT NULL DEFAULT 'meter', + strand_gmr REAL NOT NULL CHECK (strand_gmr > 0), + strand_gmr_unit TEXT NOT NULL DEFAULT 'meter', + phase_ac_resistance REAL NOT NULL CHECK (phase_ac_resistance > 0), + phase_ac_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + strand_ac_resistance REAL NOT NULL CHECK (strand_ac_resistance > 0), + strand_ac_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + num_neutral_strands INTEGER NOT NULL CHECK (num_neutral_strands > 0), + rated_voltage REAL NOT NULL CHECK (rated_voltage > 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + insulation TEXT NOT NULL DEFAULT 'PE' + REFERENCES wire_insulation_types (name) +); + +-- ============================================================ +-- BRANCH EQUIPMENT +-- ============================================================ + +-- Sequence impedance branch equipment. +CREATE TABLE sequence_impedance_branch_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + pos_seq_resistance REAL NOT NULL, + pos_seq_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + zero_seq_resistance REAL NOT NULL, + zero_seq_resistance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + pos_seq_reactance REAL NOT NULL, + pos_seq_reactance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + zero_seq_reactance REAL NOT NULL, + zero_seq_reactance_unit TEXT NOT NULL DEFAULT 'ohm/meter', + pos_seq_capacitance REAL NOT NULL, + pos_seq_capacitance_unit TEXT NOT NULL DEFAULT 'farad/meter', + zero_seq_capacitance REAL NOT NULL, + zero_seq_capacitance_unit TEXT NOT NULL DEFAULT 'farad/meter', + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere' +); + +-- Matrix impedance branch equipment header. +-- The N*N R/X/C matrices are stored row-by-row in impedance_matrix_entries. +CREATE TABLE matrix_impedance_branch_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + construction TEXT NOT NULL DEFAULT 'OVERHEAD' REFERENCES line_types (name), + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere' +); + +-- Shared N*N impedance matrix storage for line, fuse, recloser, and switch equipment. +-- equipment_type IN ('LINE','FUSE','RECLOSER','SWITCH') identifies the parent table. +-- matrix_type IN ('R','X','C') identifies resistance, reactance, or capacitance. +-- Cross-table FK enforcement is the responsibility of the application layer. +CREATE TABLE impedance_matrix_entries ( + id INTEGER PRIMARY KEY, + equipment_id INTEGER NOT NULL, + equipment_type TEXT NOT NULL CHECK (equipment_type IN ('LINE','FUSE','RECLOSER','SWITCH')), + matrix_type TEXT NOT NULL CHECK (matrix_type IN ('R','X','C')), + row_idx INTEGER NOT NULL CHECK (row_idx >= 0), + col_idx INTEGER NOT NULL CHECK (col_idx >= 0), + value REAL NOT NULL, + value_unit TEXT NOT NULL, + UNIQUE (equipment_id, equipment_type, matrix_type, row_idx, col_idx) +); + +-- Fuse equipment: matrix impedance + TCC curve + trip delay. +CREATE TABLE matrix_impedance_fuse_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + construction TEXT NOT NULL DEFAULT 'OVERHEAD' REFERENCES line_types (name), + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere', + delay REAL NOT NULL DEFAULT 0.0 CHECK (delay >= 0), + delay_unit TEXT NOT NULL DEFAULT 'second', + tcc_curve_id INTEGER NOT NULL REFERENCES time_current_curves (id) ON DELETE RESTRICT +); + +-- Recloser controller equipment: physical relay model identified by name. +CREATE TABLE recloser_controller_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +-- Recloser equipment: matrix impedance, protection logic is in recloser_controllers. +CREATE TABLE matrix_impedance_recloser_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + construction TEXT NOT NULL DEFAULT 'OVERHEAD' REFERENCES line_types (name), + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere' +); + +-- Switch controller: delay, normal state, and lock flag. +CREATE TABLE switch_controllers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + delay REAL NOT NULL CHECK (delay >= 0), + delay_unit TEXT NOT NULL DEFAULT 'second', + normal_state TEXT NOT NULL CHECK (normal_state IN ('open', 'close')), + is_locked INTEGER NOT NULL DEFAULT 0 CHECK (is_locked IN (0, 1)) +); + +-- Switch equipment: matrix impedance + optional switch controller. +CREATE TABLE matrix_impedance_switch_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + construction TEXT NOT NULL DEFAULT 'OVERHEAD' REFERENCES line_types (name), + ampacity REAL NOT NULL CHECK (ampacity > 0), + ampacity_unit TEXT NOT NULL DEFAULT 'ampere', + switch_controller_id INTEGER NULL + REFERENCES switch_controllers (id) ON DELETE SET NULL +); + +-- Geometry branch equipment header. +-- Conductor positions are stored in geometry_branch_conductors (one row per conductor). +CREATE TABLE geometry_branch_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + insulation TEXT NOT NULL DEFAULT 'AIR' REFERENCES wire_insulation_types (name) +); + +-- Each row is one conductor slot in a geometry branch. +-- Exactly one of bare_conductor_id / concentric_cable_id must be set. +-- horizontal_position and vertical_position replace the former JSON position arrays. +CREATE TABLE geometry_branch_conductors ( + id INTEGER PRIMARY KEY, + equipment_id INTEGER NOT NULL + REFERENCES geometry_branch_equipment (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + horizontal_position REAL NOT NULL, + horizontal_position_unit TEXT NOT NULL DEFAULT 'meter', + vertical_position REAL NOT NULL, + vertical_position_unit TEXT NOT NULL DEFAULT 'meter', + bare_conductor_id INTEGER NULL + REFERENCES bare_conductor_equipment (id) ON DELETE SET NULL, + concentric_cable_id INTEGER NULL + REFERENCES concentric_cable_equipment (id) ON DELETE SET NULL, + CHECK ((bare_conductor_id IS NOT NULL) != (concentric_cable_id IS NOT NULL)), + UNIQUE (equipment_id, position_index) +); + +-- ============================================================ +-- TRANSFORMER EQUIPMENT +-- ============================================================ + +-- Distribution transformer / regulator equipment catalog header. +CREATE TABLE distribution_transformer_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + mounting TEXT NOT NULL DEFAULT 'POLE_MOUNT' + REFERENCES transformer_mountings (name), + pct_no_load_loss REAL NOT NULL + CHECK (pct_no_load_loss >= 0 AND pct_no_load_loss <= 100), + pct_full_load_loss REAL NOT NULL + CHECK (pct_full_load_loss >= 0 AND pct_full_load_loss <= 100), + is_center_tapped INTEGER NOT NULL CHECK (is_center_tapped IN (0, 1)) +); + +-- Individual winding. Ordered tap_positions stored in winding_tap_positions. +CREATE TABLE winding_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + transformer_equipment_id INTEGER NOT NULL + REFERENCES distribution_transformer_equipment (id) ON DELETE CASCADE, + winding_index INTEGER NOT NULL CHECK (winding_index >= 0), + resistance REAL NOT NULL + CHECK (resistance >= 0 AND resistance <= 100), + is_grounded INTEGER NOT NULL CHECK (is_grounded IN (0, 1)), + rated_voltage REAL NOT NULL CHECK (rated_voltage > 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + voltage_type TEXT NOT NULL REFERENCES voltage_types (name), + rated_power REAL NOT NULL CHECK (rated_power > 0), + rated_power_unit TEXT NOT NULL DEFAULT 'volt_ampere', + num_phases INTEGER NOT NULL CHECK (num_phases >= 1 AND num_phases <= 3), + connection_type TEXT NOT NULL REFERENCES connection_types (name), + total_taps INTEGER NOT NULL DEFAULT 32, + min_tap_pu REAL NOT NULL DEFAULT 0.9 + CHECK (min_tap_pu >= 0 AND min_tap_pu <= 1.0), + max_tap_pu REAL NOT NULL DEFAULT 1.1 CHECK (max_tap_pu >= 1.0), + UNIQUE (transformer_equipment_id, winding_index) +); + +-- Per-phase tap position for a winding (replaces tap_positions JSON array). +CREATE TABLE winding_tap_positions ( + id INTEGER PRIMARY KEY, + winding_id INTEGER NOT NULL REFERENCES winding_equipment (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + tap_value REAL NOT NULL, + UNIQUE (winding_id, position_index) +); + +-- Winding-pair coupling record. +-- Merges coupling_sequences and winding_reactances JSON arrays into one table. +CREATE TABLE transformer_coupling_sequences ( + id INTEGER PRIMARY KEY, + transformer_equipment_id INTEGER NOT NULL + REFERENCES distribution_transformer_equipment (id) ON DELETE CASCADE, + sequence_index INTEGER NOT NULL CHECK (sequence_index >= 0), + from_winding_index INTEGER NOT NULL CHECK (from_winding_index >= 0), + to_winding_index INTEGER NOT NULL CHECK (to_winding_index >= 0), + reactance REAL NOT NULL CHECK (reactance >= 0 AND reactance <= 100), + reactance_unit TEXT NOT NULL DEFAULT 'percent', + UNIQUE (transformer_equipment_id, sequence_index) +); + +-- ============================================================ +-- LOAD EQUIPMENT +-- ============================================================ + +-- Single-phase load (ZIP model). +CREATE TABLE phase_load_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + real_power REAL NOT NULL DEFAULT 0.0 CHECK (real_power >= 0), + real_power_unit TEXT NOT NULL DEFAULT 'kilowatt', + reactive_power REAL NOT NULL DEFAULT 0.0, + reactive_power_unit TEXT NOT NULL DEFAULT 'kilovar', + z_real REAL NOT NULL, + z_imag REAL NOT NULL, + i_real REAL NOT NULL, + i_imag REAL NOT NULL, + p_real REAL NOT NULL, + p_imag REAL NOT NULL, + num_customers INTEGER NULL CHECK (num_customers > 0) +); + +-- Multi-phase load equipment header. +CREATE TABLE load_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + connection_type TEXT NOT NULL DEFAULT 'STAR' REFERENCES connection_types (name) +); + +-- Ordered per-phase load list (replaces phase_load_ids JSON array). +CREATE TABLE load_equipment_phases ( + id INTEGER PRIMARY KEY, + load_equipment_id INTEGER NOT NULL REFERENCES load_equipment (id) ON DELETE CASCADE, + phase_load_equipment_id INTEGER NOT NULL REFERENCES phase_load_equipment (id) ON DELETE RESTRICT, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (load_equipment_id, position_index) +); + +-- ============================================================ +-- CAPACITOR EQUIPMENT +-- ============================================================ + +-- Single-phase capacitor bank. +CREATE TABLE phase_capacitor_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + resistance REAL NOT NULL DEFAULT 0.0 CHECK (resistance >= 0), + resistance_unit TEXT NOT NULL DEFAULT 'ohm', + reactance REAL NOT NULL DEFAULT 0.0 CHECK (reactance >= 0), + reactance_unit TEXT NOT NULL DEFAULT 'ohm', + rated_reactive_power REAL NOT NULL CHECK (rated_reactive_power > 0), + rated_reactive_power_unit TEXT NOT NULL DEFAULT 'var', + num_banks_on INTEGER NOT NULL CHECK (num_banks_on >= 0), + num_banks INTEGER NOT NULL DEFAULT 1 CHECK (num_banks > 0) +); + +-- Multi-phase capacitor equipment header. +CREATE TABLE capacitor_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + connection_type TEXT NOT NULL DEFAULT 'STAR' REFERENCES connection_types (name), + rated_voltage REAL NOT NULL CHECK (rated_voltage > 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + voltage_type TEXT NOT NULL REFERENCES voltage_types (name) +); + +-- Ordered per-phase capacitor list (replaces phase_capacitor_ids JSON array). +CREATE TABLE capacitor_equipment_phases ( + id INTEGER PRIMARY KEY, + capacitor_equipment_id INTEGER NOT NULL + REFERENCES capacitor_equipment (id) ON DELETE CASCADE, + phase_capacitor_equipment_id INTEGER NOT NULL + REFERENCES phase_capacitor_equipment (id) ON DELETE RESTRICT, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (capacitor_equipment_id, position_index) +); + +-- ============================================================ +-- SOLAR / BATTERY / INVERTER EQUIPMENT +-- ============================================================ + +-- PV array equipment. +-- power_temp_curve_id replaces the former power_temp_curve JSON column. +CREATE TABLE solar_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + rated_power REAL NOT NULL CHECK (rated_power > 0), + rated_power_unit TEXT NOT NULL DEFAULT 'kilowatt', + power_temp_curve_id INTEGER NULL REFERENCES curves (id) ON DELETE SET NULL, + resistance REAL NOT NULL CHECK (resistance >= 0 AND resistance <= 100), + reactance REAL NOT NULL CHECK (reactance >= 0 AND reactance <= 100), + rated_voltage REAL NOT NULL CHECK (rated_voltage > 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + voltage_type TEXT NOT NULL REFERENCES voltage_types (name) +); + +-- Battery (DC) equipment. +CREATE TABLE battery_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + rated_energy REAL NOT NULL CHECK (rated_energy > 0), + rated_energy_unit TEXT NOT NULL DEFAULT 'kilowatt_hour', + rated_power REAL NOT NULL CHECK (rated_power >= 0), + rated_power_unit TEXT NOT NULL DEFAULT 'kilowatt', + charging_efficiency REAL NOT NULL + CHECK (charging_efficiency >= 0 AND charging_efficiency <= 100), + discharging_efficiency REAL NOT NULL + CHECK (discharging_efficiency >= 0 AND discharging_efficiency <= 100), + idling_efficiency REAL NOT NULL + CHECK (idling_efficiency >= 0 AND idling_efficiency <= 100), + rated_voltage REAL NOT NULL CHECK (rated_voltage >= 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + voltage_type TEXT NOT NULL REFERENCES voltage_types (name) +); + +-- Inverter equipment. +-- eff_curve_id replaces the former eff_curve JSON column. +CREATE TABLE inverter_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + rated_apparent_power REAL NOT NULL CHECK (rated_apparent_power > 0), + rated_apparent_power_unit TEXT NOT NULL DEFAULT 'volt_ampere', + rise_limit REAL NULL CHECK (rise_limit > 0), + rise_limit_unit TEXT NULL DEFAULT 'watt/second', + fall_limit REAL NULL CHECK (fall_limit > 0), + fall_limit_unit TEXT NULL DEFAULT 'watt/second', + cutout_percent REAL NOT NULL CHECK (cutout_percent >= 0 AND cutout_percent <= 100), + cutin_percent REAL NOT NULL CHECK (cutin_percent >= 0 AND cutin_percent <= 100), + dc_to_ac_efficiency REAL NOT NULL + CHECK (dc_to_ac_efficiency >= 0 AND dc_to_ac_efficiency <= 100), + eff_curve_id INTEGER NULL REFERENCES curves (id) ON DELETE SET NULL +); + +-- ============================================================ +-- VOLTAGE SOURCE EQUIPMENT +-- ============================================================ + +-- Single-phase Thevenin voltage source. +CREATE TABLE phase_voltage_source_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + r0 REAL NOT NULL, r0_unit TEXT NOT NULL DEFAULT 'ohm', + r1 REAL NOT NULL, r1_unit TEXT NOT NULL DEFAULT 'ohm', + x0 REAL NOT NULL, x0_unit TEXT NOT NULL DEFAULT 'ohm', + x1 REAL NOT NULL, x1_unit TEXT NOT NULL DEFAULT 'ohm', + voltage REAL NOT NULL CHECK (voltage > 0), + voltage_unit TEXT NOT NULL DEFAULT 'volt', + voltage_type TEXT NOT NULL DEFAULT 'line-to-line' REFERENCES voltage_types (name), + angle REAL NOT NULL, + angle_unit TEXT NOT NULL DEFAULT 'degree' +); + +-- Three-phase voltage source header. +CREATE TABLE voltage_source_equipment ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +-- Ordered list of per-phase sources (replaces source_ids JSON array). +CREATE TABLE voltage_source_phases ( + id INTEGER PRIMARY KEY, + voltage_source_equipment_id INTEGER NOT NULL + REFERENCES voltage_source_equipment (id) ON DELETE CASCADE, + phase_source_equipment_id INTEGER NOT NULL + REFERENCES phase_voltage_source_equipment (id) ON DELETE RESTRICT, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (voltage_source_equipment_id, position_index) +); + +-- ============================================================ +-- LIMIT SETS +-- ============================================================ + +CREATE TABLE voltage_limit_sets ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + limit_type TEXT NOT NULL REFERENCES limit_types (name), + value REAL NOT NULL CHECK (value > 0), + value_unit TEXT NOT NULL DEFAULT 'volt' +); + +CREATE TABLE thermal_limit_sets ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + limit_type TEXT NOT NULL REFERENCES limit_types (name), + value REAL NOT NULL CHECK (value > 0), + value_unit TEXT NOT NULL DEFAULT 'ampere' +); + +-- ============================================================ +-- TOPOLOGY +-- ============================================================ + +CREATE TABLE distribution_feeders ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE distribution_substations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +-- Junction: feeders belonging to a substation. +CREATE TABLE substation_feeders ( + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + PRIMARY KEY (substation_id, feeder_id) +); + +-- Bus: fundamental node of the distribution network. +-- coordinate_x / coordinate_y replace the former coordinate JSON column. +-- Phases are stored in bus_phases. +CREATE TABLE distribution_buses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + substation_id INTEGER NOT NULL + REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL + REFERENCES distribution_feeders (id) ON DELETE CASCADE, + voltage_type TEXT NOT NULL REFERENCES voltage_types (name), + rated_voltage REAL NOT NULL CHECK (rated_voltage > 0), + rated_voltage_unit TEXT NOT NULL DEFAULT 'volt', + coordinate_x REAL NULL, + coordinate_y REAL NULL, + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Ordered phase list for a bus (replaces phases JSON array). +CREATE TABLE bus_phases ( + id INTEGER PRIMARY KEY, + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (bus_id, position_index), + UNIQUE (bus_id, phase) +); + +-- Voltage limit sets associated with a bus. +CREATE TABLE bus_voltage_limits ( + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + limit_set_id INTEGER NOT NULL REFERENCES voltage_limit_sets (id) ON DELETE CASCADE, + PRIMARY KEY (bus_id, limit_set_id) +); + +-- ============================================================ +-- INVERTER CONTROLLER TABLES +-- ============================================================ + +-- Reactive power control setting for an inverter. +-- controller_type discriminates between POWER_FACTOR and VOLT_VAR subtypes. +CREATE TABLE inverter_reactive_power_controls ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + controller_type TEXT NOT NULL CHECK (controller_type IN ('POWER_FACTOR', 'VOLT_VAR')), + supported_by TEXT NOT NULL + CHECK (supported_by IN ('battery-only','solar-only','battery-and-solar')), + -- POWER_FACTOR subtype: + power_factor REAL NULL CHECK (power_factor >= -1 AND power_factor <= 1), + -- VOLT_VAR subtype: + volt_var_curve_id INTEGER NULL REFERENCES curves (id) ON DELETE SET NULL, + var_follow INTEGER NULL CHECK (var_follow IN (0, 1)) +); + +-- Active power control setting for an inverter. +-- Subtypes: VOLT_WATT, PEAK_SHAVING, CAPACITY_FIRMING, TIME_BASED, +-- SELF_CONSUMPTION, TIME_OF_USE, DEMAND_CHARGE. +CREATE TABLE inverter_active_power_controls ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + controller_type TEXT NOT NULL CHECK (controller_type IN ( + 'VOLT_WATT','PEAK_SHAVING','CAPACITY_FIRMING', + 'TIME_BASED','SELF_CONSUMPTION', + 'TIME_OF_USE','DEMAND_CHARGE')), + supported_by TEXT NOT NULL + CHECK (supported_by IN ('battery-only','solar-only','battery-and-solar')), + -- VOLT_WATT subtype: + volt_watt_curve_id INTEGER NULL REFERENCES curves (id) ON DELETE SET NULL, + -- PEAK_SHAVING subtype: + peak_shaving_target REAL NULL CHECK (peak_shaving_target >= 0), + peak_shaving_target_unit TEXT NULL DEFAULT 'watt', + base_loading_target REAL NULL CHECK (base_loading_target >= 0), + base_loading_target_unit TEXT NULL DEFAULT 'watt', + -- CAPACITY_FIRMING subtype: + max_active_power_roc REAL NULL, + max_active_power_roc_unit TEXT NULL DEFAULT 'watt/second', + min_active_power_roc REAL NULL, + min_active_power_roc_unit TEXT NULL DEFAULT 'watt/second', + -- TIME_BASED subtype (times stored as 'HH:MM:SS'): + charging_start_time TEXT NULL, + charging_end_time TEXT NULL, + discharging_start_time TEXT NULL, + discharging_end_time TEXT NULL, + charging_power REAL NULL CHECK (charging_power >= 0), + charging_power_unit TEXT NULL DEFAULT 'watt', + discharging_power REAL NULL CHECK (discharging_power >= 0), + discharging_power_unit TEXT NULL DEFAULT 'watt', + -- TIME_OF_USE / DEMAND_CHARGE subtype: + tariff_id INTEGER NULL, + CONSTRAINT check_volt_watt CHECK (controller_type != 'VOLT_WATT' OR volt_watt_curve_id IS NOT NULL), + CONSTRAINT check_peak_shave CHECK (controller_type != 'PEAK_SHAVING' OR + (peak_shaving_target IS NOT NULL AND base_loading_target IS NOT NULL)), + CONSTRAINT check_cap_firm CHECK (controller_type != 'CAPACITY_FIRMING' OR + (max_active_power_roc IS NOT NULL AND min_active_power_roc IS NOT NULL)), + CONSTRAINT check_time CHECK (controller_type != 'TIME_BASED' OR + (charging_start_time IS NOT NULL AND discharging_start_time IS NOT NULL)) +); + +-- Top-level inverter controller (used by DistributionSolar and DistributionBattery). +CREATE TABLE inverter_controllers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + prioritize_active_power INTEGER NOT NULL CHECK (prioritize_active_power IN (0, 1)), + night_mode INTEGER NOT NULL CHECK (night_mode IN (0, 1)), + active_power_control_id INTEGER NULL + REFERENCES inverter_active_power_controls (id) ON DELETE SET NULL, + reactive_power_control_id INTEGER NULL + REFERENCES inverter_reactive_power_controls (id) ON DELETE SET NULL +); + +-- ============================================================ +-- DISTRIBUTION COMPONENTS +-- ============================================================ + +-- DistributionLoad: ZIP load attached to a bus. +CREATE TABLE distribution_loads ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + load_equipment_id INTEGER NOT NULL REFERENCES load_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Ordered phase list for a DistributionLoad (replaces phases JSON array). +CREATE TABLE distribution_load_phases ( + id INTEGER PRIMARY KEY, + load_id INTEGER NOT NULL REFERENCES distribution_loads (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (load_id, position_index), + UNIQUE (load_id, phase) +); + +-- DistributionSolar: PV system attached to a bus. +-- inverter_controller_id replaces the former controller JSON column. +CREATE TABLE distribution_solar ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + irradiance REAL NOT NULL CHECK (irradiance >= 0), + irradiance_unit TEXT NOT NULL DEFAULT 'watt/meter^2', + active_power REAL NOT NULL CHECK (active_power >= 0), + active_power_unit TEXT NOT NULL DEFAULT 'watt', + reactive_power REAL NOT NULL, + reactive_power_unit TEXT NOT NULL DEFAULT 'watt', + solar_equipment_id INTEGER NOT NULL REFERENCES solar_equipment (id), + inverter_equipment_id INTEGER NOT NULL REFERENCES inverter_equipment (id), + inverter_controller_id INTEGER NULL + REFERENCES inverter_controllers (id) ON DELETE SET NULL, + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Ordered phase list for a DistributionSolar (replaces phases JSON array). +CREATE TABLE distribution_solar_phases ( + id INTEGER PRIMARY KEY, + solar_id INTEGER NOT NULL REFERENCES distribution_solar (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (solar_id, position_index), + UNIQUE (solar_id, phase) +); + +-- DistributionBattery: battery energy storage attached to a bus. +-- inverter_controller_id replaces the former controller JSON column. +CREATE TABLE distribution_batteries ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + active_power REAL NOT NULL, + active_power_unit TEXT NOT NULL DEFAULT 'watt', + reactive_power REAL NOT NULL, + reactive_power_unit TEXT NOT NULL DEFAULT 'watt', + battery_equipment_id INTEGER NOT NULL REFERENCES battery_equipment (id), + inverter_equipment_id INTEGER NOT NULL REFERENCES inverter_equipment (id), + inverter_controller_id INTEGER NULL + REFERENCES inverter_controllers (id) ON DELETE SET NULL, + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Ordered phase list for a DistributionBattery (replaces phases JSON array). +CREATE TABLE distribution_battery_phases ( + id INTEGER PRIMARY KEY, + battery_id INTEGER NOT NULL REFERENCES distribution_batteries (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (battery_id, position_index), + UNIQUE (battery_id, phase) +); + +-- DistributionCapacitor: shunt capacitor bank attached to a bus. +CREATE TABLE distribution_capacitors ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + capacitor_equipment_id INTEGER NOT NULL REFERENCES capacitor_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Per-phase controller for a DistributionCapacitor (replaces controllers JSON array). +-- controller_type discriminates the five CapacitorControllerBase subtypes. +CREATE TABLE capacitor_controllers ( + id INTEGER PRIMARY KEY, + capacitor_id INTEGER NOT NULL + REFERENCES distribution_capacitors (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + name TEXT NOT NULL DEFAULT '', + controller_type TEXT NOT NULL CHECK (controller_type IN ( + 'VOLTAGE','ACTIVE_POWER','REACTIVE_POWER','CURRENT','DAILY_TIMED')), + -- CapacitorControllerBase shared fields: + delay_on REAL NULL CHECK (delay_on >= 0), + delay_on_unit TEXT NULL DEFAULT 'second', + delay_off REAL NULL CHECK (delay_off >= 0), + delay_off_unit TEXT NULL DEFAULT 'second', + dead_time REAL NULL CHECK (dead_time >= 0), + dead_time_unit TEXT NULL DEFAULT 'second', + -- VOLTAGE subtype (VoltageCapacitorController): + on_voltage REAL NULL CHECK (on_voltage > 0), + on_voltage_unit TEXT NULL DEFAULT 'volt', + off_voltage REAL NULL CHECK (off_voltage > 0), + off_voltage_unit TEXT NULL DEFAULT 'volt', + pt_ratio REAL NULL CHECK (pt_ratio >= 0), + -- ACTIVE_POWER subtype (ActivePowerCapacitorController): + on_active_power REAL NULL CHECK (on_active_power >= 0), + on_active_power_unit TEXT NULL DEFAULT 'watt', + off_active_power REAL NULL CHECK (off_active_power > 0), + off_active_power_unit TEXT NULL DEFAULT 'watt', + -- REACTIVE_POWER subtype (ReactivePowerCapacitorController): + on_reactive_power REAL NULL CHECK (on_reactive_power > 0), + on_reactive_power_unit TEXT NULL DEFAULT 'var', + off_reactive_power REAL NULL CHECK (off_reactive_power > 0), + off_reactive_power_unit TEXT NULL DEFAULT 'var', + -- CURRENT subtype (CurrentCapacitorController): + on_current REAL NULL CHECK (on_current > 0), + on_current_unit TEXT NULL DEFAULT 'ampere', + off_current REAL NULL CHECK (off_current >= 0), + off_current_unit TEXT NULL DEFAULT 'ampere', + ct_ratio REAL NULL CHECK (ct_ratio >= 0), + -- DAILY_TIMED subtype (DailyTimedCapacitorController) -- stored as 'HH:MM:SS': + on_time TEXT NULL, + off_time TEXT NULL, + UNIQUE (capacitor_id, position_index) +); + +-- Ordered phase list for a DistributionCapacitor (replaces phases JSON array). +CREATE TABLE distribution_capacitor_phases ( + id INTEGER PRIMARY KEY, + capacitor_id INTEGER NOT NULL REFERENCES distribution_capacitors (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (capacitor_id, position_index), + UNIQUE (capacitor_id, phase) +); + +-- DistributionVoltageSource: Thevenin equivalent voltage source at a bus. +CREATE TABLE distribution_voltage_sources ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + bus_id INTEGER NOT NULL + REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL + REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL + REFERENCES distribution_feeders (id) ON DELETE CASCADE, + voltage_source_equipment_id INTEGER NOT NULL + REFERENCES voltage_source_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); + +-- Ordered phase list for a DistributionVoltageSource (replaces phases JSON array). +CREATE TABLE distribution_voltage_source_phases ( + id INTEGER PRIMARY KEY, + vsource_id INTEGER NOT NULL REFERENCES distribution_voltage_sources (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (vsource_id, position_index), + UNIQUE (vsource_id, phase) +); + +-- ============================================================ +-- BRANCH COMPONENTS -- Lines +-- ============================================================ +-- from_bus_id / to_bus_id are the explicit FK columns; no buses JSON needed. + +-- MatrixImpedanceBranch: full matrix line model. +CREATE TABLE matrix_impedance_branches ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES matrix_impedance_branch_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered phase list for a MatrixImpedanceBranch (replaces phases JSON array). +CREATE TABLE matrix_impedance_branch_phases ( + id INTEGER PRIMARY KEY, + branch_id INTEGER NOT NULL REFERENCES matrix_impedance_branches (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (branch_id, position_index), + UNIQUE (branch_id, phase) +); +-- Thermal limits junction (replaces thermal_limit_ids JSON array). +CREATE TABLE matrix_impedance_branch_thermal_limits ( + branch_id INTEGER NOT NULL + REFERENCES matrix_impedance_branches (id) ON DELETE CASCADE, + thermal_limit_set_id INTEGER NOT NULL + REFERENCES thermal_limit_sets (id) ON DELETE CASCADE, + PRIMARY KEY (branch_id, thermal_limit_set_id) +); + +-- SequenceImpedanceBranch: positive/zero sequence line model. +CREATE TABLE sequence_impedance_branches ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES sequence_impedance_branch_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +CREATE TABLE sequence_impedance_branch_phases ( + id INTEGER PRIMARY KEY, + branch_id INTEGER NOT NULL REFERENCES sequence_impedance_branches (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (branch_id, position_index), + UNIQUE (branch_id, phase) +); +CREATE TABLE sequence_impedance_branch_thermal_limits ( + branch_id INTEGER NOT NULL + REFERENCES sequence_impedance_branches (id) ON DELETE CASCADE, + thermal_limit_set_id INTEGER NOT NULL + REFERENCES thermal_limit_sets (id) ON DELETE CASCADE, + PRIMARY KEY (branch_id, thermal_limit_set_id) +); + +-- GeometryBranch: impedance computed from conductor geometry. +CREATE TABLE geometry_branches ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES geometry_branch_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +CREATE TABLE geometry_branch_phases ( + id INTEGER PRIMARY KEY, + branch_id INTEGER NOT NULL REFERENCES geometry_branches (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (branch_id, position_index), + UNIQUE (branch_id, phase) +); +CREATE TABLE geometry_branch_thermal_limits ( + branch_id INTEGER NOT NULL REFERENCES geometry_branches (id) ON DELETE CASCADE, + thermal_limit_set_id INTEGER NOT NULL REFERENCES thermal_limit_sets (id) ON DELETE CASCADE, + PRIMARY KEY (branch_id, thermal_limit_set_id) +); + +-- ============================================================ +-- BRANCH COMPONENTS -- Protective / Switching Devices +-- ============================================================ + +-- MatrixImpedanceFuse. +CREATE TABLE matrix_impedance_fuses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES matrix_impedance_fuse_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered phase list for a fuse (replaces phases JSON array). +CREATE TABLE matrix_impedance_fuse_phases ( + id INTEGER PRIMARY KEY, + fuse_id INTEGER NOT NULL REFERENCES matrix_impedance_fuses (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (fuse_id, position_index), + UNIQUE (fuse_id, phase) +); +-- Per-phase closed/open state for a fuse (replaces is_closed JSON array). +CREATE TABLE fuse_phase_states ( + id INTEGER PRIMARY KEY, + fuse_id INTEGER NOT NULL REFERENCES matrix_impedance_fuses (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + phase TEXT NOT NULL REFERENCES phases (name), + is_closed INTEGER NOT NULL CHECK (is_closed IN (0, 1)), + UNIQUE (fuse_id, position_index) +); + +-- Recloser controller tables. +-- ground_delayed/fast, phase_delayed/fast reference time_current_curves. +CREATE TABLE recloser_controllers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + delay REAL NOT NULL CHECK (delay >= 0), + delay_unit TEXT NOT NULL DEFAULT 'second', + ground_delayed_curve_id INTEGER NOT NULL + REFERENCES time_current_curves (id) ON DELETE RESTRICT, + ground_fast_curve_id INTEGER NOT NULL + REFERENCES time_current_curves (id) ON DELETE RESTRICT, + phase_delayed_curve_id INTEGER NOT NULL + REFERENCES time_current_curves (id) ON DELETE RESTRICT, + phase_fast_curve_id INTEGER NOT NULL + REFERENCES time_current_curves (id) ON DELETE RESTRICT, + num_fast_ops INTEGER NOT NULL CHECK (num_fast_ops >= 0), + num_shots INTEGER NOT NULL CHECK (num_shots >= 1), + reset_time REAL NOT NULL CHECK (reset_time >= 0), + reset_time_unit TEXT NOT NULL DEFAULT 'second', + equipment_id INTEGER NOT NULL + REFERENCES recloser_controller_equipment (id) ON DELETE RESTRICT +); +-- Reclose intervals ordered array (replaces reclose_intervals Time[] array). +CREATE TABLE recloser_reclose_intervals ( + id INTEGER PRIMARY KEY, + recloser_controller_id INTEGER NOT NULL + REFERENCES recloser_controllers (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + interval_value REAL NOT NULL CHECK (interval_value >= 0), + interval_unit TEXT NOT NULL DEFAULT 'second', + UNIQUE (recloser_controller_id, position_index) +); + +-- MatrixImpedanceRecloser. +-- controller_id replaces the former controller JSON column. +CREATE TABLE matrix_impedance_reclosers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES matrix_impedance_recloser_equipment (id), + controller_id INTEGER NOT NULL REFERENCES recloser_controllers (id) ON DELETE RESTRICT, + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered phase list for a recloser (replaces phases JSON array). +CREATE TABLE matrix_impedance_recloser_phases ( + id INTEGER PRIMARY KEY, + recloser_id INTEGER NOT NULL REFERENCES matrix_impedance_reclosers (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (recloser_id, position_index), + UNIQUE (recloser_id, phase) +); +-- Per-phase closed/open state for a recloser (replaces is_closed JSON array). +CREATE TABLE recloser_phase_states ( + id INTEGER PRIMARY KEY, + recloser_id INTEGER NOT NULL REFERENCES matrix_impedance_reclosers (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + phase TEXT NOT NULL REFERENCES phases (name), + is_closed INTEGER NOT NULL CHECK (is_closed IN (0, 1)), + UNIQUE (recloser_id, position_index) +); + +-- MatrixImpedanceSwitch. +CREATE TABLE matrix_impedance_switches ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + from_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + to_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + substation_id INTEGER NOT NULL REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL REFERENCES distribution_feeders (id) ON DELETE CASCADE, + length REAL NOT NULL CHECK (length > 0), + length_unit TEXT NOT NULL DEFAULT 'meter', + equipment_id INTEGER NOT NULL REFERENCES matrix_impedance_switch_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered phase list for a switch (replaces phases JSON array). +CREATE TABLE matrix_impedance_switch_phases ( + id INTEGER PRIMARY KEY, + switch_id INTEGER NOT NULL REFERENCES matrix_impedance_switches (id) ON DELETE CASCADE, + phase TEXT NOT NULL REFERENCES phases (name), + position_index INTEGER NOT NULL CHECK (position_index >= 0), + UNIQUE (switch_id, position_index), + UNIQUE (switch_id, phase) +); +-- Per-phase closed/open state for a switch (replaces is_closed JSON array). +CREATE TABLE switch_phase_states ( + id INTEGER PRIMARY KEY, + switch_id INTEGER NOT NULL REFERENCES matrix_impedance_switches (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + phase TEXT NOT NULL REFERENCES phases (name), + is_closed INTEGER NOT NULL CHECK (is_closed IN (0, 1)), + UNIQUE (switch_id, position_index) +); + +-- ============================================================ +-- TRANSFORMER / REGULATOR COMPONENTS +-- ============================================================ + +-- DistributionTransformer. +-- bus_ids JSON replaced by transformer_winding_buses. +-- winding_phases JSON replaced by transformer_winding_phases. +CREATE TABLE distribution_transformers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + substation_id INTEGER NOT NULL + REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL + REFERENCES distribution_feeders (id) ON DELETE CASCADE, + equipment_id INTEGER NOT NULL + REFERENCES distribution_transformer_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered winding-to-bus mapping (replaces bus_ids JSON array). +CREATE TABLE transformer_winding_buses ( + id INTEGER PRIMARY KEY, + transformer_id INTEGER NOT NULL REFERENCES distribution_transformers (id) ON DELETE CASCADE, + winding_index INTEGER NOT NULL CHECK (winding_index >= 0), + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + UNIQUE (transformer_id, winding_index) +); +-- Ordered per-phase list for each winding (replaces winding_phases JSON array of arrays). +CREATE TABLE transformer_winding_phases ( + id INTEGER PRIMARY KEY, + transformer_id INTEGER NOT NULL REFERENCES distribution_transformers (id) ON DELETE CASCADE, + winding_index INTEGER NOT NULL CHECK (winding_index >= 0), + phase TEXT NOT NULL REFERENCES phases (name), + phase_index INTEGER NOT NULL CHECK (phase_index >= 0), + UNIQUE (transformer_id, winding_index, phase_index), + UNIQUE (transformer_id, winding_index, phase) +); + +-- DistributionRegulator. +-- bus_ids JSON replaced by regulator_winding_buses. +-- winding_phases JSON replaced by regulator_winding_phases. +-- controllers JSON replaced by regulator_controllers. +CREATE TABLE distribution_regulators ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + substation_id INTEGER NOT NULL + REFERENCES distribution_substations (id) ON DELETE CASCADE, + feeder_id INTEGER NOT NULL + REFERENCES distribution_feeders (id) ON DELETE CASCADE, + equipment_id INTEGER NOT NULL + REFERENCES distribution_transformer_equipment (id), + in_service INTEGER NOT NULL DEFAULT 1 CHECK (in_service IN (0, 1)) +); +-- Ordered winding-to-bus mapping for a regulator. +CREATE TABLE regulator_winding_buses ( + id INTEGER PRIMARY KEY, + regulator_id INTEGER NOT NULL REFERENCES distribution_regulators (id) ON DELETE CASCADE, + winding_index INTEGER NOT NULL CHECK (winding_index >= 0), + bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE CASCADE, + UNIQUE (regulator_id, winding_index) +); +-- Ordered per-phase list for each winding of a regulator. +CREATE TABLE regulator_winding_phases ( + id INTEGER PRIMARY KEY, + regulator_id INTEGER NOT NULL REFERENCES distribution_regulators (id) ON DELETE CASCADE, + winding_index INTEGER NOT NULL CHECK (winding_index >= 0), + phase TEXT NOT NULL REFERENCES phases (name), + phase_index INTEGER NOT NULL CHECK (phase_index >= 0), + UNIQUE (regulator_id, winding_index, phase_index), + UNIQUE (regulator_id, winding_index, phase) +); +-- Per-phase regulator controller (replaces controllers JSON array). +-- controlled_bus_id and controlled_phase replace the embedded DistributionBus object. +CREATE TABLE regulator_controllers ( + id INTEGER PRIMARY KEY, + regulator_id INTEGER NOT NULL REFERENCES distribution_regulators (id) ON DELETE CASCADE, + position_index INTEGER NOT NULL CHECK (position_index >= 0), + name TEXT NOT NULL DEFAULT '', + delay REAL NULL CHECK (delay >= 0), + delay_unit TEXT NULL DEFAULT 'second', + v_setpoint REAL NOT NULL CHECK (v_setpoint > 0), + v_setpoint_unit TEXT NOT NULL DEFAULT 'volt', + min_v_limit REAL NOT NULL CHECK (min_v_limit > 0), + min_v_limit_unit TEXT NOT NULL DEFAULT 'volt', + max_v_limit REAL NOT NULL CHECK (max_v_limit > 0), + max_v_limit_unit TEXT NOT NULL DEFAULT 'volt', + pt_ratio REAL NOT NULL CHECK (pt_ratio >= 0), + use_ldc INTEGER NOT NULL CHECK (use_ldc IN (0, 1)), + is_reversible INTEGER NOT NULL CHECK (is_reversible IN (0, 1)), + ldc_R REAL NULL CHECK (ldc_R >= 0), + ldc_R_unit TEXT NULL DEFAULT 'volt', + ldc_X REAL NULL CHECK (ldc_X >= 0), + ldc_X_unit TEXT NULL DEFAULT 'volt', + ct_primary REAL NULL CHECK (ct_primary >= 0), + ct_primary_unit TEXT NULL DEFAULT 'ampere', + max_step INTEGER NOT NULL CHECK (max_step >= 0), + bandwidth REAL NOT NULL CHECK (bandwidth >= 0), + bandwidth_unit TEXT NOT NULL DEFAULT 'volt', + controlled_bus_id INTEGER NOT NULL REFERENCES distribution_buses (id) ON DELETE RESTRICT, + controlled_phase TEXT NOT NULL REFERENCES phases (name), + UNIQUE (regulator_id, position_index) +); + +-- ============================================================ +-- TIME SERIES ASSOCIATIONS +-- ============================================================ +CREATE TABLE time_series_associations ( + id INTEGER PRIMARY KEY, + time_series_uuid TEXT NOT NULL, + time_series_type TEXT NOT NULL, + initial_timestamp TEXT NOT NULL, + resolution TEXT NOT NULL, + horizon TEXT NULL, + "interval" TEXT NULL, + window_count INTEGER NULL, + length INTEGER NULL, + name TEXT NOT NULL, + owner_id INTEGER NOT NULL, + owner_type TEXT NOT NULL, + owner_category TEXT NOT NULL, + features TEXT NOT NULL, + scaling_factor_multiplier TEXT NULL, + metadata_uuid TEXT NOT NULL, + units TEXT NULL +); + +CREATE UNIQUE INDEX dist_ts_by_owner_name_res_feat + ON time_series_associations (owner_id, time_series_type, name, resolution, features); + +CREATE INDEX dist_ts_by_uuid + ON time_series_associations (time_series_uuid); diff --git a/docs/cli/reduce.md b/docs/cli/reduce.md index e97ea953..07ad2b0f 100644 --- a/docs/cli/reduce.md +++ b/docs/cli/reduce.md @@ -2,24 +2,27 @@ ## Network Reduction -You can use `reduce` command to perform network reduction is GDM system. +You can use the `reduce` command to perform network reduction on a GDM system. ```bash -Usage: gdm reduce [OPTIONS] +Usage: gdm [OPTIONS] + + Reduce a GDM distribution system. Options: - -g, --gdm-file TEXT GDM system JSON file path. - -t, --target-file TEXT Target GDM system JSON file path. + -g, --gdm-file TEXT GDM system JSON file path. [required] + -t, --target-file TEXT Target GDM system JSON file path. [required] -f, --force Force delete the target GDM system file if already exists. - -r, --reducer [three_phase] Delete target GDM file forcefully if exists. - -ts, --timeseries Delete target GDM file forcefully if exists. + -r, --reducer [three_phase] Reducer type to apply. [default: three_phase] + -ts, --timeseries Include timeseries data in the reduced system. + --install-completion Install completion for the current shell. + --show-completion Show completion for the current shell. --help Show this message and exit. - ``` -Following command converts test.json file to test_reduced.json file including timeseries data. +Following command converts `test.json` to `test_reduced.json` including timeseries data. ```bash -gdm redcue -g 'test.json' -t 'test_reduced.json' -r "three_phase" -ts +gdm reduce -g 'test.json' -t 'test_reduced.json' -r "three_phase" -ts ``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 919ff1dd..1ad6f04f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "pydantic", "infrasys~=1.0", "importlib_metadata", - "click~=8.1.7", + "typer", "pandas~=2.2.3", "geopandas", "plotly", @@ -44,7 +44,7 @@ doc = [ ] [project.scripts] -gdm = "gdm.cli.cli:cli" +gdm = "gdm.cli.cli:app" gdm-mcp-server = "gdm.mcp.server:main" [project.urls] diff --git a/schema.sql b/schema.sql new file mode 100644 index 00000000..43309750 --- /dev/null +++ b/schema.sql @@ -0,0 +1,367 @@ +-- DISCLAIMER +-- The current version of this schema only works for SQLITE >=3.45 +-- When adding new functionality, think about the following: +-- 1. Simplicity and ease of use over complexity, +-- 2. Clear, consice and strict fields but allow for extensability, +-- 3. User friendly over peformance, but consider performance always, +-- WARNING: This script should only be used while testing the schema and should not +-- be applied to existing dataset since it drops all the information it has. +DROP TABLE IF EXISTS thermal_generators; + +DROP TABLE IF EXISTS renewable_generators; + +DROP TABLE IF EXISTS hydro_generators; + +DROP TABLE IF EXISTS storage_units; + +DROP TABLE IF EXISTS prime_mover_types; + +DROP TABLE IF EXISTS balancing_topologies; + +DROP TABLE IF EXISTS supply_technologies; + +DROP TABLE IF EXISTS storage_technologies; + +DROP TABLE IF EXISTS transmission_lines; + +DROP TABLE IF EXISTS planning_regions; + +DROP TABLE IF EXISTS transmission_interchanges; + +DROP TABLE IF EXISTS entities; + +DROP TABLE IF EXISTS time_series_associations; + +DROP TABLE IF EXISTS attributes; + +DROP TABLE IF EXISTS loads; + +DROP TABLE IF EXISTS static_time_series; + +DROP TABLE IF EXISTS entity_types; + +DROP TABLE IF EXISTS supplemental_attributes; + +DROP TABLE IF EXISTS attributes_associations; + +DROP TABLE IF EXISTS arcs; + +DROP TABLE IF EXISTS hydro_reservoir; + +DROP TABLE IF EXISTS hydro_reservoir_connections; + +DROP TABLE IF EXISTS fuels; + +DROP TABLE IF EXISTS supplemental_attributes_association; + +DROP TABLE IF EXISTS transport_technologies; + +PRAGMA foreign_keys = ON; + +-- NOTE: This table should not be interacted directly since it gets populated +-- automatically. +-- Table of certain entities of griddb schema. +CREATE TABLE entities ( + id integer PRIMARY KEY, + entity_table text NOT NULL, + entity_type text NOT NULL, + FOREIGN KEY (entity_type) REFERENCES entity_types (name) +); + +-- Table of possible entity types +CREATE TABLE entity_types (name text PRIMARY KEY); + +-- NOTE: Sienna-griddb follows the convention of the EIA prime mover where we +-- have a `prime_mover` and `fuel` to classify generators/storage units. +-- However, users could use any combination of `prime_mover` and `fuel` for +-- their own application. The only constraint is that the uniqueness is enforced +-- by the combination of (prime_mover, fuel) +-- Categories to classify generating units and supply technologies +CREATE TABLE prime_mover_types ( + id integer PRIMARY KEY, + name text NOT NULL UNIQUE, + description text NULL +); + +CREATE TABLE fuels( + id integer PRIMARY KEY, + name text NOT NULL UNIQUE, + description text NULL +); + +-- Investment regions +CREATE TABLE planning_regions ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name text NOT NULL UNIQUE, + description text NULL +); + +-- Balancing topologies for the system. Could be either buses, or larger +-- aggregated regions. +CREATE TABLE balancing_topologies ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name text NOT NULL UNIQUE, + area integer NULL REFERENCES planning_regions (id) ON DELETE SET NULL, + description text NULL +); + +-- NOTE: The purpose of this table is to provide links different entities that +-- naturally have a relantionship not model dependent (e.g., transmission lines, +-- transmission interchanges, etc.). +-- Physical connection between entities. +CREATE TABLE arcs ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + from_id integer, + to_id integer, + FOREIGN KEY (from_id) REFERENCES entities (id) ON DELETE CASCADE, + FOREIGN KEY (to_id) REFERENCES entities (id) ON DELETE CASCADE +); + +-- Existing transmission lines +CREATE TABLE transmission_lines ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name text NOT NULL UNIQUE, + arc_id integer, + continuous_rating real NULL CHECK (continuous_rating >= 0), + ste_rating real NULL CHECK (ste_rating >= 0), + lte_rating real NULL CHECK (lte_rating >= 0), + line_length real NULL CHECK (line_length >= 0), + FOREIGN KEY (arc_id) REFERENCES arcs (id) ON DELETE CASCADE +) strict; + +-- NOTE: The purpose of this table is to provide physical limits to flows +-- between areas or balancing topologies. In contrast with the transmission +-- lines, this entities are used to enforce given physical limits of certain +-- markets. +-- Transmission interchanges between two balancing topologies or areas +CREATE TABLE transmission_interchanges ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name text NOT NULL UNIQUE, + arc_id int REFERENCES arcs(id) ON DELETE CASCADE, + max_flow_from real NOT NULL, + max_flow_to real NOT NULL +) strict; + +-- NOTE: The purpose of these tables is to capture data of **existing units only**. +-- Table of thermal generation units (ThermalStandard, ThermalMultiStart) +CREATE TABLE thermal_generators ( + id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name TEXT NOT NULL UNIQUE, + 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, + rating REAL NOT NULL CHECK (rating >= 0), + base_power REAL NOT NULL CHECK (base_power > 0), + -- Power limits (JSON: {"min": ..., "max": ...}): + active_power_limits JSON NOT NULL, + reactive_power_limits JSON NULL, + -- Ramp limits (JSON: {"up": ..., "down": ...}, MW/min): + ramp_limits JSON NULL, + -- Time limits (JSON: {"up": ..., "down": ...}, hours): + time_limits JSON NULL, + -- Operational flags: + must_run BOOLEAN NOT NULL DEFAULT FALSE, + available BOOLEAN NOT NULL DEFAULT TRUE, + STATUS BOOLEAN NOT NULL DEFAULT FALSE, + -- Initial setpoints: + active_power REAL NOT NULL DEFAULT 0.0, + reactive_power REAL NOT NULL DEFAULT 0.0, + -- Cost (complex structure, stored as JSON): + operation_cost JSON NOT NULL DEFAULT '{"cost_type": "THERMAL", "fixed": 0, "shut_down": 0, "start_up": 0, "variable": {"variable_cost_type": "COST", "power_units": "NATURAL_UNITS", "value_curve": {"curve_type": "INPUT_OUTPUT", "function_data": {"function_type": "LINEAR", "proportional_term": 0, "constant_term": 0}}, "vom_cost": {"curve_type": "INPUT_OUTPUT", "function_data": {"function_type": "LINEAR", "proportional_term": 0, "constant_term": 0}}}}' +); + +-- Table of renewable generation units (RenewableDispatch, RenewableNonDispatch) +CREATE TABLE renewable_generators ( + id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + 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, + rating REAL NOT NULL CHECK (rating >= 0), + base_power REAL NOT NULL CHECK (base_power > 0), + -- Renewable-specific: + power_factor REAL NOT NULL DEFAULT 1.0 CHECK ( + power_factor > 0 + AND power_factor <= 1.0 + ), + -- Power limits (JSON: {"min": ..., "max": ...}): + reactive_power_limits JSON NULL, + -- Operational flags: + available BOOLEAN NOT NULL DEFAULT TRUE, + -- Initial setpoints: + active_power REAL NOT NULL DEFAULT 0.0, + reactive_power REAL NOT NULL DEFAULT 0.0, + -- Cost (NULL for RenewableNonDispatch): + operation_cost JSON NULL DEFAULT '{"cost_type":"RENEWABLE","fixed":0,"variable":{"variable_cost_type":"COST","power_units":"NATURAL_UNITS","value_curve":{"curve_type":"INPUT_OUTPUT","function_data":{"function_type":"LINEAR","proportional_term":0,"constant_term":0}},"vom_cost":{"curve_type":"INPUT_OUTPUT","function_data":{"function_type":"LINEAR","proportional_term":0,"constant_term":0}}},"curtailment_cost":{"variable_cost_type":"COST","power_units":"NATURAL_UNITS","value_curve":{"curve_type":"INPUT_OUTPUT","function_data":{"function_type":"LINEAR","proportional_term":0,"constant_term":0}},"vom_cost":{"curve_type":"INPUT_OUTPUT","function_data":{"function_type":"LINEAR","proportional_term":0,"constant_term":0}}}}' +); + +-- Table of hydro generation units (HydroDispatch, HydroTurbine, HydroPumpTurbine) +CREATE TABLE hydro_generators ( + id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + 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, + rating REAL NOT NULL CHECK (rating >= 0), + base_power REAL NOT NULL CHECK (base_power > 0), + -- Power limits (JSON: {"min": ..., "max": ...}): + active_power_limits JSON NOT NULL, + reactive_power_limits JSON NULL, + -- Ramp limits (JSON: {"up": ..., "down": ...}, MW/min): + ramp_limits JSON NULL, + -- Time limits (JSON: {"up": ..., "down": ...}, hours): + time_limits JSON NULL, + -- Operational flags: + available BOOLEAN NOT NULL DEFAULT TRUE, + -- Initial setpoints: + active_power REAL NOT NULL DEFAULT 0.0, + reactive_power REAL NOT NULL DEFAULT 0.0, + -- HydroTurbine/HydroPumpTurbine fields (nullable for HydroDispatch): + powerhouse_elevation REAL NULL DEFAULT 0.0 CHECK (powerhouse_elevation >= 0), + -- Outflow limits (JSON: {"min": ..., "max": ...}): + outflow_limits JSON NULL, + conversion_factor REAL NULL DEFAULT 1.0 CHECK (conversion_factor > 0), + travel_time REAL NULL CHECK (travel_time >= 0), + -- Cost: + operation_cost JSON NOT NULL DEFAULT '{"cost_type": "HYDRO_GEN", "fixed": 0.0, "variable": {"variable_cost_type": "COST", "power_units": "NATURAL_UNITS", "value_curve": {"curve_type": "INPUT_OUTPUT", "function_data": {"function_type": "LINEAR", "proportional_term": 0, "constant_term": 0}}, "vom_cost": {"curve_type": "INPUT_OUTPUT", "function_data": {"function_type": "LINEAR", "proportional_term": 0, "constant_term": 0}}}}' -- Note: efficiency (varies by type), turbine_type, and HydroPumpTurbine-specific + -- fields (active_power_limits_pump, etc.) are stored in the attributes table +); + +-- NOTE: The purpose of this table is to capture data of **existing storage units only**. +-- Table of energy storage units (including PHES or other kinds), +CREATE TABLE storage_units ( + id INTEGER PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name TEXT NOT NULL UNIQUE, + prime_mover_type TEXT NOT NULL REFERENCES prime_mover_types(name), + storage_technology_type TEXT NOT NULL, + balancing_topology INTEGER NOT NULL REFERENCES balancing_topologies (id) ON DELETE CASCADE, + rating REAL NOT NULL CHECK (rating >= 0), + base_power REAL NOT NULL CHECK (base_power > 0), + -- Storage capacity and limits (JSON: {"min": ..., "max": ...}): + storage_capacity REAL NOT NULL CHECK (storage_capacity >= 0), + storage_level_limits JSON NOT NULL, + initial_storage_capacity_level REAL NOT NULL CHECK (initial_storage_capacity_level >= 0), + -- Power limits (JSON: {"min": ..., "max": ...}, input = charging, output = discharging): + input_active_power_limits JSON NOT NULL, + output_active_power_limits JSON NOT NULL, + -- Efficiency (JSON: {"in": ..., "out": ...}): + efficiency JSON NOT NULL, + -- Reactive power (JSON: {"min": ..., "max": ...}): + reactive_power_limits JSON NULL, + -- Initial setpoints: + active_power REAL NOT NULL DEFAULT 0.0, + reactive_power REAL NOT NULL DEFAULT 0.0, + -- Status: + available BOOLEAN NOT NULL DEFAULT TRUE, + -- Storage-specific with defaults: + conversion_factor REAL NOT NULL DEFAULT 1.0 CHECK (conversion_factor > 0), + storage_target REAL NOT NULL DEFAULT 0.0, + cycle_limits INTEGER NOT NULL DEFAULT 10000 CHECK (cycle_limits > 0), + -- Cost: + operation_cost JSON NULL +); + +-- Topological hydro reservoirs +CREATE TABLE hydro_reservoir(name text NOT NULL, UNIQUE(name)); + +CREATE TABLE hydro_reservoir_connections( + source_id integer NOT NULL REFERENCES entities(id) ON DELETE CASCADE, + sink_id integer NOT NULL REFERENCES entities(id) ON DELETE CASCADE, + CHECK (source_id <> sink_id), + PRIMARY KEY (source_id, sink_id) +); +-- investment for expansion problems. +-- Investment technology options for expansion problems +CREATE TABLE supply_technologies ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + prime_mover_type text NOT NULL REFERENCES prime_mover_types(name), + fuel text NULL REFERENCES fuels(name), + area text NULL REFERENCES planning_regions (name) ON DELETE SET NULL, + balancing_topology text NULL REFERENCES balancing_topologies (name) ON DELETE SET NULL, + scenario text NULL, + UNIQUE(prime_mover_type, fuel, scenario) +); + +CREATE TABLE transport_technologies( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + arc_id integer NULL REFERENCES arcs(id) ON DELETE SET NULL, + scenario text NULL, + UNIQUE(id, arc_id, scenario) +); + +-- NOTE: Attributes are additional parameters that can be linked to entities. +-- The main purpose of this is when there is an important field that is not +-- capture on the entity table that should exist on the model. Example of this +-- fields are variable or fixed operation and maintenance cost or any other +-- field that its representation is hard to fit into a `integer`, `real` or +-- `text`. It must not be used for operational details since most of the should +-- be included in the `operational_data` table. +CREATE TABLE attributes ( + id integer PRIMARY KEY, + entity_id integer NOT NULL, + TYPE text NOT NULL, + name text NOT NULL, + value json NOT NULL, + json_type text generated always AS (json_type(value)) virtual, + FOREIGN KEY (entity_id) REFERENCES entities (id) ON DELETE CASCADE +); + +-- NOTE: Supplemental are optional parameters that can be linked to entities. +-- The main purpose of this is to provide a way to save relevant information +-- but that could or could not be used for modeling. not `text`. Examples of +-- this field are geolocation (e.g., lat, long), outages, etc.) +CREATE TABLE supplemental_attributes ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + TYPE text NOT NULL, + value json NOT NULL, + json_type text generated always AS (json_type (value)) virtual +); + +CREATE TABLE supplemental_attributes_association ( + attribute_id integer NOT NULL, + entity_id integer NOT NULL, + FOREIGN KEY (entity_id) REFERENCES entities (id) ON DELETE CASCADE, + FOREIGN KEY (attribute_id) REFERENCES supplemental_attributes (id) ON DELETE CASCADE +) strict; + +CREATE TABLE time_series_associations( + id INTEGER PRIMARY KEY, + time_series_uuid TEXT NOT NULL, + time_series_type TEXT NOT NULL, + initial_timestamp TEXT NOT NULL, + resolution TEXT NOT NULL, + horizon TEXT, + "interval" TEXT, + window_count INTEGER, + length INTEGER, + name TEXT NOT NULL, + owner_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE, + owner_type TEXT NOT NULL, + owner_category TEXT NOT NULL, + features TEXT NOT NULL, + scaling_factor_multiplier TEXT NULL, + metadata_uuid TEXT NOT NULL, + units TEXT NULL +); +CREATE UNIQUE INDEX "by_c_n_tst_features" ON "time_series_associations" ( + "owner_id", + "time_series_type", + "name", + "resolution", + "features" +); +CREATE INDEX "by_ts_uuid" ON "time_series_associations" ("time_series_uuid"); + + +CREATE TABLE loads ( + id integer PRIMARY KEY REFERENCES entities (id) ON DELETE CASCADE, + name TEXT NOT NULL UNIQUE, + balancing_topology INTEGER NOT NULL, + base_power DOUBLE, + FOREIGN KEY(balancing_topology) REFERENCES balancing_topologies (id) ON DELETE CASCADE +); + +CREATE TABLE static_time_series ( + id integer PRIMARY KEY, + uuid text NULL, + idx integer NOT NULL, + value real NOT NULL + \ No newline at end of file diff --git a/src/gdm/cli/cli.py b/src/gdm/cli/cli.py index eaa7ec11..74626e03 100644 --- a/src/gdm/cli/cli.py +++ b/src/gdm/cli/cli.py @@ -1,10 +1,5 @@ -import click +import typer from gdm.cli.reducer import reduce - -@click.group() -def cli(): - pass - - -cli.add_command(reduce) +app = typer.Typer() +app.command()(reduce) diff --git a/src/gdm/cli/reducer.py b/src/gdm/cli/reducer.py index c2e3e121..b57efb30 100644 --- a/src/gdm/cli/reducer.py +++ b/src/gdm/cli/reducer.py @@ -1,59 +1,51 @@ +from enum import Enum from pathlib import Path import shutil -import click +from typing import Annotated -from gdm.distribution.network.reducer import reduce_to_three_phase_system +import typer + +from gdm.distribution.model_reduction.reducer import reduce_to_three_phase_system from gdm.distribution.distribution_system import DistributionSystem from gdm.exceptions import FolderAlreadyExistsError -@click.command() -@click.option( - "-g", - "--gdm-file", - type=str, - help="GDM system JSON file path.", -) -@click.option( - "-t", - "--target-file", - type=str, - help="Target GDM system JSON file path.", -) -@click.option( - "-f", - "--force", - is_flag=True, - default=False, - help="Force delete the target GDM system file if already exists.", -) -@click.option( - "-r", - "--reducer", - type=click.Choice(["three_phase"]), - help="Delete target GDM file forcefully if exists.", -) -@click.option( - "-ts", - "--timeseries", - is_flag=True, - default=False, - help="Delete target GDM file forcefully if exists.", -) -def reduce(gdm_file: str, target_file: str, force: bool, reducer: str, timeseries: bool): - target_file: Path = Path(target_file) - if force and target_file.exists(): - shutil.rmtree(target_file.parent / f"{target_file.stem}_time_series") - target_file.unlink() +class ReducerType(str, Enum): + three_phase = "three_phase" + + +def reduce( + gdm_file: Annotated[str, typer.Option("-g", "--gdm-file", help="GDM system JSON file path.")], + target_file: Annotated[ + str, typer.Option("-t", "--target-file", help="Target GDM system JSON file path.") + ], + force: Annotated[ + bool, + typer.Option( + "-f", "--force", help="Force delete the target GDM system file if already exists." + ), + ] = False, + reducer: Annotated[ + ReducerType, typer.Option("-r", "--reducer", help="Reducer type to apply.") + ] = ReducerType.three_phase, + timeseries: Annotated[ + bool, + typer.Option("-ts", "--timeseries", help="Include timeseries data in the reduced system."), + ] = False, +): + """Reduce a GDM distribution system.""" + target_path = Path(target_file) + if force and target_path.exists(): + shutil.rmtree(target_path.parent / f"{target_path.stem}_time_series") + target_path.unlink() - if not force and target_file.exists(): + if not force and target_path.exists(): raise FolderAlreadyExistsError( - f"""{target_file} already exists. Consider - deleting it first.""" + f"{target_path} already exists. Consider deleting it first." ) sys = DistributionSystem.from_json(gdm_file) reducer_func = {"three_phase": reduce_to_three_phase_system} new_sys_name = sys.name + "_reduced" if sys.name else None - new_sys = reducer_func[reducer](sys, new_sys_name, timeseries) - new_sys.to_json(target_file) - click.echo(str(target_file)) + new_sys = reducer_func[reducer.value](sys, new_sys_name, timeseries) + new_sys.to_json(target_path) + typer.echo(str(target_path)) diff --git a/src/gdm/distribution/components/base/distribution_branch_base.py b/src/gdm/distribution/components/base/distribution_branch_base.py index 5da69f49..624ca290 100644 --- a/src/gdm/distribution/components/base/distribution_branch_base.py +++ b/src/gdm/distribution/components/base/distribution_branch_base.py @@ -41,6 +41,15 @@ def validate_fields_base(self) -> "DistributionBranchBase": msg = f"Conductor phase ({phase=}) does not match bus phases ({bus.phases=})" raise ValueError(msg) + num_branch_phases = len(self.phases) + if not any(num_branch_phases == len(bus.phases) for bus in self.buses): + bus_phase_counts = {bus.name: len(bus.phases) for bus in self.buses} + msg = ( + f"Number of branch phases ({num_branch_phases}) must match the number of phases " + f"of at least one connected bus. Connected bus phase counts: {bus_phase_counts}" + ) + raise ValueError(msg) + if self.buses[0].name == self.buses[1].name: msg = ( f"From bus {self.buses[0].name=} and to bus" diff --git a/src/gdm/distribution/components/matrix_impedance_branch.py b/src/gdm/distribution/components/matrix_impedance_branch.py index a23682e6..a20792e2 100644 --- a/src/gdm/distribution/components/matrix_impedance_branch.py +++ b/src/gdm/distribution/components/matrix_impedance_branch.py @@ -41,7 +41,7 @@ def kron_reduce(self) -> None: """Kron reduce the branch to remove neutral.""" self.equipment.kron_reduce(self.phases) - self.phases = [p for p in self.phases if p != Phase.N] + object.__setattr__(self, "phases", [p for p in self.phases if p != Phase.N]) @classmethod def example(cls) -> "MatrixImpedanceBranch": diff --git a/src/gdm/distribution/upgrade_handler/from__2_3_0__to__2_3_1.py b/src/gdm/distribution/upgrade_handler/from__2_3_0__to__2_3_1.py new file mode 100644 index 00000000..923e145d --- /dev/null +++ b/src/gdm/distribution/upgrade_handler/from__2_3_0__to__2_3_1.py @@ -0,0 +1,15 @@ +from loguru import logger +from infrasys.migrations.metadata_migration import migrate_component_metadata + + +def from__2_3_0__to__2_3_1(data: dict, from_version: str, to_version: str) -> dict: + logger.info(f"Upgrading DistributionSystem from verion {from_version} to {to_version}") + data["data_format_version"] = str(to_version) + number_of_components_before = len(data["components"]) + data["components"] = migrate_component_metadata(data["components"]) + number_of_components_after = len(data["components"]) + assert ( + number_of_components_before == number_of_components_after + ), "Number of components should be the same before and after model upgrade" + + return data diff --git a/src/gdm/distribution/upgrade_handler/upgrade_handler.py b/src/gdm/distribution/upgrade_handler/upgrade_handler.py index 30730456..18032107 100644 --- a/src/gdm/distribution/upgrade_handler/upgrade_handler.py +++ b/src/gdm/distribution/upgrade_handler/upgrade_handler.py @@ -18,6 +18,7 @@ from gdm.distribution.upgrade_handler.from__2_1_5__to__2_2_0 import from__2_1_5__to__2_2_0 from gdm.distribution.upgrade_handler.from__2_2_0__to__2_2_1 import from__2_2_0__to__2_2_1 from gdm.distribution.upgrade_handler.from__2_2_1__to__2_3_0 import from__2_2_1__to__2_3_0 +from gdm.distribution.upgrade_handler.from__2_3_0__to__2_3_1 import from__2_3_0__to__2_3_1 def fix_version(version): @@ -100,6 +101,11 @@ class UpgradeHandler(BaseModel): from_version="2.2.1", to_version="2.3.0", ), + UpgradeSchema( + method=from__2_3_0__to__2_3_1, + from_version="2.3.0", + to_version="2.3.1", + ), ] @model_validator(mode="after") diff --git a/src/gdm/mcp/server.py b/src/gdm/mcp/server.py index 1dcb4312..20cc0493 100644 --- a/src/gdm/mcp/server.py +++ b/src/gdm/mcp/server.py @@ -7,9 +7,9 @@ import json import logging from pathlib import Path -from typing import Any +from typing import Annotated, Any -import click +import typer from gdm.distribution import DistributionSystem from mcp.server import Server from mcp.server.stdio import stdio_server @@ -741,12 +741,14 @@ async def _get_component_fields(args: dict) -> dict: return {"component_name": component_name, "fields": fields} -@click.command() -@click.option("--host", default="localhost", help="Server host") -@click.option("--port", default=8000, type=int, help="Server port") -@click.option("--log-level", default="INFO", help="Logging level") -@click.option("--allow-auto-fix", is_flag=True, help="Allow auto-fix operations") -def main(host: str, port: int, log_level: str, allow_auto_fix: bool): +def _run_server( + host: Annotated[str, typer.Option(help="Server host")] = "localhost", + port: Annotated[int, typer.Option(help="Server port")] = 8000, + log_level: Annotated[str, typer.Option(help="Logging level")] = "INFO", + allow_auto_fix: Annotated[ + bool, typer.Option("--allow-auto-fix", help="Allow auto-fix operations") + ] = False, +): """Start the GDM MCP server.""" # Set log level logging.getLogger("gdm_mcp").setLevel(log_level.upper()) @@ -765,5 +767,9 @@ async def run(): asyncio.run(run()) +def main(): + typer.run(_run_server) + + if __name__ == "__main__": main() diff --git a/src/gdm/version.py b/src/gdm/version.py index def4270b..c7260acb 100644 --- a/src/gdm/version.py +++ b/src/gdm/version.py @@ -3,7 +3,7 @@ import platform import sys -VERSION = "2.3.0" +VERSION = "2.3.1" SUPPORTED_VERSIONS = [VERSION] diff --git a/tests/distribution/components/test_matrix_impedance_branch.py b/tests/distribution/components/test_matrix_impedance_branch.py index 8f4cfca6..8821c275 100644 --- a/tests/distribution/components/test_matrix_impedance_branch.py +++ b/tests/distribution/components/test_matrix_impedance_branch.py @@ -1,8 +1,36 @@ import pytest from gdm.distribution.components import MatrixImpedanceBranch, DistributionBus -from gdm.quantities import Voltage -from gdm.distribution.enums import Phase +from gdm.distribution.components.distribution_feeder import DistributionFeeder +from gdm.distribution.components.distribution_substation import DistributionSubstation +from gdm.distribution.equipment.matrix_impedance_branch_equipment import ( + MatrixImpedanceBranchEquipment, +) +from gdm.quantities import Voltage, Distance +from gdm.quantities import ResistancePULength, ReactancePULength, CapacitancePULength, Current +from gdm.distribution.enums import Phase, VoltageTypes + + +def _make_bus(name: str, phases: list[Phase], voltage_v: float = 400.0) -> DistributionBus: + return DistributionBus( + name=name, + voltage_type=VoltageTypes.LINE_TO_LINE, + phases=phases, + rated_voltage=Voltage(voltage_v, "volt"), + substation=DistributionSubstation.example(), + feeder=DistributionFeeder.example(), + ) + + +def _two_phase_equipment() -> MatrixImpedanceBranchEquipment: + """2×2 matrix equipment for two-phase [A, B] branches.""" + return MatrixImpedanceBranchEquipment( + name="2-phase-equipment", + r_matrix=ResistancePULength([[0.0882, 0.0312], [0.0312, 0.0902]], "ohm/mi"), + x_matrix=ReactancePULength([[0.2074, 0.0935], [0.0935, 0.2008]], "ohm/mi"), + c_matrix=CapacitancePULength([[2.903, -0.679], [-0.679, 3.159]], "nanofarad/mi"), + ampacity=Current(90, "ampere"), + ) def test_wrong_number_of_buses(): @@ -66,3 +94,47 @@ def test_connecting_buses_with_different_voltage(): phases=branch.phases, equipment=branch.equipment, ) + + +# --------------------------------------------------------------------------- +# Phase count validation tests +# --------------------------------------------------------------------------- + + +def test_branch_phase_count_matches_both_buses(): + """Branch with same phase count as both buses is valid.""" + branch = MatrixImpedanceBranch.example() + # The example has 3 phases on branch and both buses — just confirm it validates. + assert len(branch.phases) == 3 + + +def test_branch_phase_count_matches_one_bus_valid(): + """Branch phase count matching one bus (different count on the other) should be valid.""" + bus1 = _make_bus("bus-ab-1", [Phase.A, Phase.B]) + bus2 = _make_bus("bus-abc-2", [Phase.A, Phase.B, Phase.C]) + branch = MatrixImpedanceBranch( + name="branch-ab", + buses=[bus1, bus2], + length=Distance(100, "meter"), + phases=[Phase.A, Phase.B], + substation=DistributionSubstation.example(), + feeder=DistributionFeeder.example(), + equipment=_two_phase_equipment(), + ) + assert len(branch.phases) == 2 + + +def test_branch_phase_count_matches_no_bus_invalid(): + """Branch phase count differing from every connected bus must be rejected.""" + bus1 = _make_bus("bus-abc-1", [Phase.A, Phase.B, Phase.C]) + bus2 = _make_bus("bus-abc-2", [Phase.A, Phase.B, Phase.C]) + with pytest.raises(ValueError, match="Number of branch phases"): + MatrixImpedanceBranch( + name="branch-ab-count-mismatch", + buses=[bus1, bus2], + length=Distance(100, "meter"), + phases=[Phase.A, Phase.B], + substation=DistributionSubstation.example(), + feeder=DistributionFeeder.example(), + equipment=_two_phase_equipment(), + )