Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ gem 'ruby-progressbar'

# own gems
gem 'quintel_merit', ref: 'aae77e0', github: 'quintel/merit'
gem 'atlas', ref: '21a6046', github: 'quintel/atlas'
gem 'atlas', ref: '5504fac', github: 'quintel/atlas'
gem 'fever', ref: '2afebd1', github: 'quintel/fever'
gem 'refinery', ref: '36b8e34', github: 'quintel/refinery'
gem 'rubel', ref: '9fe7010', github: 'quintel/rubel'
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
GIT
remote: https://github.com/quintel/atlas.git
revision: 21a60468b711e700ec2964a4f3c9c942f5ba3c7b
ref: 21a6046
revision: 5504fac349b1b25fda2ef285b044b025fbfbdb1e
ref: 5504fac
specs:
atlas (1.0.0)
activemodel (>= 7)
Expand Down
20 changes: 11 additions & 9 deletions app/models/gql/runtime/functions/lookup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ def MSECTOR(*keys)

# Returns an Array with {Qernel::Node} for given energy use.
#
# See Qernel::Node::USES
#
# Examples
#
# USE(energetic)
Expand Down Expand Up @@ -321,16 +319,17 @@ def DATASET_CURVE(key)
# Returns an attribute {Qernel::Emissions} or {Qernel::Emissions::ScopedSector} or a value.
#
# Emissions data is loaded from CSV files in ETSource with the following structure:
# etm_sector, etm_subsector, use, ghg, unit, value
# etm_sector, etm_subsector, use, ghg, year, unit, value
#
# Parameters:
# - sector: ETM sector name (e.g., 'households', 'buildings_non_specified')
# Dashes/dots in sector names are converted to underscores for key generation
# - use: Emission use type (energetic, non_energetic) - REQUIRED when accessing values
# - ghg: GHG type (co2, other_ghg) - optional
# - year: Year of emission (e.g., 1990) - optional, reads from emissions_YEAR.csv files
# - year: Pass 1990 to read the historic baseline; the present year is
# implicit and needs no argument
#
# Key generation combines: sector_[subsector_]use_ghg[_year]
# Key generation combines: sector_[subsector_]use_ghg[_1990]
# Note: Unit column from CSV is not included in keys, blank values return nil
#
# EMISSIONS() without any keys returns {Qernel::Emissions}
Expand All @@ -351,8 +350,8 @@ def DATASET_CURVE(key)
# EMISSIONS(sector, use, ghg) or EMISSIONS(sector, use, ghg, year) returns an emission value
#
# Examples
# EMISSIONS(households, energetic, other_ghg) # => 12.0 (from emissions.csv)
# EMISSIONS(households, energetic, co2, 1990) # => value (from emissions_1990.csv)
# EMISSIONS(households, energetic, other_ghg) # => 12.0 (present year)
# EMISSIONS(households, energetic, co2, 1990) # => value for 1990
# EMISSIONS(buildings_non_specified, energetic, other_ghg) # => 18.0
#
def EMISSIONS(*keys)
Expand All @@ -364,8 +363,11 @@ def EMISSIONS(*keys)
# EMISSIONS(sector, use) -> return ScopedSector for UPDATE operations
return scope.graph.emissions.scope(keys.join('_').to_sym) if keys.size == 2

# EMISSIONS(sector, use, ghg [, year]) -> return value
scope.graph.emissions[keys.join('_').to_sym]
# EMISSIONS(sector, use, ghg [, year]) -> return value.
# The present year is implicit; only 1990 is suffixed with a year.
scope.graph.emissions[
[*keys.first(3), (1990 if keys[3].to_i == 1990)].compact.join('_').to_sym
]
end
end
end
Expand Down
66 changes: 45 additions & 21 deletions app/models/qernel/emissions.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
module Qernel
# Class for getting and setting emissions data
# Class for getting and setting emissions data.
# Behaves much like Qernel::Area, can be seen as an extension of
# area attributes, scoped for emissions
# area attributes, scoped for emissions.
#
# == Data Structure
#
# Emissions data is loaded from CSV files in ETSource with structure:
# etm_sector, etm_subsector, use, ghg, unit, value
# etm_sector, etm_subsector, use, ghg, year, unit, value
#
# Values are stored flat, keyed as: sector_subsector_use_ghg[_1990]
# Examples:
# - buildings_non_specified_energetic_other_ghg
# - energy_electricity_and_heat_production_energetic_co2_1990
#
# == Year Handling
#
# Keys are generated as: sector_[subsector_]use_ghg[_year]
# Example: buildings_non_specified_energetic_co2
# The present year is implicit and carries no suffix. Only the historic
# 1990 baseline is suffixed with its year, so both can coexist in the
# same dataset.
class Emissions
include DatasetAttributes

