DKE Library — Reference

Version: 1.0.5, changed in rev 171 · alpha · API 1.5 · rev 171
Status: Reference for the DKE Library.
Audience: Customers using a published reasoning module in their own store, and the programs that write its facts.
This document as Markdown: dke-library-ref.md — the same text this page is rendered from, for readers and tools that would rather have the source than the page.

This is the lookup for the DKE Library. §1 to §25 cover the reasoning modules, taking the published ones one by one: the kinds each declares, the fields you write, the fields it derives for you, and the queries you can call. §28 lists the public names of dkepy, the package that writes your facts, and §29 places dkekey. The Library Tutorial teaches how to use them; this page is what you consult once you are using them.

Module semantics in general — how import resolves, how namespacing works, what re-submitting does — are in the Python Reference §9. This page covers what the individual modules contain.


1. What a module declares

Up to five kinds of declaration, all of which become part of your store the moment you import it:

Declaration What it is
Classes the kinds of thing the domain has, and the fields they carry
Derived fields values the engine works out from the ones you wrote, and keeps current as those change — you never write one
Standing rules conclusions that hold exactly while their condition holds over your data, and withdraw themselves when it stops
Standing constraints conditions your data is expected to meet — checkup names every subject that breaks one, and no write is refused for breaking one. Of the published modules agenda (§5), quantum (§6) and pqc (§7) declare some
Queries defs you call by name, which answer over whatever facts you have loaded

Names are scoped to the module: code.Function and finance.Function are different kinds and never read each other’s data.

Derived fields and rule conclusions carry a derived provenance rather than a source, so you can always tell a conclusion from something you were told.


2. code — reasoning about a codebase

code reasons about a codebase: import it, load facts about your own source, and ask.

Version 1.12.3. Download it at dke.langsyn.com/source/code/latest/code.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/code/1.12.3/code.dpy. A versioned URL says exactly which bytes you read and serves them until the next release, which replaces the previous version rather than keeping it — so latest is the link that goes on lasting, and the copy you have already downloaded is yours either way. A version never changes without its content changing, so the version you hold answers whether you are current without your having to diff anything.

2.1 Kinds and the fields you write

Kind Fields you write
code.Function name, module, lines, branches, loops, deprecated, test, called, returns_ignored, language, callers, callees, explained
code.Class name, module, methods, language
code.Call caller, callee, site
code.UnresolvedCall caller, site, text
code.Rationale subject, marker, text, site
code.Import importer, imported, root
code.Dependency name, version, direct (absent when transitive), pinned
code.Counterpart name, left, right, relation, left_covers, right_covers

Every query that answers “which functions” returns subjects, never display names — the same identity code.Call.caller holds and callers_of(fn) takes. So an answer from one query feeds straight into another, and name stays what it is: an optional label for reading, not an identity.

Three fields decide whether the queries work at all:

  • code.Call.caller and code.Call.callee hold the subject you recorded a Function under, not its display name. The queries compare them directly, so display names make every function look uncalled, with nothing to tell you why.
  • code.Function.test names a test that reaches the function, and untested reads its absence. Write it only where a test really does reach, or the rule falls silent everywhere instead of where it should be.
  • code.Function.called says something in this codebase calls this function, and uncalled reads its absence — the same shape as test. Record it on the CALLEE, next to the Call you record for the same edge. That looks redundant and is not: uncalled reads a field of the function, not the Call kind, so the absence is a fact about this function only when it is recorded here. Recording the Call alone is not enough — write both, or every function you recorded comes back uncalled however many callers the store holds.

code.Function.callers and code.callees are degree — how many distinct functions call this one, and how many it calls. Distinct neighbours, not call sites: a caller that calls you three times is one dependency, not three, and counting sites would rank a function by how chatty its callers happen to be.

They are recorded rather than counted for you, for the same reason called is — and unlike called, write them for every function, zero included. Those above are read by their absence, so a value everywhere silences the rule. callers is read by comparison, and a function nothing calls has a fan-in of zero, which is a fact rather than a gap. Optional and absent-tolerant: record neither and everything else answers the same, with hub simply never concluding.

code.UnresolvedCall is the opposite record: a call you saw and could not name a target for. It asserts no edge, deliberately — a missing edge is a known gap, and a guessed one is a wrong answer no query could tell from a fact. Write caller as the subject of the enclosing function (as on Call), site as the location to look at, and text as the callee exactly as it appears in the source. site is what makes an entry worth having; a subject without one is skipped. This kind is optional and absent-tolerant — see §2.5 for what it buys you.

code.Rationale is a reason the author wrote down, attached to what it explains. Every other kind here is structure — something a reader could recompute from your source. This is the part that cannot be recomputed: why the structure is the way it is.

It is stated, never inferred, for the reason Counterpart is: intent is a claim only a reader can make, and an inferred one produces facts you cannot tell from the real ones. Write subject as the subject of the Function or Class it explains, marker as which convention it was, text as what the author wrote, and site as where to read it in full. text is what makes an entry worth having; a subject without one is skipped.

Which comments count is your producer’s decision, exactly as what counts as a branch is — and the same warning applies in the same direction. A convention wide enough to sweep ordinary comments turns every line in your tree into a fact and rationale_for into noise. Choose something an author had to type on purpose. Ours records WHY: and NOTE: at the start of a comment, uppercase, and nothing else.

code.Function.explained says a reason exists, and unexplained_critical reads its absence — the same shape as test and called. Write it only where a reason really was recorded, or the rule falls silent everywhere instead of firing.

language says what a fact is about — python, cpp, rust, whatever you call it. It is optional, and a store that never records it answers every other question the same. Record it once your store holds more than one language: module is a path, so it hints at the language at best and no query can rely on it, and without language there is no way to ask about one half of a repository. It also tells two producers apart, which matters because branches is whatever your analysis counted, and two producers can both be right on different scales while complexity compares them against one threshold either way.

code.Counterpart records two functions that have to agree — an encoder and its decoder, a writer and its reader, two siblings that ought to guard their inputs alike. Nothing in a call graph says two functions are halves of one contract; that is a claim about intent, and only you know it. State the pair, name in relation what is being counted, and give the count each side handles. left_covers and right_covers count members of that one named set, which makes them comparable to each other and to nothing else.

2.2 Derived and maintained for you

Field Meaning
code.Function.complexity one path, plus one per branch and per loop — see §2.3
code.Function.hotspot complexity above 10
code.Function.oversized more than 200 lines
code.Function.untested has lines recorded, and no test was seen to reach it
code.Function.uncalled has lines recorded, and nothing was seen to call it
code.Function.hub more than 5 distinct functions call it
code.Function.critical_untested complexity above 10, and no test was seen to reach it
code.Function.unexplained_critical the same, and no reason was recorded either
code.Function.discards_result throws away at least one result it was handed
code.Class.sprawling more than 20 methods
code.Counterpart.gap / reverse_gap how far apart the two sides are, each direction as a positive number
code.Counterpart.left_stricter the left side handles more — the right accepts what the left refuses
code.Counterpart.right_stricter the same, the other way round

2.3 How to count branches and loops

hotspot fires at a complexity above 10. A fixed threshold over a number you compute yourself only means something if we agree on how it is computed, so here is the rule. Follow it and your numbers are comparable — to your own past numbers, to another producer’s, and to the threshold.

complexity is one, plus one for each decision the function makes:

Count one for Notes
each if (and each else if / elif) else counts nothing
each loop for, while, do, and each clause of a comprehension
each case arm that tests a catch-all default / case _ counts nothing
each exception handler one per except / catch clause
each short-circuit boolean and / or, && / \|\|
each conditional expression the a ? b : c / b if a else c form

else, default and a bare case _ are where control lands when none of the decisions above them fired, so they add nothing — three arms and a catch-all is three, not four.

Two scope rules decide what belongs to which function:

  • a named nested function is its own Function, with its own counts; its decisions are not charged to the function that contains it;
  • an anonymous inline function — a lambda, a closure, a block — counts toward the function containing it. It is part of that function’s text and cannot be called from anywhere else, so there is no other function for its decisions to belong to.

The list is closed. Anything not on it counts nothing, including constructs with a fair claim to being a decision. An assert is the clearest one: it is a conditional failure, and most complexity tools count it — but in some languages it is a macro or a compiled-out call that a reader working from source cannot reliably see, and a rule that some producers can follow and others cannot is the problem this rule exists to solve. A number everyone computes the same way is worth more than a number that is arguably more faithful.

If you want a different scale — a stricter threshold, a metric of your own — define your own module named code.

2.4 Queries

Call Answers
code.callers_of(fn) everything that calls fn, directly
code.callees_of(fn) everything fn calls, directly
code.impact_of(fn) what breaks if fn changes — callers, and theirs, to three hops
code.importers_of(module) every module importing module
code.uncalled() functions with no recorded caller
code.unused_dependencies() dependencies you declared that nothing imports
code.unpinned_dependencies() dependencies declared as a range, not an exact version
code.asymmetries() pairs whose two sides do not handle the same amount, and which way
code.discarded_results() functions that throw a result away, so a failure cannot reach the caller
code.critical() complicated and unreached — where a defect is likeliest to survive
code.unexplained() critical() with everything already accounted for taken out
code.rationale_for(fn) why fn is the way it is, in the author’s own words
code.functions_in(language) every function recorded as belonging to one language
code.hotspots_in(language) the complicated ones in one language
code.hubs() the most depended-on functions, most first — what breaks most if you touch it
code.dispatch_sites() every call whose target was not determined — where reading stopped

2.5 What the answers can and cannot mean

The queries are only as good as the data, and for a codebase that has consequences worth stating plainly.

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 not visible. Every edge you record that way is real; edges are missing. That matters most for uncalled(): a function reached only dynamically appears there. Read it as candidates to look at, never a list to delete.

How loose that bound is, is measurable. Record an UnresolvedCall wherever your producer sees a call and cannot name the target, and dispatch_sites() lists them: the location, and the callee as written. Read it beside uncalled() — any of those sites could be the call that a function on that list is reached by. A codebase with two dispatch sites and one with two hundred deserve very different trust in the same uncalled() answer, and this is what tells them apart. Nothing on the list is a defect by itself: a plugin table, a callback and a decorator all work exactly this way. It is a measure of how much of your call graph is decidable by reading.

Recording none of them is fine. The kind is absent-tolerant — everything else answers the same, and dispatch_sites() returns nothing.

untested inherits that bound, since reaching a function is worked out over the same graph — and reaching is not executing, because a branch may never be taken. Read it the same way.

critical() cannot tell deliberate from overlooked, and unexplained() can. Two functions can be identical in every structural respect — same complexity, neither reached by a test — and be entirely different objects: one whose author wrote down why it is shaped that way and why it is not tested, and one that says nothing. critical() returns both, because everything it reads is structure. Record Rationale and explained and unexplained() is the same list with the accounted-for taken out. A short unexplained() under a long critical() is a good result: the risk is known and written down rather than absent. Record none of it and unexplained() is simply critical() again, which is where you were.

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 a 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, so a distribution whose import name differs from its package name (Pillow is imported as PIL) will look unused.

A subject missing the field a query reads is skipped, not fatal. You are never obliged to fill every field of a kind, and one half-written subject does not take out the answer for the complete ones beside it — a Dependency with no name, a Counterpart with no relation, an UnresolvedCall with no site and a Rationale with no text are each passed over, and everything else answers as it would have. So a partial record costs you that record and nothing more.


3. identity — which of your records name the same thing

identity reasons about records that may describe one thing: two customer entries, a person in two systems, a supplier under two spellings. You record the records, and what your sources say about them — that two are the same, might be, or are not — and it works out which records name the same thing, keeps the possibilities apart from the facts, and reports where your sources disagree.

Version 1.0.2. Download it at dke.langsyn.com/source/identity/latest/identity.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/identity/1.0.2/identity.dpy.

Nothing is merged. Every statement stays a claim with the source that made it. That two records are the same is a conclusion you read, and it withdraws itself when a statement it rests on is forgotten. The records themselves never change.

3.1 Kinds and the fields you write

Kind Fields you write
identity.Record name
identity.Same a, b
identity.Possibly a, b
identity.Different a, b

A record is a subject of identity.Record, and name is what it is called for reading. The rules do not need it: every name a statement uses is a record to them, recorded or not. So a misspelt name in a statement is not refused. It becomes a record of its own, and shows up under the misspelling in same_as. identity.unrecorded() lists every name a statement uses that no record carries.

A statement is a subject of Same, Possibly or Different, and a and b hold the names of the two records it is about. The statement’s own name is yours to choose, and only keeps two statements apart. Its source is the source you write it with, so who said two records are the same is recorded with the saying:

remember(identity.Record.alice.name, "Alice Smith", "crm")
remember(identity.Record.asmith.name, "A. Smith", "billing")
remember(identity.Same.m1.a, "alice", "crm")
remember(identity.Same.m1.b, "asmith", "crm")

3.2 Derived and maintained for you

Field Meaning
identity.Different.contradicted other statements, taken together, make its two records the same
identity.Record.contested a contradicted Different statement names this record

Two relations are concluded as well. A relation is read with both record names, SameEntity.alice.asmith, and holds True or nothing:

Relation Holds when
SameEntity.x.y a chain of Same statements, each read either way round, connects x to y. A record in any chain is also the same entity as itself.
PossiblySame.x.y a Possibly statement names x and y, either way round. Possibility is not chained: two possibilities in a row do not make a third.

A relation a module concludes is not namespaced by the module: it is SameEntity, never identity.SameEntity, in any unit, whether or not that unit imports identity (Python Reference §9.6). That is what lets your own rules read it. Two consequences follow from the same rule. A path you write naming SameEntity or PossiblySame, with no class of that name in scope, writes into these relations. And a unit that declares a class named either one reads its own class by that name, so its rules cannot reach the relation.

3.3 Queries

Call Answers
identity.same_as(r) every other record that names the same thing as r
identity.possibly_same_as(r) every record a source says might name the same thing as r
identity.contested() the records a contradicted Different statement names
identity.contradictions() the Different statements other statements contradict, by the names you gave them
identity.unrecorded() every name a statement uses that no record carries — usually a misspelling

3.5 What the answers can and cannot mean

SameEntity is exactly as right as your Same statements. One wrong statement joins two entities, and everything either one reaches. The conclusion names what it rests on — why(SameEntity.x.y) answers the statements, with the conclusions between them — and forgetting the wrong one withdraws every conclusion it supported, and nothing else.

A Different statement does not undo a Same one. Where they disagree, both stand, and the disagreement is reported on the statement and on both records. Which source to believe is yours to decide; the module decides nothing.

A name is matched exactly. alice and Alice are two records, and so are a record and its misspelling. Nothing here compares spellings; a statement is what says two names are one thing. unrecorded() is the check that catches a statement naming a record you never recorded.

The relation grows with the square of an entity. An entity of n records holds n² SameEntity conclusions, one for each ordered pair. That is what makes every question about it one read, and it is the cost to keep in mind before stating that ten thousand records are one thing.


4. argument — which of your claims stand

argument reasons about claims that bear on one another: a finding and the evidence for it, a proposal and the objections to it, a diagnosis and what rules it out. You record the claims, and what your sources say about them — that one claim supports another, attacks it, or weakens it — and it works out which claims stand, which fall, and which the statements leave open.

Version 1.0.2. Download it at dke.langsyn.com/source/argument/latest/argument.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/argument/1.0.2/argument.dpy.

Nothing is decided for you. Every statement stays a claim with the source that made it. That a claim stands or falls is a conclusion you read, and it withdraws itself when a statement it rests on is forgotten.

4.1 Kinds and the fields you write

Kind Fields you write
argument.Claim text
argument.Support by, target
argument.Attack by, target
argument.Weakening by, target

A claim is a subject of argument.Claim, and text is what it says, for reading. As in identity, every name a statement uses is a claim to the rules, recorded or not, so a misspelt name becomes a claim of its own. argument.unrecorded() lists every name a statement uses that no claim carries.

A statement is a subject of Support, Attack or Weakening. It says that the claim named in by supports, attacks or weakens the claim named in target, and its source is who says so. A statement counts once both of its fields are written; one with only a target bears on nothing.

remember(argument.Claim.leak.text, "The outage was caused by a memory leak", "ops")
remember(argument.Claim.flat.text, "Memory stayed flat for the whole outage", "metrics")
remember(argument.Attack.k1.by, "flat", "metrics")
remember(argument.Attack.k1.target, "leak", "metrics")

4.2 Derived and maintained for you

Field Meaning
argument.Claim.attacked an attack names this claim as its target
argument.Claim.unattacked no attack names this claim. It stands.
argument.Claim.undefended a claim nothing attacks attacks this one. It falls.
argument.Claim.reinstated this claim is attacked, and every claim attacking it falls. It stands again.
argument.Claim.weakened a weakening names this claim as its target
argument.Attack.answered the claim making this attack falls, so it no longer counts against its target
argument.Support.contested the claim giving this support is itself attacked

argument concludes no relation, so nothing it derives lives outside its own kinds.

4.3 Queries

Call Answers
argument.supporters_of(c) every claim a source says supports c
argument.attackers_of(c) every claim a source says attacks c
argument.weakeners_of(c) every claim a source says weakens c
argument.unattacked() the claims nothing attacks, each of which stands
argument.undefended() the claims attacked by a claim nothing attacks, each of which falls
argument.reinstated() the attacked claims whose attackers have all fallen, each of which stands again
argument.weakened() the claims a weakening reaches
argument.contested_premises() the claims that support another and are themselves attacked
argument.unrecorded() every name a statement uses that no claim carries — usually a misspelling

4.4 What the answers can and cannot mean

Three answers, and silence. A claim stands when nothing attacks it, falls when a claim that stands attacks it, and stands again when every claim attacking it falls. An attacked claim that is none of these is open: no query names it. Two claims that attack each other are the simplest case, and a longer chain of attacks is the other. argument stops after these two steps on purpose, so that every claim it does name is settled by what you recorded. An open claim is one the statements do not settle this way, not one that has been found to fall.

A weakening is reported, never counted. It does not make a claim fall and it does not answer an attack. Use it for evidence against a claim that you want on record without deciding anything.

A support is not a defence. Supporting a claim does not protect it from an attack. What a support does is carry a doubt: contested_premises() names the claims that support something while being attacked themselves, which is where an argument rests on ground that is disputed.

It reads your statements, not your data. A claim here is a name and a sentence. Where two sources give a field of one subject two different values, that disagreement is already the store’s own finding (conflicts()), and you do not need this module to see it. argument is for claims that bear on each other because someone said they do.


5. agenda — what you have taken on, and what you may not do

agenda reasons about what an agent or a person has to do: what you have undertaken and by when, what you owe someone, the goals your commitments serve, and the actions you may not take. You record them, and it tells you what is overdue at a moment you name, what is coming up, what was done late, which goals are met, and where a commitment or an obligation runs into a prohibition.

Version 1.0.1. Download it at dke.langsyn.com/source/agenda/latest/agenda.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/agenda/1.0.1/agenda.dpy.

Nothing acts, and nothing is chosen for you. The store keeps no clock and sends no reminder. Every question that depends on the time takes the time as an argument: you ask what is overdue at a moment, and asking about that moment again gives the same answer. Where what you owe and what you may not do disagree, the module names the disagreement and leaves the choice to you.

5.1 Kinds and the fields you write

Kind Fields you write
agenda.Goal text
agenda.Commitment action, due, serves, done_at
agenda.Obligation action, due, owed_to, done_at
agenda.Prohibition action

A commitment is something you have undertaken to do. action says what, due is the datetime it is due, and serves names the goal it is for, if it serves one. An obligation is something you owe owed_to, and is read the same way; the difference is that you did not choose it. Either is open until done_at records when it was done.

A prohibition is an action you may not take. Write it with a validity window to say when it applies (a change freeze, a quiet period) and it applies only then. Written without one, it always applies. A prohibition covers a commitment or an obligation whose action is exactly its own.

As in identity, every name a commitment serves is a goal to the rules, recorded or not. agenda.unrecorded() lists every such name that no goal carries.

remember(agenda.Goal.launch.text, "Launch the new site", "me")
remember(agenda.Commitment.docs.action, "write the docs", "me")
remember(agenda.Commitment.docs.due, datetime("2026-10-02T17:00:00Z"), "me")
remember(agenda.Commitment.docs.serves, "launch", "me")
remember(agenda.Prohibition.freeze.action, "deploy", "ops",
         datetime("2026-10-05T00:00:00Z"), datetime("2026-10-12T00:00:00Z"))

Moving a due date. Move one with update, under the source that set it:

update(agenda.Commitment.docs.due, datetime("2026-10-06T17:00:00Z"), "me")

The date it replaces is kept, and agenda.previous_dues("Commitment.docs") reads it back. A revision replaces only what the same source said, so a date a different source gives sits beside the first instead of replacing it, and the item is then unsettled (§5.4).

5.2 Derived and maintained for you

Field Meaning
agenda.Commitment.forbidden a prohibition covers this commitment’s action. It holds while the prohibition applies, whenever the commitment is due.
agenda.Obligation.forbidden a prohibition covers this obligation’s action: what you owe and what you may not do disagree
agenda.Commitment.late this commitment was done after it was due
agenda.Obligation.late this obligation was done after it was due
agenda.Goal.pursued a commitment serves this goal
agenda.Goal.met every commitment serving this goal is done

Two standing constraints report through checkup rather than through a query. agenda.nothing_forbidden_committed names each open commitment a prohibition covers now, and agenda.nothing_forbidden_owed names each such obligation. Neither refuses a write: recording a commitment you may not keep is not an error, and the report is how you find it.

5.3 Queries

Call Answers
agenda.overdue(at) every commitment and obligation due before at and not done by then
agenda.due_within(at, span) every commitment and obligation not done by at and due from at until span after it — what is coming up
agenda.unsettled(at) every commitment and obligation whose due date or completion two sources give differently at at
agenda.forbidden(at) every commitment and obligation open at at whose action a prohibition covers at at — what you may not do at that moment, whenever it is due
agenda.blocked() every open commitment and obligation whose action a prohibition covers at the moment it is due
agenda.late() every commitment and obligation done after it was due
agenda.met() every goal whose commitments are all done
agenda.serving(g) every commitment that serves goal g
agenda.previous_dues(item) every due date item had before the one it has now, oldest first
agenda.unrecorded() every goal a commitment serves that no goal carries — usually a misspelling

at is a datetime and span a duration. A query answers commitments and obligations as Commitment.<name> and Obligation.<name>, which is also how previous_dues takes one:

import agenda

def this_week():
    now = datetime("2026-10-01T09:00:00Z")
    for item in agenda.overdue(now):
        print("overdue: " + item)
    for item in agenda.due_within(now, duration("P7D")):
        print("this week: " + item)

5.4 What the answers can and cannot mean

The moment is always yours to name. overdue(at) answers from what the store holds now about the moment at. An item you moved yesterday is read at its new date, whatever moment you ask about; the dates it had before are what previous_dues returns.

An item done after the moment was still open at it. done_at is a moment like any other, so an item finished on Wednesday counts as open when you ask about the Tuesday before. That is what makes a past moment answerable.

Unsettled is not overdue. When two sources give an item two due dates, or two completions, the item has no one place in time. overdue and due_within leave it out rather than pick one, and unsettled(at) names it, so nothing goes missing without a trace. conflicts() shows the two values.

Forbidden and blocked are two questions. forbidden(at) asks what you may not do at a moment: during a change freeze it names every open commitment to deploy, whenever each is due. blocked() asks which items cannot be done on time without doing what you may not: those due while their action is forbidden. Move a blocked item’s due date out of the prohibition’s window and it is no longer blocked, while forbidden at a moment inside the window still names it.

Forbidden is a report, not a veto. A commitment to an action a prohibition covers is still a commitment. The module concludes forbidden on it for as long as the prohibition applies, and checkup names it while it is open; what you do about it is yours. An obligation that is forbidden is the sharper case (you owe what you may not do), and it is reported the same way.

Matching is exact. A prohibition covers an action written exactly as it is: "deploy" does not cover "deploy the site". Write the action the same way in both places, or record one prohibition for each action.


6. quantum — circuits, devices, and what was computed about them

quantum has two halves that share nothing but the subject. The simulation half runs a quantum circuit as values: a state of n qubits is a list of 2ⁿ complex amplitudes, every gate is a function that takes a state and returns the next one, and nothing is written to the store unless you keep a state by name. The reasoning half takes a circuit, a device and the results computed about them as facts, and tells you which gates must stay in order, which gates a device cannot run as written and why, which ran on a stale calibration, and whether a result computed outside the store still holds.

Version 1.3.1. Download it at dke.langsyn.com/source/quantum/latest/quantum.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/quantum/1.3.1/quantum.dpy. The file states its version on its __version__ line, so a compile given no version records 1.3.1 (Python Reference §9.1).

A run has no randomness. A measurement takes its uniform value as an argument, and two generators supply a sequence of them from a seed you choose, so the same seed gives the same answers every time: quantum.lcg_next from a 31-bit seed, and quantum.chacha_uniform, ChaCha20 as RFC 8439 defines it, from a 256-bit key. Draw the seed outside the run and pass it in.

6.1 Kinds and the fields you write

Kind Fields you write
quantum.State amplitudes
quantum.Gate op, a, b, step, circuit
quantum.Qubit label
quantum.Coupler a, b
quantum.Op qubits, diagonal
quantum.Device qubits, snapshot_at, digest
quantum.PhysicalQubit device, index, t1, t2, readout_error, measured_at
quantum.Native device, op, a, b, error, length, measured_at
quantum.Circuit device, digest
quantum.Result circuit, digest, source, source_digest, device, device_digest, producer, settings, seed, outcome

A gate is one operation of a circuit: op names the operation, a the qubit it acts on and b the second qubit of a two-qubit gate, and step its position. circuit names the circuit it belongs to; the gates that name none are one circuit together. Qubits are named, and a compiled circuit names them by number, as text. A coupler is a pair of qubits your chip can act on together, in either order. Qubit is for your own reading; no rule reads it.

An op is an operation’s properties: how many qubits it takes, and whether it is diagonal. The module records them for every operation it simulates, and for id and rzz, when it is imported; record another the same way.

A device is facts too: its physical qubits, and the operations it runs natively on each qubit and each ordered pair, with each one’s measured error, its duration and when it was measured. A circuit names the device it was compiled for and the digest of the text it was written from. A result is something computed outside the store, such as a compilation, a comparison or the bits a run measured: the circuit it is about, the circuit it came from and the device it was computed for, each with the digest its producer read, and the producer, its settings, its seed and the outcome.

You do not have to write device facts or results by hand. dkepy’s dke.quantum.device writes a device from a calibration snapshot, and dke.quantum.provenance writes a result and the digests it names:

dke-quantum-device ./torino --max-bytes 40000 > torino.dpy

State is written by quantum.save and read by quantum.load (§6.3), never by hand.

6.2 Derived and maintained for you

Field Meaning
quantum.Gate.diagonal the gate’s operation is diagonal
quantum.Gate.track the circuit the gate is ordered within: its own, or the empty track for gates that name none
quantum.Gate.uncoupled a two-qubit gate on no device whose qubits no coupler joins
quantum.Gate.device the device the gate’s circuit was compiled for
quantum.Gate.width how many qubits the gate’s operation takes
quantum.Gate.op_on_device the gate’s device runs its operation somewhere
quantum.Gate.first_on_device / second_on_device the gate’s device has its first / second qubit
quantum.Gate.off_device the gate names a qubit its device does not have
quantum.Gate.not_native the gate’s device runs its operation on no qubit at all
quantum.Gate.not_here the device runs the operation and has the qubits, but not that operation on these
quantum.Gate.reversed the device runs the operation on this pair the other way round only
quantum.Gate.illegal one of the four reasons above holds
quantum.Result.circuit_current / source_current the circuit the result is about / came from has, in the store, the digest the result read
quantum.Result.device_current the device it was computed for has the digest it read
quantum.Result.holds the result’s outcome, while everything it names is current
quantum.Result.unrecorded the result omits what it needs to be checked: a circuit, an outcome, or a digest of something it names
quantum.Result.circuit_unknown / circuit_changed the circuit it is about has no digest in the store / a different one
quantum.Result.source_unknown / source_changed likewise for the circuit it came from
quantum.Result.device_unknown the device it names has no digest in the store
quantum.Result.recalibrated that device’s digest differs from the one it read
quantum.Result.stopped one of the reasons above holds

A result that names no source, or no device, counts as current on that part.

Two standing constraints report through checkup. quantum.every_pair_coupled names each uncoupled gate, and quantum.every_gate_legal each gate its device cannot run as written. Neither refuses a write.

6.3 Queries

Simulation reads and writes no fact. psi is a state, q a qubit’s index (qubit 0 is the lowest bit of an amplitude’s index), and theta an angle in radians:

