Skip to content
Merged
23 changes: 14 additions & 9 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,17 @@ PowerFlows = "94fada2c-fd9a-4e89-8d82-81405f5cb4f6"
# LOCAL PATH CO-DEV PIN (temporary): POM's IS4 git pin resolved to an older commit
# than PSY's local checkout expects (TimeSeriesKey UnionAll mismatch on load).
# Revert to the git rev pin once PSY's local changes land on IS4.
InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
# TEMPORARY: the last IS4 commit before its OpenAPI 1.x move (2026-09-10), which needs the same
# unpushed OpenAPI model packages.
InfrastructureSystems = {rev = "04af380f8a013b150ddf7e0d8ca812ad9e24812b", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
InfrastructureOptimizationModels = {rev = "jd/market_models2", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
# LOCAL PATH CO-DEV PIN (temporary): the variable -> variable_operation_cost cost-field
# rename on PSY's generated cost types is uncommitted on disk, not on the psy6 branch yet.
# Revert to the git rev pin once pushed/merged.
PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
# TEMPORARY: psy6 as of the PowerSystems #1784 squash merge (2026-09-10). It already carries #1783
# (must_run removed, status enum), which POM's feedforwards and the pushed OpenAPI model packages do
# not follow yet, and the psy6 head has since moved to OpenAPI 1.x against unpushed model packages.
PowerSystems = {rev = "f75b4418c2928ff7efd6d110c6dce40c401f874e", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
# TEMPORARY: pinned to the arc-keyed-catalog branch (PNM #356 + #357) until they merge to
# psy6. Revert to `rev = "psy6"` once they land.
PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"}
Expand All @@ -50,13 +55,13 @@ PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/
# PSY's own sources). POM imports none of them: they sit in [deps] only because Pkg
# rejects a [sources] entry for a package absent from [deps]. Mirrors PSY's own
# Project.toml. Temporary until the OpenAPI packages are registered.
PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerCoreOpenAPIModels.jl"}
InfrastructureTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"}
InfrastructureCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "InfrastructureCoreOpenAPIModels.jl"}
PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerDynamicsOpenAPIModels.jl"}
PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerInvestmentsOpenAPIModels.jl"}
PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOpenAPIModels.jl"}
PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "main", subdir = "PowerOperationsOpenAPIModels.jl"}
PowerCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "PowerCoreOpenAPIModels.jl"}
InfrastructureTimeSeriesOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"}
InfrastructureCoreOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "InfrastructureCoreOpenAPIModels.jl"}
PowerDynamicsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "PowerDynamicsOpenAPIModels.jl"}
PowerInvestmentsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "PowerInvestmentsOpenAPIModels.jl"}
PowerOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "PowerOpenAPIModels.jl"}
PowerOperationsOpenAPIModels = {url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", rev = "rh/update_enums", subdir = "PowerOperationsOpenAPIModels.jl"}

[extensions]
PowerFlowsExt = "PowerFlows"
Expand Down
24 changes: 14 additions & 10 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,25 @@ PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"

[sources]
InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
# TEMPORARY (mirrors the root Project.toml): last IS4 commit before its OpenAPI 1.x move.
InfrastructureSystems = {rev = "04af380f8a013b150ddf7e0d8ca812ad9e24812b", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
# TEMPORARY: psy6 as of the PowerSystems #1784 squash merge (2026-09-10). It already carries #1783
# (must_run removed, status enum), which POM's feedforwards and the pushed OpenAPI model packages do
# not follow yet, and the psy6 head has since moved to OpenAPI 1.x against unpushed model packages.
PowerSystems = {rev = "f75b4418c2928ff7efd6d110c6dce40c401f874e", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
# Still-unmerged market-model work; not yet an ancestor of IOM's main.
InfrastructureOptimizationModels = {rev = "jd/market_model", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
InfrastructureOptimizationModels = {rev = "jd/market_models2", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"}
# PSY psy6 depends on these unregistered packages; [sources] of non-root projects are
# ignored by Pkg, so every environment resolving PSY must pin them itself (same rev as
# PSY's own sources). Temporary until the OpenAPI packages are registered.
PowerCoreOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerCoreOpenAPIModels.jl"}
InfrastructureTimeSeriesOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"}
InfrastructureCoreOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "InfrastructureCoreOpenAPIModels.jl"}
PowerDynamicsOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerDynamicsOpenAPIModels.jl"}
PowerInvestmentsOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerInvestmentsOpenAPIModels.jl"}
PowerOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerOpenAPIModels.jl"}
PowerOperationsOpenAPIModels = {rev = "main", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerOperationsOpenAPIModels.jl"}
PowerCoreOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerCoreOpenAPIModels.jl"}
InfrastructureTimeSeriesOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "InfrastructureTimeSeriesOpenAPIModels.jl"}
InfrastructureCoreOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "InfrastructureCoreOpenAPIModels.jl"}
PowerDynamicsOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerDynamicsOpenAPIModels.jl"}
PowerInvestmentsOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerInvestmentsOpenAPIModels.jl"}
PowerOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerOpenAPIModels.jl"}
PowerOperationsOpenAPIModels = {rev = "rh/update_enums", url = "https://github.com/Sienna-Platform/PowerOpenAPIModels.git", subdir = "PowerOperationsOpenAPIModels.jl"}

[compat]
Documenter = "^1.0"
Expand Down
2 changes: 2 additions & 0 deletions src/PowerOperationsModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,8 @@ export RampConstraint
export RampLimitConstraint
export CopperPlateBalanceConstraint
export SettlementBalanceConstraint
export BlockBidQuantityConstraint
export BlockBidLinkConstraint
export ClearedPositionConstraint
export ActiveRangeICConstraint
export NodalBalanceActiveConstraint
Expand Down
25 changes: 22 additions & 3 deletions src/common_models/market_bid_plumbing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ get_offer_curves(::IOM.DecrementalOffer, op_cost::PSY.OfferCurveCost) =
get_offer_curves(::IOM.IncrementalOffer, op_cost::PSY.OfferCurveCost) =
get_output_offer_curves(op_cost)

# `IncrementalBidOffer` reads the same incremental side as `IncrementalOffer`; only the
# objective sign and the expected curvity differ (see the type's docstring).
get_offer_curves(
::IncrementalBidOffer,
device::OFFER_CURVE_COMPONENTS,
) = get_output_offer_curves(IOM.get_operation_cost(device))
get_offer_curves(::IncrementalBidOffer, op_cost::PSY.OfferCurveCost) =
get_output_offer_curves(op_cost)

# The rest of IOM's OfferDirection dispatch table for the bid direction: incremental
# parameter/variable/constraint types, a benefit sign, and the concavity the negative sign
# requires of the curve.
Base.string(::IncrementalBidOffer) = "incremental bid"
IOM._slope_param(::IncrementalBidOffer) = IncrementalPiecewiseLinearSlopeParameter
IOM._breakpoint_param(::IncrementalBidOffer) = IncrementalPiecewiseLinearBreakpointParameter
IOM._block_offer_var(::IncrementalBidOffer) = PiecewiseLinearBlockIncrementalOffer
IOM._block_offer_constraint(::IncrementalBidOffer) =
PiecewiseLinearBlockIncrementalOfferConstraint
IOM._objective_sign(::IncrementalBidOffer) = OBJECTIVE_FUNCTION_NEGATIVE
IOM.curvity_check(::IncrementalBidOffer, x) = IS.is_concave(x)
IOM.expected_curvity(::IncrementalBidOffer) = "concave"

# direction and operating reserve demand curve (ORDC) reserve service: the curve is on the
# `variable` field (a CostCurve, static or time-series-backed) rather than split
# across incremental/decremental sides. Direction is irrelevant to the lookup; the
Expand Down Expand Up @@ -172,9 +194,6 @@ _has_market_bid_cost(device::OFFER_CURVE_COMPONENTS) =
_has_market_bid_cost(::MBC_TYPES) = true
_has_market_bid_cost(::PSY.OperationalCost) = false

"Curve-clearing style (`PSY.CurveStyles`) for a `MarketBidCost`/`MarketBidTimeSeriesCost` bid."
_curve_style(cost::MBC_TYPES) = PSY.get_curve_style(cost)

_has_import_export_cost(::PSY.StaticInjection) = false
_has_import_export_cost(device::PSY.Source) =
_has_import_export_cost(IOM.get_operation_cost(device))
Expand Down
15 changes: 15 additions & 0 deletions src/core/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ keeps its own parameters/slacks): the settlement row carries only bid variables.
"""
struct SettlementBalanceConstraint <: ConstraintType end

"""
Quantity row of a FIXED-`curve_style` market bid, one per (component, direction, period):
`p[c, t] - Q[c, t] z[c, t] == 0`, with `p` the award (`ActivePowerOutVariable` /
`ActivePowerInVariable`), `Q` the period's offer quantity and `z` its
[`BlockBidCommitmentVariable`](@ref). The bid clears its whole quantity or nothing.
"""
struct BlockBidQuantityConstraint <: ConstraintType end

"""
Link row of a MULTI_STEP-`curve_multistep` market bid between two consecutive periods of
one block: `p[c, t] - p[c, t + 1] == 0`. A block is a run of consecutive periods whose
offer curves are identical; the rows are keyed by the first period of each pair, sparse.
"""
struct BlockBidLinkConstraint <: ConstraintType end

"""
`ClearedPositionVariable[loc, t] == AggregateClearedInjection[loc, t]` — one row per
settlement location per timestep, never per bus.
Expand Down
17 changes: 11 additions & 6 deletions src/core/formulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,11 @@ Formulation for `PSY.VirtualParticipant` market components. Adds `ActivePowerOut
bounded by `max_supply`/`max_demand`, and adds both directly to the single system-wide
`SettlementBalance` row (+out, -in) — never to a physical `ActivePowerBalance` row.
`settlement_point`/`trading_hubs` are recorded on the component but unresolved here: with
one settlement row, location is moot. `PSY.CurveStyles` selects the bid's shape: CURVE gets
a fresh divisible variable per period, VARIABLE one shared divisible variable across the
horizon, and FIXED a shared binary [`BlockBidCommitmentVariable`](@ref).
one settlement row, location is moot. `PSY.CurveStyles` selects the bid's shape: VARIABLE
clears a divisible quantity per period on the PWL path, FIXED clears the period's whole
quantity or nothing through a per-period binary [`BlockBidCommitmentVariable`](@ref).
`PSY.CurveMultiStep.MULTI_STEP` links consecutive periods with identical offers into one
block ([`BlockBidLinkConstraint`](@ref)).

Simultaneous nonzero `ActivePowerOutVariable` and `ActivePowerInVariable` on the same
participant (self-crossing) is permitted, mirroring real DAM virtual-bidding rules: an
Expand All @@ -167,9 +169,12 @@ equality with no nodal effect.
struct AggregateBalance <: AbstractDeviceFormulation end

"""
Formulation for `PSY.PointToPointBid`: one [`ClearedTransferVariable`](@ref) per bid, written
as −q into the `from` location's [`AggregateClearedInjection`](@ref) and +q into the `to`
location's. Excluded from `SettlementBalance`: its two settlement terms would cancel exactly.
Formulation for `PSY.PointToPointBid`: one [`ClearedTransferVariable`](@ref) per bid and
period, written as −q into the `from` location's [`AggregateClearedInjection`](@ref) and +q
into the `to` location's. Excluded from `SettlementBalance`: its two settlement terms would
cancel exactly. A MULTI_STEP spread bid clears one MW across every period of a block of
identical curves ([`BlockBidLinkConstraint`](@ref)); FIXED is rejected, a spread bid is
always divisible.
"""
struct SpreadBid <: AbstractDeviceFormulation end

Expand Down
15 changes: 15 additions & 0 deletions src/core/market_formulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,18 @@ balance. The physical balance keeps its `StaticPowerLoad` parameters and gains z
settlement equality — not the physical slack — is the binding accounting identity.
"""
struct SettlementMarket <: IOM.AbstractMarketModel end

"""
Offer direction for a willingness-to-pay instrument whose curve is authored on the
incremental side. It reads the incremental parameters, variables and constraints exactly as
`IOM.IncrementalOffer` does, but enters the minimized objective with a **negative** sign:
the curve is a benefit, not a cost, so the instrument clears up to the quantity where its
marginal bid price meets the cleared price rather than only when it relieves congestion.

A spread bid is the motivating case. PSY stores `spread_bid` on the incremental side only,
so the side is fixed by the data contract while the sign is fixed by the economics, and the
two differ. Because the sign is negative, the delta PWL relaxation is exact only for a
non-increasing bid curve, so the expected curvity is concave (a single-segment curve, the
usual shape for a point-to-point bid, satisfies both).
"""
struct IncrementalBidOffer <: IOM.OfferDirection end
8 changes: 4 additions & 4 deletions src/core/variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ struct LiftVariable <: VariableType end

"""
Binary block-bid commitment variable (``z``) for a FIXED-`curve_style` market bid
(`PSY.CurveStyles.FIXED`): one JuMP variable per (component, direction), reused unscaled
at every period of the horizon in both the settlement row and the objective. The block
clears at its full MW envelope in every period when ``z = 1``, or not at all when
``z = 0`` — an all-or-nothing decision across the whole bid period.
(`PSY.CurveStyles.FIXED`): one per (component, direction, period). The award equals the
period's offer quantity when ``z = 1`` and zero when ``z = 0``
([`BlockBidQuantityConstraint`](@ref)); ``z`` prices the block in the objective and never
enters the settlement row itself.
"""
struct BlockBidCommitmentVariable <: VariableType end

Expand Down
Loading
Loading