Expand All @@ -20,12 +30,13 @@ class Emissions
# statements in etsource
#
# Example:
# EMISSIONS(households, energetic) returns a ScopedSector
# EMISSIONS(households_non_specified, energetic) returns a ScopedSector
# Then UPDATE can call: scoped.co2 = 100.0
class ScopedSector
def initialize(emissions, scope)
def initialize(emissions, scope, year = nil)
@emissions = emissions
@scope = scope
@year = year
end

def [](attr_name)
Expand All @@ -41,29 +52,39 @@ def inspect
end

def scoped_method(method_name)
"#{@scope}_#{method_name}"
attr = method_name.to_s.delete_suffix('=')
@year.to_i == 1990 ? "#{@scope}_#{attr}_1990" : "#{@scope}_#{attr}"
end

def respond_to_missing?(method_name, include_private = false)
data_key = scoped_method(method_name).split('=').first

@emissions.respond_to?(data_key) || super
if method_name.to_s.end_with?('=')
scope_exists?
else
@emissions.respond_to?(scoped_method(method_name)) || super
end
end

def method_missing(method_name, *args)
data_key = scoped_method(method_name).split('=').first.to_sym
key = scoped_method(method_name).to_sym

# Validate the key exists for both getters and setters
unless @emissions.respond_to?(data_key)
raise NoMethodError, "undefined method `#{method_name}' for #{inspect}"
end
if method_name.to_s.end_with?('=')
unless scope_exists?
raise NoMethodError, "undefined method `#{method_name}' for #{inspect}"
end

if data_key.to_s == scoped_method(method_name)
@emissions[data_key]
@emissions[key] = args.first
else
@emissions[data_key] = args.first
@emissions[key]
end
end

private

# Scoped writes work for any region that has the scope.
def scope_exists?
prefix = "#{@scope}_"
@emissions.dataset_attributes.keys.any? { |key| key.to_s.start_with?(prefix) }
end
end

def initialize(graph = nil)
Expand All @@ -74,9 +95,12 @@ def initialize(graph = nil)

# Public: define the sector scope for access to the hashed emission keys
#
# sector - Scope identifier (e.g., :buildings_non_specified_energetic).
# year - Optional year. Pass 1990 to target the historic baseline.
#
# Returns a scoped version of the emissions data
def scope(sector)
ScopedSector.new(self, sector)
def scope(sector, year = nil)
ScopedSector.new(self, sector, year)
end
end
end
22 changes: 22 additions & 0 deletions app/models/qernel/node_api/direct_emissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,28 @@ def direct_reporting_emissions_total_ghg_emissions
end
end

# CO2 utilisation at this node.
#
# Fossil CO2 input utilisation.
#
# @return [Float, nil] CO2 utilisation in kg, or nil if node is not in emissions group
def direct_reporting_emissions_co2_utilisation
with_emissions_node do
direct_co2_input_utilisation_fossil
end
end

# Biogenic CO2 emissions at this node.
#
# Biogenic CO2 emissions from production.
#
# @return [Float, nil] Biogenic CO2 emissions in kg, or nil if node is not in emissions group
def direct_reporting_emissions_biogenic_co2_emissions
with_emissions_node do
direct_co2_output_production_emissions_biogenic
end
end

private

# Yields the given block only if the node belongs to the :emissions group.
Expand Down
22 changes: 22 additions & 0 deletions app/models/qernel/node_api/molecule_emissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,28 @@ def direct_reporting_emissions_total_ghg_emissions
end
end

# CO2 utilisation at this node.
#
# Fossil CO2 input utilisation.
#
# @return [Float, nil] CO2 utilisation in kg, or nil if node is not in emissions group
def direct_reporting_emissions_co2_utilisation
with_emissions_node do
0.0
end
end

# Biogenic CO2 emissions at this node.
#
# Biogenic CO2 emissions from production.
#
# @return [Float, nil] Biogenic CO2 emissions in kg, or nil if node is not in emissions group
def direct_reporting_emissions_biogenic_co2_emissions
with_emissions_node do
0.0
end
end

private

