80 lines
6.5 KiB
JavaScript
80 lines
6.5 KiB
JavaScript
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']);
|
|
export const roleCode = role => Math.max(0,TROPHIC_ROLES.indexOf(role));
|
|
export const roleLabel = role => ({'primary-consumer':'一次消費者','secondary-consumer':'二次消費者','tertiary-consumer':'高次消費者','detritivore':'腐食者'}[role]||role);
|
|
|
|
const DEFAULT_EDGES = Object.freeze({
|
|
'primary-consumer': Object.freeze({
|
|
producer:Object.freeze({preferenceWeight:1,assimilationEfficiency:.45,functionalResponse:2,attackRate:2.2,handlingTimeDays:.05}),
|
|
zooplankton:Object.freeze({preferenceWeight:.42,assimilationEfficiency:.85,functionalResponse:2,attackRate:1.25,handlingTimeDays:.08}),
|
|
}),
|
|
'secondary-consumer': Object.freeze({
|
|
producer:Object.freeze({preferenceWeight:.12,assimilationEfficiency:.45,functionalResponse:2,attackRate:.55,handlingTimeDays:.08}),
|
|
zooplankton:Object.freeze({preferenceWeight:.32,assimilationEfficiency:.85,functionalResponse:2,attackRate:1.05,handlingTimeDays:.08}),
|
|
'primary-consumer':Object.freeze({preferenceWeight:1,assimilationEfficiency:.85,functionalResponse:2,attackRate:1.25,handlingTimeDays:.12}),
|
|
detritus:Object.freeze({preferenceWeight:.3,assimilationEfficiency:.85,functionalResponse:2,attackRate:.8,handlingTimeDays:.1}),
|
|
}),
|
|
'tertiary-consumer': Object.freeze({
|
|
'primary-consumer':Object.freeze({preferenceWeight:.45,assimilationEfficiency:.85,functionalResponse:2,attackRate:.8,handlingTimeDays:.16}),
|
|
'secondary-consumer':Object.freeze({preferenceWeight:1,assimilationEfficiency:.85,functionalResponse:2,attackRate:1.05,handlingTimeDays:.18}),
|
|
detritus:Object.freeze({preferenceWeight:.2,assimilationEfficiency:.85,functionalResponse:2,attackRate:.55,handlingTimeDays:.12}),
|
|
}),
|
|
detritivore: Object.freeze({
|
|
detritus:Object.freeze({preferenceWeight:1,assimilationEfficiency:.85,functionalResponse:2,attackRate:1.4,handlingTimeDays:.08}),
|
|
producer:Object.freeze({preferenceWeight:.08,assimilationEfficiency:.45,functionalResponse:2,attackRate:.45,handlingTimeDays:.1}),
|
|
}),
|
|
});
|
|
|
|
export function interactionEdge(consumer,resource){return DEFAULT_EDGES[consumer]?.[resource] || null}
|
|
|
|
export function bodyMassPreference(predatorMassKg,preyMassKg,profile,edge=null){
|
|
const preferred=edge?.preferredPredatorPreyMassRatio??profile.feeding.preferredPredatorPreyMassRatio,sigma=edge?.massRatioSigma??profile.feeding.massRatioSigma,ratio=Math.max(1e-8,predatorMassKg/Math.max(1e-8,preyMassKg)),mu=Math.log(preferred);
|
|
return Math.exp(-((Math.log(ratio)-mu)**2)/(2*sigma*sigma));
|
|
}
|
|
|
|
// Rall et al. (2012), all-data 'only slopes' model:
|
|
// attack: Ea=+0.44 eV, consumer exponent +0.47, resource exponent +0.15
|
|
// handling: Ea=-0.27 eV, consumer exponent -0.48, resource exponent +0.34.
|
|
// Their local residual analysis adds the nonlinear ln(predator:prey ratio) terms below
|
|
// and a quadratic temperature residual for handling time. Edge values remain transparent
|
|
// baseline calibrations at 20 C, 1 kg consumer, and the profile's preferred mass ratio.
|
|
const BOLTZMANN_EV_K=8.617333262e-5,REFERENCE_T_K=293.15;
|
|
const arrheniusFactor=(activationEnergyEv,temperatureC)=>{const t=Math.max(180,temperatureC+273.15);return Math.exp(clamp(activationEnergyEv*(t-REFERENCE_T_K)/(BOLTZMANN_EV_K*t*REFERENCE_T_K),-5,5))};
|
|
export function rallFeedingParameters(edge,consumerMassKg,resourceMassKg,temperatureC,profile){
|
|
if(!edge)return {attackRate:0,handlingTimeDays:Infinity,preference:0};
|
|
const mc=Math.max(consumerMassKg,1e-8),mr=Math.max(resourceMassKg,1e-10),preferredRatio=Math.max(1e-8,edge.preferredPredatorPreyMassRatio??profile.feeding.preferredPredatorPreyMassRatio),ratio=mc/mr,lr=Math.log(ratio),lr0=Math.log(preferredRatio),referenceConsumerKg=1,referenceResourceKg=1/preferredRatio;
|
|
const attackResidual=x=>-1.81+.37*x-.017*x*x;
|
|
const handlingMassResidual=x=>1.93-.48*x+.026*x*x;
|
|
const handlingTempResidual=t=>.51-.055*t+.0013*t*t;
|
|
const globalAttackMass=Math.pow(mc/referenceConsumerKg,.47)*Math.pow(mr/referenceResourceKg,.15),globalHandlingMass=Math.pow(mc/referenceConsumerKg,-.48)*Math.pow(mr/referenceResourceKg,.34);
|
|
const attackRatioResidual=Math.exp(clamp(attackResidual(lr)-attackResidual(lr0),-3,3)),handlingRatioResidual=Math.exp(clamp(handlingMassResidual(lr)-handlingMassResidual(lr0),-3,3));
|
|
const attackTemp=arrheniusFactor(.44,temperatureC),handlingTemp=arrheniusFactor(-.27,temperatureC),handlingTempResidualFactor=Math.exp(clamp(handlingTempResidual(temperatureC)-handlingTempResidual(20),-2,2));
|
|
const attackScale=clamp(globalAttackMass*attackRatioResidual*attackTemp,1e-3,1e3),handlingScale=clamp(globalHandlingMass*handlingRatioResidual*handlingTemp*handlingTempResidualFactor,1e-3,1e3),preference=bodyMassPreference(mc,mr,profile,edge);
|
|
return {attackRate:edge.attackRate*attackScale,handlingTimeDays:Math.max(1e-5,edge.handlingTimeDays*handlingScale),preference};
|
|
}
|
|
|
|
export function functionalResponseRatePerDay(params,resourceDensity,denominatorTerms=[],q=1){
|
|
const n=Math.max(0,resourceDensity),power=Math.pow(n,q),numerator=params.attackRate*params.preference*power;
|
|
let denominator=1;
|
|
for(const term of denominatorTerms){const tq=term.q??1,tn=Math.max(0,term.resourceDensity);denominator+=term.attackRate*term.preference*term.handlingTimeDays*Math.pow(tn,tq)}
|
|
return numerator/Math.max(1e-12,denominator);
|
|
}
|
|
|
|
export function fieldResourceIntakeKgPerDay(massKg,resourceDensityKgPerM2,edge,resourceRole='producer'){
|
|
if(!edge)return 0;
|
|
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)}
|