Skip to content

Commit

Permalink
Merge pull request #1825 from gridsingularity/bug/GSYE-839
Browse files Browse the repository at this point in the history
GSYE-839: Corrected savings calculation, taking into account by inclu…
  • Loading branch information
spyrostz authored Jan 13, 2025
2 parents a9e3ab0 + caacce5 commit 1567499
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 50 deletions.
43 changes: 31 additions & 12 deletions src/gsy_e/models/area/scm_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class AreaFees:

grid_import_fee_const: float = 0.0
grid_export_fee_const: float = 0.0
grid_fees_reduction: float = 0.0
grid_fees_reduction: float = 1.0
per_kWh_fees: Dict[str, FeeContainer] = field(default_factory=dict)
monthly_fees: Dict[str, FeeContainer] = field(default_factory=dict)

Expand Down Expand Up @@ -372,7 +372,6 @@ class AreaEnergyBills: # pylint: disable=too-many-instance-attributes
spent_to_grid: float = 0.0
sold_to_grid: float = 0.0
earned_from_grid: float = 0.0
self_consumed_savings: float = 0.0
import_grid_fees: float = 0.0
export_grid_fees: float = 0.0
_min_community_savings_percent: float = 0.0
Expand Down Expand Up @@ -443,11 +442,28 @@ def set_min_max_community_savings(
self._min_community_savings_percent = min_savings_percent
self._max_community_savings_percent = max_savings_percent

@property
def savings_from_buy_from_community(self):
"""Include to the savings the additional revenue from the intracommunity trading."""
return self.bought_from_community * (
self.energy_rates.utility_rate_incl_fees - self.energy_rates.intracommunity_rate
)

@property
def savings_from_sell_to_community(self):
"""
Include the savings due to the decreased intracommunity rate compared to the feed in
tariff.
"""
return self.sold_to_community * (
self.energy_rates.intracommunity_rate - self.energy_rates.feed_in_tariff
)

@property
def savings(self):
"""Absolute price savings of the home, compared to the base energy bill."""
savings_absolute = KPICalculationHelper().saving_absolute(
self.base_energy_bill_excl_revenue, self.gsy_energy_bill_excl_revenue
savings_absolute = (
self.savings_from_buy_from_community + self.savings_from_sell_to_community
)
assert savings_absolute > -FLOATING_POINT_TOLERANCE
return savings_absolute
Expand Down Expand Up @@ -539,14 +555,17 @@ 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 savings_from_buy_from_community(self):
"""Include to the savings the additional revenue from the intracommunity trading."""
return self.bought_from_community * self.energy_rates.utility_rate_incl_fees

@property
def savings_from_sell_to_community(self):
"""
Include the savings due to the decreased intracommunity rate compared to the feed in
tariff.
"""
return self.gsy_energy_bill_revenue

def calculate_base_energy_bill(
self, home_data: HomeAfterMeterData, area_rates: AreaEnergyRates
Expand Down
17 changes: 12 additions & 5 deletions src/gsy_e/models/area/scm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,6 @@ def calculate_home_energy_bills(self, home_uuid: str) -> None:
home_bill = AreaEnergyBills(
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)

Expand Down Expand Up @@ -279,6 +277,9 @@ def get_home_energy_need(self, home_uuid: str) -> float:
def community_bills(self) -> Dict:
"""Calculate bills for the community."""
community_bills = AreaEnergyBills()
savings_from_buy_from_community = 0.0
savings_from_sell_to_community = 0.0
savings = 0.0
for data in self._bills.values():
community_bills.base_energy_bill += data.base_energy_bill
community_bills.base_energy_bill_revenue += data.base_energy_bill_revenue
Expand All @@ -299,7 +300,15 @@ def community_bills(self) -> Dict:
community_bills.import_grid_fees += data.import_grid_fees
community_bills.export_grid_fees += data.export_grid_fees

return community_bills.to_dict()
savings_from_buy_from_community += data.savings_from_buy_from_community
savings_from_sell_to_community += data.savings_from_sell_to_community
savings += data.savings

comm_bills = community_bills.to_dict()
comm_bills["savings"] = savings
comm_bills["savings_from_buy_from_community"] = savings_from_buy_from_community
comm_bills["savings_from_sell_to_community"] = savings_from_sell_to_community
return comm_bills


class SCMManagerWithoutSurplusTrade(SCMManager):
Expand Down Expand Up @@ -332,8 +341,6 @@ def calculate_home_energy_bills(self, home_uuid: str) -> None:
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)

Expand Down
62 changes: 40 additions & 22 deletions src/gsy_e/models/strategy/strategy_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ def get_value(self, _time_slot: DateTime):
class EmptyProfile(StrategyProfileBase):
"""Empty profile class"""

def __init__(
self, profile_type: InputProfileTypes = None):
def __init__(self, profile_type: InputProfileTypes = None):
super().__init__()
self.profile = {}
self.profile_type = profile_type
Expand All @@ -56,8 +55,12 @@ class StrategyProfile(StrategyProfileBase):
"""Manage reading/rotating energy profile of an asset."""

def __init__(
self, input_profile=None, input_profile_uuid=None,
input_energy_rate=None, profile_type: InputProfileTypes = None):
self,
input_profile=None,
input_profile_uuid=None,
input_energy_rate=None,
profile_type: InputProfileTypes = None,
):
# pylint: disable=super-init-not-called

self.input_profile = input_profile
Expand All @@ -79,23 +82,33 @@ def __init__(
self.profile_type = profile_type

def get_value(self, time_slot: DateTime) -> float:
if not self.profile:
return 0
if time_slot in self.profile:
return self.profile[time_slot]
if GlobalConfig.is_canary_network() or is_scm_simulation():
value = get_from_profile_same_weekday_and_time(self.profile, time_slot)
if value is None:
log.error("Value for time_slot %s could not be found in profile %s in "
"Canary Network, returning 0", time_slot, self.input_profile_uuid)
log.error(
"Value for time_slot %s could not be found in profile %s in "
"Canary Network, returning 0",
time_slot,
self.input_profile_uuid,
)
return 0
return value
log.error("Value for time_slot %s could not be found in profile "
"%s.", time_slot, self.input_profile_uuid)
log.error(
"Value for time_slot %s could not be found in profile %s.",
time_slot,
self.input_profile_uuid,
)
return 0

def _read_input_profile_type(self):
if self.input_profile_uuid:
self.profile_type = global_objects.profiles_handler.get_profile_type(
self.input_profile_uuid)
self.input_profile_uuid
)
elif self.input_energy_rate is not None:
self.profile_type = InputProfileTypes.IDENTITY
else:
Expand All @@ -106,23 +119,28 @@ def read_or_rotate_profiles(self, reconfigure=False):
self._read_input_profile_type()

if not self.profile or reconfigure:
profile = (self.input_energy_rate
if self.input_energy_rate is not None
else self.input_profile)
profile = (
self.input_energy_rate
if self.input_energy_rate is not None
else self.input_profile
)
else:
profile = self.profile

self.profile = global_objects.profiles_handler.rotate_profile(
profile_type=self.profile_type,
profile=profile,
profile_uuid=self.input_profile_uuid)
profile_type=self.profile_type, profile=profile, profile_uuid=self.input_profile_uuid
)


