diff --git a/app/models/gql/runtime/functions/update.rb b/app/models/gql/runtime/functions/update.rb index 2a0cdbebf..41cb3d040 100644 --- a/app/models/gql/runtime/functions/update.rb +++ b/app/models/gql/runtime/functions/update.rb @@ -122,7 +122,7 @@ def update_something_by(*value_terms) scope.update_object = object # for UPDATE_OBJECT() input_value = input_value_proc.respond_to?(:call) ? input_value_proc.call : input_value_proc input_value = input_value.first if input_value.is_a?(::Array) - original_value = object[attribute_name] + original_value = object[attribute_name.to_sym] if original_value.is_a?(Numeric) original_value = original_value.to_f @@ -145,7 +145,11 @@ def update_something_by(*value_terms) # @private def update_element_with(object, attribute_name, value) - object.send "#{attribute_name}=", value + if object.is_a?(Hash) && object.key?(attribute_name.to_sym) + object[attribute_name.to_sym] = value + else + object.send("#{attribute_name}=", value) + end end # Private: at the moment only takes care of percentages and absolute numbers. diff --git a/app/models/qernel/fever_facade/calculators.rb b/app/models/qernel/fever_facade/calculators.rb index 725d37765..bd80276d8 100644 --- a/app/models/qernel/fever_facade/calculators.rb +++ b/app/models/qernel/fever_facade/calculators.rb @@ -9,8 +9,6 @@ def initialize(ordered_producers, ordered_consumers, context) @ordered_consumers = ordered_consumers @ordered_producers = ordered_producers - # first build then refactor - # make sure we can forget what we don't need setup end @@ -57,6 +55,7 @@ def setup producer_share -= consumer_share consumer.build_activity(producer, (consumer_share / consumer_share_in_total)) + consumer.inject_share_to_producer(producer, (consumer_share / consumer_share_in_total)) end end end diff --git a/app/models/qernel/fever_facade/consumer_adapter.rb b/app/models/qernel/fever_facade/consumer_adapter.rb index aa810a2b2..a84b695aa 100644 --- a/app/models/qernel/fever_facade/consumer_adapter.rb +++ b/app/models/qernel/fever_facade/consumer_adapter.rb @@ -9,14 +9,14 @@ class ConsumerAdapter < Adapter def initialize(node, context) super @was = @node.demand - # TODO: at the init we should set all edges shares to 0!!! - # Then set them again -rigth? or is it not neccesary? how does future graph work? they are unset right? + # Set all edges to 0.0 to ensure that all edges set in the setup phase sum to 1.0 + # after calculating the consumer producer pairs + @node.input(:useable_heat).edges.each { |e| e.share = 0.0 } end - # How much has the consumer already been filled with + # How much has the consumer already been filled with (could also be sum of input_edges) def share_met @share_met ||= 0.0 - # Of kan dat toch vanuit de summed edges zoals we in de vorige commit hadden? end # Injects share after to calculation @@ -42,8 +42,7 @@ def participant_for(tech_type, share) end def inject! - # TODO: this feels very very nasty, but I think we have to! Otherwise the - # graph wil try to solve it himself based on something unknown + # Inject shares again @node.input(:useable_heat).edges.each { |e| e.share = 0.0 } calculable_activities.each_value do |calc_activity| diff --git a/app/models/qernel/fever_facade/consumer_adapter/calculable_activity.rb b/app/models/qernel/fever_facade/consumer_adapter/calculable_activity.rb index 15ef3d0dc..f175a83d8 100644 --- a/app/models/qernel/fever_facade/consumer_adapter/calculable_activity.rb +++ b/app/models/qernel/fever_facade/consumer_adapter/calculable_activity.rb @@ -44,7 +44,9 @@ def demand_curve_from_activities def production_curve_from_activities Merit::CurveTools.add_curves( calculable_activities.filter_map do |_, activity| - activity.activities.sum(&:production_curve) unless activity.empty? + unless activity.empty? + Merit::CurveTools.add_curves(activity.activities.map(&:production_curve)) + end end ) || EMPTY_CURVE end diff --git a/app/models/qernel/fever_facade/group.rb b/app/models/qernel/fever_facade/group.rb index be3935237..33ba0d3da 100644 --- a/app/models/qernel/fever_facade/group.rb +++ b/app/models/qernel/fever_facade/group.rb @@ -7,6 +7,7 @@ module FeverFacade # Fever instance. class Group attr_reader :name + attr_reader :calculators # Public: Creates a new Group. # @@ -17,13 +18,8 @@ class Group def initialize(name, plugin) @name = name @context = Context.new(plugin, plugin.graph) - end - # Public: Sets up calculators where consumers and producers are matched - def calculators - @calculators ||= Qernel::FeverFacade::Calculators.new( - ordered_producers, ordered_consumers, @context - ) + setup_calculators end # Public: Instructs the calculator to compute a single frame. @@ -62,7 +58,20 @@ def ordered_consumers end def producer_adapters - adapters.select { |adapter| adapter.config.type == :producer } + adapters.select { |adapter| adapter.is_a?(Qernel::FeverFacade::ProducerAdapter) } + end + + def consumer_adapters + adapters.select { |adapter| adapter.is_a?(Qernel::FeverFacade::ConsumerAdapter) } + end + + private + + # Private: Sets up calculators where consumers and producers are matched + def setup_calculators + @calculators = Qernel::FeverFacade::Calculators.new( + ordered_producers, ordered_consumers, @context + ) end end end diff --git a/app/models/qernel/fever_facade/producer_adapter/inject.rb b/app/models/qernel/fever_facade/producer_adapter/inject.rb index 72b55a789..af7eba9a9 100644 --- a/app/models/qernel/fever_facade/producer_adapter/inject.rb +++ b/app/models/qernel/fever_facade/producer_adapter/inject.rb @@ -12,7 +12,6 @@ module Inject def inject_demand! production = producer.output_curve.sum - # TODO: get aggerate curve etc. inject_aggregator_attributes!(production) # MWh -> MJ @@ -50,6 +49,10 @@ def inject_aggregator_attributes!(production) # TODO: should be a method on collection of participants demand = participants.sum(&:demand) + # puts @node.key + # puts demand + # puts production + # debugger if @node.key == :buildings_space_heater_heatpump_air_water_network_gas edge = conv.output(:useable_heat).edges.first edge.share = demand.positive? ? production / demand : 0.0 diff --git a/app/models/qernel/fever_facade/summary.rb b/app/models/qernel/fever_facade/summary.rb index 2d3a6ff66..76851ad79 100644 --- a/app/models/qernel/fever_facade/summary.rb +++ b/app/models/qernel/fever_facade/summary.rb @@ -18,7 +18,7 @@ def demand suppliers = @group.adapters.select do |adapter| - adapter.installed? && adapter.participant.is_a?(Fever::Consumer) + adapter.installed? && adapter.is_a?(Qernel::FeverFacade::ConsumerAdapter) end return [0.0] * 8760 if suppliers.none? @@ -34,10 +34,7 @@ def demand def production return @production if @production - suppliers = - @group.adapters.reject do |adapter| - adapter.participant.is_a?(Fever::Consumer) - end + suppliers = @group.producer_adapters return [0.0] * 8760 if suppliers.none? @@ -138,6 +135,11 @@ def total_production_curve_for_producer(producer_key) producer.producer.output_curve end + # In the summary and export first the consumers and then the producers are shown + def nodes + @group.consumer_adapters.map(&:node) + @group.producer_adapters.map(&:node) + end + private # Internal: Compares production and demand and creates two new curves diff --git a/app/models/qernel/merit_facade/consumption_loss_adapter.rb b/app/models/qernel/merit_facade/consumption_loss_adapter.rb index 71d1d3e67..b9c56b451 100644 --- a/app/models/qernel/merit_facade/consumption_loss_adapter.rb +++ b/app/models/qernel/merit_facade/consumption_loss_adapter.rb @@ -38,7 +38,7 @@ def loss_share end edge = edges.first - loss_share = edge.rgt_output.conversion * edge.parent_share + loss_share = edge.rgt_output.conversion other_shares = edge.rgt_node.outputs.sum do |input| diff --git a/app/models/qernel/plugins/causality.rb b/app/models/qernel/plugins/causality.rb index 67c161544..20a4189c2 100644 --- a/app/models/qernel/plugins/causality.rb +++ b/app/models/qernel/plugins/causality.rb @@ -10,6 +10,7 @@ class Causality CURVE_ROTATE = 2160 before :first_calculation, :clone_dataset + before :first_calculation, :early_setup after :first_calculation, :setup after :first_calculation, :run before :recalculation, :inject @@ -57,7 +58,6 @@ def run(lifecycle) # Internal: Sets up Fever and Merit. def setup - @fever.setup @merit.setup @heat_network.setup @agriculture_heat.setup @@ -69,6 +69,10 @@ def setup @reconciliation.setup_early end + def early_setup + @fever.setup + end + def inject(lifecycle) merit_calc = Merit::StepwiseCalculator.new.calculate(@merit.order) diff --git a/app/serializers/fever_csv_serializer.rb b/app/serializers/fever_csv_serializer.rb index 1ac93ddce..b15e3c904 100644 --- a/app/serializers/fever_csv_serializer.rb +++ b/app/serializers/fever_csv_serializer.rb @@ -3,6 +3,17 @@ # Creates CSV rows describing heat demand and supply for one or more fever groups. class FeverCSVSerializer attr_reader :filename + + def self.time_column(year) + # We don't model leap days: 1970 is a safe choice for accurate times in the CSV. + base_date = Time.utc(1970, 1, 1) + + ['Time'] + + Array.new(8760) do |i| + (base_date + i.hours).strftime("#{year}-%m-%d %R") + end + end + def initialize(graph, groups, filename) @graph = graph @groups = groups.map(&:to_sym) @@ -16,32 +27,51 @@ def to_csv_rows 'enabled for this scenario']] end - [headers, *data] + data end private - def headers + def columns_for(node, summary) [ - 'Production (MW)', - 'Demand (MW)', - 'Buffering and time-shifting (MW)', - 'Deficit (MW)' + demand_column_for(node, summary), + production_column_for(node, summary) ] end - def data - curve(:production).zip( - curve(:demand), - curve(:surplus), - curve(:deficit) - ) + def safe_curve(curve) + curve.empty? ? [0.0] * 8760 : curve + end + + def demand_column_for(node, summary) + case node.fever.type + when :consumer + ["#{node.key}.input.demand (MW)"] + + safe_curve(summary.total_demand_curve_for_consumer(node.key)) + when :producer + ["#{node.key}.output.demand (MW)"] + + safe_curve(summary.total_demand_curve_for_producer(node.key)) + end + end + + def production_column_for(node, summary) + case node.fever.type + when :consumer + ["#{node.key}.input.production (MW)"] + + safe_curve(summary.total_production_curve_for_consumer(node.key)) + when :producer + ["#{node.key}.output.production (MW)"] + + safe_curve(summary.total_production_curve_for_producer(node.key)) + end end - def curve(type) - Merit::CurveTools.add_curves( - @groups.map { |group| summary(group).public_send(type) } - ) + def data + ( + [self.class.time_column(@graph.year)] + @groups.flat_map do |group| + summary = summary(group) + summary.nodes.flat_map { |node| columns_for(node, summary) } + end + ).transpose end def summary(group)