Call Answers
quantum.zero(n) the all-zero state on n qubits
quantum.phase(theta) e to the i·theta
quantum.h(psi, q) psi after a Hadamard gate on q
quantum.x(psi, q) after an X gate
quantum.y(psi, q) after a Y gate
quantum.z(psi, q) after a Z gate
quantum.s(psi, q) after an S gate
quantum.t(psi, q) after a T gate
quantum.sx(psi, q) after the square root of X
quantum.rx(psi, q, theta) after a rotation about X
quantum.ry(psi, q, theta) after a rotation about Y
quantum.rz(psi, q, theta) after a rotation about Z
quantum.cnot(psi, c, tg) after a controlled NOT, control c, target tg
quantum.cz(psi, a, b) after a controlled Z
quantum.ecr(psi, a, b) after an echoed cross-resonance gate, Qiskit’s matrix
quantum.swap(psi, a, b) with qubits a and b exchanged
quantum.ccx(psi, a, b, tg) after a Toffoli gate: tg flips when a and b are both 1
quantum.ccz(psi, a, b, c) after a gate that negates the amplitudes whose three qubits are all 1
quantum.cswap(psi, c, a, b) after a Fredkin gate: a and b exchanged when c is 1
quantum.probabilities(psi) the probability of each basis state, in index order
quantum.weights(psi, q) the probability of qubit q being 0, and of its being 1
quantum.collapsed(psi, q, bit, norm) the amplitudes whose qubit q is bit, times norm, and zero elsewhere
quantum.measure(psi, q, u) the outcome, 0 when u is below the probability of 0, and the state it leaves
quantum.unitary(name, theta) the matrix of gate name
quantum.adjoint(m) the conjugate transpose of a matrix
quantum.lcg_next(state) the next state of a seeded generator, Park and Miller’s minimal standard
quantum.lcg_uniform(state) the generator state as a value in (0, 1)
quantum.chacha_quarter(w, a, b, c, d) the list w after ChaCha20’s quarter round on its words a, b, c and d, changed in place
quantum.chacha_block(key, counter, nonce) the 16 words of one ChaCha20 block, for a key of 8 words, a block counter and a nonce of 3 words, each from 0 to 0xFFFFFFFF
quantum.chacha_unit(block, i) value i of a block, from its words 2·i and 2·i + 1, as an exact 53-bit value in [0, 1)
quantum.chacha_uniform(key, n) the value at position n of the key’s stream, from 0 to 2³⁵ − 1: value n % 8 of block n // 8, with a nonce of zero

A Clifford circuit can run on a stabilizer tableau instead, whose size grows with n² rather than 2ⁿ:

Call Answers
quantum.tableau(n) the tableau of the all-zero state on n qubits
quantum.tab_h(tab, a) the tableau after a Hadamard gate
quantum.tab_s(tab, a) after an S gate
quantum.tab_cnot(tab, a, b) after a controlled NOT
quantum.tab_gate(tab, name, q) after any Clifford gate the module names, up to a global phase
quantum.tab_measure(tab, a, u) the outcome of measuring qubit a, and the tableau it leaves
quantum.pauli_phase(x1, z1, x2, z2) the power of i one Pauli multiplied into another contributes
quantum.row_product(hrow, irow, n) one tableau row multiplied by another

Two circuits can be compared for equality up to a global phase, which is what a compiler must preserve. A step is a gate’s name, its qubits and an angle:

Call Answers
quantum.clifford(n, steps) the tableau of a Clifford circuit; two are equal exactly when the circuits are
quantum.overlap(n, steps, back, j) amplitude j after steps and then back undone, run on basis state j; equal circuits answer one phase for every j

A circuit of x, cnot, ccx, swap and cswap, with the signs z, cz and ccz, sends every basis state to one basis state under a sign of +1 or -1, so it can be checked on every input with integers alone, at any width. Qubit q of an input is bit q of the input’s index. Two circuits are equal when, on every input, they give the same output bits under one sign shared by every input: a sign that differs on some inputs only is not equal, and one that differs on all of them is. A circuit meets a specification when, on every input of its input qubits ins, those qubits are unchanged, the output qubits outs hold the value want gives for it (bit j of a value on qubit outs[j]), every other qubit is 0 again, and the sign is the one signs gives (1 for -1; an empty list is +1 for every input). The circuit starts with every qubit but ins at 0, and want is one value for each of the 2**len(ins) inputs, which the caller computes with a function of its own. Both answer (holds, inputs, first, why): the number of inputs checked, the first input that fails or -1, and the reason, which is "output" or "sign" for two circuits and "input", "ancilla", "output" or "sign" for a specification, the first that holds at that input.

A circuit may also measure. ("mx", [q], 0.0) measures qubit q in the X basis and leaves it 0. Its outcome is not drawn from anything: it is one bit of the case being checked, as a seed is an argument. Under outcome 1 it negates the sign on every input where q held 1, and under outcome 0 it changes no sign. A step named x_if, cnot_if, ccx_if, z_if, cz_if, ccz_if, swap_if or cswap_if is the gate it names, run only where an outcome was 1: it takes the measured qubit first and then the gate’s own qubits in the gate’s order, so ("cz_if", [t, a, b], 0.0) is cz on a and b where the latest mx of t before it gave 1. A step whose first qubit no earlier mx measured is refused. The outcomes are numbered by the mx steps in step order, outcome j being the j-th. rev_meets holds a circuit over every assignment of them, so a circuit of M measurements is checked on 2M times as many cases, and a case i is input i mod 2len(ins) under the outcomes i div 2len(ins); the number it answers is the count of cases, and a check of more than 262 cases is refused. With it a qubit that holds the AND of two others, computed by a Toffoli gate, is uncomputed without one: measure it, and where the outcome is 1 apply a cz to the two. rev_same takes no circuit that measures, since it looks at every input of every qubit, and a measured qubit that starts at 1 is reset and not undone; a circuit that measures is held to a specification, where every qubit but the inputs starts at 0.

Call Answers
quantum.rev_same(n, a, b) whether reversible circuits a and b on n qubits are equal, on all 2**n inputs; refuses a circuit that measures
quantum.rev_meets(n, steps, ins, outs, want, signs) whether a reversible circuit, which may measure, meets the specification, on all 2**len(ins) inputs under every outcome
quantum.rev_code(n, steps) the steps of a reversible circuit as (operation, a, b, c), refusing a gate that makes a superposition, a qubit outside the circuit, a qubit named twice in one step and a controlled step whose qubit no mx measured
quantum.rev_apply(w, code) runs what rev_code read on wires w, 64 inputs to an int, changing them in place; answers the sign of the 64 inputs, every outcome 0
quantum.rev_apply_under(w, code, outcomes) rev_apply with outcome j of the circuit’s mx steps the bit j of outcomes
quantum.rev_measures(code) the number of mx steps in what rev_code read
quantum.lowest_lane(m) the lowest bit set in a nonzero 64-bit int

A state can be kept in the store by a name you choose, so a circuit longer than one run is a series of calls that pass nothing but steps. quantum.save(name, psi) keeps one, quantum.drop(name) forgets it, quantum.keep(name, b) keeps bytes quantum.packed answered, and quantum.collapse(name, q, bit, norm) collapses a kept state; each answers nothing.

Call Answers
quantum.load(name) the state kept under name, bit for bit, or an empty list
quantum.packed(name) the bytes kept under name, or an empty blob
quantum.advance(name, steps, u) runs steps on the kept state and keeps the result; answers the generator state it ended with and the bits it measured
quantum.advance_chacha(name, steps, key, n) the same, with its values from chacha_uniform(key, n) onward; answers the next position and the bits it measured
quantum.weigh(name, q) weights of the kept state

Over the facts you have loaded:

Call Answers
quantum.after(g) every gate that must run after gate g
quantum.commuting(g) every later gate that shares a qubit with g and may run before it
quantum.uncoupled_gates() every two-qubit gate the chip cannot run as written
quantum.illegal_gates(c) every gate of circuit c its device cannot run as written, as "<gate> <reason>", once for each reason that holds
quantum.stale_gates(c, at, max_age) every gate of circuit c whose native operation was measured after at, more than max_age before it, or never
quantum.results_not_holding() every result that does not hold, as "<result> <reason>", once for each reason that holds
quantum.results_of(c) every result that read circuit c, as the circuit it is about or the one it came from
quantum.produced(r) what result r records, one "<field> <value>" line per field
import quantum

def bell():
    psi = quantum.zero(2)
    psi = quantum.h(psi, 0)
    psi = quantum.cnot(psi, 0, 1)
    print(quantum.probabilities(psi))

6.4 What the answers can and cannot mean

Equal is up to a global phase. Two circuits that differ only by a global phase are the same physical operation, and the comparisons treat them as one. overlap asks for one phase across every basis state, so a missing rz or one of the wrong sign does not pass.

A device is judged by its own facts. A gate whose circuit names a device is judged by that device’s qubits and native operations, and couplers say nothing about it. A circuit naming a device the store holds no facts for has every gate refused, which is also how a misspelt device name shows itself.

Only the 256-bit generator can stand behind a secret. lcg_next has fewer than 2³¹ seeds, so anyone can try every one of them; it is for runs you want to replay. A key for chacha_uniform is 8 words from 0 to 0xFFFFFFFF, which list(struct.unpack("<8I", secrets.token_bytes(32))) draws in Python. The key is an argument like any other: it travels to the service in the program you send, and the service sees it. A secret that is to stay on your own machine is drawn on your own machine.

The moment is yours to name. The store keeps no clock, so stale_gates takes the moment and the oldest calibration you accept, and asking about that moment again gives the same answer.

A result holds only while what it read is current. Change a circuit’s digest, or load a recalibrated device, and every result that read the old one stops holding, with a reason naming what moved. A result’s producer, settings and seed are kept as its recipe and are not judged: no rule can know whether a producer is still current. The store compares digests and never computes one, so a digest is the writer’s to keep true.


7. pqc — what to move off vulnerable cryptography first

pqc reasons about migrating off cryptography a quantum computer breaks. You record where your systems use cryptography, how each algorithm stands, how long what each use protects must stay protected, how long each system takes to migrate, the year you plan against and the deadlines you are held to. It tells you which uses to start on first, which are already exposed, which miss a deadline, and what your inventory does not yet say.

Version 1.0.2. Download it at dke.langsyn.com/source/pqc/latest/pqc.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/pqc/1.0.2/pqc.dpy.

The reasoning is Mosca’s inequality. A use of a vulnerable algorithm is a problem when the time what it protects must stay protected, plus the time it takes to migrate, reaches past the time a quantum computer can break it. Data recorded now can be decrypted then, so the year to finish is earlier than the horizon by the data’s shelf life.

7.1 Kinds and the fields you write

Kind Fields you write
pqc.Horizon year
pqc.Algorithm quantum, strength
pqc.Data shelf_life
pqc.System migration
pqc.Deadline year, strength
pqc.Use system, algorithm, protects, migrated; never horizon, shelf_life or migration, which the rules copy onto it

Every year is a calendar year, written as an int, and every boundary is the first year WITHOUT something. The horizon is the first year you assume a quantum computer able to break the vulnerable algorithms exists. An algorithm’s quantum is "vulnerable" when a quantum computer breaks it, and its strength is its classical security strength in bits. Data’s shelf_life is how many years after it is protected it must stay protected: for a signature, how long it must be trusted. A system’s migration is how many years moving it off a vulnerable algorithm takes. A deadline is the first year a vulnerable algorithm of at most strength bits may not be used. A use is one system using one algorithm to protect one kind of data, and migrated is the first year it no longer relies on the algorithm, planned or done; a use with none has no plan.

dkepy’s dke.pqc.cbom writes the algorithms and uses that a cryptographic bill of materials states. The rest are your own facts, recorded beside them:

dke-pqc-cbom ./cbom.json --max-bytes 40000 > cbom.dpy

7.2 Derived and maintained for you

Field Meaning
pqc.Use.vulnerable the use’s algorithm is vulnerable
pqc.Use.horizon / shelf_life the horizon, and the shelf life of what it protects, copied onto a vulnerable use
pqc.Use.migration its system’s migration time, copied onto it
pqc.Use.migrate_by the first year the use must no longer rely on its algorithm: the horizon less the shelf life
pqc.Use.start_by the last year to start moving it: migrate_by less the migration time
pqc.Use.late its plan finishes after migrate_by
pqc.Use.unplanned a vulnerable use with no plan
pqc.Use.deadline / deadline_name the earliest deadline covering its algorithm, under any value of a deadline’s year, and which deadline that is
pqc.Use.misses its plan finishes after that deadline

Two standing constraints report through checkup. pqc.no_late_plan names each late use and pqc.no_missed_deadline each use that misses its deadline. Neither refuses a write.

7.3 Queries

Each answers use names.

Call Answers
pqc.priority() every vulnerable use with a start year, the earliest first
pqc.exposed() every late use, and every unplanned use whose migrate-by year is known: what it protects from that year on can be recorded now and read at the horizon
pqc.noncompliant() every use that misses its deadline, and every unplanned use a deadline covers
pqc.remaining(at) every vulnerable use still relying on its algorithm in year at
pqc.unclassified() every use with no algorithm, or naming one whose standing is not recorded
pqc.unassessed() every vulnerable use with no start year: a shelf life, a migration time or a horizon is missing
pqc.unsettled() every use for which the store holds more than one value of its algorithm, its plan, or the horizon, shelf life or migration time it is reasoned about with
import pqc

def plan():
    for u in pqc.priority():
        print("start: " + u)
    for u in pqc.noncompliant():
        print("misses a deadline: " + u)

7.4 What the answers can and cannot mean

The year is yours to name. The store keeps no clock. Every conclusion is a year, and remaining takes the year you ask about, so asking about that year again gives the same answer.

Disagreement is kept, not resolved. When a scanner and a system’s owner record different algorithms, or two policies different shelf lives, every value is reasoned about. A use is late, exposed or remaining if any value on record makes it so, and priority places it by the earliest year it must start. A use’s deadline is likewise the earliest year of any value of any deadline that covers its algorithm: one source’s year, never two added. unsettled() names every use where a value of its own, or of the horizon, shelf life or migration time it is reasoned about with, disagrees, so you can settle it. A deadline’s year is not among them; why on the use’s deadline names the year that placed it, beside every year it was chosen from.

An unclassified use is not a safe one. The rules conclude nothing about a use whose algorithm’s standing is not recorded, so it appears in no answer but unclassified(). An inventory with one in it is not yet a safe inventory.


8. harmony — four-part writing, and the arithmetic of music

harmony marks four-part writing as a harmony teacher marks it. You record an exercise’s voices and its notes. It names every consecutive fifth and octave between any two voices, every crossed voice, every note outside its voice’s range or outside the key, and the reading of every chord from the intervals above its bass. Over a melody’s rhythm it checks each bar’s total, the notes held across a bar line, and syncopation. Its value scripts calculate equal-tempered frequencies, intervals in cents, set classes, twelve-tone matrices and Euclidean rhythms.

Version 1.0.1. Download it at dke.langsyn.com/source/harmony/latest/harmony.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/harmony/1.0.1/harmony.dpy.

A pitch is a MIDI note number: 60 is middle C, and each step is a semitone, so 72 is the C an octave above it. A beat is one chord, and an exercise’s chords are numbered 1, 2, 3 and so on, in order. A voice’s place orders the voices from the bottom, 1 for the bass.

8.1 Kinds and the fields you write

Kind Fields you write
harmony.Voice place, low, high
harmony.Key tonic, mode
harmony.Degree key, pc
harmony.Note voice, beat, pitch, key; never place, p1, p2, p3, spacing, range, chromatic, chord or seventh, which the rules conclude
harmony.Fifths none: a relation the rules conclude between two notes
harmony.Octaves none: a relation the rules conclude between two notes
harmony.Crossing none: a relation the rules conclude between two notes
harmony.Bar start, length, pulse; never filled, miscounted, tones, claims or disputed
harmony.Tone onset, duration; never barstart, barend, pulse, across or syncopated

A voice’s low and high are its range, as MIDI note numbers. A key’s tonic is a pitch class, 0 for C up to 11 for B, and its mode is "major" or "minor"; a degree is one pitch class that belongs to it. A note names its voice, its beat, its pitch and, to be checked against one, its key. A bar starts at start and lasts length, counted from the start of the piece in a unit you choose, such as a sixteenth, and pulse is the length of its beat; bars of different lengths are how a change of metre is written. A tone is a note of a melody, for its rhythm, and a rest is a tone too.

Five scripts write these facts for you, under the source harmony:

  • harmony.satb() writes the four voices of a chorale, bass to soprano, with the ranges bass E2 to C4, tenor C3 to G4, alto G3 to D5 and soprano C4 to G5. Textbooks differ by a note or two; update a range to use another.
  • harmony.key(name, tonic, mode) writes a key and its degrees. A minor key takes its raised sixth and seventh as well as its own degrees.
  • harmony.chord(beat, key, bass, tenor, alto, soprano) writes one chord’s four notes, named for their voice and beat: bass1 to soprano1 for beat 1. Writing a beat again corrects it.
  • harmony.bars(count, length, pulse) writes count bars of one metre, m1 onward, and harmony.melody(durations) a melody’s tones end to end, t1 onward. Writing either again replaces what an earlier call wrote: it forgets the bars or tones it had numbered past the new end.

8.2 Derived and maintained for you

Field Meaning
harmony.Note.place the place of the note’s voice
harmony.Note.p1 / p2 the pitch the voices at places 1 and 2 sound on the note’s beat
harmony.Note.p3 the pitch the voice at place 3 sounds on the note’s beat
harmony.Note.pc the note’s pitch class, pitch % 12
harmony.Note.nextbeat beat + 1, which is how a rule reaches the next chord
harmony.Note.i1 / i2 the note’s interval above places 1 and 2 within an octave: 7 is a fifth, and 0 an octave or a unison
harmony.Note.i3 the same above place 3
harmony.Note.d2 / d3 the note’s full distance in semitones above places 2 and 3
harmony.Note.spacing an upper voice more than an octave above the voice below it; the bass may lie further from the tenor
harmony.Note.range "low" or "high", for a note outside its voice’s range
harmony.Note.chromatic a note whose pitch class is no degree of its key
harmony.Note.chord / seventh on a chord’s bass note, every reading of the chord as a triad, and as a dominant seventh
harmony.Bar.end start + length
harmony.Bar.tones / claims how many tones starting in the bar have a duration, and how many claims of duration they hold
harmony.Bar.disputed more claims than tones: a tone is bound to the bar twice, and no filled is concluded
harmony.Bar.filled / miscounted how much the tones starting in the bar fill, while it is not disputed, and whether that differs from its length
harmony.Tone.barstart / barend the start and end of the bar the tone starts in
harmony.Tone.pulse the beat length of that bar
harmony.Tone.ends onset + duration
harmony.Tone.offbeat / nextpulse how far past a beat the tone starts, and where the next beat falls
harmony.Tone.across / syncopated the tone lasts past the end of its bar; it starts off the beat and lasts past the next one

Three relations are concluded between two notes on one beat. harmony.Fifths.a.m holds when the voice of note a and the lower voice of note m are a fifth apart on that beat and again on the next, and both move: consecutive fifths, in contrary motion too, with a compound interval counted as its simple form. harmony.Octaves is the same for octaves and unisons, and harmony.Crossing.a.m holds when a’s voice sounds below m’s, which is placed under it.

A chord is read from the intervals above its bass. Major, minor and diminished triads are read in root position and in first and second inversion, a major or minor triad with its fifth left out, and a dominant seventh in root position, with its fifth left out, and in its three inversions. These are the shapes figured bass names: a first inversion is a 6, a second a 6/4, and a seventh’s inversions are a 6/5, a 4/3 and a 4/2.

8.3 Queries

Call Answers
harmony.faults() every fault the rules conclude, one line each, sorted
harmony.readings(n) every reading of the chord whose bass is note n
harmony.rhythm_faults() every bar whose tones do not fill it exactly, every disputed bar, every tone held across a bar line, and every syncopated tone, one line each, sorted
harmony.pairs(kind) the pairs of notes, upper then lower, that "fifths", "octaves" or "crossing" holds between
harmony.voice_of(n) the voice of note n; where sources disagree, every voice on record, joined by or
harmony.beat_of(n) the beat of note n; where sources disagree, the earliest
harmony.filled_of(b) how much of bar b its tones fill; where sources disagree, the least; refuses a disputed bar
harmony.length_of(b) the length of bar b; where sources disagree, the least
harmony.unsettled() every voice, note, bar and tone with a field you write that sources give more than one value, as note tenor1, sorted
harmony.frequency(midi, a4) a note’s frequency in equal temperament, A4 at a4 hertz, 440.0 if not given
harmony.cents(ratio) an interval’s size in cents, 1200 to the octave
harmony.beating(f1, h1, f2, h2) how many times a second harmonic h1 of a tone at f1 hertz beats against harmonic h2 of one at f2
harmony.pcset(pcs) the distinct pitch classes, sorted
harmony.prime_form(pcs, right) the set class’s prime form, packed from the right, or with right=False packed to the left
harmony.rotations(s) each rotation of a sorted set, transposed to start on 0
harmony.packed_key(r, right) what two orderings are compared by under either packing
harmony.most_packed(s, right) the most packed rotation of a set
harmony.interval_vector(pcs) how many times each interval class, 1 to 6, occurs in the set
harmony.matrix(row) a twelve-tone row’s matrix, the row on its first line
harmony.all_interval(row) whether the row’s successive intervals are all different
harmony.euclid(k, n) k onsets spread as evenly as they go over n steps, by Bjorklund’s algorithm, as x and .
import harmony

def mark():
    for f in harmony.faults():
        print(f)
    print(harmony.readings("bass1"))

8.4 What the answers can and cannot mean

A mark is the rule, not a verdict. The rules are the ones a teacher of four-part writing marks, written exactly. A note outside the key is marked though it may be a deliberate chromatic note, such as the raised fourth of a secondary dominant, and consecutive fifths are marked whatever the style of the piece. What the module does not check is listed rather than implied: hidden fifths and octaves, doubling, the resolution of a leading note or a seventh, melodic intervals, and more than four voices.

A chord takes every reading inside it. A rule reads exactly the intervals it names, so a dominant seventh is read as a major triad as well, a chord with both a major and a minor third is read as both, and a chord of no shape the rules know, such as an augmented triad, has no reading.

A correction replaces; a disagreement is kept. chord() writes under the source harmony, so writing a beat again replaces its notes and withdraws every mark they no longer support. A note written under another source is held beside the first rather than replacing it. Every mark it reaches is drawn from both values, a line of faults() names it with each voice and beat it has, and harmony.unsettled() names it. The one exception is a mark that asks for something to be missing: a note is outside the key only when no value of its pitch is in the key, and a chord is read with its fifth omitted only when no value supplies the fifth. Only filled_of refuses, and only a disputed bar.

A bar’s fill is not drawn from a tone bound to it twice. How much a bar’s tones fill is a sum, and a sum adds every value it reads: a tone whose duration a second source writes, even the same duration, would be counted twice, and the bar would read as overfilled where every source fills it. So while a tone is bound to its bar twice, the bar is disputed: no filled and no miscounted is concluded for it, rhythm_faults() names it as bar m1: disputed, filled_of refuses it, and why_not on its filled names the dispute. A tone with no duration fills nothing and hides no dispute. Forgetting the claim that is not to stand settles it. A tone whose onset two sources disagree on starts in each bar a value puts it in, so each of those fills is one source’s.

The calculations are exact. Each value script computes what the same formula computes in CPython, to the last bit. Prime forms are given under both published criteria, which disagree for six set classes.


9. physics — units and dimensions, and classical motion

physics checks quantities as an engineer checks them. You record each quantity as a value in a unit, and the units with their dimensions. It concludes every quantity’s value in SI, and names a quantity handed over in a unit other than the one its reader expects, as a pound-force second where a newton second is expected. It judges whether an equation written as a product of quantities is dimensionally consistent, and says what is left over when it is not. It adds the resistance of resistors joined in series or in parallel. Its value scripts calculate a pendulum’s period by integration and exactly, an orbit under three integrators, the heat equation, a linear system by Gaussian elimination, the current through a bridge circuit’s meter, and the Ising model of a magnet.

Version 1.0.3. Download it at dke.langsyn.com/source/physics/latest/physics.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/physics/1.0.3/physics.dpy.

A unit has a dimension and a factor. The dimension is its powers of mass, length, time, electric current and temperature: a newton is mass 1, length 1 and time -2. The factor is what one of the unit is in the SI unit of that dimension. The module holds no units until you ask: physics.si_units() writes a list of common ones, each with the source of its value, and a unit not in it is written with physics.unit.

9.1 Kinds and the fields you write

Kind Fields you write
physics.Unit mass, length, time, current, temperature, factor
physics.Quantity value, unit, expects; never factor, mass, length, time, current, temperature, efactor, emass, elength, etime, ecurrent, etemperature, unknown, unexpected or handoff, which the rules conclude
physics.Factor equation, quantity, power; never mass, length, time, current, temperature or unknown, which the rules conclude
physics.Equation none: its sums mass, length, time, current and temperature, and consistent and unbalanced, are concluded from its factors, and so are mass_factors, length_factors, time_factors, current_factors, temperature_factors, mass_claims, length_claims, time_claims, current_claims, temperature_claims, mass_disputed, length_disputed, time_disputed, current_disputed and temperature_disputed
physics.Resistor ohms, group
physics.Group joined; never ohms, siemens, ohms_resistors, ohms_claims, ohms_disputed, siemens_resistors, siemens_claims or siemens_disputed

A unit is named by letters, digits and underscores, such as "N", "lbf_s" or "m_per_s2". A unit whose zero is not SI’s, such as a degree Celsius, is not a factor and is not written. A quantity is a value in a unit, by a name you choose; its expects is the unit its reader takes it in, where it is handed from one system to another. A factor is one quantity in an equation written 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. A pure number such as the 1/2 of E = m v²/2 has no dimension and needs no factor, and a square root is written by doubling every power in the equation. A resistor names its resistance in ohms and the group it is joined in, "series" or "parallel".

Eight scripts write these facts for you:

  • physics.si_units() writes the list of units, each under the source that gives its value: the SI Brochure (9th edition, 2019) for the SI units and those defined by them, and NIST Special Publication 811 for the inch, foot, mile, miles per hour, pound, pound-force, pound-force second, foot-pound, pound per square inch, atmosphere and calorie. A pound is 0.45359237 kg and a pound-force a pound under standard gravity, 9.80665 m/s².
  • physics.unit(name, mass, length, time, current, temperature, factor, source) writes one unit under the source you give.
  • physics.quantity(name, value, unit) writes a quantity, and physics.expect(name, unit) the unit its reader takes it in. Writing a name again corrects it.
  • physics.factor(equation, quantity, power) writes one factor of an equation.
  • physics.resistor(name, ohms, group) and physics.group(name, joined) write a resistor and the group it is joined in.

9.2 Derived and maintained for you

Field Meaning
physics.Quantity.factor the factor of the quantity’s unit
physics.Quantity.mass / length the powers of mass and length in the quantity’s unit
physics.Quantity.time / current the powers of time and current in the quantity’s unit
physics.Quantity.temperature the power of temperature in the quantity’s unit
physics.Quantity.si value * factor: the quantity’s value in SI
physics.Quantity.efactor the factor of the unit the quantity expects
physics.Quantity.emass / elength the powers of mass and length in the unit the quantity expects
physics.Quantity.etime / ecurrent the powers of time and current in the unit the quantity expects
physics.Quantity.etemperature the power of temperature in the unit the quantity expects
physics.Quantity.converted value * (factor / efactor): the value in the unit its reader expects, the ratio of the factors taken first so that a value expected in its own unit comes back as it was; a real wherever that value is one, for factors from 10^-150 to 10^150
physics.Quantity.unknown the quantity’s unit is not in the list
physics.Quantity.unexpected the unit the quantity expects is not in the list
physics.Quantity.handoff "convert" when the expected unit has the same dimension and another unit, "incompatible" when it has another dimension
physics.Factor.mass / length the powers of mass and length in its quantity’s unit
physics.Factor.time / current the powers of time and current in its quantity’s unit
physics.Factor.temperature the power of temperature in its quantity’s unit
physics.Factor.smass / slength the factor’s share of mass and of length: its power times that power
physics.Factor.stime / scurrent the factor’s share of time and of current
physics.Factor.stemperature the factor’s share of temperature
physics.Factor.unknown the factor’s quantity has no unit in the list
physics.Equation.mass_factors / length_factors how many of its factors hold a share of mass, and of length
physics.Equation.time_factors / current_factors how many hold a share of time, and of current
physics.Equation.temperature_factors how many hold a share of temperature
physics.Equation.mass_claims / length_claims how many claims of their shares of mass, and of length, those factors hold
physics.Equation.time_claims / current_claims the same of time, and of current
physics.Equation.temperature_claims the same of temperature
physics.Equation.mass_disputed / length_disputed more claims than factors: a factor is bound to the equation twice by its share of mass, or of length, and that sum is not concluded
physics.Equation.time_disputed / current_disputed the same of time, and of current
physics.Equation.temperature_disputed the same of temperature
physics.Equation.mass / length the sum of its factors’ shares of mass, and of length, while that dimension is not disputed
physics.Equation.time / current the sum of its factors’ shares of time, and of current, while that dimension is not disputed
physics.Equation.temperature the sum of its factors’ shares of temperature, while it is not disputed; all five sums are 0 when the equation is consistent
physics.Equation.consistent all five sums are 0 and no factor is unknown
physics.Equation.unbalanced a sum is not 0 and no factor is unknown
physics.Resistor.siemens 1.0 / ohms: the resistor’s conductance
physics.Group.ohms_resistors / ohms_claims for a series group, how many resistors carry a resistance into its sum, and how many claims of it they hold
physics.Group.siemens_resistors / siemens_claims for a parallel group, how many carry a conductance into its sum, and how many claims of it they hold
physics.Group.ohms_disputed / siemens_disputed more claims than resistors: a resistor is bound to the group twice, and that sum is not concluded
physics.Group.ohms for a series group, the sum of its resistors’ resistances, while it is not disputed
physics.Group.siemens for a parallel group, the sum of its resistors’ conductances, while it is not disputed
physics.Group.parallel 1.0 / siemens: a parallel group’s resistance

