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,12 +117,19 @@ class CycleManager(BaseObject):
117 117
             ))
118 118
 
119 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 134
             if behaviour_data:
128 135
                 behaviours_data[behaviour.__class__] = behaviour_data
@@ -302,7 +309,8 @@ class CycleManager(BaseObject):
302 309
                 if behaviour.is_terminated():
303 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 314
                 if behaviour.is_skip(self.current_cycle):
307 315
                     behaviour_data = False
308 316
                     self.logger.debug('Subject {}: behaviour {} skip'.format(
@@ -312,14 +320,18 @@ class CycleManager(BaseObject):
312 320
                 else:
313 321
                     with time_it() as elapsed_time:
314 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 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 336
                 if behaviour_data:
325 337
                     behaviours_data[behaviour.__class__] = behaviour_data

+ 37 - 26
synergine2/simulation.py View File

@@ -183,7 +183,7 @@ class Subjects(list):
183 183
 
184 184
 class Simulation(BaseObject):
185 185
     accepted_subject_class = Subjects
186
-    behaviours_classes = []
186
+    behaviours_classes = []  # type: typing.List[typing.Type[SimulationBehaviour]]
187 187
 
188 188
     subject_classes = shared.create('subject_classes', {})
189 189
     collections = shared.create('collections', {})
@@ -254,7 +254,7 @@ class SubjectMechanism(Mechanism):
254 254
         self.simulation = simulation
255 255
         self.subject = subject
256 256
 
257
-    def run(self):
257
+    def run(self) -> dict:
258 258
         raise NotImplementedError()
259 259
 
260 260
 
@@ -283,9 +283,40 @@ class Event(BaseObject):
283 283
 
284 284
 
285 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 303
     def run(self, data):
287 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 321
 class SubjectBehaviour(Behaviour):
291 322
     use = []  # type: typing.List[typing.Type[SubjectMechanism]]
@@ -296,18 +327,10 @@ class SubjectBehaviour(Behaviour):
296 327
             simulation: Simulation,
297 328
             subject: Subject,
298 329
     ):
330
+        super().__init__()
299 331
         self.config = config
300 332
         self.simulation = simulation
301 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 335
     def is_terminated(self) -> bool:
313 336
         """
@@ -315,19 +338,7 @@ class SubjectBehaviour(Behaviour):
315 338
         """
316 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 343
         Method called in subprocess.
333 344
         If return equivalent to False, behaviour produce nothing.
@@ -347,14 +358,14 @@ class SubjectBehaviour(Behaviour):
347 358
 
348 359
 
349 360
 class SimulationBehaviour(Behaviour):
350
-    frequency = 1
351
-    use = []
361
+    use = []  # type: typing.List[typing.Type[SimulationMechanism]]
352 362
 
353 363
     def __init__(
354 364
             self,
355 365
             config: Config,
356 366
             simulation: Simulation,
357 367
     ):
368
+        super().__init__()
358 369
         self.config = config
359 370
         self.simulation = simulation
360 371
 

+ 335 - 7
tests/test_simulation.py View File

@@ -1,23 +1,351 @@
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 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 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 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 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 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 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 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 347
         pass
348
+
349
+
350
+# TODO: Test Simulation mechanism parralelisation
351
+# TODO: Test behaviour actions generation