Skip to content
SynthStats
SynthStats
ReportBrief
DemonstrationsResearch resultsSupporting resultsHow one program is scored
Dugong growth (length against age)Disease spread (infected count against time, population 50)
Dugong growth programsDisease spread programs

Files

ResourcesSlidesData
Programs

Dugong growth programs

The ten candidate programs for dugong growth: the reference and floor model we wrote, four single attempts and four loop proposals. Each file defines model(data), which is required to return a pm.Model. Each single attempt was written in a separate call to the language model. In the loop, the language model wrote two proposals, received critiques of them, and then wrote two more.

01-reference

import numpy as np
import pymc as pm


def model(data):
    """Dugong length against age: the growth curve and priors of BoxingGym's
    dugongs environment, with a Normal likelihood and a fixed noise scale.
    """
    age = np.asarray(data["age"], dtype=float)
    y = np.asarray(data["length"], dtype=float)
    with pm.Model() as dugongs_model:
        age_data = pm.Data("age_data", age)
        alpha = pm.Normal("alpha", mu=2.0, sigma=0.2)
        beta = pm.Normal("beta", mu=1.5, sigma=0.5)
        lam = pm.Normal("lam", mu=0.4, sigma=0.5)
        mu = alpha - beta * pm.math.abs(lam) ** age_data
        pm.Normal("length", mu=mu, sigma=0.25, observed=y)
    return dugongs_model
Download 01-reference.py

02-floor-model

import numpy as np
import pymc as pm


def model(data):
    """Floor model: ignores age and fits one mean length."""
    y = np.asarray(data["length"], dtype=float)
    with pm.Model() as null_model:
        intercept = pm.Normal("intercept", mu=0.0, sigma=10.0)
        sigma = pm.HalfNormal("sigma", sigma=5.0)
        pm.Normal("length", mu=intercept, sigma=sigma, observed=y)
    return null_model
Download 02-floor-model.py

03-single-attempt-1

import pymc as pm
import numpy as np


def model(data):
    """
    Probabilistic model of dugong length as a function of age.
    
    Uses a von Bertalanffy growth model or similar asymptotic growth curve.
    The von Bertalanffy model is commonly used for animal growth:
    L(t) = L_inf * (1 - exp(-k * (t - t0)))
    
    Parameters:
    - L_inf: asymptotic maximum length
    - k: growth rate coefficient
    - t0: theoretical age at which length is zero (often negative)
    
    We place weakly informative priors on these parameters.
    """
    age = data["age"]
    length = data["length"]
    
    with pm.Model() as dugong_model:
        # Priors for von Bertalanffy parameters
        # L_inf: asymptotic length, must be positive. Based on data, max length is ~2.74
        # So L_inf should be somewhat larger than that.
        L_inf = pm.HalfNormal("L_inf", sigma=2.0)
        
        # k: growth rate, positive
        k = pm.HalfNormal("k", sigma=1.0)
        
        # t0: theoretical age at zero length. Can be negative.
        # We use a Normal prior centered at a small negative value with reasonable spread
        t0 = pm.Normal("t0", mu=-0.5, sigma=0.5)
        
        # Deterministic growth curve
        mu = L_inf * (1 - pm.math.exp(-k * (age - t0)))
        
        # Ensure mu is positive (should be by construction if L_inf > 0 and k > 0)
        # Add likelihood
        sigma = pm.HalfNormal("sigma", sigma=0.2)
        
        length_obs = pm.Normal("length_obs", mu=mu, sigma=sigma, observed=length)
    
    return dugong_model
Download 03-single-attempt-1.py

04-single-attempt-2

import pymc as pm
import numpy as np

def model(data):
    age = data['age']
    length = data['length']
    
    with pm.Model() as dugong_model:
        # Priors for the von Bertalanffy growth parameters
        # L_infinity: asymptotic length
        L_inf = pm.HalfNormal('L_inf', sigma=2)
        
        # k: growth rate
        k = pm.HalfNormal('k', sigma=1)
        
        # Observation noise
        sigma = pm.HalfNormal('sigma', sigma=0.2)
        
        # Expected length
        # Using the von Bertalanffy growth function: L(t) = L_inf * (1 - exp(-k * t))
        mu = L_inf * (1 - pm.math.exp(-k * age))
        
        # Likelihood
        likelihood = pm.Normal('likelihood', mu=mu, sigma=sigma, observed=length)
        
    return dugong_model
Download 04-single-attempt-2.py

05-single-attempt-3

import pymc as pm
import numpy as np

