import random import numpy as np import streamlit as st import plotly.graph_objects as go from plotly.subplots import make_subplots import time from collections import deque # Add this line import threading class Organelle: def __init__(self, type): self.type = type class Cell: def __init__(self, x, y, cell_type="prokaryote"): self.x = x self.y = y self.energy = 100 self.cell_type = cell_type self.organelles = set() self.size = 1 self.color = "lightblue" self.division_threshold = 150 self.update_properties() def update_properties(self): if self.cell_type == "early_eukaryote": self.organelles.add("nucleus") self.color = "green" self.size = 2 elif self.cell_type == "advanced_eukaryote": self.organelles.update(["nucleus", "mitochondria"]) self.color = "red" self.size = 3 elif self.cell_type == "plant_like": self.organelles.update(["nucleus", "mitochondria", "chloroplast"]) self.color = "darkgreen" self.size = 4 elif self.cell_type == "complete": self.organelles.update(["nucleus", "mitochondria", "chloroplast", "endoplasmic_reticulum", "golgi_apparatus"]) self.color = "purple" self.size = 5 def move(self, environment): dx, dy = random.uniform(-1, 1), random.uniform(-1, 1) self.x = max(0, min(environment.width - 1, self.x + dx)) self.y = max(0, min(environment.height - 1, self.y + dy)) self.energy -= 0.5 * self.size def feed(self, environment): base_energy = environment.grid[int(self.y)][int(self.x)] * 0.1 if "chloroplast" in self.organelles: base_energy += environment.light_level * 2 self.energy += base_energy environment.grid[int(self.y)][int(self.x)] *= 0.9 def can_divide(self): return self.energy > self.division_threshold def divide(self): if self.can_divide(): self.energy /= 2 new_cell = Cell(self.x, self.y, self.cell_type) new_cell.organelles = self.organelles.copy() return new_cell return None def can_merge(self, other): return (self.cell_type == other.cell_type and random.random() < 0.01) # 1% chance of merging def merge(self, other): new_cell_type = self.cell_type if self.cell_type == "prokaryote": new_cell_type = "early_eukaryote" elif self.cell_type == "early_eukaryote": new_cell_type = "advanced_eukaryote" elif self.cell_type == "advanced_eukaryote": new_cell_type = "plant_like" elif self.cell_type == "plant_like": new_cell_type = "complete" new_cell = Cell((self.x + other.x) / 2, (self.y + other.y) / 2, new_cell_type) new_cell.energy = self.energy + other.energy new_cell.organelles = self.organelles.union(other.organelles) new_cell.update_properties() return new_cell class Environment: def __init__(self, width, height, effects): self.width = width self.height = height self.grid = np.random.rand(height, width) * 10 self.light_level = 5 self.cells = [] self.time = 0 self.population_history = { "prokaryote": [], "early_eukaryote": [], "advanced_eukaryote": [], "plant_like": [], "complete": [] } self.effects = effects def add_cell(self, cell): self.cells.append(cell) def update(self): self.time += 1 self.grid += np.random.rand(self.height, self.width) * 0.1 self.light_level = 5 + np.sin(self.time / 100) * 2 new_cells = [] cells_to_remove = [] for cell in self.cells: cell.move(self) cell.feed(self) if cell.energy <= 0: cells_to_remove.append(cell) elif cell.can_divide(): new_cell = cell.divide() if new_cell: new_cells.append(new_cell) # Handle cell merging for i, cell1 in enumerate(self.cells): for cell2 in self.cells[i+1:]: if cell1.can_merge(cell2): new_cell = cell1.merge(cell2) new_cells.append(new_cell) cells_to_remove.extend([cell1, cell2]) # Apply effects if self.effects['radiation']: self.apply_radiation() if self.effects['predation']: self.apply_predation() if self.effects['symbiosis']: self.apply_symbiosis() # Add new cells and remove dead/merged cells self.cells.extend(new_cells) self.cells = [cell for cell in self.cells if cell not in cells_to_remove] # Record population counts for cell_type in self.population_history.keys(): count = len([cell for cell in self.cells if cell.cell_type == cell_type]) self.population_history[cell_type].append(count) def apply_radiation(self): for cell in self.cells: if random.random() < 0.01: # 1% chance of mutation cell.energy *= 0.8 if random.random() < 0.5: cell.organelles.add(random.choice(["nucleus", "mitochondria", "chloroplast", "endoplasmic_reticulum", "golgi_apparatus"])) else: if cell.organelles: cell.organelles.remove(random.choice(list(cell.organelles))) cell.update_properties() def apply_predation(self): for i, predator in enumerate(self.cells): if predator.cell_type in ["advanced_eukaryote", "plant_like", "complete"]: for prey in self.cells[i+1:]: if prey.cell_type in ["prokaryote", "early_eukaryote"] and random.random() < 0.05: predator.energy += prey.energy * 0.5 self.cells.remove(prey) def apply_symbiosis(self): for i, cell1 in enumerate(self.cells): for cell2 in self.cells[i+1:]: if cell1.cell_type != cell2.cell_type and random.random() < 0.01: shared_energy = (cell1.energy + cell2.energy) * 0.1 cell1.energy += shared_energy cell2.energy += shared_energy def get_visualization_data(self): cell_data = {cell_type: {"x": [], "y": [], "size": []} for cell_type in self.population_history.keys()} colors = {"prokaryote": "lightblue", "early_eukaryote": "green", "advanced_eukaryote": "red", "plant_like": "darkgreen", "complete": "purple"} for cell in self.cells: cell_data[cell.cell_type]["x"].append(cell.x) cell_data[cell.cell_type]["y"].append(cell.y) cell_data[cell.cell_type]["size"].append(cell.size * 3) return cell_data, self.population_history, colors def setup_figure(env): cell_types = list(env.population_history.keys()) fig = make_subplots(rows=2, cols=2, subplot_titles=("Cell Distribution", "Total Population", "Population by Cell Type", "Organelle Distribution"), vertical_spacing=0.1, horizontal_spacing=0.05) cell_data, population_history, colors = env.get_visualization_data() # Cell distribution for cell_type, data in cell_data.items(): fig.add_trace(go.Scatter( x=data["x"], y=data["y"], mode='markers', marker=dict(color=colors[cell_type], size=data["size"]), name=cell_type ), row=1, col=1) # Total population over time total_population = [sum(counts) for counts in zip(*population_history.values())] fig.add_trace(go.Scatter(y=total_population, mode='lines', name="Total"), row=1, col=2) # Population by cell type for cell_type, counts in population_history.items(): fig.add_trace(go.Scatter(y=counts, mode='lines', name=cell_type, line=dict(color=colors[cell_type])), row=2, col=1) # Organelle distribution organelle_counts = {"nucleus": 0, "mitochondria": 0, "chloroplast": 0, "endoplasmic_reticulum": 0, "golgi_apparatus": 0} for cell in env.cells: for organelle in cell.organelles: organelle_counts[organelle] += 1 fig.add_trace(go.Bar(x=list(organelle_counts.keys()), y=list(organelle_counts.values()), name="Organelles"), row=2, col=2) fig.update_xaxes(title_text="X", row=1, col=1) fig.update_yaxes(title_text="Y", row=1, col=1) fig.update_xaxes(title_text="Time", row=1, col=2) fig.update_yaxes(title_text="Population", row=1, col=2) fig.update_xaxes(title_text="Time", row=2, col=1) fig.update_yaxes(title_text="Population", row=2, col=1) fig.update_xaxes(title_text="Organelle", row=2, col=2) fig.update_yaxes(title_text="Count", row=2, col=2) fig.update_layout(height=800, width=1200, title_text="Advanced Cell Evolution Simulation") return fig def format_number(num): if num >= 1_000_000: return f"{num/1_000_000:.1f}M" elif num >= 1_000: return f"{num/1_000:.1f}K" else: return str(num) def update_chart(): global fig # Assuming fig is a global variable # Clear existing traces fig.data = [] # Cell positions cell_types = [cell.cell_type for cell in st.session_state.env.cells] x_positions = [cell.x for cell in st.session_state.env.cells] y_positions = [cell.y for cell in st.session_state.env.cells] fig.add_trace(go.Scatter(x=x_positions, y=y_positions, mode='markers', marker=dict(color=[colors[ct] for ct in cell_types]), text=cell_types, hoverinfo='text'), row=1, col=1) # Population history for cell_type, counts in st.session_state.env.population_history.items(): fig.add_trace(go.Scatter(y=counts, mode='lines', name=cell_type, line=dict(color=colors[cell_type])), row=1, col=2) # Population by cell type for cell_type, counts in st.session_state.env.population_history.items(): fig.add_trace(go.Scatter(y=counts, mode='lines', name=cell_type, line=dict(color=colors[cell_type])), row=2, col=1) # Organelle distribution organelle_counts = {"nucleus": 0, "mitochondria": 0, "chloroplast": 0, "endoplasmic_reticulum": 0, "golgi_apparatus": 0} for cell in st.session_state.env.cells: for organelle in cell.organelles: organelle_counts[organelle] += 1 fig.add_trace(go.Bar(x=list(organelle_counts.keys()), y=list(organelle_counts.values()), name="Organelles"), row=2, col=2) # Update axis labels and layout fig.update_xaxes(title_text="X", row=1, col=1) fig.update_yaxes(title_text="Y", row=1, col=1) fig.update_xaxes(title_text="Time", row=1, col=2) fig.update_yaxes(title_text="Population", row=1, col=2) fig.update_xaxes(title_text="Time", row=2, col=1) fig.update_yaxes(title_text="Population", row=2, col=1) fig.update_xaxes(title_text="Organelle", row=2, col=2) fig.update_yaxes(title_text="Count", row=2, col=2) fig.update_layout(height=800, width=1200, title_text="Advanced Cell Evolution Simulation") # Update the chart placeholder chart_placeholder.plotly_chart(fig) # Sidebar for controls and live statistics st.sidebar.header("Simulation Controls") initial_cells = st.sidebar.slider("Initial number of cells", 10, 500, 200) update_interval = st.sidebar.slider("Update interval (seconds)", 0.01, 1.0, 0.05) st.sidebar.header("Environmental Effects") radiation = st.sidebar.checkbox("Radiation") predation = st.sidebar.checkbox("Predation") symbiosis = st.sidebar.checkbox("Symbiosis") effects = { "radiation": radiation, "predation": predation, "symbiosis": symbiosis } # Live statistics placeholders st.sidebar.header("Live Statistics") total_cells_text = st.sidebar.empty() cell_type_breakdown = st.sidebar.empty() dominant_type_text = st.sidebar.empty() avg_energy_text = st.sidebar.empty() total_merges_text = st.sidebar.empty() # Event log st.sidebar.header("Event Log") event_log = deque(maxlen=10) # Keep the last 10 events event_log_text = st.sidebar.empty() # Create placeholders for the chart chart_placeholder = st.empty() if 'running' not in st.session_state: st.session_state.running = False if 'total_merges' not in st.session_state: st.session_state.total_merges = 0 if 'env' not in st.session_state: st.session_state.env = None if 'fig' not in st.session_state: st.session_state.fig = None def start_simulation(): st.session_state.running = True if st.session_state.env is None: st.session_state.env = Environment(100, 100, effects) for _ in range(initial_cells): cell = Cell(random.uniform(0, st.session_state.env.width), random.uniform(0, st.session_state.env.height)) st.session_state.env.add_cell(cell) st.session_state.fig = setup_figure(st.session_state.env) def stop_simulation(): st.session_state.running = False # Create two columns for start and stop buttons col1, col2 = st.columns(2) with col1: start_button = st.button("Start Simulation", on_click=start_simulation) with col2: stop_button = st.button("Stop Simulation", on_click=stop_simulation) # Main simulation loop simulation_container = st.empty() if st.session_state.running and st.session_state.env is not None: with simulation_container.container(): for _ in range(4): # Update 4 times per frame to increase simulation speed initial_cell_count = len(st.session_state.env.cells) st.session_state.env.update() final_cell_count = len(st.session_state.env.cells) # Check for merges if final_cell_count < initial_cell_count: merges = initial_cell_count - final_cell_count st.session_state.total_merges += merges event_log.appendleft(f"Time {st.session_state.env.time}: {merges} cell merges occurred") update_chart() update_statistics() time.sleep(update_interval) st.experimental_rerun() if not st.session_state.running: st.write("Simulation stopped. Click 'Start Simulation' to run again.")