Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions jes.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,45 @@
from jes_sim import Sim
from jes_ui import UI
import logging
import sys

def setup_logging(level=logging.INFO, log_file='app.log'):
# Create a formatter that includes timestamp, level, and module
formatter = logging.Formatter('%(asctime)s - [%(filename)s:%(lineno)d] - [%(name)s:%(funcName)s] - %(levelname)s - %(message)s')

# Configure console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)

# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
root_logger.addHandler(console_handler)

# Optional: Add file handler for persistent logs
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)

return root_logger

c_input = input("How many creatures do you want?\n100: Lightweight\n250: Standard (if you don't type anything, I'll go with this)\n500: Strenuous (this is what my carykh video used)\n")
if c_input == "":
c_input = "250"

# Simulation
# population size is 250 here, because that runs faster. You can increase it to 500 to replicate what was in my video, but do that at your own risk!

sim = Sim(_c_count=int(c_input), _stabilization_time=200, _trial_time=300,
logger = setup_logging()
sim = Sim(_c_count=int(c_input), _stabilization_time=200, _trial_time=600,
_beat_time=20, _beat_fade_time=5, _c_dim=[4,4],
_beats_per_cycle=3, _node_coor_count=4, # x_position, y_position, x_velocity, y_velocity
_y_clips=[-10000000,0], _ground_friction_coef=25,
_gravity_acceleration_coef=0.002, _calming_friction_coef=0.7,
_typical_friction_coef=0.8, _muscle_coef=0.08,
_traits_per_box=3, # desired width, desired height, rigidity
_traits_extra=1, # heartbeat (time)
_mutation_rate=0.07, _big_mutation_rate=0.025,
_UNITS_PER_METER=0.05)
mutation_size=0.05, big_mutation_size=0.1, mutation_rate=0.05, big_mutation_rate=0.1,
_UNITS_PER_METER=0.05, logger=logger)

