Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 64 additions & 0 deletions app/controllers/api/v3/curve_metadata_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module Api
module V3
# Provides metadata about available hourly curves and annual exports.
#
# This controller serves as a discovery endpoint for clients to dynamically
# determine which curves and exports are available without hardcoding them.
# The metadata is sourced from CurveMetadataRegistry, which is populated by
# the CurvesController and ExportController classes during initialization.
#
# Design rationale:
# - Metadata lives alongside the controller actions that serve each curve
# - Clients can discover new curves without a code change on their side
#
# A curve's name is repeated across its route, action, serializer and
# registration;
class CurveMetadataController < BaseController
# GET /api/v3/curves/metadata
#
# Returns metadata about all available hourly output curves.
# Each curve includes:
# - name: The curve identifier (matches route and controller method)
# - type: The curve type (merit_curve, price_curve, capacity_curve, etc.)
# - description: Human-readable explanation of the curve contents
#
# Example response:
# {
# "hourly_outputs": [
# {
# "name": "electricity_profiles",
# "type": "merit_curve",
# "description": "Load on each participant in the electricity merit order"
# },
# ...
# ]
# }
def curves
render json: { hourly_outputs: CurveMetadataRegistry.all_curves }
end

# GET /api/v3/exports/metadata
#
# Returns metadata about all available annual exports.
# Each export includes:
# - name: The export identifier (matches route and controller method)
# - description: Human-readable explanation of the export contents
#
# Example response:
# {
# "annual_exports": [
# {
# "name": "energy_flow",
# "description": "Energy flows by carrier (future year)"
# },
# ...
# ]
# }
def exports
render json: { annual_exports: CurveMetadataRegistry.all_exports }
end
end
end
end
63 changes: 63 additions & 0 deletions app/controllers/api/v3/curves_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,69 @@ class CurvesController < BaseController
load_and_authorize_resource :scenario
before_action :merit_required

# Register all available curves with metadata
# These registrations provide the single source of truth for curve metadata
# exposed via the /api/v3/curves/metadata endpoint
CurveMetadataRegistry.register_curve(
:electricity_profiles,
type: :merit,
description: 'Load on each participant in the electricity merit order'
)

CurveMetadataRegistry.register_curve(
:electricity_price,
type: :price,
description: 'Hourly price of electricity according to the merit order'
)

CurveMetadataRegistry.register_curve(
:district_heating_profiles,
type: :load,
description: 'Load on each participant in the heat merit order'
)

CurveMetadataRegistry.register_curve(
:agriculture_heat,
type: :merit,
description: 'Load on each participant in the agriculture heat merit order'
)

CurveMetadataRegistry.register_curve(
:household_heat,
type: :fever,
description: 'Supply and demand of heat in households, including deficits and surpluses'
)

CurveMetadataRegistry.register_curve(
:buildings_heat,
type: :fever,
description: 'Supply and demand of heat in buildings, including deficits and surpluses'
)

CurveMetadataRegistry.register_curve(
:hydrogen_profiles,
type: :reconciliation,
description: 'Total demand and supply for hydrogen, with storage demand and supply'
)

CurveMetadataRegistry.register_curve(
:network_gas_profiles,
type: :reconciliation,
description: 'Total demand and supply for network gas, with storage demand and supply'
)

CurveMetadataRegistry.register_curve(
:residual_load,
type: :query,
description: 'Residual loads of various carriers'
)

CurveMetadataRegistry.register_curve(
:hydrogen_integral_cost,
type: :query,
description: 'Levelised costs, production costs per MWh and hourly production per hydrogen production technology'
)