def model(data):
    ages = data['age']
    lengths = data['length']
    
    with pm.Model() as model:
        # Priors for Von Bertalanffy parameters
        # L_inf: asymptotic length. Max observed is ~2.74. 
        # A HalfNormal with sigma=2.0 allows reasonable mass above 2.74.
        L_inf = pm.HalfNormal('L_inf', sigma=2.0)
        
        # k: growth coefficient. Positive.
        k = pm.HalfNormal('k', sigma=1.0)
        
        # t0: theoretical age at length 0. Can be negative.
        t0 = pm.Normal('t0', mu=0.0, sigma=1.0)
        
        # Deterministic growth curve
        mu = L_inf * (1 - pm.math.exp(-k * (ages - t0)))
        
        # Observation noise
        sigma = pm.HalfNormal('sigma', sigma=0.2)
        
        # Likelihood
        length_obs = pm.Normal('length_obs', mu=mu, sigma=sigma, observed=lengths)
        
    return model
Download 05-single-attempt-3.py

06-single-attempt-4

import pymc as pm
import numpy as np

def model(data):
    age = data['age']
    length = data['length']
    
    with pm.Model() as dugong_model:
        # Priors for the von Bertalanffy-like growth model: L = L_max * (1 - exp(-k * age))
        # L_max: Asymptotic maximum length
        # k: Growth rate coefficient
        
        # Based on summary stats:
        # Max length is ~2.74, so L_max should be slightly higher.
        # We use a HalfNormal with a sigma large enough to cover plausible values.
        L_max = pm.HalfNormal('L_max', sigma=2.0)
        
        # k should be positive. A HalfNormal with sigma=1 is a reasonable weakly informative prior.
        k = pm.HalfNormal('k', sigma=1.0)
        
        # Deterministic growth curve
        mu = L_max * (1 - pm.math.exp(-k * age))
        
        # Likelihood
        # sigma is the standard deviation of the observation error
        sigma = pm.HalfNormal('sigma', sigma=0.5)
        
        likelihood = pm.Normal('likelihood', mu=mu, sigma=sigma, observed=length)
        
    return dugong_model
Download 06-single-attempt-4.py

07-loop-proposal-1

import pymc as pm
import numpy as np

def model(data):
    age = data['age']
    length = data['length']
    n_obs = len(age)
    
    with pm.Model() as model:
        # Create data container for age
        age_data = pm.Data('age', age, dims="obs_id")
        
        # Priors for von Bertalanffy growth parameters
        L_inf = pm.HalfNormal('L_inf', sigma=3.0)
        k = pm.HalfNormal('k', sigma=1.0)
        t0 = pm.Normal('t0', mu=0.0, sigma=1.0)
        
        # Expected length at each age
        mu = L_inf * (1 - pm.math.exp(-k * (age_data - t0)))
        
        # Likelihood
        sigma = pm.HalfNormal('sigma', sigma=0.5)
        y_obs = pm.Normal('y_obs', mu=mu, sigma=sigma, observed=length, dims="obs_id")
    
    return model

def gen_model(observed_data):
    built_model = model({column: observed_data[column].to_numpy() for column in observed_data.columns})
    with built_model:
        trace = pm.sample(200, tune=200, target_accept=0.90, chains=1, cores=1, random_seed=42, idata_kwargs={"log_likelihood": True})
        posterior_predictive = pm.sample_posterior_predictive(trace, random_seed=314, return_inferencedata=False)
    return built_model, posterior_predictive, trace
Download 07-loop-proposal-1.py

08-loop-proposal-2

import numpy as np
import pymc as pm

def model(data):
    """
    von Bertalanffy growth model for dugong length as a function of age.
    """
    age = data['age']
    length = data['length']
    
    with pm.Model() as dugong_model:
        # Data containers
        age_data = pm.Data("age", age, dims="obs_id")
        
        # Priors for von Bertalanffy parameters
        # L∞: asymptotic length, should be > max observed length
        L_inf = pm.HalfNormal("L_inf", sigma=3.0)
        
        # k: growth rate, positive
        k = pm.HalfNormal("k", sigma=2.0)
        
        # t0: theoretical age at zero length, can be negative
        t0 = pm.Normal("t0", mu=0.0, sigma=1.0)
        
        # Expected length
        mu = L_inf * (1 - pm.math.exp(-k * (age_data - t0)))
        
        # Observation model
        sigma = pm.HalfNormal("sigma", sigma=0.5)
        y_obs = pm.Normal("y_obs", mu=mu, sigma=sigma, observed=length, dims="obs_id")
    
    return dugong_model

def gen_model(observed_data):
    built_model = model({column: observed_data[column].to_numpy() for column in observed_data.columns})
    with built_model:
        trace = pm.sample(200, tune=200, target_accept=0.90, chains=1, cores=1, random_seed=42, idata_kwargs={"log_likelihood": True})
        posterior_predictive = pm.sample_posterior_predictive(trace, random_seed=314, return_inferencedata=False)
    return built_model, posterior_predictive, trace
Download 08-loop-proposal-2.py

