malfunction_generators.py 10.5 KB
Newer Older
1
2
"""Malfunction generators for rail systems"""

Erik Nygren's avatar
Erik Nygren committed
3
from typing import Callable, NamedTuple, Optional, Tuple
4

Erik Nygren's avatar
Erik Nygren committed
5
6
import numpy as np
from numpy.random.mtrand import RandomState
7

8
9
from flatland.envs.agent_utils import EnvAgent
from flatland.envs.step_utils.states import TrainState
MasterScrat's avatar
MasterScrat committed
10
from flatland.envs import persistence
11

12
13

# why do we have both MalfunctionParameters and MalfunctionProcessData - they are both the same!
Erik Nygren's avatar
Erik Nygren committed
14
15
MalfunctionParameters = NamedTuple('MalfunctionParameters',
                                   [('malfunction_rate', float), ('min_duration', int), ('max_duration', int)])
Erik Nygren's avatar
Erik Nygren committed
16
17
MalfunctionProcessData = NamedTuple('MalfunctionProcessData',
                                    [('malfunction_rate', float), ('min_duration', int), ('max_duration', int)])
18

19
20
21
Malfunction = NamedTuple('Malfunction', [('num_broken_steps', int)])

# Why is the return value Optional?  We always return a Malfunction.
Dipam Chakraborty's avatar
Dipam Chakraborty committed
22
MalfunctionGenerator = Callable[[RandomState, bool], Malfunction]
Erik Nygren's avatar
Erik Nygren committed
23
24
25
26
27
28
29
30
31
32

def _malfunction_prob(rate: float) -> float:
    """
    Probability of a single agent to break. According to Poisson process with given rate
    :param rate:
    :return:
    """
    if rate <= 0:
        return 0.
    else:
MasterScrat's avatar
MasterScrat committed
33
        return 1 - np.exp(-rate)
Erik Nygren's avatar
Erik Nygren committed
34

35
36
37
38
39
40
41
42
43
44
45
class ParamMalfunctionGen(object):
    """ Preserving old behaviour of using MalfunctionParameters for constructor,
        but returning MalfunctionProcessData in get_process_data.  
        Data structure and content is the same.
    """
    def __init__(self, parameters: MalfunctionParameters):
        #self.mean_malfunction_rate = parameters.malfunction_rate
        #self.min_number_of_steps_broken = parameters.min_duration
        #self.max_number_of_steps_broken = parameters.max_duration
        self.MFP = parameters

Dipam Chakraborty's avatar
Dipam Chakraborty committed
46
    def generate(self, np_random: RandomState) -> Malfunction:
47

Dipam Chakraborty's avatar
Dipam Chakraborty committed
48
49
50
51
52
53
        if np_random.rand() < _malfunction_prob(self.MFP.malfunction_rate):
            num_broken_steps = np_random.randint(self.MFP.min_duration,
                                                    self.MFP.max_duration + 1) + 1
        else:
            num_broken_steps = 0
        return Malfunction(num_broken_steps)
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

    def get_process_data(self):
        return MalfunctionProcessData(*self.MFP)


class NoMalfunctionGen(ParamMalfunctionGen):
    def __init__(self):
        super().__init__(MalfunctionParameters(0,0,0))

class FileMalfunctionGen(ParamMalfunctionGen):
    def __init__(self, env_dict=None, filename=None, load_from_package=None):
        """ uses env_dict if populated, otherwise tries to load from file / package.
        """
        if env_dict is None:
             env_dict = persistence.RailEnvPersister.load_env_dict(filename, load_from_package=load_from_package)

        if "malfunction" in env_dict:
            oMFP = MalfunctionParameters(*env_dict["malfunction"])
        else:
            oMFP = MalfunctionParameters(0,0,0)  # no malfunctions
        super().__init__(oMFP)


################################################################################################
# OLD / DEPRECATED generator functions below. To be removed.

