File size: 7,100 Bytes
e8def5c
 
 
 
 
cb62f63
e8def5c
 
 
 
 
 
4da0b0d
e8def5c
 
4da0b0d
 
e8def5c
 
cb62f63
e8def5c
 
 
 
 
 
cb62f63
e8def5c
 
 
 
cb62f63
e8def5c
4da0b0d
 
e8def5c
4da0b0d
e8def5c
 
 
 
 
 
 
 
 
 
 
 
4da0b0d
 
 
e8def5c
 
4da0b0d
e8def5c
 
 
a13327d
 
 
e8def5c
 
 
 
 
 
 
 
 
 
 
 
4da0b0d
eb9eaaf
e8def5c
a13327d
 
 
 
 
 
e8def5c
 
cb62f63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8def5c
 
 
 
 
 
 
 
 
 
 
cb62f63
e8def5c
 
 
 
 
 
cb62f63
e8def5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4da0b0d
e8def5c
4da0b0d
e8def5c
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# modeling an energy supplier for the purposes of peak shaving

import numpy as np
import pandas as pd
import datetime
from dataclasses import dataclass
import unittest


class Supplier:
    # price [HUF/kWh]
    # peak_demand kW
    # surcharge_per_kwh [HUF/kWh for each 15 minute timeframe]
    def __init__(self, price):
        self.hourly_prices = np.ones(168) * price
        self.peak_demand = np.inf # [kWh] (!) no demand_charge by default
        self.surcharge_per_kwh = 0

    # start and end are indices of hours starting from Monday 00:00.
    def set_price_for_weekly_interval(self, start, end, price):
        self.hourly_prices[start:end] = price

    # start and end are indices of hours of the day. for each day, this interval is set to price
    def set_price_for_daily_interval(self, start, end, price):
        for day in range(7):
            h = day * 24
            self.set_price_for_weekly_interval(h + start, h + end, price)

    def set_price_for_daily_interval_on_workdays(self, start, end, price):
        for day in range(5):
            h = day * 24
            self.set_price_for_weekly_interval(h + start, h + end, price)

    def set_demand_charge(self, peak_demand, surcharge_per_kwh):
        self.peak_demand = peak_demand # [kWh]
        # the HUF charged per kW of demand exceeding peak_demand during a 15 minutes timeframe.
        self.surcharge_per_kwh = surcharge_per_kwh # [HUF/kWh]

    @staticmethod
    def hour_of_date(date):
        hours_since_midnight = (date - datetime.datetime(date.year, date.month, date.day, 0, 0, 0)).total_seconds() / 3600
        # weekday() calculates from sunday morning:
        hungarian_weekday = (date.weekday() + 0) % 7
        hours_elapsed_in_previous_days = hungarian_weekday * 24
        return int(hours_since_midnight) + hours_elapsed_in_previous_days

    def price(self, date):
        return self.hourly_prices[self.hour_of_date(date)]

    # demand is the maximum demand in kWh during a 15 minute interval
    def demand_charge(self, demand_in_kwh):
        if demand_in_kwh <= self.peak_demand:
            return 0.0
        else:
            return (demand_in_kwh - self.peak_demand) * self.surcharge_per_kwh

    # demand_series is pandas series indexed by time.
    # during each time step demand [kW] is assumed to be constant.
    #
    # TODO the provide_detail returned value types are inconsistent and confusing.
    def fee(self, demand_series, provide_detail=False):
        prices = [self.price(date) for date in demand_series.index]
        prices_series = pd.Series(data=prices, index=demand_series.index)
        # prices are HUF/kWh, demand is kW. note the missing h.

        step_in_hour = demand_series.index.freq.n / 60 # [hour], the length of a time step.
        # for each step the product tells the fee IF the step was 1 hour long. it's actually step_in_hour long:
        consumption_charge = demand_series.dot(prices_series) * step_in_hour

        # 15 minutes (the demand charge calculation interval) should be a multiple of the series time step.
        assert 15 % demand_series.index.freq.n == 0
        time_steps_per_demand_charge_evaluation = 15 // demand_series.index.freq.n
        # fifteen_minute_peaks [kW] tells the maximum demand in a 15 minutes timeframe:
        fifteen_minute_demands_in_kwh = demand_series.resample('15T').sum() * step_in_hour
        demand_charges = pd.Series([self.demand_charge(demand_in_kwh) for demand_in_kwh in fifteen_minute_demands_in_kwh], index=fifteen_minute_demands_in_kwh.index)
        total_demand_charge = sum(demand_charges)
        total_charge = consumption_charge + total_demand_charge
        if provide_detail:
            consumption_charge_series = demand_series * prices_series * step_in_hour
            return total_charge, consumption_charge_series, demand_charges
        else:
            return total_charge