# Yields the given block only if the node belongs to the :emissions group.
Expand Down
28 changes: 19 additions & 9 deletions spec/fixtures/etsource/datasets/nl/emissions.csv
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
etm_sector,etm_subsector,use,ghg,unit,value
Energy,Electricity and heat production,energetic,other_ghg,kg CO2eq,18.0
Energy,Fugitive emissions,non_energetic,co2,kg CO2eq,20.0
Households,Non-specified,energetic,other_ghg,kg CO2eq,7.0
Buildings,Non-specified,energetic,other_ghg,kg CO2eq,2796620.0
Industry,Non-specified,energetic,other_ghg,kg CO2eq,100.0
Agriculture,Non-specified,energetic,other_ghg,kg CO2eq,50.0
Agriculture,Non-specified,non_energetic,co2,kg CO2eq,75.0
Waste,Non-specified,non_energetic,co2,kg CO2eq,25.0
etm_sector,etm_subsector,use,ghg,year,unit,value
Energy,Electricity and heat production,energetic,other_ghg,2023,kg CO2eq,18.0
Energy,Electricity and heat production,energetic,other_ghg,1990,kg CO2eq,25.0
Energy,Fugitive emissions,non_energetic,co2,2023,kg CO2eq,20.0
Energy,Fugitive emissions,non_energetic,co2,1990,kg CO2eq,30.0
Households,Non-specified,energetic,other_ghg,2023,kg CO2eq,7.0
Households,Non-specified,energetic,other_ghg,1990,kg CO2eq,10.0
Buildings,Non-specified,energetic,other_ghg,2023,kg CO2eq,2796620.0
Buildings,Non-specified,energetic,other_ghg,1990,kg CO2eq,3000000.0
Industry,Non-specified,energetic,other_ghg,2023,kg CO2eq,100.0
Industry,Non-specified,energetic,other_ghg,1990,kg CO2eq,150.0
Agriculture,Non-specified,energetic,other_ghg,2023,kg CO2eq,50.0
Agriculture,Non-specified,energetic,other_ghg,1990,kg CO2eq,75.0
Agriculture,Non-specified,non_energetic,other_ghg,2023,kg CO2eq,100.0
Agriculture,Non-specified,non_energetic,other_ghg,1990,kg CO2eq,125.0
Agriculture,Non-specified,non_energetic,co2,2023,kg CO2eq,75.0
Agriculture,Non-specified,non_energetic,co2,1990,kg CO2eq,100.0
Waste,Non-specified,non_energetic,co2,2023,kg CO2eq,25.0
Waste,Non-specified,non_energetic,co2,1990,kg CO2eq,35.0
14 changes: 14 additions & 0 deletions spec/models/etsource/dataset/import_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Etsource::Dataset::Import, :etsource_fixture do
describe '#load_emission_data' do
let(:emissions) { described_class.new('nl').send(:load_emission_data) }

it 'loads flat emission values keyed by joined CSV columns' do
expect(emissions[:emissions_data][:energy_fugitive_emissions_non_energetic_co2])
.to eq(20.0)
end
end
end
13 changes: 10 additions & 3 deletions spec/models/gql/runtime/functions/lookup_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Gql::Runtime::Functions
describe Lookup, :etsource_fixture do
let(:gql) { Scenario.default.gql(prepare: true) }
let(:molecule_graph) { gql.future.molecules }

let(:result) do |example|
gql.query_future(example.metadata[:example_group][:description])
Expand Down Expand Up @@ -51,21 +52,27 @@ module Gql::Runtime::Functions
end

describe "EMISSIONS(buildings_non_specified, energetic, other_ghg)" do
it 'returns the emission value' do
it 'returns the present-year emission value' do
expect(result).to eq(2796620.0)
end
end

describe "EMISSIONS(energy_electricity_and_heat_production, energetic, other_ghg)" do
it 'returns the emission value' do
it 'returns the present-year emission value' do
expect(result).to eq(18.0)
end
end

describe 'EMISSIONS(households_non_specified, energetic, other_ghg)' do
it 'returns the emission value' do
it 'returns the present-year emission value' do
expect(result).to eq(7.0)
end
end

describe 'EMISSIONS(households_non_specified, energetic, other_ghg, 1990)' do
it 'returns the 1990 baseline emission value' do
expect(result).to eq(10.0)
end
end
end
end
7 changes: 0 additions & 7 deletions spec/models/gql/runtime/functions/update_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@
end
end

describe 'UPDATE(EMISSIONS(invalid_sector, energetic), co2, 100.0)' do
it 'raises an error when the emission key does not exist in the dataset' do
# The full key 'invalid_sector_energetic_co2' does not exist in the dataset
expect { result }.to raise_error(Gql::CommandError)
end
end

describe 'UPDATE(EMISSIONS(energy_fugitive_emissions, non_energetic), co2, 0.0)' do
before { result }

Expand Down
Loading