A hand-over is a quantity written in one unit that its reader takes in another. If both have one dimension, the value is converted and converted reads it in the expected unit. If they have different dimensions, no conversion makes it the quantity the reader wants: a mass in kilograms where a force in newtons is expected is marked "incompatible". A quantity given in the unit it expects is no hand-over and is marked nothing.

9.3 Queries

Call Answers
physics.checks() every unknown unit, unbalanced equation, hand-over, disputed equation and disputed group the rules conclude, one line each, sorted
physics.si(name) the quantity’s value in SI; where sources disagree, the least; 0.0 when there is none
physics.converted(name) the quantity’s value in the unit its reader expects; where sources disagree, the least; 0.0 when there is none
physics.dimension(name) what is left over of an equation’s dimensions, as L1 T-1 for E = m v; consistent when nothing is, unknown unit when a factor’s unit is not recorded, no factors when it has none and disputed when a dimension’s sum is
physics.resistance(name) a group’s resistance in ohms, series or parallel; 0.0 when there is none; refuses a disputed group
physics.disputed_equations() every equation a factor is bound to twice by its share of some dimension, sorted
physics.disputed_groups() every group a resistor is bound to twice, sorted
physics.unsettled() every unit, quantity, factor, group and resistor with a field you write that sources give two different values, as quantity x, sorted
physics.small_period(length, g) a pendulum’s period through a small angle, 2π√(L/g), the same for every amplitude
physics.agm(a, b) the arithmetic–geometric mean of two numbers not below 0: their mean and geometric mean taken again and again until the two agree
physics.period(theta0, length, g) the exact period of a pendulum released at rest from theta0 radians, from the arithmetic–geometric mean
physics.pendulum(theta0, length, g, dt) the same period by integrating the equation of motion with the fourth-order Runge–Kutta method at steps of dt seconds
physics.orbit(e, method, periods, steps) the relative error in energy and in angular momentum, and the distance from the start, after a Kepler orbit of eccentricity e stepped by "euler", "verlet" or "rk4"
physics.heat(cells, r, steps) the temperature at the middle of a rod after steps steps of forward differences in time and centred differences in space
physics.heat_exact(time) the same temperature at time from Fourier’s series
physics.solve(a, b) the solution of a · x = b by Gaussian elimination with partial pivoting; [] when the system is singular
physics.bridge(volts, r1, r2, r3, r4, rg) the current in amperes through the meter of a Wheatstone bridge; 0 when it balances
physics.ising(n, temperature, sweeps, seed) the mean magnetisation per spin of an n by n magnet over the second half of its sweeps, the same for the same seed
import physics

physics.si_units()
physics.quantity("thrust", 5.0, "lbf")
physics.expect("thrust", "N")
physics.quantity("mass", 2.0, "kg")
physics.expect("mass", "N")

print(physics.checks())
print(physics.converted("thrust"))
print(physics.si("mass"))

prints ['handed over: mass in kg where N is expected, incompatible', 'handed over: thrust in lbf where N is expected, convert'], 22.2411080763025 and 2.0.

9.4 What the answers can and cannot mean

A consistent equation is not a true one. consistent says the dimensions on both sides are the same, and E = m v² and E = 2 m v² are equally consistent. An unbalanced equation is wrong, and dimension says by how much: E = m v leaves L1 T-1. What the module does not do is solve an equation, or decide whether a quantity is plausible.

A unit is only as good as the list. A quantity in a unit the list does not hold has no si and no dimension, and checks() names it. An equation with such a factor is judged neither consistent nor unbalanced, and dimension reads unknown unit. The list written by si_units() is the units it lists; there is no degree Celsius, whose zero is not SI’s, and no mole or candela. A unit you add with physics.unit is as good as the factor you give it.

A circuit of series and parallel groups is a sum. physics.resistance adds and reciprocates; a circuit that is not built of series and parallel groups, such as a bridge, is solved with physics.solve or physics.bridge. A group’s members are resistors, not groups, so a group inside a group is not added.

A correction replaces; a disagreement is kept. The writing scripts write under the source physics, so writing a quantity or a resistor again replaces its value and withdraws every conclusion that rested on the old one. A value written under another source is held beside the first. Every conclusion it reaches is drawn from both values, and physics.unsettled() names the fact.

A sum is not drawn from a member bound to it twice. An equation’s dimensions and a group’s resistance are sums, and a sum adds every value it reads: a resistor whose ohms a second source writes, even the same ohms, would be counted twice, 400 ohms where every source says 300. So while a member is bound to its sum twice, that sum is disputed and not concluded, and neither is what rests on it: dimension reads disputed, resistance refuses the group, checks() names each, disputed_equations() and disputed_groups() list them, and why_not on the sum names the dispute. A share both powers of a factor agree on, as every share of a dimensionless unit does, disputes nothing, and a parallel group is disputed only by a resistance that differs, since an equal one is one conductance. A member with no value of the sum, such as a factor in a unit not in the list or a resistor of 0 ohms in a parallel group, adds nothing and hides no dispute. Forgetting the claim that is not to stand settles it. A factor whose equation, or a resistor whose group, two sources disagree on counts in each a value names, so each of those sums is one source’s. No other query refuses.

The calculations are exact in the sense that matters. Each computes what the same formula computes in CPython. The integrators are what they are called: the Euler orbit of eccentricity 0.5 drifts a long way in ten periods and the fourth-order Runge–Kutta one hardly at all. A value script has no inputs but its arguments and reads nothing from the store. A calculation checks its arguments only where a refusal is the answer, with ValueError: small_period and period refuse a length below 0 or a g not above 0, period an amplitude outside -π to π, pendulum an amplitude not above 0 or above π and a length or a step not above 0, agm a number below 0, orbit an eccentricity outside 0 to below 1, and bridge a resistance not above 0. pendulum integrates for up to the exact period, so a pendulum released near the top is timed rather than answered with 0.0, as 1.0.2 did, and refuses a step too long to reach the vertical. solve reads a system as singular only where a pivot is exactly 0, as mathematics.solve does, where 1.0.2 read any pivot under 1e-300 so. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.


10. chemistry — molecular structure, reaction enthalpy, and the arithmetic of reactions

chemistry reads a molecule as a chemist reads a skeletal drawing. You record a molecule’s atoms and the bonds between them. It concludes the hydrogens each atom carries, each molecule’s formula and weight, and the functional groups on its atoms: acids, esters, amides, aldehydes, ketones, alcohols and amines. You record a reaction as species with coefficients and each species’ enthalpy of formation, and it concludes the reaction’s enthalpy. Its value scripts calculate a formula’s molar mass, the balance of an equation in exact integers, the pH of an acid and a titration curve, and the kinetics of consecutive and of reversible reactions.

Version 1.0.3. Download it at dke.langsyn.com/source/chemistry/latest/chemistry.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/chemistry/1.0.3/chemistry.dpy.

Hydrogens are not written. A molecule is its heavy atoms and the bonds between them. Each atom carries the hydrogens its valence leaves over, so an oxygen with one bond carries one hydrogen and an atom with no bond carries its whole valence. The valences and weights are those of the elements in the store: chemistry.elements() writes thirteen of them, and you write an Element yourself for any other.

10.1 Kinds and the fields you write

Kind Fields you write
chemistry.Element z, valence, weight
chemistry.Atom element, molecule; never valence, weight, bond_halves, bond_claims, bonds_disputed, bonds, hweight, overbonded, carbonyl or group, which the rules conclude
chemistry.Bond a, b, order; never belement, bhydrogens or bcarbonyl
chemistry.Molecule none: named by its atoms; never weight_atoms, weight_claims, weight_disputed, hydrogen_atoms, hydrogen_claims, hydrogens_disputed, weight, hydrogens or heavy
chemistry.Species formation
chemistry.Term reaction, species, coefficient; never formation or missing
chemistry.Reaction none: named by its terms; never terms, claims, disputed, enthalpy or incomplete

An element is named by its symbol: its atomic number z, its valence, the number of bonds it forms in an organic molecule, which is 0 for an element that forms none there, and its standard atomic weight. An atom is named by you, and gives its element’s symbol and the molecule it belongs to. A bond goes from atom a to atom b with an order of 1, 2 or 3; a bond is written as its two halves, one from each atom, so each reaches the other. A species is a substance in a state, such as "water_l" for liquid water, with its standard enthalpy of formation in kJ/mol. A term is one species of a reaction with its coefficient, positive for a product and negative for a reactant.

Six scripts write these facts for you:

  • chemistry.elements() writes the elements the rules read: H, C, N, O, Na, P, S, Cl, K, Ca, Cr, Mn and Fe, with the abridged standard atomic weights of IUPAC’s Commission on Isotopic Abundances and Atomic Weights and each element’s usual valence in an organic molecule. Call it once before reading a structure. chemistry.element(symbol, z, valence, weight) writes one.
  • chemistry.atom(name, symbol, molecule) writes an atom.
  • chemistry.bond(a, b, order) writes both halves of a bond, named a-b and b-a. Writing a bond again corrects it.
  • chemistry.species(name, formation) writes a species and its enthalpy of formation in kJ/mol.
  • chemistry.term(reaction, species, coefficient) writes one species of a reaction, named reaction, - and species.

10.2 Derived and maintained for you

Field Meaning
chemistry.Atom.valence the valence of the atom’s element; absent for an element whose valence is 0
chemistry.Atom.weight the standard atomic weight of the atom’s element
chemistry.Atom.hweight the weight of hydrogen, the element of atomic number 1
chemistry.Atom.bond_halves / bond_claims how many halves of its bonds carry an order, and how many claims of order they hold
chemistry.Atom.bonds_disputed more claims than halves: a bond is bound to the atom twice, and no bonds is concluded, nor its hydrogens or mass
chemistry.Atom.bonds the sum of the orders of the atom’s bonds, while they are not disputed; 0 for an atom with none
chemistry.Atom.hydrogens valence - bonds: the hydrogens the atom carries
chemistry.Atom.mass weight + hydrogens * hweight: the atom’s weight with its hydrogens
chemistry.Atom.overbonded the atom is bonded past its valence, so its hydrogens are fewer than 0
chemistry.Atom.carbonyl a carbon with a double bond to an oxygen
chemistry.Atom.group the functional group the atom carries: "carboxylic acid", "ester", "amide", "aldehyde" or "ketone" on a carbonyl carbon, "alcohol" on an oxygen, "amine" on a nitrogen
chemistry.Bond.belement the element at the far end of the bond
chemistry.Bond.bhydrogens the hydrogens of the atom at the far end
chemistry.Bond.bcarbonyl the atom at the far end is a carbonyl carbon
chemistry.Molecule.weight_atoms / weight_claims how many of its atoms have a mass, and how many claims of mass they hold
chemistry.Molecule.hydrogen_atoms / hydrogen_claims how many have hydrogens, and how many claims of them they hold
chemistry.Molecule.weight_disputed / hydrogens_disputed more claims than atoms, or an atom of it whose bonds are disputed: that sum is not concluded
chemistry.Molecule.weight the sum of its atoms’ masses, in g/mol, while it is not disputed
chemistry.Molecule.hydrogens the sum of its atoms’ hydrogens, while they are not disputed
chemistry.Molecule.heavy how many atoms it has that are not hydrogens: the atoms you wrote, each counted once
chemistry.Term.formation the enthalpy of formation of the term’s species
chemistry.Term.contribution coefficient * formation
chemistry.Term.missing the term’s species has no enthalpy of formation recorded
chemistry.Reaction.terms / claims how many of its terms have a contribution, and how many claims of it they hold
chemistry.Reaction.disputed more claims than terms: a term is bound to the reaction twice, and no enthalpy is concluded
chemistry.Reaction.enthalpy the sum of its terms’ contributions, in kJ, while it is not disputed
chemistry.Reaction.incomplete a term of the reaction is missing

Groups are read from the bonds. A carbonyl carbon with a single bond to an oxygen that carries a hydrogen is a carboxylic acid; to an oxygen that carries none, an ester; to a nitrogen, an amide. A carbonyl carbon that carries a hydrogen and has no single bond to an oxygen or a nitrogen is an aldehyde, and one with single bonds to two different carbons is a ketone. An oxygen with one hydrogen bonded to a carbon that is not a carbonyl carbon is an alcohol, and a nitrogen bonded to carbon, none of it a carbonyl carbon, is an amine. The group is concluded on the atom that carries it.

A reaction’s enthalpy is Hess’s law. The enthalpy of a reaction is the sum, over its species, of each coefficient times that species’ enthalpy of formation, whatever path the reaction takes.

10.3 Queries

Call Answers
chemistry.weight(name) the weight of molecule name in g/mol; 0.0 when it has none; refuses a molecule whose weight is disputed
chemistry.enthalpy(reaction) the enthalpy of reaction reaction in kJ; 0.0 when it has none; refuses a disputed reaction
chemistry.formula(name) a molecule’s formula in the Hill order: carbon, then hydrogen, then the other elements alphabetically, or every element alphabetically when there is no carbon; each atom counted once; refuses a molecule whose hydrogens are disputed or one of whose atoms holds two elements
chemistry.disputed_molecules() every molecule whose weight or hydrogens are disputed, sorted
chemistry.disputed_reactions() every reaction a term is bound to twice, sorted
chemistry.groups(name) the functional groups on a molecule’s atoms, each named once, sorted
chemistry.faults() every atom bonded past its valence, as overbonded: and the atom, every reaction with a species whose enthalpy of formation is not recorded, as incomplete: and the reaction, and every disputed atom, molecule and reaction, as disputed: atom and the atom and so on, sorted
chemistry.unsettled() every element, atom, bond, species and term with a field you write that sources give two different values, as element S, sorted
chemistry.parse(formula) how many atoms of each element a formula holds, as a dict: "Ca(OH)2" is one calcium, two oxygens and two hydrogens; brackets nest; a charge after a caret is left out
chemistry.charge(formula) the charge after a caret: "MnO4^-" is -1, "Fe^3+" is 3, no caret is 0
chemistry.molar_mass(formula) a formula’s molar mass in g/mol from the elements’ weights in the store; 0.0 when an element has none
chemistry.gcd(a, b) the greatest common divisor of the absolute values of two integers
chemistry.balance(reactants, products) the least whole-number coefficients that balance an equation, reactants first, every element and the charge the same on both sides; [] unless there is exactly one balance up to scale with every coefficient positive
chemistry.charge_ph(acid, base, pkas) the pH at which the charges balance: a weak acid of total concentration acid mol/L with the acid constants pkas, one for each proton, beside base mol/L of a strong base’s cation
chemistry.ph(acid, pkas) the pH of a weak acid of concentration acid mol/L in water
chemistry.titration(acid, va, base, vb, pkas) the pH after vb mL of a strong base of base mol/L is added to va mL of a weak acid of acid mol/L
chemistry.consecutive(k1, k2, t, dt) the fractions of A, B and C at time t for A to B to C, first order, from pure A, integrated by the fourth-order Runge-Kutta method in steps of dt
chemistry.consecutive_exact(k1, k2, t) the same fractions in closed form, for k1 different from k2
chemistry.peak_time(k1, k2) when B is at its most: ln(k1 / k2) / (k1 - k2), and 1 / k1 when the two are equal
chemistry.equilibrium(kf, kr, a0, b0, t, dt) the concentration of C at time t for A plus B to C and back, from a0 and b0 mol/L of A and B and no C, integrated in steps of dt
chemistry.equilibrium_exact(k, a0, b0) the concentration of C at equilibrium, where C over A times B equals k, which is kf / kr
import chemistry

chemistry.elements()
chemistry.atom("a1", "C", "methanol")
chemistry.atom("a2", "O", "methanol")
chemistry.bond("a1", "a2", 1)

for g in chemistry.groups("methanol"):
    print(g)
print(chemistry.formula("methanol"))
print(chemistry.faults())
print(chemistry.unsettled())

prints alcohol, CH4O, [] and []: the oxygen carries one hydrogen and is bonded to a carbon that is no carbonyl carbon, the carbon carries three, and nothing is overbonded or in dispute.

10.4 What the answers can and cannot mean

A structure is its bonds and nothing more. The rules read atoms, bond orders and the one valence each element has in the store. They do not represent charge, rings, aromaticity or stereochemistry, a second valence for an element such as sulfur or phosphorus, or any group beyond the seven named above. A nitrogen is read as an amine only while every bond it has is single, so a nitrile’s or an imine’s nitrogen carries no group, and an ether, which carries no group the rules name, has none.

Nothing is concluded without the elements. Before chemistry.elements() is called, or for an atom whose symbol has no Element, an atom has no valence and so no hydrogens, and its molecule’s formula has none of its hydrogens. An element of valence 0, such as sodium or iron, gives its atoms no hydrogens whatever their bonds, and its weight: a sodium bonded to a chlorine reads ClNa and weighs 58.44.

A correction replaces; a disagreement is kept. atom(), bond(), species() and term() write under the source chemistry, so writing a bond again replaces it and withdraws every group and weight that rested on the old one. A fact written under another source is held beside the first. Every conclusion reached from it is drawn from both values, and chemistry.unsettled() names it.

A sum is not drawn from a member bound to it twice. An atom’s bonds, a molecule’s weight and hydrogens and a reaction’s enthalpy are sums, and a sum adds every value it reads: a sulfur atom whose element’s weight two sources give as 32.06 and 32.065 would be weighed at both, and a bond whose order a second source writes, even the same order, counted twice. So while a member is bound to its sum twice, that sum is disputed and not concluded, and neither is what rests on it. An atom whose bonds are disputed has no hydrogens or mass, so its molecule is disputed too rather than weighed without it. weight() refuses a molecule whose weight is disputed, formula() one whose hydrogens are, and enthalpy() a disputed reaction; disputed_molecules(), disputed_reactions() and faults() name them, and why_not on the sum names the dispute. A sum both sources agree on stands: the sulfane’s formula reads H2S while its weight is disputed. Forgetting the claim that is not to stand settles it. An atom whose molecule, a bond whose atom, or a term whose reaction two sources disagree on counts in each a value names, so each of those sums is one source’s.

A formula counts each atom once. An atom two sources both give as oxygen is one oxygen. One they give as oxygen and as sulfur is refused by formula(), which names the atom, until one element is forgotten.

An incomplete reaction is a partial sum. A term whose species has no enthalpy of formation adds nothing, so chemistry.enthalpy() answers the sum of the terms that have one, and chemistry.faults() names the reaction as incomplete.

A bond past its valence is marked, not refused. An atom bonded past its valence is overbonded and carries fewer than 0 hydrogens, which lowers its molecule’s hydrogens and so its formula. chemistry.faults() names it.

balance answers only when the equation has one balance. An equation that balances in more than one independent way, or in none, answers [], as does one whose balance needs a coefficient of 0 or less. The coefficients are exact integers, found without rounding.

The pH and kinetics are calculations, not measurements. chemistry.ph() takes water’s ion product as 1.0e-14 and ignores ionic strength. The Runge-Kutta scripts approach their closed forms as dt shrinks. A calculation checks its arguments only where a refusal is the answer, with ValueError: charge_ph, ph and titration refuse a concentration or a volume below 0, the kinetics scripts a rate constant or a time below 0, consecutive a step not above 0 and one so long for the rates that the integration diverges, and equilibrium_exact a constant or a concentration below 0. equilibrium_exact takes the smaller root as 2 k a0 b0 / (p + r), which subtracts nothing, where 1.0.2’s (p - r) / 2k lost digits to cancellation, all of them for a small constant: at constants from 1e-3 to 1e3 it is within 3 units in the last place of the exact root, where 1.0.2 was off by a median of 5 and up to 89 million. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.


11. biology — networks, pedigrees and the arithmetic of inheritance

biology reads two kinds of record you write as facts. From a metabolic network, a growth medium and the reactions that connect compounds, it concludes which reactions can run and which compounds the network can make, which is its scope, and it asks which one compound added to the medium would let the network make one it cannot. From a pedigree, the people of a family and the mode their trait is inherited by, it concludes every person’s ancestors, each child whose parents are related by descent, the carriers the mode requires and each person whose record the mode cannot explain. Its value scripts calculate what genetics calculates: transcription and translation, a Mendelian cross and the chi-square test of its counts, Hardy–Weinberg proportions, logistic growth, predator and prey, genetic drift and a ring of genes that repress one another.

Version 1.0.2. Download it at dke.langsyn.com/source/biology/latest/biology.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/biology/1.0.2/biology.dpy.

A network is facts about names. A reaction names its substrates and its products, each one or two, and a compound is any name a reaction or the medium mentions. A reaction runs when every substrate is reached, and a compound is reached when the medium supplies it or a reaction that runs makes it. The scope is every compound reached. A mode is how the trait a family is recorded for is inherited: "autosomal dominant", "autosomal recessive" or "X-linked recessive".

11.1 Kinds and the fields you write

Kind Fields you write
biology.Compound medium; never reached, which the rules conclude
biology.Reaction s1, s2, p1, p2; never runs
biology.Family mode; never unexplained
biology.Person family, mother, father, sex, affected; never inbred, carrier or unexplained
biology.Parent none: a relation the rules conclude between a child and a parent
biology.Ancestor none: a relation the rules conclude between a person and an ancestor

A compound’s medium is True for one the growth medium supplies. A reaction’s s1 and s2 are its substrates and p1 and p2 its products, each the name of a compound; leave s2 or p2 unwritten for a reaction with one. A family has a mode. A person names its family, its mother and father where known, its sex, "F" or "M", and whether it is affected.

Four scripts write these facts for you:

  • biology.medium(name) marks a compound the medium supplies, under the source medium.
  • biology.reaction(name, s1, s2, p1, p2) writes a reaction under the source network. Pass "" for a second substrate or product the reaction does not have.
  • biology.family(name, mode) writes a family, and biology.person(name, family, sex, affected, mother, father) a person, under the source pedigree. Pass "" for a parent not recorded.

Writing a name again corrects it. Passing "" writes nothing, so it does not withdraw a second substrate, a second product or a parent an earlier call recorded: write the name you mean instead.

11.2 Derived and maintained for you

Field Meaning
biology.Compound.reached the medium supplies the compound, or a reaction that runs makes it
biology.Reaction.runs every substrate of the reaction is reached
biology.Family.unexplained how many members of the family the mode cannot explain, each counted once however many rules or values find them so
biology.Person.inbred the person’s parents are related by descent
biology.Person.carrier an unaffected person the mode requires to carry the allele
biology.Person.unexplained the mode cannot explain the person’s record

Two relations are concluded between people. biology.Parent.c.p holds for each parent recorded of a child c. biology.Ancestor.x.y holds for each parent, each parent’s parent, and so on to the first generation recorded.

A scope is concluded from the medium outward. A reaction that runs makes its products reached, and those start the next reactions. A cycle of reactions that make each other’s substrates is reached only while something outside it supplies one of them; when the medium stops supplying that compound, the cycle is withdrawn with it.

Descent. A child is inbred when its mother and father have an ancestor in common, or when one parent is the other’s ancestor: cousins, siblings, an uncle and a niece, a father and a daughter.

Carriers. In a recessive family, the unaffected parent of an affected child carries the allele, and so does the unaffected child of an affected parent. In an X-linked recessive family, the unaffected mother of an affected son carries it, and so does the unaffected daughter of an affected father.

What a mode cannot explain. An affected child of two unaffected parents in a dominant family; an unaffected child of two affected parents in a recessive one; in an X-linked recessive family, an affected daughter whose father is unaffected, and an unaffected son whose mother is affected.

11.3 Queries

Call Answers
biology.scope() every compound the network can make from its medium, sorted
biology.blocked() every reaction that cannot run, sorted
biology.needs(target, candidates) each candidate that, added to the medium alone, would let the network make target, in the order given; nothing is written
biology.people(name) every person recorded in a family, sorted
biology.ancestors(name) every recorded ancestor of a person, sorted
biology.inbred() every child whose parents are related by descent, sorted
biology.carriers(name) every member of a family the mode requires to carry the allele, sorted
biology.unexplained(name) every member of a family whose record the mode cannot explain, sorted
biology.unsettled() every compound, reaction, family and person with a field you write that sources give two different values, as person nell affected, sorted
biology.complement(dna) the other strand, read in its own direction
biology.transcribe(dna) the messenger RNA of a coding strand: U for each T
biology.translate(rna, table) the protein from the first AUG to the first stop, with table 1, the standard code, or 2, the vertebrate mitochondrial code; "" with no AUG
biology.gc(dna) the share of bases that are G or C
biology.gametes(genotype) the gametes of a genotype written two letters a gene, "AaBb"
biology.zygote(a, b) the genotype two gametes make
biology.cross(a, b) how many pairings of the parents’ gametes give each genotype
biology.phenotypes(a, b) the same pairings by what they show
biology.chi_square(observed, ratio) Pearson’s statistic for counts against an expected ratio
biology.hardy_weinberg(aa, ab, bb) the first allele’s frequency, and the chi-square of the counts against p^2 : 2pq : q^2
biology.logistic(r, k, n0, t, dt) a population grown toward capacity k by fourth-order Runge–Kutta
biology.logistic_exact(r, k, n0, t) the same population from the equation’s solution
biology.predator_prey(alpha, beta, gamma, delta, x0, y0, t, dt) prey, predators, and the relative change in the quantity the exact motion keeps
biology.drift(n, p0, generations, seed) an allele’s frequency after random sampling each generation, and the generation it was fixed or lost in, -1.0 if neither
biology.fixation(n, p0, runs, generations, seed) the share of runs in which the allele is fixed
biology.ring(start, steps) the states of a ring of genes that repress one another
biology.period(start) how many steps the ring takes to return to a state it has been in
import biology

biology.family("marsh", "autosomal recessive")
biology.person("ada", "marsh", "F", False, "", "")
biology.person("ben", "marsh", "M", False, "", "")
biology.person("cass", "marsh", "F", True, "ada", "ben")
biology.reaction("hexokinase", "glucose", "", "g6p", "")

print(biology.carriers("marsh"))
print(biology.needs("g6p", ["xylose", "glucose"]))
print(biology.period("1000"))

11.4 What the answers can and cannot mean

A scope is reachability, not growth. A compound is reached when a chain of reactions leads to it from the medium. The module does not check that a reaction is balanced, runs in the direction written, has its enzyme or is fast enough, and a reaction written as two directions is two reactions.

needs() asks, and writes nothing. For each candidate it supposes that compound is in the medium alone, asks whether the target is then reached, and discards the supposition. The medium, the scope and every other fact are as they were afterwards. A target already reached is answered by every candidate, and a candidate that is the target itself is answered.

Carriers and unexplained members assume a model. Every pedigree conclusion assumes that the trait shows in everyone who has its genotype, and that no new mutation arose in the family. A person marked unexplained is where one of those, or the record, does not hold: incomplete penetrance, a new mutation, a misattributed parent or a wrong mode all produce one. The module names only the carriers a mode requires from the generation next to them. It does not estimate the chance that anyone else carries, and a person it does not name may carry the allele.

A correction replaces; a disagreement is kept. The writers write as pedigree, network and medium, so writing a person or a reaction again replaces what the same source wrote and withdraws every conclusion it no longer supports. A fact written under another source is held beside the first. Every conclusion it reaches is drawn from both values, so a person recorded affected by one source and unaffected by another is reasoned about as both, and biology.unsettled() names the person and the field. A second source that writes the same value agrees with the first, and is not named. No query refuses because sources disagree. A family counts each member the mode cannot explain once: a daughter two modes leave unexplained, while two sources give the family’s mode, is one member, not two. A person whose family two sources disagree on is counted in each family a value names.

The calculations are the formulas as written. The Runge–Kutta scripts take fixed steps and are as accurate as the step is small. drift and fixation draw from a fixed generator, so a seed gives the same run every time: a run is a sample, and a single one says little about the chance of fixation. chi_square refuses a count or a share below 0, and logistic and logistic_exact a capacity not above 0 or a population below 0, with ValueError. A class the ratio expects none of adds nothing to a chi-square where none were observed in it, so hardy_weinberg answers a sample of one homozygote, and makes it inf where some were. predator_prey refuses populations that pass the largest real. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

