Browse Source

Enhance behavior cyclyng and time base + tests. Related to Issue #17

Bastien Sevajol 6 years ago
parent
commit
1a2c48c45a
3 changed files with 397 additions and 46 deletions
  1. 25 13
      synergine2/cycle.py
  2. 37 26
      synergine2/simulation.py
  3. 335 7
      tests/test_simulation.py

+ 25 - 13
synergine2/cycle.py View File

117
             ))
117
             ))
118
 
118
 
119
         for behaviour in behaviours:
119
         for behaviour in behaviours:
120
-            behaviour_data = behaviour.run(mechanisms_data)  # TODO: Behaviours dependencies
121
-            if self.logger.is_debug:
122
-                self.logger.debug('{} behaviour produce data: {}'.format(
123
-                    type(behaviour).__name__,
124
-                    behaviour_data,
120
+            if behaviour.is_skip(self.current_cycle):
121
+                behaviour_data = False
122
+                self.logger.debug('Simulation: behaviour {} skip'.format(
123
+                    str(type(behaviour)),
125
                 ))
124
                 ))
125
+            else:
126
+                # TODO: Behaviours dependencies
127
+                behaviour_data = behaviour.run(mechanisms_data)
128
+                if self.logger.is_debug:
129
+                    self.logger.debug('{} behaviour produce data: {}'.format(
130
+                        type(behaviour).__name__,
131
+                        behaviour_data,
132
+                    ))
126
 
133
 
127
             if behaviour_data:
134
             if behaviour_data:
128
                 behaviours_data[behaviour.__class__] = behaviour_data
135
                 behaviours_data[behaviour.__class__] = behaviour_data
302
                 if behaviour.is_terminated():
309
                 if behaviour.is_terminated():
303
                     del subject.behaviours[behaviour_key]
310
                     del subject.behaviours[behaviour_key]
304
 
311
 
305
-                # We identify behaviour data with it's class to be able to intersect it after subprocess data collect
312
+                # We identify behaviour data with it's class to be able to intersect
313
+                # it after subprocess data collect
306
                 if behaviour.is_skip(self.current_cycle):
314
                 if behaviour.is_skip(self.current_cycle):
307
                     behaviour_data = False
315
                     behaviour_data = False