def no_malfunction_generator() -> Tuple[MalfunctionGenerator, MalfunctionProcessData]:
    """
    Malfunction generator which generates no malfunctions

    Parameters
    ----------
    Nothing

    Returns
    -------
    generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken
    """
    print("DEPRECATED - use NoMalfunctionGen instead of no_malfunction_generator")
    # Mean malfunction in number of time steps
    mean_malfunction_rate = 0.

    # Uniform distribution parameters for malfunction duration
    min_number_of_steps_broken = 0
    max_number_of_steps_broken = 0

Dipam Chakraborty's avatar
Dipam Chakraborty committed
100
    def generator(np_random: RandomState = None) -> Malfunction:
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
        return Malfunction(0)

    return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken,
                                             max_number_of_steps_broken)



def single_malfunction_generator(earlierst_malfunction: int, malfunction_duration: int) -> Tuple[
    MalfunctionGenerator, MalfunctionProcessData]:
    """
    Malfunction generator which guarantees exactly one malfunction during an episode of an ACTIVE agent.

    Parameters
    ----------
    earlierst_malfunction: Earliest possible malfunction onset
    malfunction_duration: The duration of the single malfunction

    Returns
    -------
    generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken
    """
    # Mean malfunction in number of time steps
    mean_malfunction_rate = 0.

    # Uniform distribution parameters for malfunction duration
    min_number_of_steps_broken = 0
    max_number_of_steps_broken = 0

    # Keep track of the total number of malfunctions in the env
    global_nr_malfunctions = 0

    # Malfunction calls per agent
    malfunction_calls = dict()

    def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]:
        # We use the global variable to assure only a single malfunction in the env
        nonlocal global_nr_malfunctions
        nonlocal malfunction_calls

        # Reset malfunciton generator
        if reset:
            nonlocal global_nr_malfunctions
            nonlocal malfunction_calls
            global_nr_malfunctions = 0
            malfunction_calls = dict()
            return Malfunction(0)

        # No more malfunctions if we already had one, ignore all updates
        if global_nr_malfunctions > 0:
            return Malfunction(0)

        # Update number of calls per agent
        if agent.handle in malfunction_calls:
            malfunction_calls[agent.handle] += 1
        else:
            malfunction_calls[agent.handle] = 1

        # Break an agent that is active at the time of the malfunction
159
160
        if (agent.state == TrainState.MOVING or agent.state == TrainState.STOPPED) \
            and malfunction_calls[agent.handle] >= earlierst_malfunction: #TODO : Dipam : Is this needed?
161
162
163
164
165
166
167
168
            global_nr_malfunctions += 1
            return Malfunction(malfunction_duration)
        else:
            return Malfunction(0)

    return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken,
                                             max_number_of_steps_broken)

Erik Nygren's avatar
Erik Nygren committed
169

170
def malfunction_from_file(filename: str, load_from_package=None) -> Tuple[MalfunctionGenerator, MalfunctionProcessData]:
171
172
173
174
175
176
177
178
179
    """
    Utility to load pickle file

    Parameters
    ----------
    input_file : Pickle file generated by env.save() or editor

    Returns
    -------
Erik Nygren's avatar
Erik Nygren committed
180
    generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken
181
    """
MasterScrat's avatar
MasterScrat committed
182

183
184
    print("DEPRECATED - use FileMalfunctionGen instead of malfunction_from_file")

185
    env_dict = persistence.RailEnvPersister.load_env_dict(filename, load_from_package=load_from_package)
186
    # TODO: make this better by using namedtuple in the pickle file. See issue 282
187
188
189
    if "malfunction" in env_dict:
        env_dict['malfunction'] = oMPD = MalfunctionProcessData._make(env_dict['malfunction'])
    else:
MasterScrat's avatar
MasterScrat committed
190
        oMPD = None
191
    if oMPD is not None:
Erik Nygren's avatar
Erik Nygren committed
192
        # Mean malfunction in number of time steps
193
        mean_malfunction_rate = oMPD.malfunction_rate
Erik Nygren's avatar
Erik Nygren committed
194
195

        # Uniform distribution parameters for malfunction duration
