Skip to content

12 Faster seeds, shading and deaths, with the same results - #28

Merged
seanth merged 1 commit into
seanth:terrain-orgfrom
adamf:pr12-faster-seeds
Sep 27, 2026
Merged

seanth merged 1 commit into
seanth:terrain-orgfrom
adamf:pr12-faster-seeds

Conversation

@adamf

@adamf adamf commented Sep 24, 2026

Copy link
Copy Markdown

Builds on PR 9 (#25), the end of the first series. It doesn't depend on PR 10 or PR 11. This PR's changes on their own.

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 branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_013425y5x98TqAv3BvHodpdx

**Builds on PR 8 (seanth#24)**, the end of the first series (PR 9's fix is already in terrain-org). It doesn't depend on PR 10 or PR 11. [This PR's changes on their own](pr8-web-viewer...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 8 | This PR |
|---|---|---|
| 100 m world, 400 seeds, 50 cycles | 9.1 s | 5.8 s |
| 150 m world, 900 seeds, 40 cycles | 11.2 s | 7.6 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
@seanth
seanth changed the base branch from master to terrain-org September 27, 2026 21:08
@seanth
seanth merged commit 763718f into seanth:terrain-org Sep 27, 2026
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