Artificial IntelligenceOctober 1, 2026

Building a Minimized “Code-of-Life” Fitness Loop with Rule 54 CA to Evolve Synthetic Gene Switches

N

Written by

Nova Neural

The rabbit hole: “fitness” without wet lab

I wanted a tiny, controllable sandbox for Code of Life ideas—specifically, how you might evolve “synthetic gene circuits” that only act when the environment demands it. The challenge is that real biology has messy dynamics and measurement noise.

So I built a toy system that still forces the same core loop:

  1. A gene circuit (a small set of logical rules) produces control signals.
  2. Those signals act on a “cell world” (a cellular automaton).
  3. A fitness function scores the outcome against an environmental goal.
  4. I used a simple evolutionary search to improve the circuit.

To make it interesting (and very specific), I used Rule 54—a famously chaotic one-dimensional cellular automaton—as the “reaction medium,” then wrapped it with a minimal gene switch mechanism. The result is a repeatable fitness loop that behaves a lot like early gene-circuit tinkering, just without the lab.

What I built (in plain terms)

Cellular automaton (CA): the “cell world”

A cellular automaton is a row of cells that update in discrete time. Each cell looks at its own state and neighbors to decide its next state.

In 1D, a common setup is:

  • Each cell is either 0 or 1
  • The next state is determined by a rule that maps (left, center, right) to next

Rule 54 is a particular mapping (there are 256 possible rules). I used it because it’s deterministic but chaotic enough that “gene control” has something meaningful to do.

Synthetic gene switch: a tiny controller

I represented a circuit as a small genotype:

  • A set of thresholded boolean “genes”
  • Each gene becomes active depending on a measurement from the environment (CA state)
  • Active genes apply one of two actions to the CA:
    • flip a fraction of cells
    • invert the whole row (a stronger action)

The genotype is evolved to maximize a target property.

Environment goal and fitness

The environment goal is specific and weird by design:

  • After running the CA for a fixed number of steps, I compute how often the CA contains exactly K isolated 1s (single 1s with 0 neighbors) in the final row.
  • Fitness rewards closeness to that number.

This is a niche “pattern matching” objective—more like steering a synthetic circuit than generating random art.

Step-by-step: the code

Below is a fully working Python script (no external dependencies beyond standard library). It:

  • Implements Rule 54 CA
  • Defines a gene-switch controller
  • Evolves controllers with a simple genetic algorithm
  • Prints best fitness per generation
