Skip to content

Unitful getters and setters#1659

Open
luke-kiernan wants to merge 10 commits intopsy6from
lk/units-fold-psu
Open

Unitful getters and setters#1659
luke-kiernan wants to merge 10 commits intopsy6from
lk/units-fold-psu

Conversation

@luke-kiernan
Copy link
Copy Markdown
Contributor

@luke-kiernan luke-kiernan commented Apr 17, 2026

Add unitful getters and setters. Interface:

get_active_power(gen) # errors: must specify units
get_active_power(gen, {NU/SU/DU}) # natural units/system base/device base, with units attached 
get_active_power(gen, Float64) # system base, no units attached: internal usage.

5 + get_active_power(gen, NU) # errors: unitless combined with unitful
5 MW + get_active_power(gen, NU) # ok
0.5 SU + get_active_power(gen, NU) # errors: relative combined with natural

set_active_power!(gen, 0.5) # errors: must specify units
set_active_power!(gen, 0.5 {SU/DU}) # system base/device base
set_active_power!(gen, 50 MW) # natural units

Relies on IS #574. This will break stuff downstream left and right, yes, but here's why I think it's worth it:

  1. Usability. It is very very easy to unknowingly mix units. The classic one: read in a time series from a CSV in natural units, then attach it to a system in SYSTEM_BASE.
  2. Type stability. Our stateful unit system is inherently type unstable.
  3. Maintenance. After the catch-all "not having adequate test cases," I've found more bugs due to units issues than anything else.

edit: at the moment, turns out 1-argument getters/setters assume system base. But I'd prefer to have them error: better that than get silently incorrect results. Also, I'd consider adding a 3-arg setter, with the units separate: set_active_power!(gen, 1, {DU/SU/NU}

edit: You can still mess up by adding or comparing device-base quantities from components with different base powers. Same goes for comparing SU quantities between different systems.

@luke-kiernan luke-kiernan requested a review from jd-lara April 17, 2026 17:13
@luke-kiernan
Copy link
Copy Markdown
Contributor Author

luke-kiernan commented Apr 21, 2026

Here is perhaps the biggest argument for these changes:
units_performance_comparison
This is profiling "add up active powers on all thermal gens in the 10k bus system:"

function sum_su_strip(gens)
    s = 0.0
    for g in gens
        s += IS.ustrip(get_active_power(g, IS.SystemBaseUnit())) # change this line
    end
    return s
end

luke-kiernan and others added 8 commits April 24, 2026 17:02
- Vendor PSU sources into src/units/ (types, conversions, serialization).
  RelativeQuantity, AbstractRelativeUnit, DU/SU/NU, convert_units, and
  serialize_quantity now live in PSY.
- Replace stateful unit-system with explicit-units API on Component:
  get_value / set_value use Val-dispatch through _convert_from_device_base
  for type-stable conversions (DU/SU/MW/Mvar/Ω/S/Float64 targets).
- DEFAULT_UNITS = SU; Float64 fast path for hot loops returns raw
  system-base numbers without the wrapper.
- Add MW_ACCUMULATOR_TYPE for _sum_or_zero helpers in system_checks;
  total_capacity_rating / total_load_rating return MW-typed Unitful.Quantity.
- Fix convert_component! (PowerLoad → StandardLoad) to copy raw device-base
  fields instead of round-tripping through SU getters.
- Add supplemental 2-arg get_max_active_power overloads for StaticInjection
  and Union{StandardLoad, InterruptibleStandardLoad}.
- Drop PowerSystemsUnits dep; add Unitful and StructTypes deps.
- Port PSU test suite as test/test_units.jl (77 tests).
- Update tests for unit-bearing return types: test_accessors uses
  _unwrap_units helper; test_printing unwraps units for occursin check;
  with_units_base testsets replaced by explicit-units API tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generated files now emit:
  get_<field>(value::T)        = get_value(value, Val(:<field>), Val(:<cu>))
  get_<field>(value::T, units) = get_value(value, Val(:<field>), Val(:<cu>), units)

and matching set_<field>!(value::T, val) lines, replacing the previous
stateful-unit-system accessors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Template-generated 1-arg getters are gone; all unit-bearing getters now
  take an explicit units argument. Bare `set_foo!(c, 0.5)` errors rather
  than silently treating the value as system base.
- DU/SU/NU and RelativeQuantity now come from IS; this package imports
  and re-exports them, and trims src/units/types.jl to the power-specific
  Unitful @Units (Mvar, MVA) and the UnitArg alias.
- Supplemental, HybridSystem, DynamicBranch, and generation.jl getters
  migrated to 2-arg. print.jl `_show_accessor_value` uses the new
  IS.display_units_arg trait for display-units selection.
- Tests updated accordingly; relative-unit primitive tests moved to IS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regenerated against the updated IS template: unit-bearing structs now emit
only the 2-arg getter plus a `display_units_arg(::typeof(f), ::Type{T}) = SU`
line per field, keyed on both function and struct to avoid spooky
cross-type aliasing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public get_base_power(c, units) dispatches on NU/MW/SU/DU/Float64. The
unitless accessor is now _get_base_power (used as the internal conversion
anchor). Same pattern for System and for ThreeWindingTransformer winding
base powers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Descriptor now marks base_power fields exclude_getter=true so the generator
emits an internal _get_ accessor; the public unit-aware getter is hand-written.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow the IS CostCurve/FuelCurve parameterization: add U<:AbstractUnitSystem
as a type parameter on ImportExportCost, MarketBidCost, ReserveDemandCurve,
ImportExportTimeSeriesCost, MarketBidTimeSeriesCost, and
ReserveDemandTimeSeriesCurve. Struct fields pin the contained CostCurve to
that shared U; constructors infer and validate.

Replace UnitSystem enum usage in cost-function code with AbstractUnitSystem
type instances (NaturalUnit(), SystemBaseUnit(), DeviceBaseUnit()). The
system-level UnitSystem enum in base.jl / SystemUnitsSettings is left
untouched.

AnyCostCurve{T} alias is used only at isa checks, where existential form is
structurally required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So the IS branch this PSY work depends on is discoverable from the repo
rather than requiring local Pkg.develop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@luke-kiernan luke-kiernan marked this pull request as ready for review April 27, 2026 20:31
@luke-kiernan
Copy link
Copy Markdown
Contributor Author

luke-kiernan commented Apr 27, 2026

Change get_active_power etc. to be non-unitful. Move unitful ones to a different name. Setters are fine as-is.

Does mean for get-then-set you need:
set_active_power!(gen, get_active_power_unitful(gen, SU))

luke-kiernan and others added 2 commits May 6, 2026 16:06
Bare `get_active_power(g, NU)` now returns `100.0`;
`get_active_power_unitful(g, NU)` returns `100.0 MW`. Same pattern for
hand-written `get_base_power` and `get_base_power_{12,23,13}`.

Drop the redundant `Float64` getter dispatch path. Add NU dispatch to
`_convert_from_device_base` for `:mva`/`:ohm`/`:siemens`. Add
`IS._strip_units(::Unitful.Quantity)` so IS stays Unitful-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PSB on `psy6` calls 1-arg `get_base_power(::ThermalStandard)` (removed
on this branch), so the four new unit-aware testsets build their
fixtures manually via `_sys_with_thermal` instead of `PSB.build_system`.

Migrate `units/serialization.jl` and `test_units.jl` from JSON3 to JSON
to match the dropped dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant