Browse Source

Terminal enhancements

Bastien Sevajol 7 years ago
parent
commit
ff0d901a01

+ 33 - 22
sandbox/life_game/run.py View File

@@ -1,11 +1,14 @@
1
-import sys
2 1
 import collections
3
-from sandbox.life_game.simulation import CellBornBehaviour, CellDieBehaviour, Cell, Empty
2
+from sandbox.life_game.simulation import Cell
3
+from sandbox.life_game.simulation import Empty
4
+from sandbox.life_game.simulation import CellDieEvent
5
+from sandbox.life_game.simulation import CellBornEvent
4 6
 
5 7
 from sandbox.life_game.utils import get_subjects_from_str_representation
6 8
 from synergine2.core import Core
7 9
 from synergine2.cycle import CycleManager
8 10
 from synergine2.simulation import Simulation
11
+from synergine2.simulation import Event
9 12
 from synergine2.terminals import Terminal
10 13
 from synergine2.terminals import TerminalPackage
11 14
 from synergine2.terminals import TerminalManager
@@ -13,32 +16,33 @@ from synergine2.xyz_utils import get_str_representation_from_positions
13 16
 
14 17
 
15 18
 class SimplePrintTerminal(Terminal):
19
+    subscribed_events = [
20
+        CellDieEvent,
21
+        CellBornEvent,
22
+    ]
23
+
16 24
     def __init__(self):
17 25
         super().__init__()
18
-        self.subjects = None
26
+        self._cycle_born_count = 0
27
+        self._cycle_die_count = 0
28
+        self.register_event_handler(CellDieEvent, self.record_die)
29
+        self.register_event_handler(CellBornEvent, self.record_born)
19 30
 
20
-    def receive(self, value):
21
-        self.update_with_package(value)
22
-        self.print_str_representation()
31
+    def record_die(self, event: Event):
32
+        self._cycle_die_count += 1
23 33
 
24
-    def update_with_package(self, package: TerminalPackage):
25
-        self.subjects = package.subjects if package.subjects else self.subjects
26
-        for subject_id, actions in package.actions.items():
27
-            for action, value in actions.items():
28
-                if action == CellBornBehaviour:
29
-                    # Remove Empty subject
30
-                    self.subjects = [s for s in self.subjects[:] if s.id != subject_id]
31
-                    # Add born subject
32
-                    self.subjects.append(value)
33
-                if action == CellDieBehaviour:
34
-                    # Remove Cell subject
35
-                    self.subjects = [s for s in self.subjects[:] if s.id != subject_id]
36
-                    # Add Empty subject
37
-                    self.subjects.append(value)
34
+    def record_born(self, event: Event):
35
+        self._cycle_born_count += 1
36
+
37
+    def receive(self, package: TerminalPackage):
38
+        self._cycle_born_count = 0
39
+        self._cycle_die_count = 0
40
+        super().receive(package)
41
+        self.print_str_representation()
38 42
 
39 43
     def print_str_representation(self):
40 44
         items_positions = collections.defaultdict(list)
41
-        for subject in self.subjects:
45
+        for subject in self.subjects.values():
42 46
             if type(subject) == Cell:
43 47
                 items_positions['1'].append(subject.position)
44 48
             if type(subject) == Empty:
@@ -46,8 +50,15 @@ class SimplePrintTerminal(Terminal):
46 50
         print(get_str_representation_from_positions(
47 51
             items_positions,
48 52
             separator=' ',
49
-            #force_items_as=(('0', ' '),),
53
+            force_items_as=(('0', ' '),),
50 54
         ))
55
+
56
+        # Display current cycle events
57
+        print('This cycle: {0} born, {1} dead'.format(
58
+            self._cycle_born_count,
59
+            self._cycle_die_count,
60
+        ))
61
+
51 62
         print()
52 63
 
53 64
 

+ 15 - 2
sandbox/life_game/simulation.py View File

@@ -1,4 +1,5 @@
1 1
 from synergine2.simulation import Subject
