Source code for igloo_weta.constants

#!/usr/bin/env python3
"""
 ┌─────────────────────────────────────────────────────────────────────┐
 │  CONSTANTS                             « the numbers that matter »  │
 └─────────────────────────────────────────────────────────────────────┘

Every assumption, physical constant, material property, species
measurement, and default configuration in one file.  When a reviewer
asks "where did that number come from?" — point them here.

All values carry their source and plausible range so sensitivity
analyses can sweep them systematically.

Import what you need::

    from igloo_weta.constants import STONE, WOOD, SPECIES, SIM

Or grab a specific value::

    from igloo_weta.constants import STONE
    print(STONE.rho)          # 2650.0 kg/m³
    print(STONE.rho_range)    # (2500.0, 2800.0)
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional


# ╔═══════════════════════════════════════════════════════════════════════╗
# ║  MATERIAL PROPERTIES                   « what stuff is made of »    ║
# ╚═══════════════════════════════════════════════════════════════════════╝

[docs] @dataclass(frozen=True) class MaterialProperties: """Thermophysical properties of a burrow wall material. Attributes: name: Human-readable material name. rho: Density [kg/m³]. rho_range: Plausible density range [kg/m³]. c: Specific heat capacity [J/(kg·K)]. c_range: Plausible specific heat range [J/(kg·K)]. k_thermal: Thermal conductivity [W/(m·K)]. k_thermal_range: Plausible conductivity range [W/(m·K)]. source: Literature reference for these values. """ name: str rho: float rho_range: tuple[float, float] c: float c_range: tuple[float, float] k_thermal: float k_thermal_range: tuple[float, float] source: str @property def alpha(self) -> float: """Thermal diffusivity α = k / (ρ·c) in m²/s.""" return self.k_thermal / (self.rho * self.c)
STONE = MaterialProperties( name="Greywacke / schist (Otago)", rho=2650.0, rho_range=(2500.0, 2800.0), c=840.0, c_range=(780.0, 900.0), k_thermal=2.5, k_thermal_range=(1.5, 3.5), source=( "Greywacke typical values. Robertson (1988) Engineering Geology " "of the NZ Greywacke; Clauser & Huenges (1995) Thermal " "conductivity of rocks and minerals, AGU Ref Shelf 3." ), ) """Properties of Otago greywacke/schist stone for rock burrows.""" WOOD = MaterialProperties( name="Native hardwood (mahoe / ngaio / manuka)", rho=550.0, rho_range=(400.0, 700.0), c=1700.0, c_range=(1400.0, 2000.0), k_thermal=0.15, k_thermal_range=(0.10, 0.25), source=( "Wood handbook (Forest Products Lab, USDA 2010). NZ native " "hardwoods: mahoe (Melicytus ramiflorus) ~500 kg/m³, ngaio " "(Myoporum laetum) ~550 kg/m³, manuka (Leptospermum) ~600 kg/m³. " "Thermal conductivity across grain 0.10–0.25 W/(m·K)." ), ) """Properties of native NZ hardwood for tree gallery burrows.""" # ┌─────────────────────────────────────────────────────────────────────┐ # │ AIR « the stuff in the burrow » │ # └─────────────────────────────────────────────────────────────────────┘
[docs] @dataclass(frozen=True) class AirProperties: """Thermophysical properties of air at field-relevant temperatures. Attributes: rho: Density at ~15 °C [kg/m³]. c: Specific heat at constant pressure [J/(kg·K)]. source: Reference. """ rho: float c: float source: str
AIR = AirProperties( rho=1.225, c=1005.0, source="Standard atmosphere at 15 °C, 1 atm.", ) """Air properties at typical field temperature.""" # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ SPECIES MORPHOMETRICS « the animals themselves » ║ # ╚═══════════════════════════════════════════════════════════════════════╝
[docs] @dataclass(frozen=True) class SpeciesInfo: """Morphometric and ecological data for one wētā species. Attributes: name: Binomial species name. common_name: Common name. body_length_mm: Typical adult body length [mm]. body_length_range: Range of body lengths [mm]. body_width_mm: Typical adult body width [mm]. body_width_range: Range of body widths [mm]. mass_g: Mean adult body mass [g] (from our data). mass_range_g: Range of masses in our dataset [g]. mass_n: Sample size for mass measurements. burrow_type: ``"stone"`` or ``"wood"``. gallery_entrance_mm: Gallery entrance diameter [mm]. gallery_entrance_range: Entrance diameter range [mm]. gallery_volume_cm3: Estimated single-occupant gallery volume [cm³]. gallery_volume_range: Volume range [cm³]. gallery_SA_cm2: Estimated inner surface area [cm²]. gallery_SA_range: Surface area range [cm²]. gallery_wall_mm: Estimated wall thickness [mm]. gallery_wall_range: Wall thickness range [mm]. gallery_source: Reference for gallery dimensions. wall_material: Reference to material: ``STONE`` or ``WOOD``. field_site: Where the temperature data were collected. notes: Additional ecological notes. """ name: str common_name: str # ── body dimensions ────────────────────────────────────────────── body_length_mm: float body_length_range: tuple[float, float] body_width_mm: float body_width_range: tuple[float, float] mass_g: float mass_range_g: tuple[float, float] mass_n: int # ── burrow / gallery ───────────────────────────────────────────── burrow_type: str gallery_entrance_mm: float gallery_entrance_range: tuple[float, float] gallery_volume_cm3: float gallery_volume_range: tuple[float, float] gallery_SA_cm2: float gallery_SA_range: tuple[float, float] gallery_wall_mm: float gallery_wall_range: tuple[float, float] gallery_source: str wall_material: str # "stone" or "wood" # ── field context ──────────────────────────────────────────────── field_site: str notes: str
H_MAORI = SpeciesInfo( name="Hemideina maori", common_name="Mountain stone wētā", body_length_mm=45.0, body_length_range=(30.0, 55.0), body_width_mm=14.0, body_width_range=(10.0, 18.0), mass_g=6.27, mass_range_g=(3.09, 8.56), mass_n=17, burrow_type="stone", gallery_entrance_mm=20.0, gallery_entrance_range=(12.0, 30.0), gallery_volume_cm3=500.0, gallery_volume_range=(188.0, 3071.0), gallery_SA_cm2=1200.0, gallery_SA_range=(663.0, 2664.0), gallery_wall_mm=10.0, gallery_wall_range=(5.0, 30.0), gallery_source=( "Photogrammetric foam casts of Rock and Pillars stone burrows " "(this study). Volume and SA are the cavity dimensions. Wall " "thickness estimated as 1 cm stone shingle." ), wall_material="stone", field_site="Rock and Pillars, Otago, NZ (~1428 m asl)", notes=( "Lives above the treeline in stone shingle crevices and " "cavities. Abandoned arboreal life millions of years ago. " "Rock burrows range from small single-animal crevices to " "large multi-chamber shingle piles." ), ) """*H. maori* — the alpine species with stone burrows (this study).""" H_THORACICA = SpeciesInfo( name="Hemideina thoracica", common_name="Auckland tree wētā", body_length_mm=40.0, body_length_range=(30.0, 45.0), body_width_mm=12.0, body_width_range=(9.0, 15.0), mass_g=3.32, mass_range_g=(2.16, 4.50), mass_n=9, burrow_type="wood", gallery_entrance_mm=15.0, gallery_entrance_range=(12.0, 18.0), gallery_volume_cm3=45.0, gallery_volume_range=(35.0, 55.0), gallery_SA_cm2=75.0, gallery_SA_range=(60.0, 90.0), gallery_wall_mm=12.0, gallery_wall_range=(10.0, 20.0), gallery_source=( "Measured from custom wētā motels used in this study (photo " "documentation with ruler). Two-chamber Forstner-routed design: " "cavity ~75×30×20 mm total (~45 cm³), wall ~10–12 mm sides, " "~15 mm top/bottom. Back board ~15–20 mm. Entrance hole ~15 mm Ø. " "Mounted on native trees in bush. iButton DS1921/DS1922 loggers " "placed inside cavity (T_in) and on outer bark (T_out)." ), wall_material="wood", field_site="North Island, NZ (lowland–mid-elevation forest)", notes=( "Data collected from custom wētā motels, NOT natural galleries. " "Motel design: two-chamber cavity routed with Forstner bit, " "perspex viewing window, mounted on tree trunk. Readily occupied " "by wētā within weeks of deployment. Polygynandrous with harem " "formation. Natural galleries in trees bored by puriri moth " "(Aenetus virescens) and kanuka beetle (Ochrocydus huttoni) " "larvae would have different thermal properties." ), ) """*H. thoracica* — lowland tree galleries, North Island.""" H_CRASSIDENS = SpeciesInfo( name="Hemideina crassidens", common_name="Wellington tree wētā", body_length_mm=65.0, body_length_range=(50.0, 75.0), body_width_mm=15.0, body_width_range=(12.0, 18.0), mass_g=4.89, mass_range_g=(3.85, 6.13), mass_n=8, burrow_type="wood", gallery_entrance_mm=15.0, gallery_entrance_range=(12.0, 18.0), gallery_volume_cm3=45.0, gallery_volume_range=(35.0, 55.0), gallery_SA_cm2=75.0, gallery_SA_range=(60.0, 90.0), gallery_wall_mm=12.0, gallery_wall_range=(10.0, 20.0), gallery_source=( "Measured from custom wētā motels used in this study (photo " "documentation with ruler). Same motel design as H. thoracica: " "two-chamber Forstner-routed cavity ~75×30×20 mm (~45 cm³), " "wall ~10–12 mm sides, ~15 mm top/bottom. Entrance ~15 mm Ø. " "iButton DS1921/DS1922 paired loggers: inside cavity + outer bark. " "Literature natural gallery reference: Kelly (2008) artificial " "galleries 53.8 cm³ for ~3 adults on Maud Island; entrance " "~20 mm (Moller 1985)." ), wall_material="wood", field_site="Southern North Island / NW South Island, NZ", notes=( "Data collected from custom wētā motels (same design as " "H. thoracica motels). Largest tree wētā. Polygynous — males " "guard females in galleries. Can also use rock refuges where " "trees unavailable. Gallery entrance kept clean; entered " "head-first, exited in reverse with tibial spines outward." ), ) """*H. crassidens* — larger tree galleries, southern NI / NW SI.""" SPECIES: dict[str, SpeciesInfo] = { "H. maori": H_MAORI, "H. thoracica": H_THORACICA, "H. crassidens": H_CRASSIDENS, } """All species in a lookup dict keyed by binomial name.""" # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ ALLOMETRIC METABOLIC SCALING « the fire within » ║ # ╚═══════════════════════════════════════════════════════════════════════╝
[docs] @dataclass(frozen=True) class AllometryParams: """Parameters for insect resting metabolic rate allometry. The standard model:: RMR_25 = a · M^b (mW, grams) RMR_T = RMR_25 / Q10^((25 − T) / 10) Attributes: a: Pre-factor [mW / g^b]. b: Mass scaling exponent. Q10: Temperature coefficient. T_ref_C: Reference temperature [°C]. a_range: Plausible range for a. b_range: Plausible range for b. Q10_range: Plausible range for Q10. source: Literature reference. """ a: float b: float Q10: float T_ref_C: float a_range: tuple[float, float] b_range: tuple[float, float] Q10_range: tuple[float, float] source: str
ALLOMETRY = AllometryParams( a=10.5, b=0.75, Q10=2.5, T_ref_C=25.0, a_range=(8.0, 14.0), b_range=(0.67, 0.82), Q10_range=(2.0, 3.0), source=( "Lighton (2008) Measuring Metabolic Rates, OUP. Insect RMR " "compilation. Q10 range from Chown & Nicolson (2004) Insect " "Physiological Ecology, OUP. Exponent 0.75: Kleiber's law; " "range 0.67–0.82 from Glazier (2005) Biol Rev 80:611." ), ) """Allometric metabolic rate parameters with literature ranges.""" # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ SIMULATION DEFAULTS « knobs for the model » ║ # ╚═══════════════════════════════════════════════════════════════════════╝
[docs] @dataclass(frozen=True) class SimulationConfig: """Default parameters for the ODE integration and fitting. Attributes: n_cycles: Warm-up cycles before steady state. substeps: Euler sub-steps per hour. k_fit_bounds: Bounds on k during optimisation (1/h). shell_thickness_m: Default stone shell thickness [m]. shell_sweep_cm: Thicknesses for sensitivity sweep [cm]. wood_wall_sweep_cm: Wall thicknesses for wood gallery sweep [cm]. """ n_cycles: int substeps: int k_fit_bounds: tuple[float, float] shell_thickness_m: float shell_sweep_cm: tuple[float, ...] wood_wall_sweep_cm: tuple[float, ...]
SIM = SimulationConfig( n_cycles=25, substeps=20, k_fit_bounds=(0.005, 10.0), shell_thickness_m=0.01, shell_sweep_cm=(0.5, 1.0, 1.5, 2.0, 3.0, 5.0), wood_wall_sweep_cm=(1.0, 1.5, 2.0, 2.5, 3.0, 5.0), ) """Default simulation and fitting configuration.""" # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ VISUALISATION « making it pretty » ║ # ╚═══════════════════════════════════════════════════════════════════════╝
[docs] @dataclass(frozen=True) class VizConfig: """Plot styling defaults. Attributes: dpi: Raster resolution for PNG. svg_font_family: Font family for editable SVG text. species_colors: Colour per species for consistent plots. """ dpi: int svg_font_family: str species_colors: dict[str, str]
VIZ = VizConfig( dpi=200, svg_font_family="sans-serif", species_colors={ "H. maori": "#d62728", "H. thoracica": "#ff7f0e", "H. crassidens": "#2ca02c", }, ) """Visualisation defaults.""" # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ EXCLUSIONS & FLAGS « the fine print » ║ # ╚═══════════════════════════════════════════════════════════════════════╝ EXCLUDE_ROCK_IDS: list[int] = [22] """Rock IDs excluded from species-level pooling. Rock 22 is excluded because its humidity was at saturation (~100%), indicating potential evaporative cooling artefacts that confound the thermal model. """ # ╔═══════════════════════════════════════════════════════════════════════╗ # ║ BACKWARD COMPATIBILITY « don't break old imports » ║ # ╚═══════════════════════════════════════════════════════════════════════╝ # These module-level aliases keep existing code working while we # migrate to the structured dataclasses above. RHO_STONE: float = STONE.rho C_STONE: float = STONE.c K_THERMAL_STONE: float = STONE.k_thermal RHO_AIR: float = AIR.rho C_AIR: float = AIR.c SHELL_THICKNESS_M: float = SIM.shell_thickness_m SHELL_THICKNESSES_CM: list = list(SIM.shell_sweep_cm) N_CYCLES: int = SIM.n_cycles SUBSTEPS: int = SIM.substeps K_FIT_BOUNDS: tuple = SIM.k_fit_bounds ALLOMETRIC_A: float = ALLOMETRY.a ALLOMETRIC_B: float = ALLOMETRY.b Q10: float = ALLOMETRY.Q10 T_REF_C: float = ALLOMETRY.T_ref_C FIG_DPI: int = VIZ.dpi SVG_FONT_FAMILY: str = VIZ.svg_font_family