Skip to content
686 changes: 686 additions & 0 deletions CODEBASE_REVIEW_REPORT.md

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions src/PowerSystems.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
isdefined(Base, :__precompile__) && __precompile__()

"""
Module for constructing self-contained power system objects.
"""
Expand Down
41 changes: 22 additions & 19 deletions src/parsers/im_io/matlab.jl
Original file line number Diff line number Diff line change
Expand Up @@ -315,24 +315,27 @@ function _add_line_delimiter(mp_line::AbstractString, start_char, end_char)
end

"Checks if the given value is of a given type, if not tries to make it that type"
function check_type(typ, value)
if isa(value, typ)
return value
elseif isa(value, String) || isa(value, SubString)
try
value = parse(typ, value)
return value
catch e
@error "parsing error, the matlab string \"$(value)\" can not be parsed to $(typ) data"
rethrow(e)
end
else
try
value = typ(value)
return value
catch e
@error "parsing error, the matlab value $(value) of type $(typeof(value)) can not be parsed to $(typ) data"
rethrow(e)
end
# Multiple dispatch version - value already has correct type
function check_type(::Type{T}, value::T) where {T}
return value
end

# Multiple dispatch version - handle String/SubString by parsing
function check_type(typ::Type{T}, value::Union{String, SubString}) where {T}
try
return parse(typ, value)
catch e
@error "parsing error, the matlab string \"$(value)\" can not be parsed to $(typ) data"
rethrow(e)
end
end

# Multiple dispatch version - fallback for type conversion
function check_type(typ::Type{T}, value) where {T}
try
return typ(value)
catch e
@error "parsing error, the matlab value $(value) of type $(typeof(value)) can not be parsed to $(typ) data"
rethrow(e)
end
end
2 changes: 1 addition & 1 deletion src/parsers/pm_io/psse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool)
)
y_increment_sorted =
sort(collect(keys(y_increment)); by = x -> parse(Int, x[2:end]))
sub_data["y_increment"] = [y_increment[k] for k in y_increment_sorted]im
sub_data["y_increment"] = [y_increment[k] for k in y_increment_sorted]
sub_data["y_increment"] = sub_data["y_increment"][sub_data["y_increment"] .!= 0]

