Ecology & organismal biology kernels
Population-dynamics and community-diversity formulas for general ecology, plus five taxon-focused libraries -- ecophysiology, entomology, microbiology, mycology, and bryology -- covering organism-envi
Seven modules for biology above the single-cell/single-gene level and below the scale of climate or geology: how a population of organisms grows, competes, and gets counted (ecology, population), how one organism's body responds to its temperature, oxygen, and water environment (ecophysiology), and four taxon-specific libraries for insects, bacteria, fungi, and mosses (entomology, microbiology, mycology, bryology) that each pair a growth-kinetics model with the taxon's own field-measurement conventions -- degree-days and trap counts for insects, colony-forming units and D/Z-values for bacteria, spore dispersal and disease-progress curves for fungi, water-holding capacity and peat accumulation for mosses. population is worth calling out by name: despite the word's overload elsewhere in this corpus (the age-structured, actuarial sense used by demographic and finance modules), this module's ten functions are exclusively ecological -- logistic and Lotka-Volterra predator-prey simulation, two-species competition, extinction risk against a threshold -- and never touch birth/death-rate tables, life expectancy, or any human-demographic quantity.
from algenta import MojoRuntime
runtime = MojoRuntime(mode="local")
# ecology.logistic_growth returns an instantaneous RATE: r=0.5, N=50, K=100
runtime.execute("ecology", "logistic_growth", [0.5, 50.0, 100.0])["result"]
# 12.5 -- dN/dt at N=50 halfway to carrying capacity K=100
# population.logistic_growth takes the same three numbers in a different order
# and returns the population AFTER one full step, not a rate
runtime.execute("population", "logistic_growth", [50.0, 0.1, 100.0])["result"]
# 52.5 -- N + r*N*(1 - N/K) = 50 + 0.1*50*0.5 = 52.5, the next generation's countThe modules
ecology
Community-ecology math: logistic and exponential population growth rates, a (buggy -- see below) carrying-capacity estimator, the Lotka-Volterra predator and prey rate equations, Margalef/Shannon/Simpson diversity indices, and Jaccard/Sørensen community-similarity coefficients, plus the species-area relationship.
population
A discrete-time population-dynamics simulation engine: single-step and multi-step logistic growth, single-step and multi-step Lotka-Volterra predator-prey, two-species Lotka-Volterra competition, a heuristic carrying-capacity/growth-rate estimator from raw counts, doubling time, and a boolean extinction-risk check. Purely ecological -- see the callout above.
ecophysiology
Whole-organism physiology as a function of its environment: Kleiber's-law metabolic scaling, thermal conductance, the Q10 temperature coefficient, evaporative water loss, aerobic scope, hypoxia tolerance (Pcrit), osmoregulation cost, cardiac output from the Fick principle, respirometric oxygen consumption, and field metabolic rate from the doubly-labelled-water method.
entomology
Insect population and pest-management math: degree-day accumulation for phenology models, exponential population growth, net reproductive rate and generation time, a Simpson diversity variant, trap-catch and infestation indices, biocontrol efficacy, larval mortality, and a mass/temperature insect metabolic-rate model.
microbiology
Bacterial growth kinetics and thermal-death (food-safety/sterilization) math: exponential growth, doubling time, Monod substrate-limited growth, generation time from two counts, colony-forming-unit back-calculation, a Kirby-Bauer MIC estimate, a biofilm-formation index, first-order thermal inactivation, and the D-value/Z-value pair from canning and sterilization theory.
mycology
Fungal growth and plant-disease epidemiology: a Gaussian-plume spore-dispersal estimate, mycelial radial growth rate, ergosterol-based biomass, sporulation density, infection efficiency, a thermal latent-period model, Vanderplank's logistic disease-progress rate, a temperature/humidity spore-germination probability, colony diameter, and a Lotka-Volterra competitive-exclusion index.
bryology
Moss, liverwort, and hornwort science, heavily weighted toward Sphagnum and peatland biogeochemistry: water-holding capacity, specific growth rate, peatland carbon accumulation, desiccation-recovery kinetics, a nitrogen-deposition growth response, a light/water-stress photosynthesis model, peat accumulation rate, an acidity estimate, patch-colonisation probability, and a biomass-to-litter turnover ratio.
ecology: community diversity, similarity, and population rate equations
logistic_growth(r, n, k)
The instantaneous logistic rate dN/dt = r*n*(1 - n/k); 0.0 if |k| is below 1e-30.
exponential_growth(n0, r, t)
n0 * exp(r*t).
carrying_capacity_estimate(n_max, r, n0, t)
Attempts to invert the logistic-growth equation for K given an observed maximum, an initial size, a growth rate, and elapsed time. Its own docstring and its own code disagree -- see below.
lotka_volterra_prey(r, n, alpha, p)
dN/dt = r*n - alpha*n*p.
lotka_volterra_predator(beta, n, p, delta)
dP/dt = beta*n*p - delta*p.
species_richness_margalef(s, n)
(S - 1) / ln(N); 0.0 if ln(N) is within 1e-30 of zero (i.e. N = 1).
shannon_diversity(proportions_sum_plogp)
Negates a caller-precomputed Σ(pi * ln(pi)).
simpson_index(proportions_sum_p2)
Passes a caller-precomputed Σ(pi²) straight through.
simpson_diversity(simpson_d)
1 - D.
jaccard_similarity(shared, total_a, total_b)
shared / (A + B - shared); 0.0 if the denominator is within 1e-30 of zero.
sorensen_similarity(shared, total_a, total_b)
2*shared / (A + B); 0.0 if the denominator is within 1e-30 of zero.
species_area_relationship(c, area, z)
c * area^z.
mojo/tests/mojo/test_wave70.mojo (shared with acoustics and textile modules in that wave) pins exact numeric assertions for all twelve functions. Two worked from that file: logistic_growth(0.5, 50.0, 100.0) = 12.5 (used in the intro example above), and lotka_volterra_predator(0.02, 100.0, 10.0, 0.5) = 0.02*100*10 - 0.5*10 = 20 - 5 = 15.0 -- with zero prey (lotka_volterra_predator(0.02, 0.0, 10.0, 0.5)), the predator term vanishes and the population declines at exactly -delta*p = -5.0.
A real bug: carrying_capacity_estimate multiplies by an extra factor its own docstring doesn't have
The docstring states the formula as K = N_max / (1 - (1 - N_max/N0)*exp(-r*t)). Expanding 1 - (1 - N_max/N0) as (N_max/N0 - 1) shows this is exactly N_max / denom, where denom = 1 + (ratio - 1)*exp(-r*t) and ratio = N_max/N0. But the actual code is:
-- an extra * ratio that the docstring's own stated formula does not include. Running the numbers independently confirms it: for n_max=500, r=0.3, n0=50, t=10 (ratio = 10), the code returns 3452.84, while the docstring's own formula evaluates to 345.28 for the same inputs -- exactly ratio apart, every time. Neither number is the textbook answer, either: numerically inverting the actual continuous-time logistic solution N(t) = K / (1 + ((K-N0)/N0)*exp(-r*t)) for K at these same inputs (via root-finding, independently, in plain Python) gives K ≈ 946.18 -- a third value, different from both. mojo/tests/mojo/test_wave70.mojo::test_carrying_capacity_estimate only checks v > 0.0 for this exact input and separately checks the n0 ≈ 0 fallback branch, so none of this is caught by the existing test suite. A caller relying on this function for an actual carrying-capacity estimate is getting a number that matches neither its own documented formula nor the logistic equation it claims to invert. population.carrying_capacity_estimate (below) sidesteps the whole derivation with a much simpler heuristic that has no such discrepancy.
population: a discrete-time simulation engine, not a demographic model
exponential_growth(N0, r, t)
N0 * exp(r*t) -- identical formula to ecology.exponential_growth.
logistic_growth(N, r, K)
One discrete Euler step, N' = N + r*N*(1 - N/K), dt = 1 -- the population after the step, not a rate, and with no guard at all against K = 0 (unlike ecology.logistic_growth's |k| < 1e-30 check).
logistic_simulate(N0, r, K, n_steps)
Runs logistic_growth n_steps times, returning a list of n_steps + 1 values (the initial condition plus one per step); any step that goes negative is clamped to 0.0.
lotka_volterra_step(prey, pred, alpha, beta, delta, gamma, dt)
One Euler step of both predator-prey equations at once (dx/dt = alpha*x - beta*x*y, dy/dt = delta*x*y - gamma*y); returns [prey', pred'], each clamped at 0.0.
lotka_volterra_simulate(prey0, pred0, alpha, beta, delta, gamma, dt, n_steps)
Runs lotka_volterra_step n_steps times, returning n_steps + 1 [prey, pred] rows.
carrying_capacity_estimate(populations)
max(populations) * 1.1 -- a flat 10% headroom heuristic over a list of observed counts, 0.0 on an empty list.
growth_rate_estimate(N_t, N_t1, K)
Inverts one logistic step for r: (N_t1 - N_t) / (N_t*(1 - N_t/K)); 0.0 if that denominator is exactly 0.0.
competition_model(N1, N2, r1, r2, K1, K2, alpha12, alpha21)
One Euler step of two-species Lotka-Volterra competition (dN1/dt = r1*N1*(1 - (N1 + alpha12*N2)/K1), symmetric for N2); returns [N1', N2'], each clamped at 0.0.
population_doubling_time(r)
ln(2) / r; no guard for r = 0.
extinction_risk(population, threshold)
population < threshold, a plain Bool.
mojo/tests/mojo/test_wave54.mojo (shared with a logic-circuits suite and a hash-chain suite in that wave) exercises all ten functions with real assertions, though several are range checks rather than exact-value pins: logistic_simulate(10.0, 0.5, 100.0, 50) is asserted to return 51 entries ending between 90 and 110 (converging near K=100, not pinned to a specific decimal), and lotka_volterra_step/competition_model are asserted only to stay positive. Two are exact: exponential_growth(100.0, 0.1, 10.0) ≈ 271.828 and population_doubling_time(0.693147) ≈ 1.0. The carrying_capacity_estimate case is pinned exactly: four observed populations [10, 50, 90, 80] give max = 90, and 90 * 1.1 = 99.0, matching the test.
ecophysiology: how one organism's body meets its environment
metabolic_rate_kleiber(mass_kg)
Kleiber's law, 70 * mass^0.75 (kcal/day); 0.0 if mass_kg <= 0.
thermal_conductance(heat_loss, t_body, t_ambient)
heat_loss / (T_body - T_ambient); 0.0 if the temperatures are equal.
q10_temperature_coefficient(rate1, rate2, t1, t2)
(rate2/rate1) ^ (10 / (t2 - t1)); 0.0 if either rate is non-positive or t1 == t2.
evaporative_water_loss(surface_area, permeability, vpd)
surface_area * permeability * vpd; 0.0 on any negative input.
aerobic_scope(max_metabolic_rate, resting_metabolic_rate)
MMR - RMR, clamped at 0.0 (never negative).
hypoxia_tolerance(resting_mr, mass_specific_o2)
resting_mr / mass_specific_o2, a critical-oxygen (Pcrit) estimate; 0.0 if the coefficient is non-positive.
osmoregulation_cost(osmotic_gradient, membrane_permeability, body_mass)
gradient * permeability * body_mass; 0.0 if gradient or permeability is negative (body_mass itself is unguarded).
heart_rate_fick(vo2, o2_arterial, o2_venous)
Cardiac output from the Fick principle, vo2 / ((CaO2 - CvO2) * 10); 0.0 if the arteriovenous difference is non-positive.
oxygen_consumption_rate(delta_o2_ppm, flow_rate_l_h, mass_kg)
(delta_o2_ppm * 1e-3) * flow_rate_l_h / mass_kg; 0.0 if mass_kg <= 0.
field_metabolic_rate(mass_kg, taxon_a, taxon_b)
taxon_a * mass_kg^taxon_b, a doubly-labelled-water allometric equation; 0.0 if mass_kg <= 0 or taxon_a <= 0.
None of ecophysiology's ten functions has a real pinned test -- mojo/tests/libs/test_ecophysiology.mojo is the auto-generated smoke suite that calls each function once with the literal argument 0.5 and checks only that the result is finite. Every number below was hand-computed independently from mojo/engine/ecophysiology.mojo's formulas, in plain Python, and two of them land on textbook reference values:
heart_rate_fick's * 10.0 divisor is a real unit conversion, not an arbitrary constant: VO2 is in mL O2/min and the arteriovenous difference is in mL O2 per 100 mL blood, so the raw ratio comes out in units of 100 mL blood/min; multiplying the denominator by 10 converts that to L/min in one step (x / 100 * 1000 = x / 10). Checked independently against the standard clinical Fick-equation form (CO [L/min] = VO2 [mL/min] / ((CaO2 - CvO2) * 10)), the conversion is correct.
entomology: pest phenology and insect population statistics
degree_days(t_max, t_min, t_base)
max(0, (Tmax+Tmin)/2 - Tbase) -- single-day heat-unit accumulation for phenology models.
accumulated_degree_days(daily_dd, days)
daily_dd * days.
population_growth_intrinsic(n0, r, t)
n0 * exp(r*t) -- the same formula shared by ecology.exponential_growth, population.exponential_growth, and microbiology.exponential_growth, under a fourth name here.
net_reproductive_rate(fecundity, survival)
fecundity * survival (R0).
generation_time(r0, r)
ln(R0) / r; no guard against r = 0 or R0 <= 0.
doubling_time_population(r)
ln(2) / r; no guard against r = 0.
species_diversity_simpson(p_squared_sum)
1 - Σ(pi²) -- the same one-line formula as ecology.simpson_diversity, under a different name, taking the sum directly rather than via a separate index step.
trap_catch_index(n_caught, n_traps, n_days)
n_caught / (n_traps * n_days); no zero-denominator guard.
infestation_rate(infested_units, total_units)
infested / total * 100; no guard.
biocontrol_efficacy(treated_pop, control_pop)
(control - treated) / control * 100; no guard.
larval_mortality_rate(initial_larvae, surviving_larvae)
(initial - surviving) / initial * 100; no guard.
metabolic_rate_insect(mass_mg, temperature_c)
0.01 * mass^0.75 * exp(0.07*T), computed via exp(0.75*ln(mass)); no guard against mass_mg <= 0 (would produce a NaN from ln of a non-positive number).
mojo/tests/mojo/test_wave89.mojo (shared with structural-engineering and perfumery suites in that wave) pins all twelve functions with 25 total assertions. Worked from that file: population_growth_intrinsic(100.0, 0.1, 10.0) ≈ 271.8282 (100 * e^1), generation_time(100.0, 0.1) ≈ 46.0517 (ln(100)/0.1), and biocontrol_efficacy(30.0, 100.0) = 70.0 -- 100 untreated ("control") insects against 30 surviving treated ones is 70% control efficacy.
microbiology: growth kinetics and thermal-death (food-safety) math
exponential_growth(n0, growth_rate, time)
n0 * exp(growth_rate*time); returns n0 unchanged if any argument is negative.
doubling_time(growth_rate)
ln(2)/growth_rate; 0.0 if growth_rate <= 0.
monod_growth_rate(mu_max, substrate, ks)
mu_max * S / (Ks + S); 0.0 if the denominator is non-positive.
bacterial_generation_time(n_initial, n_final, elapsed_h)
elapsed_h / log2(n_final/n_initial); 0.0 unless n_initial > 0, n_final > n_initial, elapsed_h > 0, and the resulting generation count is positive.
colony_forming_units(colony_count, dilution_factor, volume_ml)
colony_count / (dilution_factor * volume_ml); 0.0 if either factor is non-positive.
minimum_inhibitory_conc(inhibition_zone_mm, disk_conc_ug)
Kirby-Bauer estimate: disk_conc / (zone_mm - 6) if zone_mm > 6, else disk_conc unchanged ("no inhibition at disc edge").
biofilm_formation_index(od_sample, od_control)
od_sample / od_control; 0.0 if the control is non-positive.
inactivation_first_order(n0, inactivation_rate, time)
n0 * exp(-inactivation_rate*time); returns n0 unchanged if any argument is negative.
d_value(inactivation_rate)
ln(10) / inactivation_rate, the decimal reduction time; 0.0 if the rate is non-positive.
z_value(d1, d2, t1, t2)
(t2-t1) / ln(d1/d2) * ln(10) -- algebraically equal to the standard (t2-t1) / log10(d1/d2); 0.0 if either D-value is non-positive or they're equal.
Like ecophysiology, none of these ten has a real pinned test -- mojo/tests/libs/test_microbiology.mojo is the auto-generated finite-check smoke suite. Hand-verified against mojo/engine/microbiology.mojo, with one exact-round-number example chosen specifically to be independently checkable:
mycology: fungal growth and plant-disease epidemiology
spore_dispersal_distance(release_height, wind_speed, settling_velocity)
wind_speed * release_height / settling_velocity, a simplified ballistic (no-turbulence) dispersal model; 0.0 if settling velocity is non-positive.
mycelial_growth_rate(colony_diam_t2, colony_diam_t1, time_days)
(D2 - D1) / (2*days) -- radial extension rate from two diameter measurements; 0.0 if days <= 0.
fungal_biomass(ergosterol_ug_g, conversion_factor)
ergosterol / conversion_factor; 0.0 if the factor is non-positive.
sporulation_index(spore_count, colony_area_cm2)
spore_count / colony_area_cm2; 0.0 if the area is non-positive.
infection_efficiency(successful_infections, spores_deposited)
successful / deposited; 0.0 if deposited is non-positive.
latent_period(temperature, t_min, t_opt)
1 / ((T-Tmin)*(Topt-T)), a symmetric-parabola thermal response; returns a sentinel 1e9 ("no growth") outside (Tmin, 2*Topt - Tmin).
epidemic_progress_rate(x1, x2, t1, t2)
Vanderplank's logistic disease-progress rate, (ln(x2/(1-x2)) - ln(x1/(1-x1))) / (t2-t1); 0.0 if either disease-intensity fraction is outside (0, 1) or t2 <= t1.
spore_germination_prob(temperature, t_opt, relative_humidity)
A Gaussian temperature response times a linear humidity requirement; 0.0 below 80% RH, otherwise clamped to [0, 1].
colony_diameter(growth_rate_mm_day, time_days)
2 * rate * days -- diameter from a constant radial rate, growing from a near-zero inoculum; 0.0 on negative input.
competitive_exclusion_index(alpha12, alpha21)
alpha12 * alpha21 -- a Lotka-Volterra competitive-exclusion ratio (< 1 coexistence, > 1 exclusion).
mycology's ten functions likewise have only the auto-generated smoke suite (mojo/tests/libs/test_mycology.mojo). Hand-verified worked examples:
mycelial_growth_rate and colony_diameter are inverses in spirit but not interchangeable in practice: mycelial_growth_rate measures a rate from two diameters already recorded at different times, while colony_diameter predicts a diameter forward from a constant rate assuming growth started from a vanishingly small inoculum at t=0 -- calling colony_diameter with a rate measured between two non-zero diameters silently assumes the colony started at zero, which the two measurements it was derived from did not.
bryology: moss, liverwort, and peatland biogeochemistry
sphagnum_water_holding(dry_mass_g, saturated_mass_g)
(W_sat - W_dry) / W_dry; 0.0 if dry mass is non-positive.
moss_growth_rate(initial_mass_g, final_mass_g, days)
ln(m_final/m_initial) / days; 0.0 if any mass or days is non-positive.
carbon_accumulation_peatland(npp_g_c_m2_yr, decomposition_g_c_m2_yr)
NPP - decomposition; unguarded, can go negative (net carbon loss).
bryophyte_desiccation_recovery(time_h, k_recovery, max_recovery)
max_recovery * (1 - exp(-k_recovery*time_h)).
nitrogen_deposition_effect(n_deposition_kg_ha_yr, optimal_n_kg_ha_yr)
1 - |N - N_opt| / N_opt, clamped at 0.0; 0.0 if N_opt <= 0.
moss_photosynthesis_rate(par_umol_m2_s, pmax, half_sat_par, water_potential_mpa)
Michaelis-Menten light response times an exp(ψ/2) water-stress factor (clamped at 1.0); 0.0 if the light denominator is non-positive.
peat_accumulation_rate(productivity_mm_yr, humification_factor)
productivity * (1 - humification_factor).
sphagnum_acidity(cation_exchange_capacity, base_saturation)
3.0 + 2.0 * base_saturation / 100 -- cation_exchange_capacity is accepted but never used, see below.
patch_colonisation_probability(spore_rain, substrate_suitability, moisture_factor)
Product of the three factors, clamped at 1.0.
biomass_to_litter_ratio(standing_biomass_g_m2, annual_litter_g_m2_yr)
biomass / litter_production, a mean-residence-time estimate (years); 0.0 if litter production is non-positive.
Only the auto-generated smoke suite exists here too (mojo/tests/libs/test_bryology.mojo). Worked examples, hand-computed from mojo/engine/bryology.mojo:
A dead parameter: sphagnum_acidity ignores its own cation-exchange-capacity argument
The docstring frames this function as estimating "pH from Sphagnum cation exchange characteristics," and the signature takes cation_exchange_capacity as its first argument. Reading the body directly:
cation_exchange_capacity never appears in the computation. sphagnum_acidity(1.5, 25.0) and sphagnum_acidity(2.5, 25.0) return the identical 3.5 -- only base_saturation moves the result, no matter what CEC value is passed. This is consistent with moe_routing.expert_choice_gate's unused token_score argument documented on the mixture-of-experts and routing kernels page: a parameter that is present in the signature and the docstring's framing, but plays no part in the number the function actually returns.
Repeated formulas across module boundaries
Several formulas in this family appear more than once, under different module names, sometimes with different parameter orders and always worth checking before assuming a name means the same thing everywhere:
Exponential growth (
N0 * exp(r*t)) appears under that exact formula inecology,population, andmicrobiology, and under the namepopulation_growth_intrinsicinentomology-- four modules, one formula.logistic_growthis the same name inecologyandpopulationfor two different things: an instantaneous ODE rate inecology(argumentsr, n, k) versus the population after one discrete Euler step inpopulation(argumentsN, r, K) -- see the intro example at the top of this page.carrying_capacity_estimateis likewise the same name in both modules, for two very different approaches:ecology's attempt at an analytic logistic-model inversion (which has the bug described above) versuspopulation's much simplermax(observed) * 1.1heuristic over a list of samples.Simpson diversity (
1 - Σpi²) appears asecology.simpson_diversity(taking a precomputedD) and asentomology.species_diversity_simpson(takingΣpi²directly, skipping the intermediateDstep) -- the same one-line formula, two names, one fewer argument-passing step in the entomology version.Lotka-Volterra competition coefficients (
alpha12/alpha21) show up at two different granularities:mycology.competitive_exclusion_indexreduces them to a single ratio (alpha12 * alpha21), whilepopulation.competition_modeluses the same two coefficients inside a full two-species discrete simulation step.
Verification
Three of these seven modules have real, hand-pinned numeric test suites, not just coverage smoke tests: ecology (mojo/tests/mojo/test_wave70.mojo, shared with acoustics and textile), entomology (mojo/tests/mojo/test_wave89.mojo, shared with structural engineering and perfumery), and population (mojo/tests/mojo/test_wave54.mojo, shared with logic circuits and hash chains) -- together covering all 32 functions across those three modules with real expected values, though population's simulation-returning functions (logistic_simulate, lotka_volterra_step, lotka_volterra_simulate, competition_model) are asserted on bounds and shape (positivity, list length, convergence range) rather than pinned to an exact decimal in every case. The other four modules -- ecophysiology, microbiology, mycology, bryology -- have only the auto-generated mojo/tests/libs/test_*.mojo smoke suites: each calls every function once with the literal argument 0.5 and checks only that the result is finite, with no numeric value pinned anywhere. Every worked example on this page for those four modules was hand-computed independently from the real formulas in mojo/engine/{ecophysiology,microbiology,mycology,bryology}.mojo, and where a textbook reference value exists (Q10 = 2.0 for a rate-doubling response, Monod growth at exactly half of mu_max when substrate = Ks, a Z-value of exactly 10.0 for a D-value that drops tenfold over a 10°C rise), the computed number was checked against it. The ecology.carrying_capacity_estimate bug and the bryology.sphagnum_acidity dead parameter were each confirmed independently -- by direct algebraic expansion and, for the carrying-capacity case, by numerically solving the actual logistic equation for K with scipy.optimize.brentq -- rather than asserted from a single read of the source.
Next: see Epidemiology and disease dynamics kernels for the same compartmental/rate-equation style of modeling applied to disease spread instead of species populations, or browse the Runtime library catalog for every module and function.
Last updated
Was this helpful?