Skip to content

Commit

Permalink
GSYE-805: Added simplified self consumption scheme.
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrostz committed Oct 30, 2024
1 parent fa6442f commit 6f90c70
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 63 deletions.
46 changes: 32 additions & 14 deletions src/gsy_e/gsy_e_core/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import psutil
from gsy_framework.constants_limits import ConstSettings, GlobalConfig
from gsy_framework.enums import CoefficientAlgorithm, SpotMarketTypeEnum
from gsy_framework.enums import CoefficientAlgorithm, SpotMarketTypeEnum, SCMSelfConsumptionType
from gsy_framework.utils import format_datetime, str_to_pendulum_datetime
from pendulum import DateTime, Duration, duration

Expand All @@ -43,9 +43,10 @@
from gsy_e.gsy_e_core.simulation.time_manager import simulation_time_manager_factory
from gsy_e.gsy_e_core.util import NonBlockingConsole
from gsy_e.models.area.event_deserializer import deserialize_events_to_areas
from gsy_e.models.area.scm_manager import SCMManager
from gsy_e.models.area.scm_manager import SCMManager, SCMManagerWithoutSurplusTrade
from gsy_e.models.config import SimulationConfig


if TYPE_CHECKING:
from gsy_e.models.area import Area, AreaBase, CoefficientArea

Expand Down Expand Up @@ -546,6 +547,34 @@ def _cycle_markets(self, slot_no: int) -> None:
global_objects.profiles_handler.current_scm_profiles,
)

def _execute_scm_manager_cycle(self, slot_no: int) -> SCMManager:
if (
ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE
== SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value
):
scm_manager = SCMManagerWithoutSurplusTrade(
self.area, self._get_current_market_time_slot(slot_no)
)
production_kwh = self.area.aggregate_production_from_all_homes(
self.progress_info.current_slot_time
)
self.area.calculate_home_after_meter_data_for_collective_self_consumption(
self.progress_info.current_slot_time, scm_manager, production_kwh
)
else:
scm_manager = SCMManager(self.area, self._get_current_market_time_slot(slot_no))
self.area.calculate_home_after_meter_data(
self.progress_info.current_slot_time, scm_manager
)

scm_manager.calculate_community_after_meter_data()
self.area.trigger_energy_trades(scm_manager)
scm_manager.accumulate_community_trades()

if ConstSettings.SCMSettings.MARKET_ALGORITHM == CoefficientAlgorithm.DYNAMIC.value:
self.area.change_home_coefficient_percentage(scm_manager)
return scm_manager

