mixle.analysis.biodiversity module¶
Biodiversity impact, habitat connectivity, and reclamation offsets (workstream N; N4 + N6).
Two related pieces share this module because they share the same underlying object – an N1
HabitatModel’s fitted suitability field:
N4 – connectivity (
resistance_raster(),least_cost_corridor(),effective_conductance()/habitat_connectivity(),max_flow_connectivity(),fragmentation_impact()): graph resistance on a habitat-cost raster. Suitability is inverted into a per-cell movement cost;mixle.relations.ShortestPathgives the cheapest corridor between two patches, and the equivalent conductance graph feeds two genuinely different connectivity metrics:effective_conductance()(aliased ashabitat_connectivity()) solves the Dirichlet/Laplacian boundary-value problem that circuit theory actually means by “effective conductance” / “effective current” (McRae et al. 2008’s Circuitscape method – fix the source patch cells at potential 1 and the sink patch cells at potential 0, solve for the harmonic potential everywhere else, and read off the current the network carries at that unit potential difference), whilemax_flow_connectivity()is the separate, bottleneck-only maximum-flow quantity over the same conductance graph. An earlier revision used max-flow as the circuit-theory metric (MXR-080-0071): that is wrong, because series conductances add reciprocally while max-flow only sees the tightest cut – the two agree only on a single-edge network. Usehabitat_connectivity()for the population-viability proxy;max_flow_connectivity()is kept only because bottleneck throughput is occasionally independently useful on its own terms, never as a stand-in for the other.fragmentation_impact()scores a candidate development footprint (a mine footprint, in this module’s H3 worked instantiation) by how much it raises corridor resistance / dropshabitat_connectivity()relative to the undisturbed baseline – the population-viability proxy N6 and J’s objective consume. Nothing about the connectivity graph is mining-specific: the same resistance-raster/least-cost-corridor/effective-conductance construction is standard landscape-ecology practice for any development footprint (a road, a subdivision, a mine) cutting through habitat.N6 – reclamation ecology & biodiversity offsets (
habitat_offset_liability(),no_net_loss_constraint()): prices the habitat impact of a development footprint as a liability the same shape as J6’s other priced terms (reclamation/remediation, health, carbon) and emits the companion no-net-loss hard constraint, so biodiversity offsets trade off against grade/cost/carbon inside ONE risk-adjusted objective instead of being a separate side calculation.habitat_offset_liability/no_net_loss_constraintwork off the same “lost habitat-hectare- equivalents” quantity: the fitted suitability field (HabitatModel.mean, i.e.lambda_c) times per-cell area, summed over whatever footprint of cells a candidate disturbance plan (a mine plan, in this module’s worked instantiation) disturbs. Suitability, area, and every economic rate/cost feeding these two functions are validated finite and sign-correct before they reach a solver or accounting payload (MXR-080-0073) – see_lost_equivalents()and the_require_*helpers below.
Every function here reads only duck-typed attributes off habitat (.mean, optionally
.cell_area) or takes plain arrays, so anything satisfying the IC-1 Posterior surface over a
suitability field – in particular N1’s HabitatModel – works; the HabitatModel type hint is a
forward reference (evaluated only under TYPE_CHECKING), so this module has no hard runtime dependency
on mixle.analysis.sdm. effective_conductance() / habitat_connectivity() use SciPy’s sparse
linear solver; max_flow_connectivity() and fragmentation_impact() use this module’s sparse
float-capacity push-relabel implementation. Only least_cost_corridor() calls the frozen
ShortestPath surface, imported lazily because mixle.relations transitively imports mixle.stats
and this module can be loaded while the statistics/analysis import graph is still initializing.
- resistance_raster(habitat, *, floor=1e-3)[source]
Per-cell movement cost from a fitted suitability field:
cost_c = 1 / max(suitability_c, floor).Reads only
habitat.mean(N1’s fitted intensity fieldlambda_c);floorkeeps near-zero suitability cells at a large-but-finite cost (near-impermeable) rather than blowing up toinf, so the resulting raster is always safe to feed straight intoleast_cost_corridor()/habitat_connectivity()without any further cleanup.- Raises:
ValueError – if
flooris not finite and strictly positive, or ifhabitat.meancontains a NaN/Inf/negative entry. An earlier revision silently clamped negative suitability into the same near-impermeable cost as zero suitability (MXR-080-0073), hiding invalid upstream model output; a negative suitability now always raises instead of being clamped. Zero suitability remains a legitimate input (clamped tofloor, as documented above).- Parameters:
habitat (HabitatModel)
floor (float)
- Return type:
np.ndarray
- least_cost_corridor(resistance, patch_a, patch_b)[source]
Cheapest movement path between two cells of a resistance raster (the least-cost corridor).
resistanceis ann-dimensional cost raster (e.g. fromresistance_raster());patch_a/patch_bare flat (row-major/np.ravel) cell indices into it. Delegates tomixle.relations.ShortestPathover the rook-adjacency grid graph with each edge weighted by_edge_cost()(mean resistance of its two endpoint cells); a cell resistance ofinf(a mined footprint,fragmentation_impact()) makes every edge touching it non-finite and so unusable, effectively removing that cell from the graph. Lower resistance = better connected.ShortestPath’s underlying engine (mixle.relations.best_first_paths) is a pure best-first search with no closed/visited set – intentional there, since it is also used to enumerate k best (not just the single best) paths, where revisiting a state via a costlier path is legitimate output. On a cyclic grid graph asked for only its single best path (k=1), that same lack of a closed set is combinatorially explosive (every back-and-forth detour is a distinct, never-pruned path). Sincesuccessors(state)is called exactly once per pop and pops come out in non-decreasing cost order, the first call for a given state is provably its cheapest arrival (Dijkstra’s invariant, valid here because every edge cost is non-negative) – so a small closed set local to this call, never touchingrelations.py, turns the search back into ordinary Dijkstra: each state’s successors are computed once and[](already settled) on every later call.
- effective_conductance(resistance, sources, sinks)[source]
Circuit-theory effective conductance between
sourcesandsinksover a resistance raster.Solves the standard Dirichlet/Laplacian boundary-value problem for landscape connectivity (McRae et al. 2008; the “Circuitscape” formulation): every
sourcescell is held at potential1and everysinkscell at potential0– equivalent to shorting each terminal’s cells together with an ideal conductor, with no arbitrary coupling constant needed (unlike a max-flow super-source/-sink). The remaining free cells’ potentials solve the reduced linear systemL_free @ v_free = -L_free,fixed @ v_fixedfor the graph LaplacianL(edge weight = conductance =1 / edge_cost,_edge_cost()); the total current flowing out of the source terminal at this unit potential difference is the effective conductance (I = G @ V,V = 1), read off directly from the Laplacian: current out of nodeiis(L @ v)[i], which is exactly0at every free node (that is what the linear solve enforces) and the true injected/withdrawn current at every fixed node.This is a genuinely different quantity from maximum flow (
max_flow_connectivity()): conductances in series add reciprocally (two unit conductances in series give an effective conductance of0.5, not1), so a max-flow solver – which only sees bottleneck capacity and ignores series resistance entirely – systematically overstates connectivity whenever current has to cross more than one edge to reach the sink. On a unit-conductance square between opposite corners this function returns the textbook value1; a max-flow solver over the same graph returns2(the sum of the two parallel corner-to-corner paths’ raw capacities, MXR-080-0071).Cells with no path at all to either terminal (an isolated island cut off by
+inf-resistance neighbours on every side) carry no current and are excluded from the linear solve rather than left to make it singular; a raster whosesourceshave no path to itssinksat all correctly yields0.0(the linear system still solves – each side simply settles to its own terminal’s potential everywhere reachable, so no potential gradient, hence no current, exists anywhere).
- habitat_connectivity(resistance, sources, sinks)[source]
Domain-friendly alias for
effective_conductance(), the circuit-theory connectivity metric.Kept under this name because it is the pre-existing public entry point N4/N6/J callers and tests already use; see
effective_conductance()for the full Dirichlet/Laplacian derivation and its McRae et al. (2008) citation. Prior to MXR-080-0071 this function actually computed maximum flow (nowmax_flow_connectivity()) despite the circuit-theory claim in its docstring – a different, systematically-inflated quantity for any network with more than one edge between source and sink. It now computes the real thing.
- max_flow_connectivity(resistance, sources, sinks)[source]
Maximum-flow connectivity between
sourcesandsinksover a resistance raster’s conductance graph.Builds a conductance graph (arc capacity
1 / edge_costbetween rook-adjacent cells) with a super-source oversourcesand a super-sink oversinks, then returnsmixle.relations.max_flow()’s value between them – the maximum flow (bottleneck-limited throughput) the habitat network can carry from the source patch(es) to the sink patch(es).This is NOT the circuit-theory effective-conductance quantity (
effective_conductance()/habitat_connectivity()): max flow only sees the tightest cut and entirely ignores series resistance, so it systematically overstates connectivity whenever current must cross more than one edge to reach the sink (MXR-080-0071). This function exists as a distinctly-named, independently useful bottleneck-throughput metric in its own right – never use it as a stand-in for the other.
- fragmentation_impact(resistance, footprint_mask, sources, sinks)[source]
Habitat-connectivity impact of a development footprint: baseline vs. disturbed corridor/connectivity.
Sets every
footprint_maskcell’s resistance toinf(impassable / zero conductance – an H3 mine plan, in this module’s worked instantiation) and recomputes bothleast_cost_corridor()(between the firstsources/sinkscell, taken as the representative patch-to-patch corridor endpoints – a judgment call: the public API only takes cell sets here, not a single designated pair) andhabitat_connectivity()(over the fullsources/sinkssets) before and after. This is the population-viability proxy N6/J feed into the biodiversity-offset objective: a footprint that severs the only corridor raisescorridor_resistanceand dropsconnectivitysharply; one that misses every real corridor leaves both essentially unchanged.- Returns:
A dict with
corridor_resistance_baseline/corridor_resistance_mined,connectivity_baseline/connectivity_mined,delta(connectivity_baseline - connectivity_mined, i.e. connectivity lost), andmincut_edges– the baseline network’s minimum-cut arcs (mixle.relations.min_cut()), i.e. the single weakest link a footprint would need to sever to maximally damage connectivity.- Parameters:
- Return type:
- habitat_offset_liability(plan_footprint, habitat, *, offset_ratio, unit_offset_cost)[source]
Priced biodiversity-offset liability of disturbing
plan_footprint(a J6 priced-objective term).lost_equivalents = sum_{c in footprint} suitability_c * area_c(suitability = N1’s fittedHabitatModel.mean); the liability isoffset_ratio * lost_equivalents * unit_offset_cost– an additive dollar term the same shape J6’spriced_liabilitiesalready sums for carbon/health/ remediation (workstream-J.md J6).offset_ratio=0orunit_offset_cost=0reduces this to zero, i.e. no biodiversity-offset requirement – both remain valid, meaningful inputs and are not rejected.Because
lost_equivalentsis linear in the boolean footprint, the per-cell rateoffset_ratio * unit_offset_cost * suitability_c * area_cis itself a valid per-block deduction a MILP-based optimizer (H4/J6’srisk_adjusted_plan) can net directly out of expected per-block profit – this function is the scalar evaluator for a given (candidate or solved) footprint.- Raises:
ValueError – if
offset_ratio/unit_offset_costis negative or non-finite, or if_lost_equivalentsrejectsplan_footprint/habitat. A negative ratio or cost used to silently turn habitat damage into a “profit” (MXR-080-0073); both are now rejected. Also if the priced product itself is not representable in float64 – finite inputs near the float64 ceiling used to publish an infinite liability (MXR-080-1572).- Parameters:
- Return type:
- no_net_loss_constraint(plan_footprint, habitat, *, offset_ratio)[source]
Hard no-net-loss constraint payload: created/purchased offsets >=
offset_ratio * lost_equivalents.Returns a dict carrying both the raw quantities (
lost_equivalents,per_cell_lost_equivalents,required_offset) and a solver-agnostic linear-constraint row in this repo’s standardcoeffs @ x <= boundconvention (mixle.relations/mixle.stochastic_opt’sa_ubrows), expressed over a singleoffsets_createddecision variable:coeffs=[-1.0],bound=-required_offsetencodes-offsets_created <= -required_offset, i.e.offsets_created >= required_offset. Placing that row (and theoffsets_createdcolumn it references) into the wider extraction/offset-purchase decision space is H4/J6’s job – this module never edits their MILP variable indexing, only hands them the row.Because that row is emitted ALREADY normalized into the
<=convention, its"sense"label is"<="– the label describes the row that is actually here, not the inequality the row expresses aboutoffsets_created. It used to read">="(MXR-080-1570), which made the payload internally contradictory:mixle.analysis.objective.hard_constraints()andmixle.stochastic_opt.risk_adjusted_plan()both trust the label and negate a">="row into<=form, so a second negation turned the no-net-loss FLOOR into the CEILINGoffsets_created <= required_offset– permitting zero offsets and forbidding any offset purchase above the bare minimum, i.e. exactly the opposite of a no-net-loss guarantee.- Raises:
ValueError – if
offset_ratiois negative or non-finite, or if_lost_equivalentsrejectsplan_footprint/habitat(NaN/negative suitability or area). A NaN or negativelost_equivalentsused to be able to propagate intorequired_offset, silently weakening or inverting a constraint that is supposed to be a hard floor (MXR-080-0073); both inputs are now validated before this payload is built. Also ifrequired_offsetitself is not representable in float64 – finite inputs near the float64 ceiling used to publish an infinite bound, which is not a hard constraint but an unsatisfiable row (MXR-080-1572).- Parameters:
plan_footprint (np.ndarray)
habitat (HabitatModel)
offset_ratio (float)
- Return type: