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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions