Skip to content

Commit e14f553

Browse files
committed
release 0.12.6: correctness fixes from a full review, and a website theory page
Engine: unbuildable cells are no longer counted as green periphery, isolated centres are tracked correctly, and green access, green corridors and corridor spans treat unbuildable land as a barrier, matching centre access; a new build must itself keep green access; parameter validation hardened; centre seeds on unbuildable land are rejected. A golden test asserts the incremental green arrays equal a fresh rebuild on the final state. Plugin: input layers are snapshotted on the main thread before the background run; the engine install runs as a background task; stranded centre seeds snap to the nearest buildable cell; the confirm button is labelled Run; re-fetching OSM data first releases layers loaded from the previous GeoPackage; grid edge rounding, locale-free load, leap-day temporal setup, GDAL open options and transform checks fixed; dead code removed. Packaging and docs: the plugin zip now carries the LICENSE file; metadata credits D'Acci and Voto and points at the correct homepage; the recommended- plan notes describe the shipped pipeline; the website gains a theory page comparing the published model rule by rule, an extended Departures section, Copernicus and ODbL attribution, per-page titles, and regenerated figures.
1 parent 2d97e9b commit e14f553

90 files changed

Lines changed: 1141 additions & 837 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ jobs:
140140
- name: Build plugin zip
141141
run: |
142142
mkdir -p dist
143+
# the plugin repository requires a licence file inside the zip
144+
cp LICENSE isobenefit_qgis/LICENSE
143145
zip -r dist/isobenefit_qgis.zip isobenefit_qgis \
144146
-x '*/__pycache__/*' '*.pyc'
145147
- uses: actions/upload-artifact@v4

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ If the automatic install is blocked (e.g. a locked-down environment), the dialog
3737
shows the exact command to run yourself, which is simply:
3838

3939
```bash
40-
<qgis-python> -m pip install "isobenefit>=0.12,<0.13"
40+
<qgis-python> -m pip install "isobenefit>=0.12.6,<0.13"
4141
```
4242

4343
## Usage

core/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "isobenefit"
3-
version = "0.12.5"
3+
version = "0.12.6"
44
edition = "2021"
55
license = "AGPL-3.0-or-later"
66
description = "Rust simulation core for the Isobenefit Urbanism QGIS plugin"

