Skip to content

Commit

Permalink
Merge pull request #1409 from quintel/beccus
Browse files Browse the repository at this point in the history
Implement BECCUS in the ETM
  • Loading branch information
mabijkerk authored Mar 25, 2024
2 parents e2cabf7 + 518260b commit 15dc075
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ gem 'ruby-progressbar'

# own gems
gem 'quintel_merit', ref: '421f3fb', github: 'quintel/merit'
gem 'atlas', ref: 'd5c84b5', github: 'quintel/atlas'
gem 'atlas', ref: 'cdb74be', github: 'quintel/atlas'
gem 'fever', ref: '2a91194', github: 'quintel/fever'
gem 'refinery', ref: '5439199', github: 'quintel/refinery'
gem 'rubel', ref: 'e36554a', github: 'quintel/rubel'
Expand Down
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ GIT

GIT
remote: https://github.com/quintel/atlas.git
revision: d5c84b505494c92f816624f8b30b4b47941ff75d
ref: d5c84b5
revision: cdb74be0ed878ecfd83a98b3528c538f6aa47bec
ref: cdb74be
specs:
atlas (1.0.0)
activemodel (>= 7)
Expand Down Expand Up @@ -170,7 +170,7 @@ GEM
dry-validation (~> 1.0, >= 1.0.0)
connection_pool (2.4.1)
crass (1.0.6)
csv (3.2.8)
csv (3.3.0)
dalli (3.2.3)
date (3.3.4)
debug_inspector (1.1.0)
Expand Down Expand Up @@ -252,7 +252,7 @@ GEM
gctools (0.2.4)
globalid (1.2.1)
activesupport (>= 6.1)
gpgme (2.0.23)
gpgme (2.0.24)
mini_portile2 (~> 2.7)
haml (5.2.2)
temple (>= 0.8.0)
Expand Down
6 changes: 6 additions & 0 deletions app/models/qernel/method_meta_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def calculation_methods
'variable_costs_per(:plant)' => {},
'fuel_costs_per(:plant)' => {},
'co2_emissions_costs_per(:plant)' => {},
'captured_biogenic_co2_costs_per(:plant)' => {},
'fixed_operation_and_maintenance_costs_per(:plant)' => {},
'variable_operation_and_maintenance_costs_per(:plant)' => {},
'total_initial_investment_per(:plant)' => {},
Expand All @@ -62,6 +63,7 @@ def calculation_methods
'variable_costs_per(:node)' => {},
'fuel_costs_per(:node)' => {},
'co2_emissions_costs_per(:node)' => {},
'captured_biogenic_co2_costs_per(:node)' => {},
'fixed_operation_and_maintenance_costs_per(:node)' => {},
'variable_operation_and_maintenance_costs_per(:node)' => {},
'total_initial_investment_per(:node)' => {},
Expand All @@ -74,6 +76,7 @@ def calculation_methods
'variable_costs_per(:mw_electricity)' => {},
'fuel_costs_per(:mw_electricity)' => {},
'co2_emissions_costs_per(:mw_electricity)' => {},
'captured_biogenic_co2_costs_per(:mw_electricity)' => {},
'fixed_operation_and_maintenance_costs_per(:mw_electricity)' => {},
'variable_operation_and_maintenance_costs_per(:mw_electricity)' => {},
'total_initial_investment_per(:mw_electricity)' => {},
Expand All @@ -86,6 +89,7 @@ def calculation_methods
'variable_costs_per(:mwh_electricity)' => {},
'fuel_costs_per(:mwh_electricity)' => {},
'co2_emissions_costs_per(:mwh_electricity)' => {},
'captured_biogenic_co2_costs_per(:mwh_electricity)' => {},
'fixed_operation_and_maintenance_costs_per(:mwh_electricity)' => {},
'variable_operation_and_maintenance_costs_per(:mwh_electricity)' => {},
'total_initial_investment_per(:mwh_electricity)' => {},
Expand All @@ -98,6 +102,7 @@ def calculation_methods
'variable_costs_per(:mw_heat)' => {},
'fuel_costs_per(:mw_heat)' => {},
'co2_emissions_costs_per(:mw_heat)' => {},
'captured_biogenic_co2_costs_per(:mw_heat)' => {},
'fixed_operation_and_maintenance_costs_per(:mw_heat)' => {},
'variable_operation_and_maintenance_costs_per(:mw_heat)' => {},
'total_initial_investment_per(:mw_heat)' => {},
Expand All @@ -110,6 +115,7 @@ def calculation_methods
'variable_costs_per(:mwh_heat)' => {},
'fuel_costs_per(:mwh_heat)' => {},
'co2_emissions_costs_per(:mwh_heat)' => {},
'captured_biogenic_co2_costs_per(:mwh_heat)' => {},
'fixed_operation_and_maintenance_costs_per(:mwh_heat)' => {},
'variable_operation_and_maintenance_costs_per(:mwh_heat)' => {},
'total_initial_investment_per(:mwh_heat)' => {},
Expand Down
4 changes: 4 additions & 0 deletions app/models/qernel/node_api/conversion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def co2_emissions_costs_per(unit)
convert_to(co2_emissions_costs, unit)
end

