# DKE Library — Tutorial
**Version:** 1.0.4, changed in rev 163 · alpha · API 1.4 · rev 170\
**Status:** Tutorial for the DKE Library.\
**Audience:** Customers putting a published reasoning module to work on their
own facts.\
**This document as Markdown:** [dke-library-tutorial.md](/tutorial/dke/library/dke-library-tutorial.md) — the same text
this page is rendered from, for readers and tools that would rather have
the source than the page.
The DKE Library is the code we publish for you to install and edit: reasoning
modules, which supply the reasoning; `dkepy`, the Python package that writes
your facts; and `dkekey`, the tool that issues a store key on your own machine.
This tutorial teaches what a reasoning module is, how to put one to work on your
own facts — by hand, or with `dkepy` when a program produces them — and where
its answers stop being trustworthy. The
[Library Reference](/ref/dke/library/) is the lookup — kinds, fields, derived
fields and queries, module by module, then the public names of `dkepy`.
It assumes you have read enough of the
[Python Tutorial](/tutorial/dke/python/) to know what a claim is.
---
## Chapter 1 — Two steps, and both of them are yours
Reasoning about anything takes two steps.
1. **Work out what is true, and record it.** You write claims to your store one
at a time, with a write verb — `update` if you mean to run the producer
again, which you usually do (Chapter 3).
2. **Write the DKE Python that reasons over those claims** — the classes, the
standing rules, the queries.
Analysing a codebase is exactly that: read your source, record what you found,
then compile the code that answers questions about it.
**A reasoning module is the second step, already written.** For some domains we
have written that code for you. You download the file — `code` is at
[dke.langsyn.com/source/code/latest/code.dpy](https://dke.langsyn.com/source/code/latest/code.dpy)
— compile it into your store once, and import it from then on:
```python
import code
```
That is the whole of what a reasoning module is. Nothing about it is privileged:
it is an ordinary `.dpy` file, the same kind you could have written, and one you
write yourself behaves identically. You can read every line of it before you
compile it, and you can edit it — it is yours once you have it.
The first step stays yours. **Nothing here knows anything about your subject
matter until you record it** — importing `code` does not scan your repository,
because it has no access to your repository. It installs the vocabulary and the
reasoning; you supply the facts.
---
## Chapter 2 — What you get when you import one
`import code` resolves against your store, and the only modules a store has are
the ones you compiled into it. So the module you get is the one you put there —
whether you downloaded it from us or wrote it yourself.
What arrives is up to five kinds of declaration:
- **Classes** — the kinds of thing the domain has, and the fields they carry.
`code` brings `Function`, `Class`, `Call`, `UnresolvedCall`, `Rationale`,
`Import`, `Dependency` and `Counterpart`.
- **Derived fields** — values the engine works out from the ones you wrote, and
keeps current as those change. You never write a derived field.
- **Standing rules** — conclusions that hold exactly while their condition holds
over your data, and withdraw themselves when it stops holding.
- **Standing constraints** — conditions your data is expected to meet. `checkup`
names every subject that breaks one, and a write that breaks one is not
refused. Of the published modules `agenda`, `quantum` and `pqc` have some
(Chapters 11 to 13).
- **Queries** — `def`s you call by name, which answer over whatever facts you
have loaded.
Names are scoped to the module, so `code.Function` and `finance.Function` are
different kinds that never read each other's data. Two modules can each have a
`Function` without either knowing about the other.
---
## Chapter 3 — Loading facts
`code` reasons over data in your store. Data gets there the way all data does —
one claim at a time — and `code` neither knows nor cares what produced it. There
is no import step and no format to convert to. Analyse your source however you
like, then write what you found:
```python
import code
update(code.Function.parse.name, "parse", "my-analysis")
update(code.Function.parse.module, "compiler", "my-analysis")
update(code.Function.parse.lines, 42, "my-analysis")
update(code.Function.parse.branches, 8, "my-analysis")
update(code.Function.parse.loops, 4, "my-analysis")
update(code.Call.c1.caller, "main", "my-analysis")
update(code.Call.c1.callee, "parse", "my-analysis")
update(code.Function.parse.called, True, "my-analysis")
```
That last line looks redundant next to the `Call` above it, and it is not.
`uncalled()` reads a field of the function, not the `Call` kind, so the answer
has to be recorded on the callee to be a fact about it. Write both for every
edge you record: the `Call` is what `callers_of` and `impact_of` walk, and
`called` is what stops `uncalled()` from returning a function the store plainly
shows being called.
A third party can do the analysis, or a script you write, or your CI. The store
sees claims either way, and the provenance you pass — the third argument —
records where they came from.
**Why `update` and not `remember`.** Both write a claim, and neither is a
fallback for the other. `remember` asserts something new; `update` says *revise
my earlier reading*, and establishes a value on an empty cell so it also works
the first time. A producer you mean to run again — a script, a CI step, anything
pointed at a codebase that changes — means the second, so it should ask for it
by name. `remember` is permissive and a re-run does succeed either way; what you
lose by leaning on that is a reader of your program being able to see which you
meant. Chapter 8 says submitting a module again reconciles it; this is the same
idea one layer down, for the facts.
Supersession is per SOURCE. `update` replaces what *this* source said and leaves
every other source's claim standing, so re-running your producer never quietly
settles a disagreement someone else recorded.
### When a machine writes the facts
Writing the calls by hand is right for a handful of facts, and for learning what
the cells are. When a program produces them — a reader over your source tree, a
CI step, anything you point at data that changes — there is a package that
builds the program for you: **`dkepy`** (distribution `dkepy`, import name
`dke`), published at .
```python
from dke import FactProgram
prog = FactProgram("code", source="my-analysis")
prog.open()
prog.lines.append(prog.put("Function.parse.lines", "42"))
print(prog.render())
```
It emits the same program you would have written, and the output is text you can
read before you run it. What it saves you is the three things that go wrong
quietly when a machine writes facts: a subject that moves between runs, so a
re-run records a second set beside the first instead of revising it; the write
verb, which it always emits as `update`; and a value interpolated into the
program without escaping, where one quote in your data ends the string early and
changes what the REST of the program says.
It also carries readers for this module's own domain, `dke.ingest`, which read
source code and emit `code` facts — both a working producer and the worked
example.
If you write a reader of your own, its subjects will usually come from
qualified names like `app.parse`, which is not a subject as written.
`sanitize("app.parse")` reduces one to a subject, `app_parse`. That loses
something — `app.parse` and `app_parse` reduce to the same subject — so hand
names out through an `Ids` instead: `ids.get(name)` answers the same subject
for the same name for as long as you keep the instance, and when two names
reduce to one subject, the second is given a short suffix of its own rather
than sharing the first one's cells. Which of the two gets the suffix depends on
which you ask for first, so ask in the same order on every run — sorted, say —
and a re-run lands on the same subjects. `Ids` and `sanitize` import from the
package root, beside `FactProgram`.
**Sending the program is a separate, deliberate step.** Nothing that produces
facts opens a socket — not `FactProgram`, not `Ids`, not the readers — so
producing facts needs no key, and you can run a reader over your source without
granting it anything. The one module that talks to the network is
`dke.client`, which nothing that produces facts imports; you have it only by
naming it: `from dke.client import Client`, then
`Client.from_env().run(prog.render())`. `from_env` reads your key from
`DKE_API_KEY`, and raises an error when it is not set. `run` submits the
program to your store, as the `run` tool of the
[MCP Reference](/ref/dke/mcp/) does, and returns a result whose `ok` says
whether the work succeeded. A program that fails to compile comes back as a
result, not an exception; call `raise_for_status()` on it when you would rather
have one. A call that does not complete raises an error that says how it
clears, and every one of them is a `DkeError`: `TransportError` when the
request never reached the service, and `ProtocolError` when the service refused
it — `PaymentRequired`, which clears by paying, and `RateLimited`, which clears
by waiting, are both of that kind.
### The one mistake that silently breaks everything
`code.Call.caller` and `code.Call.callee` hold the **subject** you recorded a
`Function` under — not its display name.
Above, the `Function` is recorded under the subject `parse`, so the `Call`
records `"parse"`. If you wrote a display name there instead, the queries would
still run and return answers: `uncalled()` compares those fields against
`Function` subjects directly, so every function would look uncalled, with
nothing in the output to tell you why. Getting this right is worth more than any
other single detail on this page.
The same care applies to `code.Function.test`, which names a test that reaches
the function. The `untested` rule reads its **absence**, so write it only where a
test really does reach — otherwise the rule falls silent everywhere instead of
where it should be silent.
---
## Chapter 4 — Asking
Once facts are in, the derived fields are already current — you do not run
anything to refresh them:
```python
import code
def review(fn: string):
who = code.callers_of(fn)
print(str(who.length) + " caller(s)")
for r in code.impact_of(fn):
print(" reaches " + r)
```
`impact_of` answers "what breaks if this changes" by following callers, and
theirs, to three hops. `callees_of(fn)` looks the other way, at what `fn`
calls. `uncalled()` reports functions with no recorded caller.
`critical()` reports the complicated and unreached — where a defect is likeliest
to survive. The full list is in the
[Library Reference](/ref/dke/library/#queries).
Those all start from a function you already suspect. `hubs()` is the one that
tells you **where to look first** — the functions most depended on, most first:
```python
import code
def where_to_look():
for h in code.hubs():
print(h)
```
It needs one thing from your producer: `callers` on each `Function`, counting
the **distinct** functions that call it. Record `callees` too if you want the
other direction.
```python
import code
update(code.Function.parse.callers, 12, "my-analysis")
update(code.Function.parse.callees, 3, "my-analysis")
```
Write it for every function including zero — unlike `called`, this one is read
by comparison rather than by absence, so a fan-in of zero is a fact worth
having. Record none of it and nothing breaks; `hubs()` simply stays empty.
One more number is worth recording on a function: `returns_ignored`, how many of
its calls throw the result away — neither bound, nor tested, nor handed on.
```python
import code
update(code.Function.parse.returns_ignored, 2, "my-analysis")
```
`code.discarded_results()` then answers `parse`. It lists every function that
throws a result away at least once, which is where a failure a callee reports
can go unseen by the caller.
Derived conclusions carry a **`derived` provenance** rather than a source, so
you can always tell a conclusion from something you were told.
### Imports and dependencies
`code` also reasons about what your code imports and what your project
declares. An `Import` records one import: `importer` is the module doing it,
`imported` the module as written, and `root` its top-level package — `requests`
for `import requests.adapters`. A `Dependency` records one package your project
depends on: its `name`, its `version` when an exact one is declared, `pinned`,
and `direct` for the ones your project declares itself.
```python
import code
update(code.Import.i1.importer, "app.main", "my-analysis")
update(code.Import.i1.imported, "requests", "my-analysis")
update(code.Import.i1.root, "requests", "my-analysis")
update(code.Dependency.requests.name, "requests", "my-analysis")
update(code.Dependency.requests.direct, True, "my-analysis")
update(code.Dependency.requests.pinned, False, "my-analysis")
update(code.Dependency.rich.name, "rich", "my-analysis")
update(code.Dependency.rich.version, "13.7", "my-analysis")
update(code.Dependency.rich.direct, True, "my-analysis")
update(code.Dependency.rich.pinned, True, "my-analysis")
```
Then ask three questions at once:
```python
import code
def dependency_check():
for m in code.importers_of("requests"):
print("imports requests: " + m)
for d in code.unused_dependencies():
print("declared, never imported: " + d)
for d in code.unpinned_dependencies():
print("not pinned: " + d)
```
That prints `imports requests: app.main`, `declared, never imported: rich` and
`not pinned: requests`. `importers_of` matches `imported` exactly as written,
and `unused_dependencies()` compares each dependency's `name` with the `root` of
every import, so `import requests.adapters` still counts as using `requests`.
Two fields are read by value, so write them deliberately. Leave `direct`
unwritten on a dependency your project did not declare — one that another
package brought in — and `unused_dependencies()` does not report it. Write
`pinned` for every dependency, `False` included: `unpinned_dependencies()`
looks for `False`, and a dependency with no `pinned` is never reported. A
dependency with no `name` is passed over rather than failing the question, and
that holds for every query here: a half-written subject costs you that subject
and nothing else.
### Two functions that have to agree
An encoder and its decoder, a writer and its reader, two functions that ought to
check their input alike — nothing in a call graph says two functions are halves
of one contract. Only you know that, so `code` has you state it. A `Counterpart`
names the pair, says in `relation` what is being counted, and gives the count
each side handles:
```python
import code
update(code.Counterpart.users.name, "create/update", "review")
update(code.Counterpart.users.left, "create_user", "review")
update(code.Counterpart.users.right, "update_user", "review")
update(code.Counterpart.users.relation, "input checks", "review")
update(code.Counterpart.users.left_covers, 4, "review")
update(code.Counterpart.users.right_covers, 3, "review")
```
`code.asymmetries()` then answers `create/update [input checks] left stricter
by 1`: `update_user` accepts input that `create_user` refuses. The difference is
kept current — record a new count for either side and the answer follows, with
no audit to re-run. The two counts are comparable with each other and with
nothing else, since each pair counts something of its own.
---
## Chapter 5 — What the answers can and cannot mean
This chapter matters more than the query list, because the failure it describes
is quiet.
**A call graph derived from source is a lower bound.** Python decides calls as it
runs, so anything reading source sees the calls the source makes plain and no
others — a call through a variable, a handler table or `getattr` is invisible to
it. Every edge you record that way is real; edges are **missing**.
That matters most for `uncalled()`. A function reached only dynamically will
appear on it. Read it as *candidates to look at*, never a list to delete.
**You can measure how loose the bound is.** Wherever your producer sees a call
and cannot work out what it reaches, record it:
```python
import code
update(code.UnresolvedCall.u1.caller, "app.dispatch", "ingest")
update(code.UnresolvedCall.u1.site, "app.py:11", "ingest")
update(code.UnresolvedCall.u1.text, "handlers[name]", "ingest")
```
Then `dispatch_sites()` lists them, and you read it beside `uncalled()`:
```python
import code
def audit():
dead = code.uncalled()
sites = code.dispatch_sites()
print(str(dead.length) + " uncalled, " + str(sites.length) + " dispatch site(s)")
for s in sites:
print(" " + s)
```
Two dispatch sites and two hundred are very different reasons to believe the
same `uncalled()` list. Nothing on it is a defect on its own — a plugin table, a
callback and a decorator all work this way — it is where reading stopped telling
you what runs.
If your producer records none of these, nothing changes: `dispatch_sites()`
returns nothing and every other answer is what it was.
**`untested` inherits the same bound**, because which functions your tests reach
is worked out over the same graph — and reaching a function is not executing it,
since a branch may never be taken.
**And structure cannot tell deliberate from overlooked.** Two functions can look
identical to every query on this page — same complexity, neither reached by a
test — while one of them has a comment saying exactly why it is shaped that way
and why testing it is not worth the trouble, and the other says nothing.
`critical()` returns both. That is not a defect in `critical()`; it is that
everything it reads is structure, and the difference is not structural.
So record the reasons too. Most codebases already write them down:
```python
import code
update(code.Rationale.w1.subject, "app.gnarly", "ingest")
update(code.Rationale.w1.marker, "WHY", "ingest")
update(code.Rationale.w1.text, "the arms mirror the wire states", "ingest")
update(code.Rationale.w1.site, "app.py:40", "ingest")
update(code.Function.gnarly.explained, True, "ingest")
```
Then `rationale_for` reads them back, and `unexplained()` is `critical()` with
everything already accounted for taken out:
```python
import code
def triage():
for f in code.unexplained():
print(f)
for r in code.rationale_for(f):
print(" " + r)
```
A short `unexplained()` under a long `critical()` is a good result — it means
the risk is known and written down rather than absent.
**Which comments count is yours to decide**, the same way the counting rule in
Chapter 6 is, and the danger runs the same direction. A convention wide enough
to sweep ordinary comments makes every line in your tree a fact and every
function explained, which silences the rule instead of firing it. Pick something
an author had to type on purpose. Ours records `WHY:` and `NOTE:` at the start
of a comment, uppercase, and nothing else.
`explained` is read by its **absence**, like `test`. Write it only where a
reason really is recorded.
**There is no dependency age**, and that is the schema being honest rather than
an omission. Python's package metadata carries no release date, so an age could
only come from asking an index — which means sending your dependency list to a
third party. A field nothing can honestly fill is worse than no field, because it
reads as a capability.
**`unused_dependencies()` matches by name.** A package whose import name is not
its package name — Pillow is imported as `PIL` — looks unused however much of it
you import. Read the list the way you read `uncalled()`: names to check, not
names to remove.
---
## Chapter 6 — Comparable numbers
`code.Function.hotspot` fires at a complexity above 10. A fixed threshold over a
number *you* compute only means something if we agree on how it is computed — so
the Library Reference states the counting rule
([§2.3](/ref/dke/library/#how-to-count-branches-and-loops)), and it
is worth following even if you dislike the specific choices.
The rule is: one, plus one for each decision the function makes. `else`,
`default` and a bare catch-all add nothing, because they are where control lands
when none of the decisions above them fired.
**The list of what counts is closed**, and deliberately excludes some constructs
with a fair claim to being a decision — `assert` is the clearest. In some
languages it is a macro or a compiled-out call that a reader working from source
cannot reliably see, and a rule some producers can follow and others cannot is
the problem the rule exists to solve. A number everyone computes the same way is
worth more than a number that is arguably more faithful.
If your store holds more than one language, record `language` on each function.
`branches` is whatever *your* analysis counted, two producers can both be right
on different scales, and `complexity` compares them against one threshold either
way. Recording it also lets you ask about one language at a time:
`code.functions_in("python")` lists every function recorded as `python`, and
`code.hotspots_in("python")` only the hotspots among them.
---
## Chapter 7 — The module in your store is yours
Compiling `code.dpy` into your store gives you the module as we publish it —
every class, every rule, every query. From that moment it is an ordinary module
in your store, and everything you can do to a module you can do to this one.
**Its rules are removable one at a time.** `forget_rule` on one of them removes
it, and nothing puts it back: your store holds what you compiled into it, and
compiling is something you do. If a rule does not suit your codebase, remove it
— or better, edit the line in your copy of the file and compile again, so the
file and the store agree.
**What you run is what you can read.** The version we publish is the version you
downloaded; the file is the whole of it; and if you have changed it, you know,
because you changed it.
**Check the version before you assume you are current.** The
[Library Reference](/ref/dke/library/) states the version each module ships
at, and the download URL carries it — `https://dke.langsyn.com/source/code/1.12.3/code.dpy` is that
exact release for as long as it is the one we publish, while
`https://dke.langsyn.com/source/code/latest/code.dpy` is whatever is current. Publishing a new version replaces the old one at its
URL rather than keeping both, so link `latest` when a link has to keep working;
the file you downloaded is yours regardless, and nothing reaches into your store.
A version never changes without its content changing, so if the version you
downloaded is the version we publish, your copy is the current one.
---
## Chapter 8 — Writing your own
Nothing distinguishes yours from ours. Compile a file under a name and it
behaves exactly like one of ours:
```python
class Ledger:
balance: int
limit: int
over: bool = balance > limit
def exposed() -> list:
out = []
accounts = list_subjects(Ledger.balance)
for s in accounts:
c = current(Ledger.s.over)
match c:
case active_claim:
out = out + [s]
case empty:
pass
return out
```
Your modules are private to your store.
**Submitting a module again reconciles it.** A field whose value changed is
updated, one that did not is left alone, and an edited rule replaces the earlier
version of itself rather than joining it. So editing and re-submitting is the
ordinary way to change a module, and doing it twice costs nothing the second
time.
One thing to know when a module you import changes: a call is resolved when the
*calling* module is submitted, and the callee's body at that moment is the body
your call runs. If a module you depend on is updated afterwards, submit yours
again to move onto the new version.
---
## Chapter 9 — A second module: `identity`
Records that describe one thing turn up in every system that has more than one
source: a customer in the CRM and again in billing, a supplier under two
spellings. `identity` reasons about which of your records name the same thing,
without merging any of them. Version **1.0.2**, at
[dke.langsyn.com/source/identity/latest/identity.dpy](https://dke.langsyn.com/source/identity/latest/identity.dpy).
You record the records, and what your sources say about them. A `Same`
statement says two records name the same thing, and its `a` and `b` hold their
names; the statement's own name is yours to choose:
```python
import identity
update(identity.Record.alice.name, "Alice Smith", "crm")
update(identity.Record.asmith.name, "A. Smith", "billing")
update(identity.Record.ann.name, "Ann Smith", "support")
update(identity.Same.m1.a, "alice", "crm")
update(identity.Same.m1.b, "asmith", "crm")
update(identity.Same.m2.a, "asmith", "billing")
update(identity.Same.m2.b, "ann", "billing")
```
The CRM says `alice` and `asmith` are one person, and billing says `asmith` and
`ann` are. Neither source said anything about `alice` and `ann`, and the module
concludes it from the two statements together:
```python
import identity
def who_is_alice():
for r in identity.same_as("alice"):
print(r)
```
That prints `ann` and `asmith`. Nothing was merged: the three records are as you
wrote them, and the conclusion stands only while both statements do. Forget
either one and `ann` drops out of the answer by itself.
**Disagreement is reported, not settled.** Suppose support says otherwise:
```python
import identity
update(identity.Different.d1.a, "ann", "support")
update(identity.Different.d1.b, "alice", "support")
```
Now `identity.contested()` answers `alice` and `ann`, and
`identity.contradictions()` answers `d1`. Every statement still stands. Which
source to believe is your decision, and the module makes none.
**A possibility is kept apart from the facts.** Marketing has a second
`A. Smith` and cannot tell whether it is the same person. A `Possibly` statement
says exactly that, with `a` and `b` as on `Same`:
```python
import identity
update(identity.Record.asmith2.name, "A. Smith", "marketing")
update(identity.Possibly.p1.a, "asmith", "marketing")
update(identity.Possibly.p1.b, "asmith2", "marketing")
```
`identity.possibly_same_as("asmith")` answers `asmith2`, and `same_as("asmith")`
still answers `alice` and `ann` and nothing more: a possibility never adds a
record to an entity. Nor does it travel along a chain of `Same` statements:
`possibly_same_as("alice")` answers nothing, because no statement said that
`alice` might be `asmith2`. The module concludes a possibility into a relation
of its own, `PossiblySame`, which your rules can read by that bare name exactly
as they read `SameEntity` below.
**Carrying a fact across the link is yours to write.** The module does not know
what your records hold, but `SameEntity`, the relation it concludes from `Same`
statements, can be read by any rule of yours by that bare name. Name your
subjects after the records they describe:
```python
class Person:
email: string
@rule
def email_seen():
for p in Person, q in Person:
if SameEntity.p.q == True:
p.email_seen = q.email
```
With an email recorded for `alice` and another for `ann`, `Person.alice.email_seen`
holds both, and the difference is an ordinary disagreement that `conflicts()`
reports. The [Library Reference](/ref/dke/library/) has the rest: how to
include a record's own value, and what one wrong statement costs.
One habit worth having: a statement matches names exactly, and a misspelt name
is not refused — it becomes a record of its own. `identity.unrecorded()` lists
every name your statements use that no record carries, so run it after loading.
---
## Chapter 10 — A third module: `argument`
A post-mortem, a design review and a diagnosis all end in the same place: a set
of claims, some of which cut against others, and the question of which ones are
left standing. `argument` answers that from what your sources said, and decides
nothing on its own. Version **1.0.2**, at
[dke.langsyn.com/source/argument/latest/argument.dpy](https://dke.langsyn.com/source/argument/latest/argument.dpy).
You record the claims, and which claim attacks which. An `Attack` statement's
`by` names the attacking claim and its `target` the claim attacked:
```python
import argument
update(argument.Claim.leak.text, "The outage was caused by a memory leak", "ops")
update(argument.Claim.flat.text, "Memory stayed flat for the whole outage", "metrics")
update(argument.Attack.k1.by, "flat", "metrics")
update(argument.Attack.k1.target, "leak", "metrics")
```
Nothing attacks `flat`, so it stands, and the claim it attacks falls:
`argument.undefended()` answers `leak`. Then ops points out that the memory graph
has a gap exactly where the outage was:
```python
import argument
update(argument.Claim.gap.text, "The memory graph is missing the outage window", "ops")
update(argument.Attack.k2.by, "gap", "ops")
update(argument.Attack.k2.target, "flat", "ops")
```
Now `gap` stands, `flat` falls to it, and `leak` — whose only attacker has
fallen — **stands again**. Ask for all three answers at once:
```python
import argument
def where_we_stand():
for c in argument.unattacked():
print("stands: " + c)
for c in argument.reinstated():
print("stands again: " + c)
for c in argument.undefended():
print("falls: " + c)
```
That prints `stands: gap`, `stands again: leak` and `falls: flat`. Every step
followed from a statement, and forgetting `k2` puts the answer back where it was.
**Some questions it leaves open.** Suppose metrics replies that the gap is a
display fault, and records that `flat` attacks `gap`. Now `flat` and `gap` attack
each other, and neither one's fall can be established without first knowing the
other's. `argument` does not guess: all three claims leave every query, and
`attackers_of("leak")` still shows you where the argument stands. The
[Library Reference](/ref/dke/library/) says exactly which claims the module
settles.
**Supports and weakenings go on the record and decide nothing.** A `Support`
statement says one claim speaks for another, and a `Weakening` that one counts
against another without attacking it. Both take `by` and `target`, as an attack
does, and like an attack a statement counts once both of its fields are
written:
```python
import argument
update(argument.Claim.heap.text, "A heap dump shows one cache growing without bound", "ops")
update(argument.Support.s1.by, "heap", "ops")
update(argument.Support.s1.target, "leak", "ops")
update(argument.Claim.deploy.text, "A deploy went out ten minutes before the outage", "release")
update(argument.Weakening.w1.by, "deploy", "release")
update(argument.Weakening.w1.target, "leak", "release")
```
`argument.supporters_of("leak")` answers `heap`, `argument.weakeners_of("leak")`
answers `deploy`, and `argument.weakened()` answers `leak` — and whether `leak`
stands has not moved. A support is not a defence, and a weakening is not an
attack. What a support does carry is doubt. Suppose a developer points out that
the cache has a size limit:
```python
import argument
update(argument.Claim.capped.text, "That cache has a size limit", "dev")
update(argument.Attack.k3.by, "capped", "dev")
update(argument.Attack.k3.target, "heap", "dev")
```
Now `argument.contested_premises()` answers `heap`: part of the case for `leak`
rests on a claim that is itself attacked. `heap` falls to `capped`, and `leak`
is still exactly where it was.
As with `identity`, a statement names claims exactly and a misspelt name is not
refused, so run `argument.unrecorded()` after loading.
---
## Chapter 11 — A fourth module: `agenda`
An agent that works on someone's behalf takes on things to do, owes things to
other people, and works under rules about what it may not do. `agenda` keeps
that straight from what you recorded, and never acts on it: the store has no
clock, so it does not remind anyone of anything. You ask it what is due at a
moment you choose. Version **1.0.1**, at
[dke.langsyn.com/source/agenda/latest/agenda.dpy](https://dke.langsyn.com/source/agenda/latest/agenda.dpy).
Record a commitment you made and an invoice you owe:
```python
import agenda
update(agenda.Commitment.report.action, "send the quarterly report", "me")
update(agenda.Commitment.report.due, datetime("2026-10-02T17:00:00Z"), "me")
update(agenda.Obligation.invoice.action, "pay the hosting invoice", "hosting")
update(agenda.Obligation.invoice.due, datetime("2026-09-30T00:00:00Z"), "hosting")
update(agenda.Obligation.invoice.owed_to, "hosting", "hosting")
```
Then ask, on the morning of the first of October:
```python
import agenda
def morning():
now = datetime("2026-10-01T09:00:00Z")
for item in agenda.overdue(now):
print("overdue: " + item)
for item in agenda.due_within(now, duration("P2D")):
print("coming up: " + item)
```
That prints `overdue: Obligation.invoice` and `coming up: Commitment.report`.
Asking about the same morning next week gives the same answer, because the
moment is an argument and not the time you happen to ask.
**Moving a date keeps the old one.** The report will be late, so move it:
```python
import agenda
update(agenda.Commitment.report.due, datetime("2026-10-05T17:00:00Z"), "me")
```
It leaves the next two days, and `agenda.previous_dues("Commitment.report")`
answers `['2026-10-02T17:00:00Z']`. The date you first promised stays on record.
`agenda.late()` compares a completion with the date now in force, so once you
record `done_at`, finishing on the fourth is not late.
**A prohibition with a window.** Ops announces a change freeze for the week of
the twelfth, and you have a release planned inside it:
```python
import agenda
update(agenda.Commitment.release.action, "deploy", "me")
update(agenda.Commitment.release.due, datetime("2026-10-14T12:00:00Z"), "me")
update(agenda.Prohibition.freeze.action, "deploy", "ops",
datetime("2026-10-12T00:00:00Z"), datetime("2026-10-19T00:00:00Z"))
```
`agenda.blocked()` answers `Commitment.release`: it is due while deploying is
forbidden. Move its due date to the twentieth and `blocked()` no longer names
it. `agenda.forbidden(at)` asks the other question, what you may not do at a
moment, so asked about the fourteenth it still names the release, and asked
about the twentieth it names nothing. While the freeze is in force, `checkup`
names every open commitment to deploy under
`agenda.nothing_forbidden_committed`, and every open obligation to deploy under
`agenda.nothing_forbidden_owed` — the sharper case, since that is something you
owe and may not do. Nothing is removed or refused: moving the release, or
getting the freeze lifted, is your decision.
**Goals.** A commitment can say what it is for. Record the goal, and name it in
the commitment's `serves`:
```python
import agenda
update(agenda.Goal.close.text, "Close the quarter cleanly", "me")
update(agenda.Commitment.report.serves, "close", "me")
```
`agenda.serving("close")` answers `Commitment.report`. A goal is met once every
commitment serving it is done, so `agenda.met()` answers nothing yet. Record the
report as done:
```python
import agenda
update(agenda.Commitment.report.done_at, datetime("2026-10-04T15:00:00Z"), "me")
```
and `agenda.met()` answers `close`. A commitment names its goal exactly, and a
misspelt name in `serves` is not refused — it becomes a goal of its own, as a
misspelt record does in `identity` — so run `agenda.unrecorded()` after
loading.
Two sources can also disagree about a date. The item then has no one place in
time, and `agenda.unsettled(now)` names it instead of the other queries picking
a date for you.
---
## Chapter 12 — A fifth module: `quantum`
`quantum` does two things that share nothing but the subject. It **simulates** a
quantum circuit, as values, and it **reasons about** one recorded as facts:
which gates a device cannot run as written, which ran on a stale calibration,
and whether a result computed outside the store still holds. Version **1.3.0**,
at
[dke.langsyn.com/source/quantum/latest/quantum.dpy](https://dke.langsyn.com/source/quantum/latest/quantum.dpy).
A state of n qubits is a list of 2ⁿ complex amplitudes, and every gate takes a
state and returns the next one, as a Python function would. Nothing is written
to the store:
```python
import quantum
psi = quantum.zero(2)
psi = quantum.h(psi, 0)
psi = quantum.cnot(psi, 0, 1)
print(quantum.probabilities(psi))
```
prints `[0.5000000000000001, 0.0, 0.0, 0.5000000000000001]`, a Bell pair. A run
has no randomness of its own, so a measurement takes its uniform value as an
argument, and a seeded generator supplies one. Measuring the first qubit of the
same pair:
```python
import quantum
psi = quantum.zero(2)
psi = quantum.h(psi, 0)
psi = quantum.cnot(psi, 0, 1)
seed = 20261016
u = quantum.lcg_uniform(seed)
bit, after = quantum.measure(psi, 0, u)
print(bit)
print(quantum.probabilities(after))
```
prints `0` and `[1.0, 0.0, 0.0, 0.0]`. The same seed gives the same bit every
time; draw the seed outside the run and record it.
That generator has fewer than 2³¹ seeds, so a bit it gave can stand for no
secret. When a measured bit is to become part of one, take its value from
`chacha_uniform`, ChaCha20 as RFC 8439 defines it, from a key of eight 32-bit
words, at a position you count from 0:
```python
import quantum
psi = quantum.zero(2)
psi = quantum.h(psi, 0)
psi = quantum.cnot(psi, 0, 1)
key = [0x03020100, 0x07060504, 0x0B0A0908, 0x0F0E0D0C,
0x13121110, 0x17161514, 0x1B1A1918, 0x1F1E1D1C]
bit, after = quantum.measure(psi, 0, quantum.chacha_uniform(key, 0))
print(bit)
```
prints `0`. That key is the one RFC 8439's examples use, printed in the RFC, so
it is no secret. Draw yours in Python with
`list(struct.unpack("<8I", secrets.token_bytes(32)))`, on the machine where the
secret is to be kept: a key you pass travels to the service in the program, as
every argument does. `advance_chacha` measures a kept circuit from a key the same way
`advance` does from a seed, and answers the position to start from next.
A device is facts: its physical qubits, and the operations it runs natively on
each qubit and each ordered pair. dkepy's `dke-quantum-device` writes them from
a calibration snapshot, and here they are by hand for a two-qubit device that
runs `sx` on qubit 0 and `cnot` from 0 to 1, and nothing else:
```python
import quantum
update(quantum.Device.toy.qubits, 2, "me")
update(quantum.PhysicalQubit.toy_q0.device, "toy", "me")
update(quantum.PhysicalQubit.toy_q0.index, "0", "me")
update(quantum.PhysicalQubit.toy_q1.device, "toy", "me")
update(quantum.PhysicalQubit.toy_q1.index, "1", "me")
update(quantum.Native.toy_sx0.device, "toy", "me")
update(quantum.Native.toy_sx0.op, "sx", "me")
update(quantum.Native.toy_sx0.a, "0", "me")
update(quantum.Native.toy_sx0.measured_at, datetime("2026-10-01T09:00:00Z"), "me")
update(quantum.Native.toy_cx01.device, "toy", "me")
update(quantum.Native.toy_cx01.op, "cnot", "me")
update(quantum.Native.toy_cx01.a, "0", "me")
update(quantum.Native.toy_cx01.b, "1", "me")
update(quantum.Native.toy_cx01.measured_at, datetime("2026-09-01T09:00:00Z"), "me")
```
Then a circuit compiled for it, whose gates name the circuit:
```python
import quantum
update(quantum.Circuit.bell.device, "toy", "me")
update(quantum.Gate.bell_1.circuit, "bell", "me")
update(quantum.Gate.bell_1.op, "h", "me")
update(quantum.Gate.bell_1.a, "0", "me")
update(quantum.Gate.bell_1.step, 1, "me")
update(quantum.Gate.bell_2.circuit, "bell", "me")
update(quantum.Gate.bell_2.op, "cnot", "me")
update(quantum.Gate.bell_2.a, "1", "me")
update(quantum.Gate.bell_2.b, "0", "me")
update(quantum.Gate.bell_2.step, 2, "me")
update(quantum.Gate.bell_3.circuit, "bell", "me")
update(quantum.Gate.bell_3.op, "sx", "me")
update(quantum.Gate.bell_3.a, "0", "me")
update(quantum.Gate.bell_3.step, 3, "me")
print(quantum.illegal_gates("bell"))
print(quantum.after("bell_1"))
```
prints `['bell_1 not_native', 'bell_2 reversed']` and `['bell_2', 'bell_3']`.
The device runs no `h` anywhere, and runs `cnot` on that pair the other way
round only; each reason is its own conclusion, so `why` on it names the facts it
stands on. And `bell_1` must run before both later gates, since it is not
diagonal and shares a qubit with each.
The store keeps no clock, so a calibration is judged at a moment you name,
against the oldest you accept:
```python
import quantum
print(quantum.stale_gates("bell", datetime("2026-10-16T00:00:00Z"), duration("P7D")))
```
prints `['bell_3']`: its `sx` was measured fifteen days before.
**A result names what it read.** Record a result computed outside the store
with the digest of each circuit and device it read, and it holds only while
those are what the store holds:
```python
import quantum
update(quantum.Device.toy.digest, "sha256:aaaa", "me")
update(quantum.Circuit.bell.digest, "sha256:bbbb", "me")
update(quantum.Result.bell_check.circuit, "bell", "me")
update(quantum.Result.bell_check.digest, "sha256:bbbb", "me")
update(quantum.Result.bell_check.device, "toy", "me")
update(quantum.Result.bell_check.device_digest, "sha256:aaaa", "me")
update(quantum.Result.bell_check.outcome, "legal", "me")
held = current(quantum.Result.bell_check.holds)
print(held.value)
```
prints `legal`. Load a recalibrated device, and the result stops holding:
```python
import quantum
update(quantum.Device.toy.digest, "sha256:cccc", "me")
print(quantum.results_not_holding())
```
prints `['bell_check recalibrated']`. dkepy's `dke.quantum.provenance` computes
real digests, and writes a result's lines for you.
---
## Chapter 13 — A sixth module: `pqc`
A quantum computer able to break RSA and elliptic-curve cryptography would also
decrypt what was recorded before it existed. `pqc` reasons about which uses of
that cryptography to move first, from facts you record: where your systems use
it, how long what each use protects must stay protected, how long each system
takes to migrate, and the year you plan against. Version **1.0.2**, at
[dke.langsyn.com/source/pqc/latest/pqc.dpy](https://dke.langsyn.com/source/pqc/latest/pqc.dpy).
Record a horizon, two algorithms, what two systems protect and how long they
take to move:
```python
import pqc
update(pqc.Horizon.plan.year, 2035, "me")
update(pqc.Algorithm.rsa_2048.quantum, "vulnerable", "me")
update(pqc.Algorithm.rsa_2048.strength, 112, "me")
update(pqc.Algorithm.ml_kem_768.quantum, "resistant", "me")
update(pqc.Data.customer_records.shelf_life, 10, "me")
update(pqc.Data.sessions.shelf_life, 0, "me")
update(pqc.System.billing.migration, 3, "me")
update(pqc.System.portal.migration, 1, "me")
update(pqc.Use.billing_rsa.system, "billing", "me")
update(pqc.Use.billing_rsa.algorithm, "rsa_2048", "me")
update(pqc.Use.billing_rsa.protects, "customer_records", "me")
update(pqc.Use.portal_rsa.system, "portal", "me")
update(pqc.Use.portal_rsa.algorithm, "rsa_2048", "me")
update(pqc.Use.portal_rsa.protects, "sessions", "me")
update(pqc.Use.portal_kem.system, "portal", "me")
update(pqc.Use.portal_kem.algorithm, "ml_kem_768", "me")
update(pqc.Use.portal_kem.protects, "sessions", "me")
print(pqc.priority())
start = current(pqc.Use.billing_rsa.start_by)
print(start.value)
```
prints `['billing_rsa', 'portal_rsa']` and `2022`. Customer records must stay
protected for ten years, so billing must be off RSA by 2025, and moving it takes
three years. The resistant algorithm is in no answer. dkepy's `dke-pqc-cbom`
writes the algorithms and uses from a cryptographic bill of materials; the
shelf lives, migration times and horizon are yours.
Add a plan for billing, and a deadline you are held to:
```python
import pqc
update(pqc.Use.billing_rsa.migrated, 2028, "me")
update(pqc.Deadline.policy.year, 2030, "me")
update(pqc.Deadline.policy.strength, 112, "me")
print(pqc.exposed())
print(pqc.noncompliant())
print(pqc.remaining(2029))
```
prints `['billing_rsa', 'portal_rsa']`, `['portal_rsa']` and `['portal_rsa']`.
Billing's plan beats the deadline and still finishes after 2025, so what it
protects in those years can be recorded now and read at the horizon: it is
exposed. The portal has no plan at all, so a deadline covers it and it is still
on RSA in 2029.
The store keeps no clock: `remaining` takes the year you ask about. Two sources
that disagree about an algorithm or a shelf life are both reasoned about, and
`pqc.unsettled()` names each use where that happened.
---
## Chapter 14 — A seventh module: `harmony`
`harmony` marks four-part writing as a harmony teacher marks it: consecutive
fifths and octaves, crossed voices, spacing, notes outside a voice's range or
outside the key, and the reading of every chord. It checks a melody's rhythm
against its bars, and it calculates what music theory calculates: frequencies,
cents, set classes, twelve-tone matrices and even rhythms. Version **1.0.1**, at
[dke.langsyn.com/source/harmony/latest/harmony.dpy](https://dke.langsyn.com/source/harmony/latest/harmony.dpy).
A pitch is a MIDI note number, 60 for middle C. Write the four voices of a
chorale, a key, and three chords from the bass up:
```python
import harmony
harmony.satb()
harmony.key("C", 0, "major")
harmony.chord(1, "C", 48, 55, 64, 72)
harmony.chord(2, "C", 50, 57, 66, 74)
harmony.chord(3, "C", 43, 55, 62, 71)
for f in harmony.faults():
print(f)
print(harmony.readings("bass2"))
```
prints three marks, then `['major']`: `consecutive fifths: bass and tenor,
beats 1 to 2`, `consecutive octaves: bass and soprano, beats 1 to 2` and
`outside the key: alto, beat 2`. From the first chord to the second every voice rises a step, so the bass and
tenor move from a fifth to a fifth, and the bass and soprano from an octave to
an octave. The second chord's F sharp is outside C major, and it is marked
although a composer writes it on purpose: D major leads to G here as the
dominant of the dominant. A mark is the rule as a teacher states it, not a
verdict on the music.
Correct the second chord by writing its beat again:
```python
import harmony
harmony.chord(2, "C", 50, 53, 62, 69)
print(harmony.faults())
print(harmony.readings("bass2"))
```
prints `[]` and `['minor']`. The tenor and soprano now move against the bass,
and D minor uses only notes of the key. Writing a beat again replaces its
notes, so every mark they supported is withdrawn. A note written under another
source is held beside the first instead, and `harmony.unsettled()` names it.
Rhythm is counted in a unit you choose. In sixteenths, a bar of four beats is 16
long, with a beat of 4:
```python
import harmony
harmony.bars(2, 16, 4)
harmony.melody([2, 4, 2, 8, 6, 6, 4])
print(harmony.rhythm_faults())
print(harmony.euclid(3, 8))
```
prints `['syncopated: t2', 'syncopated: t6']` and `x..x..x.`. Each bar is full,
and both marked tones start off a beat and last past the next one. The second
bar is three, three and two eighths, and `euclid(3, 8)`, which spreads three
onsets as evenly as they go over eight steps, gives the same pattern.
The calculations answer values and write nothing:
```python
import harmony
print(harmony.frequency(60))
print(harmony.cents(1.5))
print(harmony.prime_form([0, 1, 5, 6, 8]))
print(harmony.prime_form([0, 1, 5, 6, 8], False))
print(harmony.interval_vector([0, 4, 7]))
```
prints `261.6255653005986`, `701.9550008653874`, `[0, 1, 5, 6, 8]`,
`[0, 1, 3, 7, 8]` and `[0, 0, 1, 1, 1, 0]`. Middle C is 261.6 hertz with A at
440, and a fifth tuned pure is almost two cents wider than the tempered fifth's
700. The two prime forms are one set class: published lists pack a set from
opposite ends, and `prime_form` gives either. The last line is a major triad's
interval vector, one minor third, one major third and one fifth.
What `harmony` does not check is listed in the
[Library Reference](/ref/dke/library/): hidden fifths and octaves, doubling,
resolution, melodic intervals and more than four voices.
---
## Chapter 15 — An eighth module: `physics`
`physics` checks quantities, units and dimensions, and calculates classical
motion. You record each quantity as a value in a unit, and it concludes the
value in SI, names a quantity handed over in a unit other than the one its
reader expects, judges whether an equation is dimensionally consistent, and
adds the resistance of resistors joined in series or in parallel. Its value
scripts calculate a pendulum, an orbit under three integrators, the heat
equation, a linear system, a bridge circuit and the Ising model of a magnet.
Version **1.0.3**, at
[dke.langsyn.com/source/physics/latest/physics.dpy](https://dke.langsyn.com/source/physics/latest/physics.dpy).
A unit is a name and a dimension, its powers of mass, length, time, electric
current and temperature, and what one of it is in SI. `physics.si_units()`
writes a list of common ones. A spacecraft's thrust is reported in pound-force
seconds to a program that reads newton seconds:
```python
import physics
physics.si_units()
physics.quantity("impulse", 100.0, "lbf_s")
physics.expect("impulse", "N_s")
print(physics.checks())
print(physics.si("impulse"))
print(physics.converted("impulse"))
physics.quantity("impulse", 444.822, "N_s")
print(physics.checks())
```
prints `['handed over: impulse in lbf_s where N_s is expected, convert']`,
`444.82216152604997`, `444.82216152604997` and `[]`. The two units have the same
dimension, so the value converts, and `si` and `converted` agree because the
unit expected is the SI one. The mark is the hand-over, not an error in the
number. Writing the quantity again in newton seconds replaces it, and the mark
is withdrawn. Had the reader expected a unit of another dimension, the mark
would read `incompatible`, and no factor makes it right.
An equation is checked as a product of powers. Write every power on the left
side positive and every power on the right side negative, so `F = m a` is `F` to
the 1, `m` to the -1 and `a` to the -1:
```python
import physics
physics.si_units()
physics.quantity("F", 12.0, "N")
physics.quantity("m", 3.0, "kg")
physics.quantity("a", 4.0, "m_per_s2")
physics.quantity("E", 6.0, "J")
physics.quantity("v", 2.0, "m_per_s")
physics.factor("newton", "F", 1)
physics.factor("newton", "m", -1)
physics.factor("newton", "a", -1)
physics.factor("kinetic", "E", 1)
physics.factor("kinetic", "m", -1)
physics.factor("kinetic", "v", -2)
physics.factor("wrong", "E", 1)
physics.factor("wrong", "m", -1)
physics.factor("wrong", "v", -1)
for e in ["newton", "kinetic", "wrong"]:
print(e, physics.dimension(e))
print(physics.checks())
```
prints `newton consistent`, `kinetic consistent`, `wrong L1 T-1` and
`['unbalanced: wrong, L1 T-1 left over']`. Force is mass times acceleration, and
the energy of a body is its mass times its speed squared, with the 1/2 left
out because a pure number has no dimension. `E = m v` is neither: it leaves a
length per time over, which `dimension` writes as `L1 T-1`. Consistent means the
dimensions agree, not that the equation is true.
Resistors in series add their resistances, and in parallel their conductances:
```python
import physics
physics.group("pair", "parallel")
physics.resistor("r1", 100.0, "pair")
physics.resistor("r2", 100.0, "pair")
physics.group("chain", "series")
physics.resistor("r3", 100.0, "chain")
physics.resistor("r4", 220.0, "chain")
print(physics.resistance("pair"))
print(physics.resistance("chain"))
physics.resistor("r2", 300.0, "pair")
print(physics.resistance("pair"))
```
prints `50.0`, `320.0` and `75.0`. Two 100 ohm resistors in parallel are 50
ohms, and 100 and 220 ohms in series are 320. The last three lines write the second
resistor again at 300 ohms, which replaces its first value, and the parallel
pair is now 75 ohms.
Where two sources disagree, both values are reasoned about and nothing refuses:
```python
import physics
physics.si_units()
physics.quantity("span", 10.0, "ft")
physics.expect("span", "m")
update(physics.Quantity("span").value, 12.0, "surveyor")
print(physics.unsettled())
print(physics.si("span"))
print(physics.checks())
physics.group("loop", "series")
physics.resistor("ra", 100.0, "loop")
physics.resistor("rb", 50.0, "loop")
update(physics.Resistor("ra").ohms, 120.0, "meter")
print(physics.unsettled())
print(physics.disputed_groups())
forget(physics.Resistor("ra").ohms, 120.0, "meter")
print(physics.resistance("loop"))
```
prints `['quantity span']`, `3.048`, `['handed over: span in ft where m is
expected, convert']`, `['quantity span', 'resistor ra']`, `['loop']` and
`150.0`. A survey gave the span as 10 feet and another source as 12.
`unsettled()` names the quantity, and `si` answers the least of the values,
3.048 metres. The resistor is the case to watch: a resistance is a sum, and a
sum adds every value it reads, so the series group would read 100 plus 120
plus 50, 270 ohms, which is no circuit anyone built. While a resistor is bound
to its group twice, the group is disputed and has no resistance:
`disputed_groups()` names it, `resistance` refuses it, and forgetting the claim
that is not to stand settles it. A second source giving the same 100 ohms
disputes a series group too, since the sum would add it twice.
The calculations answer values and write nothing:
```python
import physics
print(physics.small_period(1.0, 9.80665))
print(physics.period(1.0, 1.0, 9.80665))
print(physics.pendulum(1.0, 1.0, 9.80665, 0.001))
print(physics.orbit(0.5, "euler", 10, 400))
print(physics.orbit(0.5, "rk4", 10, 400))
print(physics.heat(20, 0.25, 40))
print(physics.heat_exact(0.025))
print(physics.bridge(10.0, 100.0, 200.0, 100.0, 200.0, 50.0))
print(physics.bridge(10.0, 100.0, 200.0, 100.0, 210.0, 50.0))
print(physics.solve([[2.0, 1.0], [1.0, 3.0]], [3.0, 5.0]))
print(physics.solve([[1.0, 2.0], [2.0, 4.0]], [3.0, 6.0]))
```
prints, one to a line, `2.0064092925890407`, `2.139502939337562`,
`2.1395029388040956`, `[0.7650100270596601, 0.37507303664083647, 5.398487191688995]`,
`[-2.898865276712569e-07, -4.485077864423118e-08, 3.506398058980124e-05]`,
`0.644289254804568`, `0.6431765995475461`, `0.0`, `-0.0005830903790087305`,
`[0.8, 1.4]` and `[]`. A one metre pendulum released at one radian swings with a
period of 2.1395 seconds, where the small-angle formula gives 2.0064, and
integrating its equation of motion agrees with the exact answer to nine places.
The Euler method ends ten periods with its orbit's energy off by three quarters,
and the fourth-order Runge–Kutta method by three parts in ten million. The finite rod's
middle is within 0.0012 of Fourier's series, a balanced bridge passes no
current through its meter and an unbalanced one a little, and a singular linear
system has no solution and answers `[]`.
What `physics` does not check is listed in the
[Library Reference](/ref/dke/library/): whether an equation is true, a unit
absent from the list, and a circuit that is not built of series and parallel
groups.
---
## Chapter 16 — A ninth module: `chemistry`
`chemistry` reads molecular structure the way a chemist reads a skeletal
drawing. You write a molecule's atoms and the bonds between them. It works out the
hydrogens each atom carries, the molecule's formula and weight, and the
functional groups on its atoms. It adds up a reaction's enthalpy from the
enthalpies of formation of its species, and it calculates what a chemistry course
calculates: molar masses, balanced equations, the pH of an acid, a titration
curve and the kinetics of consecutive and of reversible reactions. Version
**1.0.3**, at
[dke.langsyn.com/source/chemistry/latest/chemistry.dpy](https://dke.langsyn.com/source/chemistry/latest/chemistry.dpy).
Hydrogens are not written. Write acetic acid by its four heavy atoms and three
bonds; `chemistry.elements()` first writes the table of elements the rules read:
```python
import chemistry
chemistry.elements()
chemistry.atom("c1", "C", "acid")
chemistry.atom("c2", "C", "acid")
chemistry.atom("o1", "O", "acid")
chemistry.atom("o2", "O", "acid")
chemistry.bond("c1", "c2", 1)
chemistry.bond("c2", "o1", 2)
chemistry.bond("c2", "o2", 1)
w = chemistry.weight("acid")
print(chemistry.formula("acid"), round(w, 3))
print(chemistry.groups("acid"))
for n in ["c1", "c2", "o1", "o2"]:
h = current(chemistry.Atom(n).hydrogens)
print(n, h.value)
```
prints `C2H4O2 60.052`, `['carboxylic acid']`, then the hydrogens of the four
atoms: `c1 3`, `c2 0`, `o1 0` and `o2 1`. Each atom carries what its valence
leaves over. The methyl carbon has one bond and a valence of 4, so it carries
three hydrogens; the carbonyl carbon has bonds of order 1, 2 and 1, four in
all, and carries none; the oxygen with one bond carries one. The carbon double-bonded to an oxygen is a carbonyl carbon, and
with a single bond to an oxygen that carries a hydrogen it is an acid's. The
formula is in the Hill order, carbon first and hydrogen next, and the weight is
the sum of the atoms' weights with their hydrogens.
Correct a structure by writing a bond again. Here a chain of two carbons and an
oxygen is first an alcohol, then, with the oxygen double-bonded, an aldehyde. A
carbon bonded past its valence is marked, not refused:
```python
import chemistry
chemistry.elements()
chemistry.atom("a1", "C", "chain")
chemistry.atom("a2", "C", "chain")
chemistry.atom("a3", "O", "chain")
chemistry.bond("a1", "a2", 1)
chemistry.bond("a2", "a3", 1)
print(chemistry.formula("chain"), chemistry.groups("chain"))
chemistry.bond("a2", "a3", 2)
print(chemistry.formula("chain"), chemistry.groups("chain"))
w = why(chemistry.Atom.a2.group)
for c in w:
p = str(c.path)
if "Bond" in p and ".order" in p:
print(p + " = " + str(c.value))
chemistry.atom("x1", "C", "over")
chemistry.atom("x2", "O", "over")
chemistry.atom("x3", "O", "over")
chemistry.atom("x4", "C", "over")
chemistry.bond("x1", "x2", 2)
chemistry.bond("x1", "x3", 2)
chemistry.bond("x1", "x4", 1)
print(chemistry.faults())
```
prints `C2H6O ['alcohol']`, then `C2H4O ['aldehyde']` after the correction. Writing
the bond again replaced it, so the alcohol and its hydrogens were withdrawn and
the aldehyde concluded. `why` names the bonds the group rests on:
`chemistry.Bond.a2-a3.order = 2` and `chemistry.Bond.a2-a1.order = 1`, which with
the double bond leave the carbon its one hydrogen. The last line is
`['overbonded: x1']`: the first carbon has two double bonds and a single bond, five
in all against a valence of 4.
A reaction is written as species with coefficients, negative for a reactant and
positive for a product. The reaction's enthalpy is the sum of each coefficient
times its species' enthalpy of formation:
```python
import chemistry
chemistry.species("methane_g", -74.8)
chemistry.species("oxygen_g", 0.0)
chemistry.species("carbon_dioxide_g", -393.5)
chemistry.species("water_l", -285.8)
chemistry.term("burn", "methane_g", -1)
chemistry.term("burn", "oxygen_g", -2)
chemistry.term("burn", "carbon_dioxide_g", 1)
chemistry.term("burn", "water_l", 2)
h = chemistry.enthalpy("burn")
print(round(h, 1))
chemistry.term("burn_gas", "methane_g", -1)
chemistry.term("burn_gas", "oxygen_g", -2)
chemistry.term("burn_gas", "carbon_dioxide_g", 1)
chemistry.term("burn_gas", "water_g", 2)
h = chemistry.enthalpy("burn_gas")
print(round(h, 1), chemistry.faults())
chemistry.species("water_g", -241.8)
h = chemistry.enthalpy("burn_gas")
print(round(h, 1), chemistry.faults())
```
prints `-890.3`, then `-318.7 ['incomplete: burn_gas']`, then `-802.3 []`. Burning
methane to liquid water gives off 890.3 kJ. The second reaction names `water_g`,
which has no enthalpy of formation yet, so its enthalpy is the sum of the three
terms that have one and `faults()` says the reaction is incomplete. Writing the
species completes it, and the enthalpy is concluded again.
Where two sources disagree about a fact, both values are reasoned about, and
`unsettled()` names the fact. A sum is the exception:
```python
import chemistry
chemistry.elements()
chemistry.atom("s1", "S", "sulfane")
w = chemistry.weight("sulfane")
print(chemistry.formula("sulfane"), round(w, 3))
update(chemistry.Element("S").weight, 32.065, "older table")
print(chemistry.unsettled(), chemistry.disputed_molecules())
print(chemistry.formula("sulfane"))
forget(chemistry.Element("S").weight, 32.065, "older table")
w = chemistry.weight("sulfane")
print(round(w, 3))
chemistry.species("methane_g", -74.8)
chemistry.species("oxygen_g", 0.0)
chemistry.species("carbon_dioxide_g", -393.5)
chemistry.species("water_l", -285.8)
chemistry.term("burn", "methane_g", -1)
chemistry.term("burn", "oxygen_g", -2)
chemistry.term("burn", "carbon_dioxide_g", 1)
chemistry.term("burn", "water_l", 2)
h = chemistry.enthalpy("burn")
print(round(h, 1))
update(chemistry.Species("methane_g").formation, -74.6, "other table")
print(chemistry.unsettled(), chemistry.disputed_reactions())
print(chemistry.faults())
```
prints `H2S 34.076`, `['element S'] ['sulfane']`, `H2S` and `34.076`, then
`-890.3`, `['species methane_g'] ['burn']` and `['disputed: reaction burn']`. A
weight and an enthalpy are sums, and a sum adds every value it reads: the
sulfane would be weighed once at each weight, 68.157, which is no weight of the
molecule, and the reaction would count methane at both enthalpies. So while a
member is bound to a sum twice, the sum is disputed and has no value:
`disputed_molecules()` and `disputed_reactions()` name it, `weight()` and
`enthalpy()` refuse it, and `faults()` lists it. What the dispute does not
reach stands, so the formula, whose hydrogens both weights agree on, still reads
`H2S`. Forgetting the claim that is not to stand settles it, and the weight is
34.076 again. A bond whose order a second source writes, even the same order,
disputes its atom and so its molecule, which is not weighed without it.
The calculations answer values and write nothing. They need no molecule, only a
formula, an equation or a rate:
```python
import chemistry
chemistry.elements()
w = chemistry.molar_mass("Ca3(PO4)2")
print(round(w, 3))
print(chemistry.balance(["C3H8", "O2"], ["CO2", "H2O"]))
print(chemistry.balance(["Cr2O7^2-", "Fe^2+", "H^+"], ["Cr^3+", "Fe^3+", "H2O"]))
print(chemistry.balance(["H2", "O2"], ["H2O", "H2O2"]))
p = chemistry.ph(0.1, [4.756])
print(round(p, 3))
p = chemistry.titration(0.1, 25.0, 0.1, 12.5, [4.756])
print(round(p, 3))
p = chemistry.titration(0.1, 25.0, 0.1, 25.0, [4.756])
print(round(p, 3))
t = chemistry.peak_time(1.0, 0.5)
print(round(t, 4))
f = chemistry.consecutive(1.0, 0.5, 1.0, 0.01)
print([round(x, 4) for x in f])
g = chemistry.consecutive_exact(1.0, 0.5, 1.0)
print([round(x, 4) for x in g])
c = chemistry.equilibrium_exact(10.0, 1.0, 1.0)
print(round(c, 4))
```
prints `310.174`, then `[1, 5, 3, 4]`, `[1, 6, 14, 2, 6, 7]` and `[]`, then
`2.881`, `4.756` and `8.728`, then `1.3863`, `[0.3679, 0.4773, 0.1548]`,
`[0.3679, 0.4773, 0.1548]` and `0.7298`. Propane burns as C3H8 + 5 O2 to 3 CO2 +
4 H2O, and the dichromate and iron equation balances with its charge. The third
equation answers `[]`: hydrogen and oxygen make water and hydrogen peroxide in
more than one independent proportion, so there is no single balance. The pH of
0.1 mol/L acetic acid is 2.881, and halfway through its titration the pH equals
the acid's pKa, 4.756. At equal volumes of acid and base the solution is the salt
alone and the pH is 8.728. For A to B to C with rate constants 1 and 0.5, B is at
its most at 1.3863 time units, and the Runge-Kutta fractions at time 1 agree with
the closed form to four places. At equilibrium with a constant of 10 and 1 mol/L
of each reactant, 0.7298 mol/L of C has formed.
What `chemistry` reads is the bonds: it does not represent charge, rings,
aromaticity or stereochemistry, nor a second valence for an element. The
[Library Reference](/ref/dke/library/) lists what the answers can and cannot mean.
---
## Chapter 17 — A tenth module: `biology`
`biology` reads two kinds of record. From a metabolic network, a growth medium
and the reactions between compounds, it concludes which reactions can run and
which compounds the network can make, and it asks which one compound would let
it make one it cannot. From a pedigree, it concludes every person's ancestors,
each child whose parents are related by descent, the carriers the mode of
inheritance requires, and each person whose record the mode cannot explain. It
also calculates: translation, crosses, Hardy–Weinberg proportions, drift and a
ring of genes. Version **1.0.2**, at
[dke.langsyn.com/source/biology/latest/biology.dpy](https://dke.langsyn.com/source/biology/latest/biology.dpy).
A network is a medium, which marks the compounds it supplies, and reactions of
one or two substrates and one or two products. Write a medium of glucose and
ATP, three reactions that follow from it, and one that nothing supplies:
```python
import biology
biology.medium("glucose")
biology.medium("atp")
biology.reaction("hexokinase", "glucose", "atp", "g6p", "adp")
biology.reaction("isomerase", "g6p", "", "f6p", "")
biology.reaction("kinase2", "f6p", "atp", "fbp", "adp")
biology.reaction("epimerase", "x5p", "", "ru5p", "")
print(biology.scope())
print(biology.blocked())
```
prints `['adp', 'atp', 'f6p', 'fbp', 'g6p', 'glucose']`, then `['epimerase']`. The
scope is every compound the network can make from the medium, the medium's own
included. Each of the first three reactions has every substrate reached, so it
runs, and its products are reached in turn. `epimerase` needs `x5p`, which
nothing makes, so it is blocked and `ru5p` is outside the scope.
`needs()` asks which single compound, added to the medium, would let the network
make a compound it cannot:
```python
import biology
print(biology.needs("ru5p", ["xylose", "ribose", "x5p"]))
print(biology.scope())
```
prints `['x5p']` and the same scope as before. Each candidate is a hypothesis:
the module supposes it in the medium, asks whether `ru5p` is then reached, and
writes nothing. Of the three, only `x5p` is the substrate of a reaction that makes `ru5p`.
A scope is held up from the medium, and a cycle of reactions does not hold
itself up. Add a pyruvate that `f6p` makes and a pair of reactions that turn it
into lactate and back, then take glucose out of the medium by writing it False
under its own source:
```python
import biology
biology.reaction("ferment", "f6p", "", "pyruvate", "")
biology.reaction("forward", "pyruvate", "", "lactate", "")
biology.reaction("back", "lactate", "", "pyruvate", "")
print(biology.scope())
update(biology.Compound("glucose").medium, False, "medium")
print(biology.scope())
print(biology.blocked())
update(biology.Compound("glucose").medium, True, "medium")
print(biology.scope())
```
prints four lines. The first scope has `lactate` and `pyruvate` in it. With
glucose gone, the second is `['atp']`: only the medium's other compound is left,
and `lactate` and `pyruvate` are not reached although each is the other's
product. `blocked()` lists seven reactions, the whole chain and the cycle with
it, and the fourth line is the first scope again, restored by writing glucose
back. A conclusion withdraws when the fact it rests on does.
A pedigree is families and people. A family has a mode of inheritance, and a
person a family, a sex, whether they are affected, and a mother and father where
known. Record a family with an X-linked recessive trait over three generations,
and ask for its carriers:
```python
import biology
biology.family("harrow", "X-linked recessive")
biology.person("edda", "harrow", "F", False, "", "")
biology.person("osk", "harrow", "M", False, "", "")
biology.person("bryn", "harrow", "M", True, "edda", "osk")
biology.person("tove", "harrow", "F", False, "edda", "osk")
biology.person("nell", "harrow", "F", True, "tove", "osk")
print(biology.carriers("harrow"))
print(biology.unexplained("harrow"))
print(biology.ancestors("nell"))
print(biology.inbred())
w = why(biology.Person.edda.carrier)
for c in w:
print(str(c.path) + " = " + str(c.value))
```
prints `['edda']`, `['nell']`, `['edda', 'osk', 'tove']` and `['nell']`, then the
facts the first conclusion rests on, one to a line, from
`biology.Person.edda.carrier = True` down: the family's mode, X-linked
recessive; Bryn's family, his sex, M, his being affected and his mother, Edda;
and Edda's being unaffected.
A son's one X is his mother's, so Edda, unaffected, carries the allele because
her son Bryn is affected. Nell is an affected daughter, which needs an affected
father, and Osk is not: the mode cannot explain her record, so she is
`unexplained`. She is also `inbred`, because her father Osk is her mother
Tove's father too. These conclusions assume the trait shows in everyone who has
its genotype and that no new mutation arose in the family. Nell is where one of
those, or the record, does not hold.
A correction is the same source writing again. A disagreement is another source
writing. Say the clinic's record of Nell was wrong, then let a second clinic
record her as affected:
```python
import biology
biology.person("nell", "harrow", "F", False, "tove", "osk")
print(biology.unexplained("harrow"))
update(biology.Person("nell").affected, True, "second clinic")
print(biology.unsettled())
print(biology.unexplained("harrow"))
```
prints `[]`, then `['person nell affected']` and `['nell']`. Writing Nell again
under `pedigree` replaced her first record, so no one is unexplained. The second
clinic's claim is held beside it instead of replacing it, and both are reasoned
about: Nell is unexplained again because one value on record makes her so, and
`unsettled()` names the person and the field. No query refuses.
The calculations answer values and write nothing:
```python
import biology
rna = biology.transcribe("CCATGGCTTGGTAAGG")
print(rna)
print(biology.translate(rna, 1))
print(biology.translate("AUGAUAUGAGCC", 1))
print(biology.translate("AUGAUAUGAGCC", 2))
print(biology.phenotypes("AaBb", "AaBb"))
print(biology.chi_square([315.0, 108.0, 101.0, 32.0], [9.0, 3.0, 3.0, 1.0]))
print(biology.hardy_weinberg(50, 40, 10))
print(biology.drift(50, 0.5, 100, 7))
print(biology.ring("100", 3))
print(biology.period("100"))
```
prints `CCAUGGCUUGGUAAGG`, `MAW`, `MI`, `MMWA`, `{'AB': 9, 'Ab': 3, 'aB': 3, 'ab':
1}`, `0.4700239808153477`, `[0.7, 0.226757369614513]`, `[0.0, 58.0]`,
`['100', '101', '001', '011']` and `6`. The translation starts at the first
`AUG` and stops at the stop codon `UAA`. The same message reads `MI` in the
standard code and `MMWA` in the vertebrate mitochondrial one, which reads `AUA`
as methionine and `UGA` as tryptophan. A dihybrid cross gives nine, three, three
and one; the chi-square of Mendel-like counts against that ratio is 0.47. The
allele frequency from the genotype counts is 0.7, and its counts differ little
from random mating's proportions. `drift` with a seed is the same run every time,
and this one lost the allele in generation 58. A ring of three genes that repress
each other returns to its first state after six steps.
What `biology` does not check is listed in the
[Library Reference](/ref/dke/library/): the energy, balance or direction of a
reaction, and the penetrance of a trait.
## Chapter 18 — An eleventh module: `astronomy`
`astronomy` reads two kinds of record. From sites and stars, it concludes for
each site the stars that never set, the stars that never rise and the stars it
can see. From nights and bookings, it concludes that an instrument can observe a
star for exactly the stretch where the booking, the dark and the star's time
above the horizon overlap, and it reports that stretch. It also calculates:
Julian dates, sidereal time, rising and setting, the Sun and the length of the
day, and orbits. Version **1.0.2**, at
[dke.langsyn.com/source/astronomy/latest/astronomy.dpy](https://dke.langsyn.com/source/astronomy/latest/astronomy.dpy).
A site has a latitude, a longitude and the faintest magnitude its instrument
reaches. A star has a right ascension, a declination and a magnitude. Angles are
in degrees, and a smaller magnitude is a brighter star. Write two sites and five
stars, and ask what each site sees:
```python
import astronomy
astronomy.site("oslo", 59.91, 10.75, 6.0)
astronomy.site("hobart", -42.88, 147.33, 6.0)
astronomy.star("polaris", 37.95, 89.26, 1.98)
astronomy.star("vega", 279.23, 38.78, 0.03)
astronomy.star("canopus", 95.99, -52.70, -0.74)
astronomy.star("sirius", 101.29, -16.72, -1.46)
astronomy.star("faint", 10.0, 20.0, 9.0)
print(astronomy.circumpolar("oslo"))
print(astronomy.hidden("oslo"))
print(astronomy.visible("oslo"))
print(astronomy.circumpolar("hobart"))
print(astronomy.hidden("hobart"))
print(astronomy.visible("hobart"))
```
prints `['polaris', 'vega']`, `['canopus']`, `['polaris', 'sirius', 'vega']`,
`['canopus']`, `['polaris']` and `['canopus', 'sirius', 'vega']`. At Oslo, 59.91
degrees north, a star north of 30.09 degrees never sets, and a star south of
-30.09 never rises. Polaris and Vega stay up; Canopus, at -52.70, never clears
the horizon. At Hobart, 42.88 degrees south, it is the other way about: Canopus
never sets and Polaris never rises. Sirius rises and sets at both. `faint` is
seen at neither, because its magnitude, 9.0, is fainter than the 6.0 each site's
instrument reaches. `visible` includes a star that never sets, since it is above
the horizon all the time.
The limits are computed from the latitude, and a conclusion is withdrawn when
the fact it rests on is corrected. Read the limits, ask why Vega is circumpolar
at Oslo, then correct Vega's declination to 28.0 and back:
```python
import astronomy
astronomy.site("oslo", 59.91, 10.75, 6.0)
astronomy.site("hobart", -42.88, 147.33, 6.0)
astronomy.star("polaris", 37.95, 89.26, 1.98)
astronomy.star("vega", 279.23, 38.78, 0.03)
astronomy.star("canopus", 95.99, -52.70, -0.74)
astronomy.star("sirius", 101.29, -16.72, -1.46)
limit = astronomy.Site("oslo").circumpolar_above
print(limit)
low = astronomy.Site("oslo").hidden_below
print(low)
south = astronomy.Site("hobart").circumpolar_below
print(south)
w = why(astronomy.Circumpolar.vega.oslo)
for c in w:
print(str(c.path) + " = " + str(c.value))
astronomy.star("vega", 279.23, 28.0, 0.03)
print(astronomy.circumpolar("oslo"))
astronomy.star("vega", 279.23, 38.78, 0.03)
print(astronomy.circumpolar("oslo"))
```
prints `30.090000000000003`, `-30.090000000000003` and `-47.12`, then four lines:
`astronomy.Circumpolar.vega.oslo = True`, then the two facts it rests on, Oslo's
latitude and its limit computed from it, `astronomy.Site.oslo.lat = 59.91` and
`astronomy.Site.oslo.circumpolar_above = 30.090000000000003`, and Vega's
declination, `astronomy.Star.vega.dec = 38.78`. With the declination at 28.0,
below the limit, only Polaris is left, `['polaris']`. Writing the right value
back restores `['polaris', 'vega']`.
A night is the dark time at a site, and a booking is an instrument booked at a
site for a star. Neither says when the star is up, so write that with `up()`,
passing the night's start. The booking is observable where all three overlap:
```python
import astronomy
astronomy.site("oslo", 59.91, 10.75, 6.0)
astronomy.site("hobart", -42.88, 147.33, 6.0)
astronomy.star("polaris", 37.95, 89.26, 1.98)
astronomy.star("vega", 279.23, 38.78, 0.03)
astronomy.star("canopus", 95.99, -52.70, -0.74)
astronomy.star("sirius", 101.29, -16.72, -1.46)
astronomy.night("n1", "oslo", datetime("2026-11-08T16:00:00Z"), datetime("2026-11-09T06:00:00Z"))
astronomy.up("vega", "oslo", datetime("2026-11-08T16:00:00Z"))
astronomy.book("b1", "oslo", "vega", datetime("2026-11-08T18:00:00Z"), datetime("2026-11-08T22:00:00Z"))
print(astronomy.window("b1"))
astronomy.up("sirius", "oslo", datetime("2026-11-08T16:00:00Z"))
astronomy.book("b2", "oslo", "sirius", datetime("2026-11-08T18:00:00Z"), datetime("2026-11-09T05:00:00Z"))
print(astronomy.window("b2"))
astronomy.book("b3", "oslo", "canopus", datetime("2026-11-08T18:00:00Z"), datetime("2026-11-09T05:00:00Z"))
print(astronomy.window("b3"))
print(astronomy.observable_at(datetime("2026-11-08T20:00:00Z")))
print(astronomy.observable_at(datetime("2026-11-09T01:00:00Z")))
print(astronomy.observable_at(datetime("2026-11-09T05:30:00Z")))
```
prints `['2026-11-08T18:00:00Z', '2026-11-08T22:00:00Z']`, then
`['2026-11-08T22:54:30.745764485Z', '2026-11-09T05:00:00Z']`, then `[]`, then
`['b1']`, `['b2']` and `[]`. Vega never sets at Oslo, so its `up()` is the whole
day from the start and the first booking is observable for all of its four
hours. Sirius rises at 22:54, inside the second booking, which ends at 05:00,
before the night does and before Sirius sets: the booking is observable from the
rising to its end. Canopus never rises at Oslo, so `up()` wrote nothing for it
and the third booking is never observable. Reading at an instant answers
which bookings are observable then: only the first at 20:00, only the second at
01:00, and neither at 05:30. The conclusion is the overlap of three windows, and
`window()` reports it from the last of them opening to the first closing.
A disagreement is another source writing. A second catalogue puts Canopus at
-20.0 degrees:
```python
import astronomy
update(astronomy.Star("canopus").dec, -20.0, "second catalogue")
print(astronomy.unsettled())
print(astronomy.hidden("oslo"))
print(astronomy.visible("oslo"))
```
prints `['star canopus dec']`, `['canopus']` and
`['canopus', 'polaris', 'sirius', 'vega']`. Both values are reasoned about:
Canopus is hidden at Oslo by the first and visible by the second, so it is in
both lists, and `unsettled()` names the star and the field. No query refuses
except `up()` and `window()`: `up()` needs one declination for the star, and is
refused until the disagreement is settled, and `window()` needs one start and
one end for the booking.
The calculations answer values and write nothing. Times are in UT, and a
Julian date counts days:
```python
import astronomy
print(astronomy.jd(2000, 1, 1.5))
j = astronomy.jd(1987, 4, 10.0)
print(j)
g = astronomy.gmst(j)
print(g)
print(astronomy.hms(g))
print(astronomy.altitude(38.78, 59.91, 0.0))
print(astronomy.semi_arc(-16.72, 59.91, 0.0))
print(astronomy.rise_set(101.29, -16.72, 59.91, 10.75, datetime("2026-11-08T00:00:00Z")))
print(astronomy.day_length(59.91, astronomy.jd(2026, 6, 21.0)))
print(astronomy.day_length(59.91, astronomy.jd(2026, 12, 21.0)))
print(astronomy.day_length(80.0, astronomy.jd(2026, 6, 21.0)))
print(astronomy.day_length(80.0, astronomy.jd(2026, 12, 21.0)))
```
prints `2451545.0`, `2446895.5`, `197.693195090862`,
`[13.0, 10.0, 46.36682180688041]`, `68.86999999999999`, `58.77387884370289`,
`[2026-11-07T22:58:26.655225834Z, 2026-11-08T02:52:53.871385659Z,
2026-11-08T06:47:21.087545484Z]`, `18.830588577027644`, `5.899388616579758`,
`24.0` and `0.0`. The first is the Julian date of noon on 2000 January 1. The
Greenwich sidereal time at 0h on 1987 April 10 is 13 hours 10 minutes
46.37 seconds, the value Meeus works the formula out to. Vega, at declination
38.78, stands 68.87 degrees high where it crosses the meridian at Oslo,
90 minus the 21.13 degrees between the latitude and the declination. The semi-arc of Sirius is 58.77 degrees, nearly
four hours each side of its transit, and `rise_set()` gives its rising, transit
and setting around the transit on that UT day, the rising falling the evening
before. At Oslo the day is 18.8 hours long at midsummer and 5.9 at midwinter; at
80 degrees north the Sun does not set at midsummer and does not rise at
midwinter.
The orbits are solved two ways. Kepler's equation is solved by Newton's method,
and a body is carried round an orbit by fourth-order Runge-Kutta, which can be
checked against it:
```python
import astronomy
k = astronomy.kepler(0.5, 0.1)
print(k)
print(astronomy.true_anomaly(k[0], 0.1))
print(astronomy.two_body(0.5, 1.0, 200))
print(astronomy.two_body(0.5, 1.0, 1000))
```
prints `[0.5524799869065704, 4.0]`, `0.6074229151773668`,
`[-0.5000000000000002, -0.5000004631075587, 2.5973556467815207e-05]` and
`[-0.5000000000000002, -0.5000000001484903, 3.154061045708898e-08]`. Newton's
method took four steps to find the eccentric anomaly for a mean anomaly of 0.5
and an eccentricity of 0.1, all in radians. The last two lines carry a body of
eccentricity 0.5 once round, from perihelion, in 200 steps and then in 1000. The
energy of such an orbit is -0.5 and should not change; after 200 steps it
differs in the seventh decimal place and after 1000 in the tenth, and the body is
within 2.6e-5, then 3.2e-8, of where Kepler's equation puts it.
What `astronomy` does not check is listed in the
[Library Reference](/ref/dke/library/): the atmosphere, the Moon, the weather,
the precession of the coordinates, and whether a star's magnitude is within a
booking's reach.
## Chapter 19 — A twelfth module: `geography`
`geography` reads three kinds of record. From regions and the region each lies
in, it concludes every region a region lies within and how many regions each one
holds. From a map, the borders between regions and the colour each is given, it
concludes who neighbours whom and which neighbours share a colour, and it can
choose colours so that none do. From places and boxes of latitude and longitude,
it concludes which places lie in which boxes. It also calculates distances,
bearings, projections and areas on the Earth. Version **1.0.2**, at
[dke.langsyn.com/source/geography/latest/geography.dpy](https://dke.langsyn.com/source/geography/latest/geography.dpy).
A hierarchy is regions that name their parent. Each has a level, any word you
use. Record a country, two counties, the municipalities in them and a district
in one, outermost first, and ask what lies within what:
```python
import geography
geography.region("norway", "", "country")
geography.region("vestland", "norway", "county")
geography.region("rogaland", "norway", "county")
geography.region("bergen", "vestland", "municipality")
geography.region("askoy", "vestland", "municipality")
geography.region("stavanger", "rogaland", "municipality")
geography.region("nordnes", "bergen", "district")
print(geography.within("nordnes"))
print(geography.regions_in("norway", "municipality"))
print(geography.regions_in("vestland", ""))
h = current(geography.Region.norway.holds)
print(h.value)
```
prints `['bergen', 'norway', 'vestland']`, `['askoy', 'bergen', 'stavanger']`,
`['askoy', 'bergen', 'nordnes']` and `6`. Nordnes lies within Bergen, which lies
within Vestland, which lies within Norway: the rules go up the parents to any
depth. `regions_in` goes the other way, down from a region, and takes a level or
`""` for any. Norway holds six regions, every one recorded below it. The
outermost region is written with `""` for its parent, and every region has to be
written with `geography.region` for the rules to conclude anything about it.
A conclusion withdraws when the fact it rests on is corrected. Say Askøy belongs
to Rogaland, and ask why it lies within Norway:
```python
import geography
geography.region("norway", "", "country")
geography.region("vestland", "norway", "county")
geography.region("rogaland", "norway", "county")
geography.region("bergen", "vestland", "municipality")
geography.region("askoy", "vestland", "municipality")
geography.region("stavanger", "rogaland", "municipality")
geography.region("nordnes", "bergen", "district")
geography.region("askoy", "rogaland", "municipality")
print(geography.within("askoy"))
print(geography.regions_in("vestland", ""))
print(geography.regions_in("rogaland", "municipality"))
w = why(geography.Within.askoy.norway)
for c in w:
print(str(c.path) + " = " + str(c.value))
```
prints `['norway', 'rogaland']`, `['bergen', 'nordnes']` and `['askoy',
'stavanger']`, then the facts the last conclusion rests on, one to a line, from
`geography.Within.askoy.norway = True` down: Askøy within Rogaland, then the two
written facts that give it, that Askøy's parent is Rogaland and that Rogaland's
parent is Norway. Writing Askøy again under the same source
replaced its first parent, so it left Vestland and everything that rests on
Vestland.
A map is borders and colours. Add two more counties, give each region a border
where it touches another, and colour the map by hand:
```python
import geography
geography.region("innlandet", "norway", "county")
geography.region("agder", "norway", "county")
geography.border("vestland", "rogaland")
geography.border("vestland", "innlandet")
geography.border("rogaland", "agder")
geography.border("agder", "vestland")
geography.colour("vestland", "green")
geography.colour("rogaland", "green")
geography.colour("innlandet", "yellow")
geography.colour("agder", "red")
print(geography.neighbours("vestland"))
print(geography.clashes())
print(geography.dsatur())
geography.paint(["green", "yellow", "red", "blue"])
print(geography.clashes())
c = current(geography.Region.vestland.colour)
print(c.value)
```
prints `['agder', 'innlandet', 'rogaland']`, `['rogaland vestland']`,
`{'vestland': 0, 'agder': 1, 'rogaland': 2, 'innlandet': 1}`, `[]` and `green`.
Vestland borders three counties. Rogaland and Vestland touch and are both
green, so the map has a clash, named once with the earlier name first.
`dsatur()` chooses a number for every region a border names so that no two
neighbours share one: Vestland, Rogaland and Agder all touch each other, so they
take three numbers. It writes nothing. `paint` writes them, number k as the kth
colour of the palette, in place of the colours written by hand, and the map has
no clash afterwards. A palette shorter than the numbers `dsatur` chooses is
refused.
Places and boxes are latitude and longitude in degrees, north and east positive.
A box whose western edge lies east of its eastern one crosses the antimeridian:
```python
import geography
geography.place("suva", -18.14, 178.44)
geography.place("nukualofa", -21.14, -175.2)
geography.place("oslo", 59.91, 10.75)
geography.place("tromso", 69.65, 18.96)
geography.box("fiji_tonga", -22.0, -12.0, 170.0, -173.0)
geography.box("south_norway", 57.9, 63.0, 4.5, 12.5)
print(geography.places_in("fiji_tonga"))
print(geography.places_in("south_norway"))
update(geography.Place("oslo").lat, 65.0, "second survey")
print(geography.unsettled())
print(geography.places_in("south_norway"))
update(geography.Place("oslo").lat, 59.91, "second survey")
print(geography.unsettled())
```
prints `['nukualofa', 'suva']`, `['oslo']`, `['place oslo lat']`, `['oslo']` and
`[]`. Suva lies at 178.44 degrees east and Nuku'alofa at 175.2 west, on either side of
the antimeridian, and both are in the box: with its edges at 170 and -173 it
holds the longitudes from 170 round through 180 to -173. A second survey put
Oslo at 65 degrees north, outside the box. Both values are reasoned about, so
Oslo is still in the box, which the first value puts it in, and `unsettled()`
names the place and the field. Writing the second survey as the first agrees,
and nothing is named.
The calculations answer values and write nothing. On the sphere, with the
Earth's mean radius of 6371008.8 metres:
```python
import geography
oslo = [59.91, 10.75]
bergen = [60.39, 5.32]
d = geography.haversine(oslo[0], oslo[1], bergen[0], bergen[1])
print(round(d / 1000, 1))
print(round(geography.bearing(oslo[0], oslo[1], bergen[0], bergen[1]), 1))
p = geography.destination(oslo[0], oslo[1], 270.0, 100000.0)
print(round(p[0], 4), round(p[1], 4))
print(round(geography.mercator(60.0), 6))
print(round(geography.mercator_scale(60.0), 6))
print(round(geography.triangle_area(0.0, 0.0, 0.0, 90.0, 90.0, 0.0) / 1000000, 1))
```
prints `305.1`, `282.4`, `59.8978 8.9567`, `1.316958`, `2.0` and `63758235.1`.
Bergen is 305.1 kilometres from Oslo along the great circle, in the direction
282.4 degrees clockwise from north, a little north of west. A hundred kilometres
due west of Oslo, on the great circle, is 59.8978 north and 8.9567 east: a great
circle that sets out due west bends toward the equator, so the latitude falls a little.
The Mercator projection stretches a short distance by one over the cosine of its
latitude, so at 60 degrees a distance is drawn twice as long. The triangle with
corners on the equator at 0 and 90 degrees east and at the north pole is an
eighth of the sphere, 63758235.1 square kilometres.
The Earth is not a sphere, and for a surveyor's distance the ellipsoid is
better. The example Vincenty published his method with is the line from Flinders
Peak to Buninyong, in Victoria, Australia:
```python
import geography
lat1 = -(37 + 57 / 60 + 3.7203 / 3600)
lon1 = 144 + 25 / 60 + 29.5244 / 3600
lat2 = -(37 + 39 / 60 + 10.1561 / 3600)
lon2 = 143 + 55 / 60 + 35.3839 / 3600
v = geography.vincenty_inverse(lat1, lon1, lat2, lon2)
print(round(v[0], 3), round(v[1], 4), round(v[2], 4), v[3])
there = geography.dms(v[1])
back = geography.dms(v[2])
print(there[0], there[1], round(there[2], 2))
print(back[0], back[1], round(back[2], 2))
print(round(geography.haversine(lat1, lon1, lat2, lon2), 1))
q = geography.vincenty_direct(lat1, lon1, v[1], v[0])
print(round(q[0] - lat2, 9), round(q[1] - lon2, 9))
print(geography.vincenty_inverse(0.0, 0.0, 0.5, 179.7))
```
prints `54972.271 306.8682 127.1736 4.0`, `306.0 52.0 5.37`,
`127.0 10.0 25.07`, `54925.5`, `0.0 0.0` and `[0.0, 0.0, 0.0, -1.0]`. The
distance is 54972.271 metres, and the azimuth from Flinders Peak to Buninyong is
306 degrees 52 minutes 5.37 seconds, with 127 degrees 10 minutes 25.07 seconds
the azimuth from Buninyong back: the figures Vincenty gave. The iteration took
four rounds. The sphere gives 54925.5 metres for the same line, 47 metres
short. `vincenty_direct` goes the other way, from a point, an azimuth and a
distance to the point reached, and sets out from Flinders Peak along the
azimuth for the distance to land where Buninyong is, within a nanodegree. Two
points almost opposite each other on the globe are the one case where the
method has no answer: after 200 rounds it gives up and returns `-1.0` as the
number of rounds, with zeros for the rest.
What `geography` does not do is listed in the
[Library Reference](/ref/dke/library/): it does not decide that two regions
touch, it does not choose the fewest colours a map needs, it knows no height
above the sea, and it has no answer at the poles for the Mercator projection.
## Chapter 20 — A thirteenth module: `mathematics`
`mathematics` reads three kinds of record. From whole numbers, each written with
its candidate divisors, it concludes which candidates divide, the sum of each
number's proper divisors, and whether the number is prime or composite and
perfect, abundant or deficient. From a binary operation on a finite set, given as
its table, it concludes the identity, whether the operation is closed,
commutative and associative, which elements have an inverse, and whether it is a
group; where a property fails, it names the cells that break it. From a
triangle's three sides it concludes whether they close up into a triangle, and
whether it is acute, right or obtuse. It also calculates: Euclid's algorithm,
modular arithmetic, primes, continued fractions and Pell's equation, roots,
integrals and linear systems. Version **1.0.2**, at
[dke.langsyn.com/source/mathematics/latest/mathematics.dpy](https://dke.langsyn.com/source/mathematics/latest/mathematics.dpy).
A number is written with a candidate divisor for each `k` from 1 to half of it.
Write the numbers 1 to 30 and ask which are prime, perfect and abundant:
```python
import mathematics
mathematics.numbers(1, 30)
print(mathematics.numbers_that_are("prime"))
print(mathematics.numbers_that_are("perfect"))
print(mathematics.numbers_that_are("abundant"))
print(len(mathematics.numbers_that_are("deficient")))
print(mathematics.divisors(28))
w = why(mathematics.Number.n4.composite)
for c in w:
print(str(c.path) + " = " + str(c.value))
```
prints `[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]`, `[6, 28]`, `[12, 18, 20, 24, 30]`,
`23` and `[1, 2, 4, 7, 14]`, then the facts the last conclusion rests on, one to a
line, from `mathematics.Number.n4.composite = True` down: the candidate 2 of the
number 4, which names `n4` as the number it is `of`, has `k` 2, divides, leaves a
remainder of 0, and carries the number's value, 4. A perfect
number is the sum of its proper divisors, and 28 is 1 + 2 + 4 + 7 + 14; 12 is
abundant, because its divisors 1, 2, 3, 4 and 6 sum to 16. The number 1 is
neither prime nor composite and has no divisor sum, and is deficient, one of the
23 that are.
A finite operation is its table. Write addition and subtraction modulo 4, each
as the four-by-four table of its products, and ask what holds of each:
```python
import mathematics
els = ["0", "1", "2", "3"]
add = [[str((i + j) % 4) for j in range(4)] for i in range(4)]
sub = [[str((i - j) % 4) for j in range(4)] for i in range(4)]
mathematics.operation("z4", els, add)
mathematics.operation("minus4", els, sub)
print(mathematics.properties("z4"))
print(mathematics.properties("minus4"))
print(mathematics.identity_of("z4"))
print(mathematics.identity_of("minus4") == "")
print(mathematics.noncommuting_pairs("minus4"))
print(mathematics.nonassociative_triples("minus4")[0])
```
prints `['closed', 'commutative', 'associative', 'group', 'abelian']`,
`['closed']`, `0`, `True`, `['0 1', '0 3', '1 0', '1 2', '2 1', '2 3', '3 0',
'3 2']` and `0 0 1`. Addition modulo 4 is an abelian group with identity 0.
Subtraction is closed and nothing more: 0 − 1 is 3 but 1 − 0 is 1, so the pairs
named do not commute; (0 − 0) − 1 is 3 but 0 − (0 − 1) is 1, so the first triple
named does not associate; and 0 is an identity from the right only, so the
operation has none and `identity_of` answers `""`.
A table rewritten withdraws what its cells held up. Say the cell 2 + 3 is
written as 0 instead of 1, then written back, then recorded again as 0 by a
second source:
```python
import mathematics
mathematics.cell("z4", "2", "3", "0")
print(mathematics.properties("z4"))
print(mathematics.noncommuting_pairs("z4"))
mathematics.cell("z4", "2", "3", "1")
print(mathematics.properties("z4"))
update(mathematics.Cell("z4_2_3").c, "0", "second table")
print(mathematics.unsettled())
print(mathematics.properties("z4"))
```
prints `['closed']` and `['2 3', '3 2']`, then all five properties again, then
`['cell z4_2_3 c']` and `['closed']`. With 2 + 3 equal to 0, the cell no longer
matches 3 + 2, so the pair is named and the group is gone. Writing the cell as it
was under the same source replaced the first correction, and the properties came
back. The second source's claim is held beside it instead of replacing it, and
both products are reasoned about: the operation is no longer commutative or a
group because one value on record breaks them, and `unsettled()` names the cell.
No query refuses.
A triangle is three whole lengths, and the module says what they make:
```python
import mathematics
mathematics.triangle("egypt", 3, 4, 5)
mathematics.triangle("flat", 1, 2, 3)
mathematics.triangle("none", 1, 2, 5)
mathematics.triangle("wide", 4, 4, 7)
mathematics.triangle("even", 6, 6, 6)
print(mathematics.kinds("egypt"))
print(mathematics.kinds("flat"))
print(mathematics.kinds("none"))
print(mathematics.kinds("wide"))
print(mathematics.kinds("even"))
print(mathematics.area("egypt"))
print(mathematics.area("none"))
print(mathematics.area("even"))
w = why(mathematics.Triangle.egypt.right)
for c in w:
print(str(c.path) + " = " + str(c.value))
```
prints `['right', 'scalene']`, `[]`, `[]`, `['obtuse', 'isosceles']` and
`['acute', 'equilateral', 'isosceles']`, then `6.0`, `0.0` and
`15.588457268119896`, then the facts the first conclusion rests on:
`mathematics.Triangle.egypt.right = True`, then the triangle's being valid, its
slack 2, its sides 3, 4 and 5 and its gap 0. The gap, a² + b² − c², is 0 for a
right triangle, positive for an acute one and negative for an obtuse one. Lengths
1, 2 and 3 lie flat, with a slack of 0, and 1, 2 and 5 never meet: neither is a
triangle, so neither has a kind or an area. The area comes from Heron's formula,
on a whole number the module computes from the sides.
The calculations answer values and write nothing. First the arithmetic of
divisibility:
```python
import mathematics
print(mathematics.gcd(1071, 462), mathematics.lcm(21, 6))
print(mathematics.extended_gcd(240, 46))
print(mathematics.modinv(3, 11), mathematics.modinv(4, 8))
print(mathematics.modpow(2, 100, 1000000007))
print(mathematics.is_prime(2147483647), mathematics.is_prime(561))
print(mathematics.factorize(360), mathematics.factorize(600851475143))
print(mathematics.totient(36))
print(mathematics.primes(30))
print(mathematics.factorial(20), mathematics.binomial(52, 5))
print(mathematics.fibonacci(92), mathematics.collatz(27), mathematics.collatz(97))
```
prints `21 42`, `[2, -9, 47]`, `4 0`, `976371285`, `True False`,
`[2, 2, 2, 3, 3, 5] [71, 839, 1471, 6857]`, `12`, the primes to 30,
`2432902008176640000 2598960` and `7540113804746346429 111 118`. The extended
algorithm gives 240 · (−9) + 46 · 47 = 2, the greatest common divisor. The
inverse of 3 modulo 11 is 4; 4 has none modulo 8, and the answer is 0. The
number 561 is a Carmichael number, which fools the simple Fermat test, and the
Miller–Rabin test the module runs finds it composite. A whole number is 64 bits: 20!
is computed and 21! is refused rather than wrapped, and so is the 93rd
Fibonacci number, while the 92nd is the largest.
Then continued fractions and Pell's equation:
```python
import mathematics
cf = mathematics.continued_fraction(415, 93)
print(cf)
print(mathematics.convergents(cf))
print(mathematics.sqrt_period(23))
print(mathematics.pell(61))
print(mathematics.root(2.0, 2), mathematics.root(27.0, 3))
print(mathematics.isqrt(1000000))
```
prints `[4, 2, 6, 7]`, `[[4, 1], [9, 2], [58, 13], [415, 93]]`, `[4, 1, 3, 1, 8]`,
`[1766319049, 226153980]`, `1.414213562373095 3.0` and `1000`. The fraction
415/93 is 4 + 1/(2 + 1/(6 + 1/7)), and its last convergent is the fraction
itself. The square root of 23 is 4 followed by 1, 3, 1 and 8 repeating forever.
The smallest solution of x² − 61y² = 1 is a ten-digit x and a nine-digit y, which
`pell` finds from that repeating part.
And numerical methods:
```python
import mathematics
ys = [0.0, 0.25, 1.0, 2.25, 4.0]
print(mathematics.trapezoid(ys, 0.5))
print(mathematics.simpson(ys, 0.5))
print(mathematics.machin())
m = [[2.0, 1.0], [1.0, 3.0]]
print(mathematics.determinant(m))
print(mathematics.solve(m, [3.0, 5.0]))
print(mathematics.determinant(mathematics.hilbert(3)))
print(mathematics.solve([[1.0, 2.0], [2.0, 4.0]], [1.0, 2.0]))
```
prints `2.75`, `2.6666666666666665`, `3.1415926535897936`, `5.0`, `[0.8, 1.4]`,
`0.00046296296296296135` and `[]`. The samples are x² at 0, 0.5, 1, 1.5 and 2;
Simpson's rule gets the integral, 8/3, to the last digit and the trapezoidal rule
overshoots it. The determinant of the third Hilbert matrix is exactly 1/2160, and
the figure printed is that with the rounding elimination leaves. The last system
is singular, so it has no solution to give.
What `mathematics` does not check is listed in the
[Library Reference](/ref/dke/library/): that a table is the operation it was
meant to be, and whether a number was written with the candidates it needs.
## Chapter 21 — A fourteenth module: `statistics`
`statistics` reads three kinds of record. From the strata of a study, in each of
which two arms were tried, it concludes each arm's rate in each stratum and in
the pool, and names a study whose pooled comparison reverses what every stratum
shows. From a screening test, by how common the condition is and how good the
test is, it concludes what a positive result is worth. From a significance
test, by its p-value and the level it is held to, it concludes whether the
result is significant. It also calculates what a statistician calculates: the
mean, the variance, a least-squares line, the normal, binomial and Poisson
distributions, the chi-square statistic and a seeded generator. Version
**1.0.3**, at
[dke.langsyn.com/source/statistics/latest/statistics.dpy](https://dke.langsyn.com/source/statistics/latest/statistics.dpy).
A study is strata that name it. Each stratum gives the successes and the trials
of arm A and of arm B. The counts below are those of a study of two treatments
for kidney stones, divided by the size of the stone:
```python
import statistics
statistics.stratum("stones", "small", 81, 87, 234, 270)
statistics.stratum("stones", "large", 192, 263, 55, 80)
print(statistics.paradoxes())
print(statistics.pooled("stones"))
a = current(statistics.Stratum.stones_small.a_rate)
b = current(statistics.Stratum.stones_small.b_rate)
print(a.value, b.value)
c = current(statistics.Stratum.stones_large.a_rate)
d = current(statistics.Stratum.stones_large.b_rate)
print(c.value, d.value)
```
prints `['stones']`, `[0.78, 0.8257142857142857]`,
`0.9310344827586207 0.8666666666666667` and
`0.7300380228136882 0.6875`. Arm A succeeded in 93% of small stones against
87%, and in 73% of large stones against 69%: it is ahead in both strata. Pooled,
arm B is ahead, 83% against 78%, because arm B was tried mostly on the small
stones, which are the easy ones. That reversal is Simpson's paradox, and the
module names the study. Which comparison to believe is yours to decide: the
counts do not say why the arms were given the stones they were.
A conclusion withdraws when the count it rests on is corrected. Say arm B's
count in the large stratum was recorded wrongly and is 70, not 55; then say
a second source gives a different count for a small one:
```python
import statistics
statistics.stratum("stones", "small", 81, 87, 234, 270)
statistics.stratum("stones", "large", 192, 263, 55, 80)
print(statistics.paradoxes())
statistics.stratum("stones", "large", 192, 263, 70, 80)
print(statistics.paradoxes())
print(statistics.pooled("stones"))
update(statistics.Stratum("stones_small").a_yes, 85, "audit")
print(statistics.unsettled())
print(statistics.disputed_studies())
```
prints `['stones']`, `[]`, `[0.78, 0.8685714285714285]`,
`['stratum stones_small a_yes']` and `['stones']`. With 70 of 80, arm B is ahead
in the large stratum too, so the pool no longer reverses anything and the
paradox is gone. The audit gave the small stratum a second count for arm A, 85
where the first source said 81, and `unsettled()` names the stratum and the
field. A pooled count is a sum, which would add both, 85 and 81, into a pool no
source gives, so arm A's pooled successes are disputed and not pooled, and
`pooled()` refuses the study until one value is forgotten. The other three
counts, which both sources agree on, are still pooled.
A screening test is judged by what a positive result means, and that depends on
how common the condition is as much as on the test:
```python
import statistics
statistics.screen("s1", 0.01, 0.9, 0.9)
statistics.screen("s2", 0.2, 0.9, 0.9)
print(statistics.predictive_values("s1"))
print(statistics.predictive_values("s2"))
m = current(statistics.Screen.s1.mostly_false)
n = current(statistics.Screen.s2.mostly_false)
print(m.value, n.value)
statistics.significance("t1", 0.049, 0.05)
statistics.significance("t2", 0.05, 0.05)
statistics.significance("t3", 0.2, 0.05)
print(statistics.significant_tests())
```
prints `[0.08333333333333336, 0.9988789237668162]`,
`[0.6923076923076924, 0.9729729729729729]`, `True False` and `['t1']`. Both
tests find 90% of those who have the condition and clear 90% of those who do not.
Where one person in a hundred has it, only 8% of the positive results are right,
and a positive result is more likely wrong than right. Where one in five has it,
69% are. `significant` is a comparison of two numbers you wrote: 0.049 is below
0.05, and 0.05 is not below itself.
The calculations answer values and write nothing. Anscombe's quartet is four
sets of eleven points that share a mean and a line; here is the first, with its
descriptive statistics, and then two samples:
```python
import statistics
x = [10.0, 8.0, 13.0, 9.0, 11.0, 14.0, 6.0, 4.0, 12.0, 7.0, 5.0]
y = [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68]
print(statistics.mean(x), statistics.median(x))
print(statistics.variance(x), statistics.std_dev(x))
print(statistics.regression(x, y))
print(statistics.mean([1.0, 2.0, 3.0, 4.0]), statistics.median([1.0, 2.0, 3.0, 4.0]))
a = [27.5, 21.0, 19.0, 23.6, 17.0, 17.9, 16.9, 20.1, 21.9, 22.6, 23.1, 19.6, 19.0, 21.7, 21.4]
b = [27.1, 22.0, 20.8, 23.4, 23.4, 23.5, 25.8, 22.0, 24.8, 20.2, 21.9, 22.1, 22.9, 20.5, 24.4]
print(statistics.welch_t(a, b))
```
prints `9.0 9.0`, `10.999999999999996 3.3166247903553994`,
`[3.0000909090909103, 0.5000909090909091, 0.81642051634484]`, `2.5 2.5` and
`-2.4553563982860105`. The line through the points is y = 3.0001 + 0.5001x
with a correlation of 0.8164, which rounds to the line Anscombe published for
all four of his sets, y = 3.00 + 0.500x with r = 0.816. The variance is 11, and prints as
10.999999999999996 because the sum of squares is taken in a single pass and
carries a rounding. Welch's t for the two samples is -2.455.
Distributions are functions of numbers:
```python
import statistics
print(statistics.normal_cdf(0.0), statistics.normal_cdf(-5.0))
print(statistics.normal_quantile(0.975))
print(statistics.birthday(23))
print(statistics.binom_pmf(10, 3, 0.5), statistics.binom_cdf(10, 3, 0.5))
print(statistics.poisson_pmf(2, 3.0))
print(statistics.bayes(0.01, 0.9, 0.1))
print(statistics.chi_square_p_df1(3.841458820694124))
print(statistics.monte_carlo_pi(10000, 1))
```
prints `0.5 2.8665157187919455e-07`, `1.9599639845400532`,
`0.5072972343239855`, `0.1171875 0.171875`, `0.22404180765538773`,
`0.08333333333333333`, `0.05000000000000007` and `3.1344`. Five standard
deviations below the mean leaves 2.9 in ten million, with its digits, since the
tail is not computed as one minus the body. The z that cuts off the upper 2.5%
is 1.96. Twenty-three people give a better than even chance that two share a
birthday. Three heads in ten tosses has a probability of 0.117, and three or
fewer 0.172. A chi-square statistic of 3.84 with one degree of freedom is the
5% level. The last answer is the generator's: the module carries its own,
since DKE Python has no `random` module, and 10,000 points from seed 1 give
3.1344 every time. A service bounds the memory one run may hold, and the
generator's points count against it, so the number of points is for the
thousands: 20,000 is more than a run on the service holds. A seed of 0 stays 0.
What `statistics` does not do is listed in the
[Library Reference](/ref/dke/library/): it does not compute a p-value from data,
adjust it for many tests, or say whether an effect matters, and it does not
check that a probability lies between 0 and 1.
## Chapter 22 — A fifteenth module: `earthscience`
`earthscience` reads three kinds of record. From weather stations, by their
temperatures and rainfall, it concludes each one's Köppen group. From storms,
by their wind, it concludes whether each is a hurricane and its category. From
minerals, by their hardness, it concludes which scratches which. It also
calculates what a meteorologist and a geophysicist calculate: the dew point,
the heat index, the wind chill, the standard atmosphere, the age of a rock and
the energy of an earthquake. Version **1.0.2**, at
[dke.langsyn.com/source/earthscience/latest/earthscience.dpy](https://dke.langsyn.com/source/earthscience/latest/earthscience.dpy).
A station is written with six numbers: the mean temperature of its coldest and
of its warmest month in degrees Celsius, its annual mean temperature, its
annual rainfall in millimetres, the fraction of that rain which falls in the
half of the year when the sun is high, and the temperature, 0.0 or -3.0, that
divides the temperate group from the continental. Record seven places and ask
for each one's group and kind:
```python
import earthscience
earthscience.station("manaus", 26.0, 28.0, 27.0, 2300.0, 0.5, 0.0)
earthscience.station("cairo", 14.0, 29.0, 22.0, 25.0, 0.5, 0.0)
earthscience.station("oslo", -4.0, 17.0, 6.0, 760.0, 0.5, 0.0)
earthscience.station("tromso", -3.0, 12.0, 3.0, 1030.0, 0.5, -3.0)
earthscience.station("barrow", -26.0, 4.0, -12.0, 120.0, 0.5, 0.0)
earthscience.station("vostok", -65.0, -32.0, -55.0, 20.0, 0.5, 0.0)
earthscience.station("madrid", 6.0, 25.0, 14.0, 360.0, 0.3, 0.0)
for s in ["manaus", "cairo", "oslo", "tromso", "barrow", "vostok", "madrid"]:
print(s, earthscience.groups(s), earthscience.climate(s), earthscience.dryness_threshold(s))
n = why_not(earthscience.Station.cairo.tropical)
print(n)
```
prints, one to a line, `manaus ['A'] A 680.0`, `cairo ['B'] BW 580.0`,
`oslo ['D'] D 260.0`, `tromso ['D'] D 200.0`, `barrow ['E'] ET -100.0`,
`vostok ['E'] EF -960.0` and `madrid ['B'] BS 420.0`, then ``rule
`earthscience.tropical` needs earthscience.Station[cairo].dry != True, and
earthscience.Station[cairo].dry is True (bool)``. Manaus has a coldest month of
26 degrees: tropical. Cairo's 25 millimetres is far under its threshold of
580, which is twenty times its mean of 22 degrees plus 140 for an even
year, and under half of it, so it is a desert, and `why_not` says it is not
tropical because it is dry. Madrid, with 360 millimetres against a threshold of 420, is dry, and above
half of the threshold, 210, so it is a steppe. The two polar stations are
told apart by their warmest month, Barrow's above 0 degrees a tundra and
Vostok's below it an ice cap. The threshold is negative for both, since it
depends on a temperature that is, and neither is arid because they are polar
first.
The version of the system is yours to say, and a correction replaces the old
answer:
```python
import earthscience
earthscience.station("tromso", -1.5, 12.0, 3.0, 1030.0, 0.5, 0.0)
print(earthscience.climate("tromso"))
earthscience.station("tromso", -1.5, 12.0, 3.0, 1030.0, 0.5, -3.0)
print(earthscience.climate("tromso"))
earthscience.station("cairo", 14.0, 29.0, 22.0, 25.0, 0.5, 0.0)
print(earthscience.climate("cairo"))
earthscience.station("cairo", 14.0, 29.0, 22.0, 900.0, 0.5, 0.0)
print(earthscience.climate("cairo"), earthscience.dryness_threshold("cairo"))
update(earthscience.Station("cairo").rain, 25.0, "second survey")
print(earthscience.climate("cairo"), earthscience.groups("cairo"))
print(earthscience.unsettled())
```
prints `D`, `C`, `BW`, `C 580.0`, `BW ['B', 'C']` and `['station cairo rain']`.
A station whose coldest month is -1.5 degrees is continental where the
temperate group stops at 0 and temperate where it stops at -3. Writing Tromsø
again with the second version replaced the first. Cairo with 25 millimetres of
rain is a desert; written again with 900 it is temperate, its dryness is gone,
and the threshold stays 580. A second survey put the 25 back, and both values
are reasoned about: Cairo is in two groups, B and C, `groups` names both and
`climate` reads the first, and `unsettled()` names the station and the field.
Storms and minerals are simpler:
```python
import earthscience
earthscience.storm("a", 73)
earthscience.storm("b", 74)
earthscience.storm("c", 111)
earthscience.storm("d", 157)
for s in ["a", "b", "c", "d"]:
print(s, earthscience.category(s))
h = current(earthscience.Storm.c.major)
print(h.value)
w = why_not(earthscience.Storm.a.hurricane)
print(w)
earthscience.minerals(["quartz", "calcite", "topaz", "orthoclase"], [7.0, 3.0, 8.0, 6.0])
print(earthscience.scratched_by("quartz"), earthscience.scratches("quartz", "topaz"))
print(earthscience.scratched_by("topaz"))
```
prints `a 0`, `b 1`, `c 3`, `d 5`, `True`, ``rule `earthscience.hurricane` needs
earthscience.Storm[a].wind_mph >= 74, and earthscience.Storm[a].wind_mph is 73
(int)``, `['calcite', 'orthoclase'] False` and
`['calcite', 'orthoclase', 'quartz']`. A wind of 73 miles per hour is not
a hurricane and has no category; at 74 it is category 1, at 111 a major
hurricane of category 3, and at 157 category 5. Quartz scratches calcite and
orthoclase and not topaz, which is harder; topaz scratches all three.
The calculations answer values and write nothing:
```python
import earthscience
print(earthscience.dew_point(30.0, 70.0))
print(earthscience.heat_index(90.0, 70.0))
print(earthscience.wind_chill(10.0, 20.0))
print(earthscience.pressure_at(0.0), earthscience.pressure_at(5000.0))
print(earthscience.coriolis(59.91))
print(earthscience.lcl_height(30.0, 22.0))
print(earthscience.radiometric_age(1.0, 4.463e9))
print(earthscience.quake_energy_joules(6.0))
print(earthscience.magnitude_from_energy(earthscience.quake_energy_joules(6.0)))
print(earthscience.quake_ratio(6.0, 7.0), earthscience.quake_ratio(5.0, 7.0))
print(earthscience.sp_distance_km(10.0, 8.0, 4.5))
```
prints `23.930467834837106`, `105.92202060000027`, `-8.876220556825402`,
`1013.25 540.0484237684415`, `0.00012618817712699152`, `1000.0`,
`4463000000.0`, `63095734448019.43`, `6.0`, `31.622776601683793 1000.0` and
`102.85714285714286`. Air at 30 degrees Celsius and 70% humidity has a dew point of
23.9 degrees, and air at 90 degrees Fahrenheit and 70% humidity feels like
106. A wind of 20 miles per hour at 10 degrees Fahrenheit chills to
-8.9. The standard atmosphere's pressure falls from 1013.25 hectopascals at sea
level to 540 at 5,000 metres, about half. At 59.91 degrees of latitude the
Coriolis parameter is 0.000126 per second. A sample with as many daughter atoms as
parent atoms is one half-life old, here that of uranium-238, 4.463 billion
years. An earthquake of magnitude 6 releases 6.3e13 joules, and one more step
of magnitude 31.6 times as much; two steps, a thousand. A gap of 10 seconds
between the P and the S wave, at 8 and 4.5 kilometres a second, is 102.9
kilometres.
What `earthscience` does not do is listed in the
[Library Reference](/ref/dke/library/): it does not conclude the finer climates
inside a group, it reads no hemisphere, and it checks no argument against the
range its formula was fitted for.
## Chapter 23 — A sixteenth module: `computing`
`computing` reads one kind of record. From the words of a binary code it
concludes the Kraft sum of the code, which of its words begin other words, and
whether the code is prefix-free, so that a received string can be read without a
separator. It also calculates what an information theorist and a programmer
calculate: checksums and check digits, numbers in any base, an error-correcting
code, entropy, Huffman code lengths, binary search and shortest paths. Version
**1.0.1**, at
[dke.langsyn.com/source/computing/latest/computing.dpy](https://dke.langsyn.com/source/computing/latest/computing.dpy).
A code is written as its words, each a string of 0 and 1. Two codes of four and
three words show what a Kraft sum does and does not say:
```python
import computing
computing.code("good", ["0", "10", "110", "111"])
computing.code("bad", ["0", "01", "11"])
print(computing.prefix_free_codes())
print(computing.kraft_sum("good"), computing.kraft_sum("bad"))
print(computing.breaking_pairs("bad"))
k = current(computing.Code.bad.complete)
print(k.value)
w = why_not(computing.Code.bad.prefix_free)
print(w)
computing.code("short", ["0", "1", "10"])
print(computing.kraft_sum("short"))
o = current(computing.Code.short.oversubscribed)
print(o.value)
```
prints `['good']`, `1.0 1.0`, `['0 01']`, `True`, ``rule `computing.prefix_free`
needs computing.Code[bad].prefix_free__none1 to be absent, and it is present``,
`1.25` and `True`. The Kraft sum of a code is the sum of 2 to the minus the
length of each word, and 1 is the most a code that can be read without a
separator can have. Both codes have exactly 1, and only the first can be read
without one: in the second, 0 is the beginning of 01, and a received 01 could be
a 01 or a 0 followed by a 1. The sum is a property of the lengths and
says only that a code of those lengths could exist. `breaking_pairs` names the
pair that breaks the property, and `why_not` says that a pair does. The third
code has three words of lengths 1, 1 and 2 and a sum of 1.25: over 1, no code
that can be read without a separator has such words, and `oversubscribed` says
so.
The calculations answer values and write nothing. First the checks that guard
data in transit and in print, and numbers in other bases:
```python
import computing
print(computing.crc32("123456789"), computing.to_base(computing.crc32("123456789"), 16))
print(computing.adler32("Wikipedia"), computing.to_base(computing.adler32("Wikipedia"), 16))
print(computing.luhn_valid("17893729974"), computing.luhn_valid("17893729975"))
print(computing.isbn13_valid("9780306406157"), computing.isbn13_valid("9780306406158"))
print(computing.to_base(255, 2), computing.to_base(255, 16), computing.to_base(123456789, 36))
print(computing.from_base("ff", 16), computing.from_base("21i3v9", 36))
w = computing.hamming_encode(11)
print(w)
print(computing.hamming_decode(w), computing.hamming_decode(w ^ 4), computing.hamming_decode(w ^ 64))
print(computing.hamming_distance(11, 4), computing.hamming_distance(w, w ^ 4))
```
prints `3421780262 cbf43926`, `300286872 11e60398`, `True False`, `True False`,
`11111111 ff 21i3v9`, `255 123456789`, `51`, `11 11 11` and `4 1`. The CRC-32 of
the nine characters 123456789 is the check value 0xcbf43926 that the
catalogue of CRCs gives, and the Adler-32 of `Wikipedia` is the 0x11e60398 its
article gives. A single changed digit fails Luhn's check and the ISBN-13 check.
Hamming(7,4) encodes the four bits of 11 as the seven-bit word 51; flip any one
bit, here the third from the right and then the first from the left, and the
decoder still answers 11. Two flipped bits would be decoded wrongly, since the
code cannot tell them from one. The Hamming distance between 11 and 4 is 4 bits,
and between the word and the one with a bit flipped, 1.
Then information and algorithms:
```python
import computing
print(computing.entropy([1, 1]), computing.entropy([1, 1, 1, 1]), computing.entropy([3, 1]))
print(computing.huffman_lengths([45, 13, 12, 16, 9, 5]))
print(computing.binary_search([1, 3, 5, 7, 9, 11], 7), computing.binary_search([1, 3, 5, 7, 9, 11], 4))
print(computing.binary_search_steps(1000, 0), computing.binary_search_steps(1000000, 999999))
edges = [[0, 1, 7], [0, 2, 9], [0, 5, 14], [1, 2, 10], [1, 3, 15], [2, 3, 11], [2, 5, 2], [3, 4, 6], [5, 4, 9]]
print(computing.dijkstra(6, edges, 0))
print(computing.dijkstra(7, edges, 0))
```
prints `1.0 2.0 0.8112781244591328`, `[1, 3, 3, 3, 4, 4]`, `3 -1`, `9 20`,
`[0, 7, 9, 20, 20, 11]` and `[0, 7, 9, 20, 20, 11, 1000000]`. Two equally
likely symbols carry one bit and four carry two; a source that says one thing
three times in four carries 0.81. The symbols of frequencies 45, 13, 12, 16, 9
and 5 get codewords of 1, 3, 3, 3, 4 and 4 bits in a Huffman code. A binary
search finds the 7 at index 3 and reports -1 for the 4 that is not there; it
takes 9 probes to find the first of a thousand and 20 to find the last of a
million. In the graph, the shortest distance from node 0 to node 4 is 20, by
way of 2 and 5. A seventh node that no edge reaches keeps the distance 1000000,
which is how the script says it was not reached.
What `computing` does not do is listed in the
[Library Reference](/ref/dke/library/): it does not check that a word is a
string of 0 and 1, it holds numbers only to the 63 binary digits of an `int`,
and its shortest paths need weights from 0 up.
## Chapter 24 — A seventeenth module: `calendars`
`calendars` reads three kinds of record. From the years you name it concludes
which are leap years under the Julian rule and under the Gregorian rule, and
which the two disagree about. From a calendar reform it counts the days the
reform skipped, and from the days you name it marks those that fall in the gap,
days a place never had. It also calculates what a historian and a programmer
calculate: the length of a month, whether a date exists, the weekday of a date,
Easter, day numbers and the span between two instants. Version **1.0.0**, at
[dke.langsyn.com/source/calendars/latest/calendars.dpy](https://dke.langsyn.com/source/calendars/latest/calendars.dpy).
A year is written by its number, and the module concludes the rest. Eleven
years around 1900 show the two rules part:
```python
import calendars
calendars.years(1895, 1905)
print(calendars.years_that_differ())
print(calendars.julian_leap(1900), calendars.gregorian_leap(1900), calendars.gregorian_leap(1904))
print(calendars.differ(1900), calendars.differ(1904))
n = why_not(calendars.Year.y1900.gregorian_leap)
print(n)
print(calendars.gregorian_leap(2000))
```
prints `[1900]`, `True False True`, `True False`, two lines for the `why_not`
and `False`. The Julian rule makes every fourth year a leap year, so 1900 is
one, and the Gregorian rule makes it one only if it is not a century, or if it
divides by 400, so 1900 is not. It is the one year in the eleven the rules
disagree about, and `differ` says so. The `why_not` of 1900 names both ways it
could have been a leap year and why each failed: ``rule
`calendars.gregorian_leap_fourth_century` needs calendars.Year[y1900].r400 ==
0, and calendars.Year[y1900].r400 is 300 (int)`` and ``rule
`calendars.gregorian_leap_ordinary` needs calendars.Year[y1900].r100 != 0, and
calendars.Year[y1900].r100 is 0 (int)``. The last line is a trap worth seeing
once: 2000 is a Gregorian leap year, but this store holds no year 2000, so the
rules have concluded nothing about it and the answer is `False`. The
calculations, which come last, answer for any year.
A reform is the last day of the old calendar in a place and the first of the
new, written as year, month and day, with the source that says so. A day is
written the same way. Rome's reform and Britain's, and three days:
```python
import calendars
calendars.reform("rome", 1582, 10, 4, 1582, 10, 15, "Inter gravissimas")
calendars.reform("britain", 1752, 9, 2, 1752, 9, 14, "Calendar (New Style) Act 1750")
calendars.day("d1", 1582, 10, 10)
calendars.day("d2", 1582, 10, 20)
calendars.day("d3", 1752, 9, 10)
print(calendars.skipped("rome"), calendars.skipped("britain"))
print(calendars.missing("d1", "rome"), calendars.missing("d2", "rome"), calendars.missing("d1", "britain"))
print(calendars.missing("d3", "britain"), calendars.missing("d3", "rome"))
```
prints `[10] [11]`, `True False False` and `True False`. Rome went from Thursday
4 October 1582 to Friday 15 October and skipped 10 days; Britain went from 2
September 1752 to 14 September and skipped 11. The tenth of October 1582 is in
Rome's gap, a day Rome never had, and the twentieth is not. The same day is not
in Britain's, which still kept the old calendar, and the tenth of September
1752 is in Britain's gap and not in Rome's. The module marks a day that falls
in a gap and keeps it: the day is a fact, and the gap is a conclusion about
it.
A correction withdraws what rested on it, and a rival reading is kept beside
the first:
```python
import calendars
calendars.day("d5", 1752, 9, 14)
calendars.reform("britain", 1752, 9, 2, 1752, 9, 15, "Calendar (New Style) Act 1750")
print(calendars.skipped("britain"), calendars.missing("d5", "britain"))
calendars.reform("britain", 1752, 9, 2, 1752, 9, 14, "Calendar (New Style) Act 1750")
print(calendars.skipped("britain"), calendars.missing("d5", "britain"))
calendars.reform("rome", 1582, 10, 4, 1582, 10, 16, "a rival reading")
print(calendars.skipped("rome"))
print(calendars.unsettled())
calendars.day("d4", 1582, 10, 15)
print(calendars.missing("d4", "rome"))
```
prints `[12] True`, `[11] False`, `[10, 11]`, `['reform rome first']` and
`True`. Britain's reform is first written with a first day of 15 September, by
mistake, and the 14th is then in the gap and 12 days are skipped. Writing the
reform again under the same source replaces the value, the skipped days go back
to 11, and the 14th is out of the gap: nothing that rested on the mistake
remains. The rival reading of Rome's reform is written under another source, so
nothing is replaced. Both values are reasoned about, `skipped` gives both
counts, `unsettled()` names the disagreement, and the 15th of October is in the
gap under the rival value, so `missing` says so. No answer refuses.
The calculations answer values and write nothing:
```python
import calendars
print(calendars.month_length(1900, 2), calendars.month_length(2000, 2), calendars.julian_month_length(1900, 2))
print(calendars.date_exists(2026, 2, 29), calendars.date_exists(2028, 2, 29))
print(calendars.weekday(2026, 11, 9), calendars.weekday(1582, 10, 15))
print(calendars.easter(2026), calendars.easter(2000))
print(calendars.day_number(2000, 1, 1) - calendars.day_number(1900, 1, 1))
print(calendars.julian_day_number(1582, 10, 4), calendars.julian_day_number(1582, 10, 5))
a = datetime("2026-11-09T12:00:00Z")
b = datetime("2028-03-01T00:00:00Z")
print(calendars.span(a, b), calendars.after(a, 100))
```
prints `28 29 29`, `False True`, `1 5`, `[4, 5] [4, 23]`, `36524`, `577737 577738`
and `P477DT12H 2027-02-17T12:00:00Z`. February 1900 has 28 days under the
Gregorian rule and 29 under the Julian, and February 2000 has 29. 29 February
2026 does not exist and 2028's does. The weekday is a number from 0 for Sunday,
so 9 November 2026 is a Monday and 15 October 1582 a Friday, counted in the
proleptic Gregorian calendar. Easter Sunday is 5 April in 2026 and 23 April in
2000. The days from 1 January 1900 to 1 January 2000 are 36524, a century of
365-day years with 24 leap days. The Julian day numbers of 4 and 5
October 1582 are consecutive, which is how the module counts the days a reform
skipped. The span from noon on 9 November 2026 to the start of 1 March 2028 is
477 days and 12 hours, and a hundred days after the first is 17 February 2027.
What `calendars` does not do is listed in the
[Library Reference](/ref/dke/library/): it counts dates in the proleptic
Gregorian calendar and a `datetime` in years 1 to 9999, a leap second has no
place in it, `span` refuses a time of more than about 292 years, and it does
not check that a date given as three integers exists unless you ask with
`date_exists`.
## Chapter 25 — An eighteenth module: `colour`
`colour` reads two kinds of record. From the relative luminance of a text and of
its background it concludes which of the four contrast levels of the Web Content
Accessibility Guidelines the pair clears. From the refractive indices on either
side of a boundary and the sine of the angle at which a ray meets it, it
concludes whether the ray is totally reflected. It also calculates what a
designer and a physicist calculate: relative luminance, the contrast ratio,
hue, saturation and lightness, Snell's law, the critical angle, the thin lens,
Wien's law and the frequency of a wavelength. Version **1.0.1**, at
[dke.langsyn.com/source/colour/latest/colour.dpy](https://dke.langsyn.com/source/colour/latest/colour.dpy).
A pair is written as the luminance of its text and of its background, and
`luminance_hex` gives one from a colour written `#rrggbb`:
```python
import colour
colour.pair("grey", colour.luminance_hex("#777777"), 1.0)
colour.pair("ink", colour.luminance_hex("#000000"), 1.0)
colour.pair("white_on_blue", 1.0, colour.luminance_hex("#0000ee"))
print(colour.levels("grey"), colour.levels("ink"), colour.levels("white_on_blue"))
print(colour.contrast_hex("#777777", "#ffffff"))
n = why_not(colour.Pair.grey.aa)
print(n)
colour.pair("grey", colour.luminance_hex("#595959"), 1.0)
print(colour.levels("grey"))
update(colour.Pair.grey.fg, 0.3, "a second reading")
print(colour.unsettled(), colour.levels("grey"))
```
prints `['large AA'] ['AA', 'AAA', 'large AA', 'large AAA'] ['AA', 'AAA', 'large
AA', 'large AAA']`, `4.478089453577214`, two lines for the `why_not`,
`['AA', 'AAA', 'large AA', 'large AAA']` and `['pair grey fg']
['AA', 'AAA', 'large AA', 'large AAA']`. Grey `#777777` on white has a contrast
ratio of 4.478, just under the 4.5 that AA asks, so it clears only the level for
large text. The `why_not` of `aa` names the two ways a pair could have cleared
it and what failed: ``rule `colour.aa_dark_text` needs
colour.Pair[grey].inverse >= 4.5, and colour.Pair[grey].inverse is
4.478089453577214 (real)`` and ``rule `colour.aa_light_text` needs
colour.Pair[grey].fg > colour.Pair[grey].bg, and colour.Pair[grey].fg is
0.184474994500441 (real)``. Black on white clears all four, as does white on
`#0000ee`, a blue. Writing the grey pair again with the darker `#595959`
replaces the first value, and it clears all four. A second source gives the
text a luminance of 0.3. Both values are reasoned about: `unsettled()` names
the fact, and the pair clears the levels the darker value clears, because a
conclusion stands if any value on record supports it.
A ray is written as the index it leaves, the index it meets and the sine of its
angle of incidence:
```python
import colour
colour.ray("glass_air", 1.52, 1.0, 0.8)
colour.ray("glass_air_shallow", 1.52, 1.0, 0.5)
colour.ray("air_glass", 1.0, 1.52, 0.99)
print(colour.total("glass_air"), colour.total("glass_air_shallow"), colour.total("air_glass"))
n = why_not(colour.Ray.glass_air_shallow.total)
print(n)
print(colour.critical_angle(1.52, 1.0), colour.critical_angle(2.417, 1.0))
print(colour.refraction_angle(1.0, 1.333, 30.0), colour.refraction_angle(1.52, 1.0, 30.0))
```
prints `True False False`, ``rule `colour.total_internal_reflection` needs
colour.Ray[glass_air_shallow].sin_t > 1.0, and colour.Ray[glass_air_shallow].sin_t
is 0.76 (real)``, `41.139510414899156 24.439743865451863` and
`22.030108725578987 49.46419788868344`. Light in glass at a sine of 0.8 meets
air at a refracted sine of 1.216, which no angle has, so it is totally
reflected. At a sine of 0.5 the refracted sine is 0.76 and the ray escapes, and
the `why_not` says so. A ray going into glass is never totally reflected. The
critical angle for glass into air is 41.1 degrees and for diamond into air 24.4,
the small angle that lets a cut diamond hold light. Light in air meeting water at 30
degrees refracts to 22.0, and in glass meeting air at 30 degrees to 49.5.
The calculations answer values and write nothing:
```python
import colour
print(colour.luminance(255, 255, 255), colour.luminance(255, 0, 0), colour.luminance(0, 128, 0))
print(colour.contrast_ratio(1.0, 0.0), colour.contrast_ratio(0.0, 1.0))
print(colour.hsl(255, 0, 0), colour.hsl(0, 128, 0), colour.hsl(128, 128, 128))
print(colour.lens_image(10.0, 30.0), colour.lens_magnification(10.0, 30.0), colour.lens_image(10.0, 5.0))
print(colour.wien_peak(5778.0), colour.wien_peak(300.0))
print(colour.frequency_hz(500.0), colour.frequency_hz(700.0))
```
prints `1.0 0.2126 0.15438342968146074`, `21.0 21.0`, `[0.0, 1.0, 0.5]
[120.0, 1.0, 0.25098039215686274] [0.0, 0.0, 0.5019607843137255]`,
`14.999999999999996 -0.5 -10.0`, `5.015181645898235e-07
9.65923985e-06` and `599584915999999.9 428274939999999.94`. White has a
luminance of 1.0, pure red 0.2126, the weight of red in the formula, and a
mid green 0.154. The contrast ratio is the same whichever of the two is
lighter, and 21 for black and white. Pure red has hue 0, saturation 1 and
lightness one half; the green is at 120 degrees; a grey has no hue and no
saturation. A converging lens of focal length 10 with an object at 30 forms a
real image at 15, half the size of the object and inverted, so the
magnification is a negative one half, exactly, since it is taken as f / (f - u)
rather than from the image distance; the last digits of 15 are those of the
floating point, as in every calculation here. An object at 5, inside the
focus, gives an image at -10, on the same side: a virtual one. The Sun's
radiation peaks near 5.0e-07 metres, 500 nanometres, and a room at 300 kelvin
near 9.7e-06, about nineteen times longer. A wavelength of 500 nanometres is a frequency of
about 6.0e14 hertz and 700 nanometres 4.3e14.
Some calls refuse where no value exists. `colour.critical_angle(1.0, 1.52)`,
into a denser medium, refuses with a math domain error, as does
`refraction_angle` for a ray that is totally reflected. `lens_image` with the
object at the focus refuses with a division by zero, and `luminance_hex` of a
string that is not `#` and six hexadecimal digits refuses and says how a colour
is written.
What `colour` does not do is listed in the
[Library Reference](/ref/dke/library/): it covers sRGB and no other colour
space, the saturation it computes is the HSL model's, it has no spectra, the
sign conventions of a diverging lens are outside it, and a refractive index does
not vary with wavelength.
## Chapter 26 — A nineteenth module: `body`
**`body` is not medical advice.** It states rules taught in every introduction
to the subject and arithmetic with published formulas. Blood compatibility in
practice is decided by laboratory testing, a body mass index is a measure for
populations that its own namer judged inappropriate for individual evaluation,
and the energy and heart-rate formulas are estimates that may deviate
significantly from a person's actual value. It gives no dose, no diagnosis and
no treatment. Ask a clinician. Nothing in this chapter is a recommendation for
anyone's body.
`body` reads two kinds of record. From the blood group of a donor and of a
recipient it concludes which of the antigens A, B and D the donor's red cells
carry and the recipient's do not, and from that whether red cells of the donor
would react, from a lacked A or B, and whether they put the recipient at risk of
sensitisation, from a lacked D. From a person's mass and height it concludes
the World Health Organization's bands of body mass index. It also calculates
the index, a resting energy, a body surface and the maximum heart rate by two
formulas. Version **1.0.2**, at
[dke.langsyn.com/source/body/latest/body.dpy](https://dke.langsyn.com/source/body/latest/body.dpy).
A transfusion is written as two groups, with the source that says so:
```python
import body
body.transfusion("t1", "O-", "AB+", "ward")
body.transfusion("t2", "AB+", "O-", "ward")
body.transfusion("t3", "O-", "O+", "ward")
body.transfusion("t4", "O+", "O-", "ward")
body.transfusion("t5", "A+", "B+", "ward")
print(body.verdict("t1"), body.verdict("t2"), body.verdict("t3"))
print(body.verdict("t4"), body.verdict("t5"))
print(body.lacks("t2"), body.lacks("t4"), body.lacks("t5"))
n = why_not(body.Transfusion.t1.reacts)
print(n)
print(body.antigens("AB+"), body.antigens("O-"))
update(body.Transfusion.t4.rd, 1, "a second reading")
print(body.unsettled(), body.verdict("t4"))
```
prints `[] ['reacts', 'sensitises'] []`, `['sensitises'] ['reacts']`,
`['A', 'B', 'D'] ['D'] ['A']`, two lines for the `why_not`, `[1, 1, 1] [0, 0, 0]`
and `['transfusion t4 rd'] ['sensitises']`. Red cells of group O negative carry
none of the three antigens, so given to an AB positive recipient nothing is
lacked and nothing is concluded: the verdict is empty, and the `why_not` of
`reacts` says that both of the premises it reads, a lacked A and a lacked B, have
nothing recorded. The reverse, AB positive to O negative, lacks all three: it
reacts, from the A and the B, and sensitises, from the D. O negative to O
positive lacks nothing. O positive to O negative lacks only the D antigen, and
the verdict is the risk of sensitisation without a reaction. A positive to B
positive lacks the A antigen, so it reacts. `antigens` shows how a group is held:
AB positive is A, B and D, all present, and O negative none. A second source
that gives the recipient of the fourth transfusion a D antigen is held beside
the first. `unsettled()` names the fact, and the verdict is unchanged, because a
conclusion stands if any value on record supports it. This is a statement of
a taught rule, and a laboratory decides a real transfusion.
A person is written as a mass in kilograms and a height in metres:
```python
import body
body.person("p1", 70.0, 1.75)
body.person("p2", 50.0, 1.80)
body.person("p3", 95.0, 1.70)
body.person("p4", 80.0, 0.0)
print(body.band("p1"), body.band("p2"), body.band("p3"))
print(body.bmi("p1"), body.bmi("p3"))
print(repr(body.band("p4")), body.bmi("p4"))
n = why_not(body.Person.p4.normal)
print(n)
body.person("p1", 75.0, 1.75)
print(body.band("p1"), body.bmi("p1"))
```
prints `normal underweight obese`, `22.857142857142858 32.871972318339104`, `''
0.0`, ``rule `body.normal_weight` needs body.Person[p4].bmi >= 18.5, and nothing
is recorded there`` and `normal 24.489795918367346`. The index is the mass over
the square of the height: 70 kilograms at 1.75 metres is 22.9, which is in the
normal band from 18.5 up to but not including 25. 50 kilograms at 1.80 is under
18.5, and 95 at 1.70 is 32.9, obese, and also overweight, since the bands above
25 are cumulative; `band` gives the narrowest. A height of zero draws no index.
The index has no answer, so no band is concluded, `band` gives an empty string
and `bmi` 0.0, and the `why_not` says that nothing is recorded where the rule
reads the index. Writing a person again replaces the values the same source
wrote, and the index moves to 24.49. These are bands of a population statistic,
not a judgement of a person.
The calculations answer values and write nothing:
```python
import body
print(body.body_mass_index(70.0, 1.75), body.body_mass_index(95.0, 1.7))
print(body.mifflin_st_jeor(70.0, 175.0, 30.0, True), body.mifflin_st_jeor(60.0, 165.0, 30.0, False))
print(body.body_surface_mosteller(70.0, 175.0), body.body_surface_mosteller(60.0, 165.0))
print(body.max_heart_rate_haskell_fox(40.0), body.max_heart_rate_tanaka(40.0))
print(body.max_heart_rate_haskell_fox(20.0), body.max_heart_rate_tanaka(20.0))
```
prints `22.857142857142858 32.871972318339104`, `1648.75 1320.25`,
`1.8446619684315546 1.6583123951777`, `180.0 180.0` and `200.0 194.0`. The first
line is the index again, as a calculation. The Mifflin-St Jeor equation gives a
resting energy of 1648.75 kilocalories a day for a man of 70 kilograms, 175
centimetres and 30 years, and 1320.25 for a woman of 60 kilograms, 165
centimetres and 30 years; they are estimates for adults. Mosteller's body
surface is 1.84 square metres for the first and 1.66 for the second. The two
formulas for the maximum heart rate agree at 40 years, at 180 beats a minute, and
part at 20, where 220 minus the age gives 200 and 208 minus 0.7 times the age
gives 194. Their standard deviations are 12 to 15 beats and about 10, so neither
number is a limit for a person.
A group written any other way refuses: `body.antigens("C+")` says that a
blood group is A, B, AB or O, then plus or minus. A height of zero given to
`body_mass_index` refuses with a division by zero.
What `body` does not do is listed in the
[Library Reference](/ref/dke/library/): plasma and platelet compatibility, the
Kell and other blood systems, a rare group, children's growth and any dose.
## Chapter 27 — A twentieth module: `sound`
**`sound` is not occupational-health advice.** It states published limits as data
and arithmetic with published formulas. A limit is a figure on a page: whether a
worker is protected is decided by a measurement and a hearing conservation
programme, and a dose over a limit is not a finding of hearing loss. It gives no
diagnosis and no treatment. Ask an occupational-health professional. Nothing in
this chapter is a recommendation for anyone's hearing.
`sound` reads three kinds of record. From a sound level held for some hours, and
the limit it is judged against, it concludes whether the exposure is over its
limit. From the segments of a worker's day, each at its own level, it sums their
doses and concludes whether the day is over. From a frequency it concludes
whether the tone is audible, or lies below the range of hearing or above it. It
also calculates what an acoustician calculates: the level of a pressure, two
sources added, the fall with distance, the speed of sound, a wavelength, the
Doppler shift and the average level of a dose. Version **1.0.4**, at
[dke.langsyn.com/source/sound/latest/sound.dpy](https://dke.langsyn.com/source/sound/latest/sound.dpy).
A limit is two numbers: a reference level for eight hours, and an exchange rate,
the decibels by which a level may rise while the time permitted halves. 90 and 5
is the limit OSHA's table sets out, and 85 and 3 the level NIOSH recommends.
Two exposures to 95 decibels for four hours, against each:
```python
import sound
sound.exposure("e1", 95.0, 4.0, 85.0, 3.0)
sound.exposure("e2", 95.0, 4.0, 90.0, 5.0)
print(sound.over_limit_names(), sound.dose("e1"), sound.dose("e2"))
n = why_not(sound.Exposure.e2.over)
print(n)
sound.exposure("e3", 100.0, 2.0, 90.0, 5.0)
print(sound.dose("e3"), sound.over_limit_names())
sound.exposure("e1", 95.0, 0.5, 85.0, 3.0)
print(sound.over_limit_names(), sound.dose("e1"))
print(sound.permissible_hours(95.0, 90.0, 5.0), sound.permissible_hours(100.0, 90.0, 5.0), sound.permissible_hours(85.0, 85.0, 3.0))
```
prints `['e1'] 5.039684199579493 1.0`, one line for the `why_not`, `1.0 ['e1']`,
`[] 0.6299605249474366` and `4.0 2.0 8.0`. Against 85 and 3 the permitted time at
95 decibels is 0.79 hours, so four hours is a dose of 5.04, five times what the
limit permits, and the exposure is over. Against 90 and 5 it is exactly four
hours, a dose of 1.0, and the `why_not` says so: ``rule `sound.over_limit` needs
sound.Exposure[e2].dose > 1.0, and sound.Exposure[e2].dose is 1.0 (real)``. A dose
of one is the whole of what the limit permits, and it is not over it. The third
exposure, 100 decibels for two hours against 90 and 5, is also exactly at the
limit, so only `e1` is over. An exposure over its limit is marked and kept: write
`e1` again with half an hour and the mark is withdrawn, the dose 0.63. The last
line shows the hours a limit permits: 4 at 95 decibels and 2 at 100 against 90
and 5, and 8 at the reference level itself. That formula is this module's own
derivation, not one read from the standards, and a published table rounds where
it does not.
A worker's day is a sum of the doses of its segments, each with its own limit:
```python
import sound
sound.segment("w1", "s1", 95.0, 2.0, 85.0, 3.0)
sound.segment("w1", "s2", 88.0, 3.0, 85.0, 3.0)
sound.segment("w2", "s3", 90.0, 4.0, 90.0, 5.0)
sound.segment("w2", "s4", 95.0, 2.0, 90.0, 5.0)
print(sound.days_over(), sound.total_of("w1"), sound.total_of("w2"))
n = why_not(sound.Worker.w2.over)
print(n)
sound.segment("w1", "s2", 88.0, 1.0, 85.0, 3.0)
print(sound.days_over(), sound.total_of("w1"))
print(sound.twa_from_dose(100.0), sound.twa_from_dose(50.0), sound.twa_from_dose(200.0))
```
prints `['w1'] 3.2698420997897464 1.0`, one line for the `why_not`, `['w1']
2.7698420997897464` and `90.0 84.99989177202127 95.00010822797873`. The first worker
spent two hours at 95 decibels and three at 88 against 85 and 3, doses of 2.52 and
0.75, so the day's total is 3.27 and it is over. The second spent four hours at 90
and two at 95 against 90 and 5, doses of 0.5 and 0.5, so the total is 1.0, exactly
at the limit, and the `why_not` of `over` says so. Writing the second segment of
the first worker again with one hour replaces its three: the total falls to 2.77,
still over. The last line turns a dose in percent into an 8-hour average level: a
dose of 100 percent is 90 decibels, and half of it is just under 85, 84.99989177202127
and not 85, because the formula's constant, 16.61, is itself a rounding of the
5 decibels over the logarithm of 2.
A tone is a frequency in hertz:
```python
import sound
sound.tone("t1", 440.0)
sound.tone("t2", 20.0)
sound.tone("t3", 19.99)
sound.tone("t4", 20000.0)
sound.tone("t5", 40000.0)
print(sound.range_of("t1"), sound.range_of("t2"), sound.range_of("t3"), sound.range_of("t4"), sound.range_of("t5"))
n = why_not(sound.Tone.t3.audible)
print(n)
print(repr(sound.range_of("nobody")))
```
prints `audible audible infrasound audible ultrasound`, one line for the `why_not`
and `''`. Both ends of 20 to 20,000 are inside the range, so 20 and 20,000 are
audible, 19.99 is below it and 40,000 above. The `why_not` of `audible` for the
19.99 names the premise that failed: ``rule `sound.audible_tone` needs
sound.Tone[t3].hz >= 20.0, and sound.Tone[t3].hz is 19.99 (real)``. A name that
was never written has no range: the answer is the empty string.
The calculations answer values and write nothing:
```python
import sound
print(sound.level_of_pressure(0.02, 0.00002), sound.level_of_pressure(1.0, 0.00002))
print(sound.add_levels(80.0, 80.0), sound.add_levels(90.0, 70.0))
print(sound.at_distance(90.0, 1.0, 2.0), sound.at_distance(90.0, 1.0, 10.0))
print(sound.speed_of_sound(0.0), sound.speed_of_sound(20.0))
print(sound.wavelength(343.0, 440.0))
print(sound.doppler(440.0, 343.0, 0.0, 30.0), sound.doppler(440.0, 343.0, 0.0, -30.0))
print(sound.level_of_pressure(1.4142135623730951, 1.0))
```
prints `60.0 93.97940008672037`, `83.01029995663981 90.04321373782642`,
`83.97940008672037 70.0`, `331.32 343.2353419474446`, `0.7795454545454545`,
`482.17252396166134 404.6112600536193` and `3.0102999566398125`. A pressure of
0.02 pascal against the reference of 20 micropascals is 60 decibels, and one
pascal 94. Two equal sources of 80 decibels make 83.0, three decibels more, and
a loud source and a quiet one barely add: 90 and 70 make 90.04. In free field the
level falls 6.02 decibels for each doubling of the distance, from 90 at one metre
to 83.98 at two and to 70 at ten. The speed of sound is 331.32 metres a second at
freezing and 343.2 at 20 degrees, and a note of 440 hertz has a wavelength of 0.78
metres. A source at 440 hertz coming toward a still listener at 30 metres a second
is heard at 482 hertz, and going away at 405. The last line is the one place a
result is not always the same as CPython's: `math.log10` is correctly rounded here,
and CPython's gives 3.010299956639812 for the same pressure.
Some calls refuse where no value exists. `sound.level_of_pressure(0.0, 0.00002)`
refuses with a math domain error, and so does `sound.speed_of_sound(-300.0)`, below
absolute zero; `sound.doppler` with a source moving at the speed of sound refuses
with a division by zero, and `sound.at_distance` with a distance of zero refuses.
A second source can disagree about a fact:
```python
import sound
update(sound.Exposure.e2.hours, 6.0, "a second reading")
print(sound.over_limit_names(), sound.unsettled())
```
prints `['e2'] ['exposure e2 hours']`. The second reading gives `e2` six hours and
the first four. Both are reasoned about, and the exposure is over if any value
puts it over, so the list names it, and `unsettled()` names the fact the two
sources disagree about, whichever of the fields you write it is. A read of one value is a different thing: `sound.dose("e2")` now
refuses, saying that the cell holds two competing values and that the
single-claim read refuses.
What `sound` does not do is listed in the
[Library Reference](/ref/dke/library/): frequency weighting, octave bands, room
acoustics and reverberation, sound in water, the action level and the hearing
conservation programme of either limit, and any limit of another country.
## Chapter 28 — A twenty-first module: `finance`
**`finance` is not financial advice.** It states which of several offers costs the
least in total and which carry a payment over a limit you give it, and it does
arithmetic with published formulas. It does not say which offer to take: the
cheapest in total and an offer over a household's limit can be the same offer, and
the module reports both. Every rate, price and income is yours. Ask a financial
adviser. Nothing in this chapter is a recommendation for anyone's money.
`finance` reads two kinds of record. From the loan offers you record, each a
borrower, a principal, a monthly payment and a number of months, it concludes the
total and the interest of each, which offer is the cheapest in total of those
that share a borrower and a principal, and which carry a payment over the limit
of the borrower's budget. It also calculates what a lender and a saver
calculate: the payment of an amortised loan, compound interest, effective rates,
the doubling time, present value and the real rate. Version **1.0.2**, at
[dke.langsyn.com/source/finance/latest/finance.dpy](https://dke.langsyn.com/source/finance/latest/finance.dpy).
An offer is written as a rate, which `quote` turns into a payment, or as the
payment itself. Two offers for the same 200,000, and a household limit:
```python
import finance
finance.quote("a", "ann", 200000.0, 0.05, 360.0)
finance.quote("c", "ann", 200000.0, 0.06, 120.0)
finance.budget("ann", 1500.0)
print(finance.cheapest_offers(), finance.unaffordable_offers())
n = why_not(finance.Offer.a.cheapest)
print(n)
m = why_not(finance.Offer.a.unaffordable)
print(m)
print(finance.total("a"), finance.total("c"))
print(finance.monthly_payment(200000.0, 0.0, 240.0))
finance.quote("b", "ann", 200000.0, 0.055, 240.0)
print(finance.cheapest_offers(), finance.total("b"))
finance.quote("c", "ann", 200000.0, 0.06, 360.0)
print(finance.cheapest_offers(), finance.unaffordable_offers())
```
prints `['c'] ['c']`, two lines for the `why_not`s, `386511.56856874016
266449.20465995866`, `833.3333333333334`, `['c'] 330185.9077724346` and `['b'] []`.
The 30-year loan at 5 percent has a monthly payment of 1073.64 and costs
386,511.57 in all, and the 10-year loan at 6 percent has a payment of 2220.41 and
costs 266,449.20. The shorter one is the cheapest in total, and it is also the
only one over the household's limit of 1500, so `c` is both: the module reports
the two facts and does not choose between them. The `why_not` of `cheapest` for
`a` says that the absence it needs does not hold, a cheaper offer being present:
``rule `finance.cheapest_offer` needs finance.Offer[a].cheapest_offer__none1 to be
absent, and it is present``. The other says that the payment of 1073.64 is not
above the limit. A rate of zero is a payment of the principal over the months,
833.33 for 240. A third offer, 20 years at 5.5 percent, costs 330,185.91 and does
not displace `c`. Writing `c` again as a 30-year loan at 6 percent raises its
total to 431,676 and withdraws the mark: `b` is now the cheapest, and with `c`'s
payment down to 1199.10 nothing is over the limit.
Offers are compared only within a borrower and a principal, and equal totals are
each cheapest:
```python
import finance
finance.offer("t1", "bo", 10000.0, 500.0, 24.0)
finance.offer("t2", "bo", 10000.0, 1000.0, 12.0)
finance.offer("t3", "bo", 10000.0, 400.0, 36.0)
finance.offer("t4", "bo", 5000.0, 900.0, 6.0)
print(finance.cheapest_offers(), finance.unaffordable_offers())
print(finance.total("t1"), finance.total("t2"), finance.total("t3"), finance.total("t4"))
n = why_not(finance.Offer.t1.unaffordable)
print(n)
finance.budget("bo", 450.0)
print(finance.unaffordable_offers())
print(finance.total("nobody"))
```
prints `['b', 't1', 't2', 't4'] []`, `12000.0 12000.0 14400.0 5400.0`, one line for
the `why_not`, `['t1', 't2', 't4']` and `0.0`. The list begins with `b` from the
offers above. `t1` and `t2` cost the same, 12,000, and each is cheapest, because
neither has a cheaper one; `t3` costs 14,400 and is not. `t4` is for another
principal, 5,000, and is alone in its group, so it is cheapest of it. Until a
budget is recorded for the borrower nothing is over a limit, and the `why_not` of
`unaffordable` says that nothing is recorded where the rule reads the limit. With
a limit of 450, the payments of 500, 1000 and 900 are over it, and the 400 is
not. The total of an offer never written is 0.0.
The calculations answer values and write nothing:
```python
import finance
print(finance.monthly_payment(200000.0, 0.05, 360.0), finance.monthly_payment(10000.0, 0.12, 12.0))
print(finance.compound_amount(1000.0, 0.05, 12.0, 10.0), finance.compound_amount(1000.0, 0.05, 1.0, 10.0))
print(finance.effective_annual_rate(0.1299, 12.0), finance.effective_annual_rate(0.2999, 12.0), finance.effective_annual_rate(0.12, 1.0))
print(finance.continuous_annual_rate(0.05), finance.effective_annual_rate(0.05, 365.0))
print(finance.doubling_exact(8.0), finance.doubling_rule_of_72(8.0), finance.doubling_exact(30.0), finance.doubling_rule_of_72(30.0))
print(finance.present_value(1000.0, 0.10, 5.0), finance.todays_value(1000.0, 0.03, 20.0))
print(finance.real_rate(0.05, 0.03), 0.05 - 0.03)
```
prints `1073.6432460242781 888.487886783417`, `1647.00949769028 1628.894626777442`,
`0.1379199031259423 0.34475762144741345 0.12`,
`0.05127109637602404 0.05126749646746255`, `9.006468342000588 9.0
2.6419267958111403 2.4`, `620.9213230591549 553.6757541863345` and
`0.01941747572815533 0.020000000000000004`. A year-long loan of 10,000 at 12
percent costs 888.49 a month. 1,000 at 5 percent for ten years is 1647.01 compounded
monthly and 1628.89 compounded yearly. A card at 12.99 percent compounded monthly
is an effective 13.79 percent and one at 29.99 percent is 34.48, the nominal rate
being lower than the one paid. Compounding continuously adds only a little over
daily: 5.127 percent against 5.1267. At 8 percent money doubles in 9.006 years
and the rule of 72 says 9.0; at 30 percent the exact time is 2.64 years and the
rule's 2.4, which shows why the rule suits rates from 6 to 10 percent. 1,000 in
five years at 10 percent is worth 620.92 now, and 1,000 after twenty years of 3
percent inflation buys what 553.68 does today. A nominal 5 percent with 3 percent
inflation is a real 1.94 percent by the exact form, where the subtraction gives 2.
The 0.020000000000000004 is the double: the last digits of every result here are
those of the floating point, and nothing is rounded to the cent.
Some calls refuse where no value exists. `finance.monthly_payment(1000.0, 0.05,
0.0)` and `finance.doubling_exact(0.0)` refuse with a division by zero, a sum of
no months and one that never doubles, and `finance.doubling_exact(-100.0)` with a
math domain error.
A second source can disagree about a fact:
```python
import finance
update(finance.Offer.t3.payment, 450.0, "a second reading")
print(finance.cheapest_offers(), finance.unaffordable_offers(), finance.unsettled())
```
prints `['b', 't1', 't2', 't4'] ['t1', 't2', 't4'] ['offer t3 payment']`. Both
payments are reasoned about, and neither makes `t3` the cheapest or puts it over
the limit of 450, so the lists do not change, and `unsettled()` names the
payment. The total of `t3` is a different matter:
`finance.total("t3")` refuses, saying that the cell holds two competing values.
What `finance` does not do is listed in the
[Library Reference](/ref/dke/library/): fees folded into an annual percentage
rate, taxes, a schedule row by row, variable rates and any statute.
## Chapter 29 — A twenty-second module: `food`
**`food` is not food-safety or medical advice.** It states published guidance as
data and rules over the facts you record, and arithmetic with published
conversions. A temperature in it is a figure from a chart: whether a food is safe
to eat is decided by the cook, with a thermometer that reads the food, and
whether an ingredient is safe for a person with an allergy is decided by that
person and their doctor. A dish it does not mark unsafe is not thereby safe, and
a dish it does not mark as holding an allergen holds none only of the ingredients
you recorded. It gives no diagnosis. Ask a clinician about an allergy. Nothing in
this chapter is a recommendation for anyone's kitchen or plate.
`food` reads four kinds of record. From a dish's kind, the temperature its core
reached and the minutes it spent in the danger zone, it concludes whether the
dish is undercooked, was held too long, or is unsafe for either cause; a dish
whose kind has no minimum recorded it calls unknown, and never safe. From the
items you describe and the lines that put them in a dish, it concludes which of
the nine major allergens the dish holds. It also calculates what a cook
calculates: Fahrenheit to Celsius, a recipe scaled by servings, a baker's
percentage, a volume as a mass, food energy and kilojoules. Version **1.0.3**, at
[dke.langsyn.com/source/food/latest/food.dpy](https://dke.langsyn.com/source/food/latest/food.dpy).
The minimum temperatures are a chart's: FoodSafety.gov's "Cook to a Safe Minimum
Internal Temperature", whose page reads "Date Last Reviewed November 21, 2024".
They were read from a copy of that page downloaded on 2026-11-09, because an
automated fetch of the agency's sites was refused. `chart()` writes the eleven
standards of its rows that give a temperature, and `standard` writes one of your
own from a Fahrenheit figure. A `casserole` is one with meat or poultry, or
meatless, at 165 F, and the chart's note that cooked hams packaged in
USDA-inspected plants are reheated to 140 F is a kind of its own,
`ham-packaged`, where `ham-precooked` stays at 165 F:
```python
import food
food.chart()
food.dish("d1", "poultry", 70.0, 30.0)
food.dish("d2", "salmon", 70.0, 30.0)
food.dish("d3", "steak", 63.0, 30.0)
food.dish("d4", "fish", 70.0, 150.0)
food.dish("d5", "casserole", 70.0, 30.0)
food.dish("d6", "ham-packaged", 62.0, 30.0)
food.dish("d7", "ham-precooked", 62.0, 30.0)
food.dish("d8", "casserole", 74.0, 30.0)
print(food.unsafe_dishes(), food.unknown_dishes())
n = why_not(food.Dish.d2.undercooked)
print(n)
m = why_not(food.Dish.d3.undercooked)
print(m)
food.dish("d1", "poultry", 75.0, 30.0)
print(food.unsafe_dishes())
food.standard("salmon", 145.0)
print(food.unsafe_dishes(), food.unknown_dishes())
print(food.danger_zone("usda"), food.danger_zone("uk"))
```
prints `['d1', 'd4', 'd5', 'd7'] ['d2']`, two lines for the `why_not`s,
`['d4', 'd5', 'd7']`, `['d4', 'd5', 'd7'] []` and `[4.0, 60.0] [8.0, 63.0]`. Poultry has a minimum of 165 F, 73.89 C, and the
core of `d1` reached 70, so it is undercooked and unsafe. `d4`, a fish with a core
of 70, is above its minimum of 62.78 but spent 150 minutes in the danger zone,
more than the two hours the rule allows, so it is unsafe for that cause. `d5`, a casserole at 70, is under the 165 F of a
casserole, meatless or not, while `d8` at 74 is not. `d6` and `d7` are cooked hams
reheated to 62: that is above the 60 C of a packaged ham, 140 F, and below the
73.89 of other precooked ham, so `d7` is unsafe and `d6` is not. Nothing is
recorded for salmon, so `d2` is neither: it is unknown, and the `why_not` of
`undercooked` says so: ``rule `food.undercooked_dish` needs
food.Standard[salmon].minimum, and nothing is recorded there``. A dish whose kind
has no standard is never read as safe. The steak with a core of 63 is not
undercooked: the chart prints 63 for 145 F, but the module converts exactly, to
62.78, and 63 is above it, which the other `why_not` shows. Writing `d1` again at 75
withdraws its mark. Recording a standard for salmon, as 145 F, makes `d2` known,
and at 70 degrees it is neither undercooked nor unsafe. The danger zone is two
ranges, because agencies define it differently: 4 to 60 for the USDA and 8 to 63
for the United Kingdom.
An ingredient is described by the allergens it holds, and a line puts it in a
dish:
```python
import food
food.item("flour", ["wheat"])
food.item("butter", ["milk"])
food.item("eggs", ["egg"])
food.item("pesto", ["milk", "tree_nuts"])
food.item("sugar", [])
food.line("l1", "cake", "flour")
food.line("l2", "cake", "butter")
food.line("l3", "cake", "sugar")
food.line("l4", "pasta", "flour")
food.line("l5", "pasta", "pesto")
food.line("l6", "pasta", "mystery")
print(food.allergens_of("cake"), food.allergens_of("pasta"), food.allergens_of("soup"))
n = why_not(food.Dish.cake.egg)
print(n)
food.line("l2", "cake", "sugar")
print(food.allergens_of("cake"))
food.line("l7", "cake", "eggs")
print(food.allergens_of("cake"))
print(food.unknown_dishes())
```
prints `['milk', 'wheat'] ['milk', 'tree_nuts', 'wheat'] []`, one line for the
`why_not`, `['wheat']`, `['egg', 'wheat']` and `['cake', 'pasta']`. The cake holds
the wheat of its flour and the milk of its butter, and the pasta the milk and the
tree nuts of its pesto as well as the flour's wheat. Sugar was described as
holding none of the nine, which is a statement that it is free from them; the
`mystery` line in the pasta names an item that was never described, which holds
nothing the rules can see, so the answer for the pasta is only as complete as what
you recorded. The module does not say that the pasta is free of anything else. A
dish nothing was written for, `soup` here, holds none and the list is empty.
The `why_not` of `egg` for the cake says that no line of the cake uses an item
that holds an egg. Replacing the butter by sugar withdraws the milk, and adding
the eggs brings the egg. Last, the cake and the pasta exist only through their
lines. They have no kind, so no standard was there to read, and the module calls
them unknown.
The calculations answer values and write nothing:
```python
import food
print(food.fahrenheit_to_celsius(145.0), food.fahrenheit_to_celsius(32.0), food.fahrenheit_to_celsius(212.0))
print(food.scale_by_servings(250.0, 4.0, 6.0), food.scale_by_servings(1.5, 4.0, 10.0))
print(food.bakers_percentage(700.0, 1000.0), food.bakers_percentage(1000.0, 1000.0), food.bakers_percentage(20.0, 1000.0))
print(food.volume_to_mass(1.0, 1.0), food.volume_to_mass(2.0, 0.53))
print(food.food_energy(10.0, 30.0, 5.0, 0.0), food.food_energy(0.0, 0.0, 0.0, 14.0))
print(food.kilocalories_to_kilojoules(100.0), food.kilocalories_to_kilojoules(food.food_energy(10.0, 30.0, 5.0, 0.0)))
```
prints `62.77777777777778 0.0 100.0`, `375.0 3.75`, `70.0 100.0 2.0`,
`236.5882365 250.78353069`, `205.0 98.0` and `418.40000000000003 857.72`. 145 F
is 62.78 C, freezing is 0 and boiling 100. A recipe for 4 that uses 250 grams
uses 375 for 6, and 1.5 teaspoons for 4 is 3.75 for 10. A dough with 700 grams of
water to 1000 of flour is at 70 percent hydration, the flour itself is always 100,
and 20 grams of salt is 2 percent. One US customary cup is 236.59 millilitres, so
a cup of a liquid with a density of 1 is 236.59 grams, and two cups of something
with a density of 0.53 weigh 250.78; the module holds no density, and the one you
supply is the one used. Ten grams of protein, 30 of carbohydrate and 5 of fat are
205 kilocalories at the labelling table's factors of 4, 4 and 9, 14 grams of
alcohol are 98, and a kilocalorie is 4.184 kilojoules, so 100 of them are 418.4
and 205 of them 857.72. Those factors are approximate.
A second source can disagree about a fact:
```python
import food
update(food.Dish.d3.core, 60.0, "a second reading")
print(food.unsafe_dishes(), food.unsettled())
```
prints `['d3', 'd4'] ['dish d3 core']`. The second reading gives the steak a core
of 60, below its minimum, and the first gave 63. Both are reasoned about, and a
dish is unsafe if any value puts it there, so the steak is now on the list, and
`unsettled()` names the core the two sources disagree about.
Some calls refuse where no value exists: `food.item("x", ["gluten"])` refuses an
allergen that is not one of the nine, `food.danger_zone("who")` an agency other
than `usda` and `uk`, and `food.bakers_percentage(5.0, 0.0)` a flour of zero, with
a division by zero.
What `food` does not do is listed in the
[Library Reference](/ref/dke/library/): a cooling curve, a rest, nutrients beyond
protein, carbohydrate, fat and alcohol, cooking losses and yields, and the rows of
the chart that give no temperature.
## Chapter 30 — A twenty-third module: `architecture`
**`architecture` is not engineering, naval-architecture, acoustic-design or
heritage advice.** It states published definitions and simple models as rules
over the facts you record. A tower in it is a straight column of even weight, a
hull is a box, and a room is Sabine's equation: whether a real structure is safe,
a real ship stable or a real hall good for music is decided by those who survey,
test and certify it. Nothing in this chapter is a judgement of any real building,
ship or hall.
`architecture` reads eight kinds of record. From the features you record for a
building, it concludes whether the building is Gothic or Romanesque and whether
it carries a dome on pendentives. From the heights and years of structures, it
concludes which was the tallest structure, and which the tallest building, when
it was completed. It decides whether a tower tips on the model of a straight
column, whether a box hull's metacentric height is not positive, whether a room
rings longer or shorter than you want, and whether a channel falls too little.
It also calculates a lean, Sabine's reverberation time, a hull's metacentric
height, a gradient and a pendulum. Version **1.0.2**, at
[dke.langsyn.com/source/architecture/latest/architecture.dpy](https://dke.langsyn.com/source/architecture/latest/architecture.dpy).
A style is a family resemblance, and a rule makes it strict. `building()` writes
all nine features it knows, true for those you name and false for the rest:
```python
import architecture
architecture.building("cologne", ["pointed_arch", "rib_vault", "flying_buttress"])
architecture.building("abbey", ["round_arch", "thick_walls", "groin_vault"])
architecture.building("hagia", ["dome", "pendentives"])
architecture.building("chapel", ["pointed_arch", "rib_vault", "round_arch"])
print(architecture.styles_of("cologne"), architecture.styles_of("abbey"), architecture.styles_of("hagia"))
print(architecture.styles_of("chapel"), architecture.styles_of("nowhere"))
n = why_not(architecture.Building.chapel.gothic)
print(n)
architecture.building("chapel", ["pointed_arch", "rib_vault", "flying_buttress", "round_arch"])
print(architecture.styles_of("chapel"))
```
prints `['gothic'] ['romanesque'] ['pendentive_dome']`, `[] []`, one line for the
`why_not` and `['gothic']`. Gothic is pointed arches, rib vaults and flying
buttresses, the three of the four features its definition names that are parts
of the structure; the fourth is its windows. Romanesque is round arches and thick
walls with a barrel vault or a groin vault. A dome on pendentives is a form and
not a style, because a pendentive is a device for setting a round dome on a
square room, not a period. The chapel has two of the three
Gothic features, and the `why_not` names the third: ``rule
`architecture.gothic_building` needs architecture.Building[chapel].flying_buttress
== True, and architecture.Building[chapel].flying_buttress is False (bool)``.
`nowhere` was never described, so it has no style, which is not the same as
having none. Writing the chapel again with flying buttresses makes it Gothic.
A record of height is kept for structures and for buildings separately. Here are
six of the records on the list of the tallest structures built before the
twentieth century, with the years the two that fell came down:
```python
import architecture
architecture.structure("lincoln", 160.0, 1311.0, True)
architecture.fell("lincoln", 1548.0)
architecture.structure("beauvais", 153.0, 1569.0, True)
architecture.fell("beauvais", 1573.0)
architecture.structure("cologne", 157.0, 1880.0, True)
architecture.structure("washington", 169.0, 1884.0, False)
architecture.structure("eiffel", 300.0, 1889.0, False)
architecture.structure("ulm", 161.53, 1890.0, True)
architecture.structure("pyramid", 146.6, -2570.0, False)
print(architecture.tallest_structures())
print(architecture.tallest_buildings())
print(architecture.record_until("cologne", False), architecture.record_until("cologne", True), architecture.record_until("eiffel", False))
print(architecture.record_until("pyramid", False))
n = why_not(architecture.Structure.ulm.tallest_structure)
print(n)
update(architecture.Structure.oldspire.height, 170.0, "structure")
print(architecture.undated_structures(), architecture.tallest_structures())
```
prints `['beauvais', 'cologne', 'eiffel', 'lincoln', 'pyramid', 'washington']`,
`['beauvais', 'cologne', 'lincoln', 'ulm']`, `1884.0 1890.0 10000.0`, `1311.0`,
one line for the `why_not`, and `['oldspire'] ['beauvais', 'cologne', 'eiffel',
'lincoln', 'pyramid', 'washington']`. A structure is the tallest when it was
completed if no structure you recorded was completed in that year or before it,
still stood after it and was taller. Lincoln's spire fell in 1548, so Beauvais, at 153 metres, is the tallest in
1569 though it is shorter than Lincoln was. The Washington Monument and the
Eiffel Tower are not buildings, so they end Cologne's record as a structure, in
1884, and leave its record as a building to Ulm, in 1890. `record_until` gives
those years, and 10000.0 for the Eiffel Tower, which nothing you recorded passed:
passed by nothing recorded, not held for ever. The Great Pyramid, completed about
2570 BC, is written with a negative year and compared like any other. Its record
passes in 1311 here, with Lincoln, because Lincoln is the first taller structure
this program records; the list ends it in 1221, with Old St Paul's, which is not
recorded here. Ulm was not the tallest structure,
and its `why_not` says ``rule `architecture.tallest_structure_when_completed`
needs architecture.Structure[ulm].tallest_structure_when_completed__none1 to be
absent, and it is present``: the rule asks that no taller structure be found,
and one was. Last, a structure with a height and no year is `undated` and is not
ranked. With no year, nothing could be completed before it, so it would
otherwise be called the tallest.
A tower tips, on the model, when its centre of mass, half the top's offset from
the vertical, passes the edge of its base. A hull is unstable when its
metacentric height is not positive:
```python
import architecture
architecture.tower("t1", 56.67, 3.9, 15.484)
architecture.tower("t2", 40.0, 6.0, 6.0)
architecture.tower("t3", 40.0, 6.0, 5.0)
print(architecture.tipping_towers())
s = current(architecture.Tower.t1.shift)
r = current(architecture.Tower.t1.radius)
print(s.value, r.value)
architecture.hull("box1", 6.0, 3.0, 4.0)
architecture.hull("box2", 6.0, 3.0, 2.5)
architecture.hull("box3", 10.0, 2.0, 3.0)
print(architecture.unstable_hulls())
architecture.hull("box1", 6.0, 3.0, 2.0)
g = current(architecture.Hull.box1.gm)
print(architecture.unstable_hulls(), g.value)
```
prints `['t3']`, `1.95 7.742`, `['box1', 'box2']` and `['box2'] 0.5`. A tower 56.67
metres high with its top 3.9 metres out and a base 15.484 metres across shifts its
centre of mass 1.95 metres, far inside a radius of 7.742, and does not tip on the
model; whether a real tower stands depends also on the ground under it, which the
model does not see. `t2`'s shift is exactly its radius, which is not past it, and
`t3`'s passes it. A box hull's metacentric height is half its draught, plus its
beam squared over twelve times its draught, less the height of its centre of
gravity: 1.5 + 1.0 - 4.0 for `box1`, which is unstable, and exactly 0 for `box2`,
which is unstable too. Ballast that lowers `box1`'s centre of gravity to 2 metres
gives it 0.5 and withdraws the verdict. The metacentric height measures stability
at small angles of heel only.
A room's time is Sabine's, from its volume and the absorption of the surfaces
that line it:
```python
import architecture
architecture.room("hall", 18750.0, 1.6, 2.2)
architecture.room("booth", 30.0, 0.3, 0.5)
print(architecture.unmeasured_rooms())
architecture.surface("s1", "hall", 2000.0, 0.3)
architecture.surface("s2", "hall", 1500.0, 0.1)
architecture.surface("s3", "booth", 40.0, 0.9)
t = current(architecture.Room.hall.rt)
print(architecture.live_rooms(), architecture.dead_rooms(), t.value)
architecture.surface("s4", "hall", 800.0, 0.8)
u = current(architecture.Room.hall.rt)
print(architecture.live_rooms(), u.value)
```
prints `['booth', 'hall']`, `['hall'] ['booth'] 4.025` and `[] 2.1717625899280577`.
Before any surface is recorded, both rooms are unmeasured: they have no time, and
are neither too live nor too dead. Then the hall's two surfaces absorb 600 and
150 square metres, and 0.161 x 18750 / 750 is 4.025 seconds, longer than the 2.2
you want, while the booth's 36 give it 0.13, shorter than its 0.3. A fourth
surface brings the hall's absorption to 1390 and its time to 2.17, inside the
range, and the verdict is withdrawn. The absorption is the exact sum of the
surfaces' sabins, rounded once, so the order you write them in does not move it.
A channel is held to a standard, and a standard can be read two ways:
```python
import architecture
architecture.vitruvius()
architecture.channel("nimes", 50000.0, 17.0, "vitruvius")
architecture.channel("steep", 1000.0, 10.0, "vitruvius")
architecture.channel("mill", 1000.0, 1.0, "mine")
print(architecture.flat_channels(), architecture.channels_without_standard())
print(architecture.unsettled())
architecture.fall("mine", 1.0 / 2000.0, "my survey")
print(architecture.flat_channels(), architecture.channels_without_standard())
```
prints `['nimes'] ['mill']`, `['fall vitruvius minimum']` and `['nimes'] []`.
Vitruvius's minimum fall for a channel is half a foot in a hundred in one
translation and one in 4,800 in another account, and `vitruvius()` writes both as
two values of one fact. A channel falling 17 in 50,000 is too flat under the
first and within the second, and it is marked too flat because one value puts it
there; a channel not marked is within every value. `unsettled()` names the fact
the two disagree about. The mill's standard, `mine`, has no value until you write
one, so it is marked as without a standard rather than within it; one in 2,000
then holds it, and its one in 1,000 is within it.
The calculations answer values and write nothing:
```python
import architecture
print(architecture.lean_degrees(3.9, 56.67), architecture.top_offset(56.67, 3.97))
print(architecture.reverberation_time(18750.0, 1390.0), architecture.metacentric_height(10.0, 2.0, 3.0))
print(architecture.gradient(456.0, 0.025), architecture.one_in(456.0, 0.025))
print(architecture.pendulum_period(1.0), architecture.pendulum_length(2.0))
print(architecture.lean_degrees(1.0, 14.0))
```
prints `3.936858505470097 3.9329360885791114`, `2.1717625899280577
2.166666666666667`, `5.482456140350877e-05 18240.0`,
`2.0064092925890407 0.9936213855661318` and `4.085616779974876`. A top 3.9 metres
out on a height of 56.67 is a lean of 3.94 degrees, and a lean of 3.97 degrees on
that height puts the top 3.93 metres out. The hall above rings for 2.17 seconds,
and a box hull 10 wide, 2 deep, with its centre of gravity 3 above the keel has a
metacentric height of 2.17 metres. A drop of 2.5 centimetres in 456 metres is one
in 18,240. A pendulum a metre long swings back and forth in 2.006 seconds at
standard gravity, and one that takes two seconds is 0.994 metres long. Each step
is rounded once, and `atan` and `tan` are correctly rounded, so a lean or an
offset can differ from CPython's in the last place: the last line is
4.085616779974877 in CPython 3.11. Every other result here is CPython's to the
last digit.
A second source can disagree about a fact:
```python
import architecture
update(architecture.Hull.box3.kg, 6.0, "a second survey")
print(architecture.unstable_hulls(), architecture.unsettled())
```
prints `['box2', 'box3'] ['fall vitruvius minimum', 'hull box3 kg']`. The second
survey puts `box3`'s centre of gravity at 6 metres, where its metacentric height is
negative, and the first put it at 3. Both are reasoned about, and a hull is
unstable if any value makes it so, so `box3` is on the list, and `unsettled()`
names the fact beside the one Vitruvius left.
Some calls refuse where no value exists: `architecture.building("x", ["spire"])`
refuses a feature that is not one of the nine, `architecture.record_until("ulm",
False)` a structure that was not the tallest when it was completed, and
`architecture.reverberation_time(100.0, 0.0)` an absorption of zero, with a
division by zero. `record_until` also refuses while a structure's height or year
holds two values, since either could move the year it gives.
What `architecture` does not do is listed in the
[Library Reference](/ref/dke/library/): loads, materials and foundations, wind,
stability at large angles of heel, hull forms other than a box, reverberation
formulas other than Sabine's, and the flow of water in a channel.
## Chapter 31 — A twenty-fourth module: `elections`
**`elections` is not legal or electoral advice.** It states published counting
methods as rules and calculations over the ballots and populations you record. It
certifies no election and counts no real ballot for anyone: which method an
election uses, who may vote and what a ballot says are decided by the law and the
officials who run it. Nothing in this chapter is a judgement of any real election.
`elections` reads five kinds of record. From an election's ballots, each a ranking
of every candidate and the number of voters who cast it, it concludes which
candidate the most voters put first, which more than half put first, which beats
every other candidate head to head and which every other beats, and which has the
highest Borda score. It finds that no candidate beats every other when the
head-to-head counts run in a cycle, and it refuses to name a winner while a count
is disputed. It also counts an instant runoff, round by round, and calculates the
Hare and Droop quotas and a house's seats by four methods. Version **1.0.0**, at
[dke.langsyn.com/source/elections/latest/elections.dpy](https://dke.langsyn.com/source/elections/latest/elections.dpy).
You write an election with its candidates, and a ballot for each ranking, with the
number of voters who cast it. The four cities of the example the sources share
choose a capital: 42 voters live in Memphis, 26 in Nashville, 15 in Chattanooga
and 17 in Knoxville, and each wants the nearest city first:
```python
import elections
elections.election("tn", ["memphis", "nashville", "chattanooga", "knoxville"])
elections.ballot("m", "tn", 42.0, ["memphis", "nashville", "chattanooga", "knoxville"])
elections.ballot("n", "tn", 26.0, ["nashville", "chattanooga", "knoxville", "memphis"])
elections.ballot("c", "tn", 15.0, ["chattanooga", "knoxville", "nashville", "memphis"])
elections.ballot("k", "tn", 17.0, ["knoxville", "chattanooga", "nashville", "memphis"])
w = elections.winners("tn")
print(w["plurality"], w["majority"], w["condorcet"], w["condorcet_loser"], w["borda"])
print(elections.first_choices("tn"))
print(elections.borda_scores("tn"))
print(elections.head_to_head("tn")["nashville>memphis"], elections.without_condorcet_winner())
n = why_not(elections.Candidate.memphis.condorcet)
print(n)
print(elections.runoff("tn"))
```
prints `['memphis'] [] ['nashville'] ['memphis'] ['nashville']`, then `{'chattanooga':
15.0, 'knoxville': 17.0, 'memphis': 42.0, 'nashville': 26.0}`, `{'chattanooga': 173.0,
'knoxville': 107.0, 'memphis': 126.0, 'nashville': 194.0}`, `58.0 []`, one line for
the `why_not` and the instant-runoff count, which is taken up below. Each method gives its own answer. Memphis has the most first
choices, 42, and is the plurality winner, but 42 is not more than half of 100, so
there is no majority winner. Nashville beats every other city head to head, and 58
voters rank it over Memphis, so it is the Condorcet winner, and it has the highest
Borda score, 194. Memphis, which the most voters put first, is beaten by every
other city, so it is also the Condorcet loser. The Borda score is the sum of a
candidate's head-to-head tallies, which is the count of n - 1 points for a first
place down to none for a last. The election has a Condorcet winner, so
`without_condorcet_winner()` is empty. The `why_not` on Memphis's `condorcet` says
``rule `elections.condorcet_winner` needs
elections.Candidate[memphis].condorcet_winner__none1 to be absent, and it is
present``: the rule asks that no duel be lost or tied, and one was.
The last line is an instant-runoff count, which takes the ballots round by round.
Each ballot counts for the highest candidate on it still standing, and the
candidate with the fewest votes goes out. It is a list of the four rounds' tallies,
`[{'chattanooga': 15.0, 'knoxville': 17.0, 'memphis': 42.0, 'nashville':
26.0}, {'knoxville': 32.0, 'memphis': 42.0, 'nashville': 26.0}, {'knoxville': 58.0,
'memphis': 42.0}, {'knoxville': 100.0}]`. Chattanooga, with 15, goes out first,
and its 15 voters go to Knoxville, their next choice. Nashville goes out next,
and its 26 go to Knoxville too, who then holds 58 against Memphis's 42 and wins.
The instant runoff chooses Knoxville, whom none of the three methods above chose:
a method is a rule for reading the same ballots, and they do not agree. A tie for
last place refuses, since the method names no way to break one.
A Condorcet winner need not exist:
```python
import elections
elections.election("cycle", ["a", "b", "c"])
elections.ballot("x", "cycle", 1.0, ["a", "b", "c"])
elections.ballot("y", "cycle", 1.0, ["b", "c", "a"])
elections.ballot("z", "cycle", 1.0, ["c", "a", "b"])
w = elections.winners("cycle")
print(w["condorcet"], w["condorcet_loser"], w["plurality"], w["borda"])
print(elections.without_condorcet_winner())
```
prints `[] [] ['a', 'b', 'c'] ['a', 'b', 'c']` and `['cycle']`. Two of the three
voters rank A over B, two rank B over C and two rank C over A, so every candidate
beats one and loses to another, and no candidate beats every other: the election
is in `without_condorcet_winner()`. Nor does any candidate lose to every other.
Each candidate has one first choice, the most any has, so each is a plurality
winner, and each has the same Borda score.
A rule that adds up a fact adds every value it holds, so a second count of a ballot
would be added to the first. `recount()` writes the second count under a source of
its own, and the election is then in dispute and decides nothing:
```python
import elections
elections.election("pair", ["x", "y"])
elections.ballot("b1", "pair", 5.0, ["x", "y"])
elections.ballot("b2", "pair", 3.0, ["y", "x"])
print(elections.winners("pair")["plurality"])
elections.recount("b2", 4.0, "recount")
print(elections.disputed_elections())
d = why_not(elections.Candidate.x.plurality)
print(d)
forget(elections.Ballot.b2.count, 4.0, "recount")
print(elections.disputed_elections(), elections.winners("pair")["plurality"])
```
prints `['x']`, `['pair']`, one line for the `why_not` and `[] ['x']`. Five voters
put `x` first and three put `y` first, so `x` wins. While the ballot `b2` holds
both 3 and 4, no winner is concluded for `pair` and the readers refuse it, and the
`why_not` on `x`'s `plurality` says ``rule `elections.plurality_winner` needs
elections.Election[pair].disputed to be absent, and it is present``. A winner
concluded from the total, 7 for `y` against 5, would be the winner under neither
count: `x` wins under each. Forgetting the claim that is not to stand settles the
dispute, and every verdict returns.
The calculations answer values and write nothing. A house's seats are shared among
states by their populations, in whole numbers, so a tie is found exactly:
```python
import elections
print(elections.hare_quota(100000, 8), elections.droop_quota(100000, 8))
print(elections.largest_remainder({"a": 6, "b": 6, "c": 2}, 10))
print(elections.highest_averages({"a": 100000, "b": 80000, "c": 30000, "d": 20000}, 8, "dhondt"))
print(elections.highest_averages({"a": 100000, "b": 80000, "c": 30000, "d": 20000}, 8, "sainte_lague"))
print(elections.highest_averages({"a": 100000, "b": 80000, "c": 30000, "d": 20000}, 8, "huntington_hill"))
print(elections.priority_value(100000, 3))
```
prints `12500.0 11111.111111111111`, `{'a': 4, 'b': 4, 'c': 2}`, `{'a': 4, 'b': 3,
'c': 1, 'd': 0}`, `{'a': 3, 'b': 3, 'c': 1, 'd': 1}`, `{'a': 3, 'b': 3, 'c': 1,
'd': 1}` and `28867.51345948129`. A hundred thousand votes for eight seats is a
Hare quota of 12,500 and an exact Droop quota of 11,111.11. Largest remainders
gives each state the whole part of its share, and the one seat left goes to the
largest remainder, which is state `c`'s. D'Hondt divides each population by one,
two, three and on, which favours the large state: `d` gets none. Sainte-Lague
divides by one, three, five and on, and gives `d` a seat. Huntington-Hill gives
every state one seat first and then ranks each population over the square root of
n (n + 1), which is a priority of 28,867.51 for a state of 100,000 holding three
seats; it compares squared populations in whole numbers.
Some calls refuse where no answer exists:
```python
import elections
elections.election("duo", ["u", "v"])
elections.ballot("d1", "duo", 5.0, ["u", "v"])
try:
elections.ballot("d2", "duo", 3.0, ["v"])
except refuse as r:
print(r.reason)
try:
elections.highest_averages({"a": 100000, "b": 100000}, 3, "dhondt")
except refuse as r:
print(r.reason)
try:
elections.winners("nowhere")
except refuse as r:
print(r.reason)
elections.recount("d1", 7.0, "recount")
try:
elections.winners("duo")
except refuse as r:
print(r.reason)
forget(elections.Ballot.d1.count, 7.0, "recount")
print(elections.winners("duo")["plurality"])
```
prints `a ballot ranks every candidate of its election once`, `a tie for the last
seat`, `election nowhere is not written`, `the count of election duo is disputed,
and disputed_elections() names it` and `['u']`. A ballot that leaves candidates out
refuses, and so does a tie that more states share than there are seats left to
give: two states of 100,000 tie for the last seat of three, and no method here
breaks a tie. An election that is not written refuses its readers, and so does a
disputed one, and settling the dispute returns its winners.
What `elections` does not do is listed in the
[Library Reference](/ref/dke/library/): the law of any election, who may vote,
what a ballot says, ballots that tie or leave candidates out, and the allocation of
seats from ballots rather than from populations.
## Chapter 32 — What it costs
Nothing beyond what you already pay. Reasoning modules carry no separate
charge: importing one, and everything it then does, is billed by the same
per-operation meter as any other work you send. A module that reasons more costs
more, in proportion, and one you never call costs nothing.
---
## Chapter 33 — Where to go next
- The [Library Reference](/ref/dke/library/) lists every
kind, field, derived field and query, module by module, and every public
name in `dkepy`.
- `dkekey` issues a store key on your own machine, for a program with no
browser; [MCP Reference §3.1](/ref/dke/mcp/#issuing-a-store-key-on-your-own-machine)
says how, and `dke.keys` in `dkepy` does the same from Python.
- The [Python Reference §9](/ref/dke/python/#modules) specifies module
semantics in general — resolution, namespacing, reconciliation.
- The [Python Tutorial](/tutorial/dke/python/) teaches the language modules
are written in.
- The [Library Specification](/spec/dke/library/) states the modules and the
`dkepy` package as a contract: what you may depend on, and what may change
under you.
## Revision history
Newest first. The revision is the surface's — one number stated by every
published document, the web pages and the service wire, moving whenever any
document changes. This list records the revisions at which this document
changed and what changed in it; a revision absent here left this document as it
was. The Version line at the top states this document's
version, the revision it last changed in and the current revision; from rev
148 each entry opens with the version that revision gave this document. The history
starts at rev 1. What a revision promises is
stated in the [Python Specification](/spec/dke/python/), its section 11.1.
- **1.0.4** · **rev 163** (2026-11-14) — The Version line states API 1.4: `range` is a value of its own, walked without being built, so a `for` over a million numbers runs, and a range keeps its three bounds as `.start`, `.stop` and `.step` ([Specification §8.6](/spec/dke/python/#indexing-slicing-and-string-methods)), and an addition moves the API's MINOR.
- **1.0.3** · **rev 151** (2026-11-13) — The Version line states API 1.3: a rule premise may test part of a text, as `r.path.startswith("/srv/")`, `t.name.endswith(".test")` and `"admin" in a.role` ([Specification §14.2](/spec/dke/python/#premises)), and an addition moves the API's MINOR.
- **1.0.2** · **rev 150** (2026-11-13) — The Version line states API 1.2: calls made with keys that may only read run on one store together, where every call on a store ran alone ([MCP §9.8](/ref/dke/mcp/#stop-end-the-call-running-on-your-store)), and an addition moves the API's MINOR. Nothing else changed, and no module did.
- **1.0.1** · **rev 149** (2026-11-13) — The Version line states API 1.1: DKE Python gained a form a rule's conclusion may take, a text written from values the rule read (Python Specification §14.3), and an addition moves the API's MINOR. Nothing else changed, and no module did.
- **1.0.0** · **rev 148** (2026-11-12) — This document states a version of its own, 1.0.0, first on the Version line and followed by the revision it last changed in, and from this revision each entry here opens with the version it made (Python Specification §11.1). Nothing else changed.
- **rev 147** (2026-11-12) — The Version line states API 1.0: the API version is `MAJOR.MINOR`. Nothing else changed.
- **rev 145** (2026-11-12) — The library carries `physics` at 1.0.3,
`chemistry` at 1.0.3, `biology` at 1.0.2, `astronomy` at 1.0.2, `geography`
at 1.0.2 and `mathematics` at 1.0.2, whose calculations answer wherever their
value is a real, where they answered `inf`, `nan`, `0.0` or a wrong solution
when their work passed the range of a real. The outputs of Chapters 15 to 20
are unchanged.
- **rev 144** (2026-11-12) — The library carries `body` at 1.0.2, `food` at
1.0.3, `colour` at 1.0.1, `sound` at 1.0.4, `finance` at 1.0.2,
`earthscience` at 1.0.2 and `architecture` at 1.0.2, whose calculations answer
wherever their value is a real, where they answered `inf`, `nan` or `0.0` when
their work passed the range of a real. Chapter 25's magnification prints the
exact -0.5, and Chapter 28's payments, totals and rates print digits closer to
the exact values; 0.12 compounded once is 0.12. The other chapters' outputs are
unchanged.
- **rev 143** (2026-11-12) — The library carries `statistics` at 1.0.3, whose
probabilities and descriptive statistics answer wherever their value is a
real, where they answered `inf`, `nan` or `0.0` when their work passed the
range of a real. Chapter 21's outputs are unchanged.
- **rev 142** (2026-11-12) — The library carries `sound` at 1.0.3, whose
`add_levels` adds two levels of any size; the example's second sum prints its
last digit as the exact value rounds it.
- **rev 133** (2026-11-10) — The library carries eleven modules at new versions,
which no longer add a fact two sources give into a sum: while a member is bound to
a sum twice, the sum is disputed and has no value, a query names it, and its
reader refuses it until the claim that is not to stand is forgotten. Chapter 15
shows a series group refused and named by `physics.disputed_groups()`, Chapter 16
a sulfane whose weight is disputed while its formula stands and a reaction that
`faults()` lists, and Chapter 21 a study whose disputed count is not pooled. No
other chapter changed except its statement of the version, and Chapter 21's had
said 1.0.0 since 1.0.1 was published.
- **rev 132** (2026-11-10) — The library carries `quantum` at 1.3.0, whose exact check of a
reversible circuit takes a measurement: a circuit may measure a qubit in the X basis and
correct its sign with a gate that runs only where the outcome was 1, and the check holds
over every outcome. No chapter changed except its statement of the version.
- **rev 131** (2026-11-10) — The library carries `quantum` at 1.2.0, which adds the
Toffoli, CCZ and Fredkin gates and the exact checks `rev_same` and `rev_meets` of
a reversible circuit. No chapter changed except its statement of the version.
- **rev 130** (2026-11-10) — Chapter 31 teaches `elections` 1.0.0, which is not
legal or electoral advice: an election's ballots and the winners under plurality,
majority, the Condorcet method and the Borda count, an instant-runoff count, an
election with no Condorcet winner, a count in dispute that decides nothing, and
the Hare and Droop quotas and four methods of sharing seats. What it costs is now
Chapter 32 and Where to go next Chapter 33.
- **rev 129** (2026-11-09) — Chapter 30 teaches `architecture` 1.0.0: a building's
style and the feature it lacks, the records of height of six structures, a tower
and a hull on their models, a room's reverberation time over its surfaces, a
channel held to Vitruvius's two readings, the calculations, and a second source
that disagrees. What it costs is now Chapter 31 and Where to go next Chapter 32.
- **rev 126** (2026-11-09) — Chapter 29 `food` is at 1.0.2, credits FoodSafety.gov's
chart, and its first program adds a meatless casserole and two hams reheated to
62 C, under `ham-packaged` and `ham-precooked`, which `chart()` now writes among
eleven standards.
- **rev 125** (2026-11-09) — Chapters 18, 22 and 26 name `astronomy`, `earthscience`
and `body` at 1.0.1. The astronomy chapter says that `window()` refuses with
`up()` while a booking's start or end has two values.
- **rev 124** (2026-11-09) — Chapters 27 to 29 name `sound` 1.0.1, `finance` 1.0.1
and `food` 1.0.1, whose `unsettled()` names a dispute over any fact you record,
and each shows it beside the second reading it already gave.
- **rev 123** (2026-11-08) — The library carries `calendars`, `colour` and `body`
at 1.0.0, each on the source surface. Chapter 24 is new: `calendars`; Chapter 25
`colour`; Chapter 26 `body`, which says in its opening that it is not medical
advice. The library also carries `sound`, `finance` and `food` at 1.0.0: Chapter
27 is `sound`, which says in its opening that it is not occupational-health
advice; Chapter 28 `finance`, not financial advice; Chapter 29 `food`, not
food-safety or medical advice. What it costs is now Chapter 30 and where to go
next Chapter 31.
- **rev 122** (2026-11-08) — The library carries `statistics`, `earthscience` and
`computing` at 1.0.0, each on the source surface. Chapter 21 is new:
`statistics`; Chapter 22 `earthscience`; Chapter 23 `computing`. What it costs
is now Chapter 24 and where to go next Chapter 25.
- **rev 121** (2026-11-08) — The library carries `astronomy`, `geography` and
`mathematics` at 1.0.0, each on the source surface. Chapter 18 is new:
`astronomy`; Chapter 19 `geography`; Chapter 20 `mathematics`. What it costs
is now Chapter 21 and where to go next Chapter 22.
- **rev 119** (2026-11-08) — Chapters 15 and 16 name `physics` 1.0.1 and
`chemistry` 1.0.1, whose `unsettled()` names a dispute over any fact you
record.
- **rev 118** (2026-11-08) — Chapters 15 to 17 are new: `physics` 1.0.0,
checking a hand-over of units, the dimensions of equations and a circuit, and
a few calculations; `chemistry` 1.0.0, reading a molecule's formula, weight
and groups from its bonds, a reaction's enthalpy, a balanced equation and a
pH; and `biology` 1.0.0, a network's scope and what it needs, a pedigree's
carriers and what its mode cannot explain, and a few calculations. What it
costs is now Chapter 18 and where to go next Chapter 19.
- **rev 116** (2026-11-08) — Chapter 14 is new: `harmony` 1.0.0, marking an
exercise and its correction, a melody's rhythm and a few calculations. What
it costs is now Chapter 15 and where to go next Chapter 16.
- **rev 114** (2026-11-06) — Chapter 12 names `quantum` 1.1.0, and shows a
measurement whose value comes from a 256-bit key, ChaCha20 as RFC 8439
defines it, the generator a secret can stand behind.
- **rev 112** (2026-11-06) — Chapter 12 names `quantum` 1.0.3, where it still
named 1.0.1: 1.0.2 collapses a measured state in one `vector.apply`, and
1.0.3 weighs its two outcomes in one `vector.sumsq`.
- **rev 33** (2026-10-23) — "What you run is what you can read" states the
guarantee on its own.
- **rev 31** (2026-10-19) — This is the DKE Library's Tutorial, served at
`/tutorial/dke/library/` beside the Library Specification; the old address
leads here. The introduction says what the library holds: the reasoning
modules, `dkepy` and `dkekey`. Cross-references name the Library Reference,
and Chapter 15 points to `dkekey` and to the Reference's names for `dkepy`.
The Status line says what the document is.
- **rev 30** (2026-10-17) — Chapter 7 names `code` 1.12.3, a release that
changes one comment and nothing the module answers.
- **rev 27** (2026-10-17) — Each module moves to its next patch release, whose
source opens with an SPDX licence line and is otherwise unchanged: Chapter 7
names `code` 1.12.2, and each module's chapter states its own new version.
- **rev 26** (2026-10-17) — The header states Version, Status, Audience and the
Markdown copy, one field to a line, and the introduction follows it. Chapter
14: a module carries no separate charge. Chapter 15 links the Library
Specification.
- **rev 25** (2026-10-16) — Chapter 12's measurement example is a whole
program: it builds the Bell pair itself, where it continued the example
before it.
- **rev 24** (2026-10-16) — Two chapters: Chapter 12 on `quantum` and Chapter
13 on `pqc`, each with examples whose output is shown as a store printed it.
What it costs is now Chapter 14 and where to go next Chapter 15. Chapter 2
names the three modules that have standing constraints, where it named
`agenda` alone.
- **rev 22** (2026-10-14) — The modules and `dkepy` are served from
`https://dke.langsyn.com/source/`, beside these documents, and every download
link names that address. The `dke.langsyn.org` addresses still lead to the
same files.
- **rev 4** (2026-10-11) — The closing chapter is numbered, Chapter 13, as the
other tutorials number theirs. Cross-references take the published documents'
short names, the ones the footer uses — Modules Reference, Python Reference,
Python Tutorial, MCP Reference, Python Specification. Nothing else changed.
- **rev 3** (2026-10-11) — The revision is the surface's: one number shared by
every published document, the web pages and the service wire. This section
says so, and the Version line moves from rev 1 to it. Nothing else changed.
- **rev 1** (2026-10-11) — The Version line states the document's revision and
this section is added. Nothing else changed.