
Changelog
landisutils 0.0.161
loss_from_stats()forms the observed annual area burned aslambda_obs * mean(fire_sizes_ha)rather thansum(fire_sizes_ha) / n_years. The two agree only when the size sample is every fire in the area and years the counts were taken over, and a payload may deliberately break that: per-fire sizes are scarce where fire counts are not, so a small study area may borrow its size sample from a wider region while counting ignitions only within itself. The summed form then returns the wider region’s annual area – on one such payload, 181.6 ha/yr against a landscape that burns 22.4, an 8.1x target that would have dragged the fitted ignition rate up to match an area the landscape does not contain. Where the two pools do agree the forms are numerically identical, so a payload whose sizes and counts come from the same fires is unaffected to the last digit. The component no longer readsprimary$n_years, and its coherence warning now fires on an emptyfire_sizes_harather than a missing year count.area_burnedwas released at weight 0 in 0.0.160 and no calibration is known to have weighted it, so no fitted result changes.The roxygen for
area_burnednow says to compute thecount-versus-area_burnedsteepness factor for your own record rather than assuming it is large. It is roughly 3.4 on a record averaging 27.8 fires per year with a standard deviation of 18.9, but only 1.6 on a sparse record averaging 0.87 – a record whose annual counts are nearly Poisson has a small standard deviation to divide by, so the sparser the record, the less headroom this component has before it starts competing with the fire-count target.
landisutils 0.0.160
-
loss_from_stats()gains anarea_burnedcomponent,|log10(area_sim / area_obs)|on annual area burned. Nothing in the loss scored how much area burns:area_fuelscores how burned area is distributed across base fuel types,countandsizescore the number of fires and the shape of their size distribution, and a parameter set can satisfy all of them while burning several times too much or too little. On a range-of-variation study that is the quantity the exercise turns on, because annual area burned is what drives the seral-stage distribution. It is also the one area quantity measured to separate candidate fire-size tables: across two landscapes no distributional size statistic separated two real candidate tables by as much as one standard error, while annual area burned separated them by 3.4. A log10 ratio keeps the component scale-free, so one weight means the same thing on study areas whose burn rates differ by orders of magnitude. Simulated area is summed from each replicate’s events over the yearscountscores, so the two components always divide by the same denominator, and converted withobserved$pixel_area_ha; a replicate set that burns nothing scores 3.0 rather than the infinite valuelog10(0)would give, which DEoptim could not rank. The default weight is 0, so an existing calibration is unchanged until its weights ask for the component, andcalibrate_dynamic_fire()warns when a non-zero weight meets a payload that cannot supply an annual rate or is missingpixel_area_ha. Note thatarea_burnedandcountboth move with the number of fires and so compete for the same lever:countis normalised by the observed year-to-year standard deviation, which makes it steeper by roughlylambda_obs / sd(n_fires_obs) * ln(10), and above that ratio of weights the fitted fire count is pulled off its own target to compensate for a fire-size distribution the search cannot change.
landisutils 0.0.159
-
parse_dynamic_fire_logs()takes each fire’s size from the event log’sSitesCheckedinstead ofDamagedSites, and rescalesmean_severityonto the same cells. Dynamic Fire logsDamagedSitesas one more than the cells a fire burned, on every event: across 9,106 events it was never below 2, the 1,118 fires that burned only their ignition cell all logged 2, and on 372 timesteps with a single fire each the severity map’s burned cells matchedSitesCheckedexactly andDamagedSitesminus one. Every simulated fire was therefore scored one cell too large, so a calibration could never produce a one-cell fire while the record it was fitted to was full of them; the size loss was inflated most at the small-fire end, where one cell is a large fraction of a fire.MeanSeverityis divided by the same inflated count, which put a one-cell fire at severity 3 into severity class 1 and moved 130 of 928 checked fires into the wrong class.total_sites_burnedfalls by one cell per fire.sim_landis()now converts cells to hectares with the cell area of the scenario it runs, read fromscenario.txt’sCellLength. Neithercalibrate_dynamic_fire()norrun_calibration_validation()passedpixel_area_ha, so it defaulted to 1 and every simulated fire on a grid other than 100 m was scored in cells against observed hectares. A caller-supplied value that disagrees withCellLengthis now an error, and both drivers refuse observed targets built on a different cell size.parse_dynamic_fire_logs()warns if a log breaksDamagedSites == SitesChecked + 1, the relation the correction was verified on. Direct callers ofparse_dynamic_fire_logs()still get its defaultpixel_area_ha = 1, so pass the cell area on a grid other than 100 m. Losses computed before this version are not comparable with losses computed after it, andeval_fpdoes not include the package version, so a checkpoint from an earlier version must not be resumed.
landisutils 0.0.158
calibration_par_names()gainsNumFiresMultiplier, which scales each fire ecoregion’s own ignition rate by a common factor instead of replacing every rate with one value.NumFires, added in 0.0.154, is right where the rate is a property of the landscape as a whole, butpatch_fire_config()writes that scalar into every ecoregion row, so a landscape whose ecoregions carry markedly different rates – 0.57 ignitions per year in one and 17 in another is a real case – loses that structure entirely by being calibrated. The correction being fitted is the gap between a count of observed FIRES and a count of ignitions, and that gap applies to every ecoregion alike, so a common factor expresses it where a common value does not.patch_fire_config()andapply_calibrated_num_fires()share the scaling through one internal helper, so a calibration trial and the production table it is applied to cannot drift apart. The two forms are alternatives, not companions: supplying both is an error, raised where a caller first passes them – the bounds incalibrate_dynamic_fire()’s config, a trial’s parameter vector, or a production parameter vector.ForCS’sMapOutputIntervalnow accepts0, which writes no ForCS spatial maps – the only way to turn them off, and a value ForCS itself accepts. The setter required a positive value, so a configuration that asked for no maps could not be written at all:write()never ran, and a pipeline wiring the ForCS config into a scenario file failed there instead. The fourForCSOutputlog intervals are unchanged, because ForCS rejects0for those; a log can only be thinned, for instance to9999.
landisutils 0.0.157
- Every extension’s log-file default is now a full path rather than a bare relative one, so the defaults are usable as written.
SummaryLogFile,EventLogFile,EventLogandLogFileare stored through active bindings that apply.relPath(value, self$path), so a default of"wind/summary-log.csv"was made relative a SECOND time: against a scenario directory ofLANDIS-II/ForCS_windit became../../wind/summary-log.csv, two levels above the replicate, and the extension wrote its log outside the run. Nothing caught it because no caller relied on the defaults –BiomassHarvestalready built full paths ininitialize(), and callers of the other extensions worked around it one call site at a time. Ten extensions and fourteen parameters were affected:BiomassBrowse,ClimateBDA,DynamicFire,EDA,Hurricane,LinearWind,OriginalFire,OriginalWind,OutputLandscapeHabitatandRootRot. A caller that already passes a full path is unaffected, and the written config is unchanged for every such caller.
landisutils 0.0.156
calibration_par_names()gainsDamageAgeMultiplier, andpatch_fire_config()scales theFireDamageTable’s cohort-age column by it, so the damage table can be calibrated. The paired severity-minus-tolerance column is deliberately left alone: the user guide (2.16.3) requires an integer there, which gives only a few reachable mortality levels and cannot be fitted, while the age column is a percentage of longevity (2.16.2) and scales smoothly. Scaled values are rounded to whole percentages and forced strictly increasing, since a row that does not exceed its predecessor can never be reached.New
apply_calibrated_damage_age()applies a calibratedDamageAgeMultiplierto a production fire damage table, so a fitted table reaches the simulations it was fitted for; it leaves the table alone when the vector does not carry one. It andpatch_fire_config()share the scaling, so the calibration trial and the production config cannot drift apart.landis_overstory_mortality_share()no longer scores mortality on cells the extension burned without damaging any cohort. The severity map writes 2 for those cells and severity + 2 for damaged ones (user guide 3.1), so treating the value as severity + 2 throughout gave them a severity of 0 and then asked the damage table what a severity of 0 kills – inventing mortality against the extension’s own statement that nothing was killed. Such cells remain in the denominator, matching an observed reference that counts every assessed pixel inside a fire perimeter.
landisutils 0.0.155
-
DynamicFuels’sDisturbanceConversionTablenow takes the three columns the Dynamic Fuels user guide (2.8) specifies – fuel type index, duration in years, and either a harvest prescription name or aFireSeverityN/WindSeverityNkeyword – instead of four. The four-column form came from reading the written header’s “Fuel Type” as two columns, and survived because the table had only ever been empty: the first project to populate it could not write a config at all.
landisutils 0.0.154
New loss component
mortality, withlandis_overstory_mortality_share()behind it: the share of a replicate’s burned cells whose dominant cohort was killed, from the severity maps, the initial communities, the species tolerances and the fire damage table. It exists because theseveritycomponent compares quantities on different scales – an observed burn-severity class measures vegetation change, while the extension’s severity classes are crown fraction burned, and the two disagree wherever fire kills the canopy from the ground without crowning.save_observed_fire_targets()gainsmortality_shareto carry the observed side. Existing calibrations are unaffected: without an observed share the component contributes 0.calibration_par_names()gainsNumFires, andpatch_fire_config()writes it into the fire-size table, so the ignition rate can be calibrated. The Dynamic Fire System definesNumFiresas the mean number of IGNITIONS per year, each of which becomes a fire only if the initiation probability of the fuel on its cell allows it, so a rate taken from a count of observed fires is systematically low as a count of ignitions. Newapply_calibrated_num_fires()applies a calibrated rate to a production fire-size table, and leaves the table alone when the vector does not carry one.New
observed_fire_sizes()returns one size per ignition point: the point’s ownSIZE_HA, or theSIZE_HAof a same-year perimeter polygon that contains it. This is the rulesave_observed_fire_targets()already used for the calibration’s size target, now exported so that a fire-size distribution fitted from the same record uses the same sizes. Fitting to points and polygons bound as separate rows counts every mapped fire twice.
landisutils 0.0.153
-
run_calibration_validation()now fails with a message that names the failed replicates when a validation replicate’s process dies.parallel::mclapply()reports a replicate that raised an R error as atry-error, but one whose child process was killed comes back asNULLwith only a warning; the guard tested for the first and not the second, so a dead replicate reachedloss_from_stats()and surfaced there as “missing value where TRUE/FALSE needed”, naming neither the replicate nor the cause. The error now distinguishes the two modes and points at an OOM kill, a segfault or a container fault rather than at an R error.loss_from_stats()also rejects arepslist holding a non-list element, so any caller that maps replicates in parallel gets the same clear failure.
landisutils 0.0.152
-
fps_run_docker()no longer fails on every non-emptyFPS_log.txt. FPSM writes one benign message there, a missing substitution factor, which moves no carbon between pools; it is now reported without failing the run. Every other message still fails, including one this package does not recognise, so a message added by a future FPSM release cannot be waved through. The previous behaviour would have failed the first harvesting run against parameters whose substitution table does not cover every primary product, with an error saying carbon had been lost when none had.
landisutils 0.0.151
- New script
inst/scripts/verify_candidate_loss.Rrescores a calibrating project’s storedrun_calibration_validation()result under a candidate landisutils branch, tag or sha beside the installed version, and reports which loss components moved. The candidate is installed into a temporary library and each version is scored in its own R process, so neither the project library nor any checkout changes. Use it to measure what a loss-code change does to a calibration before adopting the release.
landisutils 0.0.150
-
tar_landis()’s replicate input hash no longer depends on the collation locale of the R process computing it. It sorted dependency paths withsort(), which collates with ICU in UTF-8 locales and by bytes under C/POSIX, so a worker in a different locale from the one that wrotelog/input_hash.jsonre-simulated a finished replicate. The hash and the skip check now live in the exportedlandis_input_hash()andlandis_rep_is_current(); hashes written by earlier versions are still accepted, so upgrading does not re-run completed replicates (it does change everytar_landis()command once, sotargetsre-dispatches each replicate, which then skips). Reproducing an ICU-sorted legacy hash unsets theLC_ALLenvironment variable and setsLC_COLLATEfor the duration of the sort, because R collates by bytes while either pinsC, whateverSys.setlocale()says.
landisutils 0.0.149
-
plot_growth_structures()gainsregen_flags, which marks the panels whose composition holds a species whose extra regeneration pathway cannot operate in the runs being plotted, and names the mechanism in the caption. LANDIS-II arms serotiny and resprouting ONLY from a disturbance-caused cohort death:CheckForPostFireRegen()andCheckForResprouting()are reachable only where the succession extension seesdisturbanceType != null, and a cohort dying of longevity passesnull. A calibration landscape that registers no disturbance extension therefore runs a serotinous or sprouting species with that pathway permanently inert, which is invisible in a carbon trajectory – the reader sees a species that “should” come back after fire, and no fire – and was being carried in the calling project’s prose, which a figure shared on its own does not travel with. Matching is onspecies_setand never oncomposition, which is a display label that would readHw x2as a species namedHw x2; without that column the flags are ignored with a warning rather than mis-applied. Only the flagged species a figure actually shows are named, so the caption cannot send a reader looking for a panel that is not there.
landisutils 0.0.148
-
loss_from_stats()validatesweightsinstead of trusting it, closing the same class of silent wrong answer as thelambda_obsguard in 0.0.147. Passingweights = NULLleft the internal weight vector at its zero initialisation –w[names(NULL)] <- NULLis a no-op – so the weighted total collapsed to 0 while every component was computed correctly, with no error and no warning. An unknown weight name was worse: it GREW the weight vector past the component vector, and the multiply then recycled across mismatched pairs, applying one component’s weight to another. Both now error, as does an unnamed or NA weight. The known component names are defined once as an internal constant shared withcalibrate_dynamic_fire()’scfg$weightscheck, so the two cannot drift apart – they did once before, whensize_tailexisted for four releases without being whitelisted and was silently stripped from everycfg$weights.
landisutils 0.0.147
-
loss_from_stats()no longer counts the Dynamic Fire summary log’s initial timestep as a simulated year. The log reportsTime = 0– the landscape’s initial state, written before any disturbance – which necessarily carries zero fires, son_fires_by_yearhadsim_years + 1rows and every simulated annual rate was understated bysim_years / (sim_years + 1). On a 10-year trial that is 9%, and it lands directly on the count component: measured against a downstream calibration’s 20 validation replicates, the simulated rate went from 25.373 to 27.910 against an observed 27.760, andL_countfell from 0.1264 to 0.0079 – a sixteenfold inflation that had made a well-matched ignition rate look like a 9% shortfall and pushed the search to pin both ignition multipliers at their bounds. Loss values are not comparable across this change, and getting a clean slate needs more than deleting the checkpoint. The evaluation fingerprint digests the calibration’s INPUTS – parameters, weights, replicate count, seed, simulator, image, observed targets, scenario template – but not the loss code and not the package version. This fix therefore leaves it byte-identical, so a checkpoint and its memoized trial-trace rows stay fingerprint-valid and are ACCEPTED into a post-fix run, serving pre-fix losses silently alongside correctly computed ones. Removingcheckpoint.rdsdoes not help, because the memoized losses live in the trial-trace andworker_*.csvfiles, which are folded in separately and recursively fromout_dir. Start the next calibration withresume = "never", which skips that step entirely. -
parse_dynamic_fire_logs()drops that row at the source, so it never reaches any consumer. The correction is also applied defensively where the row is read, so summaries cached before this release are corrected on read rather than silently scoring wrong. -
plot_calibration_fire_counts()was affected by the same row and is fixed with it. One spurious zero per replicate both shifts the plotted mean down and, where annual counts are large, collapses the apparent spread – a zero is an extreme outlier there. On the same data the plotted standard deviation was 9.5 against a true 5.2. The distortion scales with how far a zero sits from the mean, so it is worst exactly where the fire regime is most active. -
loss_from_stats()validatesobserved$primary$lambda_obsinstead of trusting it. When it was absent,L_countevaluated tonumeric(0)andc(count = numeric(0), size = ...)silently DROPPED the element, socomponentscame back one short, its names shifted by one, and the weighted total was computed from mismatched pairs – a wrong answer behind nothing louder than a recycling warning. -
apply_calibrated_ignprob()documents that multipliers above1 / defaultare inert. The product is clamped to LANDIS-II’s required[0, 1], and the defaults are 1.0 for every base butDeciduous(0.5), so a pinned multiplier is saturation rather than an estimate wanting a wider bound – widening it is a no-op.
landisutils 0.0.146
- A family of diagnostic plots for a Dynamic Fire calibration, built on the objects the package already produces:
plot_calibration_convergence(),plot_calibration_loss(),plot_calibration_fire_sizes(),plot_calibration_fire_counts(),plot_calibration_area_by_fuel(),plot_calibration_severity()andplot_calibration_severity_by_size(). Each returns a ggplot object rather than writing a file, and the titles are descriptive rather than conclusory – what a given gap means is a property of the landscape being calibrated, not of this package, so the interpretation stays with the caller. ggplot2 remains in Suggests; the plots check for it and fail with a clear message when it is absent. -
calibration_events()pools the per-replicate fire-event logs into one data frame and adds each event’s burned area in hectares. It also documents that the event log’sMeanSeverityandDamagedSitesare not on the same denominator, which is why the former can fall below 1. -
calibration_plot_palette()exposes the colour roles the plots use, with checked per-role overrides (an unknown role is an error rather than a silent no-op). The defaults are separated for colour vision against each other and against the plot surface. -
plot_calibration_severity()gainsweight_by, because the choice is not cosmetic. The objective’s severity term counts fire EVENTS – one vote per fire, whatever its size – while an observed severity reference is almost always a share of burned AREA. Where severity rises with fire size, the two summaries of the same simulation can disagree about the DIRECTION of the error and not merely its size, soweight_by = "area"is the like-for-like comparison against an area-weighted reference;"events"is the default because it reproduces what was actually scored. Neither recovers within-fire variation, since both summarise a fire by its mean. -
plot_calibration_severity_by_size()plots each simulated fire’s size against its severity and returns the per-size-class share of fires, share of area and mean severity as asize_summaryattribute – the evidence for whetherweight_bymatters on a given landscape.
landisutils 0.0.145
-
plot_growth_candidate()labels only the aesthetics a layer actually maps.size,shapeandlinetypewere labelled unconditionally while all three are mapped conditionally –sizeon a binned series carryingn,shapeon leading species outside density mode,linetypeon reference curves being present – so ggplot2 reported “Ignoring unknown labels” on the builds that omit one. The mapped set is read off the layers rather than restated, so it cannot drift from the conditions that add them.
landisutils 0.0.144
-
plot_growth_candidate()omits the binned points’ white outline in density mode rather than passingcolour = NULL. A NULL fixed aesthetic still reaches the layer, so ggplot2 reported “Ignoring empty aesthetic:colour” on every density-mode build; the series is mapped on colour there and wants no fixed outline at all.
landisutils 0.0.143
-
growth_structure_cell_curves()orders thecompositionlabel by the OLDEST cohort rather than alphabetically, soHw+BaandBa+Hware different mixtures instead of the same one. They are not interchangeable: measured on the Gitanyow landscape, theBa+Hwpool carried 14,555 communities against 1,256 forHw+Ba, so the single alphabetical panel was an average over two populations differing more than tenfold in extent. Ties break alphabetically, so equal starting ages give a deterministic order. - A new
oldest_speciescolumn records which species the cell’s oldest cohort belongs to, carried throughgrowth_structure_summary()andgrowth_structure_cohort_table(). -
plot_growth_structures()selects a species’ panels onoldest_speciesinstead of on mere presence, so a structure appears in exactly ONE species’ figure and the focal species always leads its own labels. Selecting on presence drew every mixture in both species’ figures as the same panel, which read as a duplicate. A summary built beforeoldest_speciesexisted still plots, with the previous behaviour.
landisutils 0.0.142
- new FPSM output helpers
read_fps_raw_out(),fps_pools(),fps_stocks_by_pool(),write_fps_raw_out_parquet()andopen_fps_raw_out_dataset(), the counterparts to the ForCSlog_Summaryhelpers and following the same read / atomic-publish / union-dataset shape. -
fps_pools()drops the terminal simulation year by default. FPSM writes its annual end-of-year stock reports as types 4 and 5 up to the second-to-last year, then a different and partial residual set for the final year (types 1 and 2, decaying pools only), so carrying the final year into a stock series draws a collapse that did not happen. The cut is derived from the data – the last year carrying a type 4 or 5 report – not hard-coded. -
rlangis declared inSuggests.tests/testthat/test-growth_structures.Rcallsrlang::as_label(), whichR CMD checkreports as an unstated dependency; the check workflow runs witherror_on = "warning", so this had been failing CI since 0.0.140 (9789252a, 2026-09-02). Unrelated to the FPSM work, fixed here because it blocks the same build. - all FPSM amounts are reported as tonnes of carbon. FPSM sums
gC/m^2 * cell_length^2 * 1e-6over cells with no division by area, so its output is an absolute landscape total despite the user guide labelling the columnstC/ha; the two coincide only for a 100 m cell. It reports carbon, never CO2e.
landisutils 0.0.141
- new
fps_run_docker()andfps_output_files(), for running the Forest Product Sector Module (FPSM) over a directory holding an FPSM configuration and the two ForCS flux logs it names. FPSM is not a running LANDIS-II extension – itsPlugIn.Run()is an empty stub and the work happens in a console entry point – so it gets plain functions rather than aLandisExtensionsubclass, and the runner omits the version assertion, startup jitter, output streaming and post-completion watchdog thatlandis_run_docker()needs. It keeps the image-digest capture, because that is what identifies the bytes that ran. -
fps_run_docker()performs three pre-flight checks, each guarding a failure that is otherwise silent or obscure: the configured input filenames must exist case-exactly (the shipped FPSM examples namelog_fluxDOM.csvbesidelog_FluxDOM.csv, which is harmless on Windows and fatal elsewhere); each flux log’s header must still match the column positions FPSM indexes, since it validates no headers and would read a reordered column as the wrong quantity; and a non-emptyFPS_log.txtfails the run by default, because that file collects unallocated carbon among other non-fatal problems.
landisutils 0.0.140
-
growth_structure_summary()gainsstart_age_breaks, summarising WITHIN starting-age classes instead of pooling over them. A composition’s cells differ in two ways at once, the parameter combination and the ages the initial-communities map gave their cohorts, and the second dominates: measured on a 14,375-cell structure sweep, starting age explains 92 to 99 percent of the within-composition variance against 0.2 to 3.3 percent for the swept parameters. A band taken over the pool is therefore roughly ten times the parameter effect it is read as showing, and is mostly stand age wearing the parameters’ name.NULL(the default) pools as before. -
growth_structure_cell_curves()carriesstart_age, the oldest cohort the cell begins with, which is whatstart_age_breaksbins. Existing callers are unaffected: it is an added column, and every downstream summarise names its grouping explicitly. -
plot_growth_structures()colours bystart_classwhen the summary carries one, on a viridis ramp that DARKENS with starting age. Within a panel the previouskindcolouring was constant, since a panel is one composition, so it only restated the facet strip. The ramp is truncated at 0.75 because viridis ends in a yellow that sits at 1.23:1 against a light page; multi-hue is a deliberate departure from one-hue-sequential, since eight steps of a single hue are not separable at panel size, and lightness stays monotone so the ordering survives in greyscale and under colour-vision deficiency. Ribbons are suppressed when stratified, as they already were for two variants.
landisutils 0.0.139
-
plot_growth_structures()gainsmax_panels, keeping only the compositions with the most cells behind them. A facet per composition is right when cells hold at most a couple of cohorts and a species appears in a handful of them; where cells carry ten to thirty age classes a species appears in over a hundred, and the panel renders as unreadable slivers with truncated facet strips and overlapping axis labels.NULL(the default) keeps all, so existing callers are unchanged. What was dropped is stated in the subtitle rather than left implied.
landisutils 0.0.138
-
tar_landis()stages the RIGHT branch’s input files. When a pattern maps overscenario_dirbut not over the dependency target, every branch receives all branches’ files, and the basename deduplication then has to choose; files under the branch’s ownscenario_dirwere ordered first so they would win. That prioritisation compared apath_abs()spelling against apath_real()one, so in any project reaching its LANDIS-II tree through a symlink the prefix test never matched, the ordering became a no-op, and EVERY branch staged the first-listed branch’s inputs. A two-batch run produced byte-identical outputs from two different landscapes while each branch’s own correct inputs sat unused on disk; the failure needs at least two branches to appear, so single-branch runs cannot surface it. Both sides are now resolved withpath_real(). -
landis_dep_files()is the resolution, pulled out of thebquote()target command so it can be tested. The bug survived precisely because it was inline there, where no test could reach it. Exported (@keywords internal) because generated target code cannot reach an unexported name without:::; not part of the user-facing API.
landisutils 0.0.137
-
validate_landis_scenario()scopes the initial-communities map-code check to cells the ecoregion map calls ACTIVE. A map code is only reachable where the cell is active, since LANDIS-II never resolves a community for an inactive one, so testing the whole raster read deliberate non-vegetated land-cover codes as missing communities and rejected input that runs.landisbcwrites distinct herb, shrub, bryoid, exposed-land and water codes into inactive cells and none of them has CSV rows, because none of them is a community: measured on an 890,400-cell landscape, 140,597 cells carry four such codes and not one is ecoregion-active, while both scenarios staged from that map run to completion. Without a readable ecoregions map it falls back to the whole raster, which can over-report but never under-report. - This fix was written against 0.0.132 and stranded on an unpushed commit while the package moved on to 0.0.136, so it was absent from every released version. Restored here unchanged apart from a comment.
landisutils 0.0.136
-
write_growth_review_bundle()documentsdensity_min_plots,density_binsanddensity_points_max. They were added with the hex density and left undocumented, which is an R CMD check WARNING; CI runs witherror_on = "warning"and had been failing on it since 2026-08-25. - Declares
countas a global. It is computed bystat_binhex()and reached throughafter_stat(), so it never exists as a binding R CMD check can see.
landisutils 0.0.135
-
growth_structure_cell_curves()COUNTS a repeated species incompositionrather than repeating its name:Hwfor one cohort,Hw x3for three. The repeated form was written for a design holding at most two cohorts; a structure design is capped on SPECIES, so a landscape whose cells carry a dozen age classes produced 255-character labels, unusable as a facet strip and near-unique per cell. The counted form has the same equivalence classes, so nothing regroups. - A
species_setcolumn carries the species actually present, andplot_growth_structures()matches and counts species on it instead of parsingcomposition. The label carries counts now, so a bare species code no longer matches it; a summary without the column still falls back tocomposition. -
growth_structure_summary()andgrowth_structure_cohort_table()carryspecies_setthrough. It is a function ofcomposition, so no grouping changes.
landisutils 0.0.134
-
growth_structure_cell_curves()gainsbiomass, because whataboveground_c_mg_haMEANS differs by extension and cannot be detected from the data. A per-cell output log reports a whole-cell total that the per-cohort join then repeats, so it is de-duplicated ("cell", the default, unchanged). A per-cohort community output reports each cohort’s own biomass, which has to be SUMMED ("cohort"). Getting it wrong is quiet rather than loud: de-duplicating per-cohort values keeps one row per DISTINCT VALUE, so a cell of 16 cohorts carrying 4 distinct biomasses reduces to 4 rows that are neither a total nor a trajectory. -
plot_growth_structures()no longer labels its categories in terms of two. A structure design is capped on SPECIES, not cohorts, so a one-species cell can carry a dozen age classes; the labels are nowsingle cohort,one species, multiple cohortsandmultiple species. The previous wording was correct only for a design that also happened to hold at most two cohorts.
landisutils 0.0.133
-
growth_structure_cell_curves(),growth_structure_summary(),growth_structure_cohort_table(),plot_growth_structures()andread_landscape_cohort_structures()read a structure factorial back.growth_structure_design()andgrowth_calibration_partition()already BUILT the batched design here while every consumer reduced it downstream, which is what let themap_coderenumbering be a trap rather than an invariant: the partitioner numbersmap_codefrom 1 WITHIN each batch, so grouping on it alone merges one cell per batch into a single fictional stand carrying more cohorts than any structure holds.growth_structure_cell_curves()keys on(batch, map_code)and assertsbatchis present, so the mistake is now an error in the package that creates the condition rather than a guard each caller has to rediscover. A single-batch run cannot surface it. - A structure run’s curves carry one row per (cell, timestep, COHORT) because the design they join against is per cohort, while the biomass column is a whole-cell total, so a multi-cohort cell arrives with its trajectory repeated.
growth_structure_cell_curves()reduces that to one row per (cell, timestep) and attaches the cell’s composition as a sorted+-separated label, keeping a repeated species – two cohorts of one species is an age structure, not a monoculture, and collapsing to unique species would label it identically to a single-cohort cell. -
growth_fitting_windows()gainscapsandcap_label, an optional per-species upper bound onmature_to– e.g. the age at which an in-use reference curve plateaus, beyond which it carries no shape left to fit. The three sources now apply in increasing order of authority (derived, thencaps, thenscoring), because a hand-set bound is a constraint rather than a preference and must win over a cap as well as over the derived window.window_sourcereports which one the returned window came from. Previously a caller wanting a cap had to wrap this function and re-implement the entirescoringoverride, which is how one downstream copy came to carry a duplicate of it. -
plot_growth_structures()labelslinetypeonly where a linetype is actually mapped. Labelling an aesthetic no layer uses makes ggplot2 report “Ignoring unknown labels” on every build.
landisutils 0.0.132
-
plot_growth_candidate()gives a legend key only to series it actually draws. The colour scale carriesbreaksandlimitsso the fill and colour legends merge into one, and the cost of that is that a label left in the palette with no layer behind it renders as a key with no glyph. Both such labels can now be absent –density = TRUEreplaces the per-plot points with a hex density carrying its own scale, andbinned = NULLdrops the binned series – so a bundle drawn both ways showed two legend entries for things not on the panel. -
.growth_series_key()builds the per-key styling FROM THE KEYS PRESENT rather than from a fixed order. With a shorter key set the fixed-length vectors failed inside the legend drawing with “replacement has 5 rows, data has 4”, which names neither layer nor scale. -
simplifyCohorts()CONSERVES biomass when it merges pixel groups. Each merged community now takes, per species, the mean of the stand totals of the pixel groups it pools, and divides that among the retained age classes in proportion to age. Previously every age class received its own scaled copy of the mean COHORT biomass, so a community’s biomass grew with the number of age classes pooled into it instead of being conserved; the signature was exact, withsum(CohortBiomass)equal tomax(CohortBiomass) * sum(age / max(age))in 100% of species-in-cell cases. Measured on one landscape whosecohortDatacarries a median stand biomass of 119 t/ha against 116 t/ha observed, the initial communities built from it carried a median 440 t/ha and exceeded the succession extension’s ownmaxBby four to thirteen times; after the fix the same landscape gives 148 t/ha. Shares are computed over the DISTINCT retained age classes becauseprepInitialCommunities()deduplicates its rows. Conservation is per merged community and the mean over pooled pixel groups is unweighted, so landscape biomass is conserved only up to that weighting.
landisutils 0.0.131
-
write_growth_review_bundle()writes a README describing what it ACTUALLY DREW. The panels vary with the arguments – points or a hex density, a binned series or none – and a fixed description drifts from them silently. It had been describing diamonds a caller could switch off by passingreference_curves = NULL, and a fitting-window rule of “0.45 x longevity” that stopped being how the cap is computed in 0.0.124, where it became the species’ own onset of age-related mortality. - Where the binned series is not drawn, the README says so and points at
review-summary.csvfor the residual, rather than omitting it: the series is still what the score is computed against whether or not it is on the panel.
landisutils 0.0.130
-
plot_growth_candidate(density = TRUE)renders. 0.0.128 introduced the density and could not draw a bundle at all; 0.0.129 misdiagnosed the cause and moved the hexes toalpha, which did not fix it. The contended aesthetic is FILL:stat_binhex()mapsfill = after_stat(count)through its DEFAULT aes, which passing a fixedfillargument does not unset, while the binned-median series mapsfillto a discrete label so that its key merges with the colour legend. With both present ggplot2 applies the discrete scale to continuous counts and reports “continuous value supplied to a discrete scale”, naming neither layer. In density mode the binned series now takes a fixed fill and mapscolourinstead, which keeps its key and leavesfillto the hexes. - The regression test BUILDS the plot rather than only constructing it. Both earlier releases passed their tests: a scale collision surfaces at render time, so a test that stops at the ggplot object cannot see it.
landisutils 0.0.129
- Attempted fix for the 0.0.128 render failure by mapping the hex density on
alpha. It did not work; see 0.0.130 for the actual cause. Superseded.
landisutils 0.0.128
-
plot_growth_candidate()gainsdensity, drawing the ground-plot cloud as a WEIGHTED hexagonal density with the best-matched plots kept over it.write_growth_review_bundle()turns it on per species atdensity_min_plots(500 by default), since a bundle routinely spans two orders of magnitude in plot count and the panels stop being readable well before the data run out: one landscape’s lodgepole pine carries 1,639 plots and the cloud is solid ink with a shape key per leading species. The density carries every plot AT ITS WEIGHT, via theplot_weightcolumn, so nothing leaves the figure and the shading reads as evidence rather than as sampling effort; without that column it falls back to counts. NOTE this release could not render; fixed in 0.0.130. -
density_points_maxreplacesdensity_point_weightinplot_growth_calibration(), and is the rule inplot_growth_candidate()too. A fraction of the maximum weight does not control how many points get drawn, because the weight distribution differs by species: at 60% of maximum – the old default – one species keeps 989 of its plots and another keeps 65, and the first buries the density it was meant to annotate. A count draws the same number whatever the shape of the distribution. This is a breaking rename for callers that set the old argument. - The review bundle’s README names Biomass Succession rather than ForCS. It was a copied label; nothing in the bundle was ever ForCS-specific.
landisutils 0.0.127
-
read_landis_raster()asks terra how it reads an ungeoreferenced file instead of assuming, which fixes LANDIS-II output rasters being returned VERTICALLY MIRRORED under terra 1.9-46 and later. LANDIS-II writes no geotransform, GDAL then reports the identity transform with a pixel height of+1, and every terra up to 1.9-34 honoured that by placing the first file row at the bottom – so this package flipped the rows back. terra 1.9-46 (2026-08-22) stopped doing so and returns those rows in file order, and the unconditional flip then introduced the very inversion it existed to remove. Nothing in the package had changed; the correction had simply become a corruption. - The new behaviour is detected rather than gated on a version. Which terra release changed is not something this package can know, and a boundary guessed wrong fails identically but is harder to find. A 15-row LANDIS-II community map whose codes run 3 to 17 down the file ships in
inst/testdata/and is read once per session; the direction the values come back in is the answer. An explicit south-up geotransform is unaffected and still flips, which terra 1.9-46 did not change. - The row-order test no longer asserts which direction terra reads in. That is terra’s business, not this package’s, and asserting it meant a terra release failed the suite of a package whose own behaviour was unchanged. The test now asserts the invariant that matters: what comes back is the order LANDIS-II wrote.
landisutils 0.0.126
-
plot_growth_calibration()gainsdensity, drawing the ground-plot cloud as a WEIGHTED hexagonal density with individual points kept only for the best-matched plots, instead of one point per plot. Off by default, so nothing changes for existing callers. It exists because the panels stop being readable well before the data run out: one landscape’s lodgepole pine carries 3,106 plots and 57 BEC subzone legend keys, and the legend alone took a third of the figure while the cloud was solid ink. The density carries every plot, weighted, so nothing is dropped from view – what changes is that a thousand plots stop competing for the same ink and for a legend key each. Worth it only where the cloud is dense: a species with a hundred plots gets a sparse, blocky grid that says less than the points did, which is why the switch is the caller’s and not automatic. -
density_binsanddensity_point_weightcontrol the grid resolution and which plots are still drawn individually (default: those at 60% of the species’ maximum weight or better). The per-plot colour and shape legends are dropped in density mode along with the points that fed them, and the manual shape scale with them, since a manual scale with no layer behind it warns about levels it cannot find. - Requires ‘hexbin’, declared in Suggests and checked at call time.
landisutils 0.0.125
-
growth_bin_observations()DROPS a bin whose observations all carry zero weight, instead of emitting it with anNAvalue. The weighted quantile of an empty effective sample isNA, and such a bin is not a thin bin but an absent one – every observation in it was excluded by the weighting. Carrying it overstatedn_bins, and becausegrowth_reference_curves()takes the ground-plot LEVEL as amax()over the binned values inside the window, one valueless bin made the levelNAand dropped every plot-scored series for that species. Latent until a window widened far enough to admit one: a species scored on plots alone then returnedn_series = 0and an all-NArow, with nothing to say why. -
growth_reference_curves()computes thatmax()withna.rm = TRUEand returnsNA_real_rather than-Infwhen nothing survives, so a caller supplying its own binned series cannot reintroduce the same failure.
landisutils 0.0.124
-
growth_auto_window()andgrowth_fitting_windows()acceptmort_shpand close each species’ window at that species’ own onset of age-related mortality, instead of at one fraction of longevity shared by every species. Biomass Succession definesMortalityCurveas a POSITION in the lifespan (v7 User Guide 2.12.4), so where a species leaves its plateau varies nearly twofold across the documented range – measured on one calibration, the departure from 95% of peak biomass ran 0.43-0.48 x longevity atMortalityCurve10, 0.63-0.70 at 15 and 0.82-0.84 at 25. A single fraction cannot separate a species that breaks up early from one that holds its stand almost to the end, which is the distinction the parameter exists to make. Thesenescence_frac = 0.45fallback remains for callers that do not supplymort_shp, but its default was calibrated against a parameterisation giving every species aMortalityCurvenear 23 and does not generalise: on a set carrying 10s the earliest 95%-of-peak departure falls to 0.433, below the cap itself. -
growth_mortality_onset_frac()is new and exported: it inverts the User Guide’s definition ofMortalityCurveto the fraction of lifespan at which age-related mortality begins. Callers were open-coding the same arithmetic. -
growth_auto_window()no longer loses a year to floating-point representation error.0.10 + (10 - 5) / 20 * 0.75is 0.28749999999999998, so an onset landing mathematically on a whole year came out a hair below it and the inward rounding took the year before – a cap of 114 where 115 was meant.