A coupled linear program that derives per-transition flows from per-class area targets.
It is assembled one constraint block at a time; every block has an add_ method of the
same name, and lp_problem keeps the resulting rows tagged with that name so a program
can be solved over a subset of its blocks. That is what makes the reachability precheck
the same program as the solve, rather than a second implementation of it.
Units
The program is solved in shares of the landscape: shares and absolute areas are the same
program up to a scalar, but the share version spans six fewer orders of magnitude.
Everything the object returns is in cells, rehydrated against the number of cells
lulc_data holds at the anchor period.
Periods
Following trans_rates_t, the flows of period p take the landscape from its state at
p - 1 to its state at p. Area variables therefore exist at the anchor period – the
last observed one – and at every extrapolated period, while flow variables exist at the
extrapolated periods only.
Variables
All variables are non-negative. id_lulc identifies the class a class-level variable
belongs to; id_lulc_anterior and id_lulc_posterior identify a transition-level one.
| block | keys | meaning |
flow | transition, period | cells moving along a transition |
area | class, period | area of a class at a state |
rate_lower, rate_upper | transition, period | rate-bound violation |
historic | transition, period | distance from the historic outflow pattern |
shape, smoothness | class, period | curvature violation |
target_over, target_under | class | terminal fit either side of the target |
fairness | none | worst per-class rate-bound violation |
Constraint blocks
initial, conservation and closure are the mass balance; forbidden and
rate_limits are the hard statements about what a transition may do; the rest carry
slack variables and are paid for in the objective. blocks records which are enabled,
which enter a solve and which enter the reachability precheck.
Installation
Solving needs lpSolve, which evoland only suggests: most of the package never solves a
linear program. trans_rate_lp$new() fails with an actionable message if it is missing,
so install it before constructing one – install.packages("lpSolve").
Super class
lp_problem -> trans_rate_lp
Active bindings
blocksThe constraint blocks, whether each is enabled, and which programs each takes part in.
scenarioPer class: the initial and target areas, the elicited shape and the objective weight.
transitionsEvery ordered pair of classes with its rate bounds, whether it is viable, and whether the program forbids it.
stepsThe extrapolated periods, the state each starts from, and its length.
reachabilityPer class and period: the areas reachable under mass balance and hard historic rates, ignoring targets. Solved on demand and then cached.
areasThe solved class areas, in cells, per class and state.
flowsThe solved transition flows, in cells.
ratesThe solved flows as rates of their anterior class, which is what
adjusted_trans_pot_v()and the allocators consume.diagnosticsHow far the solution had to depart from the target and from observed history. These are results, not debug output: a solver that does not report how far outside history it went is actively misleading.
Methods
Inherited methods
trans_rate_lp$new()
Set up the program from an observed landscape and a scenario demand.
Constraint blocks are added immediately, so the object is ready to solve. Requires
the suggested lpSolve package to be installed.
Usage
trans_rate_lp$new(
lulc_data,
bounds,
periods,
targets = NULL,
shapes = NULL,
lambda_bounds = 0.1,
mu_shape = 15,
mu_smooth = 1,
mu_target = 1000,
mu_historic = 0,
margin = 0.01,
terminal_band = NA,
shape_strictness = 0,
monotone = TRUE,
fairness = TRUE,
forbid_non_viable = TRUE,
max_reachability_ratio = 10
)Arguments
lulc_dataA lulc_data_t for a single
id_run. The areas of its last observed period are the initial state.boundsPer-transition rate bounds, see
trans_rate_bounds().periodsA periods_t. Its extrapolated periods are the steps to solve for, and their lengths set the time scale of the trajectory-shape constraints.
targetsA data.table with
id_lulcand eitherarea(cells on the same grid aslulc_data) orshare(of the landscape, rehydrated against it). Without targets the object can only answerreachability.shapesA data.table with
id_lulcandshape, one of"instant growth","delayed growth","constant change","instant decline","delayed decline". Note that a straight line satisfies every one of these one-sided curvature constraints, so shapes only bind whenshape_strictness > 0.lambda_boundsPenalty weight on rate-bound violation.
mu_shapePenalty weight on trajectory-shape violation.
mu_smoothPenalty weight on the second difference of the trajectory; a tie-breaker among otherwise equivalent trajectories.
mu_targetPenalty weight on the L1 distance between the solved terminal area and the target. Without it, a hard terminal band is treated as free real estate and every class parks on a band edge.
mu_historicPenalty weight on the L1 distance between flows and the historic outflow pattern (
ref_rate). Zero by default; raising it keeps flows near the observed pattern where the target does not force otherwise.marginSlack around the rate bounds before a violation is penalised.
terminal_bandRelative half-width of a hard band around the terminal target, or
NA(the default) to rely onmu_targetalone. A hard band andforbid_non_viabletogether turn an out-of-reach target into an infeasible program rather than a near miss: on the SSP-CH demand that combination is infeasible for three of five scenarios, while the L1 fit lands as close as the viable transitions allow and reports the shortfall.shape_strictnessMinimum curvature a shaped trajectory must exhibit, as a fraction of the class's mean per-step change.
monotoneWhether to hard-constrain each class to move monotonically in the direction of
sign(target - init).fairnessMinimax bound on the worst per-class rate-bound violation.
TRUEuseslambda_boundsas its weight; a number sets the weight explicitly.forbid_non_viableWhether to hard-zero flows on transitions that trans_meta_t marks as non-viable. Such flows have no trans_pot_t rows and would be silently dropped at allocation time, so the trajectory would not materialise.
max_reachability_ratioRefuse to solve if a target asks for more than this multiple of the historically achievable change. Everything below the threshold is reported, not gated.
trans_rate_lp$add_all()
Add every enabled constraint block, by calling the add_ method of the
same name.
trans_rate_lp$add_conservation()
The classes cover the whole landscape at every state. Redundant given closure in both directions, but a cheap numerical anchor.
trans_rate_lp$add_closure()
Every cell of a class leaves it along exactly one transition, and every
cell of a state arrived along one. Together with initial this conserves area
exactly.
trans_rate_lp$add_forbidden()
A non-viable transition carries no flow at all. It is zero, not merely
expensive: it has no trans_pot_t rows and would be dropped at allocation time. One
row per transition and period – a single row summing the periods says the same
thing about non-negative flows, but it is dense enough for lpSolve::lp()'s default
scaling to fail on it numerically.
trans_rate_lp$add_rate_limits()
No transition moves faster than it ever has, as a hard bound with no
slack and no margin, and with persistence left free. This is the loosest honest
question about what a class can reach, and it is the block reachability solves
over. It is deliberately absent from a solve, where the same statement appears as
the softly penalised rate_bounds.
trans_rate_lp$add_rate_bounds()
No transition moves much faster or much slower than it historically
has, as a softly penalised bound widened by margin. Soft because elicited targets
routinely require flows outside the observed envelope; how far outside is reported
in diagnostics rather than suppressed.
trans_rate_lp$add_historic()
Flows stay near the historic outflow pattern, as an L1 penalty on
|flow - ref_rate * area|. This is the term that stops the program inventing
transitions that happen to be cheap.
trans_rate_lp$add_target()
The landscape ends where the scenario asks it to: an L1 fit that
degrades gracefully, plus a hard band when terminal_band is set.
trans_rate_lp$add_monotonicity()
Each class moves in the direction of its target and does not turn back, as a hard constraint with no tolerance.
trans_rate_lp$add_shape()
Each class trajectory curves the way its elicited shape says, as a
softly penalised one-sided constraint on the change in the per-year rate of change.
"instant" front-loads the change and "delayed" back-loads it; "constant change" asks for both at once, which is what makes it an equality.
trans_rate_lp$add_smoothness()
Each class trajectory prefers a small second difference. A tie-breaker among the many trajectories that satisfy everything else, not a modelling statement.
trans_rate_lp$add_fairness()
No class carries a much worse rate-bound violation than the others. A minimax of linear expressions is itself linear, so this needs no quadratic solver.
trans_rate_lp$solve()
Solve for the flows that take the landscape to its targets, after checking that the targets are not grossly beyond what history supports.
trans_rate_lp$trans_rates_t()
trans_rates_t The solved rates as a trans_rates_t for the indicated id_run.
Persistence and non-viable transitions are dropped.
Examples
periods <- create_periods_t("P10Y", "1990-01-01", "2020-01-01", "2040-01-01")
lulc_data <- as_lulc_data_t(data.table::data.table(
id_run = 0L,
id_coord = 1:10000,
id_period = 4L,
id_lulc = rep(1:2, c(6000, 4000))
))
bounds <- data.table::data.table(
id_trans = c(NA, 1L, NA, 2L),
id_lulc_anterior = c(1L, 1L, 2L, 2L),
id_lulc_posterior = c(1L, 2L, 2L, 1L),
min_rate = c(0.9, 0, 0.95, 0),
max_rate = c(1, 0.1, 1, 0.05),
ref_rate = c(0.95, 0.05, 0.97, 0.03),
is_viable = TRUE
)
solver <- trans_rate_lp$new(
lulc_data = lulc_data,
bounds = bounds,
periods = periods,
targets = data.table::data.table(id_lulc = 1:2, share = c(0.5, 0.5))
)
solver$solve()
solver$areas
#> id_lulc id_period area
#> <int> <int> <num>
#> 1: 1 4 6000
#> 2: 2 4 4000
#> 3: 1 5 5500
#> 4: 2 5 4500
#> 5: 1 6 5000
#> 6: 2 6 5000