# Cosmetic UI variables
ui = UI(_W_W=1920, _W_H=1078, _MOVIE_SINGLE_DIM=(650,650),
Expand Down
61 changes: 55 additions & 6 deletions jes_creature.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import numpy as np
import math
from jes_species_info import SpeciesInfo
# from jes_sim import Sim
import random
import logging

class Creature:
def __init__(self,d,pIDNumber,parent_species,_sim,_ui):
def __init__(self,d,pIDNumber,parent_species,_sim,_ui, max_offspring=4, logger=None, mutate_rate=0.05):
self.logger = logger or logging.getLogger(__name__)
self.dna = d
self.calmState = None
self.icons = [None]*2
Expand All @@ -20,6 +23,11 @@ def __init__(self,d,pIDNumber,parent_species,_sim,_ui):
self.sim = _sim
self.ui = _ui
self.codonWithChange = None
self.max_offspring = max_offspring
self.generation_offspring = 0

def __str__(self):
return f"ID: {self.IDNumber:<5} S: {str(self.sim.species_info[self.species]):<10} OG: {self.generation_offspring:<4}"

def getSpecies(self, parent_species):
if parent_species == -1:
Expand Down Expand Up @@ -95,16 +103,34 @@ def drawIcon(self, ICON_DIM, BG_COLOR, BEAT_FADE_TIME):

def saveCalmState(self, arr):
self.calmState = arr

def check_if_new_species(self, other_dna):
sp_info = self.sim.species_info[self.species]
species_rep_dna = self.sim.getCreatureWithID(sp_info.reps[1]).dna

new_creature_similarity = self.calculate_raw_dna_similarity(species_rep_dna, other_dna)
if new_creature_similarity < self.sim.species_threshold:
self.logger.info(f"Creating new species, as offspring similarity is below threshold: {new_creature_similarity}")
return True
return False


def getMutatedDNA(self, sim):
mutation = np.clip(np.random.normal(0.0, 1.0, self.dna.shape[0]),-99,99)
result = self.dna + sim.mutation_rate*mutation
mutation = np.clip(np.random.normal(-1.0, 1.0, self.dna.shape[0]),-99,99)
mutation_mask = np.random.random(self.dna.shape) < sim.mutation_rate
result = self.dna + sim.mutation_size * mutation * mutation_mask
newSpecies = self.species

big_mut_loc = 0
if random.uniform(0,1) < self.sim.big_mutation_rate: # do a big mutation

if self.check_if_new_species(result):
newSpecies = sim.species_count
sim.species_count += 1
# biggest_dna_change = mutation.argmax()
return result, newSpecies, big_mut_loc

if random.uniform(0,1) < self.sim.big_mutation_rate: # do a big mutation

cell_x = random.randint(0,self.sim.CW-1)
cell_y = random.randint(0,self.sim.CH-1)
cell_beat = random.randint(0,self.sim.beats_per_cycle-1)
Expand All @@ -113,14 +139,37 @@ def getMutatedDNA(self, sim):
for i in range(self.sim.traits_per_box):
delta = 0
while abs(delta) < 0.5:
delta = np.random.normal(0.0, 1.0, 1)
delta = np.random.normal(-1.0, 1.0, 1)
result[big_mut_loc+i] += delta

#Cells that endure a big mutation are also required to be at least somewhat rigid, because if a cell goes from super-short to super-tall but has low rigidity the whole time, then it doesn't really matter.
if i == 2 and result[big_mut_loc+i] < 0.5:
result[big_mut_loc+i] = 0.5


if self.check_if_new_species(result):
newSpecies = sim.species_count
sim.species_count += 1

return result, newSpecies, big_mut_loc
def calculate_dna_similarity(self, other_creature):
"""
Calculate the genetic similarity between this creature and another creature.
Returns a value between 0 and 1, where 1 means identical DNA.
"""
# Calculate Euclidean distance between DNA arrays, normalized
distance = np.sqrt(np.sum(np.square(self.dna - other_creature.dna)))
# Convert to similarity (closer to 1 means more similar)
max_possible_distance = np.sqrt(len(self.dna) * 36) # Maximum possible distance assuming all values differ by 6 (-3 to 3)
similarity = 1 - (distance / max_possible_distance)
return similarity

def calculate_raw_dna_similarity(self, dna_a, dna_b):
distance = np.sqrt(np.sum(np.square(dna_a - dna_b)))
# Convert to similarity (closer to 1 means more similar)
max_possible_distance = np.sqrt(len(dna_a) * 36) # Maximum possible distance assuming all values differ by 6 (-3 to 3)
similarity = 1 - (distance / max_possible_distance)
return similarity


def traitsToColor(self, dna, x, y, frame):
beat = self.sim.frameToBeat(frame)
Expand Down
130 changes: 105 additions & 25 deletions jes_dataviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
import random
import bisect

# Cache for sorted species keys
_species_keys_cache = {}
# Cache for species colors
_species_color_cache = {}


def drawAllGraphs(sim, ui):
drawLineGraph(sim.percentiles, ui.graph, [70,0,30,30], sim.UNITS_PER_METER, ui.smallFont)
drawSAC(sim.species_pops, ui.sac, [70,0], ui)
Expand Down Expand Up @@ -56,10 +62,106 @@ def drawLineGraph(data,graph,margins,u,font):
thickness = 3
pygame.draw.line(graph, color, (x1, y1), (x2, y2), width=thickness)

def drawSAC(data,sac,margins,ui):
sac.fill((0,0,0))

def drawSAC(data, sac, margins, ui):
"""
Optimized Species Area Chart drawing function.
"""
sac.fill((0, 0, 0))

# Pre-calculate constants
W = sac.get_width() - margins[0] - margins[1]
H = sac.get_height()
LEN = len(data)
LEFT = margins[0]

# Cache the species keys for each generation to avoid repeated sorting
for g in range(len(data)):
_species_keys_cache[g] = sorted(list(data[g].keys()))

# Draw all generations
for g in range(len(data)):
scanDownTrapezoids(data, g, sac, margins, ui)
x1 = LEFT + (g / LEN) * W
x2 = LEFT + ((g + 1) / LEN) * W

if g == 0:
# Optimization for first generation
keys = _species_keys_cache[g]
c_count = data[g][keys[-1]][2] # ending index of the last entry
FAC = H / c_count

for sp in keys:
pop = data[g][sp]
points = [[x1, H/2], [x1, H/2], [x2, H-pop[1]*FAC], [x2, H-pop[2]*FAC]]
color = get_cached_species_color(sp, ui)
pygame.draw.polygon(sac, color, points)
else:
keys = _species_keys_cache[g]
c_count = data[g][keys[-1]][2] # ending index of the last entry
FAC = H / c_count
trapezoidHelper(sac, data, g, g-1, 0, c_count, x1, x2, FAC, 0, ui)

def get_cached_species_color(species_id, ui):
"""
Retrieve cached species color or compute and cache it if not available.
"""
if species_id not in _species_color_cache:
_species_color_cache[species_id] = speciesToColor(species_id, ui)
return _species_color_cache[species_id]

def getRangeEvenIfNone(dicty, key, generation=None):
"""
Optimized function to get the range for a species, even if the species
doesn't exist in the given dictionary.
"""
# Use cached sorted keys if available
if generation is not None and generation in _species_keys_cache:
keys = _species_keys_cache[generation]
else:
# Fall back to sorting if cache not available
keys = sorted(list(dicty.keys()))

if key in keys:
return dicty[key]
else:
n = bisect.bisect_right(keys, key)
if n >= len(keys):
val = dicty[keys[n-1]][2]
else:
val = dicty[keys[n]][1]
return [0, val, val]

def trapezoidHelper(sac, data, g1, g2, i_start, i_end, x1, x2, FAC, level, ui):
"""
Optimized trapezoid drawing helper that uses caching for performance.
"""
# Get the keys from the cache
keys1 = _species_keys_cache.get(g1, sorted(list(data[g1].keys())))

H = sac.get_height()
pop2 = [0, 0, 0]

# Pre-compute all colors needed to avoid calling speciesToColor in the loop
colors = {}
for sp in keys1:
colors[sp] = get_cached_species_color(sp, ui)

# Loop through species using the cached sorted keys
for sp in keys1:
pop1 = data[g1][sp]
if level == 0 and pop1[1] != pop2[2]: # there was a gap
trapezoidHelper(sac, data, g2, g1, pop2[2], pop1[1], x2, x1, FAC, 1, ui)

# Use optimized function with generation hint
pop2 = getRangeEvenIfNone(data[g2], sp, g2)

# Draw the polygon
points = [[x1, H-pop2[1]*FAC], [x1, H-pop2[2]*FAC],
[x2, H-pop1[2]*FAC], [x2, H-pop1[1]*FAC]]
pygame.draw.polygon(sac, colors[sp], points)

# Update for next iteration
pop2 = pop1

def scanDownTrapezoids(data, g, sac, margins, ui):
W = sac.get_width()-margins[0]-margins[1]
Expand All @@ -81,28 +183,6 @@ def scanDownTrapezoids(data, g, sac, margins, ui):
else:
trapezoidHelper(sac, data, g, g-1, 0, c_count, x1, x2, FAC, 0, ui)

def getRangeEvenIfNone(dicty, key):
keys = sorted(list(dicty.keys()))
if key in keys:
return dicty[key]
else:
n = bisect.bisect(keys, key+0.5)
if n >= len(keys):
val = dicty[keys[n-1]][2]
else:
val = dicty[keys[n]][1]
return [0,val,val]

def trapezoidHelper(sac, data, g1, g2, i_start, i_end, x1, x2, FAC, level, ui):
pop2 = [0,0,0]
H = sac.get_height()
for sp in data[g1].keys():
pop1 = data[g1][sp]
if level == 0 and pop1[1] != pop2[2]: #there was a gap
trapezoidHelper(sac, data, g2, g1, pop2[2], pop1[1], x2, x1, FAC, 1, ui)
pop2 = getRangeEvenIfNone(data[g2],sp)
points = [[x1,H-pop2[1]*FAC],[x1,H-pop2[2]*FAC],[x2,H-pop1[2]*FAC],[x2,H-pop1[1]*FAC]]
pygame.draw.polygon(sac,speciesToColor(sp, ui),points)

def drawGeneGraph(species_info, ps, gg, sim, ui, font): # ps = prominent_species
R = ui.GENEALOGY_COOR[4]
Expand Down
Loading