def captured_biogenic_co2_costs_per(unit)
convert_to(captured_biogenic_co2_costs, unit)
end

def variable_operation_and_maintenance_costs_per(unit)
convert_to(variable_operation_and_maintenance_costs, unit)
end
Expand Down
27 changes: 25 additions & 2 deletions app/models/qernel/node_api/cost.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def marginal_costs
elsif electricity_output_conversion.zero?
0.0
else
variable_costs_per_typical_input(include_waste: false) *
costs = variable_costs_per_typical_input(include_waste: false)
(costs.negative? ? 0.0 : costs) *
SECS_PER_HOUR / # Highlighting
electricity_output_conversion
end
Expand Down Expand Up @@ -368,7 +369,8 @@ def variable_costs_per_typical_input(include_waste: true)
fetch(cache_key) do
costable =
weighted_carrier_cost_per_mj +
co2_emissions_costs_per_typical_input
co2_emissions_costs_per_typical_input +
captured_biogenic_co2_costs_per_typical_input

costable *= costable_energy_factor unless include_waste

Expand Down Expand Up @@ -417,6 +419,27 @@ def co2_emissions_costs_per_typical_input
end
end

# Internal: Determines the costs of captured biogenic co2 .
#
# Return the the yearly costs for captured biogenic co2 for one plant.
def captured_biogenic_co2_costs
fetch(:captured_biogenic_co2_costs) do
typical_input * captured_biogenic_co2_costs_per_typical_input
end
end

# Internal: Calculates the captured biogenic co2 costs per typical input (in MJ).
# A positive price should lead to negative costs i.e., benefits.
#
# Returns a float representing cost per MJ.
def captured_biogenic_co2_costs_per_typical_input
fetch(:captured_biogenic_co2_costs_per_typical_input) do
weighted_carrier_potential_co2_per_mj *
- area.captured_biogenic_co2_price *
free_co2_factor
end
end

# Internal: Calculates the fixed operation and maintenance costs for one plant.
#
# These costs are made up of fixed O&M costs for the plant.
Expand Down
6 changes: 6 additions & 0 deletions app/models/qernel/node_api/molecule_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def co2_emissions_costs_per_typical_input
0.0
end

# Public: The price of captured biogenic CO2 emissions by the node. Molecule nodes do not have
# CO2 emissions so this is always zero.
def captured_biogenic_co2_costs_per_typical_input
0.0
end

private

# Molecule nodes define demand in kg (kg/year), while capacities are specified in kg/hour.
Expand Down
20 changes: 20 additions & 0 deletions app/models/qernel/recursive_factor/weighted_carrier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,24 @@ def weighted_carrier_co2_per_mj_factor(edge)

# Else: continue traversing right.
end

# Same as weighted_carrier_cost_per_mj but for co2 potential biogenic capture
def weighted_carrier_potential_co2_per_mj
fetch(:weighted_carrier_potential_co2_per_mj) do
recursive_factor_without_losses(:weighted_carrier_potential_co2_per_mj_factor)
end
end

def weighted_carrier_potential_co2_per_mj_factor(edge)
return unless edge

# Carriers with no or zero intrinsic biogenic CO2 capture potential are not
# counted in this calculation.

if edge.carrier.potential_co2_conversion_per_mj || domestic_dead_end?
edge.carrier.potential_co2_conversion_per_mj
end

# Else: continue traversing right.
end
end
17 changes: 10 additions & 7 deletions app/serializers/costs_parameters_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class CostsParametersSerializer
'Capital costs (eur)', 'Depreciation costs (eur)',
'Fixed operation and maintenance costs (eur)', 'Variable operation and maintenance costs (eur)',
'Total investment over lifetime (eur)', 'WACC', 'Construction time (years)', 'Technical lifetime (years)',
'CO2 emission costs (eur)', 'Number of units'
'CO2 emission costs (eur)', 'Captured biogenic CO2 costs (eur)', 'Number of units'
]

# Creates a new costs csv serializer.
Expand All @@ -133,7 +133,7 @@ class CostsParametersSerializer
# Returns a CostsCsvSerializer.
def initialize(scenario)
@graph = scenario.gql.future.graph
@molecule_graph = Qernel::Plugins::Molecules.new(@graph).molecule_graph
@molecule_graph = scenario.gql.future.molecules
@gql = scenario.gql
end

