Skip to content

9/9 Use the terrain's height range when finding where a seed lands (changes terrain results) - #25

Merged
seanth merged 1 commit into
seanth:terrain-orgfrom
adamf:pr9-terrain-landing-fix
Sep 27, 2026
Merged

seanth merged 1 commit into
seanth:terrain-orgfrom
adamf:pr9-terrain-landing-fix

Conversation

@adamf

@adamf adamf commented Sep 23, 2026

Copy link
Copy Markdown

Part 9 of 9. Builds on PR 8. This PR's changes on their own.

Use the terrain's height range when finding where a seed lands

This one changes results for runs that use a terrain file whose height range is not 50 m. That's why it comes last and on its own: it can be left out without affecting the other eight.

After a seed is thrown (dispersal methods 3 and 4), disperseSeed looks up the ground height where it lands with elevationFromPixel(pixel, theGarden.maxElevation). If that is higher than where the seed started, it searches back towards the plant for a lower landing point. The lookups inside that search called elevationFromPixel(pixel) without the maximum, so they used the default 50 m range instead of the terrain's own.

The search now uses theGarden.maxElevation, like the first lookup. It's a one-line change in vplantr.py.

Tests: only the terrain_water recording changes (its second stage, whose terrain spans 12.5 m), and it is re-recorded here.

🤖 Generated with Claude Code

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx

adamf pushed a commit to adamf/vida that referenced this pull request Sep 24, 2026
**Builds on PR 9 (seanth#25)**, the end of the first series. It doesn't depend on PR 10 or PR 11. [This PR's changes on their own](pr9-terrain-landing-fix...pr12-faster-seeds).

## Faster seeds, shading and deaths, with the same results

About a third off the running time, and **no change to any result**: the characterization tests pass unchanged. A tiny deliberate change to the photon loop (a radius 0.1% smaller) makes three scenarios fail, so they do check this code.

| Run (best of two) | PR 9 | This PR |
|---|---|---|
| 100 m world, 400 seeds, 50 cycles | 13.4 s | 9.1 s |
| 150 m world, 900 seeds, 40 cycles | 14.0 s | 9.2 s |

### Making a seed (`copyForNewSeed` in `vplantr.py`)
PR 6 stopped a new seed copying its parent's whole family tree, but each of the parent's ~100 settings still went through `copy.deepcopy`, 53,000 times a run.
- **Numbers, strings, True/False and None** can't be changed in place, and `deepcopy` gave back the very same value for them anyway. So the seed now shares them.
- **A list of such values** (the colours, the growth records) gets a new list with the same values in it, which is also what `deepcopy` made. The seed still has its own copy, so changing it doesn't change the parent.
- **Anything else** (a list of lists, a dictionary) still goes through `deepcopy`, so nothing a species file could add is shared by mistake.

### Classic shading's photon loop (`vworldr.py`)
- The loop is now its own function, `countPhotonsGettingThrough`, so it can be read (and tested) on its own.
- Each overlapping canopy's x, y, radius and transmittance are looked up once per plant, not once for every photon.
- The distance is worked out inline, with the same `math.hypot` that `pointInsideCircle` uses.
- The random numbers are drawn in exactly the same order, so every photon lands where it did before.

### Removing a dead plant (`kill` in `vworldr.py`)
`kill` looked through the whole soil list twice for the plant being removed: once to see whether it was there, and again to remove it. With thousands of plants and seeds and 44,000 deaths a run, that added up to about a quarter of the running time. Now it finds the plant's place once and deletes it there. Dropping the plant's seeds only adds to the end of the soil, so the place stays right.

### Tests
- The characterization tests pass unchanged; so do all the unit tests.
- A new unit test checks that a seed still gets its own deep copy of anything that isn't a plain list.

### What's left, and Rust
Timed without a profiler, a run's time is now spread fairly evenly over looking after Python objects: finding which canopies overlap, making seeds, and germination. None of it is one tight loop of arithmetic any more.

I tried the photon loop in Rust. It gives exactly the same results: a copy of Python's own random number generator, and `math.hypot` ported step for step. The loop itself was 4 times faster, but it's only about 8% of a run now, so the whole run went from 10.6 s to 10.1 s. That's not worth adding a Rust toolchain to Vida, so it isn't in this PR. It's kept on the [`experiment-rust-photons`](https://github.com/adamf/vida/tree/experiment-rust-photons/rust) branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx
adamf pushed a commit to adamf/vida that referenced this pull request Sep 24, 2026
**Builds on PR 9 (seanth#25)**, the end of the first series. It doesn't depend on PR 10 or PR 11. [This PR's changes on their own](pr9-terrain-landing-fix...pr12-faster-seeds).

## Faster seeds, shading and deaths, with the same results

About a third off the running time, and **no change to any result**: the characterization tests pass unchanged. A tiny deliberate change to the photon loop (a radius 0.1% smaller) makes three scenarios fail, so they do check this code.

| Run (best of two) | PR 9 | This PR |
|---|---|---|
| 100 m world, 400 seeds, 50 cycles | 13.4 s | 9.1 s |
| 150 m world, 900 seeds, 40 cycles | 14.0 s | 9.2 s |

### Making a seed (`copyForNewSeed` in `vplantr.py`)
PR 6 stopped a new seed copying its parent's whole family tree, but each of the parent's ~100 settings still went through `copy.deepcopy`, 53,000 times a run.
- **Numbers, strings, True/False and None** can't be changed in place, and `deepcopy` gave back the very same value for them anyway. So the seed now shares them.
- **A list of such values** (the colours, the growth records) gets a new list with the same values in it, which is also what `deepcopy` made. The seed still has its own copy, so changing it doesn't change the parent.
- **Anything else** (a list of lists, a dictionary) still goes through `deepcopy`, so nothing a species file could add is shared by mistake.

### Classic shading's photon loop (`vworldr.py`)
- The loop is now its own function, `countPhotonsGettingThrough`, so it can be read (and tested) on its own.
- Each overlapping canopy's x, y, radius and transmittance are looked up once per plant, not once for every photon.
- The distance is worked out inline, with the same `math.hypot` that `pointInsideCircle` uses.
- The random numbers are drawn in exactly the same order, so every photon lands where it did before.

### Removing a dead plant (`kill` in `vworldr.py`)
`kill` looked through the whole soil list twice for the plant being removed: once to see whether it was there, and again to remove it. With thousands of plants and seeds and 44,000 deaths a run, `kill` took about a quarter of the running time, mostly in those two searches. Now it finds the plant's place once and deletes it there. Dropping the plant's seeds only adds to the end of the soil, so the place stays right.

### Tests
- The characterization tests pass unchanged; so do all the unit tests.
- A new unit test checks that a seed still gets its own deep copy of anything that isn't a plain list.

### What's left, and Rust
Timed without a profiler, a run's time is now spread fairly evenly over looking after Python objects: finding which canopies overlap, making seeds, and germination. None of it is one tight loop of arithmetic any more.

I tried the photon loop in Rust. It gives exactly the same results: a copy of Python's own random number generator, and `math.hypot` ported step for step. The loop itself was 4 times faster, but it's only about 8% of a run now, so the whole run went from 10.6 s to 10.1 s. That's not worth adding a Rust toolchain to Vida, so it isn't in this PR. It's kept on the [`experiment-rust-photons`](https://github.com/adamf/vida/tree/experiment-rust-photons/rust) branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx
adamf pushed a commit to adamf/vida that referenced this pull request Sep 26, 2026
**Builds on PR 9 (seanth#25)**, for the `-rngstart` option from PR 3. It doesn't touch any of Vida's code, and doesn't depend on PRs 10–12. [This PR's changes on their own](pr9-terrain-landing-fix...pr13-run-many).

## Run several simulations at once (`tools/run_many.py`)

Within one simulation everything happens in a set order, drawing on the same stream of random numbers, so one simulation can't be split across cores without changing its results. But separate simulations don't share anything. This tool runs each one as its own copy of Vida, side by side, so a computer with 4 or 8 cores gets through 4 or 8 times as many runs.

```sh
# the same simulation with -rngstart 1 to 8, four at a time
python tools/run_many.py -rngstarts 1-8 -jobs 4 -- -n forest -w 100 -s 400 -t 50

# a list of different simulations, one line of options each
python tools/run_many.py -file runs.txt
```

- **Names:** with `-rngstarts`, each run is named after the `-n` name and its `-rngstart` (`forest-rng1`, `forest-rng2`, ...). Its output goes where Vida puts it (`Output-forest-rng1/`), and what it prints goes to `Output-forest-rng1.log` next to it.
- **How many at once:** `-jobs` defaults to the number of cores.
- **Summary:** at the end it says how long everything took, and which runs failed, if any.

### Checked
- **Speed:** on a 4-core machine, four 100 m, 400-seed, 50-cycle runs took 41.0 s one after another and 11.7 s side by side.
- **Same results:** each of those runs' `viewer.jsonl` (every plant, every cycle) is identical to a run with the same `-rngstart` on its own, apart from the run's name.
- **Unit tests** cover reading the `-rngstarts` list and the file, and naming the runs.

### Also
The HOWTO gets a short section next to `-x`, which still runs its repeats one after another. The `-x` repeats share one stream of random numbers: the second repeat starts where the first one left off. So they can't simply run side by side with the same results. `run_many` gives each run its own `-rngstart` instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx
adamf pushed a commit to adamf/vida that referenced this pull request Sep 26, 2026
…-g bts view

**Builds on PR 9 (seanth#25).** It doesn't depend on PRs 10–13. [This PR's changes on their own](pr9-terrain-landing-fix...pr14-small-fixes).

## Three small fixes: kill zone selections, placement files, and the `-g bts` view

None of these change a normal run. They only matter for input files with mistakes in them, or for the one graphics view that crashed. All the characterization recordings match except `graphics_outputs`, where the `-g bts` stage now makes its files instead of crashing.

### 1. A kill zone's `selection` missing a part crashed (`vevents.py`)
`readZoneSelection` checked `("attribute" and "logic" and "value") in theSelectionDict`. In Python, `("attribute" and "logic" and "value")` is just `"value"`, so only `value` was checked. A selection without `attribute` or `logic` then crashed with a `KeyError`, instead of printing the warning and ignoring the selection, which is what the code meant to do. Each of the three is now checked.

### 2. A placement file with bad lines could lose good ones (`vplacement.py`)
`checkSeedPlacementList` deleted bad lines from the list while looping over it. That skipped the line after each deleted one, so:
- a bad line straight after another bad one was kept;
- a later bad line could make it delete the wrong line.

It now keeps the good lines in a new list instead.

### 3. `-g bts` crashed (`vgraphics.py`)
The combined bottom + top + side view filled the side view's template, which takes six numbers, with only four. It now gets six, the same way the other side views get theirs. The `.cfdg` files it makes are now pinned by the `graphics_outputs` scenario. I haven't seen the picture itself (the tests don't run the `cfdg` program), and that view's layout code looks unfinished, so it's worth a look.

### Tests
- **Unit tests:**
  - a selection missing each of its three parts is ignored (the old code crashes on two of them);
  - a placement file with several bad lines, two of them in a row, keeps exactly the good ones. This replaces the test that pinned the old behaviour.
- **Characterization:** `graphics_outputs` re-recorded. Its first two stages are identical to before; the third now makes files instead of crashing. The test README's list of bugs found is updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx
@adamf
adamf force-pushed the pr9-terrain-landing-fix branch from ab137cc to e30a21a Compare September 26, 2026 20:31
@seanth
seanth changed the base branch from master to terrain-org September 27, 2026 20:25
@seanth
seanth changed the base branch from terrain-org to terrain September 27, 2026 20:53
@seanth
seanth changed the base branch from terrain to terrain-org September 27, 2026 20:54
**Part 9 of 9.** Builds on PR 8 (seanth#24). [This PR's changes on their own](pr8-web-viewer...pr9-terrain-landing-fix).

This PR used to fix `disperseSeed`'s search for where a seed lands on terrain, which called `elevationFromPixel(thePixelValue)` without `theGarden.maxElevation`, so it used the default 50 m range instead of the terrain's real one.

Sean has since fixed that on terrain-org, together with the direction of the pixel coordinates and the image's pixel range (`getPixelRange`), so the code change isn't needed any more. What's left is the characterization tests' README, which still listed the bug as open: it now says it's fixed.

No code changes; every recording matches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx
@adamf
adamf force-pushed the pr9-terrain-landing-fix branch from e30a21a to c88c760 Compare September 27, 2026 20:59
@seanth
seanth merged commit c5eb204 into seanth:terrain-org Sep 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants