Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/openapi/attributes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,21 @@ function _impedance_correction_curves(data::Dict)
return curves
end

"""The curve field a correction table's control mode selects, with the unit PSS/E states
the table's x axis in: a tap ratio is dimensionless, a phase-shift angle is in degrees
(converted to the schema's radians on write). Exhaustive over the enum."""
function _correction_curve_field(control_mode::AbstractString)
control_mode == "TAP_RATIO" && return (:tap_ratio_correction_curve, "1")
control_mode == "PHASE_SHIFT_ANGLE" && return (:phase_angle_correction_curve, "deg")
throw(IS.DataFormatError("unhandled impedance correction control mode $control_mode"))
end

"""
Build and register a new `ImpedanceCorrectionData` for `(table_number, winding)` and
attach it to `transformer_id` — the first-sighting path for a (table, winding) pair.
Returns the attribute, so later sightings of the same pair associate against it directly
(see [`_attach_impedance_correction!`](@ref)).
(see [`_attach_impedance_correction!`](@ref)). The table's curve lands on the one field its
control mode selects; the other curve stays absent.
"""
function _new_impedance_correction_attribute!(
sys::OpenAPISystem,
Expand All @@ -71,7 +81,8 @@ function _new_impedance_correction_attribute!(
attribute = stage(PO.ImpedanceCorrectionData)
set_value!(attribute, :id, next_id!(get_registry(sys)))
set_value!(attribute, :table_number, table_number)
set_value!(attribute, :impedance_correction_curve, curve)
field, x_unit = _correction_curve_field(control_mode)
set_value!(attribute, field, curve, x_unit)
set_value!(attribute, :transformer_winding, winding)
set_value!(attribute, :transformer_control_mode, control_mode)
add_supplemental_attribute!(sys, attribute, transformer_id)
Expand Down
144 changes: 79 additions & 65 deletions src/openapi/branch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,37 +129,37 @@ const TRANSFORMER_CONTROL_OBJECTIVE_NAMES = Dict(
5 => "ASYMMETRIC_ACTIVE_POWER_FLOW",
)

"""COD values whose control objective is a phase-shift (angle) control rather than a tap
(voltage/reactive) control."""
const _PHASE_SHIFT_OBJECTIVES = (
"ACTIVE_POWER_FLOW",
"ACTIVE_POWER_FLOW_DISABLED",
"ASYMMETRIC_ACTIVE_POWER_FLOW",
"ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED",
"""Objective → `(actuator, target)` band fields, one field per physical quantity; the same
pairing PowerSystems validates against. `UNDEFINED` has no control block and no bands."""
const _CONTROL_BAND_FIELDS = Dict{String, Union{Nothing, NTuple{2, Symbol}}}(
"UNDEFINED" => nothing,
"FIXED" => (:tap_ratio_limits, :controlled_voltage_limits),
"VOLTAGE" => (:tap_ratio_limits, :controlled_voltage_limits),
"VOLTAGE_DISABLED" => (:tap_ratio_limits, :controlled_voltage_limits),
"REACTIVE_POWER_FLOW" =>
(:tap_ratio_limits, :controlled_reactive_power_flow_limits),
"REACTIVE_POWER_FLOW_DISABLED" =>
(:tap_ratio_limits, :controlled_reactive_power_flow_limits),
"CONTROL_OF_DC_LINE" => (:tap_ratio_limits, :controlled_active_power_flow_limits),
"CONTROL_OF_DC_LINE_DISABLED" =>
(:tap_ratio_limits, :controlled_active_power_flow_limits),
"ACTIVE_POWER_FLOW" => (:phase_angle_limits, :controlled_active_power_flow_limits),
"ACTIVE_POWER_FLOW_DISABLED" =>
(:phase_angle_limits, :controlled_active_power_flow_limits),
"ASYMMETRIC_ACTIVE_POWER_FLOW" =>
(:phase_angle_limits, :controlled_active_power_flow_limits),
"ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED" =>
(:phase_angle_limits, :controlled_active_power_flow_limits),
)
@assert Set(keys(_CONTROL_BAND_FIELDS)) == Set(values(TRANSFORMER_CONTROL_OBJECTIVE_NAMES))

"""`TransformerCircuit.control_limits`' unit per `control_objective`, read directly off
the schema's `x-units` table (`TransformerCircuit.json`)."""
const _CONTROL_LIMITS_UNIT = Dict(
"UNDEFINED" => "1", "VOLTAGE_DISABLED" => "1",
"REACTIVE_POWER_FLOW_DISABLED" => "1",
"ACTIVE_POWER_FLOW_DISABLED" => "rad", "CONTROL_OF_DC_LINE_DISABLED" => "1",
"ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED" => "rad", "FIXED" => "1", "VOLTAGE" => "1",
"REACTIVE_POWER_FLOW" => "1", "ACTIVE_POWER_FLOW" => "rad",
"CONTROL_OF_DC_LINE" => "1",
"ASYMMETRIC_ACTIVE_POWER_FLOW" => "rad",
)

"""`TransformerCircuit.controlled_quantity_limits`' unit per `control_objective`, read
directly off the schema's `x-units` table."""
const _CONTROLLED_QUANTITY_LIMITS_UNIT = Dict(
"UNDEFINED" => "pu", "VOLTAGE_DISABLED" => "pu",
"REACTIVE_POWER_FLOW_DISABLED" => "MVAr",
"ACTIVE_POWER_FLOW_DISABLED" => "MW", "CONTROL_OF_DC_LINE_DISABLED" => "MW",
"ASYMMETRIC_ACTIVE_POWER_FLOW_DISABLED" => "MW", "FIXED" => "pu", "VOLTAGE" => "pu",
"REACTIVE_POWER_FLOW" => "MVAr", "ACTIVE_POWER_FLOW" => "MW",
"CONTROL_OF_DC_LINE" => "MW",
"ASYMMETRIC_ACTIVE_POWER_FLOW" => "MW",
"""The wire unit of each band, matching `TransformerCircuit.json`'s `x-unit`."""
const _CONTROL_BAND_UNITS = Dict(
:tap_ratio_limits => "1",
:phase_angle_limits => "rad",
:controlled_voltage_limits => "pu",
:controlled_reactive_power_flow_limits => "MVAr",
:controlled_active_power_flow_limits => "MW",
)

function _transformer_control_objective(cod::Real)
Expand All @@ -170,54 +170,68 @@ function _transformer_control_objective(cod::Real)
return TRANSFORMER_CONTROL_OBJECTIVE_NAMES[code]
end

"""Warn and swap an inverted `(lo, hi)` band, naming the PSS/E columns it came from."""
function _ordered_band(lo, hi, lo_col, hi_col, record, suffix)
if lo > hi
@warn "Transformer $record winding $suffix has inverted $lo_col$suffix = $lo > $hi_col$suffix = $hi; normalizing to (min = $hi, max = $lo)."
return hi, lo
end
return lo, hi
end

"""
Assign a `TransformerCircuit`'s flat control block from a pm transformer dict `d` for
winding `suffix` (1/2/3). Ported from PSCB's `_transformer_control_fields`: PSS/E's
`RMI`/`RMA`/`VMI`/`VMA` are already expressed in the unit `control_objective` implies, so
every value is a direct passthrough once `_CONTROL_LIMITS_UNIT`/
`_CONTROLLED_QUANTITY_LIMITS_UNIT` supply that unit. `record` names the site in the
inverted-limits warnings.
winding `suffix` (1/2/3). The objective selects one actuator band and one target band
(`_CONTROL_BAND_FIELDS`); every other band stays absent, and so does every band under
`UNDEFINED`, so a circuit with no control block carries no invented limits.

PSS/E's `RMI`/`RMA` and `VMI`/`VMA` arrive already in the unit the objective implies, but
`pti.jl` substitutes 0.9/1.1 for a column the file omitted. That substitute is a voltage or
tap-ratio default and nothing else, so `VM_PRESENT`/`RM_PRESENT` decide what may be written:
`controlled_voltage_limits` and `tap_ratio_limits` take the default when the file omitted
them, while an omitted power band or angle band stays absent rather than carrying a
fabricated value. `record` names the site in the inverted-limits warnings.
"""
function _set_transformer_control_fields!(
circuit,
d::Dict,
suffix::Int,
record::AbstractString,
)
cod = get(d, "COD$suffix", -99)
objective = _transformer_control_objective(cod)
phase_shifting = objective in _PHASE_SHIFT_OBJECTIVES
if phase_shifting
rmi_default, rma_default = -180.0, 180.0
else
rmi_default, rma_default = 0.9, 1.1
end
rmi = get(d, "RMI$suffix", rmi_default)
rma = get(d, "RMA$suffix", rma_default)
if rmi > rma
@warn "Transformer $record winding $suffix has inverted control limits RMI$suffix = $rmi > RMA$suffix = $rma; normalizing to (min = $rma, max = $rmi)."
rmi, rma = rma, rmi
end
if phase_shifting
rmi, rma = deg2rad(rmi), deg2rad(rma)
end
vmi = get(d, "VMI$suffix", 0.9)
vma = get(d, "VMA$suffix", 1.1)
if vmi > vma
@warn "Transformer $record winding $suffix has inverted controlled-quantity limits VMI$suffix = $vmi > VMA$suffix = $vma; normalizing to (min = $vma, max = $vmi)."
vmi, vma = vma, vmi
end
objective = _transformer_control_objective(get(d, "COD$suffix", -99))
set_value!(circuit, :control_objective, objective)
set_value!(circuit, :regulated_bus_number, Int(get(d, "CONT$suffix", 0)))
set_value!(
circuit,
:control_limits,
(min = rmi, max = rma),
_CONTROL_LIMITS_UNIT[objective],
)
set_value!(circuit, :controlled_quantity_limits, (min = vmi, max = vma),
_CONTROLLED_QUANTITY_LIMITS_UNIT[objective])
set_value!(circuit, :number_of_tap_positions, Int(get(d, "NTP$suffix", 33)))
bands = _CONTROL_BAND_FIELDS[objective]
isnothing(bands) && return
actuator, target = bands

# A hand-built dict without the flag states its values explicitly.
rm_present = get(d, "RM_PRESENT$suffix", true)
vm_present = get(d, "VM_PRESENT$suffix", true)

if actuator == :tap_ratio_limits || rm_present
rmi, rma = _ordered_band(
get(d, "RMI$suffix", 0.9), get(d, "RMA$suffix", 1.1), "RMI", "RMA", record,
suffix,
)
if actuator == :phase_angle_limits
rmi, rma = deg2rad(rmi), deg2rad(rma)
end
set_value!(circuit, actuator, (min = rmi, max = rma), _CONTROL_BAND_UNITS[actuator])
end

if target == :controlled_voltage_limits
vmi, vma =
vm_present ? (get(d, "VMI$suffix", 0.9), get(d, "VMA$suffix", 1.1)) :
(0.9, 1.1)
vmi, vma = _ordered_band(vmi, vma, "VMI", "VMA", record, suffix)
set_value!(circuit, target, (min = vmi, max = vma), _CONTROL_BAND_UNITS[target])
elseif vm_present
vmi, vma =
_ordered_band(d["VMI$suffix"], d["VMA$suffix"], "VMI", "VMA", record, suffix)
set_value!(circuit, target, (min = vmi, max = vma), _CONTROL_BAND_UNITS[target])
end
return
end

Expand Down
56 changes: 31 additions & 25 deletions src/openapi/dc_branch.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `data["dcline"]` IS a native PowerModels section — `_make_per_unit!` divides its power
# fields (`pf`/`qf`/`p*f`/`q*f`/`p*t`/`q*t`) by `baseMVA`. The LCC-specific fields (`r`,
# `transfer_setpoint`, `scheduled_dc_voltage`, `rectifier_*`, `inverter_*`, and the rest
# `transfer_setpoint` (raw SETVL), `scheduled_dc_voltage`, `rectifier_*`, `inverter_*`, and the rest
# of the PSS/E-native block) have no PowerModels counterpart, so `_make_per_unit!` never
# touches them: they arrive already in the natural unit the schema's `NATURAL_UNITS`
# default expects (ohms, kV, radians, ...). `TwoTerminalGenericHVDCLine`/
Expand Down Expand Up @@ -58,18 +58,18 @@ function make_lcc_line!(
set_value!(component, :active_power_flow, get(d, "pf", 0.0) * sys_mbase, "MW")
set_value!(component, :parameter_units, "NATURAL_UNITS")
set_value!(component, :r, d["r"], "ohm")
set_value!(component, :power_mode, Bool(d["power_mode"]))
if d["power_mode"]
transfer_setpoint_unit = "MW"
else
transfer_setpoint_unit = "A"
# `control_mode` (PSS/E MDC) selects which schedule the raw SETVL is: a power in MW
# under POWER, a current in amperes under CURRENT, and nothing under BLOCKED, where the
# line holds no schedule and both setpoints stay absent. Exhaustive over the enum.
control_mode = d["control_mode"]
set_value!(component, :control_mode, control_mode)
if control_mode == "POWER"
set_value!(component, :power_transfer_setpoint, d["transfer_setpoint"], "MW")
elseif control_mode == "CURRENT"
set_value!(component, :current_transfer_setpoint, d["transfer_setpoint"], "A")
elseif control_mode != "BLOCKED"
throw(IS.DataFormatError("DC line $name: unknown LCC control_mode $control_mode"))
end
set_value!(
component,
:transfer_setpoint,
d["transfer_setpoint"],
transfer_setpoint_unit,
)
set_value!(component, :dc_voltage_units, "NATURAL_UNITS")
set_value!(component, :scheduled_dc_voltage, d["scheduled_dc_voltage"], "kV")
set_value!(component, :rectifier_bridges, Int(d["rectifier_bridges"]))
Expand Down Expand Up @@ -179,16 +179,18 @@ convention as `data["dcline"]`'s native fields, so this maker multiplies them ba

`dc_current`("if")/`max_dc_current_from`/`to`/`power_factor_weighting_fraction_from`/`to`
are already natural (Amperes / a bare fraction) and pass through unscaled.
`dc_setpoint_from`/`to` is per-unit on `rated_dc_voltage` when the converter controls DC
voltage, or on `sys_mbase` when it controls DC power.
The pm dict's `dc_setpoint_from`/`to` is per-unit on `rated_dc_voltage` when the converter
controls DC voltage, or on `sys_mbase` when it controls DC power; each lands on the one wire
field its mode selects, `dc_voltage_setpoint_*` (pu) or `dc_power_setpoint_*` (MW), and the
other stays absent. Likewise `ac_setpoint_from`/`to` lands on `ac_voltage_setpoint_*` (pu)
under AC voltage control or `power_factor_setpoint_*` otherwise.

`setpoint_voltage_units` (decoupled from `voltage_units`, which tags only
`voltage_limits_from`/`to`) is set unconditionally to `COMPONENT_BASE`: PSS/E always reports a
voltage-controlling side's DC setpoint as p.u. of `rated_dc_voltage` (`psse.jl` pre-divides
`DCSET` by `base_voltage`) and a voltage-controlling AC setpoint (`ACSET`) as p.u. of the AC
bus's own base voltage — never kV. The `DC_POWER`/`AC_REACTIVE_POWER` branches have their own
fixed units (`MW`/`1`) and ignore this discriminator, so setting it unconditionally is safe
regardless of which sides actually control voltage.
bus's own base voltage — never kV. It governs only `dc_voltage_setpoint_*` and
`ac_voltage_setpoint_*`; the power and power-factor setpoints carry their own fixed units.

`psse.jl` also captures each converter's own AC bus base kV as `base_voltage_from`/
`base_voltage_to`, threaded onto the document as `rated_ac_voltage_from`/
Expand Down Expand Up @@ -225,17 +227,19 @@ function make_vscline!(
set_value!(component, :setpoint_voltage_units, "COMPONENT_BASE")
if d["dc_voltage_control_from"]
set_value!(component, :dc_control_from, "DC_VOLTAGE")
set_value!(component, :dc_setpoint_from, d["dc_setpoint_from"], "pu")
set_value!(component, :dc_voltage_setpoint_from, d["dc_setpoint_from"], "pu")
else
set_value!(component, :dc_control_from, "DC_POWER")
set_value!(component, :dc_setpoint_from, d["dc_setpoint_from"] * sys_mbase, "MW")
set_value!(
component, :dc_power_setpoint_from, d["dc_setpoint_from"] * sys_mbase, "MW",
)
end
if d["ac_voltage_control_from"]
set_value!(component, :ac_control_from, "AC_VOLTAGE")
set_value!(component, :ac_setpoint_from, d["ac_setpoint_from"], "pu")
set_value!(component, :ac_voltage_setpoint_from, d["ac_setpoint_from"], "pu")
else
set_value!(component, :ac_control_from, "AC_REACTIVE_POWER")
set_value!(component, :ac_setpoint_from, d["ac_setpoint_from"], "1")
set_value!(component, :power_factor_setpoint_from, d["ac_setpoint_from"], "1")
end
set_value!(component, :rated_ac_voltage_from, d["base_voltage_from"], "kV")
set_value!(
Expand All @@ -257,17 +261,19 @@ function make_vscline!(
set_value!(component, :reactive_power_to, get(d, "qt", 0.0) * sys_mbase, "MVAr")
if d["dc_voltage_control_to"]
set_value!(component, :dc_control_to, "DC_VOLTAGE")
set_value!(component, :dc_setpoint_to, d["dc_setpoint_to"], "pu")
set_value!(component, :dc_voltage_setpoint_to, d["dc_setpoint_to"], "pu")
else
set_value!(component, :dc_control_to, "DC_POWER")
set_value!(component, :dc_setpoint_to, d["dc_setpoint_to"] * sys_mbase, "MW")
set_value!(
component, :dc_power_setpoint_to, d["dc_setpoint_to"] * sys_mbase, "MW",
)
end
if d["ac_voltage_control_to"]
set_value!(component, :ac_control_to, "AC_VOLTAGE")
set_value!(component, :ac_setpoint_to, d["ac_setpoint_to"], "pu")
set_value!(component, :ac_voltage_setpoint_to, d["ac_setpoint_to"], "pu")
else
set_value!(component, :ac_control_to, "AC_REACTIVE_POWER")
set_value!(component, :ac_setpoint_to, d["ac_setpoint_to"], "1")
set_value!(component, :power_factor_setpoint_to, d["ac_setpoint_to"], "1")
end
set_value!(component, :rated_ac_voltage_to, d["base_voltage_to"], "kV")
set_value!(
Expand Down
Loading
Loading