NEWS
regnans 0.2
Changes to results
- A plant equilibrium found by root-finding (TF24's default Newton, or
nleqslv and dfsane) searches on a frozen node schedule, in rounds, each accepted only where one run that refines the schedule meets equilibrium_eps, instead of refining at every run. demographic_step_control(equilibrium_discretisation_rounds = ) sets the most rounds (default 5; 0 refines at every run, as before). Each run on the frozen schedule chooses its own ODE steps. On TF24 a solve from far from equilibrium costs about half (one resident from a birth rate of 10: 24 s to 13 s; two residents: 172 s to 67 s), and warm solves 4-8% less. Equilibria move within the tolerance (1e-7 relative on TF24 residents). FF16's default, fixed-point iteration, is unchanged.
- Every finite difference across plant equilibria (the resident and parameter Jacobians, and through them classification, continuation, the canonical equation's Jacobian and the boundary Jacobian) solves its points on the centre's discretisation: the node schedule and ODE steps of its equilibrium run, replayed with no refinement, and plant's
offspring_production_tol (its root-finds for a newborn's height and the light compensation point) to the cube of the step; a parameter Jacobian's column in max_patch_lifetime refines its own, since the schedule spans one lifetime. Each point used to refine its own schedule, and the change of steps swamped the derivative (plant#653): at FF16 lma 0.15 the selection gradient's derivative read -1619, 193 and 107 at steps 1e-2, 1e-3 and 1e-4, and now -1955, -2346 and -2290, about 40% faster. New harness connectors community_discretisation() and community_freeze_discretisation(); the reference models have nothing to freeze.
plant_default_assembly_control() carries the size density on the birth-date coordinate (node_density_coordinate = "birth_date"), on which alone plant's gradients are defined. Equilibria move by about 1% (FF16 lma 0.0825: birth rate 112.65 to 113.89); pass node_density_coordinate = "height" for the height coordinate.
- Finite differences across equilibrium solves (the resident Jacobian and the parameter Jacobian, and through them classification, the singularity solver, sensitivity and continuation) solve each stencil point to the cube of the stencil's relative step, or to
equilibrium_eps where that is tighter, so their error is set by the step rather than by equilibrium_eps over the step (#80). On iterated equilibria they move towards their closed forms (DD99 from 3e-4 to 5e-7, GM99 from 5e-2 to 4e-6 relative); a stencil point whose solve stops short of its tolerance warns.
- The canonical equation's RODAS stepper takes the right-hand side's Jacobian from
community_selection_gradient_jacobian() instead of odelia's forward difference, which was too noisy for its step-size control on an iterated equilibrium: such a run now costs what it does with the model's own equilibrium (120-238 solves on the reference cases, not 427-2178).
- Of two residents in a canonical run within two finite-difference steps of each other, the rarer goes extinct, recorded as a
"merge" event: the converse of a branching.
- When an assembly's or a label's residents evolve by the canonical equation (the fallback of
evolve = "newton", or "canonical"), two that end within eps_too_close of each other (a fraction of the bounds' width on the trait scale, as for births) are one resident, and the rarer is dropped. The canonical run stops on an absolute gradient tolerance, which a pair collapsing onto one singular strategy meets while still closing; at zero the finite-difference steps that otherwise decide a merge are relative and vanish. GK98's backward boundary sweep at d = 1 ended on residents at ±2.3e-5 and counted two species where there is one (it passed under odelia 0.7.3 only because that run stopped inside the absolute-step region). Roots Newton solves and classifies keep the step rule, since a pair just past a pitchfork is genuinely that close.
canonical_control() defaults to rtol = 1e-2 and atol = 1e-4 (were 1e-3 and 1e-5): a run's end point is found by Newton, so the tighter trajectory cost RODAS 25-30% more equilibrium solves without moving it. The time at which a run reports reaching a stable point can move by up to one step.
New features
- TF24 runs through the plant harness:
harness_plant("TF24") with plant_default_assembly_pars(model = "TF24"). Its resident equilibrium is found by Newton's method by default, since fixed-point iteration settles on a two-cycle on TF24, and its mutant gradient comes from plant's sweep by default (fitness_gradient = "model"), which on TF24 costs what the finite difference does at one trait and less from two. harness_plant()'s fitness_gradient now defaults to NULL, resolved per model: "fd" for FF16, so FF16 callers see no change. community_start() given no harness takes harness_plant() for the model model_support$p was built for, and refuses a harness_plant() for another. A TF24 community's environment (soil profile, drivers such as rainfall) is described as plain data in model_support$environment and built afresh for every run; with none, plant's default.
harness_plant(fitness_gradient = "model") takes the fitness gradient in the mutant direction from plant's reverse sweep (plant#650 E2): the mutants' replay in the recorded resident canopy, differentiated through the hyperparameter function, with a finite difference for any point plant refuses. On the birth-date coordinate only. Off by default for FF16, where a sweep still costs about three times the finite difference it replaces (plant#677); the default for TF24.
community_label() labels a community by the number of species its parameters support: the evolutionarily stable community reached by alternating evolution of the residents to their singular coalition and immigration at the maximum of invasion fitness, with a status saying whether the count is the model's (stable) or censored by the search (capped, edge, unresolved). A label needs bounds finite on the trait scale. boundary_scout() labels the points of a parameter design through the parallel map, its columns matched to the map's parameters by name, boundary_design() builds one, and boundary_control() holds their settings (#60).
boundary_bisect() finds where the coexistence label changes along rays in parameter space, each ray bisected on a worker; boundary_sweep() labels a path cold and carried in both directions and flags points where the model's alternative attractors make the history decide, every route searching the cold label's box and an unresolved route leaving the flag NA (#60).
community_solve_boundary() solves for a point on a coexistence boundary: the residents at their singular coalition and either a new fitness peak y* exactly marginal (type = "invasion") or a resident becoming a branching point ("branching"), one parameter free, by Newton with a Jacobian from the derivative layer (#60).
community_continue_boundary() follows a coexistence boundary through two parameters by pseudo-arclength continuation, rejecting steps whose tangent turns, monitoring test functions (a resident going extinct or ceasing to be an ESS, the marginal peak annihilating, residents merging, another peak becoming marginal, a crossing branch) and ending at the first event, located by false position, then verifying points cold against labels either side. It returns a regnans_branch with print(), plot() and as.data.frame() (cubic Hermite between points); boundary_switch() gives the start of the branch an event seeds, and boundary_diagram() collects branches (#60).
assembler_control() gains evolve ("none", "newton" or "canonical"): the residents evolve to their singular coalition between maximum-fitness births, so the assembly ends at an evolutionarily stable community; max_residents and edge_tol refuse a birth beyond a cap or on the edge of the bounds; branch_offset places the daughter of a resident at a branching point. An assembler counts its equilibrium solves in $evaluations; one saved before these fields restores with their defaults.
community_selection_gradient_jacobian() returns the Jacobian of the residents' equilibrium birth rates, from the same solves, in attr(., "birth_rate_jacobian").
derivative_control(resident_jacobian = "implicit") differentiates the resident Jacobian through the equilibrium condition rather than across equilibrium solves: 2m(k + 1) model runs at given traits and densities and no solves, reported in attr(., "environments") and in the canonical equation's environments (#88). The default is unchanged. Those runs replay the centre's discretisation, as the stencil's solves do; before, each refined its own schedule, which on plant gave a wrong Jacobian (on TF24 of the wrong sign) and now agrees with the stencil to the steps' truncation error.
Dependency changes
- Built against plant
develop@a7729146 (plant#724), which pins odelia 0.10.5 and phylloptim 0.31.0 and makes TF24's fitness differentiable as FF16's is (plant#699). Three plant renames since bd09645d reach regnans: control(node_density_coordinate = "birth_date") replaces node_density_in_birth_date = TRUE, scm$keep_states replaces scm$record_trajectory, and a recorded ODE schedule has four fields, the soil's substep records beside the times and step sizes, which model_support, the frozen discretisation and the runner now carry and clear together (plant refuses sizes without the records, empty as they are for FF16). Under odelia's Hairer step control every adaptive run's steps change; every number pinned by the plant tests held within its tolerance.
- Built against plant
develop@bd09645d (plant#658), which makes an FF16 stand's fitness differentiable (plant#650 E1-E4) and pins odelia 0.7.3 and phylloptim 0.10.2. regnans keeps its plain odelia floor and inherits plant's pins. No call needed changing.
- Migrated to plant
develop@8657b3a (traitecoevo/plant#648), which removed run_scm(use_ode_times =) and Control$save_RK45_cache, and now replays any ODE schedule a Parameters object carries. regnans' equilibrium runner now integrates freely at each birth rate rather than replaying the previous iteration's steps. The resident run behind mutant fitness replays the equilibrium run exactly (times and step sizes), so resident fitness at equilibrium is 0 again. The plant interface baseline is recorded in .plant-interface-version.
- Earlier changes are recorded in the git history and the pull requests on GitHub.