12. astronomy — sites, stars and bookings, and the arithmetic of the sky

astronomy reads two kinds of record you write as facts. From sites, each with a latitude, a longitude and the faintest magnitude its instrument reaches, and stars, each with a right ascension, a declination and a magnitude, it concludes for every site the stars that never set, the stars that never rise and the stars it can see. From nights, the dark time at a site, and bookings, an instrument booked at a site for a star, it concludes that a booking is observable for exactly the stretch where the booking, a night and the star’s time above the horizon overlap, and reports that stretch. Its value scripts calculate what observing calculates: Julian dates, Greenwich and local sidereal time, a star’s altitude and its rising and setting, the Sun’s position and the length of the day, Kepler’s equation, and the two-body problem.

Version 1.0.2. Download it at dke.langsyn.com/source/astronomy/latest/astronomy.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/astronomy/1.0.2/astronomy.dpy.

A sky is facts about names. A star never sets at a site when it is so far from the pole the site is nearest that it stays above the horizon all day, and never rises when it is so far from it that it stays below. Those limits depend on the site’s latitude alone, and the module works them out for you. Angles are in degrees, longitude is positive east and latitude and declination positive north, a smaller magnitude is a brighter star, and every time is UT.

12.1 Kinds and the fields you write

Kind Fields you write
astronomy.Site lat, lon, limit; never the four declination limits of 12.2, which are computed from lat
astronomy.Star ra, dec, mag
astronomy.Night site, opens, closes
astronomy.Up star, site, rises, sets
astronomy.Booking site, star, starts, ends; never observable
astronomy.Circumpolar none: a relation the rules conclude between a star and a site
astronomy.Hidden none: a relation the rules conclude between a star and a site
astronomy.Visible none: a relation the rules conclude between a star and a site

A site’s lat and lon are its latitude and longitude in degrees, and its limit the faintest magnitude its instrument reaches. A star’s ra and dec are its right ascension and declination in degrees, and its mag its magnitude. A night names its site and is the dark time there, and a booking names the site an instrument is booked at and the star it is booked for. An up is a star above a site’s horizon, from its rising to its setting.

A night’s site, a booking’s site and star, and an up’s star and site are written with a window, the stretch of time they hold for. So that a window can be read when it is not now, it is also recorded as two facts that hold at all times: opens and closes for a night, starts and ends for a booking, rises and sets for an up.

Five scripts write these facts for you:

  • astronomy.site(name, latitude, longitude, faintest) writes a site under the source sites.
  • astronomy.star(name, right_ascension, declination, magnitude) writes a star under the source catalogue.
  • astronomy.night(name, where, start, end) writes a night at the site where from start to end, under the source nights.
  • astronomy.book(name, where, target, start, end) books the star target at the site where from start to end, under the source bookings.
  • astronomy.up(target, where, start) writes the stretch the star target is above the horizon of the site where, under the source ephemeris, named <star>_<site>_<yyyymmdd> by the date of start. It is the first passage that has not ended by start: pass a night’s start to get that night’s passage, which may have begun before it.

Writing a name again corrects it. up() writes nothing for a star that never rises, and for one that never sets it writes the day from start.

12.2 Derived and maintained for you

Field Meaning
astronomy.Site.circumpolar_above 90 - lat: at a northern site, the declination above which a star never sets
astronomy.Site.hidden_below lat - 90: at a northern site, the declination below which a star never rises
astronomy.Site.circumpolar_below -90 - lat: at a southern site, the declination below which a star never sets
astronomy.Site.hidden_above 90 + lat: at a southern site, the declination above which a star never rises
astronomy.Booking.observable the booking, a night at its site and its star’s time above that site’s horizon overlap, and it holds for that overlap alone

Three relations are concluded between a star and a site, Circumpolar.s.t for a star s and a site t. astronomy.Circumpolar holds for a star that never sets at the site, astronomy.Hidden for one that never rises, and astronomy.Visible for one that rises, or never sets, and is no fainter than the site’s limit. A site at latitude 0 is read as a northern site.

Observing. A booking is observable only where all three of its premises hold: the booking itself, a night at its site, and an up of its star at its site. The conclusion holds for the stretch where the three overlap, so reading it at a moment inside the booking answers whether the star is up and the night dark then. A star that never rises has no up, and its booking is never observable.

12.3 Queries

Call Answers
astronomy.circumpolar(where) the stars that never set at a site, sorted
astronomy.hidden(where) the stars that never rise at a site, sorted
astronomy.visible(where) the stars that rise at a site, or never set, and are no fainter than its limit, sorted
astronomy.stars_in(relation, where) the stars in "circumpolar", "hidden" or "visible" for a site, sorted; any other name reads "visible"
astronomy.observable_at(t) the bookings observable at an instant, sorted
astronomy.window(booking) the first and last instants a booking is observable for, as text, or [] where it never is
astronomy.unsettled() every site and star with a field you write that sources give two different values, as star vega dec, sorted
astronomy.number(c) the number a claim holds, an integer as a real, and 0.0 for any other value
astronomy.instant_of(c) the instant a claim holds, as a list of one, and [] for any other value
astronomy.jd(year, month, day) the Julian date of a calendar date, the day carrying its fraction
astronomy.jd_of(t) the Julian date of an instant
astronomy.instant(j) the instant of a Julian date, to the nanosecond
astronomy.gmst(j) Greenwich mean sidereal time in degrees, from 0 to 360
astronomy.lst(j, longitude) local mean sidereal time at a longitude, east positive
astronomy.hms(degrees) an angle of the sky’s rotation as hours, minutes and seconds, 15 degrees an hour
astronomy.altitude(declination, latitude, hour_angle) a body’s altitude above the horizon, in degrees
astronomy.semi_arc(declination, latitude, horizon) the hour angle at which a body crosses the altitude horizon: 0 for one that never reaches it, 180 for one that never falls below it
astronomy.rise_set(ra, declination, latitude, longitude, day) a star’s rising, transit and setting around its transit on the day starting at day, as a list of instants
astronomy.sun(j) the Sun’s right ascension and declination at a Julian date
astronomy.day_length(latitude, j) the hours from sunrise to sunset at a latitude, 24 where the Sun never sets and 0 where it never rises
astronomy.kepler(mean_anomaly, e) the eccentric anomaly that solves Kepler’s equation, and the steps it took
astronomy.true_anomaly(eccentric_anomaly, e) the angle from perihelion seen from the focus
astronomy.two_body(e, orbits, per_orbit) the energy at the start and the end of an orbit integrated by fourth-order Runge-Kutta, and the distance from where Kepler’s equation puts the body
import astronomy

astronomy.site("oslo", 59.91, 10.75, 6.0)
astronomy.star("polaris", 37.95, 89.26, 1.98)
astronomy.star("canopus", 95.99, -52.70, -0.74)
astronomy.star("vega", 279.23, 38.78, 0.03)

print(astronomy.circumpolar("oslo"))
print(astronomy.hidden("oslo"))
print(astronomy.day_length(59.91, astronomy.jd(2026, 6, 21.0)))

12.4 What the answers can and cannot mean

The sky is geometry. Circumpolar, hidden and visible are limits of declination set by latitude. They take the horizon to be the geometric one, a level plane through the observer: they allow for no refraction, terrain, twilight, Moon or cloud, and visible compares a star’s magnitude with the site’s limit and nothing else. Rising and setting from up() and rise_set() are the same: for the geometric horizon unless an altitude is given to semi_arc(), and from the sidereal rate. Refraction lifts a body about half a degree, which day_length() includes for the Sun. A star whose declination is exactly on a limit is neither circumpolar nor hidden.

Coordinates are as you supplied them. The module applies no precession, nutation, aberration or proper motion to a star, so its right ascensions and declinations are taken to be for the epoch you want the answer in. Sidereal time is the mean one. A star’s rising and setting are computed for a declination that does not change over the passage.

An observable booking is an overlap, not a promise. observable says that the booking, a night at the site and the star’s time above the horizon overlap. It does not read the star’s magnitude, the site’s limit, the Moon or the weather. window() reports the stretch from the last of the three opening to the first of them closing, and is [] for a booking that never overlaps; it expects the booking to have been written. An up() is for one passage, so a night long enough to hold two of a star’s passages is covered for the first alone, and two calls on the same date for one star and site replace one another.

The sky is written by you. A rising and a setting are facts you write with up(); nothing concludes one from the star and the site alone, and an up already written does not follow a correction to a star’s or a site’s coordinates until you write it again.

A correction replaces; a disagreement is kept. The writers write as sites, catalogue, nights, bookings and ephemeris, so writing a site or a star again replaces what the same source wrote and withdraws every conclusion it no longer supports. A fact written under another source is held beside the first, and every conclusion is drawn from both values: a star recorded at two declinations can be Hidden by one and Visible by the other, and astronomy.unsettled() names the star or the site and the field. No read refuses because sources disagree, except up() and window(). up() needs one value of the star’s right ascension and declination and of the site’s latitude and longitude, and is refused while any has two. window() needs one value of the booking’s start and of its end, and is refused while either has two; unsettled() does not name a booking. observable_at() is not refused: a booking is observable at an instant if any value of it is.

The calculations are the formulas as written. jd() follows Meeus: the Julian calendar before 1582 October 15 and the Gregorian from then, and a date from the ten that calendar dropped is read as Gregorian. A Julian date held as a real steps in about 40 microseconds, so instant() returns the instant of the real rather than of the decimal you wrote. sun() is good to about 0.01 degrees from 1950 to 2050. kepler() stops when a step changes the answer by less than 1e-15, in at most 64 steps, for an eccentricity from 0 up to 1. two_body() takes fixed steps and is as accurate as the step is small: the energy of every orbit it integrates is -0.5, so the change in the energy is the integration’s error. kepler() refuses an eccentricity outside 0 to 1, and takes a mean anomaly past π to within π of 0 first, so it finds E for any M, where 1.0.1 stopped short below -π or a few turns out. gmst() refuses a date so far from 2000 that its angle passes the largest real. An angle is taken modulo 360 degrees by math.fmod first, which is exact, so an angle past a turn loses no digits on its way to radians: past a turn the sine of one is now within a unit or so in the last place, where it was off by a median of 1,251. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

13. geography — regions, maps, places and the arithmetic of the Earth

geography reads three kinds of record you write as facts. From regions, each with the region it lies in directly and a level, it concludes every region a region lies within, to any depth, and how many regions each one holds. From a map, the borders between regions and the colour each region is given, it concludes which regions neighbour which and which neighbours share a colour, and it can choose colours so that none do. From places by latitude and longitude and boxes of latitude and longitude, it concludes which places lie in which boxes, a box across the antimeridian included. Its value scripts calculate what geography calculates: the distance, bearing and destination along a great circle on a sphere, the distance and azimuths between two points on the WGS84 ellipsoid and the point a distance away, the Mercator projection and its scale, and the area of a triangle on the sphere.

Version 1.0.2. Download it at dke.langsyn.com/source/geography/latest/geography.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/geography/1.0.2/geography.dpy.

A hierarchy is facts about names. A region names the region it lies in directly, its parent, and a level, any word you use for what kind of region it is. A region lies within its parent, its parent’s parent, and so on to the outermost region recorded. A border joins two regions, and two regions joined by a border are neighbours, both ways round. A clash is two neighbours the map gives one colour. A box is a range of latitude and a range of longitude, in degrees; a box whose western edge lies east of its eastern one crosses the antimeridian, 180°, and holds the longitudes from the western edge round to the eastern. Angles are in degrees and distances in metres.

13.1 Kinds and the fields you write

Kind Fields you write
geography.Region parent, level, colour; never holds, which the rules conclude
geography.Within none: a relation the rules conclude between a region and a region it lies within
geography.Border a, b
geography.Neighbour none: a relation the rules conclude between two regions that share a border
geography.Clash none: a relation the rules conclude between two neighbours of one colour
geography.Place lat, lon
geography.Box south, north, west, east
geography.Inside none: a relation the rules conclude between a place and a box it lies in

A region’s parent is the name of the region it lies in directly, left unwritten for one that lies in none you record; its level is what kind of region it is, in any word you use; and its colour is the colour a map gives it. A border’s a and b are the names of the two regions it lies between. A place is at latitude lat and longitude lon, north and east positive. A box runs from south to north and from west to east.

Five scripts write these facts for you:

  • geography.region(name, parent, level) writes a region under the source regions. Pass "" for a parent not recorded.
  • geography.border(a, b) writes a border between two regions, and geography.colour(name, c) the colour a region has, under the source map.
  • geography.place(name, lat, lon) and geography.box(name, south, north, west, east) write a place and a box under the source places.

A sixth, geography.paint(palette), writes colours under map for you: it gives each region named in a border the colour that geography.dsatur() chooses, number k as palette[k].

Writing a name again corrects it. Passing "" writes nothing, so it does not withdraw a parent an earlier call recorded: write the parent you mean instead. Record every region, the outermost too, with geography.region: a region is a name that call has recorded, and a parent nobody recorded concludes nothing.

13.2 Derived and maintained for you

Field Meaning
geography.Region.holds how many regions lie within the region, at any depth, each counted once however many paths lead to it; unwritten for a region that holds none

Four relations are concluded. geography.Within.r.p holds for each region r and each region p it lies within: its parent, its parent’s parent, and so on. geography.Neighbour.x.y holds both ways round for each border. geography.Clash.x.y holds both ways round for two neighbours the map gives one colour; it is declared so that a map with no clash reads as empty rather than as an unknown kind. geography.Inside.p.b holds for each place p within a box b, the edges included.

A move leaves every region above. Write a region again with another parent and it lies within the new parent’s regions and no longer within the old parent’s, and every holds follows. The conclusions rest on the facts, and withdraw when a fact is corrected.

A box across the antimeridian is read as two ranges. A place is within when its latitude is from the box’s south to its north and its longitude is at or east of the box’s west, or at or west of its east. Longitudes are compared as written, so write them from -180 to 180. A box whose two longitudes are equal is that one meridian, and a box whose south lies north of its north holds no place.

13.3 Queries

Call Answers
geography.regions() every region recorded with a level, sorted
geography.within(name) every region a region lies within, sorted
geography.regions_in(name, at) every region within a region at the level at, or at any level for "", sorted
geography.neighbours(name) every recorded region that shares a border with a region, sorted
geography.clashes() every pair of neighbours the map gives one colour, as "x y" with x first, sorted
geography.places_in(name) every place within a box, sorted
geography.unsettled() every region, border, place and box with a field you write that sources give two different values, as place oslo lat, sorted
geography.dsatur() a colour number from 0 for every region named in a border, so that no two neighbours share one; writes nothing
geography.mean_radius() the Earth’s mean radius, 6371008.8 metres
geography.central_angle(lat1, lon1, lat2, lon2) the angle between two points seen from the sphere’s centre, in radians
geography.haversine(lat1, lon1, lat2, lon2) the great-circle distance on the sphere, in metres
geography.bearing(lat1, lon1, lat2, lon2) the initial bearing of the great circle from the first point to the second, from 0 to 360
geography.destination(lat, lon, heading, distance) the point reached on a bearing after a distance in metres on the sphere: [lat, lon]
geography.spherical_excess(lat1, lon1, lat2, lon2, lat3, lon3) the amount by which a spherical triangle’s angles sum past π, in radians
geography.triangle_area(lat1, lon1, lat2, lon2, lat3, lon3) the area of that triangle on the sphere, in square metres
geography.mercator(lat) the Mercator northing for a latitude, in units of the sphere’s radius
geography.mercator_inverse(y) the latitude a Mercator northing comes from
geography.mercator_scale(lat) how much the projection enlarges a short distance at a latitude, 1 / cos of the latitude
geography.dms(x) an angle as [degrees, minutes, seconds], each carrying the angle’s sign
geography.vincenty_inverse(lat1, lon1, lat2, lon2) on the WGS84 ellipsoid, [metres, azimuth there, azimuth back, rounds]; [0.0, 0.0, 0.0, -1.0] when near antipodal points do not converge
geography.vincenty_direct(lat1, lon1, az1, s) on the WGS84 ellipsoid, the point a distance s along the geodesic from a point at azimuth az1: [lat, lon, azimuth back]
import geography

geography.region("norway", "", "country")
geography.region("vestland", "norway", "county")
geography.region("bergen", "vestland", "municipality")
geography.place("bergen_centre", 60.39, 5.32)
geography.box("vestland_box", 59.0, 62.0, 4.5, 8.0)

print(geography.within("bergen"))
print(geography.regions_in("norway", "municipality"))
print(geography.places_in("vestland_box"))
print(geography.haversine(59.91, 10.75, 60.39, 5.32))

13.4 What the answers can and cannot mean

A hierarchy is what you recorded. within and regions_in follow the parents you wrote. The module does not know that a county is larger than a municipality, that two regions overlap or that a region has a border it was not given; a level is a word, and it is compared as one.

A neighbour is a border you wrote. Two regions are neighbours only when a border names them, and the module does not decide from places or boxes that two regions touch. The readers list only regions recorded with a level, so record a region before you read its neighbours or its clashes. dsatur reads the borders alone: it names every region a border names, recorded or not.

A colouring is not the fewest colours. dsatur colours the region whose neighbours already show the most colours, the most neighbours breaking a tie and then the name, and gives it the lowest number none of them shows. No two neighbours share one, but the heuristic does not promise the fewest colours any map needs. paint needs a palette with a colour for every number it chooses; a shorter palette is refused part way, after the colours it reached have been written, so give it a palette as long as the colouring needs, which dsatur shows. dsatur ignores a border from a region to itself.

A place is a point, a box is a rectangle in latitude and longitude. A box is not an area on the ground: its eastern and western edges converge toward a pole, and a box that crosses the antimeridian is read as the two ranges it covers. A place on an edge is within.

The sphere is the mean sphere. haversine, bearing, destination, triangle_area and central_angle treat the Earth as a sphere of radius 6371008.8 metres, so a distance can differ from the ellipsoid’s by a fraction of a percent. vincenty_inverse and vincenty_direct use the WGS84 ellipsoid and are good to well under a millimetre where they converge. They return full-precision numbers and round nothing. Every coordinate and azimuth is taken modulo 360 degrees by math.fmod first, which is exact, so one past a turn loses no digits on its way to radians. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

Some questions have no answer. vincenty_inverse stops after 200 rounds on points that are nearly antipodal, and answers [0.0, 0.0, 0.0, -1.0] rather than a distance; the fourth number is how many rounds it took, and -1.0 means it did not converge. Two points that coincide answer [0.0, 0.0, 0.0, 1.0]. spherical_excess answers 0.0 for three points on one great circle. The Mercator projection has no northing at a pole: do not ask for 90 or -90, where the answer is either a very large number or a refusal, and expect the scale to grow without bound as the latitude nears either pole.

A correction replaces; a disagreement is kept. The writers write as regions, map and places, so writing a region or a place again replaces what the same source wrote and withdraws every conclusion it no longer supports. A fact written under another source is held beside the first. Every conclusion it reaches is drawn from both values, so a region two sources put in two parents lies within both, and a place two sources put at two latitudes is within every box either latitude falls in. geography.unsettled() names the region or the place and the field. A second source that writes the same value agrees with the first, and is not named. No query refuses because sources disagree. A region holds each region within it once: a city two sources put both in its country and directly in its continent lies within the continent by two paths, and the continent holds it once.

14. mathematics — numbers, finite operations and triangles, and the arithmetic of number theory

mathematics reads three kinds of record you write as facts. From whole numbers, each with a row for every candidate divisor up to half of it, 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 a two-sided inverse, and whether it is a group and an abelian one; where a property fails, it names the cells that break it. From a triangle’s three sides in whole units it concludes whether they close up into a triangle at all, and if they do, whether it is acute, right or obtuse and equilateral, isosceles or scalene. Its value scripts calculate number theory and numerical methods: Euclid’s algorithm and its extension, modular inverses and powers, a primality test, factorization, Euler’s totient, the sieve, binomial coefficients, factorials and Fibonacci numbers, Collatz trajectories, continued fractions and Pell’s equation, roots by Newton’s method, integrals from samples by the trapezoidal rule and Simpson’s, and determinants and linear systems by elimination.

Version 1.0.2. Download it at dke.langsyn.com/source/mathematics/latest/mathematics.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/mathematics/1.0.2/mathematics.dpy.

A number is judged by its candidates. A number is written with a candidate for each k from 1 to half of it, and a candidate divides when what is left over is 0. A number is prime when it is above 1 and no candidate above 1 divides it, composite when one does, and perfect, abundant or deficient as the sum of its proper divisors equals, exceeds or falls short of it. An operation is its table. The product a · b of every pair of elements is a cell, and associativity is asked of every three elements in order. A triangle is three whole lengths.

14.1 Kinds and the fields you write

Kind Fields you write
mathematics.Number n; never divisor_sum, prime, composite, perfect, abundant, deficient, divisor_count, claims or disputed, which the rules conclude
mathematics.Candidate of, n, k; never divides, which a rule concludes, or its remainder, which is computed
mathematics.Operation order; never identity, closed, commutative, associative, group or abelian
mathematics.Element op, el; never identity, invertible or noninvertible
mathematics.Cell op, a, b, c, left, right; never outside, noncommuting, to_identity or twosided
mathematics.Triple op, x, z, xy, yz; never left, right or breaks
mathematics.Triangle a, b, c; never acute, right, obtuse, equilateral, isosceles or scalene; its perimeter, gap, slack, heron and valid are computed

A number is named n followed by its value, and its n is the value, at least 1. A candidate names the number it is of, that number’s n and its own k. An operation has an order, how many elements the set has. An element names its op and its label el, and is itself named <operation>_<label>. A cell is one product of the table, a · b = c, named <operation>_<a>_<b>: a, b and c are labels, and left and right name the elements a and b. A triple is three elements in order, named <operation>_<x>_<y>_<z>: x and z are the first and third labels, and xy and yz name the cells x · y and y · z. A triangle has three sides a, b and c in whole units, with c the longest.

Five scripts write these facts for you:

  • mathematics.number(v) writes one number and a candidate for each k from 1 to half of it, and mathematics.numbers(lo, hi) every number from lo to hi, under the source numbers. The candidates grow as the square of hi, so write numbers in the hundreds, not the millions.
  • mathematics.operation(name, elements, table) writes an operation from its table, table[i][j] being elements[i] · elements[j], under the source table. It writes n² cells and n³ triples for n elements, so it is for tables of a few elements. Give the elements labels without an underscore: the readers split a name at its underscores.
  • mathematics.cell(name, a, b, c) rewrites one cell of an operation’s table, so that a · b is c.
  • mathematics.triangle(name, x, y, z) writes a triangle from three sides in any order, sorted into a, b and c, under the source figures.

Writing a name again corrects it.

14.2 Derived and maintained for you

Field Meaning
mathematics.Number.divisor_count / claims how many of its candidates divide it, and how many claims of k they hold
mathematics.Number.disputed more claims than divisors: a divisor is bound to the number twice, and no divisor sum is concluded
mathematics.Number.divisor_sum the sum of the number’s proper divisors, each divisor but itself, while it is not disputed; none for 1
mathematics.Number.prime the number is above 1 and no candidate above 1 divides it
mathematics.Number.composite a candidate above 1 divides the number
mathematics.Number.perfect divisor_sum equals the number
mathematics.Number.abundant divisor_sum exceeds the number
mathematics.Number.deficient divisor_sum falls short of the number, and the number 1
mathematics.Candidate.remainder n % k: what is left when the candidate divides the number
mathematics.Candidate.divides the remainder is 0
mathematics.Operation.identity the label of the element that leaves every element as it is from either side
mathematics.Operation.closed every cell’s product is an element of the set
mathematics.Operation.commutative no two cells a · b and b · a differ
mathematics.Operation.associative no triple gives (x · y) · z and x · (y · z) apart
mathematics.Operation.group the operation is closed and associative, has an identity, and every element has a two-sided inverse
mathematics.Operation.abelian the operation is a commutative group
mathematics.Element.identity the element leaves every element as it is, from the left and from the right
mathematics.Element.invertible the element has a two-sided inverse
mathematics.Element.noninvertible the operation has an identity and the element has no two-sided inverse
mathematics.Cell.outside the cell’s product is not an element of the set
mathematics.Cell.noncommuting the cell b · a gives another product
mathematics.Cell.to_identity the cell’s product is the identity
mathematics.Cell.twosided the cell and its mirror b · a both give the identity
mathematics.Triple.left the label of (x · y) · z
mathematics.Triple.right the label of x · (y · z)
mathematics.Triple.breaks left and right differ
mathematics.Triangle.perimeter a + b + c
mathematics.Triangle.gap a * a + b * b - c * c: positive for an acute triangle, 0 for a right one, negative for an obtuse one
mathematics.Triangle.slack a + b - c: positive exactly when the three lengths close up into a triangle
mathematics.Triangle.heron (a + b + c) * (b + c - a) * (a + c - b) * (a + b - c): sixteen times the square of the area
mathematics.Triangle.valid slack > 0
mathematics.Triangle.acute / right valid, and gap above 0 or equal to 0
mathematics.Triangle.obtuse valid, and gap below 0
mathematics.Triangle.equilateral valid, and all three sides are equal
mathematics.Triangle.isosceles valid, and two sides are equal or three
mathematics.Triangle.scalene valid, and the three sides all differ

Where a property fails, the table names why. noncommuting_pairs names each pair a b whose products differ, nonassociative_triples each three elements x y z whose two bracketings differ, and without_inverse each element that has no inverse. A property that holds is not explained by a list: it is the absence of one.

Everything rests on its facts. A table cell rewritten withdraws the properties that cell held up, and writing the cell back restores them. A divisor is concluded from its candidate’s remainder, which is computed from the number and the candidate.

14.3 Queries

Call Answers
mathematics.numbers_that_are(property) the values of every number that is "prime", "composite", "perfect", "abundant", "deficient" or "disputed", sorted; any other name reads as "deficient"
mathematics.divisors(v) the proper divisors of the number v, sorted
mathematics.properties(name) the properties that hold of an operation, from closed, commutative, associative, group and abelian, in that order
mathematics.identity_of(name) the label of an operation’s identity, "" where it has none
mathematics.noncommuting_pairs(name) every pair a b of an operation whose products a · b and b · a differ, sorted
mathematics.nonassociative_triples(name) every three elements x y z for which (x · y) · z and x · (y · z) differ, sorted
mathematics.without_inverse(name) every element of an operation with an identity that has no two-sided inverse, sorted
mathematics.kinds(name) what a triangle is, from acute, right, obtuse, equilateral, isosceles and scalene, in that order; empty where the sides make no triangle
mathematics.area(name) a triangle’s area by Heron’s formula, the square root of heron over 4; 0.0 where the sides make no triangle
mathematics.unsettled() every number, candidate, cell and triangle side with a field you write that sources give two different values, as cell z4_2_3 c, sorted
mathematics.gcd(a, b) the greatest common divisor by Euclid’s algorithm, of the absolute values
mathematics.lcm(a, b) the least common multiple; 0 when either is 0
mathematics.extended_gcd(a, b) [g, x, y] with a * x + b * y = g, the greatest common divisor, for a and b at least 0
mathematics.modinv(a, m) the inverse of a modulo m, from 1 to m - 1; 0 where there is none; m at least 2
mathematics.modpow(b, e, m) b to the power e modulo m by repeated squaring, for e at least 0 and m up to 3,037,000,499
mathematics.isqrt(n) the largest whole number whose square is at most n
mathematics.is_prime(n) whether n is prime, by the Miller–Rabin test, for n up to 3,037,000,499
mathematics.factorize(n) the prime factors of n, smallest first, each as often as it divides, by trial division
mathematics.totient(n) Euler’s totient: how many of 1 to n share no factor with n
mathematics.primes(n) every prime up to n by the sieve of Eratosthenes; n in the thousands
mathematics.factorial(n) n! for n from 0 to 20
mathematics.binomial(n, k) n choose k, exactly; 0 for k below 0 or above n
mathematics.fibonacci(n) the n-th Fibonacci number, for n from 0 to 92
mathematics.collatz(n) how many steps n takes to reach 1, halving an even number and taking 3n + 1 for an odd one; -1 if it has not reached 1 in 1,024 steps
mathematics.continued_fraction(p, q) the terms of the fraction p / q, for q at least 1
mathematics.convergents(terms) the convergents [p, q] of a continued fraction, one for each term
mathematics.sqrt_period(n) the continued fraction of the square root of n: [a0, a1, …, ar], where a1 to ar repeat forever
mathematics.pell(n) the smallest [x, y] at least 1 with x * x - n * y * y = 1, for n not a square
mathematics.root(x, k) the k-th root of x above 0 by Newton’s method from above
mathematics.trapezoid(ys, h) the integral of samples at equal steps h by the trapezoidal rule
mathematics.simpson(ys, h) the integral of samples at equal steps h by Simpson’s rule, over an odd number of samples
mathematics.machin() π by Machin’s formula of 1706
mathematics.determinant(m) the determinant of a square matrix by Gaussian elimination with partial pivoting
mathematics.solve(m, b) the solution of m · x = b by elimination with partial pivoting; [] where m is singular
mathematics.hilbert(n) the n by n Hilbert matrix, 1 / (i + j + 1)
import mathematics

