Lunagen · v0.1.1

DocumentaciónDocumentation

Todo lo que necesitas para simular, optimizar y visualizar cultivos con Lunagen: del primer ejemplo a la referencia de la API.Everything you need to simulate, optimize and visualize cultures with Lunagen: from the first example to the API reference.

1. Qué es lunagen

lunagen simula un cultivo célula por célula (un agente por célula, vectorizado con NumPy) y permite buscar las condiciones que maximizan o minimizan lo que elijas. Cada célula tiene una reserva de energía, un ciclo de división propio, puede morir (por hambre, vejez o azar) y puede producir uno o varios compuestos.

Nació de un proyecto de celulosa bacteriana (Komagataeibacter xylinus), y se generalizó para que el motor no dependa de ningún organismo: lo específico se declara. Con eso se puede:

  • optimizar la producción de celulosa, de etanol, de proteína u otra biomasa;
  • medir solo el tiempo de vida, solo la reproducción o cualquier otra métrica;
  • elegir con qué variables operar y cuáles dejar fijas;
  • animar cada simulación en una ventana de estilo ciencia ficción;
  • exportar los mejores resultados a CSV, JSON y XML (para Unity).

Un principio de diseño importante: las condiciones del cultivo no actúan mientras corre la simulación. Se traducen una sola vez a parámetros efectivos; con ellos constantes, el motor aplica siempre las mismas ecuaciones.

2. Instalación

Requisitos: Python 3.9 o superior (probado en 3.12) y NumPy.

pip install lunagen                  # núcleo
pip install "lunagen[viz]"           # + pygame-ce y scipy para la ventana en vivo
pip install lunagen-0.1.1-py3-none-any.whl     # desde el archivo .whl
  • pygame-ce abre la ventana (viz=True). También funciona con pygame clásico, pero no instales los dos a la vez: ambos se importan como pygame y se estorban. pygame-ce suele publicar versiones precompiladas para los Python nuevos antes que pygame. Sin ninguno o sin pantalla, optimize sigue funcionando y solo imprime en consola.
  • scipy es opcional: añade colisiones entre las cápsulas de la ventana. Sin él las células se pueden solapar, pero todo lo demás es idéntico.

Comprobar la instalación:

python -c "import lunagen; print(lunagen.__version__)"
lunagen --metrics

3. Inicio rápido

import lunagen as lg

if __name__ == "__main__":
    organismo = lg.presets.komagataeibacter_xylinus()
    estudio = lg.Study(organismo, objective="yield[cellulose]", workers=4)
    resultado = estudio.optimize(samples=60, generations=8, population=16, viz=True)
    print(resultado.best_conditions)       # las mejores condiciones
    print(resultado.best_value)            # g/L de celulosa (validado con semillas nuevas)

Una simulación suelta, sin optimizar:

res = estudio.simulate(temperature_c=29, ph=5.3)
print(res.product_mg["cellulose"], res.peak_alive, res.deaths_by_cause)

4. Cómo funciona

 Condiciones ──► Organism.derive ──► Parámetros efectivos ──► Motor (simulate) ──► SimulationResult
 (temperatura,        (leyes y                (constantes)         (agentes)               │
  pH, glucosa…)        factores)                                                            ▼
                                                                        Métricas ──► Objective ──► valor
                                                                                                    ▲
                                                           Study.optimize: línea base → exploración → refinamiento → validación
Pieza Módulo Qué hace
Organism organism Describe el organismo: variables, factores, leyes, productos.
motor simulate engine Simula un cultivo. Determinista para una semilla.
métricas / Objective metrics Qué se mide y qué se optimiza.
Study study Une todo: simula, evalúa y optimiza, con informes y exportación.
ventana viz Anima cada simulación (se activa con viz=True).
presets presets Organismos ya descritos (Komagataeibacter xylinus).
respuestas responses Curvas combinables: Cardinal, CPM, Saturation…

5. El modelo de simulación

Unidades: tiempo en horas, masa en miligramos (por célula), longitudes en µm. En cada paso de tiempo dt, y para cada célula, en este orden:

  1. Metabolismo. Con c = nutriente_restante / nutriente_inicial: toma = uptake_rate · c/(Ks + c) · (1 − reserva/capacidad) · dt; reserva += toma · rendimiento; reserva −= mantenimiento · dt. Si entre todas piden más nutriente del que queda, se reparte proporcionalmente.
  2. Muerte (prioridad: hambre, vejez, azar). Hambre: reserva ≈ 0 durante starvation_tolerance horas. Vejez: edad ≥ max_lifespan (solo si max_lifespan > 0). Azar: probabilidad 1 − exp(−random_death_rate · dt).
  3. Crecimiento. escala = clip(reserva / (umbral · capacidad), 0, 1); el reloj del ciclo avanza dt · escala (más lento con poca reserva) y cuesta growth_cost · dt · escala. Con la población llena (max_agents) no empiezan a crecer las células que aún no iniciaron el estrangulamiento.
  4. Productos. Para cada producto: masa = rate · factor_individual · escala_prod · dt, y se gasta masa · cost de reserva (nunca más reserva de la que hay).
  5. División. Al completar el ciclo, la célula se divide en dos hijas que se reparten la reserva a partes iguales; la hija hereda el factor individual de producción. Si no cabe, se dividen solo algunas, elegidas al azar.

Toda la aleatoriedad sale de una semilla, así que la misma semilla da exactamente el mismo resultado. Cada célula inicial tiene una fase distinta del ciclo y variación individual de ciclo y de producción (variation), que se hereda.

6. Describir un organismo

Un Organism es una descripción declarativa. Mira lunagen/presets.py (xylinus) y examples/03_new_organism.py (levadura) como modelos (vienen en el código fuente del paquete, el .tar.gz de PyPI).

import lunagen as lg
from lunagen import Cardinal, CPM, Inhibition, Ref, Saturation