core/src/access.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ pub fn agg_dijkstra_dist(
186186

187187
/// Builds the green-periphery (`green_itx`) and green-access (`green_acc`) arrays.
188188
///
189-
/// `green_itx`: built/centre cells -> 1; their rook-adjacent green cells -> 2
190-
/// (the developable periphery). `green_acc`: for every periphery (==2) cell, the
191-
/// accessibility footprint summed together — computed in parallel.
189+
/// `green_itx`: unbuildable cells -> -1; built/centre cells -> 1; green cells
190+
/// rook-adjacent to built -> 2 (the developable periphery). `green_acc`: for every
191+
/// periphery (==2) cell, the accessibility footprint summed together — computed in
192+
/// parallel. Footprints traverse `[0, 1, 2]` only, so unbuildable cells (water,
193+
/// carved corridors) block green access exactly as they block centre access.
192194
pub fn prepare_green_arrs(
193195
state: &Array2<i16>,
194196
max_distance_m: f64,
@@ -198,7 +200,9 @@ pub fn prepare_green_arrs(
198200
let mut green_itx = Array2::<i16>::zeros((rows, cols));
199201
for y in 0..rows {
200202
for x in 0..cols {
201-
if state[[y, x]] > 0 {
203+
if state[[y, x]] < 0 {
204+
green_itx[[y, x]] = -1;
205+
} else if state[[y, x]] > 0 {
202206
green_itx[[y, x]] = 1;
203207
for (ny, nx) in iter_nbs(rows, cols, y, x, true) {
204208
if state[[ny, nx]] == 0 {
@@ -285,6 +289,25 @@ mod tests {
285289
assert!(dist[[0, 4]].is_infinite());
286290
}
287291

292+
#[test]
293+
fn unbuildable_blocks_green_access() {
294+
// built column at x=0, unbuildable column at x=2: periphery footprints
295+
// from x=1 must not cross the barrier to reach x>=3
296+
let mut state = Array2::<i16>::zeros((5, 5));
297+
for y in 0..5 {
298+
state[[y, 0]] = 1;
299+
state[[y, 2]] = -1;
300+
}
301+
let (itx, acc) = prepare_green_arrs(&state, 300.0, 100.0);
302+
for y in 0..5 {
303+
assert_eq!(itx[[y, 2]], -1);
304+
assert_eq!(itx[[y, 1]], 2);
305+
assert_eq!(acc[[y, 3]], 0);
306+
assert_eq!(acc[[y, 4]], 0);
307+
assert!(acc[[y, 1]] > 0);
308+
}
309+
}
310+
288311
#[test]
289312
fn prepare_green_arrs_marks_periphery() {
290313
// a single built cell in a green field -> its rook neighbours become itx==2

core/src/neighbours.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,18 @@ pub fn count_cont_nbs(state: &Array2<i16>, y: usize, x: usize, targets: &[i16])
8181
(total, longest, adds.len() as i64)
8282
}
8383

84-
/// Length of the run of non-built (`<= 0`) cells from `start` along a 1-D line,
85-
/// in the positive or negative direction, stopping at the first built (`> 0`) cell
86-
/// or the array edge.
84+
/// Length of the run of green (`== 0`) cells from `start` along a 1-D line, in
85+
/// the positive or negative direction, stopping at the first non-green cell
86+
/// (built `> 0` or unbuildable `< 0`) or the array edge. Unbuildable land is not
87+
/// usable green, so water or a carved corridor terminates a span rather than
88+
/// extending it.
8789
pub fn green_span(line: ArrayView1<i16>, start: usize, positive: bool) -> i64 {
8890
let n = line.len();
8991
let mut span: i64 = 0;
9092
if positive {
9193
let mut i = start + 1;
9294
while i < n {
93-
if line[i] > 0 {
95+
if line[i] != 0 {
9496
break;
9597
}
9698
span += 1;
@@ -102,7 +104,7 @@ pub fn green_span(line: ArrayView1<i16>, start: usize, positive: bool) -> i64 {
102104
}
103105
let mut i = start as i64 - 1;
104106
while i >= 0 {
105-
if line[i as usize] > 0 {
107+
if line[i as usize] != 0 {
106108
break;
107109
}
108110
span += 1;
@@ -208,6 +210,14 @@ mod tests {
208210
assert_eq!(green_span(line.view(), 4, false), 0);
209211
}
210212

213+
#[test]
214+
fn green_span_stops_at_unbuildable() {
215+
// water/carved corridors terminate a green span; they do not extend it
216+
let line = array![0i16, 0, -1, 0, 0];
217+
assert_eq!(green_span(line.view(), 0, true), 1);
218+
assert_eq!(green_span(line.view(), 4, false), 1);
219+
}
220+
211221
#[test]
212222
fn green_spans_blocks_short_corridor() {
213223
// a single green cell flanked by built on the row -> span 0 both sides on x,

core/src/py_bindings.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ impl PySimulation {
3232
total_iters, random_seed,
3333
))]
3434
fn new(
35+
py: Python<'_>,
3536
state: PyReadonlyArray2<i16>,
3637
origin: PyReadonlyArray2<i16>,
3738
density: PyReadonlyArray2<f32>,
@@ -62,22 +63,30 @@ impl PySimulation {
6263
density_factors_km2,
6364
)
6465
.map_err(PyValueError::new_err)?;
65-
let inner = Simulation::new(
66-
state.as_array().to_owned(),
67-
origin.as_array().to_owned(),
68-
density.as_array().to_owned(),
69-
&centre_seeds,
70-
params,
71-
total_iters,
72-
random_seed,
73-
)
74-
.map_err(PyValueError::new_err)?;
66+
let state = state.as_array().to_owned();
67+
let origin = origin.as_array().to_owned();
68+
let density = density.as_array().to_owned();
69+
// the constructor's green-access build is the heaviest single call; run it
70+
// (and its rayon fan-out) without holding the GIL
71+
let inner = py
72+
.allow_threads(|| {
73+
Simulation::new(
74+
state,
75+
origin,
76+
density,
77+
&centre_seeds,
78+
params,
79+
total_iters,
80+
random_seed,
81+
)
82+
})
83+
.map_err(PyValueError::new_err)?;
7584
Ok(Self { inner })
7685
}
7786

7887
/// Run one iteration.
79-
fn step(&mut self) {
80-
self.inner.step();
88+
fn step(&mut self, py: Python<'_>) {
89+
py.allow_threads(|| self.inner.step());
8190
}
8291

8392
/// Run to completion (or until the population target is reached).

0 commit comments

Comments
 (0)