Reference for all public classes and functions. Every parameter listed
was extracted from the real signature (inspect.signature), not from
memory.
import disslucc as dexposes everything below directly (d.PotentialLinearRegression, etc.)
-- no need to import from submodules.
Land use demand precomputed per step. It's a dissmodel.core.Model
-- registers itself in the active Environment when constructed.
DemandPreComputedValues(
annual_demand: list[list[float]], # [step][class], step 0 = env.now()==0
land_use_types: list[str], # class names, same order as the columns
)Use load_demand_csv() to build annual_demand from a CSV instead of
writing the list by hand.
Implements DemandProtocol:
| Method | Returns |
|---|---|
get_current_lu_demand(i) |
demand for class i at the current step |
get_previous_lu_demand(i) |
demand for class i at the previous step |
get_current_lu_direction(i) |
1 growing, -1 shrinking, 0 stable |
change_lu_direction(i) |
flips and returns the direction (used by Allocation when elasticity saturates) |
Same as DemandPreComputedValues, but takes the matrix directly as a
Python list (values) instead of annual_demand coming from a CSV.
Doesn't exist in the original repository -- convenience for
scripts, no external file needed.
DemandInline(values: list[list[float]], land_use_types: list[str])Demand computed from the land-use layers instead of a table (LuccME's
components of the same names). The start-year area of each class is read
from the layers at construction; the later area(s) from other columns of the
layer (for example f2014); the years in between are interpolated linearly.
Both subclass DemandPreComputedValues, so they implement the same
DemandProtocol. Create them before env.run().
DemandComputeTwoDates(
backend, # layers: land_use_types + final_land_use_types (+ optional "mask")
land_use_types: list[str], # e.g. ["f", "d", "outros"], layers at start_year
final_land_use_types: list[str], # e.g. ["f2014", "d2014", "outros"], same order
start_year: int, end_year: int, # rows = end_year - start_year + 1
final_year: int, # year the final layers describe
cell_area: float, # unit of the demand
direction_for_interpolation: list[str] | None = None, # "Increase" / "Decrease" per class
)
DemandComputeThreeDates(
backend, land_use_types,
middle_land_use_types, # e.g. ["f2011", "d2011", "outros"]
final_land_use_types,
start_year, end_year, middle_year, final_year, # start < middle < final
cell_area, direction_for_interpolation=None,
)Rounding follows LuccME: each year adds the step to the already rounded
previous demand and rounds half up (floor(x + 0.5)); the reference years are
set to their exact area, rounded. So the demand is integer-valued and can
differ by a few units from exact linear interpolation. The final year may lie
after end_year (lab17); beyond it the same step keeps being applied. With
float32 layers the areas carry float32 rounding: use float64 layers when the
demand must match a reference to the unit. The pure functions
interpolate_two_dates / interpolate_three_dates (areas in, rows out) are
exported from disslucc.components.demand too.
Parses a CSV with one column per class, one row per step. Column order
in the CSV doesn't need to match land_use_types -- mapping is by
header name.
from disslucc.components.demand import load_demand_csv
annual_demand = load_demand_csv(Path("demand.csv").read_text(), ["f", "d", "outros"])Potential for change via linear regression (Verburg et al. 1999).
Writes <lu>_pot to the backend -- that's what AllocationClueLike
reads.
PotentialLinearRegression(
backend,
potential_data: list[list[RegressionSpec]], # [region][class]
demand, # object with DemandProtocol
land_use_types: list[str],
land_use_no_data: str | None = None, # array in [0,1]: scales potential down by (1 - value); NOT a binary exclusion mask
region_attr: str = "region", # region array in the backend; created as 1 if absent
)potential_data is an outer list per REGION (the same scenario can
have different coefficients per zone) -- for a single-region scenario,
use a list with one element: [[class1_spec, class2_spec, ...]].
Each step, _adapt_constants() adjusts the regression constant
proportionally to the change in demand -- it's the implicit "learning"
that keeps the model close to the target without manual recalibration.
Transition potential via logistic regression (Verburg et al. 2002).
No raster version exists in the original repository -- this one
was written following the same pattern as PotentialLinearRegression,
it's not a literal translation.
PotentialDLogisticRegression(
backend,
potential_data: list[list[LogisticRegressionSpec]],
land_use_types: list[str],
region_attr: str = "region",
)Writes two arrays per class: <lu>_reg (raw logistic probability) and
<lu>_pot (prob + elasticity, used by Allocation -- elasticity
reinforces the class a cell already has, "locking" pixels into their
current class).
Doesn't implement .modify() -- CLUE-S doesn't use per-potential
feedback, it adjusts a global correction vector in
AllocationDClueSLike instead (same behavior as the original).
Port of LuccME's PotentialCSpatialLagRegression, the potential of LuccME-BR.
For each class: reg = newconst + Σ beta·x + ro·Y, where Y is the mean share
of the class over the cell and its Moore neighbours (each share divided by
1 - no-data share; all-no-data neighbours left out); cells whose
const + Σ beta·x + ro·Y exceeds max_reg get 1, below min_reg get 0; then
reg · (1 - no-data share), and pot = reg - past share. Writes <lu>_pot and
<lu>_reg.
PotentialSpatialLagRegression(
backend,
potential_data: list[list[SpatialLagRegressionSpec]], # [region][class]
demand, # DemandProtocol
land_use_types: list[str],
land_use_no_data: str | None = None, # share of the no-data class, as in PotentialLinearRegression
region_attr: str = "region", # created as 1 if absent
mask_attr: str = "mask", # cells that exist; created as 1 if absent
)Unlike PotentialLinearRegression, the constant adaptation accumulates
(LuccME writes the adapted value back into const every step), and
modify_driver(attr, rate) ports LuccME's modifyDriver (used by
AllocationClueLikeSaturation). The pure function spatial_lag_regression()
in the same module computes one class, for tests and reuse.
CLUE allocation -- elasticity + iterative convergence + proportional
correction between classes + complementar_lu. Cell fraction in
[0,1], not categorical.
AllocationClueLike(
backend,
demand, # DemandProtocol
potential, # PotentialProtocol (only uses .modify())
land_use_types: list[str],
static: dict[str, int], # -1 follows demand | 0 free | 1 fixed, per class
complementar_lu: str, # class that absorbs the remainder (sum always = 1)
cell_area: float,
max_difference: float = 1643, # convergence tolerance (scales with total area!)
max_iteration: int = 1000,
initial_elasticity: float = 0.1,
min_elasticity: float = 0.001,
max_elasticity: float = 1.5,
allocation_data: list[AllocationSpec] | None = None, # min/max per class, in land_use_types order
cell_correction: bool = True, # False reproduces TerraME (see below)
)
cell_correction=True(default) runs_correct_cell_change, which brings each cell's classes back to a total of 1. LuccME meant to do the same, but itscorrectCellChangenever runs (the guard readscell.regionregionAloc, a typo, inAllocationCClueLike.lua). So the default is the intended algorithm and differs from TerraME;cell_correction=Falsereproduces TerraME year by year (tests/test_goldens_per_year.py).After a run,
iterations_per_steplists the convergence-loop iterations of each step, same meaning as LuccME's "Number of iterations" (0 = first allocation accepted).
max_differencehas to be calibrated to the scenario's scale -- the default (1643) is the value used in the real Lab1 (~900k ha). In a synthetic scenario with 900 cells, using the default never triggers the iteration; lower it to something like5.0.
Reads backend.get(lu + "_pot") directly (doesn't call
potential.get_potential() -- that indirection only existed on an
experimental branch of the original repository, not on main).
Cell-by-cell competition allocation with a transition matrix. Each
cell belongs to exactly one class (binary 0/1 array).
New raster version, no equivalent in the original (same situation
as PotentialDLogisticRegression).
AllocationDClueSLike(
backend,
demand, # DemandProtocol
land_use_types: list[str],
transition_matrix: list[list[list[int]]], # [region][from][to], 1=allowed
cell_area: float = 1.0,
max_difference: float = 10.0,
max_iteration: int = 2000,
factor_iteration: float = 0.0001, # correction-vector adjustment rate
region_attr: str = "region",
)Optional tau_<lu> arrays in the backend act as per-cell/class
attraction/repulsion; if absent, tau=0 (default behavior).
After a run, iterations_per_step lists the convergence-loop iterations of
each step (the largest n in LuccME's Iteration -> n log; 0 = first pass
accepted). It matches TerraME in every year of lab15_md10 (0, 67, 56, 56, 61, 61).
Port of LuccME's AllocationCClueLikeSaturation, the allocation of LuccME-BR:
AllocationClueLike's CLUE loop plus a per-cell saturation indicator —
the share of the available area (not no-data, not protected) no longer in
complementar_lu, averaged over the 3 × 3 window with the cell, recomputed each
step. Where it exceeds a class's change_limiar_value, the change in the
demand's direction is halved or capped at max_change_above_limiar.
AllocationClueLikeSaturation(
backend,
demand, # DemandProtocol
potential, # RegionalPotentialProtocol
land_use_types: list[str],
allocation_data: list[list[SaturationAllocationSpec]], # [region][class], static per class
complementar_lu: str,
cell_area: float,
land_use_no_data: str | None = None,
attr_protection: str | None = None, # protected share, left out of the indicator
saturation_indicator: str = "saturationLimiar", # array written each step
max_difference: float = 1643,
max_iteration: int = 1000,
initial_elasticity: float = 0.1,
min_elasticity: float = 0.001,
max_elasticity: float = 1.5,
region_attr: str = "regionAloc",
mask_attr: str = "mask",
order_attr: str | None = None, # LuccME's cell order (see below); default row-major
)Unlike
AllocationClueLike, this is LuccME's owncorrectCellChange, which runs in the Saturation variant (the region test is spelled right there). ItsBACKPis declared outside the loop over cells, so a cell that lowers it lowers it for the cells visited after it: the result depends on the order of the cells, whichorder_attrgives (TerraME visits them in the order of the layer).
iterations_per_stepandmax_error_per_steprecord, per step, what LuccME logs as "Number of iterations" and "Maximum error".
Pontius & Millones (2011) decomposition into quantity + allocation
disagreement. Accepts continuous [0,1] fraction OR 0/1 categorical
-- reduces exactly to the classic discrete formula in the binary case
(proof embedded in validation/pontius.py, run
python3 src/disslucc/validation/pontius.py to see the self-test).
pontius_millones(pred: array-like, ref: array-like) -> {
"n": int, "mae": float,
"quantity_disagreement": float, "allocation_disagreement": float,
"total_disagreement": float, # == mae
}pred/ref are the per-cell value of one class -- to compare
several classes, call once per class.
Accuracy/precision/recall/F1, binarizes at threshold before
comparing. Only makes sense for categorical comparison -- for
genuinely continuous maps, use pontius_millones() without binarizing
(binarizing throws away fractional information).
confusion_metrics(pred, ref, threshold=0.5) -> {
"n": int, "tp": int, "tn": int, "fp": int, "fn": int,
"accuracy": float, # in %
"precision": float, "recall": float, "f1": float,
}Plain dataclasses -- type validation, no logic.
@dataclass
class RegressionSpec:
const: float
betas: dict[str, float] = {}
is_log: bool = False # if True, applies 10**reg before the clip
@dataclass
class LogisticRegressionSpec:
const: float
elasticity: float = 0.0
betas: dict[str, float] = {}
@dataclass
class AllocationSpec:
static: int = -1 # -1 follows demand | 0 free | 1 fixed
min_value: float = 0.0
max_value: float = 1.0
min_change: float = 0.0
max_change: float = 1.0
@dataclass
class SpatialLagRegressionSpec:
const: float # adapted every step, cumulatively
ro: float # spatial autoregressive coefficient
betas: dict[str, float] = {}
is_log: bool = False
min_reg: float = 0.0
max_reg: float = 1.0
@dataclass
class SaturationAllocationSpec:
static: int = -1
min_value: float = 0.0
max_value: float = 1.0
min_change: float = 0.0
max_change: float = 1.0
change_limiar_value: float = 1.0 # saturation above this limits the change...
max_change_above_limiar: float = 0.0 # ...to half, or to thisContracts between roles -- @runtime_checkable, so
isinstance(obj, DemandProtocol) actually works at runtime.
class DemandProtocol(Protocol):
def get_current_lu_demand(self, lu_index: int) -> float: ...
def get_previous_lu_demand(self, lu_index: int) -> float: ...
def get_current_lu_direction(self, lu_index: int) -> int: ...
def change_lu_direction(self, lu_index: int) -> int: ...
class PotentialProtocol(Protocol):
def modify(self, r_number: int, lu_idx: int, direction: int) -> None: ...
class RegionalPotentialProtocol(PotentialProtocol, Protocol): # AllocationClueLikeSaturation
potential_data: list # one entry per region
def modify_driver(self, attr_protection: str, rate: float) -> None: ...Second entry point, with automatic provenance (ExperimentRecord) --
coexists with direct construction in a script, doesn't replace it.
Not imported by import disslucc (heavier import, pulls in
geopandas/rasterization) -- import it explicitly:
from disslucc.executors import LuccContinuousExecutor, LuccDiscreteExecutorBase shared by LuccContinuousExecutor/LuccDiscreteExecutor,
extracted after measuring with diff that validate()/load() were
identical between the two and save() had the same skeleton -- not
premature abstraction. Provides:
validate()-- checksrecord.source.uriandself.required_parameters(list of required keys, declared per subclass)load()-- a.tif/.tiffsource is a GeoTIFF with named bands (anametag per band, asdissmodel.io.save_geotiffwrites): read as it is, with its georeference, after checking that every land use and driver has a band. Any other source is loaded as a GeoDataFrame viaload_dataset, getscolumn_map, and is already rasterized (vector_to_raster_backend). Either way it returns theRasterBackendready to use, not the GeoDataFrame. Same convention as the realLUCCRasterExecutor, checked against the source: rasterizing is expensive, it runs once insideload(), never insiderun().save()-- a default that serves both cases: writes the land-use bands (+"mask", if present) as a GeoTIFF torecord.output_path-- local path ors3://(MinIO), viadissmodel.io.raster.save_geotiff-- thenrecord.metrics.update(result["metrics"]), output checksum (of the written file), status, final log.crs/transformcome straight off theRasterBackend(load()sets both, from the GeoTIFF or viavector_to_raster_backend). Same mechanism asbrmangue-dissmodel'sRasterExecutor.save().
run(data, record) remains abstract -- data arrives as the
RasterBackend already built (not the GeoDataFrame); subclasses just
run the model on top of it and return
{"backend", "land_use_types", "metrics", "final_log"}.
name = "lucc_continuous". Expects in record.parameters:
| Key | Type | Required |
|---|---|---|
land_use_types |
list[str] |
yes |
demand_csv |
str (path) |
yes |
potential_data |
list[dict] (one per class: const, betas, is_log) |
yes |
static |
dict[str, int] |
yes |
complementar_lu |
str |
yes |
allocation_data |
list[dict] (AllocationSpec fields) |
yes |
cell_area, n_steps, resolution, max_difference, land_use_no_data |
-- | no (defaults) |
record.source.uri points to the input shapefile/GeoDataFrame
(rasterized via vector_to_raster_backend, resolution parameter).
name = "lucc_discrete". Same shape, swapping potential_data for
LogisticRegressionSpec specs (const, elasticity, betas) and
static/complementar_lu/allocation_data for transition_matrix
(list[list[list[int]]], [region][from][to]).
name = "lucc_continuous_saturation": DemandPreComputedValues +
PotentialSpatialLagRegression + AllocationClueLikeSaturation, the
continuous model of LuccME-BR. Parameters as the components', plus:
- each
potential_data/allocation_dataentry names its class (lu) and, optionally, itsregion(default 1) — one entry per class per region; record.source.uriis usually a GeoTIFF with named bands (read by the base'sload()): land uses, drivers and, optionally,mask,region/regionAlocand the cell order (order_attr);save_steps: list[int]writes those steps too, as<output>_step<k>.tif.
examples/dissmodel-configs/lucc_saturation.toml is LuccME's lab03;
tests/test_executor_saturation.py runs it through the local CLI and matches
the lab03 golden:
python -m disslucc.executors.saturation run \
--toml examples/dissmodel-configs/lucc_saturation.toml \
--input cellspace.tif --param demand_csv=demand.csv --output out.tiffrom dissmodel.executor import ExperimentRecord
from dissmodel.executor.runner import execute_lifecycle
from disslucc.executors import LuccContinuousExecutor
record = ExperimentRecord(
model_name=LuccContinuousExecutor.name,
source={"uri": "my_shapefile.zip"},
parameters={...},
)
record, timings = execute_lifecycle(LuccContinuousExecutor(), record)
print(record.metrics, record.artifacts, record.source.checksum)See examples/run_lab1_via_executor.py and
examples/run_lab15_via_executor.py for a full example with real data.
The Executors are also the entry point registered in
dissmodel-configs
for running on dissmodel-platform, and (as of the dissmodel fix
below) runnable locally too, via dissmodel.executor.cli.run_cli
(continuous.py/discrete.py each have an if __name__ == "__main__"
block):
python -m disslucc.executors.continuous run \
--toml examples/dissmodel-configs/lucc_continuous.toml \
--input data/input/csAC.zip \
--param demand_csv=data/input/examples_demand_lab1.csvexamples/dissmodel-configs/ has one TOML per executor --
lucc_continuous.toml
and
lucc_discrete.toml
-- each encoding the exact same coefficients as its *_via_executor.py
sibling above (verified: same final metrics, same output checksum for
the continuous/Lab1 case). Everything under [model] besides
[model.parameters] (land_use_types, [[model.potential_data]],
[[model.allocation_data]], static, transition_matrix, ...) is
what gets merged into record.parameters before run() is called --
by the platform when registered in dissmodel-configs, and (as of the
fix below) by dissmodel.executor.cli for local --toml runs too.
Table names match this package's own required_parameters
(potential_data/allocation_data, not the shorter potential/
allocation used in some other dissmodel-configs entries) --
disslucc's executors read those keys as-is, with no renaming layer
of its own.
Fixed in
dissmodel0.6.4. The local---tomlmerge above didn't exist indissmodel's CLI before this was found (only[model.parameters]was ever read intorecord.parameters, matching the docstring's promise but not the code). A fix was written and validated (fulldissmodeltest suite + new regression tests for the merge,mypyclean, and this exactlucc_continuous.toml/lucc_discrete.tomlrun end-to-end through the real CLI with matching output checksums) and shipped indissmodel0.6.4 --dissluccpinsdissmodel>=0.6.4, so both commands above work as written. Seedocs/decisions.md, "TOML config example added for the executors path", for the full history.