09-loop-proposal-3

import pymc as pm
import numpy as np

def model(data):
    """
    Bayesian von Bertalanffy growth model with Log-Normal likelihood.
    
    This model addresses the critique that a simple Normal likelihood with constant
    variance may not capture the heteroscedasticity and skewness in dugong length data.
    By using a Log-Normal likelihood, we assume the errors are multiplicative,
    which is often more appropriate for biological growth data.
    """
    age = data['age']
    length = data['length']
    
    with pm.Model() as dugong_model:
        # Data container for age
        age_data = pm.Data('age', age, dims="obs_id")
        
        # Priors for von Bertalanffy growth parameters
        # L_inf: Asymptotic length. HalfNormal with sigma=3.0 allows for lengths up to ~6m (3 sigma), 
        # which is biologically plausible for dugongs.
        L_inf = pm.HalfNormal('L_inf', sigma=3.0)
        
        # k: Growth rate coefficient. HalfNormal with sigma=2.0 is weakly informative.
        k = pm.HalfNormal('k', sigma=2.0)
        
        # t0: Theoretical age at zero length. Normal centered at 0 with sigma=1.0.
        # This allows for negative t0 (early growth) or positive t0 (delayed start).
        t0 = pm.Normal('t0', mu=0.0, sigma=1.0)
        
        # Expected length according to von Bertalanffy growth function
        vb_mean = L_inf * (1 - pm.math.exp(-k * (age_data - t0)))
        
        # Ensure vb_mean is positive to take log. 
        # In practice, with reasonable priors and data, this should be positive.
        # We add a small epsilon for numerical stability if needed, but 
        # the model should constrain it naturally.
        mu_log = pm.math.log(vb_mean)
        
        # Observation model: Log-Normal likelihood
        # sigma is the standard deviation on the log scale.
        sigma = pm.HalfNormal('sigma', sigma=0.5)
        
        # Observed likelihood
        y_obs = pm.LogNormal('y_obs', mu=mu_log, sigma=sigma, observed=length, dims="obs_id")
    
    return dugong_model

def gen_model(observed_data):
    # Convert pandas DataFrame to dictionary of numpy arrays
    data_dict = {column: observed_data[column].to_numpy() for column in observed_data.columns}
    
    built_model = model(data_dict)
    with built_model:
        # Sample from posterior
        trace = pm.sample(
            200, 
            tune=200, 
            target_accept=0.90, 
            chains=1, 
            cores=1, 
            random_seed=42, 
            idata_kwargs={"log_likelihood": True}
        )
        
        # Generate posterior predictive samples
        posterior_predictive = pm.sample_posterior_predictive(
            trace, 
            random_seed=314, 
            return_inferencedata=False
        )
        
    return built_model, posterior_predictive, trace
Download 09-loop-proposal-3.py

10-loop-proposal-4

import pymc as pm
import numpy as np

def model(data):
    age = data['age']
    length = data['length']
    
    with pm.Model() as dugong_model:
        # Data containers
        age_data = pm.Data("age", age, dims="obs_id")
        
        # Priors for von Bertalanffy parameters
        L_inf = pm.HalfNormal("L_inf", sigma=3.0)
        k = pm.HalfNormal("k", sigma=2.0)
        t0 = pm.Normal("t0", mu=0.0, sigma=1.0)
        
        # Expected length
        mu = L_inf * (1 - pm.math.exp(-k * (age_data - t0)))
        
        # Heteroscedastic noise model: sigma scales with the mean length
        # alpha is the log-scale intercept, beta is the scaling factor
        alpha = pm.Normal("alpha", mu=0.0, sigma=1.0)
        beta = pm.Normal("beta", mu=0.0, sigma=0.5)
        
        sigma = pm.math.exp(alpha + beta * pm.math.log(mu))
        
        # Likelihood
        y_obs = pm.Normal("y_obs", mu=mu, sigma=sigma, observed=length, dims="obs_id")
    
    return dugong_model

def gen_model(observed_data):
    built_model = model({column: observed_data[column].to_numpy() for column in observed_data.columns})
    with built_model:
        trace = pm.sample(200, tune=200, target_accept=0.90, chains=1, cores=1, random_seed=42, idata_kwargs={"log_likelihood": True})
        posterior_predictive = pm.sample_posterior_predictive(trace, random_seed=314, return_inferencedata=False)
    return built_model, posterior_predictive, trace
Download 10-loop-proposal-4.py
SynthStats team

Disease spread (infected count against time, population 50)

Previous Page

Disease spread programs

Next Page

On this page

Dugong growth programs01-reference02-floor-model03-single-attempt-104-single-attempt-205-single-attempt-306-single-attempt-407-loop-proposal-108-loop-proposal-209-loop-proposal-310-loop-proposal-4