def electricity_profiles
render_csv Curves::ElectricityCSVSerializer.new(
@scenario.gql.future_graph, :electricity, :merit_order,
Expand Down
53 changes: 53 additions & 0 deletions app/controllers/api/v3/export_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,59 @@ class ExportController < BaseController
authorize!(:read, @scenario)
end

# Register all available exports with metadata
# These registrations provide the single source of truth for export metadata
# exposed via the /api/v3/exports/metadata endpoint
CurveMetadataRegistry.register_export(
:energy_flow,
description: 'Energy flows by carrier (future year)'
)

CurveMetadataRegistry.register_export(
:energy_flow_present,
description: 'Energy flows by carrier (present year)'
)

CurveMetadataRegistry.register_export(
:molecule_flow,
description: 'Molecule/hydrogen flows'
)

CurveMetadataRegistry.register_export(
:sankey,
description: 'Sankey diagram data'
)

CurveMetadataRegistry.register_export(
:storage_parameters,
description: 'Storage capacity and parameters'
)

CurveMetadataRegistry.register_export(
:costs_parameters,
description: 'Cost breakdown by technology'
)

CurveMetadataRegistry.register_export(
:electricity_capacities,
description: 'Installed/peak capacity per participant in the electricity merit order'
)

CurveMetadataRegistry.register_export(
:hydrogen_capacities,
description: 'Installed/peak capacity for hydrogen participants'
)

CurveMetadataRegistry.register_export(
:network_gas_capacities,
description: 'Installed/peak capacity for network gas participants'
)

CurveMetadataRegistry.register_export(
:district_heating_capacities,
description: 'Installed/peak capacity per participant in the heat merit order'
)

# GET /api/v3/scenarios/:id/energy_flow
#
# Returns a CSV file containing the energetic inputs and outputs of every node in the future graph.
Expand Down
10 changes: 6 additions & 4 deletions app/serializers/causality_curves_csv_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
# The CSV contains the key of each node and the direction of energy
# flow (input or output) and the hourly load in MWh.
class CausalityCurvesCSVSerializer
# Single CSV row returned when a scenario has no time-resolved (merit/Fever)
# data. Shared by the curve and capacity serializers.
TIME_RESOLVED_DISABLED_MESSAGE =
'Merit order and time-resolved calculation are not enabled for this scenario'

# Provides support for multiple carriers in the serializer.
class Adapter
attr_reader :attribute, :carrier
Expand Down Expand Up @@ -47,10 +52,7 @@ def filename
# Returns an array of arrays.
def to_csv_rows
# Empty CSV if time-resolved calculations are not enabled.
unless @adapter.supported?(@graph)
return [['Merit order and time-resolved calculation are not ' \
'enabled for this scenario']]
end
return [[TIME_RESOLVED_DISABLED_MESSAGE]] unless @adapter.supported?(@graph)

CurvesCSVSerializer.new(
raw_columns,
Expand Down
3 changes: 1 addition & 2 deletions app/serializers/curves/district_heating_csv_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ def initialize(graph, conv_cust = nil)
def to_csv_rows
# Empty CSV if time-resolved calculations are not enabled.
unless Qernel::Plugins::Causality.enabled?(@graph)
return [['Merit order and time-resolved calculation are not ' \
'enabled for this scenario']]
return [[CausalityCurvesCSVSerializer::TIME_RESOLVED_DISABLED_MESSAGE]]
end

CurvesCSVSerializer.new(
Expand Down
10 changes: 10 additions & 0 deletions app/serializers/electricity_csv_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Hourly curve CSV for electricity merit order participants.
# Reuses MeritCSVSerializer filtering (producer/consumer types,
# curtailment exclusion, NodeCustomisation).
class ElectricityCSVSerializer < MeritCSVSerializer
def filename
:electricity_profiles
end
end
78 changes: 78 additions & 0 deletions app/services/curve_metadata_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

# Registry for curve and export metadata.
#
# Controllers declare their curves/exports here with a small DSL, so the metadata
# lives next to the actions that serve it and powers the /curves/metadata and
# /exports/metadata discovery endpoints.
module CurveMetadataRegistry
CURVE_TYPES = %i[merit price capacity load fever reconciliation query].freeze

class << self
# Registers a new hourly curve
#
# @param name [Symbol] The curve name (must match controller method and route)
# @param type [Symbol] The curve type (:merit, :price, :capacity, :load, :fever,
# :reconciliation, :query)
# @param description [String] Human-readable description of what the curve contains
def register_curve(name, type:, description:)
curves[name] = {
name: name.to_s,
type: normalize_type(type),
description: description
}
end

# Registers a new annual export
#
# @param name [Symbol] The export name (must match controller method and route)
# @param description [String] Human-readable description of what the export contains
def register_export(name, description:)
exports[name] = {
name: name.to_s,
description: description
}
end

# Returns all registered hourly curves as an array of hashes
#
# @return [Array<Hash>] Array of curve metadata with keys: name, type, description
def all_curves
curves.values
end

# Returns all registered annual exports as an array of hashes
#
# @return [Array<Hash>] Array of export metadata with keys: name, description
def all_exports
exports.values
end

# Clears all registrations
def clear!
curves.clear
exports.clear
end

private

def curves
@curves ||= {}
end

def exports
@exports ||= {}
end

# Stores a curve type symbol as its '<type>_curve' API string, e.g.
# :merit => 'merit_curve'.
def normalize_type(type)
unless CURVE_TYPES.include?(type)
raise ArgumentError,
"Unknown curve type: #{type}. Valid types: #{CURVE_TYPES.map(&:inspect).join(', ')}"
end

"#{type}_curve"
end
end
end
10 changes: 10 additions & 0 deletions config/initializers/curve_metadata_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

# Ensure CurvesController and ExportController are loaded to populate the registry
# This initializer guarantees that metadata registrations happen at application startup

Rails.application.config.to_prepare do
CurveMetadataRegistry.clear!
require_dependency 'api/v3/curves_controller'
require_dependency 'api/v3/export_controller'
end
27 changes: 15 additions & 12 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@
resources :areas, only: %i[index show]
resources :gqueries, only: :index

get 'curves/metadata', to: 'curve_metadata#curves'
get 'exports/metadata', to: 'curve_metadata#exports'

resources :scenarios, only: %i[index show create update destroy] do
member do
get :batch
get :energy_flow, to: 'export#energy_flow'
get :energy_flow_present, to: 'export#energy_flow_present'
get :molecule_flow, to: 'export#molecule_flow'
get :costs_parameters, to: 'export#costs_parameters'
get :sankey, to: 'export#sankey'
get :storage_parameters, to: 'export#storage_parameters'
get :direct_emissions_present, to: 'export#direct_emissions_present'
get :direct_emissions_future, to: 'export#direct_emissions_future'
get :electricity_capacities, to: 'export#electricity_capacities', as: :electricity_capacities_download
get :hydrogen_capacities, to: 'export#hydrogen_capacities', as: :hydrogen_capacities_download
get :network_gas_capacities, to: 'export#network_gas_capacities', as: :network_gas_capacities_download
get :district_heating_capacities, to: 'export#district_heating_capacities', as: :district_heating_capacities_download
get :energy_flow, to: 'export#energy_flow'
get :energy_flow_present, to: 'export#energy_flow_present'
get :molecule_flow, to: 'export#molecule_flow'
get :costs_parameters, to: 'export#costs_parameters'
get :sankey, to: 'export#sankey'
get :storage_parameters, to: 'export#storage_parameters'
get :direct_emissions_present, to: 'export#direct_emissions_present'
get :direct_emissions_future, to: 'export#direct_emissions_future'
get :electricity_capacities, to: 'export#electricity_capacities', as: :electricity_capacities_download
get :hydrogen_capacities, to: 'export#hydrogen_capacities', as: :hydrogen_capacities_download
get :network_gas_capacities, to: 'export#network_gas_capacities', as: :network_gas_capacities_download
get :district_heating_capacities, to: 'export#district_heating_capacities', as: :district_heating_capacities_download
get :merit
get :dump
put :dashboard
Expand Down
Loading