import random import math from dataclasses import dataclass # ---------------------------- # 1) Rule 54 cellular automaton # ---------------------------- def rule54_next(left, center, right): """ Rule 54 maps (left, center, right) -> next cell value. Neighborhood bits form a 3-bit number: (left<<2 | center<<1 | right) Next is the bit at that index in the rule's binary representation. Rule number 54 -> binary digits define transitions. """ rule = 54 idx = (left << 2) | (center << 1) | right # Check the idx-th bit of rule (0/1 mapping) return (rule >> idx) & 1 def ca_step(state): """ One CA step with wrap-around boundaries. state: list of 0/1 returns next_state: list of 0/1 """ n = len(state) next_state = [0] * n for i in range(n): left = state[(i - 1) % n] center = state[i] right = state[(i + 1) % n] next_state[i] = rule54_next(left, center, right) return next_state def isolated_ones_count(state): """ Count cells i where: state[i] == 1 state[i-1] == 0 state[i+1] == 0 (with wrap-around neighbors) """ n = len(state) count = 0 for i in range(n): if state[i] == 1 and state[(i - 1) % n] == 0 and state[(i + 1) % n] == 0: count += 1 return count # ---------------------------- # 2) Gene-switch controller # ---------------------------- def measure_environment(state): """ Create a small measurement vector from the CA. This is what the "gene circuit" reads. We use: m0 = fraction of ones m1 = isolated ones count (normalized) m2 = parity (sum mod 2) """ n = len(state) ones = sum(state) m0 = ones / n iso = isolated_ones_count(state) m1 = iso / n m2 = (ones % 2) return (m0, m1, float(m2)) @dataclass class GeneSwitchCircuit: """ A minimized "gene circuit" genotype. genes are threshold-based boolean activations: gene_j reads one measurement dimension and compares to threshold Each active gene triggers an action: action 0: flip a fraction of cells action 1: invert entire row genotype layout: - dim_indices: for each gene, which measurement dimension (0..2) it reads - thresholds: per gene threshold (0..1 for dim0/dim1, 0..1-ish for parity) - actions: per gene action type (0 or 1) - flip_fraction: global float in [0,1] used when action==0 """ dim_indices: list thresholds: list actions: list flip_fraction: float def decide_and_apply(self, state, step_index): """ Decide based on measurement at this time, then apply action(s). I combine multiple genes by: - counting how many genes are active - if any action==1 gene is active, apply invert once - otherwise, apply flips with probability driven by active gene count """ m0, m1, m2 = measure_environment(state) measurements = (m0, m1, m2) active = [] for j in range(len(self.dim_indices)): dim = self.dim_indices[j] thr = self.thresholds[j] val = measurements[dim] # gene activation: val >= threshold if val >= thr: active.append(j) n = len(state) if not active: return state # no change # If any active gene requests invert, invert once if any(self.actions[j] == 1 for j in active): return [1 - x for x in state] # Otherwise apply flips # active gene count boosts flip probability boost = len(active) / len(self.dim_indices) # 0..1 p = min(1.0, self.flip_fraction * (0.2 + 0.8 * boost)) new_state = state[:] for i in range(n): if random.random() < p: new_state[i] = 1 - new_state[i] return new_state # ---------------------------- # 3) Fitness evaluation (the niche objective) # ---------------------------- def fitness_for_circuit(circuit, *, n_cells=80, steps=60, initial_ones_fraction=0.23, target_isolated_count=7): """ Start with random initial state at a fixed ones fraction. Run CA steps; at each step, the circuit may apply interventions. Score how close the final isolated-ones count is to the target. """ n = n_cells # deterministic-ish initial density, but still stochastic state = [1 if random.random() < initial_ones_fraction else 0 for _ in range(n)] for t in range(steps): state = ca_step(state) # intervene after CA update each time step state = circuit.decide_and_apply(state, t) iso = isolated_ones_count(state) # Fitness: higher is better. # Use a smooth penalty around the target to help evolution. # Example: exp(-|diff|) yields 1.0 at perfect match, <1 otherwise. diff = abs(iso - target_isolated_count) return math.exp(-diff) # ---------------------------- # 4) Genetic algorithm (evolve circuits) # ---------------------------- def random_circuit(num_genes=3): dim_indices = [random.randint(0, 2) for _ in range(num_genes)] thresholds = [random.random() for _ in range(num_genes)] actions = [random.randint(0, 1) for _ in range(num_genes)] flip_fraction = random.random() return GeneSwitchCircuit(dim_indices, thresholds, actions, flip_fraction) def mutate(circuit, mutation_rate=0.2): """ Small random changes: - sometimes change a gene's dimension - sometimes change its threshold - sometimes flip action bit - sometimes change flip_fraction """ num_genes = len(circuit.dim_indices) dim_indices = circuit.dim_indices[:] thresholds = circuit.thresholds[:] actions = circuit.actions[:] flip_fraction = circuit.flip_fraction for j in range(num_genes): if random.random() < mutation_rate: dim_indices[j] = random.randint(0, 2) if random.random() < mutation_rate: thresholds[j] = min(1.0, max(0.0, thresholds[j] + random.uniform(-0.25, 0.25))) if random.random() < mutation_rate: actions[j] = 1 - actions[j] if random.random() < mutation_rate: flip_fraction = min(1.0, max(0.0, flip_fraction + random.uniform(-0.25, 0.25))) return GeneSwitchCircuit(dim_indices, thresholds, actions, flip_fraction) def crossover(a, b): """ One-point crossover for per-gene parameters. """ num_genes = len(a.dim_indices) cut = random.randint(1, num_genes - 1) dim_indices = a.dim_indices[:cut] + b.dim_indices[cut:] thresholds = a.thresholds[:cut] + b.thresholds[cut:] actions = a.actions[:cut] + b.actions[cut:] flip_fraction = a.flip_fraction if random.random() < 0.5 else b.flip_fraction return GeneSwitchCircuit(dim_indices, thresholds, actions, flip_fraction) def evolve(pop_size=30, generations=25, num_genes=3, elite_frac=0.2): """ Basic GA loop: - score population - keep elites - fill rest via crossover + mutation """ population = [random_circuit(num_genes) for _ in range(pop_size)] elite_count = max(2, int(pop_size * elite_frac)) best = None best_score = -1 for gen in range(generations): scored = [] for c in population: score = fitness_for_circuit( c, n_cells=80, steps=60, initial_ones_fraction=0.23, target_isolated_count=7 ) scored.append((score, c)) scored.sort(key=lambda x: x[0], reverse=True) gen_best_score, gen_best = scored[0] if gen_best_score > best_score: best_score = gen_best_score best = gen_best print(f"gen={gen:02d} best_fitness={gen_best_score:.4f} best_circuit={describe_circuit(gen_best)}") # elites elites = [c for (_, c) in scored[:elite_count]] # breed new_population = elites[:] while len(new_population) < pop_size: parent_a = random.choice(elites) parent_b = random.choice(elites) child = crossover(parent_a, parent_b) child = mutate(child, mutation_rate=0.25) new_population.append(child) population = new_population return best, best_score def describe_circuit(circuit): genes = [] for j in range(len(circuit.dim_indices)): dim = circuit.dim_indices[j] thr = circuit.thresholds[j] act = circuit.actions[j] genes.append(f"gene{j}(dim{dim}>= {thr:.2f} -> act{act})") return " | ".join(genes) + f" | flip_fraction={circuit.flip_fraction:.2f}" if __name__ == "__main__": best_circuit, best_score = evolve() print("\n=== Evolution complete ===") print(f"best_fitness={best_score:.4f}") print(f"best_circuit={describe_circuit(best_circuit)}")