308
                     self.logger.debug('Subject {}: behaviour {} skip'.format(
316
                     self.logger.debug('Subject {}: behaviour {} skip'.format(
312
                 else:
320
                 else:
313
                     with time_it() as elapsed_time:
321
                     with time_it() as elapsed_time:
314
                         behaviour.last_execution_time = time.time()
322
                         behaviour.last_execution_time = time.time()
315
-                        behaviour_data = behaviour.run(mechanisms_data)  # TODO: Behaviours dependencies
323
+                        # TODO: Behaviours dependencies
324
+                        behaviour_data = behaviour.run(mechanisms_data)
316
                         if self.logger.is_debug:
325
                         if self.logger.is_debug:
317
-                            self.logger.debug('Subject {}: behaviour {} produce data: {} in {}s'.format(
318
-                                str(type(behaviour)),
319
-                                str(subject.id),
320
-                                str(behaviour_data),
321
-                                elapsed_time.get_time(),
322
-                            ))
326
+                            self.logger.debug(
327
+                                'Subject {}: behaviour {} produce '
328
+                                'data: {} in {}s'.format(
329
+                                    str(type(behaviour)),
330
+                                    str(subject.id),
331
+                                    str(behaviour_data),
332
+                                    elapsed_time.get_time(),
333
+                                )
334
+                            )
323
 
335
 
324
                 if behaviour_data:
336
                 if behaviour_data:
325
                     behaviours_data[behaviour.__class__] = behaviour_data
337
                     behaviours_data[behaviour.__class__] = behaviour_data

+ 37 - 26
synergine2/simulation.py View File

183
 
183
 
184
 class Simulation(BaseObject):
184
 class Simulation(BaseObject):
185
     accepted_subject_class = Subjects
185
     accepted_subject_class = Subjects
186
-    behaviours_classes = []
186
+    behaviours_classes = []  # type: typing.List[typing.Type[SimulationBehaviour]]
187
 
187
 
188
     subject_classes = shared.create('subject_classes', {})
188
     subject_classes = shared.create('subject_classes', {})
189
     collections = shared.create('collections', {})
189
     collections = shared.create('collections', {})
254
         self.simulation = simulation
254
         self.simulation = simulation
255
         self.subject = subject
255
         self.subject = subject
256
 
256
 
257
-    def run(self):
257
+    def run(self) -> dict:
258
         raise NotImplementedError()
258
         raise NotImplementedError()
259
 
259
 
260
 
260
 
283
 
283
 
284
 
284
 
285
 class Behaviour(BaseObject):
285
 class Behaviour(BaseObject):
286
+    def __init__(self):
287
+        self.last_execution_time = 0
288
+
289
+    @property
290
+    def cycle_frequency(self) -> typing.Optional[float]:
291
+        return None
292
+
293
+    @property
294
+    def seconds_frequency(self) -> typing.Optional[float]:
295
+        """
296
+        If this behaviour is time based, return here the waiting time between two
297
+        executions. IMPORTANT: your behaviour have to update it's
298
+        self.last_execution_time attribute when executed (in `run` method)!
299
+        :return: float number of period in seconds
300
+        """
301
+        return None
302
+
286
     def run(self, data):
303
     def run(self, data):
287
         raise NotImplementedError()
304
         raise NotImplementedError()
288
 
305
 
306
+    def is_skip(self, cycle_number: int) -> bool:
307
+        """
308
+        :return: True if behaviour have to be skip this time
309
+        """
310
+        cycle_frequency = self.cycle_frequency
311
+        if cycle_frequency is not None:
312
+            return not bool(cycle_number % cycle_frequency)
313
+
314
+        seconds_frequency = self.seconds_frequency
315
+        if seconds_frequency is not None:
316
+            return time.time() - self.last_execution_time <= seconds_frequency
317
+
318
+        return False
319
+
289
 
320
 
290
 class SubjectBehaviour(Behaviour):
321
 class SubjectBehaviour(Behaviour):
291
     use = []  # type: typing.List[typing.Type[SubjectMechanism]]
322
     use = []  # type: typing.List[typing.Type[SubjectMechanism]]
296
             simulation: Simulation,
327
             simulation: Simulation,
297
             subject: Subject,
328
             subject: Subject,
298
     ):
329
     ):
330
+        super().__init__()
299
         self.config = config
331
         self.config = config
300
         self.simulation = simulation
332
         self.simulation = simulation
301
         self.subject = subject
333
         self.subject = subject
302
-        self.last_execution_time = 0
303
-
304
-    @property
305
-    def cycle_frequency(self) -> typing.Optional[float]:
306
-        return None
307
-
308
-    @property
309
-    def seconds_frequency(self) -> typing.Optional[float]:
310
-        return None
311
 
334
 
312
     def is_terminated(self) -> bool:
335
     def is_terminated(self) -> bool:
313
         """
336
         """
315
         """
338
         """
316
         return False
339
         return False
317
 
340
 
318
-    def is_skip(self, cycle_number: int) -> bool:
319
-        """
320
-        :return: True if behaviour have to be skip this time
321
-        """
322
-        if self.cycle_frequency is not None:
323
-            return not bool(cycle_number % self.cycle_frequency)
324
-
325
-        if self.seconds_frequency is not None:
326
-            return time.time() - self.last_execution_time <= self.seconds_frequency
327
-
328
-        return False
329
-
330
-    def run(self, data):
341
+    def run(self, data) -> object:
331
         """
342
         """
332
         Method called in subprocess.
343
         Method called in subprocess.
333
         If return equivalent to False, behaviour produce nothing.
344
         If return equivalent to False, behaviour produce nothing.
347
 
358
 
348
 
359
 
349
 class SimulationBehaviour(Behaviour):
360
 class SimulationBehaviour(Behaviour):
350
-    frequency = 1
351
-    use = []
361
+    use = []  # type: typing.List[typing.Type[SimulationMechanism]]
352
 
362
 
353
     def __init__(
363
     def __init__(
354
             self,
364
             self,
355
             config: Config,
365
             config: Config,
356
             simulation: Simulation,
366
             simulation: Simulation,
357
     ):
367
     ):
368
+        super().__init__()
358
         self.config = config
369
         self.config = config
359
         self.simulation = simulation
370
         self.simulation = simulation
360
 
371
 

+ 335 - 7
tests/test_simulation.py View File

1
+import datetime
2
+import time
3
+
4
+from synergine2.config import Config
5
+from synergine2.cycle import CycleManager
6
+from synergine2.log import SynergineLogger
7
+from synergine2.processing import ProcessManager
8
+from synergine2.share import shared
9
+from synergine2.simulation import Simulation
10
+from synergine2.simulation import Subjects
11
+from synergine2.simulation import SubjectBehaviour
12
+from synergine2.simulation import SubjectMechanism
13
+from synergine2.simulation import Subject
14
+from synergine2.simulation import SimulationMechanism
15
+from synergine2.simulation import SimulationBehaviour
1
 from tests import BaseTest
16
 from tests import BaseTest
2
 
17
 
18
+from freezegun import freeze_time
19
+
20
+config = Config()
21
+logger = SynergineLogger('test')
22
+
23
+
24
+class MySubjectMechanism(SubjectMechanism):
25
+    def run(self):
26
+        return {'foo': 42}
27
+
28
+
29
+class MySimulationMechanism(SimulationMechanism):
30
+    def run(self, process_number: int=None, process_count: int=None):
31
+        return {'foo': 42}
32
+
33
+
34
+class MySubjectBehaviour(SubjectBehaviour):
35
+    use = [MySubjectMechanism]
36
+
37
+    def run(self, data):
38
+        return {'bar': data[MySubjectMechanism]['foo'] + 100}
39
+
40
+
41
+class MySimulationBehaviour(SimulationBehaviour):
42
+    use = [MySimulationMechanism]
43
+
44
+    def run(self, data):
45
+        return {'bar': data[MySimulationMechanism]['foo'] + 100}
46
+
47
+
48
+class MySimulation(Simulation):
49
+    behaviours_classes = [MySimulationBehaviour]
50
+
3
 
51
 
4
 class TestBehaviours(BaseTest):
52
 class TestBehaviours(BaseTest):
5
-    def test_behaviour_produce_data(self):
53
+    def test_subject_behaviour_produce_data(
54
+        self,
55
+        do_nothing_process_manager: ProcessManager,
56
+    ):
57
+        shared.reset()
58
+
59
+        class MySubject(Subject):
60
+            behaviours_classes = [MySubjectBehaviour]
61
+
62
+        simulation = Simulation(config)
63
+        my_subject = MySubject(config, simulation)
64
+        subjects = Subjects(simulation=simulation)
65
+        subjects.append(my_subject)
66
+        simulation.subjects = subjects
67
+
68
+        cycle_manager = CycleManager(
69
+            config,
70
+            logger,
71
+            simulation=simulation,
72
+            process_manager=do_nothing_process_manager,
73
+        )
74
+        results_by_subjects = cycle_manager._job_subjects(worker_id=0, process_count=1)
75
+        assert results_by_subjects
76
+        assert id(my_subject) in results_by_subjects
77
+        assert MySubjectBehaviour in results_by_subjects[id(my_subject)]
78
+        assert 'bar' in results_by_subjects[id(my_subject)][MySubjectBehaviour]
79
+        assert 142 == results_by_subjects[id(my_subject)][MySubjectBehaviour]['bar']
80
+
81
+    def test_simulation_behaviour_produce_data(
82
+        self,
83
+        do_nothing_process_manager: ProcessManager,
84
+    ):
85
+        shared.reset()
86
+        simulation = MySimulation(config)
87
+        subjects = Subjects(simulation=simulation)
88
+        simulation.subjects = subjects
89
+
90
+        cycle_manager = CycleManager(
91
+            config,
92
+            logger,
93
+            simulation=simulation,
94
+            process_manager=do_nothing_process_manager,
95
+        )
96
+        data = cycle_manager._job_simulation(worker_id=0, process_count=1)
97
+        assert data
98
+        assert MySimulationBehaviour in data
99
+        assert 'bar' in data[MySimulationBehaviour]
100
+        assert 142 == data[MySimulationBehaviour]['bar']
101
+
102
+    def test_subject_behaviour_cycle_frequency(
103
+        self,
104
+        do_nothing_process_manager: ProcessManager,
105
+    ):
106
+        shared.reset()
107
+
108
+        class MyCycledSubjectBehaviour(MySubjectBehaviour):
109
+            @property
110
+            def cycle_frequency(self):
111
+                return 2
112
+
113
+        class MySubject(Subject):
114
+            behaviours_classes = [MyCycledSubjectBehaviour]
115
+
116
+        simulation = Simulation(config)
117
+        my_subject = MySubject(config, simulation)
118
+        subjects = Subjects(simulation=simulation)
119
+        subjects.append(my_subject)
120
+        simulation.subjects = subjects
121
+
122
+        cycle_manager = CycleManager(
123
+            config,
124
+            logger,
125
+            simulation=simulation,
126
+            process_manager=do_nothing_process_manager,
127
+        )
128
+
129
+        # Cycle 0: behaviour NOT executed
130
+        cycle_manager.current_cycle = 0
131
+        results_by_subjects = cycle_manager._job_subjects(worker_id=0, process_count=1)
132
+        assert results_by_subjects
133
+        assert id(my_subject) in results_by_subjects
134
+        assert not results_by_subjects[id(my_subject)]
135
+
136
+        # Cycle 1: behaviour executed
137
+        cycle_manager.current_cycle = 1
138
+        results_by_subjects = cycle_manager._job_subjects(worker_id=0, process_count=1)
139
+        assert results_by_subjects
140
+        assert id(my_subject) in results_by_subjects
141
+        assert results_by_subjects[id(my_subject)]
142
+
143
+    def test_subject_behaviour_seconds_frequency(
144
+        self,
145
+        do_nothing_process_manager: ProcessManager,
146
+    ):
147
+        shared.reset()
148
+
149
+        class MyTimedSubjectBehaviour(MySubjectBehaviour):
150
+            @property
151
+            def seconds_frequency(self):
152
+                return 1.0
153
+
154
+            def run(self, data):
155
+                self.last_execution_time = time.time()
156
+                return super().run(data)
157
+
158
+        class MySubject(Subject):
159
+            behaviours_classes = [MyTimedSubjectBehaviour]
160
+
161
+        simulation = Simulation(config)
162
+        my_subject = MySubject(config, simulation)
163
+        subjects = Subjects(simulation=simulation)
164
+        subjects.append(my_subject)
165
+        simulation.subjects = subjects
166
+
167
+        cycle_manager = CycleManager(
168
+            config,
169
+            logger,
170
+            simulation=simulation,
171
+            process_manager=do_nothing_process_manager,
172
+        )
173
+
174
+        # Thirst time, behaviour IS executed
175
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0)):
176
+            data = cycle_manager._job_subjects(worker_id=0, process_count=1)
177
+            assert data
178
+            assert id(my_subject) in data
179
+            assert data[id(my_subject)]
180
+
181
+        # Less second after: NOT executed
182
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0, 500000)):
183
+            data = cycle_manager._job_subjects(worker_id=0, process_count=1)
184
+            assert data
185
+            assert id(my_subject) in data
186
+            assert not data[id(my_subject)]
187
+
188
+        # Less second after: NOT executed
189
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0, 700000)):
190
+            data = cycle_manager._job_subjects(worker_id=0, process_count=1)
191
+            assert data
192
+            assert id(my_subject) in data
193
+            assert not data[id(my_subject)]
194
+
195
+        # Less second after: NOT executed
196
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 1, 500000)):
197
+            data = cycle_manager._job_subjects(worker_id=0, process_count=1)
198
+            assert data
199
+            assert id(my_subject) in data
200
+            assert data[id(my_subject)]
201
+
202
+    def test_simulation_behaviour_cycle_frequency(
203
+        self,
204
+        do_nothing_process_manager: ProcessManager,
205
+    ):
206
+        shared.reset()
207
+
208
+        class MyCycledSimulationBehaviour(MySimulationBehaviour):
209
+            @property
210
+            def cycle_frequency(self):
211
+                return 2
212
+
213
+        class MyCycledSimulation(Simulation):
214
+            behaviours_classes = [MyCycledSimulationBehaviour]
215
+
216
+        simulation = MyCycledSimulation(config)
217
+        subjects = Subjects(simulation=simulation)
218
+        simulation.subjects = subjects
219
+
220
+        cycle_manager = CycleManager(
221
+            config,
222
+            logger,
223
+            simulation=simulation,
224
+            process_manager=do_nothing_process_manager,
225
+        )
226
+
227
+        # Cycle 0: behaviour NOT executed
228
+        cycle_manager.current_cycle = 0
229
+        data = cycle_manager._job_simulation(worker_id=0, process_count=1)
230
+        assert not data
231
+
232
+        # Cycle 1: behaviour executed
233
+        cycle_manager.current_cycle = 1
234
+        data = cycle_manager._job_simulation(worker_id=0, process_count=1)
235
+        assert data
236
+
237
+    def test_simulation_behaviour_seconds_frequency(
238
+        self,
239
+        do_nothing_process_manager: ProcessManager,
240
+    ):
241
+        shared.reset()
242
+
243
+        class MyTimedSimulationBehaviour(MySimulationBehaviour):
244
+            @property
245
+            def seconds_frequency(self):
246
+                return 1.0
247
+
248
+            def run(self, data):
249
+                self.last_execution_time = time.time()
250
+                return super().run(data)
251
+
252
+        class MyTimedSimulation(Simulation):
253
+            behaviours_classes = [MyTimedSimulationBehaviour]
254
+
255
+        simulation = MyTimedSimulation(config)
256
+        subjects = Subjects(simulation=simulation)
257
+        simulation.subjects = subjects
258
+
259
+        cycle_manager = CycleManager(
260
+            config,
261
+            logger,
262
+            simulation=simulation,
263
+            process_manager=do_nothing_process_manager,
264
+        )
265
+
266
+        # Thirst time, behaviour IS executed
267
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0)):
268
+            data = cycle_manager._job_simulation(worker_id=0, process_count=1)
269
+            assert data
270
+
271
+        # Less second after: NOT executed
272
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0, 500000)):
273
+            data = cycle_manager._job_simulation(worker_id=0, process_count=1)
274
+            assert not data
275
+
276
+        # Less second after: NOT executed
277
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 0, 700000)):
278
+            data = cycle_manager._job_simulation(worker_id=0, process_count=1)
279
+            assert not data
280
+
281
+        # More second after: IS executed
282
+        with freeze_time(datetime.datetime(2000, 12, 1, 0, 0, 1, 500000)):
283
+            data = cycle_manager._job_simulation(worker_id=0, process_count=1)
284
+            assert data
285
+
286
+    def test_subject_behavior_not_called_if_no_more_subjects(
287
+        self,
288
+        do_nothing_process_manager: ProcessManager,
289
+    ):
290
+        shared.reset()
291
+
292
+        class MySubject(Subject):
293
+            behaviours_classes = [MySubjectBehaviour]
294
+
295
+        simulation = Simulation(config)
296
+        my_subject = MySubject(config, simulation)
297
+        subjects = Subjects(simulation=simulation)
298
+        subjects.append(my_subject)
299
+        simulation.subjects = subjects
300
+
301
+        cycle_manager = CycleManager(
302
+            config,
303
+            logger,
304
+            simulation=simulation,
305
+            process_manager=do_nothing_process_manager,
306
+        )
307
+        results_by_subjects = cycle_manager._job_subjects(worker_id=0, process_count=1)
308
+        assert results_by_subjects
309
+        assert id(my_subject) in results_by_subjects
310
+        assert results_by_subjects[id(my_subject)]
311
+
312
+        # If we remove subject, no more data generated
313
+        subjects.remove(my_subject)
314
+        results_by_subjects = cycle_manager._job_subjects(worker_id=0, process_count=1)
315
+        assert not results_by_subjects
316
+
317
+
318
+class TestMechanisms(BaseTest):
319
+    def test_mechanism_called_once_for_multiple_subject_behaviors(self):
320
+        shared.reset()
321
+
6
         pass
