Skip to content

feat(sandbox): traits, reported as odds rather than answers - #33

Merged
SakarZaidan merged 1 commit into
mainfrom
feat/sandbox-phenotype
Sep 16, 2026
Merged

SakarZaidan merged 1 commit into
mainfrom
feat/sandbox-phenotype

Conversation

@SakarZaidan

Copy link
Copy Markdown
Owner

A trait takes a genome and an environment and returns a distribution, never a single value. For most traits a single value would be false, and a sandbox that reported "blue eyes" would be teaching the school model with better graphics.

Two lessons ship, chosen because they teach opposite things.

Sickle cell: the clean case

The first thirteen codons here are the real HBB coding sequence, so the sixth codon of the mature protein is GAG at base 18 and changing its middle A to T gives GTG. Glutamate to valine, checkable by anyone.

reference: MVHLTPEEKSAVT
sickle   : MVHLTPVEKSAVT

Truncating the gene is a simplification; moving the variant would have been an error, so the position is exact and the length is not. A test asserts the substitution is Glu6Val and nothing else, which is what catches a coordinate drifting.

The explanation is derived, not recited. An amino acid table carries Kyte and Doolittle hydropathy and formal charge, and substitution_effect works out what changed:

Glutamate is negatively charged and hydrophilic (-3.5); valine is uncharged and hydrophobic (+4.2). The charge at this position changes by +1... the residue goes from hydrophilic to hydrophobic. On the surface of a protein that matters a great deal, because a hydrophobic patch where there was none gives the protein somewhere to stick.

Because it comes from the table, the same reasoning works for a substitution a student invents.

Eye colour: the correction

Subplan 33 was specific that the school model must not be reproduced. The brown-dominant, blue-recessive model is not a simplification of the biology, it is wrong, and this says so in the output rather than in a footnote:

The brown-dominant, blue-recessive model taught in most schools is not a simplification of this. It is wrong. Two blue-eyed parents having a brown-eyed child is uncommon but entirely possible, which the simple model says cannot happen.

Roughly 16 genes contribute, and the largest, rs12913832, is not in a pigment gene at all: it sits in a regulatory region of HERC2 and controls OCA2 expression.

test_eye_colour_is_never_certain checks all eight variant combinations exhaustively and fails if any reaches 95 percent. That is the test most likely to matter later: if one ever did, someone had tuned an effect size until a demo looked convincing, and the lesson would have inverted into the model it exists to correct.

The bug a test caught

A genome carrying no variants came back with its own baseline distorted: 55/30/15 became 67/23/10.

Odds are a two-class idea. Converting each of three mutually exclusive classes to odds against "everything else" and then renormalising is not the identity. Effects are additive on the log probability scale, where adding to one class and renormalising multiplies its odds against every other class by exp(effect), so with two outcomes it is exactly the log-odds shift the literature reports.

The same fix made a rare baseline shiftable again. With log odds, a baseline clamped at 1e-6 sat at -13.8 while the other class sat at +13.8, so a +20 effect moved it to 0.05 percent. test_a_genome_with_no_variants_reproduces_the_baseline now guards the identity directly.

What is reported as missing

Alongside every result, not in documentation: the other thirteen eye colour genes, the diploid nature of real genomes, and the fact that rs12913832 acts through regulation rather than by changing a protein. Sickle cell carries the same note about recessiveness, since this sandbox holds one copy of each gene and one copy of that variant gives sickle cell trait rather than the disease.

Self-review

  • 26 new tests. The chemistry ones check facts that can be looked up: the Kyte and Doolittle scale really does run -4.5 to +4.5, and exactly D, E, K and R carry charge.
  • Three test expectations were wrong on first run. One was mine (a docstring on a module-level assignment is not readable at runtime, so the diploid caveat moved into the trait's output where a student actually sees it, which is better anyway). One was arithmetic I had not worked through. One was the real bug above.
  • The documented example output was regenerated after the fix, because a doc showing numbers the code no longer produces is worse than no example.
  • Trait is a Protocol like Process, with a test asserting a student class needs no base class.
  • 73 Python tests, 165 Rust. mypy --strict, ruff, and the voice check clean.

Not in this PR

The drug design puzzle from subplan 33, which needs the permanent Uncalibrated marker wired through before anything is scored. Huntington's repeat-count dose response, which is the natural third lesson and needs repeat expansion in Sequence first.

A trait takes a genome and an environment and returns a distribution,
never a single value. For most traits a single value would be false, and
a sandbox that reported "blue eyes" would be teaching the school model
with better graphics.

Two lessons ship, chosen because they teach opposite things.

Sickle cell anaemia is the clean case. The first thirteen codons here are
the real HBB coding sequence, so the sixth codon of the mature protein is
GAG at base 18 and changing its middle A to T makes GTG. Glutamate to
valine, verifiable by anyone.

The explanation is derived rather than recited. An amino acid table
carries Kyte and Doolittle hydropathy and formal charge, and
substitution_effect works out that the residue loses a negative charge
and goes from hydrophilic to hydrophobic, which puts a sticky patch on a
protein surface. The same reasoning then works for a substitution a
student invents.

Eye colour is the correction. The brown-dominant, blue-recessive model is
not a simplification of the biology, it is wrong, and the sandbox says so
in the output rather than in a footnote. Roughly 16 genes contribute and
the largest, rs12913832, is not in a pigment gene: it sits in a
regulatory region of HERC2 and controls OCA2 expression. A test asserts
that no combination of modelled variants reaches 95 percent, because if
one did, someone had tuned an effect size until a demo looked convincing
and the lesson had inverted into the model it corrects.

A test caught a real bug in the odds arithmetic. A genome carrying no
variants came back with its own baseline distorted: 55/30/15 became
67/23/10. Odds are a two-class idea, and converting each of three
mutually exclusive classes to odds against everything else and then
renormalising is not the identity. Effects are additive on the log
probability scale, where adding to one class and renormalising multiplies
its odds against every other class by exp(effect), so with two outcomes
it is exactly the log-odds shift the literature reports. The same fix
made a rare baseline shiftable again, which it had not been.

What the model leaves out is reported alongside what it includes: the
other thirteen genes, the diploid nature of real genomes, and the fact
that rs12913832 acts through regulation rather than by changing a
protein. Sickle cell says the same about recessiveness, since this
sandbox holds one copy of each gene and one copy of that variant gives
trait rather than disease.
@SakarZaidan
SakarZaidan merged commit d87dcfe into main Sep 16, 2026
11 checks passed
@SakarZaidan
SakarZaidan deleted the feat/sandbox-phenotype branch September 16, 2026 17:08
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.

1 participant