2
+from synergine2.simulation import Event
2 3
 from synergine2.simulation import Behaviour
3 4
 from synergine2.xyz import ProximityMechanism
4 5
 from synergine2.xyz import XYZSubjectMixin
@@ -6,6 +7,18 @@ from synergine2.xyz import XYZSubjectMixin
6 7
 COLLECTION_CELL = 'COLLECTION_CELL'  # Collections of Cell type
7 8
 
8 9
 
10
+class CellDieEvent(Event):
11
+    def __init__(self, subject_id, *args, **kwargs):
12
+        super().__init__(*args, **kwargs)
13
+        self.subject_id = subject_id
14
+
15
+
16
+class CellBornEvent(Event):
17
+    def __init__(self, subject_id, *args, **kwargs):
18
+        super().__init__(*args, **kwargs)
19
+        self.subject_id = subject_id
20
+
21
+
9 22
 class CellProximityMechanism(ProximityMechanism):
10 23
     distance = 1.41  # distance when on angle
11 24
     feel_collections = [COLLECTION_CELL]
@@ -29,7 +42,7 @@ class CellDieBehaviour(Behaviour):
29 42
         )
30 43
         self.simulation.subjects.remove(self.subject)
31 44
         self.simulation.subjects.append(new_empty)
32
-        return new_empty
45
+        return [CellDieEvent(self.subject.id)]
33 46
 
34 47
 
35 48
 class CellBornBehaviour(Behaviour):
@@ -48,7 +61,7 @@ class CellBornBehaviour(Behaviour):
48 61
         )
49 62
         self.simulation.subjects.remove(self.subject)
50 63
         self.simulation.subjects.append(new_cell)
51
-        return new_cell
64
+        return [CellBornEvent(new_cell.id)]
52 65
 
53 66
 
54 67
 class Cell(XYZSubjectMixin, Subject):

+ 11 - 2
synergine2/core.py View File

@@ -25,9 +25,18 @@ class Core(object):
25 25
 
26 26
             while True:
27 27
                 # TODO: receive from terminals
28
-                actions = self.cycle_manager.next()
29
-                cycle_package = TerminalPackage(actions=actions)
28
+                events = self.cycle_manager.next()
29
+                cycle_package = TerminalPackage(
30
+                    events=events,
31
+                    add_subjects=self.simulation.subjects.adds,
32
+                    remove_subjects=self.simulation.subjects.removes,
33
+                )
30 34
                 self.terminal_manager.send(cycle_package)
35
+
36
+                # Reinitialize these list for next cycle
37
+                self.simulation.subjects.adds = []
38
+                self.simulation.subjects.removes = []
39
+
31 40
                 import time
32 41
                 time.sleep(1)  # TODO: tick control
33 42
         except KeyboardInterrupt:

+ 18 - 9
synergine2/cycle.py View File

@@ -1,15 +1,18 @@
1 1
 import multiprocessing
2
-import collections
3 2
 
4 3
 from synergine2.processing import ProcessManager
5
-from synergine2.simulation import Subject, Behaviour, Mechanism
4
+from synergine2.simulation import Subject
5
+from synergine2.simulation import Behaviour
6
+from synergine2.simulation import Mechanism
7
+from synergine2.simulation import Event
8
+from synergine2.simulation import Subjects
6 9
 from synergine2.utils import ChunkManager
7 10
 
8 11
 
9 12
 class CycleManager(object):
10 13
     def __init__(
11 14
             self,
12
-            subjects: list,
15
+            subjects: Subjects,
13 16
             process_manager: ProcessManager=None,
14 17
     ):
15 18
         if process_manager is None:
@@ -22,21 +25,27 @@ class CycleManager(object):
22 25
         self.subjects = subjects
23 26
         self.process_manager = process_manager
24 27
         self.current_cycle = 0