org = lg.Organism(
    name="Mi organismo",
    nutrient_label="Azúcar",                     # nombre del nutriente (solo para mostrar)
    variables=[                                  # qué se puede controlar
        lg.Variable("temperature_c", 30.0, (20, 40), "°C", "Temperatura", "{:.1f}"),
        lg.Variable("ph", 5.5, (4, 7), "", "pH", "{:.2f}"),
        lg.Variable("sugar_gl", 40.0, (10, 100), "g/L", "Azúcar", "{:.0f}"),
        lg.Variable("inoculum_pct", 5.0, (1, 15), "%", "Inóculo", "{:.1f}"),
    ],
    params=dict(base_cycle_h=2.0, base_uptake=0.04),          # números sueltos calibrables
    factors={                                                  # respuestas entre 0 y 1
        "g_temp": Cardinal("temperature_c", 10, 31, 42),
        "g_ph": CPM("ph", 3.5, 5.5, 8.0),
        "growth": Ref("g_temp") * Ref("g_ph"),
        "osmotic": Inhibition("sugar_gl", 80.0, 2),
    },
    laws={                                                     # condiciones -> parámetros
        "n_initial": lambda v: round(v["inoculum_pct"] * 40),
        "nutrient_mg": lambda v: v["sugar_gl"] * v["volume_l"] * 1000.0,
        "division_interval": lambda v: v["base_cycle_h"] / max(v["growth"], 1e-3),
        "uptake_rate": lambda v: v["base_uptake"] * v["osmotic"],
        "maintenance_rate": lambda v: 0.002,
        "random_death_rate": lambda v: 0.001 + 0.01 * (1 - v["g_temp"]) ** 2,
    },
    products=[lg.Product("biomass_x", rate=0.004, law=Ref("growth"), cost=2.0, label="Biomasa X")],
)

Las partes

variables — lista de Variable(name, default, bounds, unit, label, fmt). Son las condiciones que se pueden fijar u optimizar. bounds son los límites de búsqueda por defecto.

params — diccionario de números sueltos que las leyes pueden leer (v["base_cycle_h"]). Sirven para calibrar sin tocar las fórmulas (org.with_params(base_uptake=0.05)).

derived (opcional) — variables calculadas a partir de otras, por ejemplo un pH efectivo: derived={"ph_eff": lambda v: v["ph"] - 0.03 * max(0, v["glucose_gl"] - 10)}.

factors — respuestas intermedias (normalmente entre 0 y 1), definidas con las curvas de respuesta. Se evalúan en orden, así que un factor puede usar los anteriores con Ref("nombre").

laws — traducen el entorno a los parámetros de la simulación. Cada ley recibe un diccionario v y devuelve un número. Son obligatorias estas seis:

Ley Significado Unidad
n_initial células iniciales (se limita a [1, max_agents]) células
nutrient_mg nutriente total inicial del cultivo mg
division_interval duración del ciclo celular (tope 10 000 h) h
uptake_rate toma máxima de nutriente por célula mg/h
maintenance_rate gasto de reserva por mantenimiento reserva/h
random_death_rate mortalidad por azar 1/h

Además, cualquier campo de Physiology puede ser una ley (p. ej. max_lifespan ligada al pH). Lo que ve una ley en v: los params, las variables, las derivadas, los factores y tres constantes de la simulación: volume_l, duration_h, max_agents.

products — uno o varios Product(name, rate, law, cost, variation, reserve_threshold, label, unit, color):

Campo Significado
rate mg / h / célula en condiciones ideales
law efecto del entorno que multiplica rate (una curva; None = constante)
cost reserva consumida por cada mg producido
variation variación individual (±), heredada por las hijas
reserve_threshold fracción de la capacidad por debajo de la cual produce menos
label, unit, color solo para mostrar (ventana e informes)

El primer producto es el principal: es el que dibuja la ventana como fibras y el que usan por defecto las métricas sin argumento ("yield" = "yield[<primer producto>]").

physiology (Physiology) — constantes del metabolismo y del ciclo:

Campo Defecto Significado
half_saturation 0.1 constante de saturación del nutriente (fracción del inicial)
yield_coefficient 0.5 reserva ganada por mg de nutriente
reserve_capacity 0.05 capacidad máxima de reserva
initial_reserve 0.6 reserva inicial (fracción de la capacidad)
growth_cost 0.004 reserva/h a velocidad de crecimiento completa
growth_reserve_threshold 0.3 umbral de reserva para crecer a pleno ritmo
division_variation 0.2 variación individual de la duración del ciclo (±)
constriction_start 0.7 fracción del ciclo en que empieza el estrangulamiento (visual)
max_lifespan 0.0 h; 0 = sin muerte por vejez
starvation_tolerance 4.0 h con reserva ≈ 0 antes de morir

morphology (Morphology) — length_um, radius_um, length_variation, radius_variation. Solo se usa para la ventana y para el XML de Unity.

7. Curvas de respuesta

Se combinan con *, +, - y **. Todo parámetro numérico puede darse también como nombre de un parámetro (un texto), para calibrarlo sin tocar el código.

Curva Descripción
Cardinal(var, mín, óptimo, máx) Campana asimétrica (CTMI, Rosso 1993). 0 / 1 / 0 en mín / óptimo / máx. Típica para temperatura. Falla al construirla si no es unimodal con esos valores.
CPM(var, mín, óptimo, máx) Campana (Rosso 1995), unimodal para cualquier mín < óptimo < máx. Típica para pH.
Saturation(var, k, vmax=1, normalized=True) Monod x/(k+x). Normalizada vale 1 en el máximo.
Inhibition(var, ki, order=2) 1/(1+(x/ki)^orden): inhibición por exceso (sustrato, sal…).
Q10(var, q10=2, ref=30) Se multiplica por q10 cada +10 unidades sobre la referencia.
Overload(var, start, end=1, penalty=0.5, vmax=1) 1 − penalidad · smoothstep(inicio, fin, x): penaliza el exceso.
Gaussian(var, mu, sigma) Campana simétrica.
Hill(var, k, n=2) Respuesta sigmoidea.
Linear(var, slope, intercept, lo, hi) Recta recortada a [lo, hi].
Ref(nombre) Valor ya calculado de otra variable, factor o parámetro.
Const(valor) Constante.
Func(f) Cualquier función propia: Func(lambda v: v["ph"] * 2).

Ejemplo (crecimiento de K. xylinus): Cardinal("temperature_c", 12, 31, 40) * CPM("ph_eff", 3.5, 5.5, 8.0) * Saturation("aeration", 0.15).

8. Estudios: simular, evaluar y optimizar

study = lg.Study(
    organism,
    objective="yield[cellulose]",      # métrica, diccionario ponderado u Objective
    space=["temperature_c", "ph"],     # con qué variables operar (lista o {nombre: (mín, máx)})
    fixed={"glucose_gl": 30},          # variables que se dejan fijas
    sim={"duration_h": 96},            # opciones de SimConfig
    replicas=3,                        # réplicas estocásticas por candidata
    seed=12345,
    workers=1,                         # procesos en paralelo
    direction=None,                    # "max" | "min" (por defecto, el de la métrica)
    constraints=[],                    # [(métrica, operador, valor)]
)
  • space admite una lista de nombres (usa los límites del organismo) o un diccionario para cambiar los límites. Si se omite, se optimizan todas las variables que no estén en fixed.
  • Las variables que no están ni en space ni en fixed se quedan en su valor por defecto.
  • SimConfig: duration_h (96), dt_h (0.1), culture_volume_l (0.05), max_agents (3000), record_every (10). El volumen solo convierte masas totales en g/L.