def profile_factory(input_profile=None, input_profile_uuid=None,
input_energy_rate=None,
profile_type: InputProfileTypes = None) -> StrategyProfileBase:
def profile_factory(
input_profile=None,
input_profile_uuid=None,
input_energy_rate=None,
profile_type: InputProfileTypes = None,
) -> StrategyProfileBase:
"""Return correct profile handling class for input parameters."""
return (EmptyProfile(profile_type)
if input_profile is None and input_profile_uuid is None and input_energy_rate is None
else StrategyProfile(
input_profile, input_profile_uuid, input_energy_rate, profile_type))
return (
EmptyProfile(profile_type)
if input_profile is None and input_profile_uuid is None and input_energy_rate is None
else StrategyProfile(input_profile, input_profile_uuid, input_energy_rate, profile_type)
)
29 changes: 18 additions & 11 deletions tests/area/test_coefficient_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@

import uuid
from math import isclose
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

import pytest
from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE, TIME_ZONE
from gsy_framework.constants_limits import ConstSettings, TIME_ZONE
from gsy_framework.enums import (
SpotMarketTypeEnum,
CoefficientAlgorithm,
Expand All @@ -30,6 +29,7 @@
)
from pendulum import duration, today
from pendulum import now
import pytest

from gsy_e.models.area import CoefficientArea, CoefficientAreaException
from gsy_e.models.area.scm_manager import (
Expand Down Expand Up @@ -308,8 +308,12 @@ def test_trigger_energy_trades(_create_2_house_grid, intracommunity_base_rate):
assert isclose(scm._bills[house2.uuid].gsy_energy_bill, -0.02)
assert isclose(scm._bills[house2.uuid].export_grid_fees, 0.0)

assert isclose(scm._bills[house2.uuid].savings, 0.0, abs_tol=FLOATING_POINT_TOLERANCE)
assert isclose(scm._bills[house2.uuid].savings_percent, 0.0)
if intracommunity_base_rate is None:
assert isclose(scm._bills[house2.uuid].savings, 0.0114)
assert isclose(scm._bills[house2.uuid].savings_percent, 0.0)
else:
assert isclose(scm._bills[house2.uuid].savings, 0.015)
assert isclose(scm._bills[house2.uuid].savings_percent, 0.0)
assert len(scm._home_data[house1.uuid].trades) == 2
trades = scm._home_data[house1.uuid].trades
assert isclose(trades[0].trade_rate, 0.3)
Expand All @@ -336,12 +340,15 @@ def test_trigger_energy_trades(_create_2_house_grid, intracommunity_base_rate):

@staticmethod
def test_calculate_energy_benchmark():
bills = AreaEnergyBills()
bills.set_min_max_community_savings(10, 90)
bills.base_energy_bill_excl_revenue = 1.0
bills.gsy_energy_bill = 0.4
assert isclose(bills.savings_percent, 60.0)
assert isclose(bills.energy_benchmark, (60 - 10) / (90 - 10))
with patch(
"gsy_e.models.area.scm_manager.AreaEnergyBills.savings_from_buy_from_community", 0.6
):
bills = AreaEnergyBills()
bills.set_min_max_community_savings(10, 90)
bills.base_energy_bill_excl_revenue = 1.0
bills.gsy_energy_bill = 0.4
assert isclose(bills.savings_percent, 60.0)
assert isclose(bills.energy_benchmark, (60 - 10) / (90 - 10))

@staticmethod
@pytest.fixture()
Expand Down

0 comments on commit 1567499

Please sign in to comment.