mathematics.numbers(1, 30)
mathematics.triangle("egypt", 3, 4, 5)

print(mathematics.numbers_that_are("perfect"))
print(mathematics.kinds("egypt"), mathematics.gcd(1071, 462))

prints [6, 28], then ['right', 'scalene'] 21.

14.4 What the answers can and cannot mean

A number is as prime as its candidates say. prime asks that no candidate above 1 divide the number, and the candidates are the facts that were written: those number() writes, from 1 to half of the number. A number written by hand with no candidates, and above 1, is concluded prime, whatever it is. The number 1 is neither prime nor composite, has no divisor sum and is deficient. is_prime is a different thing: it computes from its argument, for n up to 3,037,000,499, and reads no fact.

A property of a table is a property of that table. closed, commutative and associative say that no cell or triple of the table breaks them. They do not say what the table means, or that its labels are the set the operation was meant to act on. A triple whose bracket leaves the set has no result to compare, so it is not judged; a table that is not closed can be reported associative because nothing it contains breaks the law, and it is not a group, which asks for both. An element is noninvertible only where the operation has an identity: an operation with none has no element marked either way, and without_inverse is empty.

A triangle has a kind only when its sides close up. Three lengths with a slack of 0, which lie flat, or below it, have none: kinds answers an empty list and area 0.0. isosceles includes the equilateral. The sides are read in order, c the longest, which mathematics.triangle arranges and which sides you write yourself must already be. area refuses a triangle that was not written, and one whose heron passes 9,223,372,036,854,775,807, as for three equal sides of 42,000 units.

A correction replaces; a disagreement is kept. The writing scripts write under the sources numbers, table and figures, so writing a cell or a triangle again replaces its value and withdraws every conclusion that rested on the old one. A value written under another source is held beside the first. Every conclusion it reaches is drawn from both values: a cell given two products breaks commutativity or associativity if either does, and mathematics.unsettled() names the cell. No query refuses because sources disagree.

A divisor sum is not drawn from a divisor bound to it twice. A divisor sum adds every value it reads: a candidate whose k a second source writes, even the same k, would be added twice, and 6 would read as abundant, which no source says. So while a divisor is bound to its number twice, the number is disputed: no divisor sum, perfect, abundant or deficient is concluded for it, numbers_that_are("disputed") names it, and why_not on its divisor sum names the dispute. Forgetting the claim that is not to stand settles it. A candidate whose number two sources disagree on is added to each number a value names, so each of those divisor sums is one source’s.

The calculations are exact in whole numbers and honest at the edge. An int is 64 bits. A result past 9,223,372,036,854,775,807 refuses as an overflow instead of wrapping, so 20! is computed and 21! is refused, F(92) is computed and F(93) is refused. modpow takes a modulus up to 3,037,000,499 so that the product of two remainders fits; binomial refuses only for a coefficient that itself passes the limit. factorize works by trial division, its steps growing as the square root of the second largest prime factor, so a number with two large prime factors takes the longest. The reals are ordinary double-precision arithmetic: solve reads a matrix as singular only when a pivot is exactly 0, so a nearly singular one, as a Hilbert matrix is, answers with the rounding it suffers. A value script has no inputs but its arguments and reads nothing from the store. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

15. statistics — studies, screening tests and significance tests, and the arithmetic of data

statistics reads three kinds of record you write as facts. From the strata of a study, in each of which two arms were tried and some succeeded, it concludes each arm’s rate in each stratum and in the pooled study, and names a study whose pooled comparison reverses what every stratum shows. From a screening test, by how common the condition is and the test’s sensitivity and specificity, it concludes the chance that a positive result is right and that a negative one is, and whether a positive result is more likely wrong than right. From a significance test, by its p-value and the level it is held to, it concludes whether the result is significant at that level. Its value scripts calculate what a statistician calculates: the mean, the sample variance and the median, a least-squares line and Pearson’s correlation, Welch’s t, the normal distribution and its inverse, the binomial and Poisson probabilities, the birthday problem, Bayes’ rule, the chi-square statistic and its p, and a seeded generator with a Monte Carlo estimate of pi.

Version 1.0.3. Download it at dke.langsyn.com/source/statistics/latest/statistics.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/statistics/1.0.3/statistics.dpy.

A study is facts about strata. Pooling the strata of a study can show one arm ahead where every stratum shows the other, because the arms were not tried in the same proportions in each. The module sums the counts for you, compares the rates, and names the study; deciding which of the two comparisons to believe is yours.

15.1 Kinds and the fields you write

Kind Fields you write
statistics.Stratum study, a_yes, a_n, b_yes, b_n; never the rates or the arms ahead of 15.2, which are computed
statistics.Study label; never a_yes, a_n, b_yes, b_n, which the rules fold from its strata, paradox, which a rule concludes, or a_yes_strata, a_n_strata, b_yes_strata, b_n_strata, a_yes_claims, a_n_claims, b_yes_claims, b_n_claims, a_yes_disputed, a_n_disputed, b_yes_disputed and b_n_disputed, which the rules conclude
statistics.Screen prevalence, sensitivity, specificity; never the predictive values of 15.2
statistics.Significance p, alpha; never the verdict of 15.2

A stratum names the study it belongs to, and gives the successes and the trials of arm A and of arm B. A study is folded from its strata. A screen is a screening test: its prevalence is how common the condition is among those screened, its sensitivity the chance a positive result is given to someone who has it and its specificity the chance a negative result is given to someone who does not, each from 0 to 1. A significance is a test result, with its p and the level alpha it is held to.

Three scripts write these facts for you:

  • statistics.stratum(study, name, a_yes, a_n, b_yes, b_n) writes the stratum named <study>_<name> and the study it belongs to, under the source strata.
  • statistics.screen(name, prevalence, sensitivity, specificity) writes a screen under the source screens.
  • statistics.significance(name, p, alpha) writes a test result under the source tests.

Writing a name again corrects it.

15.2 Derived and maintained for you

Field Meaning
statistics.Stratum.a_rate a_yes / a_n: arm A’s share of successes in the stratum
statistics.Stratum.b_rate b_yes / b_n: arm B’s
statistics.Stratum.a_wins arm A’s rate is strictly the higher
statistics.Stratum.b_wins arm B’s rate is strictly the higher
statistics.Study.a_yes_strata / a_n_strata how many of its strata hold an a_yes, and an a_n
statistics.Study.b_yes_strata / b_n_strata how many hold a b_yes, and a b_n
statistics.Study.a_yes_claims / a_n_claims how many claims of a_yes, and of a_n, those strata hold
statistics.Study.b_yes_claims / b_n_claims the same of b_yes, and of b_n
statistics.Study.a_yes_disputed / a_n_disputed more claims than strata: a stratum is bound to the study twice by that count, and it is not pooled
statistics.Study.b_yes_disputed / b_n_disputed the same of b_yes, and of b_n
statistics.Study.a_yes the sum of its strata’s a_yes, while it is not disputed
statistics.Study.a_n the sum of its strata’s a_n, while it is not disputed
statistics.Study.b_yes the sum of its strata’s b_yes, while it is not disputed
statistics.Study.b_n the sum of its strata’s b_n, while it is not disputed
statistics.Study.a_rate arm A’s rate over the pooled counts
statistics.Study.b_rate arm B’s rate over the pooled counts
statistics.Study.pooled_a_ahead arm A’s pooled rate is strictly the higher
statistics.Study.pooled_b_ahead arm B’s pooled rate is strictly the higher
statistics.Study.paradox one arm is ahead in the pool and the other is ahead in every stratum
statistics.Screen.ppv the chance that a positive result is right, by Bayes’ rule
statistics.Screen.npv the chance that a negative result is right
statistics.Screen.mostly_false ppv is below one half: a positive result is more likely wrong than right
statistics.Significance.significant p is strictly below alpha

Simpson’s paradox. paradox is concluded for a study when its pooled comparison reverses what every stratum shows: arm B ahead in the pool and arm A ahead in every stratum, or the other way round. A stratum in which the arms are level, or in which the arm ahead in the pool is also ahead, is not a reversal, and a stratum with no trials has no rate and does not stand in the way. The conclusion answers why, and withdraws when a count it rests on is rewritten.

Screening. ppv and npv follow from the three figures you wrote and nothing else. A rare condition makes a positive result mostly false even for a good test, which is what mostly_false says.

15.3 Queries

Call Answers
statistics.paradoxes() the studies whose pooled comparison reverses their strata, sorted
statistics.pooled(study) [A's pooled rate, B's pooled rate] for a study; refuses a disputed study, and where either rate is absent
statistics.disputed_studies() every study a stratum is bound to twice by one of its counts, sorted
statistics.predictive_values(name) [ppv, npv] for a screen; refuses where either is absent
statistics.significant_tests() the tests whose p is below their alpha, sorted
statistics.unsettled() every stratum count, screen figure and test figure that sources give two different values, as stratum stones_small a_yes, sorted
statistics.scale_of(xs) the power of two that brings the largest finite value of xs near 1, where that value is huge or tiny; 0 otherwise
statistics.mean(xs) the arithmetic mean
statistics.variance(xs) the sample variance, dividing by n - 1; at least two values
statistics.std_dev(xs) the sample standard deviation
statistics.median(xs) the middle value of the sorted list, or the mean of the two middle ones
statistics.regression(xs, ys) [intercept, slope, r]: the least-squares line and Pearson’s correlation
statistics.welch_t(xs, ys) Welch’s t statistic for two samples of unequal variance
statistics.z_score(x, mu, sigma) how many standard deviations x lies from mu
statistics.normal_pdf(x, mu, sigma) the normal density at x
statistics.normal_cdf(z) the standard normal distribution function, with the tails’ digits kept
statistics.normal_quantile(p) the z whose distribution function is p, for p between 0 and 1
statistics.stirling_error(n) ln(n!) less the logarithm of Stirling’s approximation, for n from 0
statistics.deviance(x, mean) x ln(x / mean) + mean - x, for a finite x from 0 and a finite mean above 0
statistics.exact_product(a, b) [a * b rounded, the part the rounding lost], for factors and a product under 1e300 in size
statistics.binom_pmf(n, k, p) the probability of exactly k successes in n trials, for n from 0 and p from 0 to 1
statistics.binom_cdf(n, k, p) the probability of at most k successes
statistics.poisson_pmf(k, lam) the probability of k events at a mean rate lam, a finite real from 0
statistics.birthday(n) the chance that two of n people share a birthday
statistics.bayes(prior, likelihood, false_likelihood) the posterior chance of a hypothesis after evidence, for a prior from 0 to 1 and finite likelihoods from 0
statistics.chi_square(obs, exp) Pearson’s statistic, for observed counts from 0 and expected counts above 0
statistics.chi_square_p_df1(x) the chance of a statistic at least x with one degree of freedom
statistics.chi_square_p_df2(x) the same with two degrees of freedom
statistics.lehmer_next(seed) the next state of the Lehmer generator
statistics.lehmer_uniform(seed) a state as a number between 0 and 1
statistics.monte_carlo_pi(n, seed) an estimate of pi from n points drawn from the seed
import statistics

statistics.stratum("stones", "small", 81, 87, 234, 270)
statistics.stratum("stones", "large", 192, 263, 55, 80)
statistics.screen("s1", 0.01, 0.9, 0.9)

print(statistics.paradoxes())
print(statistics.pooled("stones"))
print(statistics.predictive_values("s1"))
print(statistics.birthday(23))

prints ['stones'], [0.78, 0.8257142857142857], [0.08333333333333336, 0.9988789237668162] and 0.5072972343239855.

15.4 What the answers can and cannot mean

A reversal is a fact about the counts, not a verdict. paradox says that the pooled comparison and the strata’s disagree. Which one answers the question you asked depends on why the arms were tried in different proportions, which the counts do not say.

A rate needs a trial. A stratum with no trials has no rate, and pooled() and predictive_values() refuse where the rate they read is absent. A script given a list too short for it refuses with a division by zero: a variance needs two values, a mean one, and a regression two different values of x.

A screen is judged at its prevalence. ppv and npv hold for people drawn from the population the prevalence describes. The same test has another ppv among people more likely to have the condition.

A significance is a comparison. significant compares the p you wrote with the alpha you wrote. The module does not compute the p, correct it for many tests, or say whether an effect is large enough to matter.

Nothing checks the figures you write. A probability outside 0 to 1, or a success count above its trials, is a fact like any other. The value scripts are another matter: binom_pmf, binom_cdf, poisson_pmf, bayes, chi_square, stirling_error and deviance refuse a figure outside the range their table names with ValueError, rather than answer a number no probability could be.

A correction replaces; a disagreement is kept. The writers write as strata, screens and tests, so writing a name again replaces what the same source wrote and withdraws every conclusion it no longer supports. A fact written under another source is held beside the first, and every conclusion is drawn from both values: a screen with two prevalences can be mostly false by one and not by the other, and statistics.unsettled() names the fact. No read refuses because sources disagree, except pooled() and predictive_values(), which read one value.

A pooled count is not drawn from a stratum bound to it twice. The pooled counts are sums, and a sum adds every value it reads: a stratum whose successes a second source writes, even the same number, would be counted twice, 354 successes in 350 trials, and a paradox both sources show would vanish. So while a stratum is bound to its study twice by one of its four counts, that count is disputed and not pooled, and neither is what rests on it, the rate and the paradox; a count both sources agree on is still pooled. disputed_studies() names the study, pooled() refuses it, and why_not on the pooled count names the dispute. A stratum with no value of a count adds nothing to it and hides no dispute. Forgetting the claim that is not to stand settles it. A stratum whose study two sources disagree on counts in each study a value names, so each of those pools is one source’s.

The calculations are the formulas as written, kept in range. The variance is Welford’s single pass, so it can differ from a two-pass variance in the last digits; the normal distribution function is erfc correctly rounded, which keeps its digits in the tails; and the quantile is found by 200 halvings and is exact to the last digit of the distribution function. Where a sum or a square in the work would pass the largest real while the statistic does not, as in the mean of 1e308 and 1e307, the values are scaled by a power of two first, which changes no digit of an answer that stays in range. binom_pmf is the product C(n, k) p^k (1 - p)^(n - k) wherever its factors are normal reals, the power of 1 - p corrected for the rounding of 1 - p, and poisson_pmf the running product from e^-lam, which is for a rate to about 708. Past them, each is Catherine Loader’s saddle-point form, through stirling_error, deviance and exact_product, so every probability that is a real is had, at any n or rate: binom_pmf(1100, 550, 0.5) is 0.0240… and poisson_pmf(1000, 1000.0) is 0.0126…. Measured against exact rational arithmetic, the binomial’s product is within 17 units in its last digit to n of 5,000, Poisson’s within 50, and Loader’s form, the looser, within 6 (25 + |ln P|) of them for a probability P. The generator is the module’s own, since DKE Python has no random module, and a run from the same seed repeats exactly: a seed of 0 stays 0, and a seed from 1 to 2147483646 is the one it is meant for. The points of monte_carlo_pi count against the memory one run may hold, so n is for the thousands: 10,000 points from seed 1 give 3.1344, and a run on the service refuses at 20,000.


16. earthscience — climates, storms and minerals, and the arithmetic of the atmosphere and the solid Earth

earthscience reads three kinds of record you write as facts. From weather stations, each with its coldest and warmest month, its annual mean temperature and its annual rainfall and the share of it that falls in the high-sun half of the year, it concludes the Köppen group of each, A (tropical), B (arid), C (temperate), D (continental) or E (polar), the kind of an arid or polar climate, and why a station is not in the group you expected. From storms, by their sustained wind, it concludes whether each is a hurricane, its Saffir-Simpson category from 1 to 5 and whether it is a major one. From minerals, by their hardness, it concludes which scratches which. Its value scripts calculate what a meteorologist and a geophysicist calculate: the dew point, the heat index, the wind chill, the standard atmosphere’s temperature and pressure by height, potential temperature, the Coriolis parameter and the height of the lifting condensation level, radiometric age, the energy of an earthquake and the magnitude of an energy, the ratio of two magnitudes’ energies and a distance from the interval between the P and S waves.

Version 1.0.2. Download it at dke.langsyn.com/source/earthscience/latest/earthscience.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/earthscience/1.0.2/earthscience.dpy.

A climate is facts about a place. A station is dry when its rain is under a threshold that depends on its temperature and on when in the year the rain falls, and warm or cold by its coldest and warmest months. The module works the threshold out for you, applies Köppen’s limits as the sources state them, and puts every station in exactly one group. Temperatures are in degrees Celsius, rainfall in millimetres, wind in miles per hour.

16.1 Kinds and the fields you write

Kind Fields you write
earthscience.Station coldest, warmest, annual_temp, rain, summer_share, boundary; never addition, tundra, icecap, arid, desert, steppe, tropical, temperate or continental, which the rules conclude
earthscience.Storm wind_mph; never hurricane, category or major
earthscience.Mineral hardness
earthscience.Pair first, second, h_first, h_second

A station’s coldest and warmest are the mean temperatures of its coldest and warmest months, annual_temp its annual mean and rain its annual rainfall. summer_share is the fraction, from 0 to 1, of that rain which falls in the half of the year when the sun is high. boundary is the coldest-month temperature that divides the temperate group from the continental: 0.0 or -3.0, as the version of the system you follow has it. A storm’s wind_mph is its sustained wind in whole miles per hour. A mineral’s hardness is a number on a scale on which a harder mineral scratches a softer one. A pair is two minerals in order, named <first>__<second>.

Three scripts write these facts for you:

  • earthscience.station(name, coldest, warmest, annual_temp, rain, summer_share, boundary) writes a station under the source stations.
  • earthscience.storm(name, wind_mph) writes a storm under the source storms.
  • earthscience.minerals(names, hardness) writes each mineral, and a pair for each ordered two different minerals, under the source minerals. The pairs grow as the square of the number of minerals.

Writing a name again corrects it.

16.2 Derived and maintained for you

Field Meaning
earthscience.Station.addition 280, 140 or 0 by summer_share: 280 where 70% or more of the rain falls in the high-sun half-year, 140 for 30% to 70%, 0 below 30%
earthscience.Station.threshold 20.0 * annual_temp + addition: the rainfall under which the station is dry
earthscience.Station.half half the threshold: the line between desert and steppe
earthscience.Station.dry rain is strictly below the threshold
earthscience.Station.polar the warmest month is strictly under 10 degrees: group E
earthscience.Station.tundra polar, with the warmest month 0 or more: ET
earthscience.Station.icecap polar, with the warmest month under 0: EF
earthscience.Station.arid not polar, and dry: group B
earthscience.Station.desert arid, with rain under half the threshold: BW
earthscience.Station.steppe arid, with rain at or above half the threshold: BS
earthscience.Station.tropical not polar, not dry, and the coldest month at least 18 degrees: group A
earthscience.Station.temperate not polar, not dry, the coldest month under 18 degrees and above the boundary: group C
earthscience.Station.continental not polar, not dry, and the coldest month no higher than the boundary: group D
earthscience.Storm.hurricane the wind is 74 miles per hour or more
earthscience.Storm.category the Saffir-Simpson category: 1 for 74 to 95, 2 for 96 to 110, 3 for 111 to 129, 4 for 130 to 156, 5 for 157 or more
earthscience.Storm.major the wind is 111 miles per hour or more: category 3 to 5
earthscience.Pair.scratches the first mineral is strictly harder than the second

Five groups, one each. The rules test the polar group first and the arid second, and only a station that is neither is tropical, temperate or continental, so a station whose facts are all written is in exactly one. The why_not of a group names the premise that failed: why_not of a desert station’s tropical says that the station is dry.

Two versions of the system. A station whose coldest month falls between -3 and 0 degrees is continental by one version and temperate by the other. boundary is the version you follow, and is written with the station.

16.3 Queries

Call Answers
earthscience.groups(name) the Köppen groups a station is in, from "A" to "E"; one for a station whose facts are all written
earthscience.climate(name) the group with its kind: "A", "BW", "BS", "C", "D", "ET" or "EF", and "" before the facts are written
earthscience.dryness_threshold(name) the rainfall in millimetres under which a station is dry, 0.0 before its facts are written
earthscience.category(name) a storm’s Saffir-Simpson category, 0 below hurricane force
earthscience.scratches(first, second) whether the first mineral is strictly harder than the second
earthscience.scratched_by(name) the minerals the named one scratches, sorted
earthscience.unsettled() every station, storm and mineral figure that sources give two different values, as station cairo rain, sorted
earthscience.dew_point(t, rh) the dew point in degrees Celsius of air at t degrees and rh percent humidity
earthscience.heat_index(t, rh) the heat index in degrees Fahrenheit
earthscience.wind_chill(t, v) the wind chill in degrees Fahrenheit for a wind of v miles per hour
earthscience.temperature_at(h) the standard atmosphere’s temperature in kelvin at h metres
earthscience.pressure_at(h) the standard atmosphere’s pressure in hectopascals at h metres
earthscience.potential_temperature(t_kelvin, p_hpa) the temperature air would have brought to 1000 hectopascals without exchanging heat
earthscience.coriolis(lat_deg) the Coriolis parameter in 1/s at a latitude
earthscience.lcl_height(t, td) the height in metres at which rising air reaches saturation
earthscience.radiometric_age(daughter_per_parent, half_life) the age of a sample from the ratio of daughter atoms to parent atoms
earthscience.quake_energy_joules(m) the energy in joules of an earthquake of surface-wave magnitude m
earthscience.magnitude_from_energy(joules) the magnitude whose energy is the given joules
earthscience.quake_ratio(m1, m2) how many times more energy magnitude m2 releases than m1
earthscience.sp_distance_km(sp_seconds, vp, vs) the distance in kilometres to an earthquake from the interval between its P and S waves
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.storm("a", 157)

print(earthscience.climate("manaus"), earthscience.climate("cairo"))
print(earthscience.category("a"))
print(earthscience.quake_ratio(6.0, 7.0))

prints A BW, 5 and 31.622776601683793.

16.4 What the answers can and cannot mean

The groups are Köppen’s first letters, and the sources’ own limits. The module concludes the five groups and the two kinds of arid and polar climate. It does not conclude the finer climates inside A, C and D, which depend on which season is dry or how warm the summer is. A station on a limit falls to one side: a warmest month of exactly 10 degrees is not polar, rain of exactly the threshold is not dry, a coldest month of exactly 18 is tropical and one of exactly the boundary is continental. The sources leave the exact values 0 and 10 degrees between their words “above” and “below”, and the module’s rule is the one stated here.

The high-sun half-year is yours to say. summer_share is the fraction of the rain falling in the half of the year when the sun is high, whichever hemisphere the station is in. The module does not read the hemisphere, and a fraction written for the wrong half classifies the station by it.

A storm is judged by its wind alone. The category is the Saffir-Simpson scale’s five bands of sustained wind, and a wind under 74 has none. The module reads no pressure, surge, size or rainfall.

A mineral scratches by its number. scratches compares two numbers you wrote. Whether a mineral of hardness 7 does scratch one of hardness 3 is the scale’s claim, and two minerals of equal hardness scratch neither.

A formula holds over its range. The calculations check their arguments only where a refusal is the answer: dew_point refuses a humidity that is not above 0 and at most 100. The Magnus dew point was fitted for -40 to 50 degrees; the heat index for 80 degrees Fahrenheit and 40% or more, and below that it returns a value that means little; the wind chill is defined for air at 50 degrees Fahrenheit or colder and a wind above 3 miles per hour; the pressure and temperature by height hold in the troposphere, to about 11,000 metres; and the lifting condensation level is accurate to about 1%. The age of a sample assumes no daughter atoms at the start and none gained or lost since. The energy of an earthquake is for the surface-wave magnitude, and a whole step of magnitude is about 32 times the energy. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. The writers write as stations, storms and minerals, so writing a name again replaces what the same source wrote and withdraws every conclusion it no longer supports. A fact written under another source is held beside the first, and every conclusion is drawn from both values: a station recorded with two annual rainfalls can be in two groups, groups() names both, climate() reads the first of them, and earthscience.unsettled() names the station and the field. No read refuses because sources disagree, except dryness_threshold() and category(), which read one value and refuse while two are in force, as a station with two annual means or a storm with two winds in two categories gives them.


17. computing — codes, checksums and algorithms, and the arithmetic of information

computing reads one kind of record you write as facts. From the words of a binary code, each a string of 0 and 1, it concludes the Kraft sum of the code, whether a word is the beginning of another, and whether the code is prefix-free, so that a received string can be read without a separator. It also concludes whether the lengths of the words could belong to any uniquely decodable code at all, and names the pair of words that breaks a code that is not prefix-free. Its value scripts calculate what an information theorist and a programmer calculate: the CRC-32 and Adler-32 checksums, the Luhn and ISBN-13 checks, numbers in any base from 2 to 36, the Hamming(7,4) code with its single-error correction and the Hamming distance, Shannon’s entropy, the lengths of a Huffman code, binary search and Dijkstra’s shortest paths.

Version 1.0.1. Download it at dke.langsyn.com/source/computing/latest/computing.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/computing/1.0.1/computing.dpy.

A code is facts about words. A code can be read without a separator when no word is the beginning of another. The Kraft sum, the sum of 2 to the minus the length of each word, says whether any code could have words of those lengths, and the module concludes it beside the property that matters. A sum of 1 is necessary and not sufficient. The module counts in ints of 64 bits, so the checksums fit, and every loop in its scripts is bounded.

17.1 Kinds and the fields you write

Kind Fields you write
computing.Code label; never kraft, complete, prefix_free, words, claims or disputed, which the rules conclude, or the sum’s verdict of 17.2
computing.Word code, bits, weight
computing.Pair code, first, second, prefix

A code is named by you, and its name holds no underscore. A word is named <code>_<bits>, with the code it belongs to in code, the string of 0 and 1 in bits, and 2 to the minus its length in weight. A pair is two words of a code in order, named <code>_<first>_<second>, and prefix is True where the first is the beginning of the second, or the same word.

One script writes these facts for you:

  • computing.code(name, words) writes the code, a word for each of words and a pair for each two different positions of the list, under the source code. The pairs grow as the square of the number of words.

Writing a name again corrects it. It does not remove a word written before: a corrected code is written under a new name.

17.2 Derived and maintained for you

Field Meaning
computing.Code.words / claims how many of its words have a weight, and how many claims of weight they hold
computing.Code.disputed more claims than words: a word is bound to the code twice, and no Kraft sum is concluded
computing.Code.kraft the sum of its words’ weight: the Kraft sum, while the code is not disputed
computing.Code.complete the Kraft sum is exactly 1
computing.Code.oversubscribed kraft > 1.0: no uniquely decodable code has words of those lengths
computing.Code.prefix_free no pair of the code has prefix True: no word begins another

The conclusions are positive. A code whose Kraft sum is not 1 has no complete rather than a false one, and a code with a word that begins another has no prefix_free. The why_not of prefix_free names the pair that breaks it. A word written twice is one word that begins itself, so the code is not prefix-free, and its weight counts once in the sum.

17.3 Queries

Call Answers
computing.prefix_free_codes() the codes in which no word begins another, sorted
computing.kraft_sum(name) the Kraft sum of a code, 0.0 before its words are written; refuses a disputed code
computing.disputed_codes() the codes a word is bound to twice, sorted
computing.breaking_pairs(name) every pair "first second" of a code in which the first word begins the second, sorted
computing.unsettled() every word and pair that sources give two different values, as word good_0 bits, sorted
computing.crc32(data) the CRC-32/ISO-HDLC of a string of characters with codes from 0 to 255
computing.adler32(data) the Adler-32 of such a string
computing.luhn_valid(digits) whether a string of digits passes Luhn’s check
computing.isbn13_valid(digits) whether a string of 13 digits passes the ISBN-13 check
computing.to_base(n, base) n written in a base from 2 to 36
computing.from_base(text, base) a number written in a base from 2 to 36, in lower-case digits
computing.hamming_distance(a, b) how many bits differ between two numbers
computing.hamming_encode(d) four data bits as a Hamming(7,4) word of seven
computing.hamming_decode(w) the four data bits of a seven-bit word, after correcting a single flipped bit
computing.entropy(counts) Shannon’s entropy in bits of a source whose symbols occurred counts[i] times
computing.huffman_lengths(freqs) the length of each symbol’s codeword in a Huffman code
computing.binary_search(items, target) the index of target in a sorted list, or -1
computing.binary_search_steps(n, target) how many probes a binary search of n indexes makes for the index target
computing.dijkstra(n, edges, src) the shortest distance from node src to each of nodes 0 to n - 1
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"))
print(computing.crc32("123456789"))

prints ['good'], 1.0 1.0, ['0 01'] and 3421780262.

17.4 What the answers can and cannot mean