28
+        self.first_cycle = True
29
+
30
+    def next(self) -> [Event]:
31
+        if self.first_cycle:
32
+            # To dispatch subjects add/removes, enable track on them
33
+            self.subjects.track_changes = True
34
+            self.first_cycle = False
25 35
 
26
-    def next(self):
27 36
         results = {}
28 37
         results_by_processes = self.process_manager.execute_jobs(self.subjects)
29
-        actions = collections.defaultdict(dict)
38
+        events = []
30 39
         for process_results in results_by_processes:
31 40
             results.update(process_results)
32 41
         for subject in self.subjects[:]:  # Duplicate list to prevent conflicts with behaviours subjects manipulations
33 42
             for behaviour_class in results[subject.id]:
34 43
                 # TODO: Ajouter une etape de selection des actions a faire (genre neuronnal)
35
-                # TODO: les behaviour_class ont le même uniqueid apres le process ?
36
-                action_result = subject.behaviours[behaviour_class].action(results[subject.id][behaviour_class])
37
-                actions[subject.id][behaviour_class] = action_result
44
+                # (genre se cacher et fuir son pas compatibles)
45
+                actions_events = subject.behaviours[behaviour_class].action(results[subject.id][behaviour_class])
46
+                events.extend(actions_events)
38 47
 
39
-        return actions
48
+        return events
40 49
 
41 50
     def computing(self, subjects):
42 51
         # compute mechanisms (prepare check to not compute slienced or not used mechanisms)

+ 24 - 1
synergine2/simulation.py View File

@@ -39,14 +39,32 @@ class Subject(object):
39 39
 
40 40
 
41 41
 class Subjects(list):
42
+    """
43
+    TODO: Manage other list methods
44
+    """
42 45
     def __init__(self, *args, **kwargs):
43 46
         self.simulation = kwargs.pop('simulation')
47
+        self.removes = []
48
+        self.adds = []
49
+        self.track_changes = False
44 50
         super().__init__(*args, **kwargs)
45 51
 
46 52
     def remove(self, value: Subject):
53
+        # Remove from subjects list
47 54
         super().remove(value)
55
+        # Remove from collections
48 56
         for collection_name in value.collections:
49 57
             self.simulation.collections[collection_name].remove(value)
58
+        # Add to removed listing
59
+        if self.track_changes:
60
+            self.removes.append(value)
61
+
62
+    def append(self, p_object):
63
+        # Add to subjects list
64
+        super().append(p_object)
65
+        # Add to adds list
66
+        if self.track_changes:
67
+            self.adds.append(p_object)
50 68
 
51 69
 
52 70
 class Mechanism(object):
@@ -62,6 +80,11 @@ class Mechanism(object):
62 80
         raise NotImplementedError()
63 81
 
64 82
 
83
+class Event(object):
84
+    def __init__(self, *args, **kwargs):
85
+        pass
86
+
87
+
65 88
 class Behaviour(object):
