This commit is contained in:
33333-33333 2026-09-29 23:45:12 +09:00
commit f5fc9b76ce
73 changed files with 32628 additions and 376 deletions

View file

@ -1,4 +1,4 @@
import {clamp} from './units.js';
import {clamp} from './units.js?v=45';
export class ClimateProvider{
constructor({meanTemperatureC=15,amplitudeC=10,phaseDay=205}={}){this.meanTemperatureC=meanTemperatureC;this.amplitudeC=amplitudeC;this.phaseDay=phaseDay;this.series=new Map}

View file

@ -1,4 +1,4 @@
import {annualSurvivalToDailyHazard,rateToProbability,clamp} from './units.js';
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({
@ -10,8 +10,8 @@ export const ROLE_DEMOGRAPHY = Object.freeze({
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){
const doy=((day%365)+365)%365;
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);

View file

@ -1,4 +1,4 @@
import {clamp,rateToProbability} from './units.js';
import {clamp,rateToProbability} from './units.js?v=45';
export const TROPHIC_ROLES = Object.freeze(['primary-consumer','secondary-consumer','tertiary-consumer','detritivore']);
export const RESOURCE_ROLES = Object.freeze(['producer','zooplankton','primary-consumer','secondary-consumer','tertiary-consumer','detritus']);
@ -62,12 +62,19 @@ export function functionalResponseRatePerDay(params,resourceDensity,denominatorT
return numerator/Math.max(1e-12,denominator);
}
export function producerIntakeKgPerDay(massKg,producerKgPerM2,edge,temperatureC=20,profile={feeding:{preferredPredatorPreyMassRatio:35,massRatioSigma:1.05}}){
export function fieldResourceIntakeKgPerDay(massKg,resourceDensityKgPerM2,edge,resourceRole='producer'){
if(!edge)return 0;
const resourceMassEquivalent=Math.max(1e-5,producerKgPerM2*.01),params=rallFeedingParameters(edge,massKg,resourceMassEquivalent,temperatureC,profile),q=edge.functionalResponse===3?2:1;
const raw=functionalResponseRatePerDay(params,producerKgPerM2,[{...params,resourceDensity:producerKgPerM2,q}],q);
const maxIntake=Math.max(.002,.16*Math.pow(Math.max(massKg,.001),.78))*edge.preferenceWeight;
return Math.min(maxIntake,raw*Math.max(.03,Math.pow(massKg,.58)));
const mass=Math.max(massKg,.001),density=Math.max(0,resourceDensityKgPerM2),q=edge.functionalResponse===3?2:1;
// A biomass field has no single prey body mass. Do not feed field density into
// predator:prey body-mass scaling. Use a density-based Holling saturation instead.
const halfSaturation=resourceRole==='zooplankton'?.012:.08;
const x=Math.pow(density,q),half=Math.pow(halfSaturation,q),saturation=x/Math.max(1e-12,half+x);
const coefficient=resourceRole==='zooplankton'?.12:.16;
const maxIntakeKgPerDay=Math.max(.002,coefficient*Math.pow(mass,.78))*edge.preferenceWeight;
return maxIntakeKgPerDay*saturation;
}
export function producerIntakeKgPerDay(massKg,producerKgPerM2,edge){
return fieldResourceIntakeKgPerDay(massKg,producerKgPerM2,edge,'producer');
}
export function attackProbabilityPerStep(ratePerDay,dtDays){return rateToProbability(ratePerDay,dtDays)}

View file

@ -1,4 +1,4 @@
import {KELVIN_OFFSET, clamp} from './units.js';
import {KELVIN_OFFSET, clamp} from './units.js?v=45';
const B0 = Object.freeze({ectotherm: 42, endotherm: 310}); // kJ d^-1 at 1 kg, simulator calibration
const ACTIVATION_EV = 0.65;

View file

@ -3,9 +3,9 @@ const MODE = Object.freeze({
// v = a * M^b * (1 - exp(-h * M^i)), v in km/h, M in kg.
// Flying, running and swimming coefficients are the published Supplementary
// Table 4 fits for the time-dependent maximum-speed model.
running:{a:25.5,b:.26,h:22,i:-.60,routineFraction:.34,homeRangeFactor:1,calibration:'Hirt-2017'},
swimming:{a:11.2,b:.36,h:19.5,i:-.56,routineFraction:.30,homeRangeFactor:1.25,calibration:'Hirt-2017'},
flying:{a:142.8,b:.24,h:2.4,i:-.72,routineFraction:.42,homeRangeFactor:1.8,calibration:'Hirt-2017'},
running:{a:25.5,b:.26,h:22,i:-.60,routineFraction:.34,calibration:'Hirt-2017'},
swimming:{a:11.2,b:.36,h:19.5,i:-.56,routineFraction:.30,calibration:'Hirt-2017'},
flying:{a:142.8,b:.24,h:2.4,i:-.72,routineFraction:.42,calibration:'Hirt-2017'},
});
export const locomotionModes=Object.freeze(Object.keys(MODE));
export const normalizeLocomotionMode=mode=>MODE[mode]?mode:'running';
@ -25,13 +25,17 @@ export function foragingSpeedMPerDay(massKg,dailyMovementBudgetM,mode='running')
export function escapeSpeedMPerDay(massKg,dailyMovementBudgetM,mode='running'){
return Math.min(maximumSpeedMPerDay(massKg,mode),routineTravelSpeedMPerDay(massKg,dailyMovementBudgetM,mode)*2.4);
}
export function dailyMovementBudgetM(massKg,geneBudgetM,mode='running'){
const c=MODE[normalizeLocomotionMode(mode)];
return Math.max(25,geneBudgetM)*(0.75+0.25*Math.pow(Math.max(massKg,.01)/3,.08))*(.9+.1*c.homeRangeFactor);
export function dailyMovementBudgetM(massKg,geneBudgetM){
// Daily travel capacity is an independent movement trait.
return Math.max(25,geneBudgetM)*(0.75+0.25*Math.pow(Math.max(massKg,.01)/3,.08));
}
export function homeRangeRadiusM(massKg,mode='running'){
const c=MODE[normalizeLocomotionMode(mode)];return Math.min(1500,Math.max(90,170*Math.pow(Math.max(massKg,.01),.22)*c.homeRangeFactor));
export function sampleRoamingDistanceM(random,dailyBudgetM){
// Persistent free-roaming waypoint: typically ~0.35-1.5 days of travel,
// capped to avoid asking a local navigator to cross the entire world at once.
const budget=Math.max(80,dailyBudgetM),scale=Math.max(120,budget*.72),draw=-Math.log(Math.max(1e-9,1-random()))*scale;
return Math.min(Math.max(90,draw),Math.min(1800,budget*1.8));
}
export function sampleDispersalDistanceM(random,massKg,mode='running'){
const scale=homeRangeRadiusM(massKg,mode)*.55;return Math.min(scale*3,-Math.log(Math.max(1e-9,1-random()))*scale);
export function limitSpeedForArrival(speedMPerDay,distanceM,arrivalRadiusM,dtDays){
const available=Math.max(0,Math.max(0,distanceM)-Math.max(0,arrivalRadiusM));
return Math.min(Math.max(0,speedMPerDay),dtDays>0?available/dtDays:0);
}

View file

@ -1,11 +1,11 @@
export const TERRESTRIAL = Object.freeze({
id:'terrestrial',type:'terrestrial',producer:{rMaxPerDay:.03,carryingCapacityKgPerM2:.5},feeding:{preferredPredatorPreyMassRatio:35,massRatioSigma:1.05},environment:{light:.82,nutrientN:.78,nutrientP:.78,dissolvedOxygen:1},movement:{homeRangeMultiplier:1}
id:'terrestrial',type:'terrestrial',producer:{rMaxPerDay:.03,carryingCapacityKgPerM2:.5},feeding:{preferredPredatorPreyMassRatio:35,massRatioSigma:1.05},environment:{light:.82,nutrientN:.78,nutrientP:.78,dissolvedOxygen:1}
});
export const FRESHWATER = Object.freeze({
id:'freshwater',type:'freshwater',producer:{carryingCapacityKgPerM2:.18},feeding:{preferredPredatorPreyMassRatio:80,massRatioSigma:1.12},environment:{light:.72,nutrientN:.58,nutrientP:.52,dissolvedOxygen:.82},movement:{homeRangeMultiplier:1.15}
id:'freshwater',type:'freshwater',producer:{carryingCapacityKgPerM2:.18},feeding:{preferredPredatorPreyMassRatio:80,massRatioSigma:1.12},environment:{light:.72,nutrientN:.58,nutrientP:.52,dissolvedOxygen:.82}
});
export const MARINE = Object.freeze({
id:'marine',type:'marine',producer:{carryingCapacityKgPerM2:.14},feeding:{preferredPredatorPreyMassRatio:55,massRatioSigma:1.08},environment:{light:.76,nutrientN:.54,nutrientP:.48,dissolvedOxygen:.9},movement:{homeRangeMultiplier:1.35}
id:'marine',type:'marine',producer:{carryingCapacityKgPerM2:.14},feeding:{preferredPredatorPreyMassRatio:55,massRatioSigma:1.08},environment:{light:.76,nutrientN:.54,nutrientP:.48,dissolvedOxygen:.9}
});
export const PROFILES=Object.freeze({terrestrial:TERRESTRIAL,freshwater:FRESHWATER,marine:MARINE});
export function getProfile(id){return PROFILES[id]||null}

View file

@ -1,4 +1,4 @@
import {clamp} from './units.js';
import {clamp} from './units.js?v=45';
export class ResourceField{
constructor({id,role,cellCount,energyDensityKJPerKg,representation='field'}){this.id=id;this.role=role;this.energyDensityKJPerKg=energyDensityKJPerKg;this.representation=representation;this.biomass=new Float32Array(cellCount)}

View file

@ -1,3 +1,3 @@
import {PROFILES} from './profiles.js';
import {PROFILES} from './profiles.js?v=45';
export function validateProfileId(id){if(!PROFILES[id])throw new RangeError(`unknown environment profile: ${id}`);return id}