Skip to content

feat(sandbox): two copies of every chromosome, and the carrier state - #35

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

SakarZaidan merged 1 commit into
mainfrom
feat/sandbox-diploid

Conversation

@SakarZaidan

Copy link
Copy Markdown
Owner

The trait layer was stating its own limit in prose. Sickle cell carried a note reading "this models one copy" because it had to: one variant copy gives sickle cell trait, two give the disease, and a one-copy model cannot tell the difference.

Reporting a carrier as affected and reporting one as unaffected are both wrong, and those were the only two options available. Now:

sickle cell anaemia:
  100.0%  sickle cell trait, a carrier

  - HBB position 19: one reference copy and one variant copy (1 of 2 copies carry the variant)
  - Inheritance is recessive.
  - One variant copy and one working copy. The working copy is enough, so this is
    the carrier state. A carrier can pass the variant on.

Carrier status is the state most people with a recessive variant are in, and it is
invisible without testing. Two carriers have a one in four chance per child of two
variant copies, which is where the condition appears.

The Punnett square as a result, not a rule

cross samples offspring from one coin flip per chromosome per parent. The classic ratio falls out:

  24.1%  two reference copies
  51.6%  one reference copy and one variant copy
  24.3%  two variant copies

A hardcoded ratio would make test_two_carriers_produce_the_classic_one_two_one_ratio worthless. Arriving at it by sampling is what makes it evidence the model does meiosis rather than reciting an answer.

inherit draws a single child instead, so a student who crosses two carriers and gets an unaffected one has seen why a one in four risk is not a promise in either direction. A square drawn on a board does not show that.

One variant, two consequences

Malaria resistance ships on the same locus, evaluated as a separate trait:

resistance to severe malaria:
  100.0%  substantially protected, without the disease

A student who sees only the disease learns this variant is a defect. One who sees both learns why it is common where malaria is common, which is a fact about selection rather than about defects. The output says so: a variant is not good or bad on its own; whether it helps depends on where you live and what you are exposed to.

Design notes

DiploidGenome is composition. A diploid genome is not a kind of haploid one, and a haploid one is not a diploid with a piece missing, so neither subclasses the other.

MendelianTrait takes a DiploidGenome and is deliberately not interchangeable with Trait. A diploid trait handed a haploid genome would have to invent a second copy, and inventing one is exactly how a carrier gets reported as clear.

A missing copy returns Zygosity.UNKNOWN rather than counting as reference. That is the one place here where the wrong default is silently wrong in the direction that matters: it turns "we do not know" into "you are fine". test_a_missing_copy_is_unknown_and_not_a_reference_copy guards it.

What is absent, and named

No recombination: meiosis is one coin flip per chromosome, so two genes on the same chromosome are always inherited together where a real genome separates them in proportion to distance. Partial linkage is how gene mapping works and it is not here.

No sex chromosomes, imprinting, or mitochondrial inheritance. All three are real, and their absence makes the two parental copies interchangeable in a way they are not in a person.

Both are in the docs' simplifications list and in TODO.md.

Self-review

  • 27 new tests, 120 Python tests total. mypy --strict, ruff and the voice check clean. Rust untouched, 165 still passing.
  • Both documented examples were corrected after being run: one had an import line that would not have worked, and one abridged the output it claimed to show. I then ran every documented snippet verbatim to confirm.
  • test_homozygous_builds_two_independent_copies exists because sharing one Genome object between both copies would make every edit appear on both, which looks like working code until a student edits one allele.
  • All four inheritance modes have a test, including additive, whose docstring admits that "half the effect" and "half the chance of crossing a threshold" are not the same thing.

The trait layer stated its own limit in prose. Sickle cell carried a note
saying "this models one copy" because it had to: one variant copy gives
sickle cell trait, two give the disease, and a model with one copy cannot
tell the difference. Reporting a carrier as affected and reporting one as
unaffected are both wrong, and those were the only options available.

DiploidGenome is composition. A diploid genome is not a kind of haploid
one, and a haploid one is not a diploid with a piece missing, so neither
subclasses the other. MendelianTrait takes a DiploidGenome and is
deliberately not interchangeable with Trait, because a diploid trait
handed a haploid genome would have to invent a second copy, and inventing
one is how a carrier gets reported as clear.

The Punnett square is a result here rather than a rule. `cross` samples
offspring from one coin flip per chromosome per parent, and 1:2:1 falls
out at 24.1 / 51.6 / 24.3 over four thousand children. A hardcoded ratio
would make that test worthless; arriving at it by sampling is what makes
it evidence the model does meiosis. `inherit` draws a single child, so a
student who crosses two carriers and gets an unaffected one sees why a
one in four risk is not a promise in either direction.

Malaria resistance ships on the same locus, which is the other half of
the lesson. A student who sees only the disease learns this variant is a
defect. One who sees both learns why it is common where malaria is
common, which is a fact about selection rather than about defects.

A missing copy returns Zygosity.UNKNOWN rather than counting as a
reference copy. That is the one place here where the wrong default is
silently wrong in the direction that matters, turning "we do not know"
into "you are fine".

What is still absent is named: no recombination, so linkage is absolute
rather than partial; no sex chromosomes, imprinting or mitochondrial
inheritance, so the two parental copies are interchangeable in a way they
are not in a person.

Both documented examples were corrected after being run. One had an
import that would not have worked and one abridged the output it claimed
to show.
@SakarZaidan
SakarZaidan merged commit a46477e into main Sep 16, 2026
11 checks passed
@SakarZaidan
SakarZaidan deleted the feat/sandbox-diploid branch September 16, 2026 17:26
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