196
197
        min_number_of_steps_broken = oMPD.min_duration
        max_number_of_steps_broken = oMPD.max_duration
Erik Nygren's avatar
Erik Nygren committed
198
199
200
201
202
203
204
    else:
        # Mean malfunction in number of time steps
        mean_malfunction_rate = 0.
        # Uniform distribution parameters for malfunction duration
        min_number_of_steps_broken = 0
        max_number_of_steps_broken = 0

Erik Nygren's avatar
Erik Nygren committed
205
    def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]:
Erik Nygren's avatar
Erik Nygren committed
206
207
208
209
210
211
212
213
214
215
216
        """
        Generate malfunctions for agents
        Parameters
        ----------
        agent
        np_random

        Returns
        -------
        int: Number of time steps an agent is broken
        """
Erik Nygren's avatar
Erik Nygren committed
217
218
219
220
221

        # Dummy reset function as we don't implement specific seeding here
        if reset:
            return Malfunction(0)

Erik Nygren's avatar
Erik Nygren committed
222
223
224
225
226
227
228
229
230
231
232
        if agent.malfunction_data['malfunction'] < 1:
            if np_random.rand() < _malfunction_prob(mean_malfunction_rate):
                num_broken_steps = np_random.randint(min_number_of_steps_broken,
                                                     max_number_of_steps_broken + 1) + 1
                return Malfunction(num_broken_steps)
        return Malfunction(0)

    return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken,
                                             max_number_of_steps_broken)


Erik Nygren's avatar
Erik Nygren committed
233
def malfunction_from_params(parameters: MalfunctionParameters) -> Tuple[MalfunctionGenerator, MalfunctionProcessData]:
234
235
236
237
238
    """
    Utility to load malfunction from parameters

    Parameters
    ----------
Erik Nygren's avatar
Erik Nygren committed
239
240

    parameters : contains all the parameters of the malfunction
241
        malfunction_rate : float rate per timestep at which each agent malfunctions
Erik Nygren's avatar
Erik Nygren committed
242
243
        min_duration : int minimal duration of a failure
        max_number_of_steps_broken : int maximal duration of a failure
244
245
246

    Returns
    -------
Erik Nygren's avatar
Erik Nygren committed
247
    generator, Tuple[float, int, int] with mean_malfunction_rate, min_number_of_steps_broken, max_number_of_steps_broken
248
    """
249
250
251
    
    print("DEPRECATED - use ParamMalfunctionGen instead of malfunction_from_params")

Erik Nygren's avatar
Erik Nygren committed
252
253
254
    mean_malfunction_rate = parameters.malfunction_rate
    min_number_of_steps_broken = parameters.min_duration
    max_number_of_steps_broken = parameters.max_duration
Erik Nygren's avatar
Erik Nygren committed
255

Erik Nygren's avatar
Erik Nygren committed
256
    def generator(agent: EnvAgent = None, np_random: RandomState = None, reset=False) -> Optional[Malfunction]:
Erik Nygren's avatar
Erik Nygren committed
257
258
259
260
261
262
263
264
265
266
267
        """
        Generate malfunctions for agents
        Parameters
        ----------
        agent
        np_random

        Returns
        -------
        int: Number of time steps an agent is broken
        """
Erik Nygren's avatar
Erik Nygren committed
268
269
270
271
272

        # Dummy reset function as we don't implement specific seeding here
        if reset:
            return Malfunction(0)

Erik Nygren's avatar
Erik Nygren committed
273
274
275
276
277
278
279
280
281
282
        if agent.malfunction_data['malfunction'] < 1:
            if np_random.rand() < _malfunction_prob(mean_malfunction_rate):
                num_broken_steps = np_random.randint(min_number_of_steps_broken,
                                                     max_number_of_steps_broken + 1) + 1
                return Malfunction(num_broken_steps)
        return Malfunction(0)

    return generator, MalfunctionProcessData(mean_malfunction_rate, min_number_of_steps_broken,
                                             max_number_of_steps_broken)