Métodos

Método Qué hace
study.simulate(seed=None, observer=None, **condiciones) Una simulación → SimulationResult.
study.evaluate(replicas=None, **condiciones) Varias réplicas + el objetivo → Candidate.
study.metrics(nombres, seed=None, **condiciones) Valores de varias métricas para unas condiciones.
study.conditions(**override) Condiciones por defecto + fixed + lo indicado.
study.optimize(...) Búsqueda completa (abajo).
Study.from_config(cfg) Construye el estudio desde un dict/JSON (devuelve (estudio, opciones)).

optimize

resultado = study.optimize(
    method="evolutionary",   # "evolutionary" | "random" | "grid"
    samples=60,              # candidatas de la exploración (random y evolutionary)
    generations=8,           # generaciones de refinamiento (solo evolutionary)
    population=16,           # hijas por generación
    finalists=3,             # candidatas que pasan a la validación
    grid_levels=3,           # niveles por variable (solo grid)
    sensitivity=True,        # informe de sensibilidad al final
    out="output",            # carpeta de salida (None = no escribe archivos)
    unity_sample=200,        # células fundadoras en el XML
    viz=False,               # True abre la ventana en vivo
    seconds_per_sim=4.0,     # duración en pantalla de cada simulación
    fps=45, fullscreen=False,
)

Etapas (todas se imprimen en consola, y salen también en el log de la ventana):

  1. Línea base: condiciones por defecto, para comparar.
  2. Exploración: muestreo Latin Hypercube (evolutionary), aleatorio (random) o malla completa (grid).
  3. Refinamiento (solo evolutionary): cada generación muta a las mejores con ruido gaussiano de paso decreciente (empieza en 0.15 del rango y se reduce; más rápido si no mejora).
  4. Validación: las mejores se reevalúan con semillas nuevas, para no quedarse con una candidata que ganó por suerte. El ganador es el mejor validado.
  5. Informe: parámetros efectivos, métricas de una réplica, avisos (población limitada por el tope o nutriente sin agotar) y sensibilidad a cada variable.
  6. Exportación: CSV, JSON y XML.

Devuelve un StudyResult:

Campo Contenido
best_conditions Conditions ganadoras
best_value, best_std valor y desviación del objetivo en la validación
baseline_value, improvement_pct línea base y mejora relativa
history, validated todas las candidatas evaluadas / las validadas
sensitivity {variable: [(valor, objetivo), …]}
replica_seed semilla de la réplica descrita y exportada
simulations, elapsed_s, out_dir, interrupted datos de la ejecución

resultado.top(n) devuelve las n mejores candidatas. Si cierras la ventana antes de terminar, el estudio se interrumpe y devuelve lo mejor encontrado hasta ese momento (interrupted=True).

9. Métricas y objetivos

Una métrica es un número sacado de una simulación. Se pide por nombre; algunas llevan argumento entre corchetes ("yield[ethanol]"); sin argumento usan el primer producto (o, en death_fraction, la causa starvation).

Métrica Unidad Sentido Descripción
yield[p] g/L max Producto acumulado al final
efficiency[p] g/g max Producto por nutriente inicial
productivity[p] g/L/h max Rendimiento dividido entre la duración
specific_production[p] mg/(cel·h) max Producto por célula y hora viva
peak_population cel max Máximo de células vivas
final_population cel max Células vivas al terminar
time_to_peak_h h min Hora de la población máxima
total_divisions max Divisiones totales
max_growth_rate 1/h max Máxima tasa específica de crecimiento (media móvil de 3 puntos)
doubling_time_h h min ln 2 / tasa máxima (la duración si no crece)
mean_lifespan_h h max Tiempo medio de vida, desde el último nacimiento/división hasta la muerte
median_lifespan_h h max Mediana del anterior
max_lifespan_h h max Máximo observado (incluye a las que siguen vivas)
survival_fraction max 1 − muertes / (células iniciales + divisiones)
total_deaths min Muertes totales
extinction_time_h h max Hora en que muere la última célula (la duración si sobreviven)
death_fraction[causa] min Fracción de muertes por starvation, age o random
nutrient_consumed_fraction max Fracción del nutriente inicial consumida

Ojo con el tiempo de vida. Se mide desde el último nacimiento o división, así que mean_lifespan_h por sí sola premia condiciones donde las células casi no se dividen. Combínala con una restricción (("final_population", ">=", …)) o con survival_fraction.

Métricas propias

@lg.metric("viabilidad", unit="%", direction="max")
def viabilidad(result, cond, ctx):
    return 100 * result.final_alive / max(1, result.peak_alive)

result es el SimulationResult, cond las Conditions y ctx tiene .organism y .sim. Con workers > 1, defínela a nivel de módulo (fuera de funciones) para que exista también en los procesos hijos. Para métricas con argumento, mira _factory en lunagen/metrics.py.

Objetivos

lg.Objective("yield")                                         # una métrica
lg.Objective("doubling_time_h", direction="min")              # minimizar
lg.Objective({"yield[cellulose]": 1.0, "survival_fraction": 2.0})   # suma ponderada
lg.Objective("yield", constraints=[("final_population", ">=", 100)])
  • En una suma ponderada un peso negativo penaliza esa métrica.
  • Una restricción incumplida resta 1000 · (1 + violación relativa) (el optimizador la evita; no se descarta la candidata). Operadores: >=, <=, >, <.
  • Con restricciones violadas, los valores impresos ya incluyen la penalización.
  • Con Study puedes pasar directamente un texto, un diccionario o un Objective.

10. Ventana en vivo

study.optimize(viz=True) abre una ventana (requiere pygame-ce) y anima cada simulación de la búsqueda en cámara rápida. La simulación avanza al ritmo al que se dibuja.

  • Izquierda: caja Petri vista desde arriba (cada célula, una cápsula 2D) y, debajo, la telemetría en tiempo real: tiempo, vivas, muertas, divisiones, reserva media, producto principal, nutriente restante, causas de muerte y tres gráficas (con una línea gris para la mejor simulación hasta ahora).
  • Derecha: nombre del organismo y progreso global con tiempo estimado, condiciones y parámetros efectivos de la simulación en curso, estado del optimizador y log con scroll.