66 89
     def __init__(
67 90
             self,
@@ -82,7 +105,7 @@ class Behaviour(object):
82 105
         """
83 106
         raise NotImplementedError()
84 107
 
85
-    def action(self, data) -> object:
108
+    def action(self, data) -> [Event]:
86 109
         """
87 110
         Method called in main process
88 111
         Return value will be give to terminals

+ 79 - 25
synergine2/terminals.py View File

@@ -1,10 +1,13 @@
1
+import collections
2
+from copy import copy
1 3
 from multiprocessing import Queue
2 4
 
3 5
 from multiprocessing import Process
4 6
 from queue import Empty
5 7
 
6 8
 import time
7
-from synergine2.simulation import Simulation, Subject
9
+from synergine2.simulation import Subject
10
+from synergine2.simulation import Event
8 11
 
9 12
 STOP_SIGNAL = '__STOP_SIGNAL__'
10 13
 
@@ -13,21 +16,39 @@ class TerminalPackage(object):
13 16
     def __init__(
14 17
             self,
15 18
             subjects: [Subject]=None,
16
-            actions: ['TODO']=None,
19
+            add_subjects: [Subject]=None,
20
+            remove_subjects: [Subject]=None,
21
+            events: [Event]=None,
22
+            *args,
23
+            **kwargs
17 24
     ):
18 25
         self.subjects = subjects
19
-        self.actions = actions or {}
26
+        self.add_subjects = add_subjects or []
27
+        self.remove_subjects = remove_subjects or []
28
+        self.events = events or []
20 29
 
21 30
 
22 31
 class Terminal(object):
23
-    """Default behaviour is to do nothing.
24
-    DEFAULT_SLEEP is sleep time between each queue read"""
25
-    DEFAULT_SLEEP = 1
32
+    # Default behaviour is to do nothing.
33
+    # DEFAULT_SLEEP is sleep time between each queue read
34
+    default_sleep = 1
35
+    # List of subscribed Event classes. Terminal will not receive events
36
+    # who are not instance of listed classes
37
+    subscribed_events = [Event]
26 38
 
27 39
     def __init__(self):
28 40
         self._input_queue = None
29 41
         self._output_queue = None
30 42
         self._stop_required = False
43
+        self.subjects = {}
44
+        self.cycle_events = []
45
+        self.event_handlers = collections.defaultdict(list)
46
+
47
+    def accept_event(self, event: Event) -> bool:
48
+        for event_class in self.subscribed_events:
49
+            if isinstance(event, event_class):
50
+                return True
51
+        return False
31 52
 
32 53
     def start(self, input_queue: Queue, output_queue: Queue) -> None:
33 54
         self._input_queue = input_queue
@@ -40,7 +61,7 @@ class Terminal(object):
40 61
         """
41 62
         try:
42 63
             while self.read():
43
-                time.sleep(self.DEFAULT_SLEEP)
64
+                time.sleep(self.default_sleep)
44 65
         except KeyboardInterrupt:
45 66
             pass
46 67
 
@@ -56,26 +77,49 @@ class Terminal(object):
56 77
             except Empty:
57 78
                 return True  # Finished to read Queue
58 79
 
59
-    def receive(self, value):
60
-        raise NotImplementedError()
80
+    def receive(self, package: TerminalPackage):
81
+        self.update_with_package(package)
82
+
83
+    def send(self, package: TerminalPackage):
84
+        self._output_queue.put(package)
85
+
86
+    def register_event_handler(self, event_class, func):
87
+        self.event_handlers[event_class].append(func)
88
+
89
+    def update_with_package(self, package: TerminalPackage):
90
+        if package.subjects:
91
+            self.subjects = {s.id: s for s in package.subjects}
92
+
93
+        for new_subject in package.add_subjects:
94
+            self.subjects[new_subject.id] = new_subject
61 95
 
62
-    def send(self, value):
63
-        self._output_queue.put(value)
96
+        for deleted_subject in package.remove_subjects:
97
+            del self.subjects[deleted_subject.id]
98
+
99
+        self.cycle_events = package.events
100
+        self.execute_event_handlers(package.events)
101
+
102
+    def execute_event_handlers(self, events: [Event]):
103
+        for event in events:
104
+            for event_class, handlers in self.event_handlers.items():
105
+                if isinstance(event, event_class):
106
+                    for handler in handlers:
107
+                        handler(event)
64 108
 
65 109
 
66 110
 class TerminalManager(object):
67 111
     def __init__(self, terminals: [Terminal]):
68
-        self._terminals = terminals
69
-        self._outputs_queues = []
70
-        self._inputs_queues = []
112
+        self.terminals = terminals
113
+        self.outputs_queues = {}
114
+        self.inputs_queues = {}
71 115
 
72 116
     def start(self) -> None:
73
-        for terminal in self._terminals:
117
+        for terminal in self.terminals:
74 118
             output_queue = Queue()
75
-            self._outputs_queues.append(output_queue)
119
+            self.outputs_queues[terminal] = output_queue
76 120
 
77 121
             input_queue = Queue()
78
-            self._inputs_queues.append(input_queue)
122
+            self.inputs_queues[terminal] = input_queue
79 123
 
80 124
             process = Process(target=terminal.start, kwargs=dict(
81 125
                 input_queue=output_queue,
@@ -84,20 +128,30 @@ class TerminalManager(object):
84 128
             process.start()
85 129
 
86 130
     def stop(self):
87
-        for output_queue in self._outputs_queues:
131
+        for output_queue in self.outputs_queues.values():
88 132
             output_queue.put(STOP_SIGNAL)
89 133
 
90
-    def send(self, value):
91
-        for output_queue in self._outputs_queues:
92
-            output_queue.put(value)
134
+    def send(self, package: TerminalPackage):
135
+        for terminal, output_queue in self.outputs_queues.items():
136
+            # Terminal maybe don't want all events, so reduce list of event
137
+            # Thirst make a copy to personalize this package
138
+            terminal_adapted_package = copy(package)
139
+            # Duplicate events list to personalize it
140
+            terminal_adapted_package.events = terminal_adapted_package.events[:]
141
+
142
+            for package_event in terminal_adapted_package.events[:]:
143
+                if not terminal.accept_event(package_event):
144
+                    terminal_adapted_package.events.remove(package_event)
145
+
146
+            output_queue.put(terminal_adapted_package)
93 147
 
94 148
     def receive(self) -> []:
95
-        values = []
96
-        for input_queue in self._inputs_queues:
149
+        packages = []
150
+        for input_queue in self.inputs_queues.values():
97 151
             try:
98 152
                 while True:
99
-                    values.append(input_queue.get(block=False, timeout=None))
153
+                    packages.append(input_queue.get(block=False, timeout=None))
100 154
             except Empty:
101 155
                 pass  # Queue is empty
102 156
 
103
-        return values
157
+        return packages

+ 94 - 19
tests/test_terminals.py View File

@@ -1,20 +1,41 @@
1 1
 import time
2 2
 
3
+from synergine2.simulation import Event
3 4
 from synergine2.terminals import Terminal
5
+from synergine2.terminals import TerminalPackage
4 6
 from synergine2.terminals import TerminalManager
5 7
 from tests import BaseTest
6 8
 
7 9
 
10
+class ValueTerminalPackage(TerminalPackage):
11
+    def __init__(self, value, *args, **kwargs):
12
+        super().__init__(*args, **kwargs)
13
+        self.value = value
14
+
15
+
8 16
 class MultiplyTerminal(Terminal):
9
-    def receive(self, value):
10
-        self.send(value * 2)
11
-        self.send(value * 4)
17
+    def receive(self, package: ValueTerminalPackage):
18
+        self.send(ValueTerminalPackage(value=package.value * 2))
19
+        self.send(ValueTerminalPackage(value=package.value * 4))
12 20
 
13 21
 
14 22
 class DivideTerminal(Terminal):
15
-    def receive(self, value):
16
-        self.send(value / 2)
17
-        self.send(value / 4)
23
+    def receive(self, package: ValueTerminalPackage):
24
+        self.send(ValueTerminalPackage(value=package.value / 2))
25
+        self.send(ValueTerminalPackage(value=package.value / 4))
26
+
27
+
28
+class AnEvent(Event):
29
+    pass
30
+
31
+
32
+class AnOtherEvent(Event):
33
+    pass
34
+
35
+
36
+class SendBackTerminal(Terminal):
37
+    def receive(self, package: ValueTerminalPackage):
38
+        self.send(package)
18 39
 
19 40
 
20 41
 class TestTerminals(BaseTest):
@@ -25,19 +46,19 @@ class TestTerminals(BaseTest):
25 46
             ]
26 47
         )
27 48
         terminals_manager.start()
28
-        terminals_manager.send(42)
49
+        terminals_manager.send(ValueTerminalPackage(value=42))
29 50
 
30 51
         # We wait max 2s (see time.sleep) to consider
31 52
         # process have finished. If not, it will fail
32
-        values = []
53
+        packages = []
33 54
         for i in range(200):
34
-            values.extend(terminals_manager.receive())
35
-            if len(values) == 2:
55
+            packages.extend(terminals_manager.receive())
56
+            if len(packages) == 2:
36 57
                 break
37 58
             time.sleep(0.01)
38 59
 
39
-        assert 2 == len(values)
40
-        values = [v for v in values]
60
+        assert 2 == len(packages)
61
+        values = [p.value for p in packages]
41 62
         assert 84 in values
42 63
         assert 168 in values
43 64
 
@@ -51,22 +72,76 @@ class TestTerminals(BaseTest):
51 72
             ]
52 73
         )
53 74
         terminals_manager.start()
54
-        terminals_manager.send(42)
75
+        terminals_manager.send(ValueTerminalPackage(value=42))
55 76
 
56 77
         # We wait max 2s (see time.sleep) to consider
57 78
         # process have finished. If not, it will fail
58
-        values = []
79
+        packages = []
59 80
         for i in range(200):
60
-            values.extend(terminals_manager.receive())
61
-            if len(values) == 4:
81
+            packages.extend(terminals_manager.receive())
82
+            if len(packages) == 4:
62 83
                 break
63 84
             time.sleep(0.01)
64 85
 
65
-        assert 4 == len(values)
66
-        values = [v for v in values]
86
+        assert 4 == len(packages)
87
+        values = [p.value for p in packages]
67 88
         assert 84 in values
68 89
         assert 168 in values
69 90
         assert 21 in values
70 91
         assert 10.5 in values
71 92
 
72
-        terminals_manager.stop()  # pytest must execute this if have fail
93
+        terminals_manager.stop()  # TODO pytest must execute this if have fail
94
+
95
+    def test_event_listen_everything(self):
96
+        class ListenEverythingTerminal(SendBackTerminal):
97
+            pass
98
+
99
+        terminals_manager = TerminalManager(
100
+            terminals=[ListenEverythingTerminal()]
101
+        )
102
+        terminals_manager.start()
103
+        terminals_manager.send(ValueTerminalPackage(value=42))
104
+        an_event = AnEvent(84)
105
+        terminals_manager.send(TerminalPackage(events=[an_event]))
106
+
107
+        # We wait max 2s (see time.sleep) to consider
108
+        # process have finished. If not, it will fail
109
+        packages = []
110
+        for i in range(200):
111
+            packages.extend(terminals_manager.receive())
112
+            if len(packages) == 2:
113
+                break
114
+            time.sleep(0.01)
115
+
116
+        assert 2 == len(packages)
117
+        assert 42 == packages[0].value
118
+        assert AnEvent == type(packages[1].events[0])
119
+
120
+        terminals_manager.stop()  # TODO pytest must execute this if have fail
121
+
122
+    def test_event_listen_specified(self):
123
+        class ListenAnEventTerminal(SendBackTerminal):
124
+            subscribed_events = [AnOtherEvent]
125
+
126
+        terminals_manager = TerminalManager(
127
+            terminals=[ListenAnEventTerminal()]
128
+        )
129
+        terminals_manager.start()
130
+        terminals_manager.send(ValueTerminalPackage(value=42))
131
+        an_event = AnEvent(84)
132
+        an_other_event = AnOtherEvent(168)
133
+        terminals_manager.send(TerminalPackage(events=[an_event, an_other_event]))
134
+
135
+        # We wait max 2s (see time.sleep) to consider
136
+        # process have finished. If not, it will fail
137
+        packages = []
138
+        for i in range(200):
139
+            packages.extend(terminals_manager.receive())
140
+            if len(packages) == 1:
141
+                break
142
+            time.sleep(0.01)
143
+
144
+        assert 2 == len(packages)
145
+        assert AnOtherEvent == type(packages[1].events[0])
146
+
147
+        terminals_manager.stop()  # TODO pytest must execute this if have fail