File size: 3,816 Bytes
ffa5234
c5bd69c
ffa5234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5bd69c
ffa5234
 
 
c5bd69c
 
 
 
 
 
ffa5234
 
 
 
 
 
 
 
 
 
c5bd69c
 
 
 
 
83ff599
c5bd69c
 
 
 
 
83ff599
ffa5234
 
 
 
 
c5bd69c
 
 
 
 
 
 
 
 
 
ffa5234
c5bd69c
 
 
 
 
83ff599
 
c5bd69c
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
from enum import IntEnum
import numpy as np


STEPS_PER_HOUR = 12


class Decision(IntEnum):
    # use solar to satisfy consumption,
    # and if it is not enough, use network.
    # BESS is not discharged in this mode,
    # but might be charged if solar has surplus.
    PASSIVE = 0

    # use the battery if possible and necessary.
    # the possible part means that there's charge in it,
    # and the necessary part means that the consumption
    # is not already covered by solar.
    DISCHARGE = 1

    # use the network to charge the battery
    # this is similar to PASSIVE, but forces the
    # BESS to be charged, even if solar does not cover
    # the whole of consumption plus BESS.
    NETWORK_CHARGE = 2


# mock class as usual
# output_window_size is not yet used, always decides one timestep.
class Decider:
    def __init__(self, params, precalculated_supplier):
        self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
        self.random_seed = 0
        self.precalculated_supplier = precalculated_supplier
        assert params.shape == (2, )
        # param_1 is how many minutes do we look ahead to decide if there's
        # an upcoming shortage.
        # param_2 is the threshhold for shortage, in kW not kWh,
        # that is, parametrized as an average over the window.
        self.surdemand_lookahead_window_min, self.lookahead_surdemand_kw = params

    def decide(self, prod_pred, cons_pred, fees, battery_model):
        # TODO 15 minutes demand charge window hardwired at this weird place
        DEMAND_CHARGE_WINDOW_MIN = 15
        time_interval_min = self.precalculated_supplier.time_index.freq.n
        assert DEMAND_CHARGE_WINDOW_MIN % time_interval_min == 0
        peak_shaving_window = DEMAND_CHARGE_WINDOW_MIN // time_interval_min
        step_in_hour = time_interval_min / 60 # [hour], the length of a time step.
        deficit_kw = (cons_pred[:peak_shaving_window] - prod_pred[:peak_shaving_window]).clip(min=0)
        deficit_kwh = (step_in_hour * deficit_kw).sum()

        surdemand_window = int(self.surdemand_lookahead_window_min // time_interval_min)
        mean_surdemand_kw = (cons_pred[:surdemand_window] - prod_pred[:surdemand_window]).mean()

        current_fee = fees[0]
        # TODO this should not be a hard threshold, and more importantly,
        # it should not be hardwired.
        HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH = 20 # [HUF/kWh]
        if mean_surdemand_kw > self.lookahead_surdemand_kw and current_fee <= HARDWIRED_THRESHOLD_FOR_CHEAP_POWER_HUF_PER_KWH:
            return Decision.NETWORK_CHARGE

        # peak shaving
        if deficit_kwh > self.precalculated_supplier.peak_demand:
            return Decision.DISCHARGE
        else:
            return Decision.PASSIVE

    # this is called by the optimizer so that meaningless parameter settings are not attempted
    # we could vectorize this easily, but it's not a bottleneck, the simulation is.
    @staticmethod
    def clip_params(params):
        assert params.shape == (2, )
        surdemand_lookahead_window_min, lookahead_surdemand_kw = params
        surdemand_lookahead_window_min = np.clip(surdemand_lookahead_window_min, 5, 60 * 24 * 3)
        # no-op right now:
        lookahead_surdemand_kw = np.clip(lookahead_surdemand_kw, -np.inf, np.inf)
        return np.array([surdemand_lookahead_window_min, lookahead_surdemand_kw])

    @staticmethod
    def initial_params():
        # surdemand_lookahead_window_min, lookahead_surdemand_kw
        # param1 [minutes]. one day mean, half day scale for lookahead horizon.
        # param2 [kWh], averaged over the horizon. negative values are meaningful.
        init_mean = np.array([60 * 24.0, 0.0])
        init_scale = np.array([60 * 12.0, 100.0])
        return init_mean, init_scale