Qué viene de la simulación y qué no. Todo es fiel a los datos: quién vive, muere (y de qué) o se divide; el tamaño de la cápsula es su fase del ciclo celular; el color, su reserva (rojo = vacía, cian = llena); la línea de estrangulamiento aparece al pasar constriction_start; las fibras son el producto principal realmente producido; el brillo ámbar del medio es el nutriente que queda. Lo único decorativo son las posiciones X/Y (el modelo no es espacial): las células se mueven con una física suave y las hijas nacen pegadas a su madre.

Tecla Acción
ESPACIO pausa
+ / - velocidad de la animación
S saltar la animación de la simulación en curso
F animación activada/desactivada (el cálculo sigue a máxima velocidad)
F11 pantalla completa
rueda del ratón, RePág, AvPág, Inicio, Fin desplazar el log
ESC o cerrar la ventana salir (el estudio se interrumpe y devuelve lo mejor hasta entonces)

Si pygame-ce no está instalado o no hay pantalla, el estudio corre igual, solo en consola. Con workers > 1, las réplicas de cada candidata (salvo la animada) se calculan en paralelo mientras se anima la primera.

11. Archivos que se generan

Con out="output" (por defecto):

Archivo Contenido
evaluations.csv cada candidata: etapa, índice, valor de cada variable, media y desviación del objetivo
results.json resumen: mejores condiciones, línea base, mejora, límites, fijas, simulación, sensibilidad, semillas
best_unity.xml la mejor condición, simulada con la replica_seed, lista para Unity

El XML tiene la raíz <Simulation> con: <Conditions> (una etiqueta por variable), <Metrics> (rendimiento de cada producto y métricas estándar, con su unidad), <Parameters> (los parámetros efectivos, con un <product> por producto) y <Founders> (muestra de unity_sample células iniciales con longitud, radio, fase del ciclo, reserva, destino y lo producido por su linaje: celluloseMg, ethanolMg…). El esquema es una propuesta y hay que alinearlo con el script que lo lee.

12. Configuración con JSON y terminal

Un estudio completo sin escribir código (solo con organismos de presets):

{
  "organism": "komagataeibacter_xylinus",
  "calibration": {"base_uptake_rate": 0.04},
  "objective": "yield[cellulose]",
  "direction": "max",
  "constraints": [["final_population", ">=", 100]],
  "space": {"temperature_c": [24, 34], "ph": [4.5, 6.5]},
  "fixed": {"glucose_gl": 30},
  "sim": {"duration_h": 72},
  "replicas": 3,
  "seed": 12345,
  "workers": 1,
  "optimize": {"samples": 20, "generations": 3, "population": 8, "viz": false, "out": "output_config"}
}

space puede ser una lista de nombres o un diccionario con límites; objective un texto o un diccionario de pesos; optimize admite los mismos argumentos que Study.optimize.

lg.run("config.json")
lunagen config.json            # equivale a: python -m lunagen config.json
lunagen --metrics              # lista las métricas disponibles
lunagen --version

Los organismos propios se describen en Python (contienen funciones); luego se pueden usar con Study como cualquier otro.

13. Calibración

Los presets son funciones cuyos argumentos son las perillas de calibración, todos con el valor del modelo original:

org = lg.presets.komagataeibacter_xylinus(
    base_uptake_rate=0.05,
    growth_ph=(3.5, 5.6, 8.0),                  # (mín, óptimo, máx)
    physiology=lg.Physiology(max_lifespan=48.0),
)
org2 = org.with_params(maintenance_q10=2.2)      # recalibrar parámetros sueltos de una copia
org3 = org.replace(morphology=lg.Morphology(length_um=3.0))

Perillas de K. xylinus: base_division_interval_h, base_uptake_rate, base_maintenance_rate, maintenance_q10, base_random_death_rate, stress_death_gain, agents_per_inoculum_pct, base_production_rate, production_variation, production_cost_per_unit, production_reserve_threshold, growth_temperature, production_temperature, growth_ph, production_ph, oxygen_half_saturation, oxygen_overload_start, oxygen_overload_penalty, glucose_inhibition, gluconic_threshold_gl, gluconic_ph_drop_per_gl, physiology, morphology.

Los efectos del entorno sobre las tasas (curvas cardinales, saturación de oxígeno, inhibición por glucosa…) son supuestos del modelo: están todos expuestos para calibrarlos con datos reales. Tras calibrar, conviene mantener una prueba que vuelva a los valores originales (ver §16).

14. Paralelismo y reproducibilidad

  • Semillas. Todo es determinista para una semilla. La búsqueda usa seed + 1000 + i y la validación seed + 50000 + i. Usar las mismas semillas para todas las candidatas hace la comparación más justa.
  • workers. Con workers > 1 se usan procesos (método spawn, seguro junto a la ventana). Solo viajan datos entre procesos (parámetros y resultados), así que se pueden usar organismos con funciones lambda. El script debe llevar el resguardo if __name__ == "__main__":.
  • Las métricas propias deben estar definidas a nivel de módulo (ver §9).
  • max_agents limita el número de células vivas por rendimiento. Si el informe avisa de que la población estuvo limitada por el tope, el resultado depende de ese tope: valora subirlo.

15. Referencia de la API

import lunagen as lg

Núcleo

Nombre Descripción
lg.Study Simular, evaluar y optimizar (§8).
lg.StudyResult, lg.Candidate Resultados de optimize y de una evaluación.
lg.run(config) Ejecuta un estudio desde un dict o JSON.
lg.Organism, lg.Variable, lg.Product, lg.Conditions Descripción del organismo (§6). Conditions es de solo lectura: c["ph"], c.ph, c.replace(ph=6), c.to_dict().
lg.SimConfig, lg.Physiology, lg.Morphology Configuración de simulación, fisiología y forma.
lg.simulate(params, sim, seed, morphology, observer) Motor de bajo nivel; params sale de Organism.derive(cond, sim).
lg.SimulationResult Resultado de una simulación (abajo).
lg.presets komagataeibacter_xylinus(...), load(nombre, **calibración), PRESETS.

Métricas y curvas

Nombre Descripción
lg.metric, lg.get_metric, lg.available_metrics Registrar, obtener y listar métricas.
lg.Objective, lg.Metric, lg.Context, lg.report_values Objetivos y utilidades de informe.
lg.Cardinal, CPM, Saturation, Inhibition, Q10, Overload, Gaussian, Hill, Linear, Const, Ref, Func, Effect Curvas de respuesta (§7).

SimulationResult

Campo Contenido
times, alive, dead, nutrient_mg, mean_reserve series temporales (un punto cada record_every pasos)
product_series {producto: producido acumulado (mg) en el tiempo}
product_mg {producto: total producido (mg)}
nutrient_initial_mg, nutrient_final_mg nutriente inicial y final (mg)
n_initial, duration_h, volume_l datos de la simulación
peak_alive, time_of_peak_h, final_alive población
total_deaths, deaths_by_cause, total_divisions, capped_steps eventos
lifespans, survivor_ages, death_times tiempos de vida (h desde el último nacimiento/división)
founders registro de las células iniciales (para Unity)