Expand Down Expand Up @@ -210,11 +210,12 @@ def node_rows_for_subgroup(graph, group, subgroup)
group, # Group
subgroup, # Subgroup
node.key, # Key
format_num(node.query.total_costs_per(:node) -
node.query.fuel_costs_per(:node) - node.query.co2_emissions_costs_per(:node)), # Total costs (eur)
format_num(node.query.capital_expenditures_excluding_ccs_per(:node) +
format_num(node.query.total_costs_per(:node) -
node.query.fuel_costs_per(:node) - node.query.co2_emissions_costs_per(:node) -
node.query.captured_biogenic_co2_costs_per(:node)), # Total costs (eur)
format_num(node.query.capital_expenditures_excluding_ccs_per(:node) +
node.query.capital_expenditures_ccs_per(:node)), # Total CAPEX (eur)
format_num(node.query.operating_expenses_excluding_ccs_per(:node) +
format_num(node.query.operating_expenses_excluding_ccs_per(:node) +
node.query.operating_expenses_ccs_per(:node)), # Total OPEX (eur)
format_num(node.query.cost_of_capital_per(:node)), # 'Capital costs (eur)'
format_num(node.query.depreciation_costs_per(:node)), # 'Depreciation costs (eur)'
Expand All @@ -226,6 +227,7 @@ def node_rows_for_subgroup(graph, group, subgroup)
format_num(node.query.construction_time), # 'Construction time (years)'
format_num(node.query.technical_lifetime), # 'Technical lifetime (years)'
format_num(node.query.co2_emissions_costs_per(:node)), # CO2 emission costs (eur)
format_num(node.query.captured_biogenic_co2_costs_per(:node)), # Biogenic CO2 emissions costs (eur)
format_num(node.query.number_of_units) # Number of units
]
end
Expand Down Expand Up @@ -262,6 +264,7 @@ def query_row(group, subgroup, query, name = '')
format_num(@query_results["#{query}_construction_time"]),
format_num(@query_results["#{query}_technical_lifetime"]),
nil, # CO2 emission costs (eur)
nil, # Biogenic CO2 emission costs (eur)
nil # Number of units
]
end
Expand All @@ -273,7 +276,7 @@ def query_row(group, subgroup, query, name = '')
# For each base key it performs a set of queries: [{key}, {key}_capex, ...]
# Skips queries that could not be found. Saves the results in @query_results.
def perform_queries!
as_queries = queries.flat_map do |key|
as_queries = queries.flat_map do |key|
[
Gquery.get(key),
Gquery.get("#{key}_annualised_costs"),
Expand Down
21 changes: 21 additions & 0 deletions db/migrate/20240321134650_rename_biomass_chp_inputs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'etengine/scenario_migration'

class RenameBiomassChpInputs < ActiveRecord::Migration[7.0]
include ETEngine::ScenarioMigration

KEYS = {
'capacity_of_energy_chp_local_wood_pellets' => 'capacity_of_energy_chp_local_wood_pellets_dispatchable',
'share_of_energy_chp_local_ht_wood_pellets' => 'share_of_energy_chp_local_ht_wood_pellets_dispatchable',
'share_of_energy_chp_local_mt_wood_pellets' => 'share_of_energy_chp_local_mt_wood_pellets_dispatchable'
}.freeze

def up
migrate_scenarios do |scenario|
KEYS.each do |old_key, new_key|
if scenario.user_values.key?(old_key)
scenario.user_values[new_key] = scenario.user_values.delete(old_key)
end
end
end
end
end
4 changes: 2 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_01_26_141138) do
ActiveRecord::Schema[7.0].define(version: 2024_03_21_134650) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", limit: 191, null: false
t.string "record_type", limit: 191, null: false
Expand Down Expand Up @@ -53,7 +53,7 @@
t.index ["scenario_id"], name: "index_heat_network_orders_on_scenario_id"
end

create_table "households_space_heating_producer_orders", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
create_table "households_space_heating_producer_orders", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.integer "scenario_id"
t.text "order"
t.index ["scenario_id"], name: "index_households_space_heating_producer_orders_on_scenario_id", unique: true
Expand Down
22 changes: 22 additions & 0 deletions spec/models/qernel/node_api/cost_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def build_slot(node, carrier, direction, conversion = 1.0)
{
weighted_carrier_cost_per_mj: 200,
co2_emissions_costs_per_typical_input: 300,
captured_biogenic_co2_costs_per_typical_input: 0,
variable_operation_and_maintenance_costs_per_typical_input: 400
}
end
Expand Down Expand Up @@ -367,6 +368,27 @@ def build_slot(node, carrier, direction, conversion = 1.0)
end
end

describe '#captured_biogenic_co2_costs_per_typical_input' do
let(:attrs) do
{
weighted_carrier_potential_co2_per_mj: 2.0,
free_co2_factor: 1.0
}
end

before do
node.with(attrs)

allow(api).to receive(:area).and_return(
double(captured_biogenic_co2_price: 2.0)
)
end

it 'calculates when everything is set' do
expect(api.send(:captured_biogenic_co2_costs_per_typical_input)).to eq(-4.0)
end
end

describe '#variable_operation_and_maintenance_costs' do
it 'calculates when everything is set' do
node.with(
Expand Down

0 comments on commit 15dc075

Please sign in to comment.