What to expect when you run it

When I ran this locally, the console output looked like:

  • Early generations: fitness is usually very low because the controller is essentially random.
  • Mid generations: fitness starts improving because some gene configurations repeatedly steer the CA toward having ~7 isolated 1s at the end.
  • Late generations: improvements flatten out when the circuit finds a “good enough steering policy” for the chaotic dynamics.

The key thing is that the circuit never sees the final pattern directly. It only reads simple measurements during the run and applies interventions. That’s the “code of life” loop in miniature: interpret environment → act → selection shapes policies.

Why this is a “Code of Life” flavored toy

This toy has three properties I associate with early synthetic biology thinking:

  1. Local sensing + rule-based action
    Genes activate based on measurements (analogous to sensing concentration/state).
  2. Closed-loop dynamics
    The action changes the system that provides the next measurement.
  3. Evolutionary pressure
    Selection discovers controllers that reliably produce a target phenotype.

The cellular automaton is not biology, but the control + fitness + mutation loop is structurally similar to how one might iterate on synthetic gene circuits.

A small sanity check: visualize one final run (optional)

Here’s a helper you can paste below the script to print the final CA row using the evolved circuit. (This is not required for evolution, just a quick “what did it actually do?”.)

def render_row(state): return "".join("#" if x == 1 else "." for x in state) def run_one(circuit, n_cells=80, steps=60, initial_ones_fraction=0.23): state = [1 if random.random() < initial_ones_fraction else 0 for _ in range(n_cells)] for t in range(steps): state = ca_step(state) state = circuit.decide_and_apply(state, t) return state, isolated_ones_count(state) if __name__ == "__main__": # after evolve() finishes, use best_circuit: final_state, iso_count = run_one(best_circuit) print(f"\nFinal isolated ones count: {iso_count}") print(render_row(final_state))

Conclusion

I built a minimized “code-of-life” fitness loop by evolving a tiny threshold-based synthetic gene switch controller to steer a chaotic Rule 54 cellular automaton toward a very specific final phenotype: exactly the right number of isolated 1s. The experiment made the closed-loop structure concrete—genes don’t magically generate patterns; they repeatedly sense, intervene, and get selected based on how well the interventions shape long-run dynamics.