File size: 4,982 Bytes
ffa5234
c5bd69c
ffa5234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f927c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffa5234
 
 
c5bd69c
ffa5234
 
c5bd69c
 
 
 
 
 
ffa5234
 
 
 
 
 
 
 
 
 
c5bd69c
 
 
 
 
83ff599
c5bd69c
 
 
4e673fc
83ff599
4e673fc
 
ffa5234
4e673fc
 
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
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
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


class RandomDecider:
    def __init__(self, params, precalculated_supplier):
        self.input_window_size = STEPS_PER_HOUR * 24 # day long window.
        self.precalculated_supplier = precalculated_supplier
        assert params.shape == (2, )
        # param_1 is prob of choosing PASSIVE
        # param_2 is prob of choosing NETWORK_CHARGE
        self.passive_prob, self.network_prob = params

    def decide(self, prod_pred, cons_pred, fees, battery_model):
        r = np.random.rand()
        if r < self.passive_prob:
            return Decision.PASSIVE
        elif r < self.passive_prob + self.network_prob:
            return Decision.NETWORK_CHARGE
        else:
            return Decision.DISCHARGE

    @staticmethod
    def clip_params(params):
        assert params.shape == (2, )
        p1, p2 = params
        p1 = max((0, p1))
        p2 = max((0, p2))
        s = p1 + p2
        if s > 1:
            p1 /= s
            p2 /= s
        return np.array([p1, p2])


    @staticmethod
    def initial_params():
        init_mean = np.array([1/3, 1/3])
        init_scale = np.array([1.0, 1.0])
        return init_mean, init_scale



# 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.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:
            decision = Decision.NETWORK_CHARGE
        # peak shaving
        elif deficit_kwh > self.precalculated_supplier.peak_demand:
            decision = Decision.DISCHARGE
        else:
            decision = Decision.PASSIVE
        return decision

    # 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