@dataclass
class PrecalculatedSupplier:
    peak_demand: float # [kWh]
    surcharge_per_kwh: float # [HUF / kWh surplus over 15 min interval]
    consumption_fees: np.ndarray
    time_index: pd.DatetimeIndex


def precalculate_supplier(supplier, time_index):
    ones = pd.Series(1, index=time_index)
    total_charge, consumption_charge_series, demand_charges = supplier.fee(ones, provide_detail=True)

    p = PrecalculatedSupplier(
        peak_demand=supplier.peak_demand,
        surcharge_per_kwh=supplier.surcharge_per_kwh,
        consumption_fees=consumption_charge_series.to_numpy(),
        time_index=time_index)
    return p


class TestSupplier(unittest.TestCase):

    def setUp(self):
        self.constant_price = 10
        self.supplier = Supplier(self.constant_price)

    def test_hourly_prices(self):
        expected_hourly_prices = np.ones(168) * self.constant_price
        self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))

    def test_set_price_for_interval(self):
        self.supplier.set_price_for_weekly_interval(0, 24, 20)
        expected_hourly_prices = np.ones(168) * self.constant_price
        expected_hourly_prices[0:24] = 20
        self.assertTrue(np.array_equal(self.supplier.hourly_prices, expected_hourly_prices))

    def test_price(self):
        increased_price = 20
        self.supplier.set_price_for_weekly_interval(0, 24, increased_price)

        date = datetime.datetime(2023, 4, 30, 12, 0, 0)  # Sunday noon
        expected_price = self.constant_price
        self.assertEqual(self.supplier.price(date), expected_price)

        date = datetime.datetime(2023, 5, 1, 12, 0, 0)  # Monday noon
        expected_price = increased_price
        self.assertEqual(self.supplier.price(date), expected_price)

        date = datetime.datetime(2023, 5, 2, 12, 0, 0)  # Tuesday noon
        expected_price = self.constant_price
        self.assertEqual(self.supplier.price(date), expected_price)

    def test_fee(self):
        start = pd.Timestamp('2021-04-28')
        end = start + pd.Timedelta(days=1)
        freq = '5T' # 5 minutes
        time_index = pd.date_range(start=start, end=end, freq=freq, inclusive='left')
        constant_demand = 100
        demand_in_kw = [constant_demand] * len(time_index)

        demand_series = pd.Series(data=demand_in_kw, index=time_index)
        # 24 because it's a 24 hour period with constant demand:
        self.assertEqual(self.supplier.fee(demand_series), constant_demand * 24 * self.constant_price)

        extreme_demand = 1000
        demand_series[12:24] = extreme_demand # in second hour we set extreme demand.

        expected_fee = (constant_demand * 23 + extreme_demand) * self.constant_price
        self.assertEqual(self.supplier.fee(demand_series), expected_fee)

        # now the (1000-500) kW above 500 kW is surcharged for (1000-500 kW) * 10 HUF/kWh/15mins, for 1 hour,
        # that is 500*10*4=20000 demand_charge.
        self.supplier.set_demand_charge(peak_demand=500, surcharge_per_kwh=10)
        expected_fee += 20000
        self.assertEqual(self.supplier.fee(demand_series), expected_fee)



if __name__ == '__main__':
    unittest.main()