Observador. simulate(..., observer=f) llama a f(StepInfo) al final de cada paso con las células vivas (uids, reserve, age_frac), las muertes y divisiones de ese paso y los totales. El observador solo lee: los resultados son idénticos con o sin él. Así funciona la ventana.

16. Pruebas

python tests/test_regression.py     # la librería == el modelo original, bit a bit
python tests/test_library.py        # métricas, objetivos, validaciones, varios productos, paralelo, exportación

Las pruebas vienen en el código fuente del paquete (el .tar.gz de PyPI). test_regression.py compara contra tests/legacy_bacillus_model.py (copia del modelo original): 5 condiciones × 3 semillas, con parámetros, series, fundadoras y totales exactamente iguales. Si cambias el motor, esta prueba debe seguir pasando.

17. Limitaciones y preguntas frecuentes

  • Las posiciones X/Y de la ventana son decorativas. El modelo no es espacial.
  • Los efectos del entorno son supuestos (curvas, inhibición, ácido glucónico…). Calíbralos con datos reales antes de sacar conclusiones biológicas.
  • El tiempo de vida se mide desde el último nacimiento o división (ver §9).
  • El esquema del XML para Unity es una propuesta: alinéalo con tu lector.
  • Rendimiento: cada simulación tarda decenas o cientos de milisegundos; la animación es lo que consume tiempo (seconds_per_sim, o F para desactivarla).
  • ¿Puedo usar un organismo propio con lunagen config.json? No: contienen funciones. Descríbelo en Python y úsalo con Study.
  • ¿Por qué if __name__ == "__main__":? Con varios procesos, cada proceso hijo vuelve a importar el script; sin ese resguardo se relanzaría el estudio.
  • La ventana no abre. Instala pygame-ce (pip install "lunagen[viz]"); si no hay pantalla, el estudio corre solo en consola.

1. What is lunagen

lunagen simulates a culture cell by cell (one agent per cell, vectorized with NumPy) and lets you search for the conditions that maximize or minimize whatever you choose. Every cell has an energy reserve and its own division cycle, it can die (from starvation, old age or chance) and it can produce one or several compounds.

It was born from a bacterial cellulose project (Komagataeibacter xylinus) and generalized so that the engine does not depend on any particular organism: everything specific is declared. With that you can:

  • optimize the production of cellulose, ethanol, protein or any other biomass;
  • measure only lifespan, only reproduction, or any other metric;
  • choose which variables to work with and which to keep fixed;
  • animate every simulation in a sci-fi style live window;
  • export the best results to CSV, JSON and XML (for Unity).

One important design principle: culture conditions do not act while the simulation runs. They are translated once into effective parameters; with those held constant, the engine always applies the same equations.

2. Installation

Requirements: Python 3.9 or later (tested on 3.12) and NumPy.

pip install lunagen                  # core
pip install "lunagen[viz]"           # + pygame-ce and scipy for the live window
pip install lunagen-0.1.1-py3-none-any.whl     # from the .whl file
  • pygame-ce opens the window (viz=True). It also works with classic pygame, but do not install both at once: they are both imported as pygame and get in each other's way. pygame-ce usually publishes prebuilt packages for new Python versions before pygame does. Without either one, or without a display, optimize still works and just prints to the console.
  • scipy is optional: it adds collisions between the capsules in the window. Without it the cells can overlap, but everything else is identical.

Check the installation:

python -c "import lunagen; print(lunagen.__version__)"
lunagen --metrics

3. Quick start

import lunagen as lg

if __name__ == "__main__":
    organism = lg.presets.komagataeibacter_xylinus()
    study = lg.Study(organism, objective="yield[cellulose]", workers=4)
    result = study.optimize(samples=60, generations=8, population=16, viz=True)
    print(result.best_conditions)       # the best conditions
    print(result.best_value)            # g/L of cellulose (validated with fresh seeds)

A single simulation, without optimizing:

res = study.simulate(temperature_c=29, ph=5.3)
print(res.product_mg["cellulose"], res.peak_alive, res.deaths_by_cause)

4. How it works

 Conditions ──► Organism.derive ──► Effective parameters ──► Engine (simulate) ──► SimulationResult
 (temperature,       (laws and             (constants)             (agents)               │
  pH, glucose…)       factors)                                                             ▼
                                                                      Metrics ──► Objective ──► value
                                                                                                  ▲
                                                     Study.optimize: baseline → exploration → refinement → validation
Piece Module What it does
Organism organism Describes the organism: variables, factors, laws, products.
simulate engine engine Simulates one culture. Deterministic for a given seed.
metrics / Objective metrics What is measured and what is optimized.
Study study Ties it all together: simulates, evaluates and optimizes, with reports and export.
window viz Animates every simulation (enabled with viz=True).
presets presets Ready-made organisms (Komagataeibacter xylinus).
responses responses Composable curves: Cardinal, CPM, Saturation…

5. The simulation model

Units: time in hours, mass in milligrams (per cell), lengths in µm. At every time step dt, and for every cell, in this order:

  1. Metabolism. With c = remaining_nutrient / initial_nutrient: uptake = uptake_rate · c/(Ks + c) · (1 − reserve/capacity) · dt; reserve += uptake · yield; reserve −= maintenance · dt. If together the cells ask for more nutrient than is left, it is shared out proportionally.
  2. Death (priority: starvation, old age, chance). Starvation: reserve ≈ 0 for starvation_tolerance hours. Old age: age ≥ max_lifespan (only if max_lifespan > 0). Chance: probability 1 − exp(−random_death_rate · dt).
  3. Growth. scale = clip(reserve / (threshold · capacity), 0, 1); the cycle clock advances dt · scale (slower with a low reserve) and costs growth_cost · dt · scale. With a full population (max_agents), cells that have not yet started constriction do not start growing.
  4. Products. For each product: mass = rate · individual_factor · product_scale · dt, and mass · cost of reserve is spent (never more reserve than is available).
  5. Division. When the cycle is complete, the cell divides into two daughters that share the reserve equally; the daughter inherits the individual production factor. If there is no room, only some divide, chosen at random.

All randomness comes from a seed, so the same seed gives exactly the same result. Every initial cell starts in a different phase of its cycle and has individual variation in cycle and production (variation), which is inherited.

6. Describing an organism