A Kraft sum of 1 is not a prefix code. Both codes in the example have a sum of 1, and only the first is prefix-free. The sum is a property of the lengths, and says only whether a code with those lengths could exist: over 1, none could, and oversubscribed says so. Under 1 or at 1 a code can still have a word that begins another, which prefix_free and breaking_pairs report.

A code is judged by the words written for it. A code with no word has a Kraft sum of 0 and is prefix-free, since no pair breaks it. The module does not check that a word is a string of 0 and 1, or that a code’s name holds no underscore, which breaking_pairs() relies on to split a pair’s name.

A bound is a limit of the answer. to_base and from_base hold numbers of up to 63 binary digits, an int. binary_search makes at most 64 halvings. dijkstra gives a node it does not reach the distance 1000000, so a path is shorter than that, and it needs weights from 0 up; it makes n rounds. A Huffman code is built with at most one fewer merge than there are symbols, the lower index first on a tie, so the lengths are those of one Huffman code and not the only one.

The checksums are the catalogue’s. crc32 is CRC-32/ISO-HDLC and gives 0xcbf43926 for "123456789"; adler32 gives 0x11e60398 for "Wikipedia". Both take each character as a byte, and a character with a code above 255 is not one. Luhn’s check and the ISBN-13 check say whether the digits are consistent, not whether the number was issued. Hamming(7,4) corrects any single flipped bit and decodes two flipped bits wrongly, because it cannot tell.

A correction replaces; a disagreement is kept. computing.code writes as code, so writing a word of a name again replaces what the same source wrote. A fact written under another source is held beside the first, and every conclusion is drawn from both values; computing.unsettled() names the fact. No read refuses because sources disagree, but kraft_sum.

A Kraft sum is not drawn from a word bound to it twice. A word’s weight is written as data, and a sum adds every value it reads: a word whose weight a second source writes, even the same weight, would be counted twice, a sum no source gives, and a code complete by every source would read as oversubscribed. So while a word is bound to its code twice, the code is disputed: no Kraft sum, complete or oversubscribed is concluded for it, disputed_codes() names it, kraft_sum() refuses it, and why_not on its Kraft sum names the dispute. A word with no weight adds nothing and hides no dispute. Forgetting the claim that is not to stand settles it. A word whose code two sources disagree on counts in each code a value names, so each of those Kraft sums is one source’s.


18. calendars — years, reforms and days, and the arithmetic of the calendar

calendars reads three kinds of record you write as facts. From the years you name it concludes which are leap years under the Julian rule, which under the Gregorian rule, and which the two rules disagree about. From a calendar reform, the last day of the old calendar in a place and the first of the new, it counts the days the reform skipped, and from the days you name it marks the ones that fall in the gap, days that place never had. Its value scripts calculate what a historian and a programmer calculate: the length of a month under either rule, whether a date exists, the weekday of a date, the date of Easter, the day number of a date under each rule, and the span between two instants.

Version 1.0.0. Download it at dke.langsyn.com/source/calendars/latest/calendars.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/calendars/1.0.0/calendars.dpy.

A calendar is a rule, and a reform is a change of rule. The Julian rule makes every fourth year a leap year, and the Gregorian rule makes every fourth year a leap year except a century not divisible by 400. A place that changed from one to the other dropped the days between: Rome after Thursday 4 October 1582, Britain after 2 September 1752. The module records both as facts about years and reforms, and calculates the dates themselves in the proleptic Gregorian calendar, years 1 to 9999, which applies the Gregorian rule to years before any place adopted it.

18.1 Kinds and the fields you write

Kind Fields you write
calendars.Year number; never julian_leap, gregorian_leap or differ, which the rules conclude
calendars.Reform last, first, last_count, first_count
calendars.Day ymd
calendars.Gap none: a relation the rules conclude between a day and a reform

A year is named y<number>, the letter y and its number. A reform is named for the place that adopted it, and its last and first are dates, each written year * 10000 + month * 100 + day: 4 October 1582 is 15821004. Its last_count and first_count are the same two days as day numbers of the Julian calendar. A day is named by you, and its ymd is a date the same way.

Four scripts write these facts for you:

  • calendars.years(first_year, last_year) writes a year for each number from the first to the last inclusive, under the source years.
  • calendars.year(n) writes the one year y<n>.
  • calendars.reform(name, last_year, last_month, last_day, first_year, first_month, first_day, source) writes a reform, with its two counts, under the source you pass.
  • calendars.day(name, y, m, d) writes a day under the source days.

Writing a name again corrects it. Writing a reform again under another source holds the rival value beside the first.

18.2 Derived and maintained for you

Field Meaning
calendars.Year.r4 number % 4
calendars.Year.r100 number % 100
calendars.Year.r400 number % 400
calendars.Year.julian_leap the year’s r4 is 0: a leap year under the Julian rule
calendars.Year.gregorian_leap r4 is 0 and r100 is not, or r400 is 0: a leap year under the Gregorian rule
calendars.Year.differ a Julian leap year that is not a Gregorian one: the centuries not divisible by 400
calendars.Reform.skipped first_count - last_count - 1: the days the reform skipped

Two rules reach one conclusion, and a year that fails has no value. gregorian_leap is concluded by either of two rules, so a year divisible by 4 and not by 100 and a year divisible by 400 are both leap years. A year that is not one has no gregorian_leap at all, and the why_not of it names the remainders that failed. differ is concluded from that absence.

A day in a gap is marked, not dropped. The module concludes the relation Gap, between a day and a reform, for each day that falls after the reform’s last day of the old calendar and before its first day of the new. The day stays a valid fact, and a reform’s first rewritten moves the gap and withdraws the marks that rested on it.

18.3 Queries

Call Answers
calendars.true_for(values, name) whether any claim in a list as list_values returns is True for the subject name
calendars.julian_leap(n) whether the year n is a Julian leap year, False for a year not written
calendars.gregorian_leap(n) whether the year n is a Gregorian leap year, False for a year not written
calendars.differ(n) whether the two rules disagree about the year n
calendars.years_that_differ() the written years the two rules disagree about, sorted
calendars.skipped(name) the days a reform skipped, one value for each source when sources disagree, sorted
calendars.missing(day_name, place) whether the day falls in the gap of the reform written as place
calendars.unsettled() every reform and day that sources give two different values, as reform rome first, sorted
calendars.is_gregorian_leap(y) whether y is a leap year under the Gregorian rule, for any integer
calendars.is_julian_leap(y) whether y is a leap year under the Julian rule
calendars.month_length(y, m) the days in month m of year y, Gregorian rule
calendars.julian_month_length(y, m) the days in month m of year y, Julian rule
calendars.date_exists(y, m, d) whether the Gregorian calendar has this day, years 1 to 9999
calendars.weekday(y, m, d) the weekday of a Gregorian date, 0 for Sunday to 6 for Saturday
calendars.easter(y) Easter Sunday of the year as [month, day] in the Western calendar
calendars.day_number(y, m, d) the Gregorian day number, 1 for 1 January of year 1
calendars.julian_day_number(y, m, d) the day number on the Julian calendar
calendars.span(a, b) the time from the instant a to b, as a duration
calendars.after(t, days) the instant a whole number of days after t
import calendars

calendars.years(1895, 1905)
calendars.reform("rome", 1582, 10, 4, 1582, 10, 15, "Inter gravissimas")
calendars.day("d1", 1582, 10, 10)

print(calendars.years_that_differ())
print(calendars.skipped("rome"), calendars.missing("d1", "rome"))
print(calendars.weekday(2026, 11, 9), calendars.easter(2026))

prints [1900], [10] True, and 1 [4, 5].

18.4 What the answers can and cannot mean

A year you have not written is not a year that is not a leap year. The rules reach the years in the store, so gregorian_leap(2000) is False while 2000 is not written, and True once it is. The calculations, is_gregorian_leap and month_length among them, answer for any year.

The date values are Gregorian throughout. datetime and duration count in the proleptic Gregorian calendar, and no reform’s gap is in them: a day inside a reform’s gap is a valid datetime, and the rules are what say that a place never had it. The range is years 1 to 9999. A day is 86,400 seconds and a leap second has no place in a datetime, which is a UTC instant written with a trailing Z. A duration has days, hours, minutes and seconds and no months or years, since neither has a fixed number of seconds; a month is calculated with month_length.

Some inputs are not checked. weekday, month_length and julian_month_length do not check that the date exists, and weekday of 30 February answers a weekday. date_exists asks first. easter is the Western date for years from 1583, and answers a date for an earlier year that is no historical Easter.

A span has a limit. span refuses a time of more than about 292 years, and after refuses a result outside years 1 to 9999. Measure a longer time in days with day_number.

A reform’s days are counted on one calendar. The module counts a reform’s skipped days from the two Julian day numbers it writes, so the count holds across the end of a month. The Julian day number’s zero is not day_number’s; only the difference of two means anything.

A correction replaces; a disagreement is kept. calendars.reform writes as the source you pass, so writing a reform again under the same source replaces it, and under another source holds it beside the first. Every conclusion is drawn from both values, a day is in a gap if it is in it under any value in force, and calendars.unsettled() names the fact. No read refuses because sources disagree.


19. colour — contrast pairs and rays, and the arithmetic of light

colour reads two kinds of record you write as facts. 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, and from the refractive indices on either side of a boundary and the sine of the angle at which a ray meets it, whether the ray is totally reflected. Its value scripts calculate what a designer and a physicist calculate: sRGB linear light and relative luminance, a colour written #rrggbb, 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. Download it at dke.langsyn.com/source/colour/latest/colour.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/colour/1.0.1/colour.dpy.

A pair is judged against four lines. The guidelines ask a contrast ratio of 4.5 to 1 for AA and 7 to 1 for AAA, and 3 to 1 and 4.5 to 1 for large-scale text, which they set at 18 point or 14 point bold. The module concludes each level the pair clears, and a pair that clears none is kept. It covers colours in sRGB and not other colour spaces, and spectra, the sign conventions of a diverging lens and the dependence of a refractive index on wavelength are outside it.

19.1 Kinds and the fields you write

Kind Fields you write
colour.Pair fg, bg; never aa, aaa, large_aa or large_aaa, which the rules conclude
colour.Ray n1, n2, sin_i; never total

A pair is named by you. Its fg is the relative luminance of the text and its bg that of the background, each from 0.0 for black to 1.0 for white; luminance_hex and luminance give one from a colour. A ray is named by you. Its n1 is the refractive index of the medium it leaves, n2 of the medium it meets, each 1 or more, as every medium’s is for visible light, and sin_i the sine of its angle of incidence, from 0.0 to 1.0.

Two scripts write these facts for you:

  • colour.pair(name, text, background) writes a pair under the source pairs.
  • colour.ray(name, from_index, into_index, sine_of_incidence) writes a ray under the source rays.

Writing a name again corrects it. To hold a second reading beside the first, write the field yourself with update under another source.

19.2 Derived and maintained for you

Field Meaning
colour.Pair.ratio (fg + 0.05) / (bg + 0.05): the contrast ratio when the text is the lighter
colour.Pair.inverse (bg + 0.05) / (fg + 0.05): the contrast ratio when the background is the lighter
colour.Pair.aa the ratio that applies is at least 4.5: WCAG AA
colour.Pair.aaa the ratio that applies is at least 7: WCAG AAA
colour.Pair.large_aa the ratio that applies is at least 3: AA for large text
colour.Pair.large_aaa the ratio that applies is at least 4.5: AAA for large text
colour.Ray.sin_t n1 * sin_i / n2: the sine of the refracted angle that Snell’s law asks for
colour.Ray.total the ray is totally reflected

The ratio that applies is the lighter’s. A pair whose text is the lighter uses ratio; one whose background is the lighter, or equal to the text, uses inverse. Black text on white and white text on black clear the same levels. A ratio exactly on a threshold clears it.

A level is concluded, never denied. A pair that fails a level has no value for it, and the why_not of the level names the ratio it had and the premise it missed.

Total reflection needs a rarer medium. total is concluded where n1 is above n2 and sin_t is above 1.0, for a sin_i of at most 1.0. At exactly 1.0 the ray runs along the boundary, at the critical angle, and refracts. Into a denser medium the rule never applies.

19.3 Queries

Call Answers
colour.holds(values, name) whether any claim in a list as list_values returns is True for the subject name
colour.levels(name) the contrast levels a pair clears, from "AA", "AAA", "large AA" and "large AAA"
colour.total(name) whether the ray is totally reflected
colour.unsettled() every pair and ray that sources give two different values, as pair grey fg, sorted
colour.srgb_linear(c) the linear light of an sRGB channel, 0 to 255
colour.luminance(r, g, b) the relative luminance of an sRGB colour
colour.luminance_hex(text) the relative luminance of a colour written #rrggbb
colour.contrast_ratio(l1, l2) the contrast ratio of two relative luminances, 1.0 to 21.0
colour.contrast_hex(a, b) the contrast ratio of two colours written #rrggbb
colour.hsl(r, g, b) hue in degrees, saturation and lightness of an sRGB colour
colour.refraction_angle(n1, n2, incidence_deg) the angle of the refracted ray in degrees
colour.critical_angle(n1, n2) the critical angle in degrees, for light into a rarer medium
colour.lens_image(focal_length, object_distance) the image distance of a thin converging lens
colour.lens_magnification(focal_length, object_distance) the magnification of the same lens
colour.wien_peak(temperature_k) the wavelength in metres at which a black body radiates most
colour.frequency_hz(wavelength_nm) the frequency in hertz of light of the wavelength in nanometres
import colour

colour.pair("grey", colour.luminance_hex("#777777"), 1.0)
colour.ray("glass_air", 1.52, 1.0, 0.8)

print(colour.levels("grey"), colour.total("glass_air"))
print(colour.contrast_hex("#000000", "#ffffff"), colour.critical_angle(1.52, 1.0))

prints ['large AA'] True, and 21.0 41.139510414899156.

19.4 What the answers can and cannot mean

A luminance is not a colour. The pair holds two relative luminances, and luminance_hex and luminance compute one from an sRGB colour. The module covers sRGB only. The saturation hsl returns is the HSL model’s, not the HSV model’s.

A refusal is the answer where no value exists. critical_angle into a denser medium, the second index larger than the first, refuses, as does refraction_angle where the ray is totally reflected, with a math domain error: there is no such angle. lens_image with the object at the focus refuses with a division by zero, the image being at infinity. luminance_hex refuses anything but # and six hexadecimal digits. A negative image distance is a virtual image; the sign conventions of a diverging lens are outside the module.

The floats are IEEE doubles. The angle scripts round once where CPython 3.11 rounds twice, so an angle can differ from CPython’s in its last digit; sines and arcsines of the same radians agree. An incidence is taken modulo 360 degrees first, which is exact, and a magnification as f / (f - u), which -v / u is. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. colour.pair writes as pairs, so writing a pair again replaces what that source wrote. A fact written under another source is held beside the first, and a level stands if any value clears it; colour.unsettled() names the fact. No read refuses because sources disagree.


20. body — blood transfusions and body size, and the arithmetic of the 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.

body reads two kinds of record you write as facts. 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, whether red cells of that 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. Its value scripts calculate the body mass index, the Mifflin-St Jeor resting energy, the Mosteller body surface area and the maximum heart rate by two formulas.

Version 1.0.2. Download it at dke.langsyn.com/source/body/latest/body.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/body/1.0.2/body.dpy.

A group is three antigens. O has neither A nor B, A has A, B has B and AB has both, and a plus sign puts D present. The module reasons about nothing else: plasma and platelet compatibility, the Kell and other blood systems, a rare group, children’s growth and any dose are outside it.

20.1 Kinds and the fields you write

Kind Fields you write
body.Transfusion da, db, dd, ra, rb, rd; never lacks_a, lacks_b, lacks_d, reacts or sensitises, which the rules conclude
body.Person kg, metres; never normal

A transfusion is named by you. Its da, db and dd are 1 where the donor’s red cells carry the A, B and D antigens and 0 where they do not, and ra, rb and rd are the recipient’s. A person is named by you, with kg the mass in kilograms and metres the height in metres.

Two scripts write these facts for you:

  • body.transfusion(name, donor, recipient, source) writes a transfusion from two groups written "A+", "O-", "AB+" and so on, under the source you pass.
  • body.person(name, kilograms, height_metres) writes a person under the source people.

Writing a name again corrects it. Writing a transfusion again under another source holds the rival value beside the first.

20.2 Derived and maintained for you

Field Meaning
body.Transfusion.lacks_a the donor’s red cells carry A and the recipient’s do not
body.Transfusion.lacks_b the same for B
body.Transfusion.lacks_d the same for D
body.Transfusion.reacts red cells that would react: a lacked A or a lacked B
body.Transfusion.sensitises a risk of sensitisation: a lacked D
body.Person.bmi kg / metres / metres: the body mass index
body.Person.underweight bmi < 18.5
body.Person.overweight bmi >= 25.0: the obese included
body.Person.obese bmi >= 30.0
body.Person.normal the index is 18.5 or more and under 25

The conclusions are positive. A transfusion that lacks nothing has none of the five conclusions, and the why_not of reacts names the two premises that were not met. sensitises is what the sources describe as a risk, not an immediate reaction.

A height of zero draws no index. bmi is absent, so no band is concluded, and the why_not of normal says that nothing is recorded where the rule reads the index. The index is a statistic of populations, and the bands are not a judgement of a person.

20.3 Queries

Call Answers
body.antigens(group) the A, B and D antigens of a blood group as [a, b, d], each 1 or 0
body.holds(values, name) whether any claim in a list as list_values returns is True for the subject name
body.verdict(name) what the rules conclude for a transfusion: "reacts", "sensitises", both, or empty
body.lacks(name) the antigens of the donor the recipient lacks, from "A", "B" and "D"
body.bmi(name) a person’s body mass index, 0.0 where none is drawn
body.band(name) the narrowest WHO band the rules conclude, "" where no index is drawn
body.unsettled() every transfusion and person that sources give two different values, as transfusion t4 rd, sorted
body.body_mass_index(kilograms, height_metres) the mass over the square of the height
body.mifflin_st_jeor(kilograms, height_cm, age_years, male) the resting energy in kilocalories a day
body.body_surface_mosteller(kilograms, height_cm) the body surface in square metres
body.max_heart_rate_haskell_fox(age_years) the maximum heart rate in beats a minute, 220 minus the age
body.max_heart_rate_tanaka(age_years) the maximum heart rate in beats a minute, 208 minus 0.7 times the age
import body

body.transfusion("t1", "O-", "AB+", "ward")
body.transfusion("t2", "AB+", "O-", "ward")
body.person("p1", 70.0, 1.75)

print(body.verdict("t1"), body.verdict("t2"))
print(body.band("p1"), body.bmi("p1"))

prints [] ['reacts', 'sensitises'] and normal 22.857142857142858.

20.4 What the answers can and cannot mean

A verdict is a stated rule. It says what a taught rule concludes about two groups, and a laboratory decides a real transfusion. The old names of the groups, the universal donor and recipient, are only generally true.

A band is for a population. The WHO bands are cumulative above 25: an index of 30 or more is overweight and obese both, and band returns the narrowest. The index’s own namer judged it inappropriate for individual evaluation.

The formulas are estimates. The Mifflin-St Jeor energy is an estimate for adults, with 5 added for a male and 161 taken off for a female. The two heart rates are estimates with standard deviations of 12 to 15 beats and about 10, and may deviate significantly from a person’s actual value; at 40 years both give 180.0 and at 20 they give 200.0 and 194.0. The module gives no dose, no diagnosis and no treatment.

Inputs are checked only where a refusal is the answer. antigens refuses a group not written as A, B, AB or O followed by a plus or minus sign, and body_mass_index refuses a height of zero with a division by zero. The scripts do not check that a mass, a height or an age is a plausible one.

The floats are IEEE doubles. math.sqrt is correctly rounded, so the body surface agrees with CPython’s. The index divides by the height twice rather than by its square, so that it is had wherever it is a real. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. body.person writes as people, so writing a person again replaces what that source wrote. A fact written under another source is held beside the first, and a conclusion stands if any value supports it, so a transfusion whose recipient’s D antigen two sources disagree about still sensitises while one value lacks it; body.unsettled() names the fact. No read refuses because sources disagree, except bmi(), which reads one value and refuses while two are in force, as a person with two weights or two heights gives it.


21. sound — exposures, workers’ days and tones, and the arithmetic of 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.

sound reads three kinds of record you write as facts. 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, below the range of hearing or above it. Its value scripts calculate what an acoustician calculates: the hours a limit permits, the level of a pressure, the level of two sources added, the fall with distance, the speed of sound, a wavelength, the Doppler shift and the 8-hour average level of a dose.

Version 1.0.4. Download it at dke.langsyn.com/source/sound/latest/sound.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/sound/1.0.4/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 permitted time halves. OSHA’s Table G-16 is 90 decibels and 5, and the level NIOSH recommends is 85 and 3. The module takes yours. It covers A-weighted levels taken as given and does not compute them from a spectrum; 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 are outside it.

21.1 Kinds and the fields you write

Kind Fields you write
sound.Exposure level, hours, ref, rate, allowed; never over
sound.Segment worker, level, hours, allowed
sound.Worker label; never total, over, segments, claims or disputed
sound.Tone hz; never audible, infrasound or ultrasound

An exposure is named by you. Its level is in decibels, hours is how long it is held, ref and rate are the limit it is judged against, and allowed is the hours that limit permits at the level. A segment is one period of a worker’s day, named by you, with the worker whose day it belongs to. A worker is that day. A tone is named by you, with its frequency in hertz.

Three scripts write these facts for you:

  • sound.exposure(name, level, hours, ref_level, exchange_rate) writes an exposure, and the allowed its limit permits, under the source exposure.
  • sound.segment(worker, name, level, hours, ref_level, exchange_rate) writes a segment and the worker’s day under the source segment.
  • sound.tone(name, hertz) writes a tone under the source tone.

Writing a name again corrects it. To hold a second reading beside the first, write the field yourself with update under another source.

21.2 Derived and maintained for you

Field Meaning
sound.Exposure.dose hours / allowed: the share of the permitted exposure the hours use
sound.Exposure.over the dose is more than one
sound.Segment.dose hours / allowed
sound.Worker.segments / claims how many of the worker’s segments have a dose, and how many claims of dose they hold
sound.Worker.disputed more claims than segments: a segment is bound to the day twice, and no total is concluded
sound.Worker.total the sum of the doses of the worker’s segments, while the day is not disputed
sound.Worker.over the total is more than one
sound.Tone.audible the frequency is from 20 to 20,000 Hz, both ends inside
sound.Tone.infrasound the frequency is below 20 Hz
sound.Tone.ultrasound the frequency is above 20,000 Hz

At the limit is not over it. A dose of exactly one is the whole of the permitted exposure and is not more than it, and the why_not of over says the dose is 1.0. A day whose total is exactly one is not over either.

An exposure over its limit is marked, not dropped. The exposure is a valid fact, and over is the only consequence. Write it again with fewer hours and the mark is withdrawn. A conclusion is positive: an exposure that is not over has no over, not a false one.

A day is the sum of its segments. Each segment carries the hours its own limit permits, so the segments of one day may be judged against different limits. A worker with no segment has no day.

21.3 Queries

Call Answers
sound.holds(values, name) whether any claim in a list as list_values returns is True for the subject name
sound.names_of(values) the subjects any claim in the list is True for, sorted
sound.over_limit_names() the exposures whose dose is more than one, sorted
sound.days_over() the workers whose segments’ doses sum to more than one, sorted
sound.dose(name) an exposure’s dose, 0.0 before the facts are written
sound.total_of(name) the sum of the doses of a worker’s segments, 0.0 before it is concluded; refuses a disputed day
sound.disputed_days() every worker a segment is bound to twice, sorted
sound.range_of(name) "audible", "infrasound" or "ultrasound" for a tone, "" before the facts are written
sound.unsettled() every exposure, segment, worker and tone with a field you write that sources give two different values, as segment s1 hours, sorted
sound.permissible_hours(level, ref_level, exchange_rate) the hours a limit permits at a level
sound.level_of_pressure(pascals, reference_pascals) the sound pressure level in decibels, 20 log10(p / p0)
sound.add_levels(a, b) the level of two incoherent sources added
sound.at_distance(level_at_r1, r1, r2) the level at another distance from a point source in free field
sound.speed_of_sound(celsius) the speed of sound in dry air in metres a second
sound.wavelength(speed, hertz) the wavelength in metres
sound.doppler(source_hz, speed, receiver_toward, source_toward) the observed frequency
sound.twa_from_dose(dose_percent) the 8-hour time-weighted average level of a dose in percent
import sound

sound.exposure("e1", 95.0, 4.0, 85.0, 3.0)
sound.exposure("e2", 95.0, 4.0, 90.0, 5.0)
sound.tone("t1", 440.0)

print(sound.over_limit_names(), sound.dose("e1"), sound.dose("e2"))
print(sound.range_of("t1"), sound.speed_of_sound(20.0), sound.add_levels(80.0, 80.0))

prints ['e1'] 5.039684199579493 1.0, and audible 343.2353419474446 83.01029995663981.

21.4 What the answers can and cannot mean

The hours a limit permits are this module’s derivation. permissible_hours is 8 / 2^((level - ref) / rate). It is not a formula read from NIOSH or from OSHA, and a published table rounds where it does not: at 92 decibels it gives 6.06 hours where Table G-16 has 6, at 97 it gives 3.03 where the table has 3, and at 102 it gives 1.52 where the table has 1.5. It reproduces the table’s 95 decibels for 4 hours and 100 for 2 at a limit of 90 and 5.

A dose is not a diagnosis. An exposure over its limit says that the hours used more than the limit permits. Whether a person is harmed is decided by a measurement and a hearing conservation programme.

The range of hearing is a convention. 20 to 20,000 Hz is the commonly stated range, and it shrinks during life. infrasound and ultrasound are the names used here for what lies below and above it.

A refusal is the answer where no value exists. level_of_pressure of a pressure or a reference of zero or below refuses with a math domain error, as do speed_of_sound below -273.15 degrees and at_distance with a distance of zero or below; doppler with a source moving at the speed of sound refuses with a division by zero.

The floats are IEEE doubles, and a decibel is not always CPython’s. math.log10 is correctly rounded and CPython’s can differ from it by one unit in the last place at some arguments: the level of a pressure of 1.4142135623730951 against a reference of 1 is 3.0102999566398125 here and 3.010299956639812 in CPython 3.11. Results are not rounded. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. sound.exposure writes as exposure, so writing an exposure again replaces what that source wrote. A fact written under another source is held beside the first, and an exposure is over if any value puts it over; over_limit_names names it, and sound.unsettled() names the fact. dose and total_of read one value, and refuse while two are in force, saying that the cell holds two competing values.

A day’s total is not drawn from a segment bound to it twice. A day’s total is a sum, and a sum adds every value it reads: a segment whose hours two sources give as 4 and 3 would be counted under both, and a day under the limit by either source would be over it. So while a segment is bound to its worker’s day twice, as two values of its hours, level or permitted hours bind it, the day is disputed: no total and no over is concluded for it, disputed_days() names it, total_of refuses it, and why_not on its total names the dispute. A segment with no dose adds nothing and hides no dispute. Forgetting the claim that is not to stand settles it. A segment whose worker two sources disagree on counts in each worker’s day a value names, so each of those totals is one source’s.


22. finance — loan offers and budgets, and the arithmetic of interest and value

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.

finance reads two kinds of record you write as facts. 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 offers carry a payment over the limit of the borrower’s budget. Its value scripts calculate what a lender and a saver calculate: the amortised payment, compound interest, the effective annual rate of a nominal one and of continuous compounding, the doubling time by the exact form and by the rule of 72, present value, a value in today’s money and the real rate.

Version 1.0.2. Download it at dke.langsyn.com/source/finance/latest/finance.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/finance/1.0.2/finance.dpy.

A result is a real number, not a cent. A bank rounds each payment to the cent and builds a schedule from those; this module does not, so a total is the payment times the months. Use it to compare, not to reproduce a lender’s statement. Fees folded into an annual percentage rate, taxes, a schedule row by row, variable rates and any statute beyond the words of the module’s sources are outside it.

22.1 Kinds and the fields you write

Kind Fields you write
finance.Offer borrower, principal, payment, months; never cheapest or unaffordable, which the rules conclude
finance.Budget limit

An offer is named by you. Its borrower and principal name the group in which it is compared, payment is monthly and months is the number of payments. A budget is named by the borrower it belongs to, and its limit is the largest monthly payment the household can bear.

Three scripts write these facts for you:

  • finance.offer(name, borrower, principal, payment, months) writes an offer with its monthly payment as the lender states it, under the source offer.
  • finance.quote(name, borrower, principal, annual_rate, months) writes an offer whose payment monthly_payment calculates from a rate.
  • finance.budget(borrower, limit) writes a budget under the source budget.

Writing a name again corrects it.

22.2 Derived and maintained for you

Field Meaning
finance.Offer.total payment * months: what the lender is paid
finance.Offer.interest (0.5 * payment * months - 0.5 * principal) * 2.0: what is paid beyond the sum lent, taken of halves so that no total past the largest real makes it one
finance.Offer.cheapest no offer of the same borrower and principal has a smaller total
finance.Offer.unaffordable the payment is above the limit of the borrower’s budget