322
         pass
7
 
323
 
8
-    def test_behaviour_timebase(self):
324
+    def test_mechanism_called_once_for_multiple_simulation_behaviors(self):
325
+        shared.reset()
326
+
9
         pass
327
         pass
10
 
328
 
11
-    def test_behavior_not_called_if_no_more_subjects(self):
329
+    def test_mechanism_not_called_if_no_subject_behavior(self):
330
+        shared.reset()
331
+
12
         pass
332
         pass
13
 
333
 
334
+    def test_mechanism_not_called_if_no_simulation_behavior(self):
335
+        shared.reset()
14
 
336
 
15
-class TestMechanisms(BaseTest):
16
-    def test_mechanism_called_once_for_multiple_behaviors(self):
17
         pass
337
         pass
18
 
338
 
19
-    def test_mechanism_not_called_if_no_behavior(self):
339
+    def test_mechanism_not_called_if_subject_behavior_timebase_not_active_yet(self):
340
+        shared.reset()
341
+
20
         pass
342
         pass
21
 
343
 
22
-    def test_mechanism_not_called_if_behavior_timebase_not_active_yet(self):
344
+    def test_mechanism_not_called_if_simulation_behavior_timebase_not_active_yet(self):
345
+        shared.reset()
346
+
23
         pass
347
         pass
348
+
349
+
350
+# TODO: Test Simulation mechanism parralelisation
351
+# TODO: Test behaviour actions generation