fix(landscape): func_zoning ranks the original P suitability rasters, not C
The reference notebook ("Three scenarios" cells) and zoning_core.py feed E plus
the five P_* WEIGHTED_CLASS rasters into functional zoning; the diagram folder
300-…-Equals_102_202 pointing at the C layers was a drawing error. C_k =
min(P_k, E) stay diagnostic conflict layers. Node 30 keeps its code/stage;
its changed inputs change every fingerprint, so V1–V3 recompute on the next run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -152,7 +152,7 @@ concurrently.
|
|||||||
| 10 | e_ecological_constraint | `lp-z-integrated`, `lp-u-integrated` | `lp-e` |
|
| 10 | e_ecological_constraint | `lp-z-integrated`, `lp-u-integrated` | `lp-e` |
|
||||||
| 20 | c_conflicts | `lp-p-irrigation`, `lp-p-snow`, `lp-p-crop`, `lp-p-pasture`, `lp-p-tourism`, `lp-e` | `lp-c-irrigation`, `lp-c-snow`, `lp-c-crop`, `lp-c-pasture`, `lp-c-tourism` |
|
| 20 | c_conflicts | `lp-p-irrigation`, `lp-p-snow`, `lp-p-crop`, `lp-p-pasture`, `lp-p-tourism`, `lp-e` | `lp-c-irrigation`, `lp-c-snow`, `lp-c-crop`, `lp-c-pasture`, `lp-c-tourism` |
|
||||||
| 21 | p_max | five P groups | `lp-p-max` |
|
| 21 | p_max | five P groups | `lp-p-max` |
|
||||||
| 30 | func_zoning | `lp-e`, five C groups | `lp-v1-conservative`, `lp-v2-balanced`, `lp-v3-development` |
|
| 30 | func_zoning | `lp-e`, five P groups (original suitability) | `lp-v1-conservative`, `lp-v2-balanced`, `lp-v3-development` |
|
||||||
| 31 | c_max | five C groups | `lp-c-max` |
|
| 31 | c_max | five C groups | `lp-c-max` |
|
||||||
|
|
||||||
Sector order everywhere: 1 irrigation, 2 snow, 3 crop, 4 pasture, 5 tourism.
|
Sector order everywhere: 1 irrigation, 2 snow, 3 crop, 4 pasture, 5 tourism.
|
||||||
@@ -172,9 +172,10 @@ float32 score to a class: `<= 1.8 → 1`, `<= 2.6 → 2`, `<= 3.4 → 3`, `<= 4.
|
|||||||
| e_ecological_constraint | Z ∧ U valid | `max(Z, U)` |
|
| e_ecological_constraint | Z ∧ U valid | `max(Z, U)` |
|
||||||
| c_conflicts | `P_j` ∧ E valid | `min(P_j, E)` per sector |
|
| c_conflicts | `P_j` ∧ E valid | `min(P_j, E)` per sector |
|
||||||
| p_max / c_max | at least one input valid | `max` over the valid ones |
|
| p_max / c_max | at least one input valid | `max` over the valid ones |
|
||||||
| func_zoning | E valid ∧ at least one `C_k` valid | winning sector + 1, else 1 |
|
| func_zoning | E valid ∧ at least one `P_k` valid | winning sector + 1, else 1 |
|
||||||
|
|
||||||
Functional zoning takes `S_k = C_k`. A sector is a candidate when `S_k` and `E`
|
Functional zoning takes `S_k = P_k`, the original suitability rasters (the conflict
|
||||||
|
layers `C_k` are diagnostics and are not zoning inputs). A sector is a candidate when `S_k` and `E`
|
||||||
are valid and the scenario rule holds — V1: `E <= 2 && S >= v1_min_s_e12` or
|
are valid and the scenario rule holds — V1: `E <= 2 && S >= v1_min_s_e12` or
|
||||||
`E == 3 && S >= v1_min_s_e3`; V2: `E <= 2`, or `E == 3 && S >= v2_min_s_e3`, or
|
`E == 3 && S >= v1_min_s_e3`; V2: `E <= 2`, or `E == 3 && S >= v2_min_s_e3`, or
|
||||||
`E == 4 && S >= v2_min_s_e4`; V3: `E <= 4`. With no candidate the pixel is class
|
`E == 4 && S >= v2_min_s_e4`; V3: `E <= 4`. With no candidate the pixel is class
|
||||||
|
|||||||
@@ -112,11 +112,14 @@ every node of all lower stages finished. Every input and output of every node is
|
|||||||
| 10 | e_ecological_constraint | `lp-z-integrated`, `lp-u-integrated` | `lp-e` |
|
| 10 | e_ecological_constraint | `lp-z-integrated`, `lp-u-integrated` | `lp-e` |
|
||||||
| 20 | c_conflicts | `lp-p-irrigation`, `lp-p-snow`, `lp-p-crop`, `lp-p-pasture`, `lp-p-tourism`, `lp-e` | `lp-c-irrigation`, `lp-c-snow`, `lp-c-crop`, `lp-c-pasture`, `lp-c-tourism` |
|
| 20 | c_conflicts | `lp-p-irrigation`, `lp-p-snow`, `lp-p-crop`, `lp-p-pasture`, `lp-p-tourism`, `lp-e` | `lp-c-irrigation`, `lp-c-snow`, `lp-c-crop`, `lp-c-pasture`, `lp-c-tourism` |
|
||||||
| 21 | p_max | five P groups | `lp-p-max` |
|
| 21 | p_max | five P groups | `lp-p-max` |
|
||||||
| 30 | func_zoning | `lp-e`, five C groups | `lp-v1-conservative`, `lp-v2-balanced`, `lp-v3-development` |
|
| 30 | func_zoning | `lp-e`, five P groups (original suitability) | `lp-v1-conservative`, `lp-v2-balanced`, `lp-v3-development` |
|
||||||
| 31 | c_max | five C groups | `lp-c-max` |
|
| 31 | c_max | five C groups | `lp-c-max` |
|
||||||
|
|
||||||
Sector order everywhere: 1 irrigation, 2 snow, 3 crop, 4 pasture, 5 tourism.
|
Sector order everywhere: 1 irrigation, 2 snow, 3 crop, 4 pasture, 5 tourism.
|
||||||
**Decision:** func_zoning takes S_k = C_k (folder `300-…-Equals_102_202`), not P_k.
|
**Decision (revised 2026-09-11):** func_zoning takes S_k = P_k, the original suitability
|
||||||
|
rasters, as in the reference notebook ("Three scenarios" cells) and `zoning_core.py`.
|
||||||
|
The diagram folder `300-…-Equals_102_202` pointing at the C layers was a drawing error;
|
||||||
|
C_k = min(P_k, E) are diagnostic conflict layers and never replace P_k in zoning.
|
||||||
|
|
||||||
Groups and the category `landscape` ("Ландшафтная программа") are seeded by
|
Groups and the category `landscape` ("Ландшафтная программа") are seeded by
|
||||||
migration `00011_landscape.sql` (`INSERT … ON CONFLICT (code) DO NOTHING`). The
|
migration `00011_landscape.sql` (`INSERT … ON CONFLICT (code) DO NOTHING`). The
|
||||||
@@ -140,7 +143,7 @@ numpy does with a float32 array.
|
|||||||
| c_conflicts (5 outputs) | P_j ∧ E valid | `min(P_j, E)` |
|
| c_conflicts (5 outputs) | P_j ∧ E valid | `min(P_j, E)` |
|
||||||
| p_max | at least one P valid | `max` over valid P |
|
| p_max | at least one P valid | `max` over valid P |
|
||||||
| c_max | at least one C valid | `max` over valid C |
|
| c_max | at least one C valid | `max` over valid C |
|
||||||
| func_zoning (3 outputs) | see TZ §4 | see below |
|
| func_zoning (3 outputs) | see TZ §4; S_k = P_k | see below |
|
||||||
|
|
||||||
Functional zoning (TZ `TZ_functional_zoning_go.md`, reference `zoning_core.py`):
|
Functional zoning (TZ `TZ_functional_zoning_go.md`, reference `zoning_core.py`):
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ var Nodes = []Node{
|
|||||||
{Code: "10", Name: "e_ecological_constraint", Stage: 1, Inputs: []string{OutZIntegrated, OutUIntegrated}, Outputs: []string{OutE}, Kernel: kernelE},
|
{Code: "10", Name: "e_ecological_constraint", Stage: 1, Inputs: []string{OutZIntegrated, OutUIntegrated}, Outputs: []string{OutE}, Kernel: kernelE},
|
||||||
{Code: "20", Name: "c_conflicts", Stage: 2, Inputs: append(append([]string{}, pInputs...), OutE), Outputs: cOutputs, Kernel: kernelCConflicts},
|
{Code: "20", Name: "c_conflicts", Stage: 2, Inputs: append(append([]string{}, pInputs...), OutE), Outputs: cOutputs, Kernel: kernelCConflicts},
|
||||||
{Code: "21", Name: "p_max", Stage: 2, Inputs: pInputs, Outputs: []string{OutPMax}, Kernel: kernelMaxAnyValid},
|
{Code: "21", Name: "p_max", Stage: 2, Inputs: pInputs, Outputs: []string{OutPMax}, Kernel: kernelMaxAnyValid},
|
||||||
{Code: "30", Name: "func_zoning", Stage: 3, Inputs: append([]string{OutE}, cOutputs...), Outputs: []string{OutV1, OutV2, OutV3}, Kernel: kernelFuncZoning},
|
// func_zoning ranks the original suitability rasters P1..P5 against E (the
|
||||||
|
// reference notebook and zoning_core.py); the conflict layers C_k are
|
||||||
|
// diagnostics and are not zoning inputs. It stays in stage 3 to keep the
|
||||||
|
// diagram's folder numbering (30x) even though it only depends on stage 1.
|
||||||
|
{Code: "30", Name: "func_zoning", Stage: 3, Inputs: append([]string{OutE}, pInputs...), Outputs: []string{OutV1, OutV2, OutV3}, Kernel: kernelFuncZoning},
|
||||||
{Code: "31", Name: "c_max", Stage: 3, Inputs: cOutputs, Outputs: []string{OutCMax}, Kernel: kernelMaxAnyValid},
|
{Code: "31", Name: "c_max", Stage: 3, Inputs: cOutputs, Outputs: []string{OutCMax}, Kernel: kernelMaxAnyValid},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,3 +132,22 @@ func TestNodeForOutput(t *testing.T) {
|
|||||||
t.Fatal("NodeForOutput(nope) should not be found")
|
t.Fatal("NodeForOutput(nope) should not be found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Functional zoning ranks the ORIGINAL suitability rasters P1..P5 against E
|
||||||
|
// (reference notebook cells "Three scenarios" and zoning_core.py); the
|
||||||
|
// conflict layers C_k are diagnostics and must not replace P_k here.
|
||||||
|
func TestFuncZoningTakesEAndOriginalP(t *testing.T) {
|
||||||
|
n, ok := NodeByCode("30")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("node 30 missing")
|
||||||
|
}
|
||||||
|
want := []string{OutE, SlotPIrrigation, SlotPSnow, SlotPCrop, SlotPPasture, SlotPTourism}
|
||||||
|
if len(n.Inputs) != len(want) {
|
||||||
|
t.Fatalf("node 30 inputs = %v, want %v", n.Inputs, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if n.Inputs[i] != want[i] {
|
||||||
|
t.Fatalf("node 30 input[%d] = %s, want %s (order is E, then sectors 1..5)", i, n.Inputs[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user