def _execute_simulation(
self, slot_resume: int, _tick_resume: int, console: NonBlockingConsole = None
) -> None:
Expand All @@ -566,18 +595,7 @@ def _execute_simulation(

self._handle_external_communication()

scm_manager = SCMManager(self.area, self._get_current_market_time_slot(slot_no))

self.area.calculate_home_after_meter_data(
self.progress_info.current_slot_time, scm_manager
)

scm_manager.calculate_community_after_meter_data()
self.area.trigger_energy_trades(scm_manager)
scm_manager.accumulate_community_trades()

if ConstSettings.SCMSettings.MARKET_ALGORITHM == CoefficientAlgorithm.DYNAMIC.value:
self.area.change_home_coefficient_percentage(scm_manager)
scm_manager = self._execute_scm_manager_cycle(slot_no)

# important: SCM manager has to be updated before sending the results
self._results.update_scm_manager(scm_manager)
Expand Down
54 changes: 42 additions & 12 deletions src/gsy_e/models/area/coefficient_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,18 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

from collections import defaultdict
from logging import getLogger
from typing import TYPE_CHECKING, List, Dict

from numpy.random import random
from pendulum import DateTime

from gsy_e.gsy_e_core.util import get_slots_per_month
from gsy_e.models.area.area_base import AreaBase
from gsy_e.models.area.scm_dataclasses import SCMAreaProperties
from gsy_e.models.config import SimulationConfig
from gsy_e.models.strategy.external_strategies import ExternalMixin
from gsy_e.models.strategy.scm import SCMStrategy
from gsy_e.models.area.scm_dataclasses import SCMAreaProperties
from gsy_e.gsy_e_core.util import get_slots_per_month

log = getLogger(__name__)

Expand Down Expand Up @@ -131,27 +130,58 @@ def _calculate_home_after_meter_data(
home_production_kWh = 0
home_consumption_kWh = 0

asset_energy_requirements_kWh = defaultdict(lambda: 0)

for child in self.children:
# import
consumption_kWh = child.strategy.get_energy_to_buy_kWh(current_time_slot)
asset_energy_requirements_kWh[child.uuid] += consumption_kWh
home_consumption_kWh += consumption_kWh
home_consumption_kWh += child.strategy.get_energy_to_buy_kWh(current_time_slot)
# export
production_kWh = child.strategy.get_energy_to_sell_kWh(current_time_slot)
asset_energy_requirements_kWh[child.uuid] -= production_kWh
home_production_kWh += production_kWh
home_production_kWh += child.strategy.get_energy_to_sell_kWh(current_time_slot)

scm_manager.add_home_data(
self.uuid,
self.name,
self.area_properties,
home_production_kWh,
home_consumption_kWh,
dict(asset_energy_requirements_kWh),
)

def aggregate_production_from_all_homes(self, current_time_slot: DateTime) -> float:
"""Aggregate energy production from all homes, in kWh."""
if self.is_home_area:
return sum(
child.strategy.get_energy_to_sell_kWh(current_time_slot) for child in self.children
)
return sum(
child.aggregate_production_from_all_homes(current_time_slot)
for child in sorted(self.children, key=lambda _: random())
)

def calculate_home_after_meter_data_for_collective_self_consumption(
self,
current_time_slot: DateTime,
scm_manager: "SCMManager",
community_production_kwh: float,
):
"""Recursive function that calculates the home after meter data."""
if self.is_home_area:
home_consumption_kwh = sum(
child.strategy.get_energy_to_buy_kWh(current_time_slot) for child in self.children
)
home_production_kwh = (
community_production_kwh
* self.area_properties.AREA_PROPERTIES["coefficient_percentage"]
)
scm_manager.add_home_data(
self.uuid,
self.name,
self.area_properties,
home_production_kwh,
home_consumption_kwh,
)
for child in sorted(self.children, key=lambda _: random()):
child.calculate_home_after_meter_data_for_collective_self_consumption(
current_time_slot, scm_manager, community_production_kwh
)

def calculate_home_after_meter_data(
self, current_time_slot: DateTime, scm_manager: "SCMManager"
) -> None:
Expand Down
73 changes: 41 additions & 32 deletions src/gsy_e/models/area/scm_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Dict, List
from uuid import uuid4

from gsy_framework.constants_limits import is_no_community_self_consumption
from gsy_framework.data_classes import Trade, TraderDetails
from gsy_framework.sim_results.kpi_calculation_helper import KPICalculationHelper
from pendulum import DateTime
Expand Down Expand Up @@ -45,7 +44,6 @@ class HomeAfterMeterData:
_self_production_for_community_kWh: float = 0.0
trades: List[Trade] = None
area_properties: SCMAreaProperties = field(default_factory=SCMAreaProperties)
asset_energy_requirements_kWh: Dict[str, float] = field(default_factory=dict)

def to_dict(self) -> Dict:
"""Dict representation of the home after meter data."""
Expand Down Expand Up @@ -111,10 +109,6 @@ def set_total_community_production(self, energy_kWh: float):

def set_production_for_community(self, unassigned_energy_production_kWh: float):
"""Assign the energy surplus of the home to be consumed by the community."""

if is_no_community_self_consumption():
self._self_production_for_community_kWh = 0
return 0.0
if self.energy_surplus_kWh <= unassigned_energy_production_kWh:
self._self_production_for_community_kWh = self.energy_surplus_kWh
return unassigned_energy_production_kWh - self.energy_surplus_kWh
Expand Down Expand Up @@ -220,6 +214,15 @@ def create_sell_trade(
logging.info("[SCM][TRADE][OFFER] [%s] [%s] %s", self.home_name, trade.time_slot, trade)


class HomeAfterMeterDataWithoutSurplusTrade(HomeAfterMeterData):
"""Dedicated HomeAfterMeterData class, specific for the non-suplus trade SCM."""

def set_production_for_community(self, unassigned_energy_production_kWh: float):
"""Assign the energy surplus of the home to be consumed by the community."""
self._self_production_for_community_kWh = 0
return 0.0


@dataclass
class CommunityData:
# pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -442,13 +445,6 @@ def set_min_max_community_savings(
@property
def savings(self):
"""Absolute price savings of the home, compared to the base energy bill."""
# The savings and the percentage might produce negative and huge percentage values
# in cases of production. This is due to the fact that the energy bill in these cases
# will be negative, and the producer will not have "savings". For a more realistic case
# the revenue should be omitted from the calculation of the savings, however this needs
# to be discussed.
if is_no_community_self_consumption():
return self.self_consumed_savings + self.gsy_energy_bill_revenue
savings_absolute = KPICalculationHelper().saving_absolute(
self.base_energy_bill_excl_revenue, self.gsy_energy_bill_excl_revenue
)
Expand Down Expand Up @@ -530,22 +526,35 @@ def calculate_base_energy_bill(
self, home_data: HomeAfterMeterData, area_rates: AreaEnergyRates
):
"""Calculate the base (not with GSy improvements) energy bill for the home."""
if is_no_community_self_consumption():
self.base_energy_bill_excl_revenue = (
home_data.consumption_kWh * area_rates.utility_rate_incl_fees
)
self.base_energy_bill_revenue = home_data.production_kWh * area_rates.feed_in_tariff
self.base_energy_bill = (
self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue
)
else:
self.base_energy_bill_revenue = (
home_data.energy_surplus_kWh * area_rates.feed_in_tariff
)
self.base_energy_bill_excl_revenue = (
home_data.energy_need_kWh * area_rates.utility_rate_incl_fees
+ area_rates.area_fees.total_monthly_fees
)
self.base_energy_bill = (
self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue
)
self.base_energy_bill_revenue = home_data.energy_surplus_kWh * area_rates.feed_in_tariff
self.base_energy_bill_excl_revenue = (
home_data.energy_need_kWh * area_rates.utility_rate_incl_fees
+ area_rates.area_fees.total_monthly_fees
)
self.base_energy_bill = self.base_energy_bill_excl_revenue - self.base_energy_bill_revenue


class AreaEnergyBillsWithoutSurplusTrade(AreaEnergyBills):
"""Dedicated AreaEnergyBills class, specific for the non-suplus trade SCM."""

@property
def savings(self):
"""Absolute price savings of the home, compared to the base energy bill."""
# The savings and the percentage might produce negative and huge percentage values
# in cases of production. This is due to the fact that the energy bill in these cases
# will be negative, and the producer will not have "savings". For a more realistic case
# the revenue should be omitted from the calculation of the savings, however this needs
# to be discussed.
return self.self_consumed_savings + self.gsy_energy_bill_revenue

def calculate_base_energy_bill(
self, home_data: HomeAfterMeterData, area_rates: AreaEnergyRates
):
"""Calculate the base (not with GSy improvements) energy bill for the home."""

self.base_energy_bill_excl_revenue = (
home_data.consumption_kWh * area_rates.utility_rate_incl_fees
+ area_rates.area_fees.total_monthly_fees
)
self.base_energy_bill_revenue = 0.0
self.base_energy_bill = self.base_energy_bill_excl_revenue
79 changes: 77 additions & 2 deletions src/gsy_e/models/area/scm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import TYPE_CHECKING, Dict, Optional

from gsy_framework.constants_limits import ConstSettings
from gsy_framework.enums import SCMSelfConsumptionType
from pendulum import DateTime

import gsy_e.constants
Expand All @@ -19,6 +20,8 @@
FeeContainer,
SCMAreaProperties,
AreaEnergyBills,
AreaEnergyBillsWithoutSurplusTrade,
HomeAfterMeterDataWithoutSurplusTrade,
)
from gsy_e.models.strategy.scm import SCMStrategy

Expand All @@ -41,6 +44,15 @@ def __init__(self, area: "CoefficientArea", time_slot: DateTime):
self._grid_fees_reduction = ConstSettings.SCMSettings.GRID_FEES_REDUCTION
self._intracommunity_base_rate_eur = ConstSettings.SCMSettings.INTRACOMMUNITY_BASE_RATE_EUR

@property
def _home_after_meter_data_class(self) -> type[HomeAfterMeterData]:
if (
ConstSettings.SCMSettings.SELF_CONSUMPTION_TYPE
== SCMSelfConsumptionType.SIMPLIFIED_COLLECTIVE_SELF_CONSUMPTION_41.value
):
return HomeAfterMeterDataWithoutSurplusTrade
return HomeAfterMeterData

@staticmethod
def _get_community_uuid_from_area(area):
# Community is always the root area in the context of SCM.
Expand All @@ -65,7 +77,6 @@ def add_home_data(
area_properties: SCMAreaProperties,
production_kWh: float,
consumption_kWh: float,
asset_energy_requirements_kWh: Dict[str, float],
):
# pylint: disable=too-many-arguments
"""Import data for one individual home."""
Expand All @@ -76,7 +87,6 @@ def add_home_data(
area_properties=area_properties,
production_kWh=production_kWh,
consumption_kWh=consumption_kWh,
asset_energy_requirements_kWh=asset_energy_requirements_kWh,
)

def calculate_community_after_meter_data(self):
Expand Down Expand Up @@ -293,6 +303,71 @@ def community_bills(self) -> Dict:
return community_bills.to_dict()


class SCMManagerWithoutSurplusTrade(SCMManager):
"""Dedicated SCMManager class, specific for the non-suplus trade SCM."""

def add_home_data(
self,
home_uuid: str,
home_name: str,
area_properties: SCMAreaProperties,
production_kWh: float,
consumption_kWh: float,
):
# pylint: disable=too-many-arguments
"""Import data for one individual home."""
self._home_data[home_uuid] = HomeAfterMeterDataWithoutSurplusTrade(
home_uuid,
home_name,
area_properties=area_properties,
production_kWh=production_kWh,
consumption_kWh=consumption_kWh,
)

def calculate_home_energy_bills(self, home_uuid: str) -> None:
"""Calculate energy bills for one home."""
assert home_uuid in self._home_data
home_data = self._home_data[home_uuid]

area_rates = self._init_area_energy_rates(home_data)
home_bill = AreaEnergyBillsWithoutSurplusTrade(
energy_rates=area_rates,
gsy_energy_bill=area_rates.area_fees.total_monthly_fees,
self_consumed_savings=home_data.self_consumed_energy_kWh
* home_data.area_properties.AREA_PROPERTIES["market_maker_rate"],
)
home_bill.calculate_base_energy_bill(home_data, area_rates)

# First handle the sold energy case. This case occurs in case of energy surplus of a home.
if home_data.energy_surplus_kWh > 0.0:
home_bill.set_sold_to_grid(
home_data.energy_surplus_kWh,
home_data.area_properties.AREA_PROPERTIES["feed_in_tariff"],
)
home_bill.set_export_grid_fees(home_data.energy_surplus_kWh)
if home_data.energy_surplus_kWh > FLOATING_POINT_TOLERANCE:
home_data.create_sell_trade(
self._time_slot,
DEFAULT_SCM_GRID_NAME,
home_data.energy_surplus_kWh,
home_data.energy_surplus_kWh
* home_data.area_properties.AREA_PROPERTIES["feed_in_tariff"],
)

if home_data.energy_need_kWh > 0.0:
home_bill.set_bought_from_grid(home_data.energy_need_kWh)

if home_data.energy_need_kWh > FLOATING_POINT_TOLERANCE:
home_data.create_buy_trade(
self._time_slot,
DEFAULT_SCM_GRID_NAME,
home_data.energy_need_kWh,
home_data.energy_need_kWh * area_rates.utility_rate_incl_fees,
)

self._bills[home_uuid] = home_bill


class SCMCommunityValidator:
"""Validator for SCM Community areas."""

Expand Down
Loading

0 comments on commit 6f90c70

Please sign in to comment.