if pm_data["source_version"] == "35"
Expand Down
12 changes: 7 additions & 5 deletions src/parsers/power_system_table_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ function System(
end

timeseries_metadata_file =
get(kwargs, :timeseries_metadata_file, getfield(data, :timeseries_metadata_file))
get(kwargs, :timeseries_metadata_file, data.timeseries_metadata_file)

if !isnothing(timeseries_metadata_file)
add_time_series!(sys, timeseries_metadata_file; resolution = time_series_resolution)
Expand Down Expand Up @@ -1268,6 +1268,10 @@ function make_synchronous_condenser_generator(
)
end

# Helper functions to parse boolean values with multiple dispatch
_parse_bool_value(value::Bool) = value
_parse_bool_value(value) = parse(Bool, lowercase(String(value)))

function make_thermal_generator(
data::PowerSystemTableData,
gen,
Expand All @@ -1289,9 +1293,7 @@ function make_thermal_generator(
op_cost = make_cost(ThermalStandard, data, gen, cost_colnames)

gen_must_run = isnothing(gen.must_run) ? false : gen.must_run
if !isa(gen_must_run, Bool)
gen_must_run = parse(Bool, lowercase(String(gen_must_run)))
end
gen_must_run = _parse_bool_value(gen_must_run)

return ThermalStandard(;
name = gen.name,
Expand Down Expand Up @@ -1743,7 +1745,7 @@ struct _FieldInfo
Tuple{UnitSystem, UnitSystem, String},
}
unit_conversion::Union{NamedTuple{(:From, :To), Tuple{String, String}}, Nothing}
default_value::Any
default_value::Union{Float64, Int, String}
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type constraint change from Any to Union{Float64, Int, String} may be too restrictive. The code shows default_value can be:

  1. String literals like "required" (line 1785, 1840)
  2. String literals like "system_base_power" which gets replaced with data.base_power (line 1786-1787)
  3. Other values from YAML descriptors (line 1785)

Without comprehensive knowledge of all possible values in the YAML descriptor files, this change could cause runtime errors if the descriptors contain other types (e.g., Nothing, Bool, other numeric types). Consider either:

  • Keeping Any if the type variety is truly unknown
  • Using Union{Float64, Int, String, Nothing} if Nothing is possible
  • Adding validation to ensure only these types are present in descriptors

The original report suggests this improves type stability, but it could introduce breaking changes if the constraint is too narrow.

Suggested change
default_value::Union{Float64, Int, String}
default_value::Union{Float64, Int, String, Nothing}

Copilot uses AI. Check for mistakes.
# TODO unit, value ranges and options
end

Expand Down
67 changes: 38 additions & 29 deletions src/parsers/psse_dynamic_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,46 @@ function _parse_dyr_file(file::AbstractString)
return parsed_values
end

function _parse_input_types(_v, val)
# If the parameter is a Float64, then use the value directly as the argument.
# Typically uses for the resistance that is not available in .dyr files.
if isa(_v, Float64)
if isnan(_v)
error("nan for $(_v)")
end
return _v
# If the parameter is an Int, then use the integer as the key of the value in the dictionary.
elseif isa(_v, Int)
return val[_v]
elseif _v == "NaN"
# Multiple dispatch versions for type stability
# Handle Float64 parameters - use value directly (e.g., resistance not in .dyr files)
function _parse_input_types(v::Float64, val)
if isnan(v)
error("nan for $(v)")
end
return v
end

# Handle Int parameters - use as key to index into val dictionary
function _parse_input_types(v::Int, val)
return val[v]
end

# Handle String parameters - either "NaN" or tuple patterns like "(1,2)" or "(1,2,3)"
function _parse_input_types(v::String, val)
if v == "NaN"
return NaN
# If the parameter is a tuple (as a string), then construct the tuple directly.
elseif isa(_v, String)
#TODO: Generalize n-length tuple
m = match(r"^\((\d+)\s*,\s*(\d+)\)$", _v)
m2 = match(r"^\((\d+)\s*,\s*(\d+),\s*(\d+)\)$", _v)
if m !== nothing
_tuple_ix = parse.(Int, m.captures)
return Tuple(val[_tuple_ix])
elseif m2 !== nothing
_tuple_ix = parse.(Int, m2.captures)
return Tuple(val[_tuple_ix])
else
error("String $(_v) not recognized for parsing")
end
else
error("invalid input value $val")
end
return

# Try to parse as tuple pattern
# TODO: Generalize n-length tuple
m = match(r"^\((\d+)\s*,\s*(\d+)\)$", v)
if m !== nothing
_tuple_ix = parse.(Int, m.captures)
return Tuple(val[_tuple_ix])
end

m2 = match(r"^\((\d+)\s*,\s*(\d+),\s*(\d+)\)$", v)
if m2 !== nothing
_tuple_ix = parse.(Int, m2.captures)
return Tuple(val[_tuple_ix])
end

error("String $(v) not recognized for parsing")
end

# Fallback for unexpected types
function _parse_input_types(v, val)
error("invalid input value $val with type $(typeof(v))")
end

"""
Expand Down
21 changes: 13 additions & 8 deletions src/utils/IO/base_checks.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# Multiple dispatch version - handles Nothing case
function orderedlimits(
limits::Union{NamedTuple{(:min, :max), Tuple{Float64, Float64}}, Nothing},
limits::Nothing,
limitsname::String,
)
if isa(limits, Nothing)
@info "'$limitsname' limits defined as nothing"
else
if limits.max < limits.min
throw(DataFormatError("$limitsname limits not in ascending order"))
end
end
@info "'$limitsname' limits defined as nothing"
return limits
end

# Multiple dispatch version - handles NamedTuple case
function orderedlimits(
limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}},
limitsname::String,
)
if limits.max < limits.min
throw(DataFormatError("$limitsname limits not in ascending order"))
end
return limits
end

Expand Down
137 changes: 86 additions & 51 deletions src/utils/IO/branchdata_checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ const MVA_LIMITS_TRANSFORMERS = Dict(
765.0 => (min = 2200.0, max = 6900.0), # This value is 3x the SIL value from https://neos-guide.org/wp-content/uploads/2022/04/line_flow_approximation.pdf
)

# Helper function to validate a single rating field without dynamic field access
function _validate_line_rating_field(
field_name::Symbol,
rating_value::Union{Float64, Nothing},
line_name::String,
basemva::Float64,
closest_v_level::Float64,
closest_rate_range::NamedTuple,
)
isnothing(rating_value) && return nothing

if (rating_value >= 2.0 * closest_rate_range.max / basemva)
@warn "$(field_name) $(round(rating_value*basemva; digits=2)) MW for $(line_name) is 2x larger than the max expected rating $(closest_rate_range.max) MW for Line at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
elseif (rating_value >= closest_rate_range.max / basemva) ||
(rating_value <= closest_rate_range.min / basemva)
@info "$(field_name) $(round(rating_value*basemva; digits=2)) MW for $(line_name) is outside the expected range $(closest_rate_range) MW for Line at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
end
return nothing
end

function check_rating_values(line::Union{Line, MonitoredLine}, basemva::Float64)
arc = get_arc(line)
vrated = get_base_voltage(get_to(arc))
Expand All @@ -93,22 +115,11 @@ function check_rating_values(line::Union{Line, MonitoredLine}, basemva::Float64)
closest_v_level = voltage_levels[closestV_ix[2]]
closest_rate_range = MVA_LIMITS_LINES[closest_v_level]

# Assuming that the rate is in pu
for field in [:rating, :rating_b, :rating_c]
rating_value = getfield(line, field)
if isnothing(rating_value)
@assert field ∈ [:rating_b, :rating_c]
continue
end
if (rating_value >= 2.0 * closest_rate_range.max / basemva)
@warn "$(field) $(round(rating_value*basemva; digits=2)) MW for $(get_name(line)) is 2x larger than the max expected rating $(closest_rate_range.max) MW for Line at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
elseif (rating_value >= closest_rate_range.max / basemva) ||
(rating_value <= closest_rate_range.min / basemva)
@info "$(field) $(round(rating_value*basemva; digits=2)) MW for $(get_name(line)) is outside the expected range $(closest_rate_range) MW for Line at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
end
end
# Validate each rating field directly (no dynamic field access)
line_name = get_name(line)
_validate_line_rating_field(:rating, line.rating, line_name, basemva, closest_v_level, closest_rate_range)
_validate_line_rating_field(:rating_b, line.rating_b, line_name, basemva, closest_v_level, closest_rate_range)
_validate_line_rating_field(:rating_c, line.rating_c, line_name, basemva, closest_v_level, closest_rate_range)
Comment on lines +120 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
_validate_line_rating_field(:rating, line.rating, line_name, basemva, closest_v_level, closest_rate_range)
_validate_line_rating_field(:rating_b, line.rating_b, line_name, basemva, closest_v_level, closest_rate_range)
_validate_line_rating_field(:rating_c, line.rating_c, line_name, basemva, closest_v_level, closest_rate_range)
_validate_line_rating_field(
:rating,
line.rating,
line_name,
basemva,
closest_v_level,
closest_rate_range,
)
_validate_line_rating_field(
:rating_b,
line.rating_b,
line_name,
basemva,
closest_v_level,
closest_rate_range,
)
_validate_line_rating_field(
:rating_c,
line.rating_c,
line_name,
basemva,
closest_v_level,
closest_rate_range,
)


return true
end
Expand All @@ -135,25 +146,36 @@ function line_rating_calculation(l::Union{Line, MonitoredLine})
return new_rate
end

# Helper function to correct a single rating field
# Accesses field directly to avoid stale value issues
function _correct_single_rate_limit!(
field_name::Symbol,
branch::Union{Line, MonitoredLine},
theoretical_line_rate_pu::Float64,
)::Bool
rating_value = getfield(branch, field_name)
isnothing(rating_value) && return true

if rating_value < 0.0
@error "PowerSystems does not support negative line rates. $(field_name): $(rating_value)"
return false
end
if rating_value == INFINITE_BOUND
@warn "Data for branch $(summary(branch)) $(field_name) is set to INFINITE_BOUND. \
PowerSystems will set a rate from line parameters to $(theoretical_line_rate_pu)" maxlog =
PS_MAX_LOG
setfield!(branch, field_name, theoretical_line_rate_pu)
end
return true
end

function correct_rate_limits!(branch::Union{Line, MonitoredLine}, basemva::Float64)
theoretical_line_rate_pu = line_rating_calculation(branch)
for field in [:rating, :rating_b, :rating_c]
rating_value = getfield(branch, field)
if isnothing(rating_value)
@assert field ∈ [:rating_b, :rating_c]
continue
end
if rating_value < 0.0
@error "PowerSystems does not support negative line rates. $(field): $(rating)"
return false
end
if rating_value == INFINITE_BOUND
@warn "Data for branch $(summary(branch)) $(field) is set to INFINITE_BOUND. \
PowerSystems will set a rate from line parameters to $(theoretical_line_rate_pu)" maxlog =
PS_MAX_LOG
setfield!(branch, field, theoretical_line_rate_pu)
end
end

# Process each rating field via helper function
_correct_single_rate_limit!(:rating, branch, theoretical_line_rate_pu) || return false
_correct_single_rate_limit!(:rating_b, branch, theoretical_line_rate_pu) || return false
_correct_single_rate_limit!(:rating_c, branch, theoretical_line_rate_pu) || return false

return check_rating_values(branch, basemva)
end
Expand Down Expand Up @@ -184,42 +206,55 @@ function validate_component_with_system(
return is_valid_reactance && is_valid_rating
end

# Helper function to validate a single transformer rating field without dynamic field access
function _validate_transformer_rating_field(
field_name::Symbol,
rating_value::Union{Float64, Nothing},
xfrm_name::String,
device_base_power::Float64,
closest_v_level::Float64,
closest_rate_range::NamedTuple,
)
isnothing(rating_value) && return nothing

if (rating_value * device_base_power >= 2.0 * closest_rate_range.max)
@warn "$(field_name) $(round(rating_value*device_base_power; digits=2)) MW for $(xfrm_name) is 2x larger than the max expected rating $(closest_rate_range.max) MW for Transformer at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
elseif (rating_value * device_base_power >= closest_rate_range.max) ||
(rating_value * device_base_power <= closest_rate_range.min)
@info "$(field_name) $(round(rating_value*device_base_power; digits=2)) MW for $(xfrm_name) is outside the expected range $(closest_rate_range) MW for Transformer at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
end
return nothing
end

function check_rating_values(
xfrm::TwoWindingTransformer,
::Float64,
)
arc = get_arc(xfrm)
v_from = get_base_voltage(get_from(arc))
v_to = get_base_voltage(get_to(arc))
vrated = maximum([v_from, v_to])
vrated = max(v_from, v_to)
voltage_levels = collect(keys(MVA_LIMITS_TRANSFORMERS))
closestV_ix = findmin(abs.(voltage_levels .- vrated))
closest_v_level = voltage_levels[closestV_ix[2]]
closest_rate_range = MVA_LIMITS_TRANSFORMERS[closest_v_level]
device_base_power = get_base_power(xfrm)
# The rate is in device pu
for field in [:rating, :rating_b, :rating_c]
rating_value = getproperty(xfrm, field)
if isnothing(rating_value)
@assert field ∈ [:rating_b, :rating_c]
continue
end
if (rating_value * device_base_power >= 2.0 * closest_rate_range.max)
@warn "$(field) $(round(rating_value*device_base_power; digits=2)) MW for $(get_name(xfrm)) is 2x larger than the max expected rating $(closest_rate_range.max) MW for Transformer at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
elseif (rating_value * device_base_power >= closest_rate_range.max) ||
(rating_value * device_base_power <= closest_rate_range.min)
@info "$(field) $(round(rating_value*device_base_power; digits=2)) MW for $(get_name(xfrm)) is outside the expected range $(closest_rate_range) MW for Transformer at a $(closest_v_level) kV Voltage level." maxlog =
PS_MAX_LOG
end
end

# Validate each rating field directly (no dynamic field access)
xfrm_name = get_name(xfrm)
_validate_transformer_rating_field(:rating, xfrm.rating, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
_validate_transformer_rating_field(:rating_b, xfrm.rating_b, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
_validate_transformer_rating_field(:rating_c, xfrm.rating_c, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
Comment on lines +247 to +249
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
_validate_transformer_rating_field(:rating, xfrm.rating, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
_validate_transformer_rating_field(:rating_b, xfrm.rating_b, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
_validate_transformer_rating_field(:rating_c, xfrm.rating_c, xfrm_name, device_base_power, closest_v_level, closest_rate_range)
_validate_transformer_rating_field(
:rating,
xfrm.rating,
xfrm_name,
device_base_power,
closest_v_level,
closest_rate_range,
)
_validate_transformer_rating_field(
:rating_b,
xfrm.rating_b,
xfrm_name,
device_base_power,
closest_v_level,
closest_rate_range,
)
_validate_transformer_rating_field(
:rating_c,
xfrm.rating_c,
xfrm_name,
device_base_power,
closest_v_level,
closest_rate_range,
)


return true
end

function check_transformer_reactance(
xfrm::TwoWindingTransformer,
)
x_pu = getproperty(xfrm, :x)
x_pu = xfrm.x # Direct field access for compile-time known field
if x_pu < TYPICAL_XFRM_REACTANCE.min
@warn "Transformer $(get_name(xfrm)) per-unit reactance $(x_pu) is lower than the typical range $(TYPICAL_XFRM_REACTANCE). \
Check if the reactance source data is correct." maxlog = PS_MAX_LOG
Expand Down
Loading