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 conpygameclásico, pero no instales los dos a la vez: ambos se importan comopygamey se estorban.pygame-cesuele publicar versiones precompiladas para los Python nuevos antes quepygame. Sin ninguno o sin pantalla,optimizesigue 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:
- 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. - Muerte (prioridad: hambre, vejez, azar).
Hambre: reserva ≈ 0 durante
starvation_tolerancehoras. Vejez: edad ≥max_lifespan(solo simax_lifespan > 0). Azar: probabilidad1 − exp(−random_death_rate · dt). - Crecimiento.
escala = clip(reserva / (umbral · capacidad), 0, 1); el reloj del ciclo avanzadt · escala(más lento con poca reserva) y cuestagrowth_cost · dt · escala. Con la población llena (max_agents) no empiezan a crecer las células que aún no iniciaron el estrangulamiento. - Productos. Para cada producto:
masa = rate · factor_individual · escala_prod · dt, y se gastamasa · costde reserva (nunca más reserva de la que hay). - 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)]
)
spaceadmite 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 enfixed.- Las variables que no están ni en
spaceni enfixedse 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):
- Línea base: condiciones por defecto, para comparar.
- Exploración: muestreo Latin Hypercube (
evolutionary), aleatorio (random) o malla completa (grid). - 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). - 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.
- 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.
- 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_hpor sí sola premia condiciones donde las células casi no se dividen. Combínala con una restricción (("final_population", ">=", …)) o consurvival_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
Studypuedes pasar directamente un texto, un diccionario o unObjective.
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 + iy la validaciónseed + 50000 + i. Usar las mismas semillas para todas las candidatas hace la comparación más justa. workers. Conworkers > 1se usan procesos (métodospawn, seguro junto a la ventana). Solo viajan datos entre procesos (parámetros y resultados), así que se pueden usar organismos con funcioneslambda. El script debe llevar el resguardoif __name__ == "__main__":.- Las métricas propias deben estar definidas a nivel de módulo (ver §9).
max_agentslimita 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, oFpara desactivarla). - ¿Puedo usar un organismo propio con
lunagen config.json? No: contienen funciones. Descríbelo en Python y úsalo conStudy. - ¿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 classicpygame, but do not install both at once: they are both imported aspygameand get in each other's way.pygame-ceusually publishes prebuilt packages for new Python versions beforepygamedoes. Without either one, or without a display,optimizestill 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:
- 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. - Death (priority: starvation, old age, chance).
Starvation: reserve ≈ 0 for
starvation_tolerancehours. Old age: age ≥max_lifespan(only ifmax_lifespan > 0). Chance: probability1 − exp(−random_death_rate · dt). - Growth.
scale = clip(reserve / (threshold · capacity), 0, 1); the cycle clock advancesdt · scale(slower with a low reserve) and costsgrowth_cost · dt · scale. With a full population (max_agents), cells that have not yet started constriction do not start growing. - Products. For each product:
mass = rate · individual_factor · product_scale · dt, andmass · costof reserve is spent (never more reserve than is available). - 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)]
)
spaceaccepts a list of names (using the organism's limits) or a dictionary to change the limits. If omitted, every variable not infixedis optimized.- Variables that are in neither
spacenorfixedstay 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):
- Baseline: default conditions, for comparison.
- Exploration: Latin Hypercube sampling (
evolutionary), random (random) or a full grid (grid). - Refinement (
evolutionaryonly): 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). - 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.
- Report: effective parameters, single-replica metrics, warnings (population limited by the cap or nutrient left over) and sensitivity to each variable.
- 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_hon its own rewards conditions where cells barely divide. Combine it with a constraint (("final_population", ">=", …)) or withsurvival_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
Studyyou can pass a string, a dictionary or anObjectivedirectly.
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 + iand the validationseed + 50000 + i. Using the same seeds for every candidate makes the comparison fairer. workers. Withworkers > 1, processes are used (thespawnmethod, safe alongside the window). Only data travels between processes (parameters and results), so organisms withlambdafunctions can be used. The script must have theif __name__ == "__main__":guard.- Custom metrics must be defined at module level (see §9).
max_agentslimits 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, orFto turn it off). - Can I use my own organism with
lunagen config.json? No: they contain functions. Describe it in Python and use it withStudy. - 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.