Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1811203
Initial plan
Copilot Mar 24, 2026
118c731
Add supports_active_power, supports_reactive_power, supports_voltage_…
Copilot Mar 24, 2026
2fc6c08
Make FACTS and SynchronousCondenser traits instance-based, checking c…
Copilot Mar 24, 2026
72065ed
Extract shared FACTS control_mode check into helper, add clarifying c…
Copilot Mar 24, 2026
18b43f6
Make supports_active_power instance-based for FACTSControlDevice (tru…
Copilot Mar 25, 2026
d1d6190
Allow SwitchedAdmittance to support reactive power (remove false over…
Copilot Mar 25, 2026
09b672a
Simplify SynchronousCondenser voltage control check to bustype only (…
Copilot Mar 25, 2026
0256834
Merge remote-tracking branch 'origin/main' into copilot/add-traits-to…
jd-lara May 29, 2026
7e32f55
Fix docs badge link
kdayday May 30, 2026
c10d160
Initial plan
Copilot May 31, 2026
84727a4
Merge pull request #1686 from Sienna-Platform/kd/badge
jd-lara May 31, 2026
9dc0d1a
add missing traits
jd-lara May 31, 2026
6d9b8da
fix: resolve broken SupplementalAttribute docs reference
Copilot May 31, 2026
c847a1c
Merge branch 'main' into copilot/fix-github-actions-build-job
jd-lara Jun 2, 2026
3b62f66
Merge pull request #1688 from Sienna-Platform/copilot/fix-github-acti…
jd-lara Jun 2, 2026
f60bec2
fix branch check bug
jd-lara Jun 5, 2026
fdefaef
Add units-management review integration brief (claude.md)
claude Jun 10, 2026
0346bf4
Merge pull request #1645 from Sienna-Platform/copilot/add-traits-to-s…
jd-lara Jun 10, 2026
e496f58
Merge remote-tracking branch 'origin/main' into claude/inspiring-john…
jd-lara Jun 10, 2026
a8af3d0
regenerate the structs
jd-lara Jun 10, 2026
e462a85
update per unit docs
jd-lara Jun 10, 2026
a3cba83
update deps
jd-lara Jun 10, 2026
8a6089c
update the structs
jd-lara Jun 10, 2026
ea90a4f
update the units handling
jd-lara Jun 10, 2026
4a1ccba
update tests deps
jd-lara Jun 10, 2026
ab2662c
update tests
jd-lara Jun 10, 2026
37aa913
Merge remote-tracking branch 'origin/psy6' into claude/inspiring-john…
jd-lara Jun 11, 2026
3fc219f
fix docs
jd-lara Jun 11, 2026
0155c49
claude added an extra end...
Jun 11, 2026
3e7a96b
Merge pull request #1696 from Sienna-Platform/ac/hybridsystem-strip-u…
jd-lara Jun 11, 2026
0f98a96
fix docs deps
jd-lara Jun 11, 2026
0e3deb1
add missing tests Luke asked for
jd-lara Jun 11, 2026
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
2 changes: 0 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
Expand All @@ -29,7 +28,6 @@ JSON = "^1.5"
LinearAlgebra = "1"
Logging = "1"
PrettyTables = "3.1"
StructTypes = "^1.9"
TimeSeries = "0.25"
Unicode = "1"
Unitful = "^1.12"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Main - CI](https://github.com/Sienna-Platform/PowerSystems.jl/workflows/Main%20-%20CI/badge.svg)](https://github.com/Sienna-Platform/PowerSystems.jl/actions/workflows/main-tests.yml)
[![codecov](https://codecov.io/gh/Sienna-Platform/PowerSystems.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Sienna-Platform/PowerSystems.jl)
[![Documentation](https://github.com/Sienna-Platform/PowerSystems.jl/actions/workflows/docs.yml/badge.svg)](https://github.com/Sienna-Platform/PowerSystems.jl/actions/workflows/docs.yml)
[![Documentation](https://github.com/Sienna-Platform/PowerSystems.jl/actions/workflows/docs.yml/badge.svg)](https://sienna-platform.github.io/PowerSystems.jl/stable/)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17703517.svg)](https://doi.org/10.5281/zenodo.17703517)
[<img src="https://img.shields.io/badge/slack-@Sienna/PSY-sienna.svg?logo=slack">](https://join.slack.com/t/core-sienna/shared_invite/zt-glam9vdu-o8A9TwZTZqqNTKHa7q3BpQ)
[![PowerSystems.jl Downloads](https://img.shields.io/badge/dynamic/json?url=http%3A%2F%2Fjuliapkgstats.com%2Fapi%2Fv1%2Ftotal_downloads%2FPowerSystems&query=total_requests&label=Downloads)](http://juliapkgstats.com/pkg/PowerSystems)
Expand Down
462 changes: 462 additions & 0 deletions claude.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e"
TypeTree = "04da0e3b-1cad-4b2c-a963-fc1602baf1af"

[sources]
InfrastructureSystems = {url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl.git", rev = "IS4"}
PowerSystemCaseBuilder = {url = "https://github.com/NREL-Sienna/PowerSystemCaseBuilder.jl", rev = "psy6"}

[compat]
CSV = "~0.10"
Documenter = "=1.15.0"
Expand Down
106 changes: 69 additions & 37 deletions docs/src/explanation/per_unit.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# [Per-unit Conventions](@id per_unit)

It is often useful to express power systems data in relative terms using per-unit conventions.
`PowerSystems.jl` supports the automatic conversion of data between three different unit systems:
`PowerSystems.jl` supports conversion of data between three different unit systems:

1. `"NATURAL_UNITS"`: The naturally defined units of each parameter (typically MW).
2. `"SYSTEM_BASE"`: Parameter values are divided by the system `base_power`.
3. `"DEVICE_BASE"`: Parameter values are divided by the device `base_power`.
1. `NU` (natural units): The naturally defined units of each parameter (typically MW).
2. `SU` (system base): Parameter values are divided by the system `base_power`.
3. `DU` (device base): Parameter values are divided by the device `base_power`.

`PowerSystems.jl` supports these unit systems because different power system tools and data
sets use different units systems by convention, such as:
Expand All @@ -15,39 +15,71 @@ sets use different units systems by convention, such as:
- Production cost modeling data is often gathered from variety of data sources,
which are typically defined in natural units

These three unit bases allow easy conversion between unit systems.
This allows `PowerSystems.jl` users to input data in the formats they have available,
as well as view data in the unit system that is most intuitive to them.

You can get and set the unit system setting of a `System` with [`get_units_base`](@ref) and
[`set_units_base_system!`](@ref). To support a less stateful style of programming,
`PowerSystems.jl` provides the `Logging.with_logger`-inspired "context manager"-type
function [`with_units_base`](@ref), which sets the unit system to a particular value,
performs some action, then automatically sets the unit system back to its previous value.

Conversion between unit systems does not change
the stored parameter values. Instead, unit system conversions are made when accessing
parameters using the [accessor functions](@ref dot_access), thus making it
imperative to utilize the accessor functions instead of the "dot" accessor methods to
ensure the return of the correct values. The units of the parameter values stored in each
struct are defined in `src/descriptors/power_system_structs.json`.

There are some unit system conventions in `PowerSystems.jl` when defining new components.
Currently, when you define components that aren't attached to a `System`,
you must define all fields in `"DEVICE_BASE"`, except for certain components that don't
have their own `base_power` rating, such as [`Line`](@ref)s, where the `rating` must be
defined in `"SYSTEM_BASE"`.

In the future, `PowerSystems.jl` hopes to support defining components in natural units.
For now, if you want to define data in natural units, you must first
set the system units to `"NATURAL_UNITS"`, define an empty component, and then use the
[accessor functions](@ref dot_access) (e.g., getters and setters), to define each field
within the component. The accessor functions will then do the data conversion from your
input data in natural units (e.g., MW or MVA) to per-unit.

By default, `PowerSystems.jl` uses `"SYSTEM_BASE"` because many optimization problems won't
converge when using natural units. If you change the unit setting, it's suggested that you
switch back to `"SYSTEM_BASE"` before solving an optimization problem (for example in
## Explicit units in accessors

As of PowerSystems 6, unit conversion is **explicit at every call site**: each unit-bearing
accessor takes a units argument, and each setter takes a unit-tagged value. There is no
system-wide mutable unit setting that changes what accessors return.

```julia
get_active_power(gen, SU) # bare Float64, system-base per-unit
get_active_power(gen, DU) # bare Float64, device-base per-unit
get_active_power(gen, NU) # bare Float64, natural units (MW)
get_active_power(gen, MW) # bare Float64 in an explicit Unitful unit
get_active_power_unitful(gen, SU) # unit-bearing value (RelativeQuantity / Unitful.Quantity)

set_active_power!(gen, 0.9 * SU) # values must carry their units
set_active_power!(gen, 90.0 * MW)
set_rating!(line, 1.2 * DU)
set_x!(transformer, 105.8 * OHMS) # impedance/admittance fields accept Ω / S
```

Conversion between unit systems does not change the stored parameter values — storage is
in device base (`DU`) for most fields. Conversions happen when accessing parameters
through the accessor functions, making it imperative to use the accessors instead of "dot"
field access. The units of the stored values for each struct are defined in
`src/descriptors/power_system_structs.json`.

Bare `Float64` arguments to converted setters are rejected with an `ArgumentError`: the
caller must say what units the number is in (`val * SU`, `val * DU`, `val * MW`, …). The
unit-tagged per-unit values are [`RelativeQuantity`](@ref)s, whose unit marker is carried
in the type; mixing `DU`- and `SU`-tagged values in arithmetic or comparisons raises a
clear error instead of producing a silently wrong number.

## Migration guide: stateful → explicit units

Code written against PowerSystems 5 used a mutable system-wide unit setting:

| PowerSystems 5 (stateful) | PowerSystems 6 (explicit) |
|:------------------------------------------------------------------------------- |:----------------------------------------------------------------------------------------------- |
| `set_units_base_system!(sys, "SYSTEM_BASE"); get_active_power(gen)` | `get_active_power(gen, SU)` |
| `with_units_base(sys, UnitSystem.NATURAL_UNITS) do; get_active_power(gen); end` | `get_active_power(gen, NU)` |
| `set_active_power!(gen, 0.9)` (interpreted via system setting) | `set_active_power!(gen, 0.9 * SU)` |
| `get_rating(line)` | `get_rating(line, SU)` |
| `scaling_factor_multiplier = get_max_active_power` (1-arg) | same name; the multiplier is invoked as `get_max_active_power(gen, units)` with `SU` by default |

Notes:

- Time-series retrieval passes a units argument to two-argument scaling-factor
multipliers; the default for PowerSystems components is `SU`. One-argument multipliers
(custom closures) are still invoked with the owner only.
- The `UnitSystem` enum (`get_units_base`, `set_units_base_system!`,
`with_units_base`) is system metadata only (shown in the `System` summary); it does
not affect any conversion.
- `CostCurve`/`FuelCurve` take the marker instances (`NaturalUnit()`,
`SystemBaseUnit()`, `DeviceBaseUnit()`) for `power_units`.

## Defining components

When you define components that aren't attached to a `System`, field values are stored as
given, in device base (`DU`), except for certain components that don't have their own
`base_power` rating, such as [`Line`](@ref)s, where values are relative to the system base
once attached. To define data in natural units, construct the component and then use the
explicit-units setters (e.g. `set_active_power!(gen, 90.0 * MW)`); the accessor does the
conversion to per-unit storage.

By default, downstream optimization packages work in `SU` because many optimization
problems won't converge when using natural units (for example in
[`PowerSimulations.jl`](https://sienna-platform.github.io/PowerSimulations.jl/stable/)).

!!! note
Expand Down
60 changes: 32 additions & 28 deletions docs/src/explanation/power_concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Base power is a fundamental parameter for the per-unit system and represents the
+ Rotor field winding limits
+ Cooling system capacity

- **Access**: Retrieved using `get_rating(device)`
- **Access**: Retrieved using `get_rating(device, units)` (the `units` argument is required, e.g. `get_rating(device, DU)`)

The rating is typically determined by the electrical design and thermal limits of the synchronous machine itself. It represents the maximum capability of the electrical generator, independent of the prime mover.

Expand All @@ -46,19 +46,19 @@ The rating is typically determined by the electrical design and thermal limits o
+ Boiler capacity (for steam generators)
+ Fuel flow limitations

- **Access**: Retrieved using `get_max_active_power(device)`
- **Access**: Retrieved using `get_max_active_power(device, units)` (the `units` argument is required, e.g. `get_max_active_power(device, DU)`)

The maximum active power is determined by the mechanical system that drives the generator. This is often less than the rating when considering only real power production.

## Key Distinctions

### Storage Convention Summary

| Concept | Storage Units | Accessor Function |
|:---------------- |:------------------- |:------------------------ |
| Base Power | Natural units (MVA) | `get_base_power()` |
| Rating | Device base (p.u.) | `get_rating()` |
| Max Active Power | Device base (p.u.) | `get_max_active_power()` |
| Concept | Storage Units | Accessor Function |
|:---------------- |:------------------- |:------------------------------------- |
| Base Power | Natural units (MVA) | `get_base_power(device)` |
| Rating | Device base (p.u.) | `get_rating(device, units)` |
| Max Active Power | Device base (p.u.) | `get_max_active_power(device, units)` |

### Physical Interpretation

Expand All @@ -84,38 +84,42 @@ In this example:

### Unit System Conversions

When you access these values through the PowerSystems.jl accessor functions, they are automatically converted based on the current unit system setting:
As of PowerSystems 6, unit conversion is **explicit at every call site**: each unit-bearing
accessor takes a `units` argument (`SU`, `DU`, `NU`, or an explicit `Unitful` unit such as
`MW`), and the value is converted accordingly. There is no system-wide mutable setting that
changes what accessors return. `base_power` is the exception — it is always in natural units
(MVA) and rejects per-unit (`SU`/`DU`) targets.

```julia
# Assuming base_power = 100 MVA, rating = 1.0 p.u., max_active_power = 0.95 p.u.
# Assuming base_power = 100 MVA, rating = 1.0 p.u. (device base), max_active_power = 0.95 p.u. (device base)
sys = System(100.0) # System base power = 100 MVA
gen = get_component(ThermalStandard, sys, "gen1")

# In DEVICE_BASE
set_units_base_system!(sys, "DEVICE_BASE")
get_base_power(gen) # Returns: 100.0 MVA (always natural units)
get_rating(gen) # Returns: 1.0 p.u. (on device base)
get_max_active_power(gen) # Returns: 0.95 p.u. (on device base)

# In NATURAL_UNITS
set_units_base_system!(sys, "NATURAL_UNITS")
get_base_power(gen) # Returns: 100.0 MVA (always natural units)
get_rating(gen) # Returns: 100.0 MVA (converted from p.u.)
get_max_active_power(gen) # Returns: 95.0 MW (converted from p.u.)

# In SYSTEM_BASE
set_units_base_system!(sys, "SYSTEM_BASE")
get_base_power(gen) # Returns: 100.0 MVA (always natural units)
get_rating(gen) # Returns: 1.0 p.u. (on system base, assuming system base = device base)
get_max_active_power(gen) # Returns: 0.95 p.u. (on system base)
# Base power is always natural units (MVA); it accepts only `NU` / power-Unitful targets
get_base_power(gen) # Returns: 100.0 (MVA, always natural units)
get_base_power(gen, NU) # Returns: 100.0 (MVA)
# get_base_power(gen, DU) # ERROR: per-unit bases are not valid for base_power

# Device base (`DU`)
get_rating(gen, DU) # Returns: 1.0 (p.u. on device base)
get_max_active_power(gen, DU) # Returns: 0.95 (p.u. on device base)

# Natural units (`NU`)
get_rating(gen, NU) # Returns: 100.0 (MVA)
get_max_active_power(gen, NU) # Returns: 95.0 (MW)

# System base (`SU`) — here the system base equals the device base
get_rating(gen, SU) # Returns: 1.0 (p.u. on system base)
get_max_active_power(gen, SU) # Returns: 0.95 (p.u. on system base)
```

!!! note

Base power is **always** returned in natural units (MVA) regardless of the unit system setting. The rating and maximum active power are stored in device base but are automatically converted when accessed based on the current unit system setting.
`base_power` is **always** in natural units (MVA) and rejects `SU`/`DU` targets — it is the
anchor that every other field's per-unitization is defined against. Rating and maximum active
power are stored in device base and converted to whatever `units` you request at the call site.

## See Also

- [Per-unit Conventions](@ref per_unit) - Detailed explanation of unit systems in PowerSystems.jl
- [`ThermalStandard`](@ref) - Generator type with these power parameters
- [`get_units_base`](@ref) and [`set_units_base_system!`](@ref) - Functions for managing unit systems
57 changes: 25 additions & 32 deletions docs/src/how_to/add_component_natural_units.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,23 @@ using PowerSystemCaseBuilder #hide
system = build_system(PSISystems, "modified_RTS_GMLC_DA_sys"); #hide
```

`PowerSystems.jl` has [three per-unitization options](@ref per_unit) for getting, setting
and displaying data.
`PowerSystems.jl` has [three per-unitization options](@ref per_unit) for getting and setting
data, selected explicitly at each call site by a units argument.

Currently, only one of these options -- `"DEVICE_BASE"` -- is supported when using a
constructor function define a component. You can see
[an example of the default capabilities using `"DEVICE_BASE"` here](@ref "Adding Loads and Generators").
Constructors define a component's numeric fields in **device base** (`DU`): bare numbers
passed to a constructor are interpreted as per-unit on the device's own `base_power`. You can
see [an example of defining a component this way here](@ref "Adding Loads and Generators").

We hope to add capability to define components in
`"NATURAL_UNITS"` with constructors in the future, but for now, below is a workaround
for users who prefer to define data using `"NATURAL_UNITS"` (e.g., MW, MVA, MVAR, or MW/min):
If you prefer to define data in **natural units** (e.g., MW, MVA, MVAR, or MW/min), pass
unit-tagged values to the "setter" functions after constructing the component — the setters
convert to the stored device-base representation for you. There is no longer a system-wide
unit setting to toggle (see [Per-unit Conventions](@ref per_unit)).

### Step 1: Set Units Base

Set your (previously-defined) `System`'s units base to `"NATURAL_UNITS"`:

```@repl add_in_nu
set_units_base_system!(system, "NATURAL_UNITS")
```

Now, the "setter" functions have been switched to define data using natural units (MW, MVA,
etc.), taking care of the necessary data conversions behind the scenes.

### Step 2: Define Empty Component
### Step 1: Define Empty Component

Define an empty component with `0.0` or `nothing` for all the power-related fields except
`base_power`, which is always in MVA.
`base_power`, which is always in MVA. (Bare numbers in the constructor are device-base
per-unit; here every power field starts at `0.0`.)

For example:

Expand All @@ -56,32 +47,34 @@ gas1 = ThermalStandard(;
);
```

### Step 3: Attach the Component
### Step 2: Attach the Component

Attach the component to your `System`:

```@repl add_in_nu
add_component!(system, gas1)
```

### Step 4: Add Data with "setter" Functions
### Step 3: Add Data with "setter" Functions

Use individual "setter" functions to set each the value of each numeric field in natural
units:
Use individual "setter" functions, passing **unit-tagged** natural-units values (`MW`,
`Mvar`, etc.). The setters convert each value to device base behind the scenes:

```@repl add_in_nu
set_rating!(gas1, 30.0) #MVA
set_active_power_limits!(gas1, (min = 6.0, max = 30.0)) # MW
set_reactive_power_limits!(gas1, (min = 6.0, max = 30.0)) # MVAR
set_ramp_limits!(gas1, (up = 6.0, down = 6.0)) #MW/min
set_rating!(gas1, 30.0 * MVA)
set_active_power_limits!(gas1, (min = 6.0 * MW, max = 30.0 * MW))
set_reactive_power_limits!(gas1, (min = 6.0 * Mvar, max = 30.0 * Mvar))
set_ramp_limits!(gas1, (up = 6.0 * MW, down = 6.0 * MW)) # ramp limits per-unitize by base_power
```

Notice the return values are divided by the `base_power` of 30 MW, showing the setters have
done the per-unit conversion into `"DEVICE_BASE"` behind the scenes.
A bare number (e.g. `set_rating!(gas1, 30.0)`) is rejected with an `ArgumentError`: setters
require the value to carry its units. Reading the values back in device base
(`get_rating(gas1, DU)`) shows them divided by the `base_power` of 30 MVA — the per-unit
conversion the setters performed.

!!! tip

Steps 2-4 can be called within a `for` loop to define many components at once (or step 3
Steps 1-3 can be called within a `for` loop to define many components at once (or step 2
can be replaced with [`add_components!`](@ref) to add all components at once).

#### See Also
Expand Down
2 changes: 0 additions & 2 deletions docs/src/how_to/create_hydro_datasets.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import PowerSystems as PSY

# Create a system
sys = System(100.0)
set_units_base_system!(sys, "NATURAL_UNITS")

# Create and add a bus
bus = ACBus(;
Expand Down Expand Up @@ -92,7 +91,6 @@ set_downstream_turbine!(reservoir, turbine)
```julia

sys = System(100.0)
set_units_base_system!(sys, "NATURAL_UNITS")

# Create and add a bus
bus = ACBus(;
Expand Down
Loading
Loading