Cheapest is the absence of a cheaper one. The conclusion is not “cheaper than some other offer”, which would mark an offer that a third one beats. It holds while no offer of the group has a smaller total and is withdrawn when a cheaper one is written. The why_not of cheapest says that the absence it needs does not hold: a cheaper offer is present.

Offers with equal totals are each cheapest. Neither has a cheaper one, and the module ranks nothing.

An offer over a limit is marked, not dropped, and the same offer can be both cheapest and unaffordable. A borrower with no budget has no offer marked unaffordable: nothing is recorded to be over, and the why_not says so. A payment exactly at the limit is not over it.

22.3 Queries

Call Answers
finance.names_of(values) the subjects any claim in a list as list_values returns is True for, sorted
finance.cheapest_offers() the offers with no cheaper one in their group, sorted; equal totals are each in it
finance.unaffordable_offers() the offers whose payment is over their borrower’s limit, sorted
finance.total(name) an offer’s total, 0.0 before the facts are written
finance.unsettled() every offer and budget with a field you write that sources give two different values, as offer a payment, sorted
finance.monthly_payment(principal, annual_rate, months) the amortised monthly payment
finance.compound_amount(present, annual_rate, periods_per_year, years) what a sum grows to
finance.effective_annual_rate(annual_rate, periods_per_year) the annual rate equal to a nominal one compounded that often
finance.continuous_annual_rate(annual_rate) the annual rate equal to continuous compounding, e^r - 1
finance.doubling_exact(percent) the years to double at a rate in percent
finance.doubling_rule_of_72(percent) the rule of 72, 72 / r
finance.present_value(future, annual_rate, years) a future sum discounted to now
finance.todays_value(nominal, inflation, years) a nominal sum in today’s money
finance.real_rate(nominal_rate, inflation) the real rate by the exact Fisher form
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())
print(finance.total("a"), finance.total("c"))
print(finance.effective_annual_rate(0.12, 12.0), finance.doubling_exact(8.0), finance.doubling_rule_of_72(8.0))

prints ['c'] ['c'], 386511.56856874016 266449.20465995866 and 0.12682503013196972 9.006468342000588 9.0.

22.4 What the answers can and cannot mean

The cheapest in total is not the one to take. In the example the shorter loan costs 120,062 less in interest and is the only one over the household’s limit. The module reports that both are true and says nothing of which to choose.

A rate is a fraction, except in two scripts. 0.05 is 5 percent. The two doubling-time scripts take percent, as the rule of 72 is written, and the rule suits rates from 6 to 10 percent.

A refusal is the answer where no value exists. monthly_payment for zero months refuses with a division by zero, and for fewer with ValueError; compound_amount and effective_annual_rate refuse a number of periods a year of zero or below with ValueError, and effective_annual_rate a rate that loses more than all of a sum in a period with a math domain error; doubling_exact of a rate of zero refuses with a division by zero, since the sum never doubles, and of -100 percent with a math domain error. A rate of zero in monthly_payment is not a refusal: the payment is the principal over the months.

The floats are IEEE doubles. The payment and the rates are worked through expm1 and log1p, which keep the digits that subtracting 1 from a number near 1 would lose: effective_annual_rate with one period gives 0.12 for 0.12. Results agree with CPython’s except where a logarithm or an exponential is taken, which is correctly rounded here and can differ by one unit in the last place in CPython. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. finance.offer writes as offer, so writing an offer again replaces what that source wrote. A fact written under another source is held beside the first, and a conclusion stands if any value supports it, and finance.unsettled() names the fact; total reads one value and refuses while two are in force, saying that the cell holds two competing values.


23. food — dishes, standards and ingredients, and the arithmetic of a recipe

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.

food reads four kinds of record you write as facts. From a dish’s kind, the temperature its core reached and the minutes it spent in the danger zone, and the minimum internal temperature of its kind, it concludes whether the dish is undercooked, whether it was held too long, and whether it is unsafe for either cause; a kind with no minimum recorded it concludes unknown, 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. Its value scripts calculate what a cook calculates: Fahrenheit to Celsius, a recipe scaled by servings, a baker’s percentage, a volume in cups as a mass, food energy and kilocalories as kilojoules.

Version 1.0.3. Download it at dke.langsyn.com/source/food/latest/food.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/food/1.0.3/food.dpy.

The temperatures are FoodSafety.gov’s. They are the chart “Cook to a Safe Minimum Internal Temperature” (https://www.foodsafety.gov/food-safety-charts/safe-minimum-internal-temperatures), 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. The chart rounds the Celsius figures and the module converts the Fahrenheit ones exactly, so 145 F is 62.77777777777778 where the chart prints 63. The rows of the chart that give no temperature, and the rest time of a steak, a roast or a chop, are not in it, and a cooling curve, nutrients beyond four, cooking losses and yields are outside it.

23.1 Kinds and the fields you write

Kind Fields you write
food.Standard minimum
food.Dish kind, core, minutes; never undercooked, held_too_long, unsafe, unknown, milk, egg, fish, shellfish, tree_nuts, peanuts, wheat, soybeans or sesame, which the rules conclude
food.Item milk, egg, fish, shellfish, tree_nuts, peanuts, wheat, soybeans, sesame
food.Line dish, item

A standard is named by a kind of food and its minimum is the minimum internal temperature in Celsius. A dish is named by you; its kind names the standard it is judged against, its core is the temperature its core reached in Celsius and its minutes the time it spent in the danger zone. An item is an ingredient named by you, with a true or false for each of the nine allergens. A line puts an item in a dish.

Five scripts write these facts for you:

  • food.standard(kind, minimum_fahrenheit) writes a standard from the chart’s Fahrenheit, converted exactly, under the source standard.
  • food.chart() writes the eleven standards of the chart that give a temperature: steak for steaks, roasts and chops, ground, poultry, egg-dish, casserole for a casserole with meat or poultry, or meatless, ham-raw, ham-precooked for precooked ham reheated, ham-packaged for cooked ham packaged in a USDA-inspected plant and reheated, which the chart’s note puts at 140 F where ham-precooked is 165 F, leftovers, rabbit-venison and fish.
  • food.dish(name, kind, core_celsius, minutes_in_zone) writes a dish under the source dish.
  • food.item(name, allergens) writes all nine allergen facts of an item, true for those in the list and false for the rest, under the source item. A name that is not one of the nine refuses.
  • food.line(name, dish_name, item_name) writes a line under the source line.

Writing a name again corrects it.

23.2 Derived and maintained for you

Field Meaning
food.Dish.undercooked the dish’s kind has a standard and its core is below the standard’s minimum
food.Dish.held_too_long the dish spent more than 120 minutes in the danger zone
food.Dish.unsafe the dish is undercooked or was held too long
food.Dish.unknown no standard is recorded for the dish’s kind
food.Dish.milk a line of the dish uses an item that holds milk
food.Dish.egg a line of the dish uses an item that holds egg
food.Dish.fish a line of the dish uses an item that holds fish
food.Dish.shellfish a line of the dish uses an item that holds shellfish
food.Dish.tree_nuts a line of the dish uses an item that holds tree nuts
food.Dish.peanuts a line of the dish uses an item that holds peanuts
food.Dish.wheat a line of the dish uses an item that holds wheat
food.Dish.soybeans a line of the dish uses an item that holds soybeans
food.Dish.sesame a line of the dish uses an item that holds sesame

A kind with no standard is unknown, never safe. An empty unsafe means that no rule concluded it. unknown says that no standard was there to conclude from, and the why_not of undercooked says that nothing is recorded for the standard.

An item you never described is unknown, not free. item writes all nine facts, so an item described as holding wheat is free from the other eight and an item you never described holds nothing the rules can see. A dish holds an allergen only of the ingredients you recorded.

Corrections withdraw what rested on them. A core temperature written again above its minimum withdraws unsafe, and an ingredient replaced in a line withdraws the allergen it brought.

23.3 Queries

Call Answers
food.danger_zone(agency) [low, high] in Celsius of the danger zone as "usda" (4 to 60) or "uk" (8 to 63) defines it
food.holds(values, name) whether any claim in a list as list_values returns is True for the subject name
food.names_of(values) the subjects any claim in the list is True for, sorted
food.unsafe_dishes() the dishes the rules conclude unsafe, sorted
food.unknown_dishes() the dishes whose kind has no standard recorded, sorted
food.allergens_of(name) the allergens a dish holds, in the order of the nine
food.unsettled() every standard, dish, item and line with a field you write that sources give two different values, as dish d1 core, sorted
food.fahrenheit_to_celsius(fahrenheit) (f - 32) * 5 / 9
food.scale_by_servings(quantity, from_servings, to_servings) a quantity for another number of servings
food.bakers_percentage(part, flour) an ingredient’s weight as a percentage of the flour’s
food.volume_to_mass(cups, density_grams_per_ml) grams for a volume in US customary cups, by a density you supply
food.food_energy(protein_g, carbohydrate_g, fat_g, alcohol_g) kilocalories by the labelling table’s approximate factors
food.kilocalories_to_kilojoules(kilocalories) kilojoules, 4.184 for each kilocalorie
import food

food.chart()
food.dish("d1", "poultry", 70.0, 30.0)
food.dish("d2", "salmon", 70.0, 30.0)
food.item("flour", ["wheat"])
food.item("butter", ["milk"])
food.line("l1", "cake", "flour")
food.line("l2", "cake", "butter")

print(food.unsafe_dishes(), food.unknown_dishes())
print(food.allergens_of("cake"), food.fahrenheit_to_celsius(165.0))

prints ['d1'] ['cake', 'd2'] and ['milk', 'wheat'] 73.88888888888889. The poultry dish is unsafe, its core of 70 being under the 73.89 of 165 F. Nothing is recorded for salmon, so that dish is unknown. cake exists only through its lines, has no kind, and is unknown too.

23.4 What the answers can and cannot mean

Not unsafe is not safe. The rules say what a chart concludes about the temperatures you recorded. A thermometer in the food, a cook’s judgement and the guidance of the agency that governs the kitchen decide whether to eat it.

The danger zone is two ranges. The United States defines it as 4 to 60 Celsius and the United Kingdom as 8 to 63, and the module holds both. The two hours of the rule is the limit of the minutes a dish spent there, and a dish with exactly 120 minutes is not held too long. The rule reads the minutes you give it and does not know which range you used.

The chart’s figures are converted, not rounded. A core of 63 is not below the standard of a steak, 62.77777777777778, though the chart prints 63.

A refusal is the answer where no value exists. item refuses an allergen that is not one of the nine, danger_zone an agency other than usda and uk, and bakers_percentage a flour of zero with a division by zero.

The floats are IEEE doubles. Each result agrees with CPython’s. A density is a number you supply: the module holds none. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. food.dish writes as dish, so writing a dish again replaces what that source wrote. A fact written under another source is held beside the first, and a dish is unsafe if any value puts it there, and food.unsettled() names the fact. No read refuses because sources disagree.


24. architecture — styles, records of height, towers, hulls, rooms and channels, and the arithmetic of building

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. A style is a family resemblance that a rule makes strict, and a record of height is a verdict over the records you wrote.

architecture reads eight kinds of record you write as facts. 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, and gives the year each record passed. From a tower’s lean it concludes whether, on the model of a straight column of even weight, it tips; from a box hull’s beam, draught and centre of gravity, whether its metacentric height is not positive; from a room’s volume and the surfaces lining it, Sabine’s reverberation time and whether the room rings longer or shorter than you want; and from a channel’s fall, whether it is flatter than the standard you hold it to, under any reading of that standard. Its value scripts calculate a lean in degrees and the offset of a top, Sabine’s time, a box hull’s metacentric height, a gradient and the same as one in N, and a pendulum’s period and length.

Version 1.0.2. Download it at dke.langsyn.com/source/architecture/latest/architecture.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/architecture/1.0.2/architecture.dpy.

The definitions are the sources’. Gothic is pointed arches, rib vaults and flying buttresses, three of the four features its definition names; the fourth is its windows. Romanesque is round arches and thick walls, with a barrel vault or a groin vault. A pendentive is a device for setting a circular dome over a square room, so a dome on pendentives is a form and not a style. A record of height is kept for structures and for buildings separately, as the list of the tallest structures built before the twentieth century keeps it. The metacentric height is KM = KB + BM less KG, with BM = I/V; for a box, KB as half the draught and BM as the beam squared over twelve times the draught are this module’s derivation, not a published formula. Sabine’s equation is used with the constant 0.161, and Vitruvius’s minimum fall is held as the two readings the sources give. Each is quoted, with its page, at the head of the module. Loads, materials, foundations, wind, stability at large angles of heel, other reverberation formulas and the flow of water are outside it.

24.1 Kinds and the fields you write

Kind Fields you write
architecture.Building pointed_arch, rib_vault, flying_buttress, round_arch, thick_walls, barrel_vault, groin_vault, dome, pendentives; never gothic, romanesque or pendentive_dome, which the rules conclude
architecture.Structure height, completed, until, building; never tallest_structure, tallest_building or undated, which the rules conclude
architecture.Tower height, offset, base; never tips, which a rule concludes
architecture.Hull beam, draught, kg; never unstable, which a rule concludes
architecture.Room volume, low, high; never absorption, too_live, too_dead, unmeasured, surfaces, claims or disputed, which the rules conclude
architecture.Surface room, area, coefficient
architecture.Fall minimum
architecture.Channel length, drop, standard; never too_flat or no_standard, which the rules conclude

A building is named by you, with a true or false for each of nine features. A structure is named by you; its height is in metres, completed is the year it was completed, negative before the common era, until the year it fell and building whether it is a building. A tower has its height, its top’s offset from the vertical and its base’s outer diameter, in metres. A hull is a box with a beam, a draught and kg, the height of its centre of gravity above the keel, in metres. A room has a volume in cubic metres and the range of reverberation times you want, low to high, in seconds, and a surface lines the room it names, with an area in square metres and an absorption coefficient. A fall is a standard named by its name, with the minimum gradient it allows. A channel has a length and a drop in any one unit, and the name of the fall it is held to as its standard.

Ten scripts write these facts for you:

  • architecture.building(name, features) writes all nine features of a building, true for those in the list and false for the rest, under the source building. A name that is not one of the nine refuses.
  • architecture.structure(name, height, completed, is_building) writes a structure, standing, under the source structure.
  • architecture.fell(name, year) writes the year a structure fell, or the part of it that made its height.
  • architecture.tower(name, height, offset, base) writes a tower under the source tower.
  • architecture.hull(name, beam, draught, kg) writes a hull under the source hull.
  • architecture.room(name, volume, low, high) writes a room under the source room.
  • architecture.surface(name, room_name, area, coefficient) writes a surface under the source surface.
  • architecture.fall(standard, minimum, source) writes a standard’s minimum under the source you name, so two sources that give it differently write two values of one fact.
  • architecture.vitruvius() writes Vitruvius’s minimum under the name vitruvius twice: half a foot in a hundred, 0.5 / 100.0, and one in 4,800.
  • architecture.channel(name, length, drop, standard) writes a channel under the source channel.

Writing a name again corrects it. structure writes the structure standing again, so write fell after it.

24.2 Derived and maintained for you

Field Meaning
architecture.Building.gothic the building has pointed arches, rib vaults and flying buttresses
architecture.Building.romanesque the building has round arches and thick walls, and a barrel vault or a groin vault
architecture.Building.pendentive_dome the building has a dome and pendentives
architecture.Structure.tallest_structure no structure completed in its year or before it still stood after it and was taller
architecture.Structure.tallest_building it is a building, and no building completed in its year or before it still stood after it and was taller
architecture.Structure.undated the structure has a height and no year of completion
architecture.Tower.shift half the top’s offset: the centre of mass’s distance from the vertical, on the model
architecture.Tower.radius half the base’s outer diameter
architecture.Tower.tips the shift is greater than the radius
architecture.Hull.kb half the draught: the centre of buoyancy above the keel, for a box
architecture.Hull.bm the beam squared over twelve times the draught, for a box
architecture.Hull.gm the metacentric height, kb + bm - kg, taken of halves and doubled
architecture.Hull.unstable the metacentric height is zero or less
architecture.Room.surfaces / claims how many of the room’s surfaces have sabins, and how many claims of sabins they hold
architecture.Room.disputed more claims than surfaces: a surface is bound to the room twice, and no absorption is concluded
architecture.Room.absorption the sum of the sabins of the room’s surfaces, exact and rounded once, while the room is not disputed
architecture.Room.rt Sabine’s time, 0.161 * volume / absorption
architecture.Room.too_live the time is longer than high
architecture.Room.too_dead the time is shorter than low
architecture.Room.unmeasured the room has a volume and no surface
architecture.Surface.sabins the surface’s absorption, area * coefficient
architecture.Channel.gradient the drop over the length
architecture.Channel.too_flat the gradient is below the standard’s minimum under at least one of its values
architecture.Channel.no_standard no minimum is recorded for the channel’s standard

A record is judged over the records you wrote. A structure stands in every year before its until, and one that is not recorded cannot end a record. A monument or a tower that is not a building ends a structure’s record and not a building’s.

A record with no year is undated and never ranked. With no year, nothing could be completed before it, so it would otherwise be concluded the tallest. undated marks it, and writing its year ranks it.

A zero divisor draws no value. A hull of zero draught has no bm and no gm, a channel of zero length no gradient, and a room whose surfaces absorb nothing no rt; no rule concludes from them, and why_not says that nothing is recorded there.

Corrections withdraw what rested on them. A fall written for a structure concludes the record a taller one had ended only while it stood, ballast that lowers kg withdraws unstable, and an absorbing surface added to a room withdraws too_live.

24.3 Queries

Call Answers
architecture.names_of(values) the subjects any claim in a list as list_values returns is True for, sorted
architecture.styles_of(name) the styles and forms a building has, in the order gothic, romanesque, pendentive_dome
architecture.tallest_structures() the structures that were the tallest when they were completed, sorted
architecture.tallest_buildings() the buildings that were the tallest building when they were completed, sorted
architecture.undated_structures() the structures with a height and no year of completion, sorted
architecture.reals(values) a dict from each subject in the list to its number; a subject with two different values refuses
architecture.record_until(name, buildings_only) the year a record passed: the earlier of the year the structure fell and the year a taller one was completed after it, among buildings only when buildings_only is True; 10000.0 when nothing recorded passed it; refuses while a fact it reads holds two values
architecture.tipping_towers() the towers that tip on the model, sorted
architecture.unstable_hulls() the hulls whose metacentric height is not positive, sorted
architecture.live_rooms() the rooms that ring longer than their range, sorted
architecture.dead_rooms() the rooms that ring shorter than their range, sorted
architecture.disputed_rooms() the rooms a surface is bound to twice, sorted
architecture.unmeasured_rooms() the rooms with a volume and no surface, sorted
architecture.flat_channels() the channels below their standard’s minimum under at least one of its values, sorted
architecture.channels_without_standard() the channels whose standard has no minimum recorded, sorted
architecture.disputes(values, noun) the facts in a list that hold two different values, as the noun, the subject and the field
architecture.unsettled() every building, structure, tower, hull, room, surface, fall and channel with a field you write that sources give two different values, as fall vitruvius minimum, sorted
architecture.lean_degrees(offset, height) a lean in degrees, degrees(atan(offset / height))
architecture.top_offset(height, degrees) the top’s offset for a lean in degrees, height * tan(radians(degrees))
architecture.reverberation_time(volume, absorption) Sabine’s time in seconds, 0.161 * volume / absorption
architecture.metacentric_height(beam, draught, kg) a box hull’s metacentric height, draught / 2 + beam * beam / (12 * draught) - kg
architecture.gradient(length, drop) drop / length
architecture.one_in(length, drop) the gradient as one in N, length / drop
architecture.pendulum_period(length) the period in seconds of a pendulum of a length in metres, at standard gravity and for a small swing, 2 pi sqrt(length / 9.80665)
architecture.pendulum_length(period) the length in metres of a pendulum with a period in seconds, 9.80665 * (period / (2 pi))^2
import architecture

architecture.building("cologne", ["pointed_arch", "rib_vault", "flying_buttress"])
architecture.structure("lincoln", 160.0, 1311.0, True)
architecture.fell("lincoln", 1548.0)
architecture.structure("cologne", 157.0, 1880.0, True)
architecture.structure("washington", 169.0, 1884.0, False)
architecture.structure("ulm", 161.53, 1890.0, True)
architecture.vitruvius()
architecture.channel("nimes", 50000.0, 17.0, "vitruvius")

print(architecture.styles_of("cologne"), architecture.tallest_structures())
print(architecture.record_until("cologne", False), architecture.record_until("cologne", True))
print(architecture.flat_channels(), architecture.unsettled())

prints ['gothic'] ['cologne', 'lincoln', 'washington'], 1884.0 1890.0 and ['nimes'] ['fall vitruvius minimum']. Lincoln is written as fallen in 1548, so it does not stand in 1880 and Cologne is the tallest when it is completed. The Washington Monument, which is not a building, passes Cologne’s record as a structure in 1884, and Ulm passes its record as a building in 1890. A channel falling 17 in 50,000 is too flat under half a foot in a hundred and within one in 4,800, and is marked too flat because one value puts it there.

24.4 What the answers can and cannot mean

A style is a family resemblance made strict. A building with two of the three Gothic features is not Gothic here, and why_not names the feature it lacks. A building you never described has no style and is not thereby of none.

A record is over what you recorded. 10000.0 from record_until means passed by nothing you recorded, not held for ever. The rules ask which structure was the tallest when it was completed, so one that became the tallest later, when taller ones fell, is not concluded. A list of records from another source may disagree about a date, and the rules reason about whichever you wrote.

The models are models. A tower that does not tip on the model may still fall, for the model sees the structure and not the ground under it. A metacentric height measures stability at small angles of heel only. Sabine’s equation is the whole of the module’s acoustics.

A refusal is the answer where no value exists. building refuses a feature that is not one of the nine, record_until a structure that was not the tallest when it was completed, and reals a value that is not a number. The value scripts refuse a zero divisor with a division by zero, and pendulum_period a negative length with a math domain error.

The floats are IEEE doubles. Each step is rounded once: atan and tan are correctly rounded, and degrees and radians round a·180/π and a·π/180 once. A lean or an offset can therefore differ from CPython’s in the last place: architecture.lean_degrees(1.0, 14.0) is 4.085616779974876 here and 4.085616779974877 in CPython 3.11. Every other script gives CPython’s digits. A lean is taken modulo 360 degrees first, which is exact. Where a step of a value script’s work would pass the largest real or fall below the smallest normal one while its value would not, the script works it so that it stays in range, which changes no digit of an answer whose work stays in range.

A correction replaces; a disagreement is kept. Each writing script writes under its own source, so writing a fact again replaces what that source wrote. A fact written under another source is held beside the first, and architecture.unsettled() names it. A verdict the rules conclude from what they find holds under any value in force, as a style or a flat channel does; one they conclude from what they do not find holds only if nothing is found under any value, so a taller value of any height ends a record and a structure whose own height holds two values is never the tallest. record_until and reals refuse while a fact they read holds two values.

A room’s absorption is not drawn from a surface bound to it twice. The absorption is a sum, and a sum adds every value it reads: a surface whose area two surveys give as 2,000 and 2,500 square metres would be counted under both, an absorption neither survey gives. So while a surface is bound to a room twice, as two values of its area or coefficient bind it, the room is disputed: no absorption, time or verdict is concluded for it, disputed_rooms() names it, and why_not on its absorption names the dispute. A second source giving the same area disputes nothing, since the surface’s sabins are then one value, and a surface with no sabins adds nothing and hides no dispute. Forgetting the claim that is not to stand settles it. A surface whose room two sources disagree on lines each room a value names, so each of those rooms’ absorption is one source’s, and a room left unmarked is within the source that puts the surface in it.


25. elections — who wins under each counting method, no winner, a count in dispute, and the arithmetic of seats

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. A winner in it is a verdict over the numbers you wrote, under the method named.

elections reads five kinds of record you write as facts. 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 concludes that no candidate beats every other when the head-to-head counts run in a cycle, and it concludes no winner at all while a ballot’s count holds a second claim, naming the dispute. Its readers give the winners of an election under each method and an instant-runoff count, round by round. Its value scripts calculate the Hare and Droop quotas, a house’s seats by largest remainders and by the D’Hondt, Sainte-Lague and Huntington-Hill methods, and a Huntington-Hill priority.

Version 1.0.0. Download it at dke.langsyn.com/source/elections/latest/elections.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/elections/1.0.0/elections.dpy.

The methods are the sources’. A Condorcet winner is the candidate “preferred by a majority of voters” in each pairing, and majorities can run in a cycle. The Borda count gives each candidate, from each ballot, a number of points equal to the number of candidates it is preferred to. Plurality asks only for the most first choices. The Hare quota is votes over seats and the exact Droop quota votes over seats plus one; largest remainders by the Hare quota is Hamilton’s method; D’Hondt divides by one, two, three and on, Sainte-Lague by one, three, five and on, and Huntington-Hill gives each state a seat and then ranks its population over the square root of n (n + 1). Each is quoted, with its page, at the head of the module. The law of any election, ballots that tie or leave candidates out, and the allocation of seats from ballots are outside it.

25.1 Kinds and the fields you write

Kind Fields you write
elections.Election candidates; never total, matches, firstclaims, rankclaims, ballots, disputed or no_condorcet, which the rules conclude
elections.Candidate election; never firsts, borda, plurality, majority, condorcet, condorcet_loser or borda_winner, which the rules conclude
elections.Ballot election, first, ranking, count
elections.Pref ballot, over, under
elections.Duel election, a, b; never ab or ba, which the rules conclude

An election is named by you and has two candidates or more. A candidate is named by you, stands in one election, and its name is its own across all your elections. A ballot ranks every candidate of its election once, first to last, and records how many voters cast it as its count; its ranking is the candidates’ names joined by >. A preference records, for one ballot and one pair of candidates, which of the two the ballot ranks higher, and a duel is one ordered pair of the candidates of an election, a against b.

Three scripts write these facts for you:

  • elections.election(name, roster) writes an election and its candidates, two or more, each named once, with the duels and preferences of every pair, under the source election. It also writes the election’s ballot of no voters, named by the election’s name and __baseline, so that an election has a total before its first ballot. A name that holds __ or > refuses, and so does a candidate named twice, one already standing in another election, and an election already written. An election’s candidates are fixed once written.
  • elections.ballot(name, poll, voters, order) writes a ballot of election poll under the source ballot, ranking every candidate of it once in order, with the preference of every pair. Written again, it replaces the ballot.
  • elections.recount(name, voters, reason) writes a second count of a ballot under the source reason, which is not ballot, and so puts the election in dispute until one of the two claims is forgotten.

25.2 Derived and maintained for you

Field Meaning
elections.Candidate.twice twice the voters who put the candidate first
elections.Duel.margin the voters who rank a over b less those who rank b over a
elections.Election.total the voters over all the election’s ballots
elections.Election.matches how many claims on a ballot’s count the election holds
elections.Election.firstclaims how many claims on a ballot’s first choice the election holds
elections.Election.rankclaims how many claims on a ballot’s ranking the election holds
elections.Election.ballots how many distinct ballots the election holds
elections.Election.disputed a ballot holds a second claim on its count, ranking or first choice
elections.Election.no_condorcet no candidate beats every other head to head
elections.Candidate.firsts the voters who put the candidate first
elections.Candidate.borda the candidate’s Borda score: the sum of its head-to-head tallies
elections.Candidate.plurality no other candidate of the election has more first choices
elections.Candidate.majority more than half of the voters put the candidate first
elections.Candidate.condorcet the candidate beats every other head to head
elections.Candidate.condorcet_loser every other candidate beats the candidate head to head
elections.Candidate.borda_winner no other candidate has a higher Borda score
elections.Duel.ab the voters ranking a over b
elections.Duel.ba the voters ranking b over a

A tie is no win and no loss. A duel with a margin of zero makes neither, so a candidate who ties another head to head is neither a Condorcet winner nor a Condorcet loser. A tie for the most first choices, or for the highest Borda score, concludes the verdict for every candidate in it.

A count in dispute decides nothing. 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, and a winner concluded from the total would be the winner under neither. While a ballot holds a second claim, no winner is concluded for its election and its readers refuse; why_not on a candidate’s verdict names the dispute. Forgetting the claim that is not to stand settles it, and every verdict returns.

25.3 Queries

Call Answers
elections.strings(values) a dict from each subject in a list as list_values returns to its string; a value that is not a string refuses
elections.candidates_of(name) the candidates of an election, sorted; empty for one not written
elections.names_of(values) the subjects any claim in a list is True for, sorted
elections.disputed_elections() the elections whose count is in dispute, sorted
elections.without_condorcet_winner() the elections in which no candidate beats every other, sorted
elections.settled(name) the candidates of an election whose count is not in dispute; an election not written, and one in dispute, refuse
elections.chosen(values, names) the subjects of a list of names that any claim in a list is True for, sorted
elections.winners(name) a dict with the keys plurality, majority, condorcet, condorcet_loser and borda, each the sorted list of the candidates the rules conclude it for; empty where no candidate wins, and with more than one where candidates tie
elections.tallies(values, names) a dict from each name to its number in a list, 0.0 where it has none
elections.first_choices(name) a dict from each candidate to the voters who put it first
elections.borda_scores(name) a dict from each candidate to its Borda score
elections.head_to_head(name) a dict from each ordered pair "a>b" to the voters who rank a over b
elections.runoff(name) an instant-runoff count, one tally per round: each ballot counts for the highest candidate on it still standing, and the candidate with the fewest votes is eliminated, until one stands; a tie for last place refuses
elections.hare_quota(votes, seats) votes over seats
elections.droop_quota(votes, seats) the exact Droop quota, votes over seats plus one
elections.largest_remainder(populations, seats) Hamilton’s method: each state takes the whole part of its share and the seats left go one each to the largest remainders; a tie for the last seat refuses
elections.wide(x, p) x times p as two halves of 31 bits, the high and the low
elections.wide_compare(x, p, y, q) 1, 0 or -1 as x * p is greater than, equal to or less than y * q, exactly
elections.ahead(a, na, b, nb, method) 1, 0 or -1 as a state of population a holding na seats has a higher, an equal or a lower priority for the next seat than one of population b holding nb
elections.highest_averages(populations, seats, method) a dict from each state to its seats, one at a time to the highest priority: by "dhondt", "sainte_lague" or "huntington_hill", which first gives every state one seat; a tie for the last seat refuses
elections.priority_value(population, held) Huntington-Hill’s priority, population / sqrt(held * (held + 1)); a state holding fewer than one seat refuses
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["condorcet"], w["borda"])
print(elections.runoff("tn")[3], elections.largest_remainder({"a": 6, "b": 6, "c": 2}, 10))

