food_chain/ecology/demography.js
2026-09-29 23:45:12 +09:00

40 lines
2.9 KiB
JavaScript

import {annualSurvivalToDailyHazard,rateToProbability,clamp} from './units.js?v=45';
// Life-history fallbacks are simulator calibration values, not universal biological constants.
export const ROLE_DEMOGRAPHY = Object.freeze({
'primary-consumer': {annualSurvival:.38,offspringPerEvent:1,reproductionIntervalDays:180},
'secondary-consumer': {annualSurvival:.58,offspringPerEvent:1,reproductionIntervalDays:240},
'tertiary-consumer': {annualSurvival:.70,offspringPerEvent:1,reproductionIntervalDays:365},
detritivore: {annualSurvival:.45,offspringPerEvent:1,reproductionIntervalDays:210},
});
export function backgroundHazardPerDay(role){return annualSurvivalToDailyHazard((ROLE_DEMOGRAPHY[role]||ROLE_DEMOGRAPHY['primary-consumer']).annualSurvival)}
export function mortalityOccurs(hazardPerDay,dtDays,random){return random()<rateToProbability(hazardPerDay,dtDays)}
export function reproductionConfig(role){return ROLE_DEMOGRAPHY[role]||ROLE_DEMOGRAPHY['primary-consumer']}
export function inBreedingSeason(day,role,phaseDays=0){
const doy=(((day-phaseDays)%365)+365)%365;
if(role==='tertiary-consumer')return doy>=60&&doy<=180;
if(role==='secondary-consumer')return (doy>=60&&doy<=165)||(doy>=245&&doy<=300);
if(role==='detritivore')return (doy>=35&&doy<=175)||(doy>=220&&doy<=315);
return (doy>=45&&doy<=170)||(doy>=225&&doy<=305);
}
export function offspringEnergyShare(role){return role==='tertiary-consumer'?.18:.13}
export function reproductionThreshold(role){return role==='tertiary-consumer'?.82:role==='secondary-consumer'?.8:.78}
export const energyFraction = (energy,maxEnergy)=>clamp(maxEnergy>0?energy/maxEnergy:0,0,1);
// Structural growth is paid from assimilated energy. These are simulator
// calibration values rather than universal life-history constants.
export const GROWTH_ENERGY_COST_KJ_PER_KG = 6000;
export const RESERVE_FLOOR_FRACTION_FOR_GROWTH = 0.50;
export function initialStructuralMassKg(adultMassKg,offspringSize,ageDays,maturityAgeDays){
const adult=Math.max(1e-6,adultMassKg),birth=adult*clamp(offspringSize,0.01,1),progress=clamp(ageDays/Math.max(1e-6,maturityAgeDays),0,1);
return birth+(adult-birth)*progress;
}
export function energyLimitedGrowth({structuralMassKg,adultMassKg,energyKJ,reserveCapacityKJPerKg=1200,ageDays,maturityAgeDays,dtDays}){
const mass=Math.max(1e-6,structuralMassKg),adult=Math.max(mass,adultMassKg),remaining=Math.max(0,adult-mass);
if(!remaining)return {structuralMassKg:mass,energyKJ,gainKg:0};
const reserveFloor=mass*reserveCapacityKJPerKg*RESERVE_FLOOR_FRACTION_FOR_GROWTH,available=Math.max(0,energyKJ-reserveFloor);
if(!available)return {structuralMassKg:mass,energyKJ,gainKg:0};
const daysToMaturity=Math.max(1,maturityAgeDays-ageDays),potential=remaining*Math.min(1,dtDays/daysToMaturity),affordable=available/GROWTH_ENERGY_COST_KJ_PER_KG,gain=Math.min(remaining,potential,affordable);
return {structuralMassKg:mass+gain,energyKJ:energyKJ-gain*GROWTH_ENERGY_COST_KJ_PER_KG,gainKg:gain};
}