Framework-LED-Matrix 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ hpp_lga_model.py
5
+
6
+ Implements the HPP (Hardy-Pomeau-Pazzis) Lattice Gas Automaton (LGA)
7
+ for the 34x9 LED matrix, using the cellpylib library.
8
+
9
+ This automaton models fluid dynamics using 4-bit particle states
10
+ and specific collision/propagation rules. It is NOT a totalistic
11
+ automaton and requires a custom 'apply_rule' function.
12
+ """
13
+
14
+ import cellpylib as cpl
15
+ import numpy as np
16
+ import time
17
+ import random
18
+ from typing import List, Optional
19
+ from framework_led_matrix.core.led_commands import log, clear_graph, WIDTH, HEIGHT, draw_matrix_on_board, reset_modules
20
+
21
+ # --- HPP Particle States (Bitmasks) ---
22
+ # A cell's state is the bitwise OR of the particles it contains.
23
+ EMPTY = 0b0000 # 0
24
+ W_PARTICLE = 0b0001 # 1 (West-moving)
25
+ E_PARTICLE = 0b0010 # 2 (East-moving)
26
+ S_PARTICLE = 0b0100 # 4 (South-moving)
27
+ N_PARTICLE = 0b1000 # 8 (North-moving)
28
+
29
+ # --- HPP Collision States ---
30
+ # These are the only two states that result in a collision
31
+ WE_COLLIDE = W_PARTICLE | E_PARTICLE # 3 (West + East)
32
+ NS_COLLIDE = N_PARTICLE | S_PARTICLE # 12 (North + South)
33
+
34
+
35
+ def draw_hpp_board(board: List[List[int]], which: str):
36
+ """
37
+ Converts the 16-state HPP board to a regular matrix
38
+ (0-1) and draws it.
39
+ """
40
+ matrix = [[0 for _ in range(WIDTH)] for _ in range(HEIGHT)]
41
+ for r in range(HEIGHT):
42
+ for c in range(WIDTH):
43
+ state = board[r][c]
44
+ if state != EMPTY:
45
+ matrix[r][c] = 1 # Occupied
46
+ else:
47
+ matrix[r][c] = 0 # Empty
48
+ draw_matrix_on_board(matrix, which)
49
+
50
+
51
+ def hpp_collide(state: int) -> int:
52
+ """
53
+ Computes the post-collision state for a single HPP cell.
54
+ This is the first half of the HPP rule.
55
+ """
56
+ # N+S collision (12)
57
+ if state == NS_COLLIDE:
58
+ return WE_COLLIDE # Becomes E+W (3)
59
+
60
+ # E+W collision (3)
61
+ if state == WE_COLLIDE:
62
+ return NS_COLLIDE # Becomes N+S (12)
63
+
64
+ # All other states (0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15)
65
+ # pass through unchanged.
66
+ return state
67
+
68
+
69
+ def hpp_lga_rule(neighbourhood: np.ndarray, c_coord: tuple, t: int) -> int:
70
+ """
71
+ The HPP rule function for cellpylib's evolve2d.
72
+ This is the "Propagation" step. It calculates the new state of
73
+ the center cell (1,1) by "gathering" all particles that will
74
+ propagate *into* it from its 4 neighbors' *post-collision* states.
75
+
76
+ Args:
77
+ neighbourhood: The 2D (3x3) NumPy array (Moore neighborhood).
78
+ c_coord (tuple): The (row, col) coordinate (unused).
79
+ t (int): The current timestep (unused).
80
+ """
81
+
82
+ # 1. Get the pre-collision state of the 4 neighbors we care about.
83
+ north_cell_pre_collide = neighbourhood[0, 1]
84
+ south_cell_pre_collide = neighbourhood[2, 1]
85
+ east_cell_pre_collide = neighbourhood[1, 2]
86
+ west_cell_pre_collide = neighbourhood[1, 0]
87
+
88
+ # 2. Compute the post-collision state for each neighbor.
89
+ # This determines what particles are *available to move*
90
+ n_post_collide = hpp_collide(north_cell_pre_collide)
91
+ s_post_collide = hpp_collide(south_cell_pre_collide)
92
+ e_post_collide = hpp_collide(east_cell_pre_collide)
93
+ w_post_collide = hpp_collide(west_cell_pre_collide)
94
+
95
+ # 3. "Gather" the particles that will arrive at the center cell.
96
+
97
+ # The new North particle comes from the South neighbor's post-collision North particle.
98
+ from_south = s_post_collide & N_PARTICLE
99
+
100
+ # The new South particle comes from the North neighbor's post-collision South particle.
101
+ from_north = n_post_collide & S_PARTICLE
102
+
103
+ # The new West particle comes from the East neighbor's post-collision West particle.
104
+ from_east = e_post_collide & W_PARTICLE
105
+
106
+ # The new East particle comes from the West neighbor's post-collision East particle.
107
+ from_west = w_post_collide & E_PARTICLE
108
+
109
+ # The new state of the center cell is the bitwise OR
110
+ # of all particles that have propagated into it.
111
+ new_center_state = from_north | from_south | from_east | from_west
112
+
113
+ return new_center_state
114
+
115
+
116
+ def create_hpp_board_np(density: float = 0.5, initial_state: Optional[np.ndarray] = None) -> np.ndarray:
117
+ """
118
+ Creates a new random board for the HPP model, or uses a provided one.
119
+
120
+ If initial_state is provided, it is used directly.
121
+ If initial_state is None, a new random board is generated
122
+ based on the density.
123
+ """
124
+ if initial_state is not None:
125
+ #take initial board, provide random particles movements
126
+ for r in range(HEIGHT):
127
+ for c in range(WIDTH):
128
+ if initial_state[r, c] != EMPTY:
129
+ #random particle type to the occupied cell
130
+ initial_state[r, c] = random.choice([W_PARTICLE, E_PARTICLE, S_PARTICLE, N_PARTICLE])
131
+ return initial_state
132
+
133
+ log(f"HPP: Creating new NumPy board with particle density {density}")
134
+ board = np.full((HEIGHT, WIDTH), EMPTY, dtype=int)
135
+
136
+ total_cells = WIDTH * HEIGHT
137
+ num_particles = int(total_cells * density)
138
+
139
+ # Get a list of all possible single-particle states
140
+ particle_types = [W_PARTICLE, E_PARTICLE, S_PARTICLE, N_PARTICLE]
141
+
142
+ # Get a list of unique cells to populate
143
+ cells_to_populate = random.sample(
144
+ [(r, c) for r in range(HEIGHT) for c in range(WIDTH)],
145
+ num_particles
146
+ )
147
+
148
+ for r, c in cells_to_populate:
149
+ # Assign a random particle type to the chosen cell
150
+ board[r, c] = random.choice(particle_types)
151
+
152
+ log(f"HPP: Seeded board with {num_particles} random particles.")
153
+ return board
154
+
155
+
156
+ def run_hpp_simulation(
157
+ initial_state: Optional[np.ndarray] = None,
158
+ density: float = 0.5,
159
+ timesteps: int = 500,
160
+ delay_sec: float = 0.1,
161
+ which: str = 'both'
162
+ ):
163
+ """
164
+ Runs the HPP Lattice Gas Automaton simulation using cellpylib.
165
+ """
166
+ log(f"HPP: Starting simulation. density={density}, steps={timesteps}")
167
+
168
+ # Use the create function, which respects the initial_state override
169
+ initial_state_2d = create_hpp_board_np(density, initial_state)
170
+
171
+ # Wrap in 3D array for cellpylib's evolve2d function
172
+ initial_state_3d = np.array([initial_state_2d])
173
+
174
+ log(f"HPP: Evolving {timesteps} steps...")
175
+
176
+ # Evolve the cellular automaton
177
+ all_generations = cpl.evolve2d(
178
+ cellular_automaton=initial_state_3d,
179
+ timesteps=timesteps,
180
+ neighbourhood='Moore', # We need N,S,E,W neighbors
181
+ apply_rule=hpp_lga_rule,
182
+ r=1 # Radius 1
183
+ )
184
+
185
+ log(f"HPP: Evolution complete. Result shape: {all_generations.shape}")
186
+
187
+ try:
188
+ stable_counter = 0
189
+ total_frames = all_generations.shape[0]
190
+
191
+ for i in range(total_frames):
192
+ current_board_np = all_generations[i]
193
+ current_board_list = current_board_np.tolist()
194
+
195
+ # Draw the state to the LED matrix
196
+ draw_hpp_board(current_board_list, which)
197
+ time.sleep(delay_sec)
198
+
199
+ if i % 20 == 0 or i == total_frames - 1:
200
+ log(f"HPP: Step {i}/{timesteps}.")
201
+
202
+ # Check for stable state (gridlock or empty)
203
+ if i > 0:
204
+ if np.array_equal(current_board_np, all_generations[i-1]):
205
+ stable_counter += 1
206
+ else:
207
+ stable_counter = 0
208
+
209
+ if stable_counter >= 20:
210
+ log("HPP: State stable for 20 steps. Halting.")
211
+ time.sleep(2)
212
+ break
213
+
214
+ except KeyboardInterrupt:
215
+ log("HPP: KeyboardInterrupt received, stopping.")
216
+ finally:
217
+ log(f"HPP: simulation finished.")
218
+ clear_graph()
219
+ log("HPP: cleared display.")
220
+
221
+ def run_test_hpp():
222
+ """
223
+ Test function to run a sample HPP simulation.
224
+ """
225
+ matrix = [[0 for _ in range(WIDTH)] for _ in range(HEIGHT)]
226
+ matrix[HEIGHT // 2][WIDTH // 2] = N_PARTICLE | S_PARTICLE
227
+ matrix[HEIGHT // 2][(WIDTH // 2) - 1] = E_PARTICLE | W_PARTICLE
228
+ board = create_hpp_board_np(initial_state=np.array(matrix))
229
+ try:
230
+ run_hpp_simulation(
231
+ initial_state=board,
232
+ density=0.3,
233
+ timesteps=200,
234
+ delay_sec=0.05,
235
+ which='both'
236
+ )
237
+ finally:
238
+ reset_modules()
239
+
240
+ if __name__ == "__main__":
241
+ run_test_hpp()
File without changes
@@ -0,0 +1,47 @@
1
+ import numpy as np
2
+ import cellpylib as cpl
3
+ from framework_led_matrix.core.led_commands import log, WIDTH, HEIGHT
4
+
5
+
6
+
7
+ def create_simple_state(k: int = 2, val: int = 1) -> np.ndarray:
8
+ """Creates a 2D (HEIGHT, WIDTH) array of zeros with a single 'val' at the center."""
9
+ log(f"CPL Helpers: Creating simple center-seed state (val={val})")
10
+ board = np.zeros((HEIGHT, WIDTH), dtype=int)
11
+ board[HEIGHT // 2, WIDTH // 2] = val
12
+ return board
13
+
14
+ def run_totalistic_ca(initial_state: np.ndarray, timesteps: int, rule_number: int) -> np.ndarray:
15
+ """
16
+ Runs a k=2 (binary) totalistic CA for 'timesteps'.
17
+
18
+ The rule is based on the *sum* of the 8 'Moore' neighbors + center.
19
+ 'rule_number' is the NKS-style rule number for k=2.
20
+
21
+ Returns:
22
+ A 3D NumPy array of shape (timesteps + 1, HEIGHT, WIDTH)
23
+ """
24
+ k = 2
25
+ log(f"Totalistic CA (k=2): Running (rule={rule_number}) for {timesteps} steps.")
26
+
27
+ rule_func = lambda n, c_coord, t: cpl.totalistic_rule(n, k=k, rule=rule_number)
28
+
29
+ initial_state_3d = np.array([initial_state])
30
+
31
+ all_generations = cpl.evolve2d(
32
+ cellular_automaton=initial_state_3d,
33
+ timesteps=timesteps,
34
+ neighbourhood='Moore',
35
+ apply_rule=rule_func
36
+ )
37
+ return all_generations
38
+
39
+
40
+ if __name__ == "__main__":
41
+ # Test block
42
+ log("--- Testing Totalistic CA ---")
43
+ init_state = create_simple_state(k=3, val=1)
44
+ # Rule 777 is a 3-color replicator
45
+ history = run_totalistic_ca(init_state, timesteps=50, rule_number=777)
46
+ log(f"History shape: {history.shape}")
47
+ log("Test complete.")
@@ -0,0 +1,112 @@
1
+ import numpy as np
2
+ import cellpylib as cpl
3
+ from framework_led_matrix.core.led_commands import log, WIDTH, HEIGHT, coordinates_to_matrix
4
+
5
+ STARTING_STATES_GOF = {
6
+ "blinker": coordinates_to_matrix([[17, 3], [17, 4], [17, 5]]),
7
+ "toad": coordinates_to_matrix([[17, 3], [17, 4], [17, 5], [18, 2], [18, 3], [18, 4]]),
8
+ "pentadecathlon": coordinates_to_matrix([
9
+ [12, 4], [13, 4], [14, 2], [14, 4], [14, 6], [15, 4], [16, 4],
10
+ [17, 4], [18, 4], [19, 2], [19, 4], [19, 6], [20, 4], [21, 4]
11
+ ]),
12
+ "glider": coordinates_to_matrix([[1, 2], [2, 3], [3, 1], [3, 2], [3, 3]]),
13
+ "lwss": coordinates_to_matrix([
14
+ [17, 3], [17, 5], [18, 2], [19, 2], [19, 5],
15
+ [20, 2], [20, 3], [20, 4], [20, 5]
16
+ ]),
17
+ "r_pentomino": coordinates_to_matrix([[17, 4], [17, 5], [18, 3], [18, 4], [19, 4]]),
18
+ "diehard": coordinates_to_matrix([
19
+ [17, 7], [18, 1], [18, 2], [19, 2], [19, 5], [19, 6], [19, 7]
20
+ ]),
21
+ "acorn": coordinates_to_matrix([
22
+ [17, 2], [18, 4], [19, 1], [19, 2], [19, 5], [19, 6], [19, 7]
23
+ ]),
24
+ "block": coordinates_to_matrix([[17, 3], [17, 4], [18, 3], [18, 4]]),
25
+ "beehive": coordinates_to_matrix([[17, 3], [17, 4], [18, 2], [18, 5], [19, 3], [19, 4]]),
26
+ "r_pentomino": coordinates_to_matrix([[17, 4], [17, 5], [18, 3], [18, 4], [19, 4]]),
27
+ "rabbit": coordinates_to_matrix([[17, 3], [17, 4], [18, 2], [18, 5], [19, 5], [20, 5]])
28
+ }
29
+
30
+
31
+ game_of_life_rules = {
32
+ 'Original': {'B': [3], 'S': [2, 3]},
33
+ 'HighLife': {'B': [3, 6], 'S': [2, 3]},
34
+ 'Day & Night': {'B': [3, 6, 7, 8], 'S': [3, 4, 6, 7, 8]},
35
+ 'Seeds': {'B': [2], 'S': []},
36
+ }
37
+
38
+ NAMED_RULES = {
39
+ "Life": ([3], [2, 3]),
40
+ "HighLife": ([3, 6], [2, 3]),
41
+ "Day & Night": ([3, 6, 7, 8], [3, 4, 6, 7, 8]),
42
+ "Seeds": ([2], []),
43
+ "Maze": ([3], [1, 2, 3, 4, 5]),
44
+ }
45
+
46
+
47
+ def run_outer_totalistic_ca(
48
+ initial_state: np.ndarray,
49
+ timesteps: int,
50
+ b_rule: list[int],
51
+ s_rule: list[int]
52
+ ) -> np.ndarray:
53
+ """
54
+ Runs a general binary Outer-Totalistic (B/S) CA for 'timesteps'.
55
+ This is the system used by Conway's Game of Life.
56
+
57
+ Args:
58
+ initial_state: The 2D (H, W) NumPy array to start with.
59
+ timesteps: The number of generations to evolve.
60
+ b_rule: A list of neighbor counts to "Birth" a dead cell (e.g., [3]).
61
+ s_rule: A list of neighbor counts to "Survive" a live cell (e.g., [2, 3]).
62
+
63
+ Returns:
64
+ A 3D NumPy array of shape (timesteps + 1, HEIGHT, WIDTH)
65
+ """
66
+ log(f"Outer-Totalistic CA: Running B{b_rule}/S{s_rule} for {timesteps} steps.")
67
+
68
+ # Use sets for fast 'in' lookups
69
+ b_set = set(b_rule)
70
+ s_set = set(s_rule)
71
+
72
+ def outer_totalistic_rule(neighbourhood: np.ndarray, c_coord: tuple, t: int) -> int:
73
+ """
74
+ The custom 'apply_rule' function that implements B/S logic.
75
+
76
+ Args:
77
+ neighbourhood: The 2D (3x3) NumPy array (Moore neighborhood).
78
+ c_coord (tuple): The (row, col) coordinate (unused).
79
+ t (int): The current timestep (unused).
80
+ """
81
+
82
+ # Get the 8-neighbor sum (total sum - center cell)
83
+ neighbor_sum = np.sum(neighbourhood) - neighbourhood[1, 1]
84
+
85
+ # Get the center cell's current state
86
+ center_val = neighbourhood[1, 1]
87
+
88
+ if center_val == 1:
89
+ # Cell is ALIVE, check SURVIVAL rule
90
+ if neighbor_sum in s_set:
91
+ return 1 # Survive
92
+ else:
93
+ return 0 # Die
94
+ else:
95
+ # Cell is DEAD, check BIRTH rule
96
+ if neighbor_sum in b_set:
97
+ return 1 # Born
98
+ else:
99
+ return 0 # Stay dead
100
+
101
+ # Wrap initial state into a 3D array (shape [1, H, W])
102
+ initial_state_3d = np.array([initial_state])
103
+
104
+ # Evolve and return the history
105
+ all_generations = cpl.evolve2d(
106
+ cellular_automaton=initial_state_3d,
107
+ timesteps=timesteps,
108
+ neighbourhood='Moore', # B/S rules require the 8-neighbor Moore
109
+ apply_rule=outer_totalistic_rule
110
+ )
111
+ return all_generations
112
+
File without changes
@@ -0,0 +1,39 @@
1
+ from framework_led_matrix.simulations.outer_totalistic import run_outer_totalistic_ca
2
+ from framework_led_matrix.core.led_commands import start_animation, stop_animation, reset_modules, log
3
+ from framework_led_matrix.utils.text_rendering import draw_text_vertical
4
+ import nltk
5
+ import time
6
+
7
+ def ensure_nltk_words():
8
+ """Ensures the 'words' corpus is downloaded."""
9
+ try:
10
+ nltk.data.find('corpora/words')
11
+ except (LookupError, AttributeError):
12
+ log("NLTK 'words' corpus not found. Downloading...")
13
+ nltk.download('words', quiet=True)
14
+
15
+ def draw_anagram_on_matrix(word: str, which: str = 'both', animate: bool = True):
16
+ """Draws an anagram of the given word on the LED matrix."""
17
+ log(f"draw_anagram_on_matrix: start word='{word}' which={which}")
18
+ ensure_nltk_words()
19
+ ana_lst = anagrams(word)
20
+ ana_lst.add(word)
21
+ ana_lst = list(ana_lst)
22
+ log(f"draw_anagram_on_matrix: will render {len(ana_lst)} strings: {ana_lst}")
23
+ for w in ana_lst:
24
+ log(f"draw_anagram_on_matrix: rendering '{w}' on matrix (vertical)")
25
+ draw_text_vertical(w, which=which)
26
+ log(f"draw_anagram_on_matrix: rendered '{w}', toggling animation briefly")
27
+ stop_animation()
28
+ time.sleep(2)
29
+ start_animation() if animate else stop_animation()
30
+ time.sleep(5)
31
+ log("draw_anagram_on_matrix: finished rendering all anagrams, resetting modules")
32
+ reset_modules()
33
+
34
+ def anagrams(word):
35
+ log(f"anagrams: finding anagrams for '{word}'")
36
+ word_sorted = sorted(word)
37
+ result = set(w for w in words.words() if sorted(w) == word_sorted)
38
+ log(f"anagrams: found {len(result)} candidates")
39
+ return result