An Organism is a declarative description. See lunagen/presets.py (xylinus) and examples/03_new_organism.py (yeast) as models (both ship in the package's source distribution, the .tar.gz on PyPI).

import lunagen as lg
from lunagen import Cardinal, CPM, Inhibition, Ref, Saturation

org = lg.Organism(
    name="My organism",
    nutrient_label="Sugar",                      # nutrient name (display only)
    variables=[                                  # what can be controlled
        lg.Variable("temperature_c", 30.0, (20, 40), "°C", "Temperature", "{:.1f}"),
        lg.Variable("ph", 5.5, (4, 7), "", "pH", "{:.2f}"),
        lg.Variable("sugar_gl", 40.0, (10, 100), "g/L", "Sugar", "{:.0f}"),
        lg.Variable("inoculum_pct", 5.0, (1, 15), "%", "Inoculum", "{:.1f}"),
    ],
    params=dict(base_cycle_h=2.0, base_uptake=0.04),          # loose, tunable numbers
    factors={                                                  # responses between 0 and 1
        "g_temp": Cardinal("temperature_c", 10, 31, 42),
        "g_ph": CPM("ph", 3.5, 5.5, 8.0),
        "growth": Ref("g_temp") * Ref("g_ph"),
        "osmotic": Inhibition("sugar_gl", 80.0, 2),
    },
    laws={                                                     # conditions -> parameters
        "n_initial": lambda v: round(v["inoculum_pct"] * 40),
        "nutrient_mg": lambda v: v["sugar_gl"] * v["volume_l"] * 1000.0,
        "division_interval": lambda v: v["base_cycle_h"] / max(v["growth"], 1e-3),
        "uptake_rate": lambda v: v["base_uptake"] * v["osmotic"],
        "maintenance_rate": lambda v: 0.002,
        "random_death_rate": lambda v: 0.001 + 0.01 * (1 - v["g_temp"]) ** 2,
    },
    products=[lg.Product("biomass_x", rate=0.004, law=Ref("growth"), cost=2.0, label="Biomass X")],
)

The parts

variables — a list of Variable(name, default, bounds, unit, label, fmt). These are the conditions that can be fixed or optimized. bounds are the default search limits.

params — a dictionary of loose numbers that the laws can read (v["base_cycle_h"]). They let you calibrate without touching the formulas (org.with_params(base_uptake=0.05)).

derived (optional) — variables computed from other ones, for example an effective pH: derived={"ph_eff": lambda v: v["ph"] - 0.03 * max(0, v["glucose_gl"] - 10)}.

factors — intermediate responses (normally between 0 and 1), defined with the response curves. They are evaluated in order, so a factor can use the previous ones with Ref("name").

laws — translate the environment into simulation parameters. Each law receives a dictionary v and returns a number. These six are required:

Law Meaning Unit
n_initial initial cells (limited to [1, max_agents]) cells
nutrient_mg total initial nutrient of the culture mg
division_interval length of the cell cycle (capped at 10 000 h) h
uptake_rate maximum nutrient uptake per cell mg/h
maintenance_rate reserve spent on maintenance reserve/h
random_death_rate death by chance 1/h

In addition, any Physiology field can be a law (e.g. max_lifespan tied to pH). What a law sees in v: the params, the variables, the derived values, the factors and three simulation constants: volume_l, duration_h, max_agents.

products — one or several Product(name, rate, law, cost, variation, reserve_threshold, label, unit, color):

Field Meaning
rate mg / h / cell in ideal conditions
law environmental effect that multiplies rate (a curve; None = constant)
cost reserve consumed per mg produced
variation individual variation (±), inherited by the daughters
reserve_threshold fraction of the capacity below which it produces less
label, unit, color display only (window and reports)

The first product is the main one: it is the one the window draws as fibers and the one used by default by metrics without an argument ("yield" = "yield[<first product>]").

physiology (Physiology) — constants of metabolism and the cycle:

Field Default Meaning
half_saturation 0.1 nutrient saturation constant (fraction of the initial amount)
yield_coefficient 0.5 reserve gained per mg of nutrient
reserve_capacity 0.05 maximum reserve capacity
initial_reserve 0.6 initial reserve (fraction of capacity)
growth_cost 0.004 reserve/h at full growth speed
growth_reserve_threshold 0.3 reserve threshold for growing at full pace
division_variation 0.2 individual variation of the cycle length (±)
constriction_start 0.7 fraction of the cycle at which constriction starts (visual)
max_lifespan 0.0 h; 0 = no death by old age
starvation_tolerance 4.0 h with reserve ≈ 0 before dying

morphology (Morphology) — length_um, radius_um, length_variation, radius_variation. Used only for the window and the Unity XML.

7. Response curves

They combine with *, +, - and **. Any numeric parameter can also be given as the name of a parameter (a string), so you can calibrate without touching the code.

Curve Description
Cardinal(var, min, optimum, max) Asymmetric bell (CTMI, Rosso 1993). 0 / 1 / 0 at min / optimum / max. Typical for temperature. Fails when built if it is not unimodal with those values.
CPM(var, min, optimum, max) Bell (Rosso 1995), unimodal for any min < optimum < max. Typical for pH.
Saturation(var, k, vmax=1, normalized=True) Monod x/(k+x). Normalized, it equals 1 at the maximum.
Inhibition(var, ki, order=2) 1/(1+(x/ki)^order): inhibition by excess (substrate, salt…).
Q10(var, q10=2, ref=30) Multiplied by q10 for every +10 units above the reference.
Overload(var, start, end=1, penalty=0.5, vmax=1) 1 − penalty · smoothstep(start, end, x): penalizes excess.
Gaussian(var, mu, sigma) Symmetric bell.
Hill(var, k, n=2) Sigmoid response.
Linear(var, slope, intercept, lo, hi) Line clipped to [lo, hi].
Ref(name) Already-computed value of another variable, factor or parameter.
Const(value) Constant.
Func(f) Any function of your own: Func(lambda v: v["ph"] * 2).

Example (growth of K. xylinus): Cardinal("temperature_c", 12, 31, 40) * CPM("ph_eff", 3.5, 5.5, 8.0) * Saturation("aeration", 0.15).

8. Studies: simulate, evaluate and optimize

study = lg.Study(
    organism,
    objective="yield[cellulose]",      # metric, weighted dictionary or an Objective
    space=["temperature_c", "ph"],     # which variables to work with (list or {name: (min, max)})
    fixed={"glucose_gl": 30},          # variables that stay fixed
    sim={"duration_h": 96},            # SimConfig options
    replicas=3,                        # stochastic replicas per candidate
    seed=12345,
    workers=1,                         # parallel processes
    direction=None,                    # "max" | "min" (by default, the metric's own)
    constraints=[],                    # [(metric, operator, value)]
)
  • space accepts a list of names (using the organism's limits) or a dictionary to change the limits. If omitted, every variable not in fixed is optimized.
  • Variables that are in neither space nor fixed stay at their default value.
  • SimConfig: duration_h (96), dt_h (0.1), culture_volume_l (0.05), max_agents (3000), record_every (10). The volume only converts total masses into g/L.

Methods

Method What it does
study.simulate(seed=None, observer=None, **conditions) A single simulation → SimulationResult.
study.evaluate(replicas=None, **conditions) Several replicas + the objective → Candidate.
study.metrics(names, seed=None, **conditions) Values of several metrics for some conditions.
study.conditions(**override) Default conditions + fixed + whatever you indicate.
study.optimize(...) Full search (below).
Study.from_config(cfg) Builds the study from a dict/JSON (returns (study, options)).

optimize

result = study.optimize(
    method="evolutionary",   # "evolutionary" | "random" | "grid"
    samples=60,              # exploration candidates (random and evolutionary)
    generations=8,           # refinement generations (evolutionary only)
    population=16,           # daughters per generation
    finalists=3,             # candidates that go on to validation
    grid_levels=3,           # levels per variable (grid only)
    sensitivity=True,        # sensitivity report at the end
    out="output",            # output folder (None = write no files)
    unity_sample=200,        # founder cells in the XML
    viz=False,               # True opens the live window
    seconds_per_sim=4.0,     # on-screen duration of each simulation
    fps=45, fullscreen=False,
)

Stages (all printed to the console, and also shown in the window's log):

  1. Baseline: default conditions, for comparison.
  2. Exploration: Latin Hypercube sampling (evolutionary), random (random) or a full grid (grid).
  3. Refinement (evolutionary only): each generation mutates the best candidates with Gaussian noise of decreasing step (starting at 0.15 of the range and shrinking; faster if there is no improvement).
  4. Validation: the best ones are re-evaluated with fresh seeds, so you do not keep a candidate that won by luck. The winner is the best validated one.
  5. Report: effective parameters, single-replica metrics, warnings (population limited by the cap or nutrient left over) and sensitivity to each variable.
  6. Export: CSV, JSON and XML.

It returns a StudyResult:

Field Content
best_conditions winning Conditions
best_value, best_std value and deviation of the objective in validation
baseline_value, improvement_pct baseline and relative improvement
history, validated every candidate evaluated / the validated ones
sensitivity {variable: [(value, objective), …]}
replica_seed seed of the replica that is described and exported
simulations, elapsed_s, out_dir, interrupted run data

result.top(n) returns the n best candidates. If you close the window before it finishes, the study is interrupted and returns the best found so far (interrupted=True).

9. Metrics and objectives

A metric is a number extracted from a simulation. You ask for it by name; some take an argument in square brackets ("yield[ethanol]"); without an argument they use the first product (or, in death_fraction, the cause starvation).

Metric Unit Direction Description
yield[p] g/L max Product accumulated at the end
efficiency[p] g/g max Product per initial nutrient
productivity[p] g/L/h max Yield divided by the duration
specific_production[p] mg/(cell·h) max Product per cell per living hour
peak_population cells max Maximum number of living cells
final_population cells max Living cells at the end
time_to_peak_h h min Hour of the maximum population
total_divisions max Total cell divisions
max_growth_rate 1/h max Maximum specific growth rate (3-point moving average)
doubling_time_h h min ln 2 / maximum rate (the duration if it does not grow)
mean_lifespan_h h max Mean lifespan, from the last birth/division to death
median_lifespan_h h max Median of the above
max_lifespan_h h max Maximum observed (includes cells still alive)
survival_fraction max 1 − deaths / (initial cells + divisions)
total_deaths min Total deaths
extinction_time_h h max Hour the last cell dies (the duration if some survive)
death_fraction[cause] min Fraction of deaths from starvation, age or random
nutrient_consumed_fraction max Fraction of the initial nutrient consumed

Careful with lifespan. It is measured from the last birth or division, so mean_lifespan_h on its own rewards conditions where cells barely divide. Combine it with a constraint (("final_population", ">=", …)) or with survival_fraction.

Custom metrics

@lg.metric("viability", unit="%", direction="max")
def viability(result, cond, ctx):
    return 100 * result.final_alive / max(1, result.peak_alive)

result is the SimulationResult, cond the Conditions, and ctx has .organism and .sim. With workers > 1, define it at module level (outside functions) so it also exists in the child processes. For metrics with an argument, see _factory in lunagen/metrics.py.

Objectives

lg.Objective("yield")                                         # one metric
lg.Objective("doubling_time_h", direction="min")              # minimize
lg.Objective({"yield[cellulose]": 1.0, "survival_fraction": 2.0})   # weighted sum
lg.Objective("yield", constraints=[("final_population", ">=", 100)])
  • In a weighted sum, a negative weight penalizes that metric.
  • A violated constraint subtracts 1000 · (1 + relative violation) (the optimizer avoids it; the candidate is not discarded). Operators: >=, <=, >, <.
  • With violated constraints, the printed values already include the penalty.
  • With Study you can pass a string, a dictionary or an Objective directly.

10. Live window

study.optimize(viz=True) opens a window (requires pygame-ce) and animates every simulation of the search in fast motion. The simulation advances at the pace at which it is drawn.

  • Left: a Petri dish seen from above (every cell, a 2D capsule) and, below it, the real-time telemetry: time, living, dead, divisions, mean reserve, main product, remaining nutrient, causes of death and three charts (with a gray line for the best simulation so far).
  • Right: organism name and overall progress with estimated time, conditions and effective parameters of the simulation in progress, optimizer status and a scrollable log.

What comes from the simulation and what does not. Everything is faithful to the data: who lives, dies (and from what) or divides; the size of the capsule is its phase of the cell cycle; the color, its reserve (red = empty, cyan = full); the constriction line appears past constriction_start; the fibers are the main product actually produced; the amber glow of the medium is the nutrient left. The only decorative thing is the X/Y positions (the model is not spatial): cells move with soft physics and daughters are born next to their mother.

Key Action
SPACE pause
+ / - animation speed
S skip the animation of the current simulation
F animation on/off (the computation keeps going at full speed)
F11 fullscreen
mouse wheel, PgUp, PgDn, Home, End scroll the log
ESC or closing the window quit (the study is interrupted and returns the best found so far)

If pygame-ce is not installed or there is no display, the study runs anyway, console only. With workers > 1, the replicas of every candidate (except the animated one) are computed in parallel while the first one is animated.

11. Files that are generated

With out="output" (the default):

File Content
evaluations.csv every candidate: stage, index, value of each variable, mean and deviation of the objective
results.json summary: best conditions, baseline, improvement, limits, fixed values, simulation, sensitivity, seeds
best_unity.xml the best condition, simulated with replica_seed, ready for Unity

The XML has the root <Simulation> with: <Conditions> (one tag per variable), <Metrics> (yield of each product and standard metrics, with their unit), <Parameters> (the effective parameters, with one <product> per product) and <Founders> (a sample of unity_sample initial cells with length, radius, cycle phase, reserve, fate and what their lineage produced: celluloseMg, ethanolMg…). The schema is a proposal and has to be aligned with the script that reads it.

12. Configuration with JSON and the terminal

A complete study without writing code (only with organisms from presets):

{
  "organism": "komagataeibacter_xylinus",
  "calibration": {"base_uptake_rate": 0.04},
  "objective": "yield[cellulose]",
  "direction": "max",
  "constraints": [["final_population", ">=", 100]],
  "space": {"temperature_c": [24, 34], "ph": [4.5, 6.5]},
  "fixed": {"glucose_gl": 30},
  "sim": {"duration_h": 72},
  "replicas": 3,
  "seed": 12345,
  "workers": 1,
  "optimize": {"samples": 20, "generations": 3, "population": 8, "viz": false, "out": "output_config"}
}

space can be a list of names or a dictionary with limits; objective a string or a dictionary of weights; optimize accepts the same arguments as Study.optimize.

lg.run("config.json")
lunagen config.json            # equivalent to: python -m lunagen config.json
lunagen --metrics              # list the available metrics
lunagen --version

Your own organisms are described in Python (they contain functions); after that you can use them with Study like any other.

13. Calibration

Presets are functions whose arguments are the calibration knobs, all set to the original model's values:

org = lg.presets.komagataeibacter_xylinus(
    base_uptake_rate=0.05,
    growth_ph=(3.5, 5.6, 8.0),                  # (min, optimum, max)
    physiology=lg.Physiology(max_lifespan=48.0),
)
org2 = org.with_params(maintenance_q10=2.2)      # recalibrate loose parameters of a copy
org3 = org.replace(morphology=lg.Morphology(length_um=3.0))

K. xylinus knobs: base_division_interval_h, base_uptake_rate, base_maintenance_rate, maintenance_q10, base_random_death_rate, stress_death_gain, agents_per_inoculum_pct, base_production_rate, production_variation, production_cost_per_unit, production_reserve_threshold, growth_temperature, production_temperature, growth_ph, production_ph, oxygen_half_saturation, oxygen_overload_start, oxygen_overload_penalty, glucose_inhibition, gluconic_threshold_gl, gluconic_ph_drop_per_gl, physiology, morphology.

The effects of the environment on the rates (cardinal curves, oxygen saturation, glucose inhibition…) are model assumptions: all of them are exposed so you can calibrate them with real data. After calibrating, it is wise to keep a test that returns to the original values (see §16).

14. Parallelism and reproducibility

  • Seeds. Everything is deterministic for a given seed. The search uses seed + 1000 + i and the validation seed + 50000 + i. Using the same seeds for every candidate makes the comparison fairer.
  • workers. With workers > 1, processes are used (the spawn method, safe alongside the window). Only data travels between processes (parameters and results), so organisms with lambda functions can be used. The script must have the if __name__ == "__main__": guard.
  • Custom metrics must be defined at module level (see §9).
  • max_agents limits the number of living cells for performance. If the report warns that the population was limited by the cap, the result depends on that cap: consider raising it.

15. API reference

import lunagen as lg

Core

Name Description
lg.Study Simulate, evaluate and optimize (§8).
lg.StudyResult, lg.Candidate Results of optimize and of an evaluation.
lg.run(config) Runs a study from a dict or JSON.
lg.Organism, lg.Variable, lg.Product, lg.Conditions Organism description (§6). Conditions is read-only: c["ph"], c.ph, c.replace(ph=6), c.to_dict().
lg.SimConfig, lg.Physiology, lg.Morphology Simulation, physiology and shape settings.
lg.simulate(params, sim, seed, morphology, observer) Low-level engine; params comes from Organism.derive(cond, sim).
lg.SimulationResult Result of a simulation (below).
lg.presets komagataeibacter_xylinus(...), load(name, **calibration), PRESETS.

Metrics and curves

Name Description
lg.metric, lg.get_metric, lg.available_metrics Register, get and list metrics.
lg.Objective, lg.Metric, lg.Context, lg.report_values Objectives and report utilities.
lg.Cardinal, CPM, Saturation, Inhibition, Q10, Overload, Gaussian, Hill, Linear, Const, Ref, Func, Effect Response curves (§7).

SimulationResult

Field Content
times, alive, dead, nutrient_mg, mean_reserve time series (one point every record_every steps)
product_series {product: accumulated production (mg) over time}
product_mg {product: total produced (mg)}
nutrient_initial_mg, nutrient_final_mg initial and final nutrient (mg)
n_initial, duration_h, volume_l simulation data
peak_alive, time_of_peak_h, final_alive population
total_deaths, deaths_by_cause, total_divisions, capped_steps events
lifespans, survivor_ages, death_times lifespans (h since the last birth/division)
founders record of the initial cells (for Unity)

Observer. simulate(..., observer=f) calls f(StepInfo) at the end of every step with the living cells (uids, reserve, age_frac), that step's deaths and divisions, and the totals. The observer only reads: results are identical with or without it. This is how the window works.

16. Tests

python tests/test_regression.py     # the library == the original model, bit by bit
python tests/test_library.py        # metrics, objectives, validations, several products, parallel, export

The tests ship in the package's source distribution (the .tar.gz on PyPI). test_regression.py compares against tests/legacy_bacillus_model.py (a copy of the original model): 5 conditions × 3 seeds, with parameters, series, founders and totals exactly equal. If you change the engine, this test must keep passing.

17. Limitations and frequently asked questions

  • The X/Y positions in the window are decorative. The model is not spatial.
  • The effects of the environment are assumptions (curves, inhibition, gluconic acid…). Calibrate them with real data before drawing biological conclusions.
  • Lifespan is measured from the last birth or division (see §9).
  • The Unity XML schema is a proposal: align it with your reader.
  • Performance: every simulation takes tens to hundreds of milliseconds; the animation is what takes time (seconds_per_sim, or F to turn it off).
  • Can I use my own organism with lunagen config.json? No: they contain functions. Describe it in Python and use it with Study.
  • Why if __name__ == "__main__":? With several processes, every child process imports the script again; without that guard the study would be launched again.
  • The window does not open. Install pygame-ce (pip install "lunagen[viz]"); if there is no display, the study runs console-only.