# Appendix B · Analysis and Set-Theory Toolkit A reference for Subsequence's analysis tools: *measure* a rhythm with Toussaint's metrics, *construct* scales and rhythm grids from Xenakis sieves, *compare* melodic shapes with contour functions, and *register* your own scales and chord qualities so they work everywhere the library accepts a built-in name. (sec-appB-intro)= ## B.0 What this appendix is for The chapters teach you to *make* music; this appendix gives you the small set of functions that let you **reason about** it numerically — and a pair of registration calls that extend the library's musical vocabulary. None of this is on the beginner path. Reach for it when you want to: - put a number on how *syncopated* or how *even* a rhythm is, so a generator can aim for a target feel ([§B.1](#sec-appB-rhythm)); - build a scale, a non-octave pitch pool, or a rhythm grid from a logical formula over the integers — the experimental composer's primitive ([§B.2](#sec-appB-sieves)); - ask whether two melodies trace the *same shape* even when their exact pitches differ ([§B.3](#sec-appB-contour)); - teach Subsequence a scale or chord quality it doesn't ship, after which `snap_to_scale`, progressions, and voice-leading all understand it ([§B.4](#sec-appB-register)). ```{note} **Almost everything here is a pure function.** It takes lists of numbers and returns lists of numbers — no `Composition`, no MIDI port, no clock. That's why this appendix's examples are runnable {doctest} blocks you can paste straight into a Python prompt: no instrument, no `render()`, no setup. The two exceptions are the `register_*` calls in [§B.4](#sec-appB-register), which change shared library state and so feed the live API. ``` **Where these live.** Two functions are exported at the top level (`subsequence.sieve`, `subsequence.residual_class`) alongside the registration calls (`subsequence.register_scale`, `subsequence.register_chord_quality`). The rest live in `subsequence.sequence_utils` and are imported explicitly: ```{doctest} appB >>> from subsequence.sequence_utils import ( ... syncopation, offbeatness, rhythmic_evenness, build_metric_weights, ... sieve, residual_class, Sieve, cseg, csim, ... ) >>> import subsequence ``` ```{list-table} The toolkit at a glance :header-rows: 1 :widths: 32 24 44 * - Function - Returns - One-line job * - `rhythmic_evenness(onsets, grid)` - `float` in `(0, 1]` - How evenly onsets spread around the cycle. * - `offbeatness(onsets, grid)` - `int` - How many onsets land on coprime (off-beat) pulses. * - `syncopation(onsets, grid)` - `float` - How far onsets pull from the metric strong points. * - `build_metric_weights(ts, grid)` - `List[float]` - The per-step "strength" table the others lean on. * - `sieve(classes, hi, lo)` - `List[int]` - Union of residual classes over a range. * - `residual_class(mod, res)` - `Sieve` - One residual class, composable with `& | ~`. * - `cseg(pitches)` - `List[int]` - The contour (rank order) of a line. * - `csim(a, b)` - `float` in `[0, 1]` - Contour similarity of two equal-length lines. * - `register_scale(name, intervals)` - `None` - Add a custom scale by name. * - `register_chord_quality(name, intervals)` - `None` - Add a custom chord quality by name. ``` (sec-appB-rhythm)= ## B.1 Rhythm metrics (Toussaint) These three functions are lifted from Godfried Toussaint's *The Geometry of Musical Rhythm*. Each takes a rhythm as a list of **onset indices** — the 0-based grid steps that carry a hit, exactly the step lists you write for `hit_steps` in [Chapter 1](01-step-grid) — plus the **grid** (how many equal pulses fill the cycle). They turn a rhythm into a single number, which is what makes them useful as *targets*: a generator can keep proposing rhythms until one scores in the range you want. ```{tip} **Onsets, not a binary mask.** Pass the *positions that sound* — `[0, 3, 6]`, not `[1, 0, 0, 1, 0, 0, 1, 0]`. If you have a binary list from a generator (Chapter 4's `generate_euclidean_sequence` returns one), convert it with `sequence_to_indices` first — that's shown at the end of this section. ``` (sec-appB-evenness)= ### B.1.1 `rhythmic_evenness` — how regular is it? `rhythmic_evenness(onsets, grid, normalize=True)` places the grid's pulses as equally spaced points on a circle and measures how spread out the onsets are around it. A **maximally even** rhythm — a regular polygon, which is exactly what a Euclidean rhythm approximates — scores `1.0`; onsets bunched together score lower. ```{doctest} appB >>> round(rhythmic_evenness([0, 3, 6], 8), 3) # tresillo — almost a triangle 0.983 >>> round(rhythmic_evenness([0, 1, 2], 8), 3) # three hits crammed together 0.567 >>> rhythmic_evenness([0, 2, 4, 6], 8) # a perfect square — maximal 1.0 ``` The default normalises against the most-even arrangement of that *same number* of onsets, so the score is comparable across rhythms with different densities. Pass `normalize=False` for the raw sum of chord lengths (useful only if you are re-deriving the maths): ```{doctest} appB >>> round(rhythmic_evenness([0, 2, 4, 6], 8, normalize=False), 3) 9.657 ``` ```{list-table} `rhythmic_evenness` arguments :header-rows: 1 :widths: 24 18 58 * - Argument - Default - Meaning * - `onsets` - — - 0-based onset indices. Out-of-range and duplicate values are reduced mod `grid` and de-duplicated, so you can pass raw step lists safely. * - `grid` - — - Pulses in the cycle (e.g. `8` for eighth-notes in 4/4, `16` for sixteenths). * - `normalize` - `True` - `True` → score in `(0, 1]` against the maximally-even k-gon. `False` → raw sum. ``` ```{note} Fewer than two onsets is always `1.0` (a single point can't be uneven). The score is **density-relative**: `[0, 4]` in a grid of 8 also scores `1.0`, because two onsets are most-even when antipodal. Compare rhythms with the *same* onset count, or the number flatters sparse rhythms. ``` (sec-appB-offbeatness)= ### B.1.2 `offbeatness` — how many hits fight the meter? `offbeatness(onsets, grid)` counts the onsets that land on **intrinsically off-beat** pulses — the positions *coprime* to the grid. Those are the pulses no regular subdivision of the cycle ever lands on, so an onset there fights every even way of dividing the bar. The result is a plain integer count, and it's meter-independent: it doesn't consult a time signature at all. ```{doctest} appB >>> offbeatness([0, 4, 8, 12], 16) # four-on-the-floor — every hit on-beat 0 >>> offbeatness([0, 2, 4, 6, 8, 10, 12, 14], 16) # straight eighths — still on-beat 0 >>> offbeatness([0, 3, 6, 10, 13], 16) # a bossa-flavoured line 2 ``` In that last rhythm, pulses `3` and `13` are coprime to 16 — they share no factor with it — so they read as genuinely off the grid; the even positions, which line up with a halving of the bar, don't count. The downbeat (pulse 0) is never off-beat. (sec-appB-syncopation)= ### B.1.3 `syncopation` — how far from the strong beats? `syncopation(onsets, grid, time_signature=(4, 4), weights=None)` is the meter-aware measure. It consults a **metric-weight table** (the downbeat is strongest, the half-bar next, then beats, then off-beat eighths, then everything finer) and, for each onset, charges it the *deficit* between the strongest pulse and the pulse it actually sits on. Onsets on the downbeat cost nothing; onsets on the weakest pulses cost the most. The result is the mean deficit per onset. ```{doctest} appB >>> syncopation([0], 16) # the downbeat alone — no pull at all 0.0 >>> syncopation([0, 4, 8, 12], 16) # on the beats, but only beat 1 is strongest 0.3125 >>> round(syncopation([0, 3, 6, 9, 12], 16), 3) # tresillo over 16 — pulls harder 0.6 >>> syncopation([3, 7, 11, 15], 16) # every onset on a weak pulse — maximal 0.875 ``` Because the table depends on the meter, the same onset list scores differently under a different time signature — pass `time_signature=(3, 4)` for a waltz, for instance. For an additive or non-isochronous meter where Subsequence's default hierarchy doesn't apply, hand in your own per-pulse `weights` list (one value per grid step): ```{doctest} appB >>> custom = [1.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.25, 0.0] # a hand-built 8-step table >>> round(syncopation([0, 2, 6], 8, weights=custom), 3) 0.417 ``` (sec-appB-weights)= ### B.1.4 `build_metric_weights` — the table underneath `build_metric_weights(time_signature=(4, 4), grid=16)` is the function `syncopation` calls for its default table, exposed so you can inspect it or build your own variant. It returns one weight per grid step: `1.0` on the downbeat, `0.75` on the half-bar (even meters only), `0.5` on the other beats, `0.25` on off-beat eighths, and `0.125` on anything finer. ```{doctest} appB >>> build_metric_weights((4, 4), grid=8) [1.0, 0.25, 0.5, 0.25, 0.75, 0.25, 0.5, 0.25] >>> build_metric_weights((3, 4), grid=12) [1.0, 0.125, 0.25, 0.125, 0.5, 0.125, 0.25, 0.125, 0.5, 0.125, 0.25, 0.125] ``` Reading the 4/4-over-8 table left to right: step 0 is the downbeat (`1.0`), step 4 is the half-bar (`0.75`), steps 2 and 6 are the other beats (`0.5`), and the odd steps are the weak off-beats (`0.25`). It is exactly the "strength" you'd assign by ear. (sec-appB-measure-generated)= ### B.1.5 Putting it together: scoring a generated rhythm The metrics shine when you turn them on rhythms a *generator* produced. Chapter 4's `generate_euclidean_sequence` returns a binary mask; `sequence_to_indices` turns that into the onset list these functions expect. Here we confirm that the Euclidean rhythm E(3, 8) really is the tresillo, and read off all three metrics at once: ```{doctest} appB >>> from subsequence.sequence_utils import generate_euclidean_sequence, sequence_to_indices >>> mask = generate_euclidean_sequence(8, 3) # 3 pulses spread over 8 steps >>> mask [1, 0, 0, 1, 0, 0, 1, 0] >>> onsets = sequence_to_indices(mask) >>> onsets [0, 3, 6] >>> round(rhythmic_evenness(onsets, 8), 3) # near-maximal — Euclid is even by design 0.983 >>> offbeatness(onsets, 8) # one hit (pulse 3) fights the meter 1 >>> round(syncopation(onsets, 8), 3) 0.417 ``` ```{tip} That `evenness ≈ 0.983` is the signature of a Euclidean rhythm: Bjorklund's algorithm (Chapter 4) spreads pulses as evenly as an integer grid allows, which is precisely what `rhythmic_evenness` rewards. If you generate a candidate rhythm and its evenness comes back low, it's clustered — useful when *clustered* is the feel you want, and a red flag when it isn't. ``` ```{admonition} Reference :class: seealso {py:func}`~subsequence.sequence_utils.rhythmic_evenness`, {py:func}`~subsequence.sequence_utils.offbeatness`, {py:func}`~subsequence.sequence_utils.syncopation`, {py:func}`~subsequence.sequence_utils.build_metric_weights`, {py:func}`~subsequence.sequence_utils.sequence_to_indices` ``` (sec-appB-sieves)= ## B.2 Xenakis sieves A **sieve** (Iannis Xenakis's *crible*) is a logical formula over the integers that denotes a subset of them. The atoms are **residual classes** — "every integer `x` where `x % modulus == residue`" — combined with union, intersection, and complement. Because the result is just a set of integers, one sieve kernel builds *anything* that indexes an ordered parameter: the semitones of a scale (over 0–11), a non-octave pitch pool (over MIDI numbers), a rhythm grid (over steps), or a bar-selection mask (over bar numbers). Subsequence gives you two entry points: a one-call `sieve()` for the common case (a union of classes over a range), and a composable `Sieve` algebra via `residual_class` for intersection and complement. (sec-appB-sieve-fn)= ### B.2.1 `sieve()` — the one-call union `sieve(classes, hi, lo=0)` takes a list of `(modulus, residue)` pairs and returns the sorted integers in the half-open range `[lo, hi)` that satisfy **any** of them — the union. The major scale, for instance, is the union of seven single-residue classes mod 12: ```{doctest} appB >>> sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12) [0, 2, 4, 5, 7, 9, 11] ``` A single class with a small modulus is a regular pattern. Mod-2-residue-0 is the whole-tone scale; mod-3 over a 16-step grid is a steady dotted-eighth pulse: ```{doctest} appB >>> sieve([(2, 0)], hi=12) # whole-tone scale [0, 2, 4, 6, 8, 10] >>> sieve([(3, 0)], hi=16) # a rhythm: every third sixteenth [0, 3, 6, 9, 12, 15] ``` The `lo` and `hi` bounds let the *same* formula address a register of MIDI notes instead of pitch classes — a **non-octave** pitch pool, the kind of structure sieves were invented to express: ```{doctest} appB >>> sieve([(5, 0), (7, 1)], lo=60, hi=84) # union of two non-octave cycles [60, 64, 65, 70, 71, 75, 78, 80] ``` ```{list-table} `sieve()` arguments :header-rows: 1 :widths: 22 16 62 * - Argument - Default - Meaning * - `classes` - — - List of `(modulus, residue)` pairs. Each `modulus` must be ≥ 1; the residue is taken mod the modulus. The result is their **union**. * - `hi` - — - Exclusive upper bound of the range to evaluate over. * - `lo` - `0` - Inclusive lower bound. ``` ```{warning} A `modulus` below 1 raises `ValueError` — there is no "every 0th integer". The range is **half-open**: `[lo, hi)` includes `lo` and excludes `hi`, exactly like Python's `range()`. So to cover one octave of pitch classes you pass `hi=12`, not `hi=11`. ``` (sec-appB-sieve-algebra)= ### B.2.2 `residual_class` and the `Sieve` algebra `sieve()` only does unions. For **intersection** and **complement** you build a `Sieve` object with `residual_class(modulus, residue)` and combine the pieces with the operators `|` (union), `&` (intersection), and `~` (complement), then call `.evaluate(hi, lo=0)` to read out the integers. ```{doctest} appB >>> rc = residual_class >>> (rc(2, 0) | rc(3, 0)).evaluate(hi=12) # multiples of 2 OR 3 [0, 2, 3, 4, 6, 8, 9, 10] >>> (rc(4, 0) & rc(6, 0)).evaluate(hi=24) # multiples of 4 AND 6 [0, 12] >>> (~rc(2, 0)).evaluate(hi=12) # the complement: odd numbers [1, 3, 5, 7, 9, 11] ``` Because complement is decided one integer at a time, it composes freely with the rest before you ever fix a range — this is the full Xenakis algebra: ```{doctest} appB >>> ((rc(2, 0) | rc(3, 0)) & ~rc(4, 1)).evaluate(hi=24) [0, 2, 3, 4, 6, 8, 10, 12, 14, 15, 16, 18, 20, 22] ``` A `Sieve` also answers a single-integer membership test with `in`, which is handy as a filter inside a loop without evaluating the whole range: ```{doctest} appB >>> 3 in rc(3, 0) True >>> 4 in rc(3, 0) False ``` ```{note} `residual_class` has the short alias `rc` only because we assigned it (`rc = residual_class`) — it isn't a separate import. The library exports `subsequence.sieve` and `subsequence.residual_class` at the top level too, so you can reach them without importing from `sequence_utils`. Don't try to `print()` a `Sieve` directly: it has no readable repr (you'd get a memory address). Always `.evaluate()` or test membership. ``` (sec-appB-sieve-scale)= ### B.2.3 From a sieve to a real scale A sieve over `[0, 12)` *is* a list of semitone offsets — exactly the shape `register_scale` wants ([§B.4](#sec-appB-register)). So you can design a scale as a formula and register the result in one breath: ```{doctest} appB >>> intervals = sieve([(12, 0), (12, 2), (12, 3), (12, 7), (12, 8)], hi=12) >>> intervals [0, 2, 3, 7, 8] >>> subsequence.register_scale("hira_from_sieve", intervals) >>> subsequence.scale_notes("C", "hira_from_sieve", low=60, high=72) [60, 62, 63, 67, 68, 72] ``` That registered name now drives `p.snap_to_scale("C", "hira_from_sieve")` and everything else in [Chapter 5](05-pitch-and-scales), with no further wiring. ```{admonition} Reference :class: seealso {py:func}`~subsequence.sequence_utils.sieve`, {py:func}`~subsequence.sequence_utils.residual_class`, {py:class}`~subsequence.sequence_utils.Sieve` ``` (sec-appB-contour)= ## B.3 Melodic contour (`cseg` / `csim`) Contour functions abstract a melody down to its **shape** — the up/down pattern of its pitches — discarding the exact intervals. Two phrases a perfect-fourth apart, or one in a major key and one in minor, can share an identical contour. This is the formal tool behind "the second phrase answers the first by inverting its shape." (sec-appB-cseg)= ### B.3.1 `cseg` — the contour of a line `cseg(pitches)` replaces each pitch with its **rank** among the distinct pitches in the line: the lowest becomes `0`, the next `1`, and so on. The result — Robert Morris's *contour segment* — is the same for any two lines that rise and fall in the same places, regardless of their actual pitches: ```{doctest} appB >>> cseg([60, 67, 64]) # low, high, middle [0, 2, 1] >>> cseg([50, 59, 55]) # different pitches, same shape [0, 2, 1] ``` Repeated pitches share a rank, so a line that holds and then drops reads cleanly: ```{doctest} appB >>> cseg([5, 5, 3]) # two equal, then lower [1, 1, 0] >>> cseg([]) # an empty line is an empty contour [] ``` (sec-appB-csim)= ### B.3.2 `csim` — how alike are two shapes? `csim(a, b)` scores the **similarity** of two equal-length contours from `0.0` (opposite shapes) to `1.0` (identical), as the fraction of pitch *pairs* whose up/down/equal relationship the two lines agree on (the Marvin–Laprade measure). Like `cseg`, it ignores absolute pitch: ```{doctest} appB >>> csim([60, 67, 64], [50, 59, 55]) # same contour, different register 1.0 >>> csim([60, 62, 64], [64, 62, 60]) # a rising line vs its exact retrograde 0.0 >>> round(csim([60, 64, 62], [60, 62, 64]), 3) # share some, differ on others 0.667 ``` ```{warning} `csim` compares **equal-length** lines and raises `ValueError` otherwise — there is no defined similarity between a four-note and a six-note phrase here. Make the lengths match first (truncate, or compare segment by segment). A single-element (or empty) pair has no pitch pairs to compare and returns `1.0` by convention. ``` ```{tip} Pair these with the motif transforms in [Chapter 8](08-motifs): generate a candidate variation, then keep it only if `csim(original_cseg, variation_cseg)` falls in a window — high enough to feel related, low enough to be a genuine variation. Contour is how you put numbers on "same idea, said differently." ``` ```{admonition} Reference :class: seealso {py:func}`~subsequence.sequence_utils.cseg`, {py:func}`~subsequence.sequence_utils.csim` ``` (sec-appB-register)= ## B.4 Registering custom scales and chord qualities The final two functions don't analyse music — they **extend the library's vocabulary**. Each adds a name to a shared table, and from then on that name works everywhere the corresponding built-in names do. Unlike everything above, they change global state, so they're the live-API members of this appendix. ```{important} **Register once, near the top of your script, before the patterns that use the name.** Both calls mutate a shared registry that the whole library reads. Re-registering the *same* custom name is deliberately allowed and never raises — that's what lets live reload ([Chapter 14](14-live-and-data)) re-run your registration on every file save. Re-defining a *built-in* name, on the other hand, always raises. ``` (sec-appB-register-scale)= ### B.4.1 `register_scale` [Chapter 5](05-pitch-and-scales) introduces `register_scale` for the common case; here is the full surface. The signature is `register_scale(name, intervals, qualities=None)`, and a registered scale immediately drives `p.snap_to_scale`, `scale_notes`, `scale_pitch_classes`, and a `Composition(scale=...)`: ```{doctest} appB >>> subsequence.register_scale("hirajoshi_ref", [0, 2, 3, 7, 8]) >>> from subsequence.intervals import scale_pitch_classes >>> scale_pitch_classes(0, "hirajoshi_ref") # pitch classes from C [0, 2, 3, 7, 8] >>> subsequence.scale_notes("C", "hirajoshi_ref", low=60, high=72) [60, 62, 63, 67, 68, 72] ``` The optional **`qualities`** argument names a chord quality per scale degree, which is what lets the harmony engine of Part IV build *diatonic* chords from your scale. It's unnecessary for pitch-only use (snapping, note pools): ```{doctest} appB >>> subsequence.register_scale( ... "my_penta", ... [0, 2, 4, 7, 9], ... qualities=["major", "minor", "minor", "major", "minor"], ... ) >>> scale_pitch_classes(0, "my_penta") [0, 2, 4, 7, 9] ``` The interval list is validated strictly; breaking a rule raises `ValueError` rather than failing quietly: ```{list-table} `register_scale` interval rules :header-rows: 1 :widths: 40 60 * - Rule - Why * - Must be a non-empty list of whole numbers - Intervals are semitone offsets — no fractions, no empty scale. * - Must start with `0` - The first degree is the root. * - Must ascend strictly (no repeats) - Each degree is a distinct, higher pitch class. * - Every value in `0`–`11` - A scale spans one octave; the engine octave-wraps from there. * - `qualities` (if given) must match the interval count - One chord quality per degree, or none at all. * - `name` must not be a built-in scale - Built-ins like `"minor"`, `"dorian"`, `"hirajoshi"` are protected. ``` ```{doctest} appB >>> subsequence.register_scale("minor", [0, 2, 3, 5, 7, 8, 10]) # built-in name Traceback (most recent call last): ... ValueError: Cannot overwrite built-in scale 'minor'. Choose a different name for your custom scale. >>> subsequence.register_scale("bad", [1, 2, 3]) # doesn't start at 0 Traceback (most recent call last): ... ValueError: intervals must start with 0 ``` (sec-appB-register-chord)= ### B.4.2 `register_chord_quality` The counterpart for harmony: `register_chord_quality(name, intervals, suffix=None)` opens the **chord-quality** table so quartal stacks, clusters, and extended chords become first-class symbolic chords. A registered quality works in `Chord` objects, in progressions ([Chapter 7](07-progressions)), in voice-leading, and in `describe()` output. Intervals are semitone offsets from the root, and may reach past the octave (up to 24) so extensions like the 9th are expressible: ```{doctest} appB >>> subsequence.register_chord_quality("quartal", [0, 5, 10], suffix="q4") >>> chord = subsequence.Chord(root_pc=2, quality="quartal") # D quartal >>> chord.tones(62) # MIDI notes centred on D4 [62, 67, 72] >>> chord.name() 'Dq4' ``` The optional **`suffix`** is what lets the new quality be *parsed* from a chord name. Give it one and `parse_chord` accepts `note + suffix` from then on — so the quality flows into the string-based progression syntax: ```{doctest} appB >>> subsequence.register_chord_quality("minor_9th", [0, 3, 7, 10, 14], suffix="m9") >>> subsequence.parse_chord("Am9") Chord(root_pc=9, quality='minor_9th') >>> subsequence.parse_chord("Am9").tones(57) # A2 up: A C E G B [57, 60, 64, 67, 71] ``` A quality registered *without* a suffix still works everywhere via the `Chord` constructor; it just can't be spelled in a chord-name string, and `name()` prints it in an unambiguous `root(quality)` form rather than pretending to be a triad: ```{doctest} appB >>> subsequence.register_chord_quality("cluster", [0, 1, 2]) # no suffix >>> subsequence.Chord(root_pc=0, quality="cluster").name() 'C(cluster)' ``` ```{list-table} `register_chord_quality` rules :header-rows: 1 :widths: 40 60 * - Rule - Why * - `intervals` start at `0`, ascend strictly, lie in `0`–`24` - Root first; distinct rising tones; extensions may pass the octave. * - `name` must not be a built-in quality - Built-ins like `"minor"`, `"dominant_7th"` are protected. * - `suffix` (if given) must not be a built-in suffix - No clashing with `m`, `7`, `maj7`, … — those already mean something. * - `suffix` must not start with a note letter, accidental, or digit - Otherwise `"Cq4"` couldn't be told apart from a root or extension. ``` ```{doctest} appB >>> subsequence.register_chord_quality("minor", [0, 3, 7]) # built-in name Traceback (most recent call last): ... ValueError: Cannot overwrite built-in chord quality 'minor'. Choose a different name for your custom quality. ``` ```{tip} Compose the two halves of this appendix: design a chord's intervals with a [sieve](#sec-appB-sieves), then hand the result to `register_chord_quality`. A quartal stack is `sieve([(5, 0)], hi=11)` → `[0, 5, 10]`; a cluster is `sieve([(1, 0)], hi=3)` → `[0, 1, 2]`. Sieves *generate* the integer structure; the `register_*` calls *give it a name* the rest of Subsequence understands. ``` ```{admonition} Reference :class: seealso {py:func}`~subsequence.intervals.register_scale`, {py:func}`~subsequence.intervals.scale_pitch_classes`, {py:func}`~subsequence.chords.register_chord_quality` ``` --- **Where to go next.** The rhythm metrics pair naturally with the generators in [Chapter 4](04-generators-euclidean) (score what you generate); sieves and custom scales extend the pitch material of [Chapter 5](05-pitch-and-scales); contour functions complement the motif work of [Chapter 8](08-motifs); and custom chord qualities feed the progressions of [Chapter 7](07-progressions). For the exhaustive, always-current signature list, see [Appendix D](appendix-d-api-reference) and the sequencer's own `api-cheatsheet.md`.