Skip to content

Type stability of cost-coefficient conversion: where it is, where it isn't #90

@luke-kiernan

Description

@luke-kiernan

The soon-to-be-opened "explicit units" PR (built on PSY lk/units-fold-psu and IS lk/units-domain-agnostic-is4) removes the Val{IS.UnitSystem.X}() cost-coefficient dispatch antipattern. This issue documents the resulting type-stability picture: which rungs of the ladder are now in effect, which are cheap follow-ups, and which require deeper architectural change.

The ladder

A call to add_variable_cost_to_objective!(container, T, component, cost_function, formulation) runs IS.convert_cost_coefficient for each cost term. How statically that conversion resolves depends on whether U (the unit-system type parameter on CostCurve{X, U} / FuelCurve{X, U}) reaches the conversion site at each rung.

  1. Val{}-on-runtime-enum (removed). Old code wrapped a runtime IS.UnitSystem enum value as a Val{} type tag at the helper call: a value-to-type boundary inside a hot loop, type-unstable by construction. Replaced by direct dispatch on IS.AbstractUnitSystem singleton instances, which the new IS.convert_cost_coefficient consumes via ordinary method dispatch.

  2. Per-call-site specialization (automatic). add_variable_cost_to_objective! dispatches on cost_function::IS.CostCurve{X} (UnionAll over U). Julia's JIT specializes the method body per concrete U it observes at runtime; inside each specialization U is statically known and convert_cost_coefficient(.., NaturalUnit(), ..) folds at compile time. The dynamic-dispatch cost is paid once at method entry — once per add_variable_cost_to_objective! call, not once per coefficient conversion. No source change needed to enable this; it follows automatically from putting U on the curve type.

  3. where U propagation (cheap, per-method). Any upstream method that wants to preserve concrete U through its body without depending on JIT specialization can write:

    function f(..., cost::CostCurve{X, U}) where {X, U}
        add_variable_cost_to_objective!(..., cost, ...)
    end

    The where clause introduces U as a bound type variable in f's signature; U is concrete inside f's specialization and propagates into the inner call. Adopt per-method where profiling suggests value.

  4. Small Union on the field (mechanical). Component fields typed as e.g. Union{MarketBidCost, MarketBidTimeSeriesCost, RenewableGenerationCost} (≤4 members) trigger Julia's union-splitting optimization. Each branch is monomorphic on cost flavor, eliminating the entry-point dynamic dispatch. (Within each branch U is still abstract — see (5).) Independent of the units work; revert from the abstract-supertype field that was adopted when time-variant cost types were split off.

  5. Union enumerating U too. 3 unit systems × 3 cost flavors = 9 union members. Past the splitting threshold; dead end.

  6. Component-level parameterization. ThermalStandard{U} with operation_cost::ThermalGenerationCost{U}. Full static recovery of U across all access paths, no JIT dependency. Largest cascade — propagates U to user-facing types and every site that constructs/holds a ThermalStandard. Justified only if profiling shows the entry-point dispatch is itself a bottleneck.

Current state and recommendations

  • (1) is fixed by the units PR.
  • (2) follows automatically; no further work.
  • (3) and (4) are cheap, independent improvements available now.
  • (5) is unreachable.
  • (6) is reserved for a profile-driven decision.

The expensive remaining boundary is the entry-point method-table lookup when cost_function arrives via an abstractly-typed field. That lookup happens once per add_variable_cost_to_objective! call. The conversion math itself is now compile-time-foldable inside the specialization, so the units refactor's payoff is real and immediate. If/when the entry dispatch shows up in a profile, (4) is the lowest-cost mitigation.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions