Organic molecules turn up across the Solar System—but organics alone aren’t life. Bivology explores the missing step: how 3D organization—folding, catalysis, compartmentalization, replication—transforms chemistry into living process.
Early 2D simulations show how digital “organics” form, persist, and sometimes replicate. But life’s essence is 3D architecture. Proteins fold into enzymes, DNA helices store dense information, and membranes compartmentalize reactions. Bivology’s roadmap moves from flat grids to volumetric worlds with folded chains, catalytic sites, and vesicle-like compartments—ultimately to organoid analogues that self-organize.
Astrobiology: seek structure, not just molecules. AI: pursue architectures that self-organize and evolve. Philosophy: treat life as organized process, not mere substance.
Research Note (Teaser)
We combined five parts into a cohesive view:
Part I — Organics vs Life (why 3D is essential)
Part II — Transition 2D → 3D (folding, catalysis, compartments)
Part III — Implications (Astrobiology, AI, Meaning)
Part IV — Experimental Directions (what to build next)
Part V — Future Horizons (platform vision)
Read the combined short essay
Organic chemistry is widespread; life is rare. The difference is organization. Bivology simulates that leap. We start with 2D worlds where monomers form polymers, harvest energy, and replicate with mutation. To cross into authentic life-like behavior, we extend into 3D: folded shapes that act, pockets that catalyze, compartments that sustain, modular clusters that self-organize. This lens reframes astrobiology (structure over ingredients), AI (organization over size), and philosophy (life as process). Our roadmap: add folding, model 3D collisions, encode catalytic motifs, form vesicles, evolve organoid analogues, and measure progress with an expanded Life Detection Scale. The horizon is a shared platform where science, technology, and meaning meet: Bivology as a living notebook of how form becomes life.
Prototype: Mars-Inspired Digital “Organics” → Life-like Thresholds
This is the compact Python (polymerization, metabolism, templated replication, Life Detection Scale). Click “Copy” and run locally.
# bivology_mars_proto.py (run with: python bivology_mars_proto.py)
import random, math
from collections import Counter, defaultdict
from dataclasses import dataclass, field
PARAMS={"GRID_W":80,"GRID_H":48,"SEED":42,"STEPS":5000,"INIT_MONOMERS":3500,
"DIFFUSION_P":0.60,"POLYMERIZE_P":0.10,"CHAIN_BREAK_P":0.001,"TEMPLATE_REPL_P":0.05,
"MUTATION_P":0.01,"MIN_REPL_LEN":6,"ENERGY_MAX":20,"ENERGY_DIFFUSE":0.25,
"ENERGY_RECHARGE":0.08,"ENERGY_HARVEST_PER_SITE":2,"CHAIN_ENERGY_USE":1,
"CHAIN_LEN_METABOLIC":5,"FRESH_SEED_RATE":6,"REPORT_EVERY":200}
@dataclass
class Chain:
bits:list; x:int; y:int; energy:int=0
def signature(self): return "".join("1" if b else "0" for b in self.bits)
def length(self): return len(self.bits)
@dataclass
class Cell:
monomers:list=field(default_factory=list)
chains:list=field(default_factory=list)
energy:float=0.0
class World:
def __init__(self,p):
if p["SEED"] is not None: random.seed(p["SEED"])
self.p=p; w,h=p["GRID_W"],p["GRID_H"]
self.grid=[[Cell() for _ in range(w)] for _ in range(h)]
self.chains=[]; self._init_energy_field(); self._seed_monomers()
def _init_energy_field(self):
for y in range(self.p["GRID_H"]):
for x in range(self.p["GRID_W"]):
band=0.5+0.5*math.sin((x/8.0)+math.sin(y/7.0))
base=band*self.p["ENERGY_MAX"]
self.grid[y][x].energy=min(self.p["ENERGY_MAX"],max(0.0,base*0.6))
def _seed_monomers(self):
for _ in range(self.p["INIT_MONOMERS"]):
x=random.randrange(self.p["GRID_W"]); y=random.randrange(self.p["GRID_H"])
self.grid[y][x].monomers.append(random.choice([0,1]))
def _neighbors4(self,x,y):
w,h=self.p["GRID_W"],self.p["GRID_H"]
return [((x-1)%w,y),((x+1)%w,y),(x,(y-1)%h),(x,(y+1)%h)]
def _inject_monomers(self):
for _ in range(self.p["FRESH_SEED_RATE"]):
x=random.randrange(self.p["GRID_W"]); y=random.randrange(self.p["GRID_H"])
self.grid[y][x].monomers.append(random.choice([0,1]))
def _energy_recharge_and_diffuse(self):
for row in self.grid:
for c in row: c.energy=min(self.p["ENERGY_MAX"],c.energy+self.p["ENERGY_RECHARGE"])
out=[[0.0 for _ in range(self.p["GRID_W"])] for _ in range(self.p["GRID_H"])]
for y,row in enumerate(self.grid):
for x,c in enumerate(row):
share=c.energy*self.p["ENERGY_DIFFUSE"]
if share<=0: continue
nbrs=self._neighbors4(x,y); give=share/len(nbrs)
c.energy-=share
for nx,ny in nbrs: out[ny][nx]+=give
for y,row in enumerate(self.grid):
for x,_ in enumerate(row):
self.grid[y][x].energy=min(self.p["ENERGY_MAX"],self.grid[y][x].energy+out[y][x])
def _diffuse_particles(self):
moves=[(-1,0),(1,0),(0,-1),(0,1)]
new=[[[] for _ in range(self.p["GRID_W"])] for _ in range(self.p["GRID_H"])]
for y,row in enumerate(self.grid):
for x,cell in enumerate(row):
for m in cell.monomers:
if random.random()self.p["POLYMERIZE_P"]: continue
local=[i for i,c in enumerate(self.chains) if c and c.x==x and c.y==y]
if local:
ci=random.choice(local); ch=self.chains[ci]
bit=cell.monomers.pop(random.randrange(len(cell.monomers)))
ch.bits.append(bit)
else:
m1=cell.monomers.pop(random.randrange(len(cell.monomers)))
m2=cell.monomers.pop(random.randrange(len(cell.monomers)))
self.chains.append(Chain([m1,m2],x,y,0))
def _chains_metabolism_and_decay(self):
P=self.p
for i,ch in enumerate(self.chains):
if not ch: continue
ch.energy-=P["CHAIN_ENERGY_USE"]
if ch.energy<-5:
self.grid[ch.y][ch.x].monomers.extend(ch.bits); self.chains[i]=None; continue
if ch.length()>=P["CHAIN_LEN_METABOLIC"]:
cell=self.grid[ch.y][ch.x]; take=min(P["ENERGY_HARVEST_PER_SITE"],int(cell.energy))
if take>0: cell.energy-=take; ch.energy+=take
if random.random()
=3:
cut=random.randrange(1,ch.length()-1)
L,R=ch.bits[:cut], ch.bits[cut:]
self.chains[i]=Chain(L,ch.x,ch.y,max(0,ch.energy//2))
if len(R)>=2: self.chains.append(Chain(R,ch.x,ch.y,max(0,ch.energy//2)))
else: self.grid[ch.y][ch.x].monomers.extend(R)
def _templated_replication(self):
from collections import defaultdict
cell_map=defaultdict(list)
for idx,ch in enumerate(self.chains):
if ch: cell_map[(ch.x,ch.y)].append(idx)
for (x,y),idxs in cell_map.items():
if not idxs: continue
t_idx=random.choice(idxs); templ=self.chains[t_idx]
if not templ or templ.length()self.p["TEMPLATE_REPL_P"]: continue
cell=self.grid[y][x]; needed=len(templ.bits)
if len(cell.monomers)=world.p["CHAIN_LEN_METABOLIC"]])/n
from collections import Counter
sigs=Counter(ch.signature() for ch in world.chains if ch)
uniq=len(sigs); dom=0 if n==0 else (sigs.most_common(1)[0][1]/n)
newborns=births[-1] if births else 0
return {"chain_count":n,"median_len":med,"frac_metabolic":frac_met,"unique_sigs":uniq,"sig_dom_frac":dom,"newborns_recent":newborns}
def life_detection_scale(s):
if s["chain_count"]==0: return 0
if s["median_len"]<4: return 1
if s["median_len"]>=4 and s["frac_metabolic"]<0.2: return 2
if s["frac_metabolic"]>=0.2 and s["newborns_recent"]==0: return 3
if s["newborns_recent"]>0 and s["sig_dom_frac"]<0.6: return 4
if s["sig_dom_frac"]>=0.6 and s["unique_sigs"]<=3: return 5
return 6
def main():
w=World(PARAMS); births=[]
for t in range(1,PARAMS["STEPS"]+1):
before=sum(1 for c in w.chains if c); w.step(t); after=sum(1 for c in w.chains if c)
births.append(max(0,after-before))
if t%PARAMS["REPORT_EVERY"]==0:
s=summarize(w,[sum(births[-PARAMS["REPORT_EVERY"]:])]); lds=life_detection_scale(s)
print(f"[t={t}] chains={s['chain_count']:5d} median_len={s['median_len']:2d} metab%={s['frac_metabolic']*100:4.1f} unique={s['unique_sigs']:4d} dom_frac={s['sig_dom_frac']:.2f} newborns={s['newborns_recent']:4d} LDS={lds}")
if __name__=="__main__": main()
Tip: For a harsher “Martian” run, lower ENERGY_RECHARGE and raise CHAIN_BREAK_P. For more life-like behavior, raise TEMPLATE_REPL_P and ENERGY_RECHARGE.