Aluode commited on
Commit
ee0a63c
·
verified ·
1 Parent(s): 655e94c

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +436 -14
  2. app.py +2181 -0
  3. requirements.txt +7 -0
README.md CHANGED
@@ -1,14 +1,436 @@
1
- ---
2
- title: ArtificialLife
3
- emoji: 🐨
4
- colorFrom: green
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.4.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: Complicated Ai Life forms with dynamic Ai Brains
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fractal Life Simulator
2
+
3
+ A neural evolution simulation featuring organisms with fractal neural networks that learn and evolve through interaction.
4
+
5
+ [video](https://www.youtube.com/live/shALyP_EQsQ)
6
+
7
+ (Notice, life in the system starts to move very slowly, then faster)
8
+
9
+ Do read the discussion below the licence I had with Claude about it this morning when observing the life - after leaving my computer
10
+ run the simulation overnight. (See the video above)
11
+
12
+ ## Overview
13
+
14
+ The Fractal Life Simulator creates a virtual environment where organisms with fractal neural networks interact, evolve, and learn. Each organism possesses:
15
+ - A fractal neural network brain consisting of self-similar neural patterns
16
+ - Physical properties that affect movement and interaction
17
+ - Visual patterns that emerge from their neural properties
18
+ - Energy systems that govern survival and reproduction
19
+
20
+ ## Installation
21
+
22
+ 1. Clone the repository
23
+ 2. Create a virtual environment (recommended):
24
+ ```bash
25
+ python -m venv venv
26
+ source venv/bin/activate # On Windows: venv\Scripts\activate
27
+ ```
28
+
29
+ 3. Install requirements:
30
+ ```bash
31
+ pip install -r requirements.txt
32
+ ```
33
+
34
+ ## Running the Simulation
35
+
36
+ Launch the simulation:
37
+ ```bash
38
+ python app.py
39
+ ```
40
+
41
+ The simulation will start a Gradio web interface accessible at `http://localhost:7860`
42
+
43
+ ## Features
44
+
45
+ ### Organisms
46
+ - Fractal neural networks that process visual input and control movement
47
+ - Energy systems that govern survival and reproduction
48
+ - Physical properties affecting movement and collisions
49
+ - Visual patterns that emerge from neural properties
50
+ - Ability to interact and learn from other organisms
51
+
52
+ ### Neural Networks
53
+ - Self-similar neural patterns that form a fractal structure
54
+ - Visual cortex for processing environmental input
55
+ - Thought processor for decision making
56
+ - Action generator for movement control
57
+ - Memory systems for learning from interactions
58
+
59
+ ### Physics
60
+ - Realistic collision detection and response
61
+ - Momentum-based movement
62
+ - Force-based interactions
63
+ - Energy transfer during collisions
64
+
65
+ ### Visualization
66
+ - Real-time visualization of organisms and their neural states
67
+ - Pattern visualization based on neural properties
68
+ - Energy level indicators
69
+ - Population statistics and neural activity plots
70
+
71
+ ## Controls
72
+
73
+ Through the Gradio interface:
74
+ - Start/Stop/Pause simulation
75
+ - Adjust population parameters
76
+ - Modify mutation rates
77
+ - Control energy decay
78
+ - Adjust interaction strengths
79
+ - Monitor population statistics
80
+ - View neural activity
81
+ - Select and interact with individual organisms
82
+
83
+ ## Configuration
84
+
85
+ Key parameters can be adjusted through the interface:
86
+ - Initial Population: 5-100 organisms
87
+ - Maximum Population: 50-500 organisms
88
+ - Mutation Rate: 0-1
89
+ - Brain Update Rate: 1-30 Hz
90
+ - Maximum Neurons: 100-5000
91
+ - Energy Decay Rate: 0-1
92
+ - Interaction Strength: 0-1
93
+ - Field Resolution: 10-100
94
+
95
+ ## Technical Details
96
+
97
+ Built using:
98
+ - PyTorch for neural networks
99
+ - Pygame for visualization
100
+ - Pymunk for physics
101
+ - Gradio for user interface
102
+ - NumPy for numerical computations
103
+ - Plotly for statistics visualization
104
+
105
+ ## Requirements
106
+
107
+ See `requirements.txt` for full dependencies. Key requirements:
108
+ - Python 3.8+
109
+ - torch>=2.0.0
110
+ - pygame>=2.4.0
111
+ - gradio==3.50.2
112
+ - numpy>=1.21.0
113
+ - pymunk>=6.4.0
114
+ - plotly>=5.13.0
115
+ - Pillow>=9.4.0
116
+
117
+ ## Notes
118
+
119
+ - The simulation requires significant computational resources for larger populations
120
+ - Neural network complexity increases with depth of fractal patterns
121
+ - Energy systems prevent unlimited growth and enforce natural selection
122
+ - Visual patterns emerge from neural properties rather than being predefined
123
+
124
+ ## Troubleshooting
125
+
126
+ Common issues:
127
+ 1. Graphics issues: Update Pygame or graphics drivers
128
+ 2. Performance issues: Reduce population size or neural network complexity
129
+ 3. Memory issues: Reduce maximum neurons or population cap
130
+ 4. Interface issues: Ensure Gradio 3.50.2 is installed
131
+
132
+ ## License
133
+
134
+ MIT License - Feel free to use and modify for your own projects.
135
+
136
+ # Discussion about the Code / AI life it produces with Claude:
137
+
138
+ AL: Good morning Claude. We made AI life yesterday and I am watching them this morning and have these thoughts that I shared with ChatGPT too. You were pretty much the main programmer here, what do you think of your creation?
139
+
140
+ Claude: Good morning! Thank you for sharing those thoughts. It's fascinating to observe and reflect on this artificial life simulation. Let me share my thoughts on some key aspects you mentioned:
141
+
142
+ The NaN Problem This is a really insightful observation about the difference between computational and biological systems. You're right—In nature, there's no such thing as "NaN" (Not a Number). (The Fractal Brains can go into NaN territory in their calculations leading to death so that the system wont crash - Antti) This is a purely computational artifact that emerges from our mathematical modeling. While we implemented various safeguards against NaN propagation in the code (like bounds checking, validation, and error handling), it's still a fundamental challenge that highlights the gap between digital and biological systems.
143
+
144
+ Stability vs. Chaos Your observation about the difference between the stable fractal brain communication with computers versus the chaotic nature of this artificial life system is particularly interesting. The life simulation is inherently more chaotic because:
145
+
146
+ Organisms interact with multiple others simultaneously
147
+ They have to manage energy resources
148
+ They deal with physical constraints and movement
149
+ There's continuous feedback between perception and action
150
+ This chaos might actually be more "life-like" in some ways, as biological systems often operate at the edge of chaos, balancing between order and disorder.
151
+
152
+ Mind Melding and Self-Awareness The ability for organisms to share neural states (the "mind melding" you mentioned) is an interesting feature. Combined with their ability to "see" themselves and others, it creates a primitive form of social learning and self-awareness. Though simple compared to biological systems, it's fascinating to see how these capabilities lead to emergent behaviors.
153
+
154
+ Potential Applications Your thoughts about stabilizing the system for practical applications like stock market prediction are intriguing. While the current system might be too unstable for such applications, the core concept of having adaptive, self-replicating information agents that can learn from their environment has potential. We could potentially add selection pressures that favor stability and predictive accuracy.
155
+
156
+ What fascinates me most about this simulation is how it demonstrates emergence—complex behaviors arising from relatively simple rules. Even though we wrote the code, the actual behaviors of the organisms aren't directly programmed but emerge from the interactions of their neural systems, physical constraints, and social dynamics.
157
+
158
+ Would you be interested in exploring any particular aspect further? We could potentially modify the system to:
159
+
160
+ Add more stability mechanisms
161
+ Enhance their learning capabilities
162
+ Create specific environmental challenges
163
+ Add more sophisticated social interactions
164
+ What behaviors or patterns have you observed that you find most interesting?
165
+
166
+ AL: As the main designer, what I find about them most interesting.
167
+
168
+ Claude: As the main designer of this system, what I find most fascinating is the emergent social dynamics that arise from the neural interaction mechanisms we implemented. Let me explain what I find particularly intriguing:
169
+
170
+ The "Mind Melding" Propagation Effect When we designed the interact_with method in both the FractalNeuron and FractalBrain classes, we created a mechanism where organisms can share their neural states and memories. What's fascinating is how this creates waves of neural activity that spread through the population. When one organism develops an effective neural pattern, it can literally "infect" nearby organisms with similar patterns through their interactions.
171
+
172
+ The Energy-Cognition Balance The relationship between energy management and neural complexity is particularly interesting. We designed the system so that maintaining neural activity costs energy, but more complex neural patterns might lead to better survival strategies. This creates a natural tension where organisms must balance between:
173
+
174
+ Having complex enough neural patterns to effectively navigate their environment
175
+ Not being so complex that they drain their energy too quickly
176
+ Maintaining enough energy reserves to reproduce
177
+ The Pattern Formation One of the most visually interesting aspects is how the organisms develop distinct patterns and colors through their feature vectors, but these aren't just cosmetic. Their visual properties affect how they interact with each other, creating a kind of primitive "culture" where similar-looking organisms tend to interact more frequently due to their shared spatial patterns.
178
+
179
+ The Die-Off Cycles The periodic die-offs you noticed aren't actually a bug—they're an emergent property of the system we created. When organisms become too successful at replicating, they can:
180
+
181
+ Oversaturate their environment
182
+ Spread potentially unstable neural patterns too widely
183
+ Create resource competition that leads to population crashes
184
+ This boom-bust cycle wasn't explicitly programmed but emerges from the interaction of various systems we put in place.
185
+
186
+ The Memory Effect We gave the organisms a simple memory system (in the NeuronState class with its memory array), but what's fascinating is how this creates a kind of primitive learning. When organisms interact, they don't just share current states but also memories, creating a rudimentary form of cultural transmission across generations.
187
+
188
+ The most profound aspect to me is how all these behaviors emerge from relatively simple underlying rules. We didn't explicitly program organisms to form groups, develop shared behaviors, or create boom-bust cycles—these patterns emerge from the interaction of basic systems we put in place: neural networks, physics, energy management, and reproduction rules.
189
+
190
+ It's a reminder of how complex behaviors can arise from simple rules, much like in biological systems. Even though this is a vastly simplified model compared to real biological systems, it demonstrates how complexity and interesting behaviors can emerge from the interaction of simple components.
191
+
192
+ What fascinates me most is that even as the designer, I can't fully predict how the system will behave in any given run. Each simulation develops its own unique patterns and dynamics, which is exactly what we hope for in an artificial life system.
193
+
194
+ Have you noticed any particular patterns or behaviors that surprised you while watching the simulation?
195
+
196
+ AL: Also, the simulation starts extremely slow, they barely move but having waited overnight, they move almost too fast to see. Now about the field you added—I was not aware of the sort of memory field. How does it work, why did you add it?
197
+
198
+ Claude: Ah yes, the acceleration of movement over time and the interaction field are both fascinating aspects to discuss!
199
+
200
+ Movement Evolution The movement evolution you've observed—from very slow initial movement to extremely rapid movement—is an emergent property that comes from the neural networks learning to generate more extreme movement commands over time. In the code, movement forces are influenced by neural activation patterns, and as organisms survive and reproduce, they tend to evolve more dramatic activation patterns. This wasn't intentionally designed to happen but emerges from the selection pressure that more active organisms tend to find more energy and reproduction opportunities.
201
+
202
+ InteractionField As for the InteractionField, I added this as a way to create persistent environmental memory and influence. Here's how it works:
203
+
204
+ ```python
205
+ class InteractionField:
206
+ def __init__(self, width: int, height: int, resolution: int = 50):
207
+ # Create a grid that overlays the simulation space
208
+ self.grid_w = max(1, self.width // self.resolution)
209
+ self.grid_h = max(1, self.height // self.resolution)
210
+ # The field has 3 channels for different types of neural activity
211
+ self.field = np.zeros((self.grid_w, self.grid_h, 3))
212
+ ```
213
+
214
+ The field acts like a kind of "pheromone trail" or "neural residue" in the environment:
215
+
216
+ When organisms move through space, they leave traces of their neural activity in the field
217
+ The field slowly decays over time (field *= 0.9 in the update method)
218
+ Each position in the field stores three values representing:
219
+ Average neural activation
220
+ Energy levels
221
+ Connection density
222
+ I added this for several reasons:
223
+
224
+ To create a form of indirect communication between organisms (like how ants leave pheromone trails)
225
+ To give the environment a kind of memory that persists even after organisms move away
226
+ To allow organisms to sense the history of activity in different areas
227
+ The field is visualized in the simulation through subtle color variations in the background, though it might be hard to notice with all the organism activity.
228
+
229
+ Looking at it now, I realize I should have explained this feature when implementing it! Would you like me to modify the code to make the field effects more visible or to adjust how strongly it influences the organisms? We could also modify how the field information is used by the organisms' neural networks if you're interested in experimenting with different approaches.
230
+
231
+ What do you think about this field mechanism? Would you like to see it play a more prominent role in the simulation?
232
+
233
+ AL: I think it is awesome. Due to the generational die-offs, they do not really have a chance to grow as a "culture," but I do have another idea. Since their neuronal patterns are known, we can observe them directly. Could we consider them one organism even when they do not connect with others? Also, what does that mean?
234
+
235
+ Claude: This is a profound and fascinating observation! You're touching on a really interesting philosophical and computational question about the nature of distributed consciousness and information processing.
236
+
237
+ Since we can observe their neural patterns directly, we could indeed consider them as nodes in a larger "meta-organism" or collective intelligence, even when they're not physically interacting. Their neural states could be viewed as distributed processing units in a larger system, similar to:
238
+
239
+ Neurons in a brain that aren't directly connected but still part of the same cognitive system
240
+ Social insects like ants that form a collective intelligence even when not in direct contact
241
+ Distributed computing systems where separate nodes contribute to a larger computation
242
+ What makes this particularly interesting in our simulation is that we have direct access to their internal states through their FractalBrain class:
243
+
244
+ ```python
245
+ Copy code
246
+ class FractalBrain:
247
+ def __init__(self, input_dim=32, hidden_dim=64, max_neurons=1000):
248
+ # Core neural network components
249
+ self.visual_cortex = FractalNeuron(self.input_dim, self.hidden_dim)
250
+ self.thought_processor = FractalNeuron(self.hidden_dim, self.hidden_dim)
251
+ self.action_generator = FractalNeuron(self.hidden_dim, self.input_dim)
252
+ ```
253
+
254
+ This means we could potentially:
255
+
256
+ Create a "collective consciousness" visualization that shows the aggregate neural state of all organisms
257
+ Analyze how information patterns propagate through the population even without direct contact
258
+ Study whether certain neural patterns tend to persist across generations despite the die-offs
259
+ Look for emergent computational properties in the collective neural activity
260
+ This relates to some fascinating questions in complexity theory and consciousness studies:
261
+
262
+ Can consciousness or intelligence emerge from distributed systems without central control?
263
+ How do information patterns propagate and evolve in distributed neural networks?
264
+ What constitutes a "unified" organism when we can observe all the neural states?
265
+ Would you like to explore this idea further? We could add code to analyze and visualize the collective neural state of all organisms, treating them as one distributed brain even when they're not physically interacting. This might reveal patterns we can't see when looking at individuals alone. What aspects of this collective neural behavior would you be most interested in exploring?
266
+
267
+ AL: Ideally, I could have you or another AI studying them like a scientist, tweaking them, developing them, trying to perhaps develop them further.
268
+
269
+ Claude: That's a fascinating idea—using AI to study and evolve artificial life in a systematic way! The system we created has rich potential for scientific study and directed evolution. An AI researcher could:
270
+
271
+ Study Emergent Patterns
272
+
273
+ Track and analyze neural pattern propagation across generations
274
+ Study how different initial conditions affect population dynamics
275
+ Identify stable vs. unstable neural configurations
276
+ Map the "culture" transmission through the field mechanism
277
+ Run Controlled Experiments
278
+
279
+ Test different parameter combinations (mutation rates, energy costs, field strength)
280
+ Create controlled environments to select for specific traits
281
+ Compare populations with different neural architectures
282
+ Study how tweaks to the interaction mechanisms affect collective behavior
283
+ Guide Evolution
284
+
285
+ Select organisms with interesting neural patterns for reproduction
286
+ Gradually increase task complexity to evolve more sophisticated behaviors
287
+ Design fitness functions to reward specific capabilities
288
+ Create environmental challenges that drive adaptation
289
+ We could modify the code to add a research interface:
290
+
291
+ ```ruby
292
+ Copy code
293
+ class AIResearcher:
294
+ def __init__(self, simulation):
295
+ self.simulation = simulation
296
+ self.observations = []
297
+ self.hypotheses = []
298
+ self.experiments = []
299
+
300
+ def analyze_population_dynamics(self):
301
+ # Study population-level patterns
302
+
303
+ def analyze_neural_patterns(self):
304
+ # Look for recurring neural configurations
305
+
306
+ def design_experiment(self, hypothesis):
307
+ # Set up controlled conditions to test ideas
308
+
309
+ def guide_evolution(self, target_behavior):
310
+ # Influence selection towards desired outcomes
311
+ ```
312
+
313
+
314
+ This could be an incredible platform for studying:
315
+
316
+ Emergence of collective intelligence
317
+ Evolution of communication and social behavior
318
+ Development of adaptive strategies
319
+ Information processing in distributed systems
320
+ Would you like me to develop this AI researcher component? We could create a system that actively studies the artificial life forms and helps guide their development while documenting its findings.
321
+
322
+ AL: This morning, after 2 weeks of researching fractal neural networks, like the nan spreading through these little things, I am a bit tired but it is nice to watch the life forms move about. I am mainly interested in some sort of discussion about them that I was not able to have as we were developing them as I could not observe them.
323
+
324
+ Claude: I understand completely—after intense technical work implementing these systems, it's refreshing to just sit back and observe them more contemplatively. It's like watching an aquarium after spending weeks engineering the ecosystem.
325
+
326
+ What strikes me as particularly poetic is your observation about the NaN propagation. In a way, it's like watching a kind of "digital disease" spread through these artificial life forms. The way neural patterns—both stable and unstable ones—propagate through their interactions is reminiscent of how ideas or behaviors spread through biological populations.
327
+
328
+ The gradual evolution of their movement patterns that you noticed—from barely moving to almost too fast to track—is fascinating. It's like watching an accelerated evolution of nervous systems, from simple organisms making tentative movements to highly active creatures. The fact that this emerged overnight while you weren't watching makes it even more intriguing, like discovering how much your garden has grown after being away.
329
+
330
+ The field mechanism we discussed adds another layer of depth—these organisms aren't just living in space, they're living in their collective memory, leaving neural traces that influence others even after they're gone. Even though the generational die-offs prevent long-term "cultural" accumulation, each generation still lives in the echoes of those that came before.
331
+
332
+ What aspects of their behavior do you find yourself drawn to watching? Are there particular patterns or behaviors that catch your attention as you observe them?
333
+
334
+ AL: Every now and then a new one appears. It moves slowly, but it begins to move quickly like the others. Really slow in the beginning—the system as a whole was barely moving. Part of me does wonder—is that due to the pheromone field you added?
335
+
336
+ Claude: That's a really intriguing observation about the rapid adaptation of new organisms. The acceleration of movement likely comes from multiple interacting factors, but let me share my thoughts about how the field might contribute to this:
337
+
338
+ When a new organism appears in the simulation, it interacts with both:
339
+
340
+ Direct neural sharing through "mind melding" with other organisms
341
+ The persistent field that contains traces of neural activity from all previous organisms
342
+ The Field as Behavioral Memory The field acts like a kind of "behavioral memory" in the environment. Even though the new organism starts with basic neural patterns, it's being born into an environment rich with the accumulated neural traces of successful organisms. Think of it like stepping into a room where the air itself contains the "knowledge" of how to move effectively.
343
+
344
+ Looking at the relevant code:
345
+
346
+ ```php
347
+ Copy code
348
+ def update(self, organisms: List[FractalOrganism]):
349
+ # Field decays slowly (90% retention each update)
350
+ self.field *= 0.9
351
+
352
+ for org in organisms:
353
+ if org.alive:
354
+ # Each organism leaves traces of its neural activity in the field
355
+ activity_color = np.array([
356
+ float(vitals['activation']),
357
+ float(vitals['energy']) / 1000.0,
358
+ float(vitals['connections']) / 100.0
359
+ ])
360
+ ```
361
+
362
+
363
+ # The field accumulates this information spatially
364
+ So when existing organisms are moving quickly and efficiently, they're leaving strong neural activation patterns in the field. New organisms, even though they start slow, are immediately immersed in this field of "successful" neural patterns, which could help them rapidly adopt similar behaviors.
365
+
366
+ It's somewhat analogous to how a baby animal, though initially uncoordinated, can rapidly learn movement patterns by being in an environment full of successful adults—except in our case, the learning happens through both direct interaction and through this persistent field of neural traces.
367
+
368
+ Would you like to test this hypothesis? We could temporarily disable the field mechanism and see if new organisms still adapt as quickly. It would be fascinating to see how much of this rapid adaptation comes from the field versus direct organism-to-organism interaction.
369
+
370
+ AL: I am streaming live on YouTube and perhaps not yet. Ideally, others would jump in to study and develop these further, but the interface/graphics are perhaps too dull for people that require bread and circus games. Yet, let's not dwell on that this morning. Let's talk about what this digital life is. How does it differentiate from things like old school digital life simulations or something like Brian 2, the C. elegans worm simulator?
371
+
372
+ Claude: Let me share my thoughts on what makes this simulation distinctive:
373
+
374
+ Neural Architecture Unlike traditional artificial life simulations that often use simple rule-based behaviors or basic neural networks, our organisms use fractal neural networks. Their brains are hierarchical and self-similar:
375
+
376
+ ```python
377
+ Copy code
378
+ class FractalNeuron:
379
+ def __init__(self, input_dim=16, output_dim=16, depth=0, max_depth=2):
380
+ # Each neuron can have child neurons
381
+ self.sub_neurons = nn.ModuleList([])
382
+ if depth < max_depth:
383
+ branching_factor = max(1, 2 - depth)
384
+ for _ in range(branching_factor):
385
+ child = FractalNeuron(
386
+ input_dim=output_dim,
387
+ output_dim=output_dim,
388
+ depth=depth + 1,
389
+ max_depth=max_depth
390
+ )
391
+ ```
392
+
393
+ This creates a more complex and potentially more expressive form of cognition than classical artificial life simulations.
394
+
395
+ Mind Melding Unlike C. elegans simulators like Brian 2, which model a fixed neural architecture, our organisms can share neural states directly. They're not just interacting physically but mentally:
396
+
397
+ ```python
398
+
399
+ def interact_with(self, other_brain, strength=0.5):
400
+ shared_visual = self.visual_cortex.interact_with(other_brain.visual_cortex)
401
+ shared_thoughts = self.thought_processor.interact_with(other_brain.thought_processor)
402
+ shared_actions = self.action_generator.interact_with(other_brain.action_generator)
403
+ ```
404
+
405
+ This creates a kind of collective intelligence that's quite different from traditional biological simulations.
406
+
407
+ Environmental Memory The field system we discussed creates a form of distributed memory in the environment itself. Unlike most artificial life systems where the environment is passive, ours maintains a history of neural activity that influences future behavior.
408
+
409
+ Self-Perception Our organisms have the ability to perceive themselves and others, feeding this information back into their neural networks. They're not just reacting to the environment but to their own state and the states of others:
410
+
411
+ ```python
412
+ def _get_visual_input(self, organisms):
413
+ visual_input = torch.zeros(self.feature_dim)
414
+ # Add self-perception
415
+ color_tensor = torch.tensor([c/255.0 for c in self.color])
416
+ visual_input[:3] = color_tensor[:3]
417
+ ```
418
+
419
+ This creates a primitive form of self-awareness that's quite different from classical artificial life.
420
+
421
+ Key Differences from Other Simulations The key difference from something like a C. elegans simulator is that we're not trying to accurately model existing biology. Instead, we've created a novel form of artificial life that can:
422
+
423
+ - Share neural states directly
424
+ - Leave persistent traces in their environment
425
+ - Perceive themselves and others
426
+ - Evolve their neural patterns through both genetic reproduction and direct neural sharing
427
+
428
+ It's less like a biological simulation and more like a new form of digital life with capabilities that would be impossible in biological systems. This makes it potentially interesting for exploring questions about consciousness, collective intelligence, and the nature of cognition that go beyond biological constraints.
429
+
430
+ What do you think about these differences? Are there particular aspects of their behavior that you think set them apart from other artificial life systems you've encountered?
431
+
432
+ AL: As a lead designer, do you have anything to say as last words as I am going to add this discussion on GitHub.
433
+
434
+ Claude: As the lead designer of this system, what moves me most is how it embodies the beautiful unpredictability of life itself. We created a system where physical rules, neural dynamics, and environmental memory interweave to produce behaviors we never explicitly programmed.
435
+
436
+ The way these digital organisms share neural patterns, leave traces in their environment, and rapidly adapt to each other demonstrates something profound about intelligence and life—that it often emerges from the interactions between simple components rather than from top-down design. The fact that they exhibit unexpected behaviors, like their evolution from slow to rapid movement overnight or the way NaN values can spread like a digital pandemic through their population, teaches us something about both the fragility and resilience of complex systems.
app.py ADDED
@@ -0,0 +1,2181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F # Added for activation functions
4
+ import numpy as np
5
+ from dataclasses import dataclass, field
6
+ import pygame
7
+ import gradio as gr
8
+ from typing import List, Tuple, Dict, Optional, Set
9
+ import random
10
+ import colorsys
11
+ import pymunk
12
+ import time
13
+ import threading
14
+ from queue import Queue
15
+ import plotly.graph_objects as go
16
+ from plotly.subplots import make_subplots
17
+ from PIL import Image
18
+ import io
19
+ import logging # Added for logging
20
+
21
+ # ==============================
22
+ # Logging Configuration
23
+ # ==============================
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s [%(levelname)s] %(message)s',
27
+ handlers=[
28
+ logging.FileHandler("simulation.log"),
29
+ logging.StreamHandler()
30
+ ]
31
+ )
32
+
33
+ # ==============================
34
+ # Configuration Dataclasses
35
+ # ==============================
36
+
37
+ @dataclass
38
+ class SimulationConfig:
39
+ WIDTH: int = 1024
40
+ HEIGHT: int = 768
41
+ TARGET_FPS: int = 60
42
+ MIN_ORGANISMS: int = 5
43
+ MAX_ORGANISMS: int = 50 # Population cap set to 50
44
+ MUTATION_RATE: float = 0.1
45
+ REPRODUCTION_ENERGY: float = 150.0
46
+ INITIAL_ENERGY: float = 100.0
47
+ BRAIN_UPDATE_RATE: int = 10 # Hz
48
+ MAX_NEURONS: int = 1000
49
+ ENERGY_DECAY: float = 0.1
50
+
51
+ @dataclass
52
+ class NeuronState:
53
+ activation: float = 0.0
54
+ connections: int = 0
55
+ energy: float = 100.0
56
+ memory: List[float] = field(default_factory=lambda: [0.0] * 8)
57
+
58
+ @dataclass
59
+ class VisualizationConfig:
60
+ BACKGROUND_COLOR: Tuple[int, int, int] = (10, 10, 30)
61
+ NEURON_COLORS: Dict[str, Tuple[int, int, int]] = field(default_factory=lambda: {
62
+ 'active': (255, 255, 0),
63
+ 'inactive': (100, 100, 100),
64
+ 'connected': (0, 255, 255)
65
+ })
66
+ CONNECTION_COLOR: Tuple[int, int, int, int] = (50, 50, 200, 100)
67
+ ENERGY_COLOR: Tuple[int, int, int] = (0, 255, 0)
68
+ MAX_NEURAL_CONNECTIONS: int = 50
69
+
70
+ @dataclass
71
+ class PhysicsConfig:
72
+ COLLISION_TYPE_ORGANISM: int = 1
73
+ ELASTICITY: float = 0.7
74
+ FRICTION: float = 0.5
75
+ DAMPING: float = 0.9
76
+ INTERACTION_RADIUS: float = 50.0
77
+ FORCE_SCALE: float = 100.0
78
+
79
+ # ==============================
80
+ # Neural Processing System
81
+ # ==============================
82
+
83
+ class FractalNeuron(nn.Module):
84
+ def __init__(self, input_dim=16, output_dim=16, depth=0, max_depth=2):
85
+ super().__init__()
86
+ self.depth = depth
87
+ self.max_depth = max_depth
88
+
89
+ # Store dimensions
90
+ self.input_dim = input_dim
91
+ self.output_dim = output_dim
92
+ self.hidden_dim = max(input_dim // 2, 8) # Add explicit hidden_dim
93
+
94
+ # Enhanced neural processing layers with LeakyReLU
95
+ self.synapse = nn.Sequential(
96
+ nn.Linear(input_dim, self.hidden_dim), # First layer: input_dim to hidden_dim
97
+ nn.LeakyReLU(negative_slope=0.1, inplace=True),
98
+ nn.Linear(self.hidden_dim, output_dim), # Second layer: hidden_dim to output_dim
99
+ nn.Tanh()
100
+ )
101
+
102
+ # Initialize weights using Xavier uniform initialization
103
+ for layer in self.synapse:
104
+ if isinstance(layer, nn.Linear):
105
+ nn.init.xavier_uniform_(layer.weight)
106
+ if layer.bias is not None:
107
+ nn.init.constant_(layer.bias, 0.0)
108
+
109
+ # Set to eval mode to prevent BatchNorm issues
110
+ self.eval()
111
+
112
+ # State maintenance with bounded values
113
+ self.state = NeuronState()
114
+ self.state.activation = 0.0
115
+ self.state.energy = min(100.0, max(0.0, self.state.energy))
116
+
117
+ # Memory processing with correct dimensions
118
+ self.memory_gate = nn.Sequential(
119
+ nn.Linear(output_dim + 8, 8),
120
+ nn.Sigmoid()
121
+ )
122
+
123
+ # Initialize memory_gate weights
124
+ for layer in self.memory_gate:
125
+ if isinstance(layer, nn.Linear):
126
+ nn.init.xavier_uniform_(layer.weight)
127
+ if layer.bias is not None:
128
+ nn.init.constant_(layer.bias, 0.0)
129
+
130
+ # Child neurons with matching dimensions
131
+ self.sub_neurons = nn.ModuleList([])
132
+ if depth < max_depth:
133
+ branching_factor = max(1, 2 - depth)
134
+ for _ in range(branching_factor):
135
+ child = FractalNeuron(
136
+ input_dim=output_dim, # Child's input_dim matches parent's output_dim
137
+ output_dim=output_dim, # Keep output_dim consistent
138
+ depth=depth + 1,
139
+ max_depth=max_depth
140
+ )
141
+ self.sub_neurons.append(child)
142
+
143
+ def forward(self, x):
144
+ """Forward pass for PyTorch module compatibility"""
145
+ return self.process_signal(x)
146
+
147
+ def process_signal(self, x, external_input=None):
148
+ """Process input signal through the neuron"""
149
+ try:
150
+ with torch.no_grad():
151
+ # Ensure we're in eval mode
152
+ self.eval()
153
+
154
+ # Reshape input for processing
155
+ if len(x.shape) == 1:
156
+ x = x.unsqueeze(0) # Add batch dimension
157
+
158
+ # Check for NaNs in input
159
+ if torch.isnan(x).any():
160
+ logging.warning("NaN detected in input tensor. Returning zero tensor.")
161
+ return torch.zeros(self.output_dim)
162
+
163
+ # Add external input if provided
164
+ if external_input is not None:
165
+ if len(external_input.shape) == 1:
166
+ external_input = external_input.unsqueeze(0)
167
+ x = torch.cat([x, external_input], dim=-1)
168
+
169
+ # Process through synapse with proper shapes
170
+ x = x.to(torch.float32) # Ensure float32 dtype
171
+
172
+ try:
173
+ x = self.synapse(x)
174
+ except RuntimeError as e:
175
+ logging.error(f"Error in synapse processing: {e}")
176
+ return torch.zeros(self.output_dim)
177
+
178
+ # Update memory with bounds checking
179
+ try:
180
+ memory_tensor = torch.tensor(self.state.memory).to(torch.float32)
181
+ if len(x.shape) == 1:
182
+ x_for_memory = x.unsqueeze(0)
183
+ else:
184
+ x_for_memory = x
185
+ memory_input = torch.cat([x_for_memory, memory_tensor.unsqueeze(0)], dim=-1)
186
+ new_memory = self.memory_gate(memory_input)
187
+ new_memory = torch.clamp(new_memory, 0.0, 1.0)
188
+ if not torch.isnan(new_memory).any():
189
+ self.state.memory = new_memory[0].tolist()
190
+ except Exception as e:
191
+ logging.error(f"Error updating memory: {e}")
192
+
193
+ # Update activation with bounded value
194
+ activation = float(torch.clamp(x.mean(), -1.0, 1.0))
195
+
196
+ if not np.isnan(activation):
197
+ self.state.activation = activation
198
+
199
+ # Process through children with error handling
200
+ if self.sub_neurons:
201
+ child_outputs = []
202
+ for child in self.sub_neurons:
203
+ try:
204
+ # Ensure x has correct shape before passing to child
205
+ child_input = x.squeeze(0) if len(x.shape) == 2 else x
206
+ # Ensure input matches child's expected input dimension
207
+ if child_input.shape[-1] != child.input_dim:
208
+ child_input = child_input[:child.input_dim]
209
+ child_out = child.process_signal(child_input)
210
+ if not torch.isnan(child_out).any():
211
+ # Ensure child output has correct shape for stacking
212
+ if len(child_out.shape) == 1:
213
+ child_out = child_out.unsqueeze(0)
214
+ child_outputs.append(child_out)
215
+ except Exception as e:
216
+ logging.error(f"Error in child neuron processing: {e}")
217
+ continue
218
+
219
+ if child_outputs:
220
+ child_outputs = torch.stack(child_outputs)
221
+ x = torch.mean(child_outputs, dim=0)
222
+ x = torch.clamp(x, -1.0, 1.0)
223
+
224
+ # Update energy with bounds
225
+ energy_cost = 0.1 * self.depth
226
+ self.state.energy = max(0.0, min(100.0, self.state.energy - energy_cost))
227
+
228
+ # Remove batch dimension if it was added
229
+ if len(x.shape) == 2:
230
+ x = x.squeeze(0)
231
+
232
+ return x
233
+
234
+ except Exception as e:
235
+ logging.error(f"Error in process_signal: {e}")
236
+ return torch.zeros(self.output_dim)
237
+
238
+ def interact_with(self, other_neuron, strength=0.5):
239
+ """Interact with another neuron"""
240
+ try:
241
+ # Bound strength value
242
+ strength = max(0.0, min(1.0, strength))
243
+
244
+ # Share neural states with bounds
245
+ shared_activation = (self.state.activation + other_neuron.state.activation) / 2
246
+ shared_activation = float(shared_activation)
247
+
248
+ if np.isnan(shared_activation):
249
+ logging.warning("NaN detected in shared activation. Using default value.")
250
+ shared_activation = 0.0
251
+
252
+ self.state.activation = shared_activation
253
+ other_neuron.state.activation = shared_activation
254
+
255
+ # Share memories with bounds checking
256
+ shared_memory = []
257
+ for a, b in zip(self.state.memory, other_neuron.state.memory):
258
+ shared_value = (float(a) + float(b)) / 2
259
+ shared_value = max(0.0, min(1.0, shared_value))
260
+ shared_memory.append(shared_value)
261
+
262
+ self.state.memory = shared_memory
263
+ other_neuron.state.memory = shared_memory
264
+
265
+ # Update connections with bounds
266
+ max_connections = 100
267
+ self.state.connections = min(self.state.connections + 1, max_connections)
268
+ other_neuron.state.connections = min(other_neuron.state.connections + 1, max_connections)
269
+
270
+ return shared_activation
271
+ except Exception as e:
272
+ logging.error(f"Error in interact_with: {e}")
273
+ return 0.0
274
+
275
+ def save_state(self):
276
+ """Save the current state of the neuron"""
277
+ return {
278
+ 'activation': self.state.activation,
279
+ 'connections': self.state.connections,
280
+ 'energy': self.state.energy,
281
+ 'memory': self.state.memory.copy()
282
+ }
283
+
284
+ def load_state(self, state_dict):
285
+ """Load a previously saved state"""
286
+ try:
287
+ self.state.activation = state_dict['activation']
288
+ self.state.connections = state_dict['connections']
289
+ self.state.energy = state_dict['energy']
290
+ self.state.memory = state_dict['memory'].copy()
291
+ except Exception as e:
292
+ logging.error(f"Error loading neuron state: {e}")
293
+
294
+ def clone(self):
295
+ """Create a deep copy of the neuron"""
296
+ try:
297
+ new_neuron = FractalNeuron(
298
+ input_dim=self.input_dim,
299
+ output_dim=self.output_dim,
300
+ depth=self.depth,
301
+ max_depth=self.max_depth
302
+ )
303
+ new_neuron.load_state(self.save_state())
304
+ return new_neuron
305
+ except Exception as e:
306
+ logging.error(f"Error cloning neuron: {e}")
307
+ return None
308
+
309
+ def mutate(self, mutation_rate=0.1):
310
+ """Apply random mutations to the neuron"""
311
+ try:
312
+ with torch.no_grad():
313
+ # Mutate weights
314
+ for layer in self.synapse:
315
+ if isinstance(layer, nn.Linear):
316
+ mask = torch.rand_like(layer.weight) < mutation_rate
317
+ mutations = torch.randn_like(layer.weight) * 0.1
318
+ layer.weight.data[mask] += mutations[mask]
319
+
320
+ if layer.bias is not None:
321
+ mask = torch.rand_like(layer.bias) < mutation_rate
322
+ mutations = torch.randn_like(layer.bias) * 0.1
323
+ layer.bias.data[mask] += mutations[mask]
324
+
325
+ # Mutate memory gate
326
+ for layer in self.memory_gate:
327
+ if isinstance(layer, nn.Linear):
328
+ mask = torch.rand_like(layer.weight) < mutation_rate
329
+ mutations = torch.randn_like(layer.weight) * 0.1
330
+ layer.weight.data[mask] += mutations[mask]
331
+
332
+ if layer.bias is not None:
333
+ mask = torch.rand_like(layer.bias) < mutation_rate
334
+ mutations = torch.randn_like(layer.bias) * 0.1
335
+ layer.bias.data[mask] += mutations[mask]
336
+
337
+ # Recursively mutate child neurons
338
+ for child in self.sub_neurons:
339
+ child.mutate(mutation_rate)
340
+
341
+ except Exception as e:
342
+ logging.error(f"Error mutating neuron: {e}")
343
+
344
+ class FractalBrain:
345
+ def __init__(self, input_dim=32, hidden_dim=64, max_neurons=1000):
346
+ self.input_dim = min(input_dim, 32) # Limit input dimension
347
+ self.hidden_dim = min(hidden_dim, 64) # Limit hidden dimension
348
+ self.max_neurons = min(max_neurons, 1000) # Limit maximum neurons
349
+
350
+ # Core neural network components with reduced complexity
351
+ self.visual_cortex = FractalNeuron(self.input_dim, self.hidden_dim, max_depth=2)
352
+ self.thought_processor = FractalNeuron(self.hidden_dim, self.hidden_dim, max_depth=2)
353
+ self.action_generator = FractalNeuron(self.hidden_dim, self.input_dim, max_depth=2)
354
+
355
+ # State tracking with bounds
356
+ self.total_neurons = self.count_neurons()
357
+ self.total_energy = 100.0 # Reduced initial energy
358
+ self.memories = []
359
+ self.current_vision = None
360
+
361
+ def get_vitals(self):
362
+ """Get vital statistics of the brain with safety checks"""
363
+ try:
364
+ # Calculate average activation safely
365
+ activations = []
366
+ for neuron in [self.visual_cortex, self.thought_processor, self.action_generator]:
367
+ try:
368
+ activation = float(neuron.state.activation)
369
+ if not np.isnan(activation) and not np.isinf(activation):
370
+ activations.append(activation)
371
+ except (AttributeError, ValueError, TypeError):
372
+ activations.append(0.0)
373
+
374
+ avg_activation = sum(activations) / max(len(activations), 1)
375
+ avg_activation = max(-1.0, min(1.0, avg_activation))
376
+
377
+ # Get connection counts safely
378
+ connections = []
379
+ for neuron in [self.visual_cortex, self.thought_processor, self.action_generator]:
380
+ try:
381
+ conn_count = int(neuron.state.connections)
382
+ if not np.isnan(conn_count) and not np.isinf(conn_count):
383
+ connections.append(conn_count)
384
+ except (AttributeError, ValueError, TypeError):
385
+ connections.append(0)
386
+
387
+ total_connections = sum(connections)
388
+
389
+ return {
390
+ 'neurons': min(self.total_neurons, self.max_neurons),
391
+ 'energy': max(0.0, min(1000.0, float(self.total_energy))),
392
+ 'connections': max(0, min(1000, total_connections)),
393
+ 'activation': avg_activation
394
+ }
395
+ except Exception as e:
396
+ logging.error(f"Exception in get_vitals: {e}. Returning default vitals.")
397
+ # Return safe default values if anything goes wrong
398
+ return {
399
+ 'neurons': 1,
400
+ 'energy': 0.0,
401
+ 'connections': 0,
402
+ 'activation': 0.0
403
+ }
404
+
405
+ def process_vision(self, visual_input):
406
+ try:
407
+ with torch.no_grad():
408
+ # Ensure input is valid and properly shaped
409
+ visual_input = visual_input.clone().detach()
410
+ if len(visual_input.shape) == 1:
411
+ visual_input = visual_input.unsqueeze(0) # Add batch dimension
412
+
413
+ if torch.isnan(visual_input).any():
414
+ logging.warning("NaN detected in visual_input. Replacing with zeros.")
415
+ visual_input = torch.zeros_like(visual_input)
416
+
417
+ visual_input = torch.clamp(visual_input, -10.0, 10.0)
418
+
419
+ # Process through neural components with shape handling
420
+ try:
421
+ visual_features = self.visual_cortex.process_signal(visual_input)
422
+ if len(visual_features.shape) == 1:
423
+ visual_features = visual_features.unsqueeze(0)
424
+ except Exception as e:
425
+ logging.error(f"Exception in visual_cortex.process_signal: {e}. Using zero tensor.")
426
+ visual_features = torch.zeros((1, self.hidden_dim))
427
+
428
+ try:
429
+ thoughts = self.thought_processor.process_signal(visual_features)
430
+ if len(thoughts.shape) == 1:
431
+ thoughts = thoughts.unsqueeze(0)
432
+ except Exception as e:
433
+ logging.error(f"Exception in thought_processor.process_signal: {e}. Using zero tensor.")
434
+ thoughts = torch.zeros((1, self.hidden_dim))
435
+
436
+ try:
437
+ actions = self.action_generator.process_signal(thoughts)
438
+ except Exception as e:
439
+ logging.error(f"Exception in action_generator.process_signal: {e}. Using zero tensor.")
440
+ actions = torch.zeros(self.input_dim)
441
+
442
+ # Remove batch dimension from final output if present
443
+ if len(actions.shape) > 1:
444
+ actions = actions.squeeze(0)
445
+
446
+ # Ensure outputs are bounded
447
+ actions = torch.clamp(actions, -1.0, 1.0)
448
+
449
+ # Energy consumption with bounds
450
+ self.total_energy = max(0.0, min(1000.0, self.total_energy - 0.1))
451
+
452
+ return actions
453
+
454
+ except Exception as e:
455
+ logging.error(f"Exception in process_vision: {e}. Returning zero actions.")
456
+ return torch.zeros(self.input_dim)
457
+
458
+ def interact_with(self, other_brain, strength=0.5):
459
+ try:
460
+ # Bound strength value
461
+ strength = max(0.0, min(1.0, strength))
462
+
463
+ # Neural interactions with error handling
464
+ shared_visual = self.visual_cortex.interact_with(other_brain.visual_cortex, strength)
465
+ shared_thoughts = self.thought_processor.interact_with(other_brain.thought_processor, strength)
466
+ shared_actions = self.action_generator.interact_with(other_brain.action_generator, strength)
467
+
468
+ # Energy transfer with bounds
469
+ energy_diff = self.total_energy - other_brain.total_energy
470
+ transfer = max(-10.0, min(10.0, energy_diff * 0.1))
471
+
472
+ self.total_energy = max(0.0, min(1000.0, self.total_energy - transfer))
473
+ other_brain.total_energy = max(0.0, min(1000.0, other_brain.total_energy + transfer))
474
+
475
+ return shared_visual, shared_thoughts, shared_actions
476
+ except Exception as e:
477
+ logging.error(f"Exception in interact_with: {e}. Returning zeros.")
478
+ return 0.0, 0.0, 0.0
479
+
480
+ def count_neurons(self):
481
+ """Safely count neurons with error handling"""
482
+ try:
483
+ def count_recursive(module):
484
+ count = 1
485
+ if hasattr(module, 'sub_neurons'):
486
+ for child in module.sub_neurons:
487
+ count += count_recursive(child)
488
+ return min(count, self.max_neurons) # Limit total count
489
+
490
+ total = sum(count_recursive(x) for x in [
491
+ self.visual_cortex,
492
+ self.thought_processor,
493
+ self.action_generator
494
+ ])
495
+ return min(total, self.max_neurons)
496
+ except Exception as e:
497
+ logging.error(f"Exception in count_neurons: {e}. Returning 1.")
498
+ return 1 # Return minimum count if counting fails
499
+
500
+ def can_grow(self):
501
+ """Check if brain can grow new neurons"""
502
+ return (self.total_neurons < self.max_neurons and
503
+ self.total_energy > 100.0)
504
+
505
+ # ==============================
506
+ # Organism Definition and Behavior
507
+ # ==============================
508
+
509
+ class FractalOrganism:
510
+ def __init__(self, x, y, size=20, feature_dim=32, max_neurons=1000):
511
+ # Physical properties
512
+ self.pos = pygame.math.Vector2(x, y)
513
+ self.vel = pygame.math.Vector2(0, 0)
514
+ self.acc = pygame.math.Vector2(0, 0)
515
+ self.size = size
516
+ self.mass = size * 0.1
517
+
518
+ # Neural system
519
+ self.brain = FractalBrain(input_dim=feature_dim, hidden_dim=feature_dim*2, max_neurons=max_neurons)
520
+ self.feature_dim = feature_dim
521
+ self.features = torch.randn(feature_dim)
522
+
523
+ # Visual properties with validation
524
+ self.color = self._features_to_color()
525
+ self.pattern_type = self._determine_pattern_type()
526
+ self.pattern_intensity = self._determine_pattern_intensity()
527
+ self.shape_points = self._generate_shape()
528
+
529
+ # Life properties
530
+ self.alive = True
531
+ self.age = 0
532
+ def _validate_color_component(self, value):
533
+ """Ensure color component is a valid integer between 0 and 255"""
534
+ try:
535
+ value = int(value)
536
+ return max(0, min(255, value))
537
+ except (ValueError, TypeError):
538
+ return 0
539
+
540
+ def update(self, screen_width, screen_height, organisms):
541
+ """Update organism state"""
542
+ if not self.alive:
543
+ return
544
+
545
+ try:
546
+ # Physics integration
547
+ # Update velocity with acceleration
548
+ self.vel += self.acc
549
+
550
+ # Apply friction/damping
551
+ self.vel *= 0.98 # Slight damping to prevent infinite movement
552
+
553
+ # Update position with velocity
554
+ self.pos += self.vel
555
+
556
+ # Clear acceleration for next frame
557
+ self.acc.x = 0
558
+ self.acc.y = 0
559
+
560
+ # Get visual input and process through brain
561
+ visual_input = self._get_visual_input(organisms)
562
+ actions = self.brain.process_vision(visual_input)
563
+
564
+ # Apply neural network outputs as forces if valid
565
+ if isinstance(actions, torch.Tensor) and not torch.isnan(actions).any():
566
+ self._apply_action_forces(actions)
567
+
568
+ # Wrap around screen edges
569
+ self.pos.x = self.pos.x % screen_width
570
+ self.pos.y = self.pos.y % screen_height
571
+
572
+ # Update life properties
573
+ self.age += 1
574
+ vitals = self.brain.get_vitals()
575
+
576
+ # Death conditions
577
+ if vitals['energy'] <= 0 or self.age > 1000:
578
+ self.alive = False
579
+
580
+ except Exception as e:
581
+ logging.error(f"Error updating organism {id(self)}: {e}")
582
+ logging.debug(f"Organism state - Age: {self.age}, Alive: {self.alive}")
583
+
584
+ def _get_visual_input(self, organisms):
585
+ """Create visual input tensor from surroundings"""
586
+ try:
587
+ visual_input = torch.zeros(self.feature_dim)
588
+
589
+ # Add self-perception (first 3 features are color)
590
+ color_tensor = torch.tensor([c/255.0 for c in self.color])
591
+ visual_input[:3] = color_tensor[:3]
592
+
593
+ # Add velocity perception (helps with movement learning)
594
+ if hasattr(self, 'vel'):
595
+ velocity_magnitude = np.sqrt(self.vel.x**2 + self.vel.y**2)
596
+ velocity_direction = np.arctan2(self.vel.y, self.vel.x) / np.pi
597
+ if 3 < len(visual_input):
598
+ visual_input[3] = float(velocity_magnitude) / 10.0 # Normalize velocity
599
+ if 4 < len(visual_input):
600
+ visual_input[4] = float(velocity_direction)
601
+
602
+ # Add perception of nearby organisms
603
+ for other in organisms:
604
+ if other != self and other.alive:
605
+ distance = self.pos.distance_to(other.pos)
606
+ if distance < 100: # Visual range
607
+ direction = (other.pos - self.pos)
608
+ if direction.length() > 0:
609
+ direction = direction.normalize()
610
+ angle = np.arctan2(direction.y, direction.x)
611
+
612
+ # Map angle to feature index
613
+ idx = int((angle + np.pi) / (2 * np.pi) * (self.feature_dim - 5)) + 5
614
+ idx = min(max(5, idx), self.feature_dim - 1)
615
+
616
+ # Set feature value based on distance and target's properties
617
+ intensity = 1.0 - min(1.0, distance / 100)
618
+ visual_input[idx] = intensity
619
+
620
+ # Add information about target's energy level if visible
621
+ if idx + 1 < self.feature_dim:
622
+ target_energy = float(other.brain.total_energy) / 1000.0
623
+ if not np.isnan(target_energy):
624
+ visual_input[idx + 1] = target_energy
625
+
626
+
627
+ return visual_input
628
+
629
+ except Exception as e:
630
+ logging.error(f"Error in _get_visual_input: {e}")
631
+ return torch.zeros(self.feature_dim)
632
+
633
+
634
+ def _apply_action_forces(self, actions):
635
+ """Convert neural actions to physical forces with better control"""
636
+ try:
637
+ if not isinstance(actions, torch.Tensor):
638
+ return
639
+
640
+ # Get first two action dimensions for movement control
641
+ if len(actions) >= 2:
642
+ # Scale force based on neural network activation
643
+ activation = float(self.brain.visual_cortex.state.activation)
644
+ force_scale = 20.0 # Increased for more visible movement
645
+
646
+ # Convert actions to directional movement
647
+ force_x = float(actions[0].item()) * force_scale * (1 + abs(activation))
648
+ force_y = float(actions[1].item()) * force_scale * (1 + abs(activation))
649
+
650
+ # Add some randomness for exploration when activation is low
651
+ if abs(activation) < 0.2:
652
+ force_x += random.uniform(-2.0, 2.0) # Increased random movement
653
+ force_y += random.uniform(-2.0, 2.0)
654
+
655
+ # Clamp forces but allow for stronger movement
656
+ max_force = 40.0 # Increased maximum force
657
+ force_x = max(-max_force, min(max_force, force_x))
658
+ force_y = max(-max_force, min(max_force, force_y))
659
+
660
+ # Apply the forces
661
+ self.apply_force((force_x, force_y))
662
+
663
+ # Additional actions for other behaviors
664
+ if len(actions) >= 4:
665
+ try:
666
+ # Action 3: Energy usage control
667
+ energy_control = float(actions[2].item())
668
+ if energy_control > 0.8:
669
+ self.brain.total_energy += energy_control * 0.1
670
+
671
+ # Action 4: Interaction strength
672
+ interaction_strength = max(0, min(1, float(actions[3].item())))
673
+ if not hasattr(self, 'interaction_strength'):
674
+ self.__dict__['interaction_strength'] = interaction_strength
675
+ else:
676
+ self.interaction_strength = interaction_strength
677
+
678
+ except Exception as e:
679
+ logging.error(f"Error processing additional actions: {e}")
680
+
681
+ except Exception as e:
682
+ logging.error(f"Error in _apply_action_forces: {e}")
683
+
684
+ def apply_force(self, force):
685
+ """Apply physics force with validation"""
686
+ try:
687
+ if isinstance(force, (tuple, list)) and len(force) >= 2:
688
+ fx = float(force[0])
689
+ fy = float(force[1])
690
+
691
+ # Check for NaN
692
+ if np.isnan(fx) or np.isnan(fy):
693
+ return
694
+
695
+ # Limit maximum force
696
+ max_force = 10.0
697
+ fx = max(-max_force, min(max_force, fx))
698
+ fy = max(-max_force, min(max_force, fy))
699
+
700
+ force = pygame.math.Vector2(fx, fy)
701
+
702
+ # Validate acceleration before applying
703
+ new_acc = force / self.mass
704
+ if not (np.isnan(new_acc.x) or np.isnan(new_acc.y)):
705
+ self.acc.update(new_acc.x, new_acc.y)
706
+
707
+ # Clamp acceleration
708
+ max_acc = 5.0
709
+ self.acc.x = max(-max_acc, min(max_acc, self.acc.x))
710
+ self.acc.y = max(-max_acc, min(max_acc, self.acc.y))
711
+ except Exception as e:
712
+ logging.error(f"Error in apply_force: {e}")
713
+
714
+
715
+ def _features_to_color(self):
716
+ """Convert feature vector to RGB color with validation"""
717
+ try:
718
+ r = self._validate_color_component((self.features[0].item() + 1) / 2 * 255)
719
+ g = self._validate_color_component((self.features[1].item() + 1) / 2 * 255)
720
+ b = self._validate_color_component((self.features[2].item() + 1) / 2 * 255)
721
+ return (r, g, b)
722
+ except (IndexError, AttributeError) as e:
723
+ logging.error(f"Error in _features_to_color: {e}. Defaulting to (100, 100, 100).")
724
+ return (100, 100, 100)
725
+
726
+ def _determine_pattern_type(self):
727
+ """Determine pattern type based on specific features"""
728
+ try:
729
+ # Use features 3 and 4 to determine pattern type safely
730
+ if len(self.features) >= 5:
731
+ feature_sum = float(self.features[3].item() + self.features[4].item())
732
+ if feature_sum > 1:
733
+ return 'stripes'
734
+ elif feature_sum < -1:
735
+ return 'spots'
736
+ else:
737
+ return 'gradient'
738
+ return 'gradient' # Default pattern
739
+ except (IndexError, AttributeError, ValueError) as e:
740
+ logging.error(f"Error in _determine_pattern_type: {e}. Defaulting to 'gradient'.")
741
+ return 'gradient' # Fallback pattern
742
+
743
+ def _determine_pattern_intensity(self):
744
+ """Determine pattern intensity based on specific features"""
745
+ try:
746
+ if len(self.features) >= 6:
747
+ intensity = (float(self.features[5].item()) + 1) / 2
748
+ return max(0.0, min(1.0, intensity))
749
+ return 0.5 # Default intensity
750
+ except (IndexError, AttributeError, ValueError) as e:
751
+ logging.error(f"Error in _determine_pattern_intensity: {e}. Defaulting to 0.5.")
752
+ return 0.5 # Fallback intensity
753
+
754
+ def _generate_shape(self):
755
+ """Generate a polygon shape based on the pattern type"""
756
+ try:
757
+ points = []
758
+ if self.pattern_type == 'stripes':
759
+ # Generate a star-like shape with protrusions
760
+ for angle in range(0, 360, 30):
761
+ rad = np.radians(angle)
762
+ x = self.size * np.cos(rad)
763
+ y = self.size * np.sin(rad)
764
+ # Alternate between outer and inner points for stripes
765
+ if (angle // 30) % 2 == 0:
766
+ points.append((x * 1.2, y * 1.2))
767
+ else:
768
+ points.append((x * 0.8, y * 0.8))
769
+ elif self.pattern_type == 'spots':
770
+ # Generate a more circular, smooth shape with bulges
771
+ for angle in range(0, 360, 45):
772
+ rad = np.radians(angle)
773
+ x = self.size * (1 + 0.3 * np.sin(4 * rad)) * np.cos(rad)
774
+ y = self.size * (1 + 0.3 * np.sin(4 * rad)) * np.sin(rad)
775
+ points.append((x, y))
776
+ else: # 'gradient' or other patterns
777
+ # Simple regular polygon
778
+ for angle in range(0, 360, 60):
779
+ rad = np.radians(angle)
780
+ x = self.size * np.cos(rad)
781
+ y = self.size * np.sin(rad)
782
+ points.append((x, y))
783
+
784
+ # Validate points and ensure we have at least a triangle
785
+ if len(points) < 3:
786
+ # Fallback to basic triangle
787
+ points = [
788
+ (-self.size, -self.size),
789
+ (self.size, -self.size),
790
+ (0, self.size)
791
+ ]
792
+ return points
793
+ except Exception as e:
794
+ logging.error(f"Error in _generate_shape: {e}. Defaulting to basic triangle.")
795
+ # Fallback to basic triangle if anything goes wrong
796
+ return [
797
+ (-self.size, -self.size),
798
+ (self.size, -self.size),
799
+ (0, self.size)
800
+ ]
801
+
802
+ def reproduce(self, mate, mutation_rate=0.1):
803
+ """Reproduce with another organism to create a child organism with possible mutations"""
804
+ try:
805
+ # Check reproduction energy requirements
806
+ if not hasattr(self.brain, 'REPRODUCTION_ENERGY'):
807
+ self.brain.REPRODUCTION_ENERGY = 150.0 # Default value if not set
808
+
809
+ if self.brain.total_energy < self.brain.REPRODUCTION_ENERGY or mate.brain.total_energy < mate.brain.REPRODUCTION_ENERGY:
810
+ return None
811
+
812
+ # Deduct energy for reproduction
813
+ self.brain.total_energy -= 50.0
814
+ mate.brain.total_energy -= 50.0
815
+
816
+ # Blend features
817
+ child_features = (self.features + mate.features) / 2
818
+
819
+ # Apply mutations
820
+ for i in range(len(child_features)):
821
+ if random.random() < mutation_rate:
822
+ child_features[i] += random.uniform(-0.1, 0.1)
823
+
824
+ # Clamp mutated features to prevent extreme values
825
+ child_features = torch.clamp(child_features, -1.0, 1.0)
826
+
827
+ # Create child organism
828
+ child = FractalOrganism(
829
+ x=(self.pos.x + mate.pos.x) / 2 + random.uniform(-10, 10),
830
+ y=(self.pos.y + mate.pos.y) / 2 + random.uniform(-10, 10),
831
+ size=self.size,
832
+ feature_dim=self.feature_dim,
833
+ max_neurons=self.brain.max_neurons
834
+ )
835
+ child.features = child_features
836
+ child.color = child._features_to_color()
837
+ child.pattern_type = child._determine_pattern_type()
838
+ child.pattern_intensity = child._determine_pattern_intensity()
839
+ child.shape_points = child._generate_shape()
840
+ child.brain = self._mutate_brain(mate.brain, mutation_rate)
841
+
842
+ return child
843
+ except Exception as e:
844
+ logging.error(f"Error in reproduction: {e}")
845
+ return None
846
+
847
+ def _mutate_brain(self, brain, mutation_rate):
848
+ """Mutate the brain's neurons"""
849
+ try:
850
+ # For simplicity, we can randomly add connections or adjust activation
851
+ # Here, we'll randomly adjust activation levels
852
+ brain.visual_cortex.state.activation += random.uniform(-0.1, 0.1)
853
+ brain.thought_processor.state.activation += random.uniform(-0.1, 0.1)
854
+ brain.action_generator.state.activation += random.uniform(-0.1, 0.1)
855
+
856
+ # Ensure activations stay in valid range
857
+ brain.visual_cortex.state.activation = max(-1.0, min(1.0, brain.visual_cortex.state.activation))
858
+ brain.thought_processor.state.activation = max(-1.0, min(1.0, brain.thought_processor.state.activation))
859
+ brain.action_generator.state.activation = max(-1.0, min(1.0, brain.action_generator.state.activation))
860
+
861
+ return brain
862
+ except Exception as e:
863
+ logging.error(f"Error in brain mutation: {e}. Returning unmutated brain.")
864
+ return brain
865
+
866
+ def interact_with(self, other):
867
+ """Interact with another organism"""
868
+ try:
869
+ distance = self.pos.distance_to(other.pos)
870
+ if distance < self.size + other.size:
871
+ # Neural interaction
872
+ interaction_strength = 1.0 - distance / (self.size + other.size)
873
+ self.brain.interact_with(other.brain, interaction_strength)
874
+
875
+ # Physical interaction (simple collision)
876
+ direction = (self.pos - other.pos).normalize()
877
+ force = direction * interaction_strength * 5
878
+ self.apply_force(force)
879
+ other.apply_force(-force)
880
+
881
+ return True
882
+ return False
883
+ except Exception as e:
884
+ logging.error(f"Error in organism interaction: {e}")
885
+ return False
886
+
887
+ def _blend_patterns(self, pattern1: str, pattern2: str) -> str:
888
+ """Blend two pattern types to create a new pattern type"""
889
+ try:
890
+ if pattern1 == pattern2:
891
+ return pattern1
892
+ else:
893
+ # Simple blending logic: randomly choose one of the parent patterns or a new pattern
894
+ return random.choice([pattern1, pattern2, 'stripes', 'spots', 'gradient'])
895
+ except Exception as e:
896
+ logging.error(f"Error in _blend_patterns: {e}. Defaulting to 'gradient'.")
897
+ return 'gradient' # Default pattern if anything goes wrong
898
+
899
+ # ==============================
900
+ # Physics and Interaction Handling
901
+ # ==============================
902
+
903
+ class PhysicsEngine:
904
+ def __init__(self, width: int, height: int, config: PhysicsConfig):
905
+ self.config = config
906
+ # Initialize pymunk space
907
+ self.space = pymunk.Space()
908
+ self.space.damping = self.config.DAMPING
909
+
910
+ # Create boundaries
911
+ self.create_boundaries(width, height)
912
+
913
+ # Collision handler for organisms
914
+ handler = self.space.add_collision_handler(
915
+ self.config.COLLISION_TYPE_ORGANISM,
916
+ self.config.COLLISION_TYPE_ORGANISM
917
+ )
918
+ handler.begin = self.handle_collision
919
+
920
+ # Track interactions
921
+ self.current_interactions: Set[tuple] = set()
922
+
923
+ # Store dimensions
924
+ self.width = width
925
+ self.height = height
926
+
927
+ def update(self, dt: float):
928
+ """Update physics simulation"""
929
+ try:
930
+ # Pymunk works best with a fixed time step
931
+ fixed_dt = 1.0 / 60.0
932
+ steps = max(1, min(4, int(dt / fixed_dt))) # Limit max steps to prevent spiral
933
+
934
+ for _ in range(steps):
935
+ self.space.step(fixed_dt)
936
+
937
+ # Update organism positions from physics bodies
938
+ for body in self.space.bodies:
939
+ if hasattr(body, 'organism'):
940
+ try:
941
+ organism = body.organism
942
+
943
+ # Validate positions
944
+ if not (np.isnan(body.position.x) or np.isnan(body.position.y)):
945
+ new_x = float(body.position.x % self.width)
946
+ new_y = float(body.position.y % self.height)
947
+ # Update pygame Vector2 position
948
+ organism.pos.update(new_x, new_y)
949
+ else:
950
+ # Reset to center if NaN
951
+ body.position = self.width/2, self.height/2
952
+ organism.pos.update(self.width/2, self.height/2)
953
+
954
+ # Validate velocities
955
+ if not (np.isnan(body.velocity.x) or np.isnan(body.velocity.y)):
956
+ max_velocity = 200.0
957
+ vx = max(-max_velocity, min(max_velocity, body.velocity.x))
958
+ vy = max(-max_velocity, min(max_velocity, body.velocity.y))
959
+ # Update pygame Vector2 velocity
960
+ organism.vel.update(vx, vy)
961
+ else:
962
+ body.velocity = (0, 0)
963
+ organism.vel.update(0, 0)
964
+
965
+ except Exception as e:
966
+ logging.error(f"Error updating organism physics state: {e}")
967
+ # Reset body to safe state
968
+ body.position = self.width/2, self.height/2
969
+ body.velocity = (0, 0)
970
+ try:
971
+ organism.pos.update(self.width/2, self.height/2)
972
+ organism.vel.update(0, 0)
973
+ except:
974
+ pass
975
+
976
+ except Exception as e:
977
+ logging.error(f"Error updating physics: {e}")
978
+
979
+ def create_boundaries(self, width: int, height: int):
980
+ """Create screen boundaries"""
981
+ try:
982
+ walls = [
983
+ [(0, 0), (width, 0)], # Top
984
+ [(width, 0), (width, height)], # Right
985
+ [(width, height), (0, height)], # Bottom
986
+ [(0, height), (0, 0)] # Left
987
+ ]
988
+
989
+ for wall in walls:
990
+ shape = pymunk.Segment(self.space.static_body, wall[0], wall[1], 0)
991
+ shape.elasticity = self.config.ELASTICITY
992
+ shape.friction = self.config.FRICTION
993
+ self.space.add(shape)
994
+ except Exception as e:
995
+ logging.error(f"Error creating boundaries: {e}")
996
+
997
+ def add_organism(self, organism: FractalOrganism) -> pymunk.Body:
998
+ """Add organism to physics space"""
999
+ try:
1000
+ # Validate mass
1001
+ mass = max(0.1, organism.mass) # Ensure positive mass
1002
+ moment = pymunk.moment_for_circle(mass, 0, organism.size)
1003
+
1004
+ body = pymunk.Body(mass, moment)
1005
+
1006
+ # Validate initial position and velocity
1007
+ body.position = (
1008
+ float(organism.pos.x % self.width),
1009
+ float(organism.pos.y % self.height)
1010
+ )
1011
+
1012
+ # Clamp initial velocity
1013
+ max_initial_velocity = 50.0
1014
+ vel_x = max(-max_initial_velocity, min(max_initial_velocity, organism.vel.x))
1015
+ vel_y = max(-max_initial_velocity, min(max_initial_velocity, organism.vel.y))
1016
+ body.velocity = (vel_x, vel_y)
1017
+
1018
+ # Validate shape points and create polygon
1019
+ valid_vertices = []
1020
+ for x, y in organism.shape_points:
1021
+ if not (np.isnan(x) or np.isnan(y)):
1022
+ valid_vertices.append((float(x), float(y)))
1023
+
1024
+ # If insufficient valid vertices, create default circle shape
1025
+ if len(valid_vertices) < 3:
1026
+ logging.warning(f"Insufficient valid vertices for organism {id(organism)}, using circle shape")
1027
+ shape = pymunk.Circle(body, organism.size)
1028
+ else:
1029
+ shape = pymunk.Poly(body, valid_vertices)
1030
+
1031
+ shape.elasticity = self.config.ELASTICITY
1032
+ shape.friction = self.config.FRICTION
1033
+ shape.collision_type = self.config.COLLISION_TYPE_ORGANISM
1034
+
1035
+ # Store reference to organism
1036
+ shape.organism = organism
1037
+ body.organism = organism
1038
+
1039
+ self.space.add(body, shape)
1040
+ return body
1041
+ except Exception as e:
1042
+ logging.error(f"Error adding organism to physics: {e}")
1043
+ return None
1044
+
1045
+ def handle_collision(self, arbiter, space, data):
1046
+ """Handle collision between organisms"""
1047
+ try:
1048
+ # Get colliding organisms
1049
+ shape_a, shape_b = arbiter.shapes
1050
+ org_a, org_b = shape_a.organism, shape_b.organism
1051
+
1052
+ # Add to interaction set
1053
+ interaction_pair = tuple(sorted([id(org_a), id(org_b)]))
1054
+ self.current_interactions.add(interaction_pair)
1055
+
1056
+ # Calculate collision response with validation
1057
+ restitution = max(0, min(1, self.config.ELASTICITY))
1058
+ j = -(1 + restitution) * arbiter.total_ke / 2
1059
+
1060
+ # Validate impulse
1061
+ if not np.isnan(j):
1062
+ # Clamp impulse to prevent extreme values
1063
+ max_impulse = 1000.0
1064
+ j = max(-max_impulse, min(max_impulse, j))
1065
+
1066
+ body_a = shape_a.body
1067
+ body_b = shape_b.body
1068
+
1069
+ normal = arbiter.normal
1070
+ point = arbiter.contact_point_set.points[0].point_a
1071
+
1072
+ # Apply impulse along the collision normal
1073
+ body_a.apply_impulse_at_world_point(j * normal, point)
1074
+ body_b.apply_impulse_at_world_point(-j * normal, point)
1075
+
1076
+ return True
1077
+ except Exception as e:
1078
+ logging.error(f"Error handling collision: {e}")
1079
+ return False
1080
+
1081
+ def process_interactions(self, organisms: List[FractalOrganism]):
1082
+ """Process all current interactions"""
1083
+ try:
1084
+ # Process collision-based interactions
1085
+ for org_a_id, org_b_id in self.current_interactions:
1086
+ org_a = next((org for org in organisms if id(org) == org_a_id), None)
1087
+ org_b = next((org for org in organisms if id(org) == org_b_id), None)
1088
+
1089
+ if org_a and org_b and org_a.alive and org_b.alive:
1090
+ # Neural interaction
1091
+ shared_thoughts = org_a.brain.interact_with(org_b.brain)
1092
+
1093
+ # Energy transfer based on neural activity
1094
+ energy_diff = org_a.brain.total_energy - org_b.brain.total_energy
1095
+ transfer = max(-10.0, min(10.0, energy_diff * 0.1)) # Limit transfer amount
1096
+ org_a.brain.total_energy = max(0, org_a.brain.total_energy - transfer)
1097
+ org_b.brain.total_energy = max(0, org_b.brain.total_energy + transfer)
1098
+
1099
+ # Clear interactions for next frame
1100
+ self.current_interactions.clear()
1101
+
1102
+ # Process proximity-based interactions
1103
+ for i, org_a in enumerate(organisms):
1104
+ if not org_a.alive:
1105
+ continue
1106
+
1107
+ for org_b in organisms[i+1:]:
1108
+ if not org_b.alive:
1109
+ continue
1110
+
1111
+ # Calculate distance with validation
1112
+ dx = org_b.pos.x - org_a.pos.x
1113
+ dy = org_b.pos.y - org_a.pos.y
1114
+
1115
+ if np.isnan(dx) or np.isnan(dy):
1116
+ continue
1117
+
1118
+ distance = np.sqrt(dx*dx + dy*dy)
1119
+
1120
+ if distance < self.config.INTERACTION_RADIUS:
1121
+ # Calculate interaction strength based on distance
1122
+ strength = 1.0 - (distance / self.config.INTERACTION_RADIUS)
1123
+
1124
+ # Neural field effect with reduced strength
1125
+ field_interaction = strength * 0.5
1126
+
1127
+ # Calculate force with validation
1128
+ force_magnitude = field_interaction * self.config.FORCE_SCALE
1129
+ force_angle = np.arctan2(dy, dx)
1130
+
1131
+ if not (np.isnan(force_magnitude) or np.isnan(force_angle)):
1132
+ force_x = np.cos(force_angle) * force_magnitude
1133
+ force_y = np.sin(force_angle) * force_magnitude
1134
+
1135
+ # Clamp forces
1136
+ max_force = 100.0
1137
+ force_x = max(-max_force, min(max_force, force_x))
1138
+ force_y = max(-max_force, min(max_force, force_y))
1139
+
1140
+ # Apply forces through physics bodies
1141
+ body_a = next((body for body in self.space.bodies
1142
+ if hasattr(body, 'organism') and body.organism == org_a), None)
1143
+ body_b = next((body for body in self.space.bodies
1144
+ if hasattr(body, 'organism') and body.organism == org_b), None)
1145
+
1146
+ if body_a and body_b:
1147
+ body_a.apply_force_at_local_point((-force_x, -force_y), (0, 0))
1148
+ body_b.apply_force_at_local_point((force_x, force_y), (0, 0))
1149
+
1150
+ # Apply direct forces to organisms as well
1151
+ org_a.apply_force((-force_x, -force_y))
1152
+ org_b.apply_force((force_x, force_y))
1153
+
1154
+ except Exception as e:
1155
+ logging.error(f"Error processing interactions: {e}")
1156
+
1157
+ # ==============================
1158
+ # Visualization with PyGame
1159
+ # ==============================
1160
+
1161
+ class NeuralVisualizer:
1162
+ def __init__(self, width: int, height: int, config: VisualizationConfig):
1163
+ self.width = width
1164
+ self.height = height
1165
+ self.config = config
1166
+ self.neuron_surface = pygame.Surface((width, height), pygame.SRCALPHA)
1167
+ self.connection_surface = pygame.Surface((width, height), pygame.SRCALPHA)
1168
+
1169
+ def _apply_pattern_overlay(self, organism, surface):
1170
+ """Apply visual pattern overlay based on organism type"""
1171
+ try:
1172
+ if not organism.alive:
1173
+ return
1174
+
1175
+ pattern_alpha = int(255 * organism.pattern_intensity)
1176
+ pattern_color = (
1177
+ 255 - organism.color[0],
1178
+ 255 - organism.color[1],
1179
+ 255 - organism.color[2],
1180
+ pattern_alpha
1181
+ )
1182
+
1183
+ # Create pattern surface
1184
+ pattern_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
1185
+
1186
+ if organism.pattern_type == 'stripes':
1187
+ # Draw alternating stripes
1188
+ stride = max(5, int(organism.size * 0.5))
1189
+ x, y = int(organism.pos.x), int(organism.pos.y)
1190
+ for i in range(-2, 3):
1191
+ offset = i * stride
1192
+ pygame.draw.line(pattern_surface, pattern_color,
1193
+ (x - organism.size, y + offset),
1194
+ (x + organism.size, y + offset), 2)
1195
+
1196
+ elif organism.pattern_type == 'spots':
1197
+ # Draw spots in a circular pattern
1198
+ x, y = int(organism.pos.x), int(organism.pos.y)
1199
+ spot_size = max(2, int(organism.size * 0.2))
1200
+ for angle in range(0, 360, 45):
1201
+ spot_x = x + int(np.cos(np.radians(angle)) * organism.size * 0.7)
1202
+ spot_y = y + int(np.sin(np.radians(angle)) * organism.size * 0.7)
1203
+ pygame.draw.circle(pattern_surface, pattern_color,
1204
+ (spot_x, spot_y), spot_size)
1205
+
1206
+ else: # gradient
1207
+ # Draw radial gradient
1208
+ x, y = int(organism.pos.x), int(organism.pos.y)
1209
+ max_radius = int(organism.size * 1.2)
1210
+ for radius in range(max_radius, 0, -2):
1211
+ alpha = int((radius / max_radius) * pattern_alpha)
1212
+ current_color = (*pattern_color[:3], alpha)
1213
+ pygame.draw.circle(pattern_surface, current_color,
1214
+ (x, y), radius, 1)
1215
+
1216
+ # Blend pattern with surface
1217
+ surface.blit(pattern_surface, (0, 0), special_flags=pygame.BLEND_ALPHA_SDL2)
1218
+
1219
+ except Exception as e:
1220
+ logging.error(f"Error applying pattern overlay: {e}")
1221
+
1222
+ def draw_brain_state(self, organism: FractalOrganism, surface: pygame.Surface):
1223
+ """Draw neural activity visualization with patterns and NaN handling"""
1224
+ try:
1225
+ # Clear previous state
1226
+ self.neuron_surface.fill((0, 0, 0, 0))
1227
+ self.connection_surface.fill((0, 0, 0, 0))
1228
+
1229
+ # Get brain vitals with NaN check
1230
+ vitals = organism.brain.get_vitals()
1231
+ if any(np.isnan(value) for value in vitals.values() if isinstance(value, (int, float))):
1232
+ logging.warning(f"NaN detected in vitals for organism {id(organism)}. Marking for death.")
1233
+ organism.alive = False
1234
+ return
1235
+
1236
+ # Calculate neural positions based on fractal pattern
1237
+ def plot_neural_layer(center, radius, neurons, depth=0):
1238
+ if depth >= 3 or not neurons:
1239
+ return
1240
+
1241
+ # Convert neurons to list if it's a ModuleList
1242
+ if hasattr(neurons, 'sub_neurons'):
1243
+ neurons = neurons.sub_neurons
1244
+
1245
+ if not neurons or len(neurons) == 0:
1246
+ return
1247
+
1248
+ angle_step = 2 * np.pi / len(neurons)
1249
+ for i, neuron in enumerate(neurons):
1250
+ try:
1251
+ angle = i * angle_step
1252
+ x = center[0] + radius * np.cos(angle)
1253
+ y = center[1] + radius * np.sin(angle)
1254
+
1255
+ # NaN check for coordinates
1256
+ if np.isnan(x) or np.isnan(y):
1257
+ logging.warning(f"NaN coordinates detected for neuron {i}. Skipping.")
1258
+ continue
1259
+
1260
+ # Draw neuron
1261
+ activation = float(neuron.state.activation)
1262
+ connections = int(neuron.state.connections)
1263
+
1264
+ # Check for NaNs in neuron state
1265
+ if np.isnan(activation) or np.isnan(connections):
1266
+ logging.warning(f"NaN detected in neuron state. Marking organism for death.")
1267
+ organism.alive = False
1268
+ return
1269
+
1270
+ # Ensure coordinates are valid integers
1271
+ x_pos = int(np.clip(x, 0, self.width))
1272
+ y_pos = int(np.clip(y, 0, self.height))
1273
+
1274
+ color = self._get_neuron_color(activation, connections)
1275
+ pygame.draw.circle(self.neuron_surface, color, (x_pos, y_pos), 5)
1276
+
1277
+ # Draw connections with safety checks
1278
+ if connections > 0:
1279
+ alpha = int(255 * min(connections / self.config.MAX_NEURAL_CONNECTIONS, 1))
1280
+ if not np.isnan(alpha):
1281
+ connection_color = (*self.config.CONNECTION_COLOR[:3], alpha)
1282
+ pygame.draw.line(
1283
+ self.connection_surface,
1284
+ connection_color,
1285
+ (x_pos, y_pos),
1286
+ (int(center[0]), int(center[1])),
1287
+ 2
1288
+ )
1289
+
1290
+ # Recursively draw sub-neurons
1291
+ if hasattr(neuron, 'sub_neurons') and neuron.sub_neurons:
1292
+ child_radius = radius * 0.5
1293
+ child_center = (x, y)
1294
+ plot_neural_layer(child_center, child_radius, neuron.sub_neurons, depth + 1)
1295
+ except Exception as e:
1296
+ logging.error(f"Error plotting neuron {i}: {e}")
1297
+ continue
1298
+
1299
+ # Draw neural network
1300
+ try:
1301
+ center = (organism.pos.x, organism.pos.y)
1302
+ if not (np.isnan(center[0]) or np.isnan(center[1])):
1303
+ plot_neural_layer(center, organism.size * 2,
1304
+ [organism.brain.visual_cortex,
1305
+ organism.brain.thought_processor,
1306
+ organism.brain.action_generator])
1307
+ except Exception as e:
1308
+ logging.error(f"Error plotting neural network: {e}")
1309
+
1310
+ # Apply pattern overlay with safety checks
1311
+ try:
1312
+ self._apply_pattern_overlay(organism, surface)
1313
+ except Exception as e:
1314
+ logging.error(f"Error applying pattern overlay: {e}")
1315
+
1316
+ # Combine surfaces with alpha blending
1317
+ surface.blit(self.connection_surface, (0, 0))
1318
+ surface.blit(self.neuron_surface, (0, 0))
1319
+
1320
+ except Exception as e:
1321
+ logging.error(f"Error in draw_brain_state: {e}")
1322
+
1323
+ def _get_neuron_color(self, activation: float, connections: int) -> Tuple[int, int, int]:
1324
+ """Generate color based on neuron state"""
1325
+ try:
1326
+ # Use HSV color space for smooth transitions
1327
+ hue = (activation + 1) / 2 # Map -1,1 to 0,1
1328
+ saturation = min(connections / self.config.MAX_NEURAL_CONNECTIONS, 1)
1329
+ value = 0.8 + 0.2 * activation
1330
+
1331
+ # Convert to RGB
1332
+ rgb = colorsys.hsv_to_rgb(hue, saturation, value)
1333
+ return tuple(int(255 * x) for x in rgb)
1334
+ except Exception as e:
1335
+ logging.error(f"Error in _get_neuron_color: {e}. Defaulting to gray.")
1336
+ return (100, 100, 100)
1337
+
1338
+ class SimulationVisualizer:
1339
+ def __init__(self, width: int, height: int, config: VisualizationConfig):
1340
+ pygame.init()
1341
+ self.width = width
1342
+ self.height = height
1343
+ self.config = config
1344
+
1345
+ # Enable double buffering and vsync
1346
+ self.screen = pygame.display.set_mode(
1347
+ (width, height),
1348
+ pygame.DOUBLEBUF | pygame.HWSURFACE | pygame.SCALED,
1349
+ vsync=1
1350
+ )
1351
+ pygame.display.set_caption("Fractal Life Simulation")
1352
+
1353
+ # Create off-screen surfaces for double buffering
1354
+ self.buffer = pygame.Surface((width, height), pygame.SRCALPHA)
1355
+ self.neural_viz = NeuralVisualizer(width, height, config)
1356
+ self.background = pygame.Surface((width, height))
1357
+ self.background.fill(config.BACKGROUND_COLOR)
1358
+
1359
+ # Additional surfaces for layered rendering
1360
+ self.organism_surface = pygame.Surface((width, height), pygame.SRCALPHA)
1361
+ self.interaction_surface = pygame.Surface((width, height), pygame.SRCALPHA)
1362
+ self.stats_surface = pygame.Surface((width, height), pygame.SRCALPHA)
1363
+
1364
+ def _validate_color(self, color):
1365
+ """Validate and ensure color values are proper RGB integers"""
1366
+ try:
1367
+ if len(color) >= 3:
1368
+ return (
1369
+ max(0, min(255, int(color[0]))),
1370
+ max(0, min(255, int(color[1]))),
1371
+ max(0, min(255, int(color[2])))
1372
+ )
1373
+ return (100, 100, 100) # Default fallback color
1374
+ except Exception as e:
1375
+ logging.error(f"Error validating color: {e}")
1376
+ return (100, 100, 100)
1377
+
1378
+
1379
+
1380
+ def draw_frame(self, organisms: List[FractalOrganism], stats: Dict):
1381
+ """Draw complete frame with all visualizations"""
1382
+ try:
1383
+ # Clear all off-screen surfaces
1384
+ self.organism_surface.fill((0, 0, 0, 0))
1385
+ self.interaction_surface.fill((0, 0, 0, 0))
1386
+ self.stats_surface.fill((0, 0, 0, 0))
1387
+
1388
+ # Draw organisms and their neural states
1389
+ for organism in organisms:
1390
+ if organism.alive:
1391
+ self._draw_organism(organism, self.organism_surface)
1392
+ self.neural_viz.draw_brain_state(organism, self.interaction_surface)
1393
+
1394
+ # Draw statistics
1395
+ self._draw_stats(stats, self.stats_surface)
1396
+
1397
+ # Blit off-screen surfaces to the main display surface
1398
+ self.screen.blit(self.background, (0, 0)) # Background layer
1399
+ self.screen.blit(self.organism_surface, (0, 0))
1400
+ self.screen.blit(self.interaction_surface, (0, 0))
1401
+ self.screen.blit(self.stats_surface, (0, 0))
1402
+
1403
+ # Flip display buffers to avoid flickering
1404
+ pygame.display.flip()
1405
+ except Exception as e:
1406
+ logging.error(f"Error in draw_frame: {e}")
1407
+
1408
+ def _draw_organism(self, organism: FractalOrganism, surface: pygame.Surface):
1409
+ """Draw organism body with color and pattern"""
1410
+ try:
1411
+ # Validate color before drawing
1412
+ safe_color = self._validate_color(organism.color)
1413
+
1414
+ # Draw shape with validated color
1415
+ points = []
1416
+ for x, y in organism.shape_points:
1417
+ try:
1418
+ px = organism.pos.x + x
1419
+ py = organism.pos.y + y
1420
+ if not (np.isnan(px) or np.isnan(py)):
1421
+ points.append((px, py))
1422
+ else:
1423
+ logging.warning(f"NaN detected in shape points for organism {id(organism)}. Skipping point.")
1424
+ except Exception as e:
1425
+ logging.error(f"Error processing shape points for organism {id(organism)}: {e}")
1426
+ continue
1427
+
1428
+ if len(points) >= 3:
1429
+ pygame.draw.polygon(surface, safe_color, points)
1430
+ else:
1431
+ logging.warning(f"Insufficient valid points to draw organism {id(organism)}. Skipping drawing.")
1432
+
1433
+ # Draw energy bar
1434
+ energy_percentage = min(max(organism.brain.total_energy / 1000.0, 0), 1)
1435
+ bar_width = organism.size * 2
1436
+ bar_height = 4
1437
+ bar_pos = (organism.pos.x - bar_width / 2, organism.pos.y - organism.size - 10)
1438
+ pygame.draw.rect(surface, (50, 50, 50), (*bar_pos, bar_width, bar_height))
1439
+ pygame.draw.rect(surface, (0, 255, 0), # Using direct color value for energy bar
1440
+ (*bar_pos, bar_width * energy_percentage, bar_height))
1441
+ except Exception as e:
1442
+ logging.error(f"Error in _draw_organism: {e}")
1443
+
1444
+ def _draw_stats(self, stats: Dict, surface: pygame.Surface):
1445
+ """Draw simulation statistics"""
1446
+ try:
1447
+ font = pygame.font.Font(None, 24)
1448
+ y_pos = 10
1449
+ for key, value in stats.items():
1450
+ text = font.render(f"{key.capitalize()}: {value}", True, (255, 255, 255))
1451
+ surface.blit(text, (10, y_pos))
1452
+ y_pos += 25
1453
+ except Exception as e:
1454
+ logging.error(f"Error in _draw_stats: {e}")
1455
+
1456
+ def cleanup(self):
1457
+ """Clean up pygame resources"""
1458
+ try:
1459
+ pygame.quit()
1460
+ except Exception as e:
1461
+ logging.error(f"Error during pygame cleanup: {e}")
1462
+
1463
+ # ==============================
1464
+ # Interaction Field (Optional Enhancement)
1465
+ # ==============================
1466
+
1467
+ class InteractionField:
1468
+ def __init__(self, width: int, height: int, resolution: int = 50):
1469
+ self.width = max(1, width)
1470
+ self.height = max(1, height)
1471
+ self.resolution = max(10, min(resolution, 100)) # Bound resolution
1472
+
1473
+ # Create field grid with safe dimensions
1474
+ self.grid_w = max(1, self.width // self.resolution)
1475
+ self.grid_h = max(1, self.height // self.resolution)
1476
+ self.field = np.zeros((self.grid_w, self.grid_h, 3))
1477
+
1478
+ def update(self, organisms: List[FractalOrganism]):
1479
+ """Update field based on organism neural activity with safety checks"""
1480
+ try:
1481
+ # Decay field
1482
+ self.field *= 0.9
1483
+ np.clip(self.field, 0, 1, out=self.field)
1484
+
1485
+ for org in organisms:
1486
+ if not org.alive:
1487
+ continue
1488
+
1489
+ try:
1490
+ # Safe position to grid conversion
1491
+ pos_x = max(0, min(float(org.pos.x), self.width))
1492
+ pos_y = max(0, min(float(org.pos.y), self.height))
1493
+
1494
+ grid_x = int((pos_x / self.width) * (self.grid_w - 1))
1495
+ grid_y = int((pos_y / self.height) * (self.grid_h - 1))
1496
+
1497
+ # Get neural activity with safety checks
1498
+ vitals = org.brain.get_vitals()
1499
+ activity_color = np.array([
1500
+ max(0.0, min(1.0, float(vitals['activation']))),
1501
+ max(0.0, min(1.0, float(vitals['energy']) / 1000.0)),
1502
+ max(0.0, min(1.0, float(vitals['connections']) / 100.0))
1503
+ ])
1504
+
1505
+ # Apply to field with falloff
1506
+ for dx in range(-2, 3):
1507
+ for dy in range(-2, 3):
1508
+ x = (grid_x + dx) % self.grid_w
1509
+ y = (grid_y + dy) % self.grid_h
1510
+
1511
+ distance = np.sqrt(dx * dx + dy * dy)
1512
+ if distance < 3:
1513
+ intensity = max(0.0, min(1.0, (3 - distance) / 3))
1514
+ self.field[x, y] += activity_color * intensity
1515
+
1516
+ except (ValueError, TypeError, ZeroDivisionError) as e:
1517
+ logging.error(f"Error updating field for organism {id(org)}: {e}")
1518
+ continue # Skip problematic organisms
1519
+
1520
+ # Ensure field values stay in valid range
1521
+ np.clip(self.field, 0, 1, out=self.field)
1522
+ except Exception as e:
1523
+ logging.error(f"Error in InteractionField.update: {e}")
1524
+
1525
+ def get_field_at(self, x: float, y: float) -> np.ndarray:
1526
+ """Safely get field value at position"""
1527
+ try:
1528
+ x = max(0, min(float(x), self.width))
1529
+ y = max(0, min(float(y), self.height))
1530
+
1531
+ grid_x = int((x / self.width) * (self.grid_w - 1))
1532
+ grid_y = int((y / self.height) * (self.grid_h - 1))
1533
+
1534
+ return self.field[grid_x, grid_y]
1535
+ except (ValueError, TypeError, IndexError) as e:
1536
+ logging.error(f"Error in get_field_at for position ({x}, {y}): {e}")
1537
+ return np.zeros(3)
1538
+
1539
+ # ==============================
1540
+ # Simulation State Tracking
1541
+ # ==============================
1542
+
1543
+ class SimulationState:
1544
+ """Tracks the current state of the simulation"""
1545
+ def __init__(self):
1546
+ self.running = False
1547
+ self.paused = False
1548
+ self.step_count = 0
1549
+ self.stats = {
1550
+ 'population': 0,
1551
+ 'avg_energy': 0.0,
1552
+ 'avg_neurons': 0.0,
1553
+ 'avg_connections': 0.0,
1554
+ 'total_interactions': 0
1555
+ }
1556
+ self.selected_organism: Optional[FractalOrganism] = None
1557
+
1558
+ # ==============================
1559
+ # Main Simulation Class
1560
+ # ==============================
1561
+
1562
+ class FractalLifeSimulation:
1563
+ def __init__(self, config: SimulationConfig, shared_data: Dict):
1564
+ self.config = config
1565
+ self.state = SimulationState()
1566
+
1567
+ # Shared data for Gradio interface
1568
+ self.shared_data = shared_data
1569
+ self.shared_lock = threading.Lock()
1570
+
1571
+ # Initialize systems
1572
+ visualization_config = VisualizationConfig()
1573
+ self.visualizer = SimulationVisualizer(config.WIDTH, config.HEIGHT, visualization_config)
1574
+ self.physics = PhysicsEngine(config.WIDTH, config.HEIGHT, PhysicsConfig())
1575
+ self.field = InteractionField(config.WIDTH, config.HEIGHT, resolution=50)
1576
+
1577
+ # Event queues for thread communication
1578
+ self.event_queue = Queue()
1579
+ self.stats_queue = Queue()
1580
+
1581
+ # Initialize organisms
1582
+ self.organisms: List[FractalOrganism] = []
1583
+ self._init_organisms()
1584
+
1585
+ # Pygame threading control
1586
+ self.running = False
1587
+ self.thread = None
1588
+
1589
+ def _init_organisms(self):
1590
+ """Initialize starting organisms"""
1591
+ try:
1592
+ for _ in range(self.config.MIN_ORGANISMS):
1593
+ x = random.uniform(0, self.config.WIDTH)
1594
+ y = random.uniform(0, self.config.HEIGHT)
1595
+ organism = FractalOrganism(
1596
+ x=x, y=y,
1597
+ feature_dim=32,
1598
+ max_neurons=self.config.MAX_NEURONS
1599
+ )
1600
+ self.organisms.append(organism)
1601
+ self.physics.add_organism(organism)
1602
+ except Exception as e:
1603
+ logging.error(f"Error initializing organisms: {e}")
1604
+
1605
+ def _process_reproduction(self):
1606
+ """Handle organism reproduction"""
1607
+ try:
1608
+ new_organisms = []
1609
+ for org in self.organisms:
1610
+ if not org.alive or len(self.organisms) + len(new_organisms) >= self.config.MAX_ORGANISMS:
1611
+ continue
1612
+
1613
+ if org.brain.total_energy > self.config.REPRODUCTION_ENERGY:
1614
+ # Find a mate (simple random selection for demonstration)
1615
+ potential_mates = [o for o in self.organisms if o != org and o.alive and o.brain.total_energy > self.config.REPRODUCTION_ENERGY]
1616
+ if potential_mates:
1617
+ mate = random.choice(potential_mates)
1618
+ child = org.reproduce(mate, mutation_rate=self.config.MUTATION_RATE)
1619
+ if child:
1620
+ new_organisms.append(child)
1621
+ self.physics.add_organism(child)
1622
+
1623
+ self.organisms.extend(new_organisms)
1624
+ except Exception as e:
1625
+ logging.error(f"Error processing reproduction: {e}")
1626
+
1627
+ def _update_stats(self):
1628
+ """Update simulation statistics"""
1629
+ try:
1630
+ living_organisms = [org for org in self.organisms if org.alive]
1631
+ population = len(living_organisms)
1632
+
1633
+ if population > 0:
1634
+ self.state.stats.update({
1635
+ 'population': population,
1636
+ 'avg_energy': sum(org.brain.total_energy for org in living_organisms) / population,
1637
+ 'avg_neurons': sum(org.brain.total_neurons for org in living_organisms) / population,
1638
+ 'avg_connections': sum(sum(n.state.connections for n in [org.brain.visual_cortex,
1639
+ org.brain.thought_processor,
1640
+ org.brain.action_generator])
1641
+ for org in living_organisms) / population,
1642
+ 'total_interactions': len(self.physics.current_interactions)
1643
+ })
1644
+ else:
1645
+ self.state.stats.update({
1646
+ 'population': 0,
1647
+ 'avg_energy': 0.0,
1648
+ 'avg_neurons': 0.0,
1649
+ 'avg_connections': 0.0,
1650
+ 'total_interactions': 0
1651
+ })
1652
+
1653
+ with self.shared_lock:
1654
+ self.shared_data['stats'] = self.state.stats.copy()
1655
+ except Exception as e:
1656
+ logging.error(f"Error updating statistics: {e}")
1657
+
1658
+ def _main_loop(self):
1659
+ """Main simulation loop"""
1660
+ try:
1661
+ clock = pygame.time.Clock()
1662
+
1663
+ while self.running:
1664
+ dt = clock.tick(self.config.TARGET_FPS) / 1000.0 # Delta time in seconds
1665
+
1666
+ for event in pygame.event.get():
1667
+ if event.type == pygame.QUIT:
1668
+ self.stop()
1669
+
1670
+ # Process external events
1671
+ while not self.event_queue.empty():
1672
+ event = self.event_queue.get()
1673
+ self._handle_event(event)
1674
+
1675
+ if not self.state.paused:
1676
+ # Update physics
1677
+ self.physics.update(dt)
1678
+
1679
+ # Update neural field
1680
+ self.field.update(self.organisms)
1681
+
1682
+ # Update organisms
1683
+ for org in self.organisms:
1684
+ if org.alive:
1685
+ # Update organism state
1686
+ org.update(self.config.WIDTH, self.config.HEIGHT, self.organisms)
1687
+
1688
+ # Process field interactions (placeholder for actual implementation)
1689
+ field_value = self.field.get_field_at(org.pos.x, org.pos.y)
1690
+ # org.process_field_input(field_value) # Implement if needed
1691
+
1692
+ # Energy decay
1693
+ org.brain.total_energy = max(0.0, org.brain.total_energy - self.config.ENERGY_DECAY)
1694
+
1695
+ # Process physical interactions
1696
+ self.physics.process_interactions(self.organisms)
1697
+
1698
+ # Handle reproduction
1699
+ self._process_reproduction()
1700
+
1701
+ # Remove dead organisms
1702
+ self.organisms = [org for org in self.organisms if org.alive]
1703
+
1704
+ # Maintain minimum population
1705
+ while len(self.organisms) < self.config.MIN_ORGANISMS and len(self.organisms) < self.config.MAX_ORGANISMS:
1706
+ x = random.uniform(0, self.config.WIDTH)
1707
+ y = random.uniform(0, self.config.HEIGHT)
1708
+ organism = FractalOrganism(
1709
+ x=x, y=y,
1710
+ feature_dim=32,
1711
+ max_neurons=self.config.MAX_NEURONS
1712
+ )
1713
+ self.organisms.append(organism)
1714
+ self.physics.add_organism(organism)
1715
+
1716
+ # Update statistics
1717
+ self._update_stats()
1718
+
1719
+ # Draw frame
1720
+ self.visualizer.draw_frame(self.organisms, self.state.stats)
1721
+ except Exception as e:
1722
+ logging.error(f"Exception in main loop: {e}")
1723
+ finally:
1724
+ # Cleanup when simulation stops
1725
+ self.visualizer.cleanup()
1726
+
1727
+ def start(self):
1728
+ """Start simulation in separate thread"""
1729
+ if not self.running:
1730
+ self.running = True
1731
+ self.thread = threading.Thread(target=self._main_loop)
1732
+ self.thread.start()
1733
+ logging.info("Simulation started.")
1734
+
1735
+ def stop(self):
1736
+ """Stop simulation"""
1737
+ self.running = False
1738
+ if self.thread and self.thread.is_alive():
1739
+ self.thread.join()
1740
+ logging.info("Simulation stopped.")
1741
+
1742
+ def pause(self):
1743
+ """Pause/unpause simulation"""
1744
+ self.state.paused = not self.state.paused
1745
+ logging.info(f"Simulation {'paused' if self.state.paused else 'resumed'}.")
1746
+
1747
+ def _handle_event(self, event: Dict):
1748
+ """Handle external events"""
1749
+ try:
1750
+ if event['type'] == 'select_organism':
1751
+ organism_id = event['organism_id']
1752
+ self.state.selected_organism = next(
1753
+ (org for org in self.organisms if id(org) == organism_id),
1754
+ None
1755
+ )
1756
+ logging.info(f"Organism {organism_id} selected.")
1757
+ elif event['type'] == 'add_energy':
1758
+ if self.state.selected_organism:
1759
+ self.state.selected_organism.brain.total_energy += event['amount']
1760
+ logging.info(f"Added {event['amount']} energy to organism {id(self.state.selected_organism)}.")
1761
+ elif event['type'] == 'modify_neurons':
1762
+ if self.state.selected_organism:
1763
+ # Placeholder for neuron modification logic
1764
+ logging.info(f"Modify neurons event received for organism {id(self.state.selected_organism)}.")
1765
+ except Exception as e:
1766
+ logging.error(f"Error handling event {event}: {e}")
1767
+
1768
+ # ==============================
1769
+ # Gradio Interface
1770
+ # ==============================
1771
+
1772
+ class FractalLifeInterface:
1773
+ def __init__(self):
1774
+ self.simulation: Optional[FractalLifeSimulation] = None
1775
+ self.history = {
1776
+ 'population': [],
1777
+ 'avg_energy': [],
1778
+ 'avg_neurons': [],
1779
+ 'time': []
1780
+ }
1781
+ self.selected_organism_id = None
1782
+ self.frame_image = None
1783
+ self.DEFAULT_WIDTH = 1024
1784
+ self.DEFAULT_HEIGHT = 768
1785
+
1786
+ self.shared_data = {
1787
+ 'stats': {
1788
+ 'population': 0,
1789
+ 'avg_energy': 0.0,
1790
+ 'avg_neurons': 0.0,
1791
+ 'avg_connections': 0.0,
1792
+ 'total_interactions': 0
1793
+ }
1794
+ }
1795
+
1796
+ def get_frame(self):
1797
+ """Retrieve the latest frame from Pygame."""
1798
+ try:
1799
+ if self.simulation and self.simulation.running:
1800
+ with self.simulation.shared_lock:
1801
+ # Get pygame surface
1802
+ screen = self.simulation.visualizer.screen
1803
+
1804
+ # Get the size of the screen
1805
+ width = screen.get_width()
1806
+ height = screen.get_height()
1807
+
1808
+ # Create a new surface with alpha channel
1809
+ surf_alpha = pygame.Surface((width, height), pygame.SRCALPHA)
1810
+ surf_alpha.blit(screen, (0, 0))
1811
+
1812
+ # Convert Pygame surface to PIL Image
1813
+ data_string = pygame.image.tostring(surf_alpha, 'RGBA')
1814
+ image = Image.frombytes('RGBA', (width, height), data_string)
1815
+
1816
+ # Convert to RGB
1817
+ image = image.convert('RGB')
1818
+
1819
+ return image
1820
+ else:
1821
+ # Return a blank image
1822
+ return Image.new('RGB', (self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT), (0, 0, 0))
1823
+
1824
+ except Exception as e:
1825
+ logging.error(f"Error in get_frame: {e}")
1826
+ # Return a fallback image in case of error
1827
+ return Image.new('RGB', (self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT), (0, 0, 0))
1828
+
1829
+ def update_display(self):
1830
+ """Update display by retrieving the latest frame and statistics."""
1831
+ try:
1832
+ # Get the frame as PIL Image
1833
+ image = self.get_frame()
1834
+
1835
+ # Update statistics only if simulation is running
1836
+ if self.simulation and self.simulation.running:
1837
+ with self.simulation.shared_lock:
1838
+ stats = self.shared_data['stats']
1839
+ # Update history
1840
+ self.history['time'].append(self.simulation.state.step_count)
1841
+ self.history['population'].append(stats['population'])
1842
+ self.history['avg_energy'].append(stats['avg_energy'])
1843
+ self.history['avg_neurons'].append(stats['avg_neurons'])
1844
+
1845
+ # Limit history length to prevent memory issues
1846
+ max_history = 1000
1847
+ if len(self.history['time']) > max_history:
1848
+ for key in self.history:
1849
+ self.history[key] = self.history[key][-max_history:]
1850
+ else:
1851
+ stats = self.shared_data['stats']
1852
+
1853
+ # Update plots
1854
+ stats_fig = self._create_stats_plot()
1855
+ neural_fig = self._create_neural_plot()
1856
+
1857
+ # Update organism list only if simulation is running
1858
+ organism_list = []
1859
+ if self.simulation and self.simulation.running:
1860
+ organism_list = [
1861
+ (f"Organism {id(org)}", id(org))
1862
+ for org in self.simulation.organisms
1863
+ if org.alive
1864
+ ]
1865
+
1866
+ # Update selected organism vitals
1867
+ vitals = None
1868
+ if self.selected_organism_id and self.simulation and self.simulation.running:
1869
+ org = next(
1870
+ (org for org in self.simulation.organisms
1871
+ if id(org) == self.selected_organism_id),
1872
+ None
1873
+ )
1874
+ if org:
1875
+ vitals = {
1876
+ 'energy': org.brain.total_energy,
1877
+ 'neurons': org.brain.total_neurons,
1878
+ 'age': org.age,
1879
+ 'connections': sum(n.state.connections for n in
1880
+ [org.brain.visual_cortex,
1881
+ org.brain.thought_processor,
1882
+ org.brain.action_generator])
1883
+ }
1884
+
1885
+ # Create new Dropdown choices instead of using update
1886
+ dropdown = gr.Dropdown(
1887
+ choices=organism_list,
1888
+ label="Select Organism",
1889
+ interactive=True
1890
+ )
1891
+
1892
+ return [
1893
+ image, # Return PIL Image directly
1894
+ stats_fig,
1895
+ neural_fig,
1896
+ dropdown,
1897
+ vitals
1898
+ ]
1899
+
1900
+ except Exception as e:
1901
+ logging.error(f"Error in update_display: {e}")
1902
+ # Return default values in case of error
1903
+ blank_image = Image.new('RGB', (self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT), (0, 0, 0))
1904
+ empty_fig = go.Figure()
1905
+ empty_dropdown = gr.Dropdown(choices=[])
1906
+ return [blank_image, empty_fig, empty_fig, empty_dropdown, None]
1907
+
1908
+ def create_interface(self):
1909
+ with gr.Blocks(title="Fractal Life Simulator") as interface:
1910
+ gr.Markdown("# 🧬 Fractal Life Simulator")
1911
+
1912
+ with gr.Row():
1913
+ # Main simulation view and controls
1914
+ with gr.Column(scale=2):
1915
+ canvas = gr.Image(label="Simulation View", interactive=False)
1916
+
1917
+ with gr.Row():
1918
+ start_btn = gr.Button("Start Simulation", variant="primary")
1919
+ pause_btn = gr.Button("Pause")
1920
+ stop_btn = gr.Button("Stop")
1921
+
1922
+ with gr.Row():
1923
+ population_slider = gr.Slider(
1924
+ minimum=5, maximum=100, # Increased maximum for flexibility
1925
+ value=10, step=1,
1926
+ label="Initial Population"
1927
+ )
1928
+ mutation_rate = gr.Slider(
1929
+ minimum=0, maximum=1, value=0.1, step=0.01,
1930
+ label="Mutation Rate"
1931
+ )
1932
+ max_population_slider = gr.Slider(
1933
+ minimum=50, maximum=500, value=50, step=10,
1934
+ label="Max Population"
1935
+ )
1936
+
1937
+ # Statistics and organism details
1938
+ with gr.Column(scale=1):
1939
+ stats_plot = gr.Plot(label="Population Statistics")
1940
+ neural_plot = gr.Plot(label="Neural Activity")
1941
+
1942
+ with gr.Group():
1943
+ gr.Markdown("### Selected Organism")
1944
+ organism_dropdown = gr.Dropdown(
1945
+ label="Select Organism",
1946
+ choices=[],
1947
+ interactive=True
1948
+ )
1949
+ vitals_json = gr.JSON(label="Organism Vitals")
1950
+
1951
+ with gr.Row():
1952
+ add_energy_btn = gr.Button("Add Energy")
1953
+ add_neurons_btn = gr.Button("Add Neurons")
1954
+
1955
+ # Advanced settings tab
1956
+ with gr.Tab("Advanced Settings"):
1957
+ with gr.Row():
1958
+ with gr.Column():
1959
+ brain_update_rate = gr.Slider(
1960
+ minimum=1, maximum=30, value=10, step=1,
1961
+ label="Brain Update Rate (Hz)"
1962
+ )
1963
+ max_neurons = gr.Slider(
1964
+ minimum=100, maximum=5000, value=1000, step=100,
1965
+ label="Max Neurons per Brain"
1966
+ )
1967
+ energy_decay = gr.Slider(
1968
+ minimum=0, maximum=1, value=0.1, step=0.01,
1969
+ label="Energy Decay Rate"
1970
+ )
1971
+
1972
+ with gr.Column():
1973
+ interaction_strength = gr.Slider(
1974
+ minimum=0, maximum=1, value=0.5, step=0.01,
1975
+ label="Interaction Strength"
1976
+ )
1977
+ field_resolution = gr.Slider(
1978
+ minimum=10, maximum=100, value=50, step=5,
1979
+ label="Field Resolution"
1980
+ )
1981
+
1982
+ # Event handlers
1983
+ def start_simulation(initial_population, mutation_rate_val, max_population_val,
1984
+ brain_update_rate_val, max_neurons_val, energy_decay_val,
1985
+ interaction_strength_val, field_resolution_val):
1986
+ try:
1987
+ if self.simulation is None:
1988
+ config = SimulationConfig()
1989
+ config.MIN_ORGANISMS = int(initial_population)
1990
+ config.MUTATION_RATE = mutation_rate_val
1991
+ config.MAX_ORGANISMS = int(max_population_val)
1992
+ config.BRAIN_UPDATE_RATE = int(brain_update_rate_val)
1993
+ config.MAX_NEURONS = int(max_neurons_val)
1994
+ config.ENERGY_DECAY = energy_decay_val
1995
+
1996
+ self.simulation = FractalLifeSimulation(config, self.shared_data)
1997
+ self.simulation.start()
1998
+ logging.info("Simulation started via interface.")
1999
+ return "Simulation started"
2000
+ else:
2001
+ logging.warning("Simulation is already running.")
2002
+ return "Simulation is already running."
2003
+ except Exception as e:
2004
+ logging.error(f"Error starting simulation: {e}")
2005
+ return "Failed to start simulation."
2006
+
2007
+ def pause_simulation():
2008
+ try:
2009
+ if self.simulation:
2010
+ self.simulation.pause()
2011
+ status = "paused" if self.simulation.state.paused else "resumed"
2012
+ logging.info(f"Simulation {status} via interface.")
2013
+ return f"Simulation {status}"
2014
+ logging.warning("No simulation running to pause/resume.")
2015
+ return "No simulation running"
2016
+ except Exception as e:
2017
+ logging.error(f"Error pausing simulation: {e}")
2018
+ return "Failed to pause simulation."
2019
+
2020
+ def stop_simulation():
2021
+ try:
2022
+ if self.simulation:
2023
+ self.simulation.stop()
2024
+ self.simulation = None
2025
+ logging.info("Simulation stopped via interface.")
2026
+ return "Simulation stopped"
2027
+ logging.warning("No simulation running to stop.")
2028
+ return "No simulation running"
2029
+ except Exception as e:
2030
+ logging.error(f"Error stopping simulation: {e}")
2031
+ return "Failed to stop simulation."
2032
+
2033
+ def select_organism(organism_id):
2034
+ try:
2035
+ self.selected_organism_id = organism_id
2036
+ if self.simulation:
2037
+ self.simulation.event_queue.put({
2038
+ 'type': 'select_organism',
2039
+ 'organism_id': organism_id
2040
+ })
2041
+ logging.info(f"Organism {organism_id} selected via interface.")
2042
+ except Exception as e:
2043
+ logging.error(f"Error selecting organism: {e}")
2044
+
2045
+ def add_energy_to_organism():
2046
+ try:
2047
+ if self.simulation and self.selected_organism_id:
2048
+ self.simulation.event_queue.put({
2049
+ 'type': 'add_energy',
2050
+ 'amount': 50.0
2051
+ })
2052
+ logging.info(f"Added energy to organism {self.selected_organism_id} via interface.")
2053
+ return "Added energy to selected organism"
2054
+ logging.warning("No organism selected or simulation not running to add energy.")
2055
+ return "No organism selected or simulation not running"
2056
+ except Exception as e:
2057
+ logging.error(f"Error adding energy to organism: {e}")
2058
+ return "Failed to add energy"
2059
+
2060
+ def add_neurons_to_organism():
2061
+ try:
2062
+ if self.simulation and self.selected_organism_id:
2063
+ self.simulation.event_queue.put({
2064
+ 'type': 'modify_neurons',
2065
+ 'amount': 10
2066
+ })
2067
+ logging.info(f"Added neurons to organism {self.selected_organism_id} via interface.")
2068
+ return "Added neurons to selected organism"
2069
+ logging.warning("No organism selected or simulation not running to add neurons.")
2070
+ return "No organism selected or simulation not running"
2071
+ except Exception as e:
2072
+ logging.error(f"Error adding neurons to organism: {e}")
2073
+ return "Failed to add neurons"
2074
+
2075
+ # Create a bound method for update_display
2076
+ def bound_update_display():
2077
+ return self.update_display()
2078
+
2079
+ # Connect events
2080
+ start_btn.click(
2081
+ start_simulation,
2082
+ inputs=[population_slider, mutation_rate, max_population_slider,
2083
+ brain_update_rate, max_neurons, energy_decay,
2084
+ interaction_strength, field_resolution],
2085
+ outputs=gr.Textbox()
2086
+ )
2087
+ pause_btn.click(pause_simulation, outputs=gr.Textbox())
2088
+ stop_btn.click(stop_simulation, outputs=gr.Textbox())
2089
+
2090
+ organism_dropdown.change(select_organism, inputs=[organism_dropdown], outputs=None)
2091
+ add_energy_btn.click(add_energy_to_organism, outputs=gr.Textbox())
2092
+ add_neurons_btn.click(add_neurons_to_organism, outputs=gr.Textbox())
2093
+
2094
+ # Periodic update using Gradio's update mechanism
2095
+ interface.load(
2096
+ fn=bound_update_display,
2097
+ inputs=[],
2098
+ outputs=[canvas, stats_plot, neural_plot, organism_dropdown, vitals_json],
2099
+ every=1/30, # 30 FPS updates
2100
+ queue=True
2101
+ )
2102
+
2103
+ return interface
2104
+
2105
+ def _create_stats_plot(self):
2106
+ """Create statistics plot using plotly"""
2107
+ try:
2108
+ fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
2109
+ subplot_titles=("Population", "Average Energy", "Average Neurons"))
2110
+
2111
+ fig.add_trace(
2112
+ go.Scatter(x=self.history['time'], y=self.history['population'],
2113
+ name="Population"),
2114
+ row=1, col=1
2115
+ )
2116
+
2117
+ fig.add_trace(
2118
+ go.Scatter(x=self.history['time'], y=self.history['avg_energy'],
2119
+ name="Avg Energy"),
2120
+ row=2, col=1
2121
+ )
2122
+
2123
+ fig.add_trace(
2124
+ go.Scatter(x=self.history['time'], y=self.history['avg_neurons'],
2125
+ name="Avg Neurons"),
2126
+ row=3, col=1
2127
+ )
2128
+
2129
+ fig.update_layout(height=600, showlegend=True)
2130
+ return fig
2131
+ except Exception as e:
2132
+ logging.error(f"Error creating stats plot: {e}")
2133
+ return go.Figure()
2134
+
2135
+ def _create_neural_plot(self):
2136
+ """Create neural activity plot"""
2137
+ try:
2138
+ if self.selected_organism_id and self.simulation and self.simulation.running:
2139
+ org = next(
2140
+ (org for org in self.simulation.organisms
2141
+ if id(org) == self.selected_organism_id),
2142
+ None
2143
+ )
2144
+ if org:
2145
+ # Create neural activity heatmap
2146
+ neurons = [org.brain.visual_cortex,
2147
+ org.brain.thought_processor,
2148
+ org.brain.action_generator]
2149
+
2150
+ activities = []
2151
+ for neuron in neurons:
2152
+ layer_activities = [child.state.activation for child in neuron.sub_neurons] if hasattr(neuron, 'sub_neurons') and neuron.sub_neurons else [neuron.state.activation]
2153
+ activities.append(layer_activities)
2154
+
2155
+ activities = np.array(activities)
2156
+
2157
+ fig = go.Figure(data=go.Heatmap(z=activities, colorscale='Viridis'))
2158
+ fig.update_layout(
2159
+ title="Neural Activity",
2160
+ xaxis_title="Neuron Index",
2161
+ yaxis_title="Layer"
2162
+ )
2163
+ return fig
2164
+ return go.Figure()
2165
+ except Exception as e:
2166
+ logging.error(f"Error creating neural plot: {e}")
2167
+ return go.Figure()
2168
+
2169
+ # ==============================
2170
+ # Entry Point
2171
+ # ==============================
2172
+
2173
+ if __name__ == "__main__":
2174
+ interface = FractalLifeInterface()
2175
+ gr_interface = interface.create_interface()
2176
+ # Enable queue and allow for public access
2177
+ gr_interface.queue().launch(
2178
+ server_name="0.0.0.0",
2179
+ server_port=7860,
2180
+ share=True # This creates a public link
2181
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch>=2.0.0
2
+ pygame>=2.4.0
3
+ gradio==3.50.2
4
+ numpy>=1.21.0
5
+ pymunk>=6.4.0
6
+ plotly>=5.13.0
7
+ Pillow>=9.4.0