prints ['memphis'] ['nashville'] ['nashville'] and {'knoxville': 100.0} {'a': 4, 'b': 4, 'c': 2}. Memphis has the most first choices, Nashville beats every other city head to head and has the highest Borda score, and the instant-runoff count ends with Knoxville, whom the other methods do not choose.

25.4 What the answers can and cannot mean

A winner is a verdict under a method. The methods can disagree, as they do above, and none of them is the election’s result: that is decided by the law and the officials who run it. A candidate with no winner under a method is not thereby a loser under it.

A cycle leaves no Condorcet winner. Where A beats B, B beats C and C beats A, no candidate beats every other, winners gives an empty list for condorcet, and elections.without_condorcet_winner() names the election.

A refusal is the answer where no value exists. An election not written is refused by every reader, and so is one whose count is in dispute, since a verdict over a disputed count is a verdict under neither. A ballot that does not rank every candidate once refuses, and so does a tie that more states share than there are seats left to give: a method that names no way to break a tie is not made to. Huntington-Hill compares squared populations in whole numbers, exact for a population up to 3,037,000,499, and refuses a larger one.

A correction replaces; a second claim is a dispute. ballot written again under the same name replaces the ballot and withdraws what rested on it, while recount writes a second count under another source and puts the election in dispute until one is forgotten.


26. How you get one, and what holds once you have it

You download the .dpy file and compile it into your own store, once. From then on import <name> resolves it there like any other module, and nothing in your store distinguishes it from one you wrote.

Modules are served from dke.langsyn.com, under /source/<name>/<version>/<name>.dpy, with /source/<name>/latest/ forwarding to the current release, which replaces the previous version at that path rather than joining it. They are MIT, they are plain text, and you can read one in a browser before deciding to compile it.

  1. You can read it before you run it. It is a file in the published language. Every class, rule and query this page documents is a line you can see, and the tables below are a lookup, not a substitute for the source.
  2. It is yours once compiled. forget_rule removes one of its rules, forget_module removes the whole module, and nothing puts either back — your store holds what you compiled into it.
  3. Editing it is expected. If a rule does not suit your data, change the line and compile again. What you get is then genuinely yours, and this page describes the version you started from rather than the one you are running.
  4. Its interface is enumerable in the store. list_modules reports it, info_module reports the scripts it defines and the classes it declares, list reports its scripts with their signatures, and list(target = rules) its standing rules by name. A module’s SOURCE is not stored — the store keeps the compiled form — so the file you downloaded is the copy to keep.
  5. The version tells you whether you are current. The version stated for each module below never changes without its content changing, so the version you hold answers the question directly.

A published module’s name is a default, not a reserved word: compile your own module named code and yours is what import code resolves to.


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


28. dkepy — writing your facts

dkepy is the Python package that writes facts into your store, for the kinds a module declares or for kinds you define yourself. Its distribution name is dkepy and its import name is dke: they differ because dke is taken on PyPI. It is served, file by file and as a tarball, at https://dke.langsyn.com/source/dkepy/, which states the version we publish; the version you hold is dke.__version__. It is not on PyPI, so pip install dkepy installs nothing of ours.

You do not need it. It is one producer of facts, published so that you have a worked example and need not write the half that goes wrong quietly yourself. A program in any language that meets Library Specification §5 is an equally good producer, and Library Tutorial Chapter 3 shows this one at work.

Producing facts opens no connection and spawns no process, so it needs no key: import dke and the readers under dke.ingest, dke.quantum and dke.pqc reach nothing that does. The two modules that talk to a network, dke.client and dke.keys, are reached only by naming them. dke.keys needs the keys extra, which adds cryptography; everything else needs only the standard library of Python 3.10 or later.

28.1 The package root

from dke import FactProgram, FactLoad, Ids, sanitize. This layer knows how to write a program that records facts and nothing about what the facts mean.

Name What it is
dke.FactProgram(module, source="ingest") One program that records facts into module’s kinds, every claim carrying source as its provenance.
dke.FactLoad(module, source="ingest", *, max_bytes) A load written as as many programs as it needs, none larger than max_bytes. A store refuses a program over a size it is set up with, so pass that size, or one well inside it.
dke.Ids() Hands out subjects: the same subject for the same qualified name for as long as you keep the instance, and a subject of its own for each of two names that would otherwise reduce to one.
dke.sanitize(name) A qualified name reduced to one subject: every character that is not a letter, digit or underscore becomes an underscore. That loses information, which is why Ids exists.

28.2 Building a program

Method What it does
FactProgram.comment(text="") One comment line in the program’s header. An empty one writes a bare #.
FactProgram.open() Writes the import. Everything written after it is a statement of the program.
FactProgram.q(s) s as a string literal, escaped, so a quote in your data cannot end the string early.
FactProgram.put(attr, value) One cell write, as a line of the program. attr is a Kind.subject.field path. The write is update, which revises this source’s earlier claim on the cell and makes one on an empty cell; claims from other sources are untouched.
FactProgram.table(name, rows, body) A list of entries and the loop that writes each one; body is the put lines for one entry. The program stays the same length however many entries there are, and an empty list writes nothing.
FactProgram.load_marker(key, stamp, kind="Load") Records that the load started and how much it means to write, and, as the program’s last statement, that it finished. An interrupted load is told from a finished one by what is absent. stamp is any strictly increasing integer.
FactProgram.render() The finished program, as text.
FactLoad.comment(text="") One line of the header every program starts with.
FactLoad.q(s) As FactProgram.q.
FactLoad.put(attr, value) As FactProgram.put.
FactLoad.declare(name, body) Names a table and the loop body that writes each of its entries. Declaring it again with the same body changes nothing; a different body is refused.
FactLoad.add(rows) One group of entries, by table name, that lands in a single program. A group too big for a program of its own is refused, never split.
FactLoad.table(name, rows, body) Declares the table and adds each entry on its own, so a split may fall between any two.
FactLoad.load_marker(key, stamp, kind="Load") A load marker in every program, under <key>_<n>, which also records how many programs the load was split into.
FactLoad.render() The finished programs, as a list, in the order they should run. A load with no entries is still one program, which records its marker.
Ids.get(qual) The subject for the qualified name qual. Ask in the same order on every run, and a re-run lands on the same subjects.

28.3 dke.client — sending a program

from dke.client import Client. It speaks the published wire, the MCP Reference, and every field it reads is one that page names.

Name What it is
dke.client.Client(key, *, endpoint=DEFAULT_ENDPOINT, store_id=None, timeout=30.0, client_name="dkepy", client_version=dke.__version__) A connection to one store, keyed by a store key (dke_s_). The key addresses exactly one store, so store_id selects nothing.
dke.client.Result One completed call, with its envelope taken apart. A call that completed and failed is still a Result: a compile error is a normal step in writing a program, not an exception.
dke.client.DkeError The base of everything this module raises.
dke.client.TransportError The request never reached the tools: HTTP was not 200, or nothing answered. status is the HTTP status and code the service’s short token, when there was one. A redirect is refused, not followed, and raised as this, because following one would send your key on.
dke.client.ProtocolError The service answered and refused the request; code is the wire’s error code.
dke.client.PaymentRequired A ProtocolError: the account cannot cover the call. Clears by paying.
dke.client.RateLimited A ProtocolError: too many calls. Clears by waiting; retry_after is the seconds the service named, when it named any.
dke.client.DEFAULT_ENDPOINT The published endpoint, https://dke.langsyn.net.
Method What it does
Client.from_env(var="DKE_API_KEY", **kwargs) A client keyed from the environment variable var, raising an error when it is not set. Any other argument is Client’s.
Client.stores() Your store’s id, size and created-at. It touches nothing, so it is the call to make first.
Client.run(source, *, edition="python", language="eng") Runs a program without storing it. The facts it records persist.
Client.compile(name, source=None, *, version=None, edition="python", language="eng") Compiles a module and keeps it. Leave out source to reload the stored one.
Client.list(target="scripts") Stored script signatures, standing-rule names, or the kinds the store holds.
Client.info(name) One script’s compile provenance, by module.script.
Client.forget(name) Removes one script, by module.script. Removing one that is not there is not an error.
Client.forget_rule(name) Removes one standing rule, by module.rule.
Client.list_modules() Each module’s name, script count and created-at.
Client.info_module(name) One module’s record, by its bare name.
Client.forget_module(name, *, confirm=False) Removes a module and everything it owns, the data in its kinds included. It needs confirm=True, and only a standing checkpoint brings the module back.
Client.checkup() Whether the store meets the reasoning standing over it.
Client.spec(topic=None, *, edition="python") The language contract, whole or one topic.
Client.tools() The tool names the endpoint serves, asked of the endpoint rather than remembered.
Client.call(tool, arguments=None) Any tool by name, including one newer than this client.

A Result carries the call’s response envelope taken apart:

Field or method What it is
Result.ok True exactly when status is success, so it is the one thing to route on.
Result.status The call’s _meta.status.
Result.text The text of the answer.
Result.ops The call’s _meta.ops, the operations it consumed (MCP Reference §7).
Result.transcript The call’s _meta.transcript.
Result.diagnostics The call’s _meta.diagnostics: what to fix, when a program did not compile.
Result.refuse_reason The call’s _meta.refuse_reason, when the call was declined.
Result.meta The whole _meta.
Result.raw The whole result, as it arrived.
Result.raise_for_status() Returns the result when the work succeeded, and raises DkeError when it did not, leading with the status and the first diagnostic or the refusal.
Result.value(name) The value bound to name, from the transcript, spelled as you wrote it. Raises KeyError when nothing was bound by that name.
Result.prints() Everything the program printed, in order.

28.4 dke.keys — a store key on your own machine

from dke.keys import mint, with the keys extra installed. It is what dkekey (§29) does, as a library, and MCP Reference §3.1 specifies both. The key is made on your machine, and only its hash and a wrap of the store’s data key are sent: never the key, the key that opened the store, or a password. A wrong argument raises ValueError before anything is sent.

Name What it is
dke.keys.mint(store, *, label, level="read", email=None, password=None, token=None, open_with=None, api=DEFAULT_API, timeout=60.0) Makes a key for store, listed in the dashboard as label and able to do what level names, and returns a MintedKey. Sign in with email and password, which opens the store with the account’s customer key unless open_with names another key; or with an account token, which opens no store by itself, so open_with is needed too. open_with is a key that already opens the store: one of its store keys or the customer key.
dke.keys.MintedKey The key that was made.
dke.keys.MintError A DkeError: the account service refused, or the store would not open with what was given. status and code are the service’s, when it answered with them.
dke.keys.LEVELS The levels a key may be made at: read, read-add, read-write and read-write-delete.
dke.keys.KEY_FORMAT The format the wrap is made in, sent with every key so that the account service can refuse one it does not take.
dke.keys.DEFAULT_API The account service mint signs in to.
Field What it is
MintedKey.key The key itself, shown nowhere else: keep it. It is left out of the object’s printed form, so a log line that prints the object never carries it.
MintedKey.id The key’s id.
MintedKey.store The store it opens.
MintedKey.level The level it was made at.
MintedKey.label Its name in the dashboard.

28.5 Readers

Readers for the domains of three published modules. Each turns what it reads into programs built on §28.1, and each is also the worked example for a reader of your own: yours supplies the entries, and everything from there on is the same.

Command Reads Writes facts for
dke-ingest-python <path>… Python source, files or directories code
dke-ingest-cpp <path>… C++ source, files or directories code
dke-quantum-device <dir> --max-bytes N a device’s calibration snapshot quantum
dke-pqc-cbom <cbom.json> --max-bytes N a CycloneDX cryptographic bill of materials, version 1.6 or later pqc

Each command runs its reader’s main, so a program can call a reader as a library too. The two code readers are submodules of dke.ingest, one per source language; dke-quantum-device runs dke.quantum.device, and dke-pqc-cbom runs dke.pqc.cbom.

The two code readers take -o PATH to write somewhere other than standard output and --exclude TEXT, repeatable, to skip paths containing TEXT. They exit non-zero for a path that does not exist and for a read that found nothing, so an empty load is never taken for a successful one; dke.ingest.core names the statuses. The other two take --module, the name your store holds the module under, and dke-pqc-cbom takes --source, the provenance every claim carries. --max-bytes is the program size your store accepts, as for FactLoad.

dke.quantum.qasm reads OpenQASM 2.0 into the gate lists quantum runs and writes one back, and dke.quantum.provenance computes the digests quantum’s rules compare. Neither has a command; Library Specification §4.5 states both.


29. dkekey — a store key without Python

dkekey is a single program for Linux, macOS and Windows that issues a store key on your own machine, for a program that has no browser: CI, an agent, a server. It is served with its source and signed builds at https://dke.langsyn.com/source/dkekey/. What it does and the key it makes are specified by MCP Reference §3.1, and dke.keys (§28.4) does the same from Python.


Companion documents: the Library Tutorial; the Library Specification, which states the same surface as a contract and governs where the two disagree; and the Python Reference for the language these modules are written in.

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, its section 11.1.

  • 1.0.5 · rev 171 (2026-11-15) — §6 names quantum 1.3.1, which states its version on its __version__ line, so a compile given no version records 1.3.1, and whose weights comment says each probability is the exact sum of its squared parts rounded once, as vector.sumsq computes it since rev 165, where it named fsum. No answer changed. The Version line states API 1.5: a module may state its version on a top-level __version__ line, which a compile given no version records and info_module reports (Python Reference §9.1), and an addition moves the API’s MINOR.
  • 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), 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), 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), 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) — §9 physics 1.0.3, §10 chemistry 1.0.3, §11 biology 1.0.2, §12 astronomy 1.0.2, §13 geography 1.0.2 and §14 mathematics 1.0.2: every value script and computed field answers wherever its value is a real, where a step of its work past the range of a real answered inf, nan, 0.0 or a wrong solution, as biology.logistic_exact(-50.0, 50.0, 50.0, 50.0) answered nan for a population at its capacity and mathematics.solve([[1e300, 1e300], [1e-300, -1e-300]], [1e300, 0.0]) answered [1.0, -0.0] for [0.5, 0.5]. Answers whose work stayed in range keep their digits, except where the new form is measured closer to the exact value: chemistry.equilibrium_exact (worst 89 million units in the last digit at ordinary constants before, 3 now), an angle past 360 degrees, taken modulo 360 first (a median of 1,251 units in its sine before, 1 now), and physics.Quantity.converted, which takes the ratio of the factors first, gives a value expected in its own unit back as it was and differs in its last digit at about three in ten conversions. Three failures at ordinary inputs are fixed: physics.pendulum released near the top answered 0.0, biology.hardy_weinberg refused a sample of one homozygote, and astronomy.kepler stopped short for a mean anomaly below -π or a few turns out (kepler(1e6, 0.9) answered 986901.85 after 64 steps, and 999999.16 now). Inputs outside a formula’s domain are refused. Every example’s output is unchanged.
  • rev 144 (2026-11-12) — §16 earthscience 1.0.2, §19 colour 1.0.1, §20 body 1.0.2, §21 sound 1.0.4, §22 finance 1.0.2, §23 food 1.0.3 and §24 architecture 1.0.2: every value script and computed field answers wherever its value is a real, where a sum, square, product or ratio in its work answered inf, nan or 0.0 past the range of a real, as architecture.metacentric_height(1e300, 1e300, 1.0) and sound.level_of_pressure(1e300, 1e-300) did. Answers whose work stayed in range keep their digits, except where the new form is measured closer to the exact value: the monthly payment, through expm1 and log1p (worst 55,462 units in the last digit over 20,000 loans before, 3 now), the effective and continuous rates, so that 0.12 compounded once gives 0.12, a lens’s magnification as f / (f - u) (worst 860 units before, 1 now), a lean or an incidence past 360 degrees, taken modulo 360 first, and the body mass index, divided by the height twice, which differs in its last digit at about a third of inputs and is as accurate. §22’s example totals and rate move to their new digits. level_of_pressure and at_distance refuse a value of zero or below, dew_point a humidity outside 0 to 100, and monthly_payment, compound_amount and effective_annual_rate a negative number of months or periods a year of zero or below.
  • rev 143 (2026-11-12) — §15 statistics 1.0.3: every value script gives the statistic wherever it is a real. binom_pmf and poisson_pmf answered inf, nan or 0.0 where the work passed the range of a real, as binom_pmf(1100, 550, 0.5) and poisson_pmf(1000, 1000.0) did; past the range of their products they are now Loader’s saddle-point forms, through the new stirling_error, deviance and exact_product. mean, variance, std_dev, median, regression, welch_t and z_score scale huge or tiny values by a power of two, by the new scale_of, and bayes and chi_square keep their products in range. binom_pmf, binom_cdf, poisson_pmf, bayes, chi_square, stirling_error and deviance refuse a figure outside their domain with ValueError. Every answer whose work stayed in range is unchanged, except the binomial’s: its power of 1 - p is now corrected for the rounding of 1 - p, and its coefficient is run over the smaller of k and n - k, which brings it from 36 units in its last digit at most by 56 trials to 3, so a few last digits move toward the exact value. A store holding 1.0.2 keeps the old answers; compile 1.0.3 to take the fix.
  • rev 142 (2026-11-12) — §21 sound 1.0.3: add_levels adds two levels as the louder plus 10 log10(1 + 10^(-d/10)), d the difference, so no power passes the largest real. Since this revision a real past the range is inf where it was refused, and the sum of two powers past it would have made add_levels(3100.0, 3100.0) answer inf for a level of 3103.0103. A store holding 1.0.2 answers inf there; compile 1.0.3 to take the fix. Over ordinary levels the answer is the exact one rounded more often than the sum of powers was, so a few differ from 1.0.2 in the last digit.
  • rev 133 (2026-11-10) — Eleven modules no longer add a fact two sources give into a sum: §7 pqc 1.0.2, §8 harmony 1.0.1, §9 physics 1.0.2, §10 chemistry 1.0.2, §11 biology 1.0.1, §13 geography 1.0.1, §14 mathematics 1.0.1, §15 statistics 1.0.2, §17 computing 1.0.1, §21 sound 1.0.2 and §24 architecture 1.0.1. A sum adds every value it reads, so a member a second source binds to it, even with the same value, was added twice, and a module gave a total and a verdict no source supports. Each sum now counts the members that carry a value into it and the claims they hold, over one binding; where the claims outnumber the members, the sum is disputed and not concluded, and neither is what rests on it. The reader of a disputed sum refuses with ValueError, naming the claim to forget, and a query names it: disputed_rooms, disputed_codes, disputed_days, disputed_equations, disputed_groups, disputed_molecules, disputed_reactions and disputed_studies are new, a bar is named by rhythm_faults() and a number by numbers_that_are("disputed"). A member with no value of the sum adds nothing and hides no dispute. chemistry’s formula, and the unexplained of biology and the holds of geography, count each member once however many claims or paths reach it, and §7 states that a use’s deadline is the earliest year of any value of any deadline. Where each fact is written once, every module answers as before.
  • rev 132 (2026-11-10) — §6 names quantum 1.3.0, which lets a reversible circuit measure. ("mx", [q], 0.0) measures qubit q in the X basis and leaves it 0, its outcome one bit of the case being checked; x_if, cnot_if, ccx_if, z_if, cz_if, ccz_if, swap_if and cswap_if run their gate only where the latest mx of their first qubit gave 1. rev_meets holds a circuit over every assignment of the outcomes, so an AND computed by a Toffoli gate can be uncomputed by a measurement and a cz, and answers the count of cases, 2len(ins) times 2 for each mx, with the case i the input i mod 2len(ins) under the outcomes i div 2**len(ins). rev_same refuses a circuit that measures. rev_apply_under and rev_measures are new helpers. A circuit with no mx is answered as 1.2.0 answered it.
  • rev 131 (2026-11-10) — §6 names quantum 1.2.0, which adds the Toffoli, CCZ and Fredkin gates as steps (ccx, ccz and cswap), and two exact checks of a reversible circuit, made of x, cnot, ccx, swap and cswap with the signs z, cz and ccz, on every input at any width. rev_same says whether two circuits are equal: on every input they give the same output bits under one sign shared by all inputs, so a sign that differs on some inputs only is refused and one that differs on all of them is a global phase and is not. rev_meets says whether a circuit meets a specification: its inputs unchanged, its outputs as given, every other qubit back at 0 and the sign as given. Each answers whether it holds, how many inputs it checked, the first input that fails and the reason. rev_code, rev_apply and lowest_lane are the helpers under them. Everything 1.1.0 answered, 1.2.0 answers the same.
  • rev 130 (2026-11-10) — §25 describes elections 1.0.0, which is not legal or electoral advice, in the same four parts as the modules before it. How you get one is now §26, What it costs §27, dkepy §28 and dkekey §29.
  • rev 129 (2026-11-09) — §24 describes architecture 1.0.0, which is not engineering, naval-architecture, acoustic-design or heritage advice, in the same four parts as the modules before it. How you get one is now §25, What it costs §26, dkepy §27 and dkekey §28.
  • rev 126 (2026-11-09) — §23 food is at 1.0.2 and credits FoodSafety.gov’s chart “Cook to a Safe Minimum Internal Temperature”, reviewed November 21, 2024. casserole is a casserole with meat or poultry, or meatless, and chart() writes eleven standards, with ham-packaged at 140 F beside ham-precooked.
  • rev 125 (2026-11-09) — §12 astronomy, §16 earthscience and §20 body are at 1.0.1, and each names the readers that refuse while sources disagree, which it had said none did: up() and window() in astronomy, dryness_threshold() and category() in earthscience, and bmi() in body. computing was right: no read of it refuses.
  • rev 124 (2026-11-09) — §21 to §23 describe sound 1.0.1, finance 1.0.1 and food 1.0.1, whose unsettled() names a dispute over any field you write, and say that dose, total_of and total refuse a cell that holds two values.
  • rev 123 (2026-11-08) — statistics is at 1.0.1. Its comments now say what the module does: pooled() and predictive_values() refuse where a figure is absent, and monte_carlo_pi is shown at 10,000 points, since a run on the service refuses at 20,000. No query changed. The library also carries calendars, colour and body at 1.0.0, each on the source surface. §18 describes calendars, §19 colour and §20 body: the kinds each declares, the fields you write, the fields it derives for you, its queries and what its answers can and cannot mean. §20 says that body is not medical advice. The library also carries sound, finance and food at 1.0.0: §21 describes sound, which is not occupational-health advice, §22 finance, which is not financial advice, and §23 food, which is not food-safety or medical advice, each in the same four parts. How you get one is now §24, What it costs §25, dkepy §26 and dkekey §27.
  • rev 122 (2026-11-08) — The library carries statistics, earthscience and computing at 1.0.0, each on the source surface. §15 describes statistics, §16 earthscience and §17 computing: the kinds each declares, the fields you write, the fields it derives for you, its queries and what its answers can and cannot mean. How you get one is now §18, What it costs §19, dkepy §20 and dkekey §21.
  • rev 121 (2026-11-08) — The library carries astronomy, geography and mathematics at 1.0.0, each on the source surface. §12 describes astronomy, §13 geography and §14 mathematics: the kinds each declares, the fields you write, the fields it derives for you, its queries and what its answers can and cannot mean. How you get one is now §15, What it costs §16, dkepy §17 and dkekey §18.
  • rev 119 (2026-11-08) — §9 and §10 describe physics 1.0.1 and chemistry 1.0.1, whose unsettled() names a dispute over any field you write. At 1.0.0 it named a dispute only over the fields a number is read from, so oxygen recorded with a valence of one beside its usual two made water H3O while nothing was named.
  • rev 118 (2026-11-08) — §9 is new: physics 1.0.0, which converts quantities to SI, names a quantity handed to a reader who expects another unit, finds the dimensions an equation leaves over and adds resistors in series and in parallel, and calculates a pendulum, an orbit, the heat equation, a linear system and its bridge, and the Ising model. §10 is new: chemistry 1.0.0, which gives each atom of a molecule drawn as atoms and bonds its hydrogens, names its formula, weight and functional groups and a reaction’s enthalpy by Hess’s law, and calculates molar masses, balanced equations, pH and kinetics. §11 is new: biology 1.0.0, which names what a metabolic network can make from its medium and which one compound would let it make another, and a pedigree’s ancestors, carriers and what its mode of inheritance cannot explain, and calculates translation in two genetic codes, crosses, chi-square, populations, drift and repressor rings. How you get one is now §12, What it costs §13, dkepy §14 and dkekey §15.
  • rev 117 (2026-11-08) — §8.4 says which marks are the exception to a disagreement being reasoned about from both values: a note is outside the key, and a chord read with its fifth omitted, only when no value on record supplies what the mark asks to be missing.
  • rev 116 (2026-11-08) — §8 is new: harmony 1.0.0, which marks four-part writing (consecutive fifths and octaves, crossed voices, spacing, range, notes outside the key and each chord’s reading), checks a melody’s rhythm against its bars, and calculates frequencies, cents, set classes, twelve-tone matrices and Euclidean rhythms. The sections after it move up by one: How you get one is §9, What it costs §10, dkepy §11 and dkekey §12.
  • rev 114 (2026-11-06) — §6 names quantum 1.1.0, which adds a second generator, ChaCha20 as RFC 8439 defines it, from a 256-bit key: chacha_quarter, chacha_block, chacha_unit and chacha_uniform, and advance_chacha, which measures with its values. §6.4 says which generator can stand behind a secret, and that a key travels to the service. Everything 1.0.3 answered, 1.1.0 answers the same.
  • rev 112 (2026-11-06) — §6 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) — An optional record a store never holds leaves every other answer the same. Client: the key addresses exactly one store, so store_id selects nothing; forget_module needs confirm=True, and only a standing checkpoint brings the module back.
  • rev 31 (2026-10-19) — This is the DKE Library’s Reference, served at /ref/dke/library/ beside the Library Specification; the old address leads here. §10 lists the public names of dkepy: its package root, how a program is built, dke.client, dke.keys and the readers. §11 places dkekey. The header and the introduction say so, and cross-references name the Library Tutorial. The Status line says what the document is.
  • rev 30 (2026-10-17) — code moves to 1.12.3, whose source changes one comment in impact_of and nothing the module declares or 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, so the MIT grant travels with the file: code 1.12.2, identity 1.0.2, argument 1.0.2, agenda 1.0.1, quantum 1.0.1 and pqc 1.0.1. The pqc section’s sentence on what dke.pqc.cbom writes reads as a sentence.
  • rev 26 (2026-10-17) — The header gains an Audience and states Version, Status, Audience and the Markdown copy, one field to a line. §9: a module carries no separate charge, and the companion documents include the Library Specification, which states the same surface as a contract and governs where the two disagree.
  • rev 24 (2026-10-16) — Two modules join the library: §6, quantum, which simulates a quantum circuit and reasons about one, the devices it runs on and the results computed about it; and §7, pqc, which reasons about moving off quantum-vulnerable cryptography. How you get one is now §8 and what it costs §9. §1 names the three modules that declare standing constraints, where it named agenda alone.
  • rev 22 (2026-10-14) — The modules 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 Version line carries a Status, as the other references do. Cross-references take the published documents’ short names, the ones the footer uses — Modules Tutorial, Python 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.