subsequence.sequence_utils ========================== .. py:module:: subsequence.sequence_utils .. autoapi-nested-parse:: Utility functions for generating and transforming step sequences. Provides algorithms for rhythm generation (Euclidean, Bresenham, van der Corput), sequence manipulation (rotate, legato, probability gate), and general-purpose generative helpers (random walk, weighted choice, shuffled choices, scale/clamp). Module Contents --------------- .. py:class:: Sieve(predicate: Callable[[int], bool]) A composable Xenakis sieve — residual classes under ``&`` ``|`` ``~``. The full algebra layer over :func:`sieve`: build a :class:`Sieve` with :func:`residual_class` (alias ``rc``) and combine with union (``|``), intersection (``&``), and complement (``~``), then :meth:`evaluate` over a range. Membership is decided per integer, so complement is well-defined without a range until evaluation. .. rubric:: Example .. code-block:: python from subsequence.sequence_utils import residual_class as rc ((rc(2, 0) | rc(3, 0)) & ~rc(4, 1)).evaluate(hi=24) Wrap a membership predicate (use :func:`residual_class` to make one). .. py:method:: evaluate(hi: int, lo: int = 0) -> List[int] The sorted integers in ``[lo, hi)`` that belong to this sieve. .. py:function:: branch_sequence(pitches: List[int], depth: int = 2, path: int = 0, mutation: float = 0.0, rng: Optional[random.Random] = None) -> List[int] Navigate a fractal tree of pitch-sequence transforms and return one variation. The ``pitches`` sequence is the trunk. At each of ``depth`` levels two transforms are assigned deterministically (derived from the pitch content itself, so the tree is identical for the same trunk regardless of any seed), and ``path`` selects left or right per level — ``2 ** depth`` variations before the index wraps. The transforms are order and interval operations (retrograde, inversion, transposition, rotation, interval compression/expansion): deliberately rhythm-free, so the caller owns timing. Feed the result to ``Motif.notes()`` for a storable variation, or let ``p.branch()`` place it directly. :param pitches: The trunk — absolute MIDI pitches. All variations derive from this. :param depth: Branching levels (``2 ** depth`` variations). :param path: Which variation (0-based; wraps modulo ``2 ** depth``). :param mutation: Probability per step of substituting a random trunk pitch on top of the deterministic branching. :param rng: Random source for ``mutation`` draws only (the tree itself is deterministic). Unseeded when omitted. :returns: A pitch list the same length as the trunk. .. py:function:: build_metric_weights(time_signature: Tuple[int, int] = (4, 4), grid: int = 16) -> List[float] Per-step metric weights for one bar — how "strong" each grid position is. The hierarchy: the downbeat is 1.0; the half-bar (even meters only) is 0.75; other beats are 0.5; off-beat eighths are 0.25; everything finer is 0.125. Derived from the time signature by default; pass a custom weight list instead wherever a metric table is accepted (additive and non-isochronous meters define their own strong beats). :param time_signature: ``(beats_per_bar, beat_unit)`` — only the beat count shapes the table. :param grid: Number of equal steps across the bar. :returns: ``grid`` floats in step order. .. rubric:: Example .. code-block:: python build_metric_weights((4, 4), grid=8) # → [1.0, 0.25, 0.5, 0.25, 0.75, 0.25, 0.5, 0.25] .. py:function:: choke(sequence: List[T], against: Optional[Sequence[Any]] = None, steps: Optional[Iterable[int]] = None, floor: Any = 0) -> List[T] Suppress the steps where a selector is active, keeping the rest. The complement of :func:`mask`: replaces ``sequence[i]`` with ``floor`` where the selector is **active**, and keeps it everywhere else. This is the classic drum *choke* — one voice silences another on the steps it sounds. Give the selector as **exactly one** of ``against`` (a parallel part, active where truthy) or ``steps`` (a collection of active step indices), with the same repeat-last / ignore-out-of-range / empty rules as :func:`mask`. ``choke(seq, against=other)`` is the same as masking by the complement of ``other``. :param sequence: The per-step data to gate. :param against: A parallel part; steps are suppressed where it is truthy. Mutually exclusive with ``steps``. :param steps: Step indices to suppress. Mutually exclusive with ``against``. :param floor: The value written to suppressed steps (default ``0``). :returns: A new list the length of ``sequence``. :raises ValueError: If neither or both of ``against`` and ``steps`` are given. .. rubric:: Example .. code-block:: python # Closed hat chokes the open hat wherever the closed hat sounds. open_hat = subsequence.sequence_utils.choke(open_hat_density, against=closed_hat_density) # Drum 2 ducks out on the steps drum 1 already fired. drum_2 = subsequence.sequence_utils.choke(drum_2_density, steps=fired_steps) .. py:function:: clamp(value: float, low: float = 0.0, high: float = 1.0) -> float clamp(value: List[float], low: float = 0.0, high: float = 1.0) -> List[float] Bound a value (or list) to the range ``[low, high]``. Returns ``max(low, min(high, value))`` element-wise. This is the plain, list-aware clamp — distinct from :func:`scale_clamp`, which *rescales* from one range to another before clamping. Defaults to ``[0, 1]``, the usual density range; pass ``low``/``high`` for any other scale. Assumes ``low <= high``. ``value`` may be a single float or a list; the return matches that shape. ``low`` and ``high`` are single scalars. An empty list yields an empty list. :param value: A number to bound, or a list of them. :param low: The lower bound (default ``0.0``). :param high: The upper bound (default ``1.0``). :returns: A float when ``value`` is a float, otherwise a list of floats. .. rubric:: Example .. code-block:: python # Keep a hand-scaled density field inside [0, 1] after arithmetic. safe = subsequence.sequence_utils.clamp([d * 1.4 for d in kick_density]) # Bound a computed velocity to the MIDI range. vel = subsequence.sequence_utils.clamp(raw_velocity, 0, 127) .. py:function:: combine_densities(layers: List[Union[float, List[float]]], strategy: str = 'geomean') -> Union[float, List[float]] Blend several density layers into one consensus density. Takes a list of density ``layers`` — each a single value in ``[0, 1]`` or a per-step list in ``[0, 1]`` — and reduces them, step by step, to one density value or list. This is the "agree on how busy this bar should be" stage; its output is meant to feed the ``amount`` of :func:`density_warp`. The ``strategy`` picks the reducer (all preserve ``[0, 1]`` for valid inputs, so no clamping is applied): - ``"geomean"`` (default) — the geometric mean, ``prod(values) ** (1/N)``. A gentle consensus where any near-zero layer pulls the result down. - ``"min"`` — the most restrictive layer wins (a strict gate). - ``"mean"`` — the plain arithmetic average (a balanced vote). - ``"product"`` — multiply them all (stacks toward sparse fast). Broadcasting generalises the rule in :func:`density_warp`: if every layer is a single value the result is a single value; if any layer is a list the result is a list as long as the longest layer, with scalar layers applied to every step and shorter lists extended by repeating their last value. An empty list layer yields ``[]``. :param layers: The density layers to blend. Each entry is a value in ``[0, 1]`` or a per-step list of such values. Must be non-empty. :param strategy: Reducer name — ``"geomean"`` (default), ``"min"``, ``"mean"``, or ``"product"``. :returns: A float when every layer is a float, otherwise a list of floats. :raises ValueError: If ``layers`` is empty, or ``strategy`` is unknown. .. rubric:: Example .. code-block:: python # Blend a metric accent curve, an intensity envelope, and a global knob # into a consensus, then warp a base probability by it. accent = subsequence.sequence_utils.build_metric_weights(16) envelope = subsequence.easing.ramp(16, 0.2, 0.9, "ease_in_out") consensus = subsequence.sequence_utils.combine_densities( [accent, envelope, 0.6], strategy="geomean") probs = subsequence.sequence_utils.density_warp(0.5, consensus) .. py:function:: constrained_walk(graph: subsequence.weighted_graph.WeightedGraph[T], start: T, length: int, rng: random.Random, pins: Optional[Dict[int, T]] = None, end: Optional[T] = None, avoid: Optional[Collection[T]] = None, weight_modifier: Optional[Callable[[T, T, int], float]] = None, before_choice: Optional[Callable[[T], None]] = None, after_choice: Optional[Callable[[T], None]] = None) -> List[T] Walk a weighted graph under constraints — the shared hybrid kernel. A backward **feasibility** pass (boolean reachability of every pin, so unsatisfiability is known before a note is emitted) followed by a forward walk through the graph's **real, possibly history-dependent** weights masked to surviving candidates. This guarantees *satisfaction*, not an exact conditional distribution — history-dependent weighting (NIR, gravity, diversity) keeps its character rather than being flattened. Positions are **1-based** (the musician count); position 1 is *start*, which is fixed — pins may name it only redundantly. ``avoid`` applies to the chosen positions (2..length); the start is exempt. With no constraints, the walk consumes the RNG exactly as repeated ``graph.choose_next()`` calls would (one draw per step, same candidate order), so an unconstrained walk reproduces the unconstrained engine. :param graph: The transition graph. :param start: The node at position 1. :param length: Total walk length, including *start*. :param rng: Random stream for the weighted draws. :param pins: ``{position: node}`` — the node that MUST sound at a 1-based position. :param end: The node at the final position — sugar for ``pins[length]``. :param avoid: Nodes excluded from every chosen position. Naming a node the graph does not contain is allowed (trivially satisfied). :param weight_modifier: ``fn(source, target, weight) -> float`` — the engine's soft weighting, applied inside the feasible mask. If it suppresses every feasible candidate at some step (all modifiers <= 0), the step falls back to the unmodified weights (stall detection) — satisfaction beats character. :param before_choice: Called with the source node before each draw — the seam for history bookkeeping (append the source to history, as the live engine's ``step()`` does, so *weight_modifier* sees the same context it would live). :param after_choice: Called with each chosen node after its draw. :returns: The walked nodes, ``[start, ...]``, exactly *length* long. :raises ValueError: If the constraints are contradictory or unsatisfiable — before any RNG draw, naming the failing position. .. py:function:: cseg(pitches: Sequence[float]) -> List[int] Contour segment: each pitch's rank within the line (Morris's CSEG). The melodic shape abstracted from exact pitch: ``[60, 67, 64]`` and ``[50, 59, 55]`` both rank ``[0, 2, 1]``. Equal pitches share a rank. .. rubric:: Example .. code-block:: python cseg([60, 67, 64]) # → [0, 2, 1] cseg([5, 5, 3]) # → [1, 1, 0] .. py:function:: csim(a: Sequence[float], b: Sequence[float]) -> float Contour similarity between two equal-length lines (Marvin/Laprade CSIM). The fraction of pairwise order relations (above/below/equal) the two contours share — 1.0 for identical shapes, regardless of exact pitch. Raises ``ValueError`` for mismatched lengths (similarity between different-length contours is not defined here). .. py:function:: de_bruijn(k: int, n: int) -> List[int] Generate a de Bruijn sequence B(k, n). A de Bruijn sequence over an alphabet of size ``k`` with window ``n`` is a cyclic sequence in which every possible subsequence of length ``n`` appears exactly once. The output length is ``k ** n``. Uses Martin's algorithm (Lyndon word decomposition), which produces a lexicographically minimal sequence with no external dependencies. :param k: Alphabet size (number of distinct symbols, e.g. 2 for binary or ``len(pitches)`` for a pitch list). :param n: Window size. Every ``n``-gram over the ``k`` symbols appears exactly once in the cyclic output. :returns: List of integers in ``[0, k-1]`` of length ``k ** n``. .. rubric:: Example .. code-block:: python # Map each integer to a pitch index indices = subsequence.sequence_utils.de_bruijn(3, 2) # length 9 pitches = [60, 62, 64] melody = [pitches[i] for i in indices] .. py:function:: density_spread(value: float, amount: float, midpoint: float = 0.5) -> float density_spread(value: List[float], amount: Union[float, List[float]], midpoint: float = 0.5) -> List[float] density_spread(value: float, amount: List[float], midpoint: float = 0.5) -> List[float] Expand or contract a probability/density about a fixed anchor. Where :func:`density_warp` *shifts* a value denser or sparser, this *scales* the spread of values around an anchor ``midpoint`` — the contrast twin of the warp. ``amount = 0.5`` is the identity; above 0.5 **expands** the spread (pushes values away from the anchor, toward 0 and 1); below 0.5 **contracts** it (pulls values toward the anchor). A ``value`` equal to ``midpoint`` never moves, and the output is always in ``[0, 1]``. The map scales each value's log-odds deviation from the anchor by a gain ``k = amount/(1-amount)``, so the odds compound as ``odds(out) = odds(value)**k * odds(midpoint)**(1-k)``. Handy points: ``amount = 0.75`` triples the spread (``k = 3``), ``0.25`` thirds it (``k = 1/3``); ``amount`` and ``1 - amount`` are exact inverses (contract then expand-by-complement returns the input). The spread is even in log-odds, not in linear distance: it stretches the mid-range most and barely moves values already near 0 or 1, and a strong expand pins near-rail values onto the rails (the same saturation :func:`density_warp` has at ``amount`` near 0 or 1). ``value`` may be a single float or a list; the return matches that shape. ``amount`` may likewise be a single float (applied to every element) or a per-step list. The result is a list whenever either is a list; on unequal lengths the shorter repeats its last value, and an empty list yields an empty list. ``midpoint`` is a single anchor in the open interval ``(0, 1)``. :param value: A probability/density in ``[0, 1]``, or a list of them. :param amount: The spread knob in ``[0, 1]`` — ``0.5`` is identity, ``>0.5`` expands, ``<0.5`` contracts. A float spreads every element uniformly; a list spreads per step. :param midpoint: The fixed anchor in ``(0, 1)`` that the spread pivots around (default ``0.5``). Values stay on their own side of it. :returns: A float when ``value`` and ``amount`` are both floats, otherwise a list of floats. :raises ValueError: If ``midpoint`` is not strictly between 0 and 1 (an anchor on a rail has no defined log-odds). .. rubric:: Example .. code-block:: python # Sharpen the contrast of a per-step accent profile around 0.5. accents = [0.55, 0.3, 0.7, 0.45, 0.8, 0.25, 0.6, 0.4] sharp = subsequence.sequence_utils.density_spread(accents, 0.75) # values above 0.5 pushed up, below pushed down — same centre, wider spread. .. py:function:: density_to_steps(density: float, rng: random.Random, length: int) -> List[int] density_to_steps(density: List[float], rng: random.Random, length: Optional[int] = None) -> List[int] Roll each step against its density and return the fired step indices. Walks the grid and, for each step, draws a fresh random number and keeps that step when the draw falls below the step's density — an independent weighted coin per step. Returns the **list of fired step indices**, ready to feed ``p.sequence(steps=...)`` or ``hit_steps`` and per-step comprehensions over those indices. This is the named form of the hand-written ``[i for i in range(n) if rng.random() < density[i]]``. It is the *stochastic* member of the density-gate family: :func:`threshold` is the deterministic gate, :func:`probability_gate` thins an already-binary sequence and returns a parallel 0/1 list, and ``density_to_steps`` takes a pure density profile (floats in ``[0, 1]``) and emits the sparse set of survivors as indices — no ``[1] * n`` base and no :func:`sequence_to_indices` bridge needed. ``density`` may be a per-step list (its length sets the grid) or a single float applied uniformly, in which case ``length`` is **required** — a bare probability has no grid of its own. A density at or above ``1.0`` always fires; at or below ``0.0`` never fires. Pass a seeded ``random.Random`` (or the pattern's ``p.rng``) for reproducible output. :param density: A per-step density list (floats in ``[0, 1]``), or a single float applied to every step (then ``length`` is required). :param rng: The seeded random generator — pass the pattern's ``p.rng``. :param length: The grid size when ``density`` is a scalar. Ignored for a list density (the list's own length is used). :returns: The fired step indices in ascending order — possibly empty. An empty result is normal: a sparse profile may fire nothing on a given cycle. :raises ValueError: If ``density`` is a scalar and ``length`` is not given — there is no grid to roll against. .. rubric:: Example .. code-block:: python # A per-step kick profile rolled into concrete hits. profile = [0.9, 0.1, 0.5, 0.1, 0.8, 0.1, 0.4, 0.1] steps = subsequence.sequence_utils.density_to_steps(profile, p.rng) velocities = [40 + int(profile[i] * 30) for i in steps] p.sequence(steps=steps, pitches="kick", velocities=velocities) # A uniform 30% across a 16-step bar needs an explicit length. ghosts = subsequence.sequence_utils.density_to_steps(0.3, p.rng, length=16) .. py:function:: density_warp(value: float, amount: float) -> float density_warp(value: List[float], amount: Union[float, List[float]]) -> List[float] density_warp(value: float, amount: List[float]) -> List[float] Warp a probability/density by a single denser/sparser knob. Pushes a probability ``value`` in ``[0, 1]`` toward 1.0 (denser) or toward 0.0 (sparser) with one knob ``amount`` in ``[0, 1]``. ``amount = 0.5`` is the identity and returns ``value`` unchanged; above 0.5 thickens, below 0.5 thins. The output is always in ``[0, 1]``. The map is ``W = (value*amount) / (value*amount + (1-value)*(1-amount))`` — the logistic of ``logit(value) + logit(amount)``. Because the log-odds add, warps **stack**: two warps equal one whose knobs combine by summing their log-odds. ``amount = 1`` forces a full ``1.0`` and ``amount = 0`` a full ``0.0`` for any ``value`` strictly between 0 and 1. ``value`` may be a single float or a list; the return matches that shape. ``amount`` may likewise be a single float (applied to every element) or a per-step list (e.g. a Perlin density field). The result is a list whenever either argument is a list; when both are lists of unequal length the result has the length of the longer, the shorter extended by repeating its last value. An empty list yields an empty list. :param value: A probability/density in ``[0, 1]``, or a list of them. :param amount: The warp knob in ``[0, 1]`` — ``0.5`` is identity, ``>0.5`` denser, ``<0.5`` sparser. A float warps every element uniformly; a list warps per step. :returns: A float when both arguments are floats, otherwise a list of floats. .. rubric:: Example .. code-block:: python # A per-step kick profile, thickened by one density knob. profile = [0.9, 0.1, 0.5, 0.1, 0.8, 0.1, 0.4, 0.1] dense = subsequence.sequence_utils.density_warp(profile, 0.7) # dense is a list in [0, 1] — use as per-step probabilities or velocities. .. py:function:: displace(sequence: List[T], amount: int) -> List[T] Phase-shift a per-step pattern by a whole number of steps, wrapping. Moves every element of ``sequence`` along by ``amount`` positions and wraps the steps that fall off one end back round to the other — the classic rhythm *necklace* rotation (metric displacement). A **positive** ``amount`` pushes the pattern **later** (to the right): the hit at step 0 in ``[1, 0, 0, 0]`` lands on step 1. A negative ``amount`` pulls it earlier. ``amount`` is taken modulo the length, so a whole revolution (or zero) returns the pattern unchanged and an over-length shift simply wraps. Works on any per-step data — a 0/1 rhythm, a density profile, a velocity or note list — since it only reorders the existing values. This is a different operation from :func:`rotate`, which adds ``shift`` to the integer *values* of a step-index list; ``displace`` moves the *positions* within a value list. :param sequence: The per-step pattern to phase-shift. :param amount: Steps to move later (positive) or earlier (negative); reduced modulo ``len(sequence)``. :returns: A new list the same length as ``sequence`` (the input is never mutated), or ``[]`` when ``sequence`` is empty. .. rubric:: Example .. code-block:: python # Push a Euclidean kick one step later in the bar. kick = subsequence.sequence_utils.generate_euclidean_sequence(8, 3) delayed = subsequence.sequence_utils.displace(kick, 1) steps = subsequence.sequence_utils.sequence_to_indices(delayed) .. py:function:: fibonacci_rhythm(steps: int, length: float = 4.0) -> List[float] Generate beat positions spaced by the golden ratio (Fibonacci spiral). Uses the golden angle method: ``position_i = frac(i * φ) * length``, where ``φ = (1 + √5) / 2 ≈ 1.618``. The result is sorted into ascending order. This distributes events with the maximum possible spread — analogous to how sunflower seeds are arranged — producing a quasi-random but aesthetically pleasing timing distribution that is distinct from both even grids (Euclidean) and pure randomness. :param steps: Number of beat positions to generate. :param length: Total span in beats to fill. Defaults to 4.0 (one bar of 4/4). :returns: Sorted list of ``steps`` float beat positions in ``[0.0, length)``. .. rubric:: Example .. code-block:: python beats = subsequence.sequence_utils.fibonacci_rhythm(8, length=4.0) for beat in beats: p.note(pitch=60, beat=beat, velocity=80, duration=0.2) .. py:function:: flip(value: float, low: float = 0.0, high: float = 1.0) -> float flip(value: List[float], low: float = 0.0, high: float = 1.0) -> List[float] Reflect a value within a range — its complement about the mid-point. Returns ``low + high - value``, mirroring ``value`` to the opposite side of the range ``[low, high]``. With the default ``[0, 1]`` this is ``1 - value`` — the density/probability complement, and a logical NOT for a 0/1 list. Being range-aware it also flips other scales: ``flip(100, 0, 127)`` is ``27``, mirroring a velocity within the MIDI range. ``flip`` is its own inverse and assumes ``value`` lies within ``[low, high]``; it does **not** clamp (compose with :func:`clamp` if the input may stray). ``value`` may be a single float or a list; the return matches that shape. ``low`` and ``high`` are single scalars applied to every element. An empty list yields an empty list. :param value: A number to reflect, or a list of them. :param low: The lower bound of the range (default ``0.0``). :param high: The upper bound of the range (default ``1.0``). :returns: A float when ``value`` is a float, otherwise a list of floats. .. rubric:: Example .. code-block:: python # Turn a kick density into its "everywhere the kick isn't" field. gaps = subsequence.sequence_utils.flip(kick_density) # Mirror a velocity within the MIDI range. soft = subsequence.sequence_utils.flip(100, 0, 127) .. py:function:: generate_bresenham_sequence(steps: int, pulses: int) -> List[int] Generate a rhythm using Bresenham's line algorithm. .. py:function:: generate_bresenham_sequence_weighted(steps: int, weights: List[float]) -> List[int] Generate a sequence that distributes weighted indices across steps. .. py:function:: generate_cellular_automaton_1d(steps: int, rule: int = 30, generation: int = 0, seed: int = 1) -> List[int] Generate a binary sequence using an elementary cellular automaton. Evolves a 1D CA from an initial state for the specified number of generations, returning the final state as a binary rhythm. Each generation the pattern evolves — use ``p.cycle`` as the generation to get a rhythm that changes every bar. Rule 30 produces "structured chaos" — patterns that look random but have hidden self-similarity. Rule 90 produces fractal (Sierpiński triangle) patterns. Rule 110 is Turing-complete. :param steps: Length of the output sequence. :param rule: Wolfram rule number (0–255). Default 30. :param generation: Number of generations to evolve from the initial state. :param seed: Initial state as a bit field. Default 1 (single centre cell). :returns: Binary list of length *steps* (0s and 1s). .. rubric:: Example .. code-block:: python seq = subsequence.sequence_utils.generate_cellular_automaton_1d( 16, rule=30, generation=p.cycle ) indices = subsequence.sequence_utils.sequence_to_indices(seq) p.hit_steps("snare_1", indices, velocity=35) .. py:function:: generate_cellular_automaton_2d(rows: int, cols: int, rule: str = 'B368/S245', generation: int = 0, seed: Union[int, List[List[int]]] = 1, density: float = 0.5) -> List[List[int]] Generate a 2D cellular automaton grid using Life-like rules. Evolves a 2D grid of cells from an initial state using Birth/Survival notation rules. The resulting grid maps rows to pitches or instruments and columns to time steps, producing polyphonic rhythmic patterns. The default rule B368/S245 (Morley/"Move") produces chaotic, active patterns well-suited to generative music. B3/S23 is Conway's Life. :param rows: Number of rows (maps to pitches or instruments). :param cols: Number of columns (maps to time steps / rhythm grid). :param rule: Birth/Survival notation, e.g. ``"B3/S23"`` for Conway's Life, ``"B368/S245"`` for Morley. :param generation: Number of evolution steps to run from the initial seed. :param seed: Initial grid state. ``1`` places a single live cell at the centre. Any other ``int`` seeds a :class:`random.Random` and fills cells with probability *density*. A ``list[list[int]]`` provides an explicit starting grid (must be rows × cols). :param density: Fill probability when *seed* is a random int (0.0–1.0). :returns: 2D grid as a list of lists (rows × cols), each cell 0 or 1. .. rubric:: Example .. code-block:: python grid = subsequence.sequence_utils.generate_cellular_automaton_2d( rows=4, cols=16, rule="B3/S23", generation=p.cycle, seed=42 ) for row_idx, pitch in enumerate([60, 62, 64, 67]): hits = [c for c, v in enumerate(grid[row_idx]) if v] p.hit_steps(pitch, hits, velocity=80) .. py:function:: generate_euclidean_sequence(steps: int, pulses: int) -> List[int] Generate a Euclidean rhythm using Bjorklund's algorithm. .. py:function:: generate_legato_durations(hits: List[int]) -> List[int] Convert a hit list into per-step legato durations. .. py:function:: generate_van_der_corput_sequence(n: int, base: int = 2) -> List[float] Generate a sequence of n numbers using the van der Corput sequence. .. py:function:: logistic_map(r: float, steps: int, x0: float = 0.5) -> List[float] Generate a deterministic chaos sequence using the logistic map. A single parameter ``r`` controls behaviour from stability to chaos: ``r < 3.0`` converges to a fixed point; ``r`` 3.0–3.57 produces periodic oscillations (period-2, -4, -8…); ``r > 3.57`` enters chaos. At ``r ≈ 3.83`` a stable period-3 window briefly returns. Complements :func:`perlin_1d` — use Perlin for smooth organic wandering and logistic_map when you need controllable order-to-chaos behaviour. Feeding logistic_map values into ``hit_steps`` probability or ghost note velocity gives ghost notes that are "the same but never exactly the same." :param r: Growth rate, typically 0.0–4.0. Values outside [0, 4] will cause ``x`` to diverge; clamp externally if needed. :param steps: Number of values to generate. :param x0: Seed value in the open interval (0, 1). Default 0.5. .. rubric:: Example .. code-block:: python # Ghost snare density that hovers at the edge of chaos chaos = subsequence.sequence_utils.logistic_map(r=3.7, steps=16) for i, v in enumerate(chaos): if v > 0.5: p.hit_steps("snare_2", [i], velocity=round(30 + 50 * v), no_overlap=True) .. py:function:: lorenz_attractor(steps: int, dt: float = 0.01, sigma: float = 10.0, rho: float = 28.0, beta: float = 8.0 / 3.0, x0: float = 0.1, y0: float = 0.0, z0: float = 0.0) -> List[Tuple[float, float, float]] Integrate the Lorenz attractor and return normalised (x, y, z) tuples. The Lorenz system is a set of three coupled differential equations originally derived to model atmospheric convection. Its trajectories orbit a butterfly-shaped strange attractor: deterministic yet sensitive to initial conditions, never exactly repeating. The three axes provide independent but correlated modulation sources — ideal for simultaneously shaping pitch, velocity, and duration from a single generative process. Integration uses the Euler method with step ``dt``. Each axis is independently min-max normalised to ``[0.0, 1.0]`` across the full trajectory so that outputs span the full musical range regardless of the chosen parameters. :param steps: Number of points to generate. :param dt: Integration time step. Smaller values produce smoother trajectories. Default 0.01. :param sigma: Lorenz σ parameter. Default 10.0. :param rho: Lorenz ρ parameter. Default 28.0 (classic chaotic regime). :param beta: Lorenz β parameter. Default 8/3. :param x0: Initial x position. Small changes diverge over time. :param y0: Initial y position. :param z0: Initial z position. :returns: List of ``(x, y, z)`` tuples, each component in ``[0.0, 1.0]``. .. rubric:: Example .. code-block:: python points = subsequence.sequence_utils.lorenz_attractor(16, x0=p.cycle * 0.001) pitches = [60, 62, 64, 65, 67, 69, 71, 72] for i, (x, y, z) in enumerate(points): pitch = pitches[int(x * len(pitches)) % len(pitches)] vel = int(40 + y * 87) p.note(pitch=pitch, beat=i * 0.25, velocity=vel, duration=0.05 + z * 0.2) .. py:function:: lsystem_expand(axiom: str, rules: Dict[str, Union[str, List[Tuple[str, float]]]], generations: int, rng: Optional[random.Random] = None) -> str Expand an L-system string by applying production rules. An L-system rewrites every symbol in the current string simultaneously, each generation replacing symbols according to ``rules``. After enough generations the string exhibits self-similarity: its large-scale structure mirrors its small-scale structure — the same property found in natural music, where motifs recur at phrase, section, and movement level. Symbols not present in ``rules`` pass through unchanged (identity rule). Symbols are single characters; each character in the string is one symbol. Rules may be deterministic (a single replacement string) or stochastic (a list of ``(replacement, weight)`` pairs). Stochastic rules require ``rng`` to be provided. .. note:: String length can grow exponentially. A doubling rule applied for 30 generations produces ~1 billion characters. Keep ``generations`` to 3–8 for practical use. :param axiom: Initial string (e.g. ``"A"``). :param rules: Production rules. Deterministic: ``{"A": "AB", "B": "A"}``. Stochastic: ``{"A": [("AB", 3), ("BA", 1)]}`` — weights are relative and do not need to sum to 1. :param generations: Number of rewriting iterations. :param rng: Random number generator. Required when any rule is stochastic; ignored for fully deterministic rule sets. :returns: Expanded string after ``generations`` iterations. :raises ValueError: If stochastic rules are present but ``rng`` is ``None``. .. rubric:: Example .. code-block:: python # Fibonacci rhythm — hits distributed at golden-ratio spacing expanded = subsequence.sequence_utils.lsystem_expand( axiom="A", rules={"A": "AB", "B": "A"}, generations=6 ) # expanded is "ABAABABAABAABABAABABA" (length 21, the gen-6 Fibonacci word) # Stochastic — different output each bar expanded = subsequence.sequence_utils.lsystem_expand( axiom="A", rules={"A": [("AB", 3), ("BA", 1)]}, generations=4, rng=rng, ) .. py:function:: mask(sequence: List[T], against: Optional[Sequence[Any]] = None, steps: Optional[Iterable[int]] = None, zero: Any = 0) -> List[T] Keep the steps where a selector is active, zeroing the rest. Gates ``sequence`` against a selector, keeping ``sequence[i]`` where the selector is **active** and replacing every other step with ``zero``. Give the selector as **exactly one** of: - ``against`` — a parallel part, active where ``against[i]`` is truthy (non-zero). Shorter than ``sequence`` it repeats its last value; an empty one is inactive everywhere. - ``steps`` — a collection of step indices, active at exactly those positions. Indices outside the sequence are ignored. Works on any per-step data — densities, 0/1 gates, notes, velocities — since the kept steps keep their value. Off steps take ``zero`` (default ``0``); pass ``zero=0.0`` to keep a float density profile float. :param sequence: The per-step data to gate. :param against: A parallel part, active where truthy. Mutually exclusive with ``steps``. :param steps: Step indices at which the selector is active. Mutually exclusive with ``against``. :param zero: The value written to inactive steps (default ``0``). :returns: A new list the length of ``sequence``. :raises ValueError: If neither or both of ``against`` and ``steps`` are given. .. rubric:: Example .. code-block:: python # Keep the open hat only on the backbeat accents. accent = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] open_hat = subsequence.sequence_utils.mask(open_hat_density, against=accent) # Or keep only the downbeats, by step index. downs = subsequence.sequence_utils.mask(open_hat_density, steps=[0, 4, 8, 12]) .. py:function:: offbeatness(onsets: Sequence[int], grid: int) -> int How many onsets fall on intrinsically off-beat pulses (Toussaint). The on-beat pulses are the vertices of every regular sub-polygon of the cycle (the divisor meters); the off-beat pulses are exactly those coprime to *grid*. Off-beatness counts onsets landing on coprime positions — a meter-independent syncopation flavour (high for rhythms that fight every even subdivision). :param onsets: 0-based onset indices (reduced modulo *grid*, de-duplicated). :param grid: Pulses in the cycle. :returns: The count of distinct onsets on off-beat (coprime) pulses. .. rubric:: Example .. code-block:: python offbeatness([0, 4, 8, 12], 16) # 0 — all on the strong polygon offbeatness([0, 3, 6, 10, 13], 16) # 2 — bossa, off-beats on pulses 3 and 13 .. py:function:: perlin_1d(x: float, seed: int = 0) -> float Generate smooth 1D noise at position *x*. Returns a value in [0.0, 1.0] that varies smoothly as *x* changes. Same *x* and *seed* always produce the same output. Use to drive density, velocity, or probability parameters that should wander organically over time — the "parameter wandering within boundaries" quality of generative electronic music systems. :param x: Position along the noise field. Use ``bar * scale`` where ``scale`` controls the rate of change (smaller = slower). 0.05–0.1 is good for bar-level wandering. :param seed: Seed for the hash function. Different seeds produce different but equally smooth noise fields. .. rubric:: Example .. code-block:: python # Smooth density that wanders over bars density = subsequence.sequence_utils.perlin_1d(p.cycle * 0.08, seed=42) p.bresenham("snare_1", pulses=max(1, round(density * 6)), velocity=35, no_overlap=True) .. py:function:: perlin_1d_sequence(start: float, spacing: float, count: int, seed: int = 0) -> List[float] Generate a sequence of smooth 1D noise values. Equivalent to calling :func:`perlin_1d` *count* times at evenly-spaced positions, but expressed as a single call. Every value is in [0.0, 1.0]. :param start: Position of the first sample in the noise field. Typically ``p.bar * p.grid * scale`` to anchor the sequence to an absolute position in the piece. :param spacing: Distance between consecutive samples. Matches the ``scale`` factor used in single calls — e.g. ``0.1`` gives the same per-sample change as ``perlin_1d(i * 0.1, seed)``. :param count: Number of values to return. :param seed: Noise field seed. Same seed as a matching :func:`perlin_1d` call produces identical values at the same positions. .. rubric:: Example .. code-block:: python # 16 smoothly-varying velocities for hi-hat ghost notes noise = subsequence.sequence_utils.perlin_1d_sequence( start = p.bar * p.grid * 0.1, spacing = 0.1, count = p.grid, seed = 10 ) hat_velocities = [ int(subsequence.easing.map_value(n, out_min=50, out_max=75, shape="ease_in")) for n in noise ] .. py:function:: perlin_2d(x: float, y: float, seed: int = 0) -> float Generate smooth 2D noise at position *(x, y)*. Returns a value in [0.0, 1.0] that varies smoothly as *x* and *y* change. Same coordinates and *seed* always produce the same output. Use to drive correlated parameters that should weave around each other organically over time, or for spatialized parameter wandering. :param x: Position along the X axis of the noise field. :param y: Position along the Y axis of the noise field. :param seed: Seed for the hash function. Different seeds produce different but equally smooth noise fields. .. rubric:: Example .. code-block:: python # Two parameters wandering smoothly but with related movement. # By locking X to time and slightly separating Y, the two values # will move in a correlated, organic dance over the bars. density = subsequence.sequence_utils.perlin_2d(p.cycle * 0.08, 0.0, seed=42) velocity = subsequence.sequence_utils.perlin_2d(p.cycle * 0.08, 0.5, seed=42) .. py:function:: perlin_2d_grid(x_start: float, y_start: float, x_step: float, y_step: float, x_count: int, y_count: int, seed: int = 0) -> List[List[float]] Generate a 2D grid of smooth noise values. Returns a list of ``y_count`` rows, each containing ``x_count`` values in [0.0, 1.0]. Access as ``grid[row][col]``. Equivalent to calling :func:`perlin_2d` for every *(x, y)* position in the grid. :param x_start: Starting X position. :param y_start: Starting Y position. :param x_step: Spacing between columns. :param y_step: Spacing between rows. :param x_count: Number of columns (samples along X). :param y_count: Number of rows (samples along Y). :param seed: Noise field seed. .. rubric:: Example .. code-block:: python # 4x4 noise grid — rows are bars, columns are steps grid = subsequence.sequence_utils.perlin_2d_grid( x_start = p.bar * 0.1, y_start = 0.0, x_step = 0.1, y_step = 0.25, x_count = 4, y_count = 4, seed = 5, ) # e.g. drive density of four voices independently for row, voice in enumerate(["kick", "snare", "hi_hat_closed", "clap"]): density = sum(grid[row]) / len(grid[row]) p.ghost_fill(voice, density=density, velocity=(20, 50)) .. py:function:: pink_noise(steps: int, sources: int = 16, seed: int = 0) -> List[float] Generate a 1/f (pink) noise sequence using the Voss-McCartney algorithm. Pink noise has equal energy per octave — it contains both slow drift and fast jitter in a single signal, matching how musical parameters naturally vary. Voss and Clarke (1978) showed that pitch and loudness fluctuations in real music follow 1/f statistics. Sits between :func:`perlin_1d` (smooth, predictable) and :func:`logistic_map` (chaos, controllable order-to-randomness): use pink noise when you want statistically "natural" variation without tuning octave weights manually. :param steps: Number of output samples. :param sources: Number of independent random sources. More sources extend the low-frequency range. Default 16 is a good general value. :param seed: RNG seed. Same seed always produces the same sequence. :returns: List of floats in [0.0, 1.0]. .. rubric:: Example .. code-block:: python # Humanise hi-hat velocity with pink noise noise = subsequence.sequence_utils.pink_noise(steps=p.grid, seed=p.bar) for i, level in enumerate(noise): if level > 0.3: p.hit_steps("hi_hat_closed", [i], velocity=round(40 + 50 * level), no_overlap=True) .. py:function:: probability_gate(sequence: List[int], probability: Union[float, List[float]], rng: random.Random) -> List[int] Filter a binary sequence by probability. Each active step (value > 0) is kept with the given probability. Inactive steps (value == 0) are never promoted. :param sequence: Binary sequence (0s and 1s) :param probability: Chance of keeping each hit (0.0–1.0). A single float applies uniformly; a list assigns per-step probability. :param rng: Random number generator instance .. rubric:: Example .. code-block:: python seq = subsequence.sequence_utils.generate_euclidean_sequence(16, 7) gated = subsequence.sequence_utils.probability_gate(seq, 0.7, p.rng) p.hit_steps("hh", subsequence.sequence_utils.sequence_to_indices(gated)) .. py:function:: random_walk(n: int, low: int, high: int, step: int, rng: random.Random, start: Optional[int] = None) -> List[int] Generate values that drift by small steps within a range. Each value moves up or down by at most ``step`` from the previous, clamped to ``[low, high]``. Similar to Max/MSP's ``drunk`` object. :param n: Number of values to generate :param low: Minimum value (inclusive) :param high: Maximum value (inclusive) :param step: Maximum change per step :param rng: Random number generator instance :param start: Starting value (default: midpoint of range) .. rubric:: Example .. code-block:: python # Wandering velocity for 16 hi-hat steps walk = subsequence.sequence_utils.random_walk(16, low=50, high=110, step=15, rng=p.rng) .. py:function:: reaction_diffusion_1d(width: int, steps: int = 1000, feed_rate: float = 0.055, kill_rate: float = 0.062, du: float = 0.16, dv: float = 0.08) -> List[float] Simulate a 1D Gray-Scott reaction-diffusion system. The Gray-Scott model describes two interacting chemicals U and V on a 1D ring. V is introduced as a small seed in the centre; the simulation evolves until a stable spatial pattern forms. The resulting V-concentration profile — spots, stripes, or travelling waves depending on the feed/kill parameters — is returned as a normalised float sequence. This is fundamentally different from cellular automata: the state is continuous, the update rule is a PDE (not a binary function), and the patterns are governed by diffusion rates rather than neighbour counts. The spatial structure maps naturally to a rhythm grid. :param width: Number of cells (maps to the pattern's step grid). :param steps: Number of simulation iterations. More steps = more developed pattern. Default 1000. :param feed_rate: Rate at which U is replenished. Default 0.055. :param kill_rate: Rate at which V is removed. Default 0.062. :param du: Diffusion rate for U. Default 0.16. :param dv: Diffusion rate for V. Default 0.08. :returns: List of ``width`` floats in ``[0.0, 1.0]`` representing final V concentration. .. rubric:: Example .. code-block:: python # Use as a rhythm grid — threshold to place notes conc = subsequence.sequence_utils.reaction_diffusion_1d(16, steps=2000) hits = [i for i, v in enumerate(conc) if v > 0.5] p.hit_steps("kick_1", hits, velocity=90) .. py:function:: residual_class(modulus: int, residue: int) -> Sieve A single residual class ``{x : x % modulus == residue}`` as a :class:`Sieve`. The atom of sieve algebra (Xenakis's notation ``modulus @ residue``). Combine with ``&`` ``|`` ``~`` and call :meth:`Sieve.evaluate`. .. py:function:: rhythmic_evenness(onsets: Sequence[int], grid: int, normalize: bool = True) -> float How evenly onsets are spread around the cycle (Toussaint's evenness). Places the *grid* pulses as equally spaced points on a unit circle and sums the chord lengths between every pair of onsets (``2·sin(π·d/grid)`` for a pulse distance ``d``). A maximally even rhythm (a regular polygon — the Euclidean rhythms) maximises this sum; clustered onsets minimise it. :param onsets: 0-based onset indices (duplicates and out-of-range values are reduced modulo *grid* and de-duplicated). :param grid: Pulses in the cycle. :param normalize: Divide by the maximally-even sum for *k* onsets, giving ``(0, 1]`` (1.0 = perfectly even). ``False`` returns the raw sum. :returns: The evenness, in ``(0, 1]`` when normalised (1.0 for fewer than two onsets). .. rubric:: Example .. code-block:: python rhythmic_evenness([0, 3, 6], 8) # tresillo — near 1.0 rhythmic_evenness([0, 1, 2], 8) # clustered — much lower .. py:function:: rotate(indices: List[int], shift: int, length: int) -> List[int] Circularly rotate step indices by the specified amount, wrapping at *length*. .. py:function:: scale_clamp(value: float, in_min: float, in_max: float, out_min: float = 0.0, out_max: float = 1.0) -> float Scale a value from an input range to an output range and clamp the result. Maps a value from [in_min, in_max] to [out_min, out_max]. If the result falls outside the output range, it is clamped to the nearest bound. Correctly handles reversed ranges (where min > max). :param value: The number to scale and clamp. :param in_min: The start of the input range. :param in_max: The end of the input range. :param out_min: The start of the target output range (default: 0.0). :param out_max: The end of the target output range (default: 1.0). .. rubric:: Example .. code-block:: python # Scale sensor data (0-1023) to a probability (0.0-1.0) prob = subsequence.sequence_utils.scale_clamp(sensor_val, 0, 1023) # Invert a MIDI CC (0-127) to a volume multiplier (1.0-0.0) vol = subsequence.sequence_utils.scale_clamp(cc_val, 0, 127, 1.0, 0.0) .. py:function:: self_avoiding_walk(n: int, low: int, high: int, rng: random.Random, start: Optional[int] = None) -> List[int] Generate a self-avoiding random walk on an integer lattice. Unlike :func:`random_walk`, which can revisit any position, this walk tracks all visited positions and avoids them. Each step moves ±1 to an unvisited neighbour. When the walk is trapped (all neighbours have been visited), the visited set is reset at the current position and the walk continues — this creates natural phrase boundaries with a fresh sense of direction after each reset. The constraint guarantees pitch diversity: within each "phrase" (before a reset), no pitch is repeated and the melody explores the range in a continuous, step-wise manner. :param n: Number of values to generate. :param low: Minimum value (inclusive). :param high: Maximum value (inclusive). :param rng: Random number generator instance. :param start: Starting value. Defaults to the midpoint of ``[low, high]``. :returns: List of ``n`` integers in ``[low, high]``. .. rubric:: Example .. code-block:: python # Self-avoiding bassline across a 2-octave range (MIDI 40–64) notes = subsequence.sequence_utils.self_avoiding_walk(16, low=40, high=64, rng=p.rng) .. py:function:: sequence_to_indices(sequence: List[int]) -> List[int] Extract step indices where hits occur in a binary sequence. .. py:function:: shuffled_choices(pool: List[T], n: int, rng: random.Random) -> List[T] Choose N items from a pool with no immediate repetition. Within each pass through the pool, every item appears before any repeats. Across reshuffles, the last item of one pass is never the first of the next. Similar to Max/MSP's ``urn`` object. The no-immediate-repeat guarantee assumes the pool values are distinct — a pool containing duplicates (e.g. ``[100, 100, 80]``) can still place equal values back to back. :param pool: Items to choose from :param n: Number of items to return :param rng: Random number generator instance .. rubric:: Example .. code-block:: python # 16 velocity values with no adjacent repeats vels = subsequence.sequence_utils.shuffled_choices([70, 85, 100, 110], 16, p.rng) .. py:function:: sieve(classes: Sequence[Tuple[int, int]], hi: int, lo: int = 0) -> List[int] Xenakis sieve: the sorted integers in ``[lo, hi)`` in any of the classes. A sieve (Xenakis's *crible*) is a logical formula over **residual classes** that denotes a subset of the integers. This primary form takes a list of ``(modulus, residue)`` pairs and returns their **union** over a bounded range — every ``x`` in ``[lo, hi)`` with ``x % modulus == residue`` for at least one class. The integers index *any* ordered parameter, so one kernel builds custom scales (over 0–11 semitones), non-octave pitch pools, rhythm grids, and bar-selection masks. For intersection and complement, compose :func:`residual_class` objects with ``&``, ``|``, ``~`` and evaluate the result (see :class:`Sieve`). :param classes: ``(modulus, residue)`` pairs. ``modulus`` must be ≥ 1; the residue is taken modulo the modulus. :param hi: Exclusive upper bound. :param lo: Inclusive lower bound (default 0). :returns: The sorted, de-duplicated integers in range that satisfy any class. :raises ValueError: If a modulus is below 1. .. rubric:: Example .. code-block:: python sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12) # → [0, 2, 4, 5, 7, 9, 11] — the major scale as a sieve sieve([(2, 0)], hi=12) # → [0, 2, 4, 6, 8, 10] — whole-tone sieve([(5, 0), (7, 1)], lo=60, hi=96) # a non-octave pitch pool .. py:function:: syncopation(onsets: Sequence[int], grid: int, time_signature: Tuple[int, int] = (4, 4), weights: Optional[Sequence[float]] = None) -> float How much a rhythm pulls away from its metric strong points. A weighted off-beat measure: each onset contributes ``max_weight − weight_at_its_pulse`` from the metric-weight table (so onsets on the downbeat add nothing, onsets on the weakest pulses add the most), normalised by the onset count. Uses :func:`build_metric_weights` by default; pass a custom per-pulse *weights* list for additive or non-isochronous meters. :param onsets: 0-based onset indices (reduced modulo *grid*, de-duplicated). :param grid: Pulses in the cycle. :param time_signature: Shapes the default weight table. :param weights: Optional explicit per-pulse weights (length *grid*). :returns: Mean weight deficit per onset, in ``[0, max_weight − min_weight]`` (0.0 only when every onset sits on the downbeat, or there are none; the downbeat alone carries the maximum weight, so even on-beat rhythms score a little above 0). .. rubric:: Example .. code-block:: python syncopation([0], 16) # 0.0 — the downbeat only syncopation([0, 4, 8, 12], 16) # low — on the beats, but beat 1 is strongest syncopation([3, 7, 11, 15], 16) # high — every onset on a weak pulse .. py:function:: threshold(sequence: List[float], cutoff: float = 0.5) -> List[int] Gate a per-step field into a deterministic 0/1 sequence. Returns ``1`` where ``sequence[i] > cutoff`` (strict) and ``0`` otherwise — the deterministic counterpart to :func:`probability_gate`'s random roll, matching the idiom ``cutoff < x``. Pair with :func:`sequence_to_indices` to turn the result into the firing step indices. This is a sequence operation: it takes a list and returns a list of ints. An empty sequence yields an empty list. :param sequence: A per-step field (e.g. a density profile in ``[0, 1]``). :param cutoff: The exclusive threshold; steps strictly above it fire (default ``0.5``). :returns: A list of ``0`` / ``1`` ints the length of ``sequence``. .. rubric:: Example .. code-block:: python # Place closed-hat hits wherever the density clears the half-way line. gate = subsequence.sequence_utils.threshold(hh_closed_density, 0.5) steps = subsequence.sequence_utils.sequence_to_indices(gate) .. py:function:: thue_morse(n: int) -> List[int] Generate the Thue-Morse sequence. The Thue-Morse sequence is an infinite aperiodic binary sequence defined by ``t(i) = popcount(i) mod 2`` — the parity of the number of 1-bits in ``i``. It is perfectly balanced (equal density of 0s and 1s over any power-of-two window) and overlap-free: no subsequence occurs three times consecutively. It is self-similar but never strictly periodic, making it useful for rhythmic patterns that feel structured yet avoid monotony. :param n: Number of values to generate. :returns: Binary list of length ``n`` (0s and 1s). .. rubric:: Example .. code-block:: python # 16-step Thue-Morse rhythm — first 8 values: 0 1 1 0 1 0 0 1 seq = subsequence.sequence_utils.thue_morse(16) .. py:function:: tile(sequence: List[T], length: int) -> List[T] Cycle a sequence to an exact length. Repeats ``sequence`` end-to-end and truncates so the result is exactly ``length`` items long — ``tile([1, 0, 0], 8)`` gives ``[1, 0, 0, 1, 0, 0, 1, 0]``. Reach for it when the target length is not a whole multiple of the pattern; for an exact multiple, plain ``[1, 0, 0] * 3`` reads more clearly. Works on any per-step data — a rhythm, a density profile, notes, velocities. :param sequence: The pattern to repeat. Must be non-empty when ``length > 0``. :param length: The exact length of the result. :returns: A new list of exactly ``length`` items, or ``[]`` when ``length <= 0``. :raises ValueError: If ``sequence`` is empty and ``length > 0`` — there is nothing to cycle. .. rubric:: Example .. code-block:: python # Stretch a three-step kick cell across a 16-step bar. bar = subsequence.sequence_utils.tile([1, 0, 0], 16) .. py:function:: vl_distance(source: Sequence[int], target: Sequence[int], pitch_classes: bool = True) -> int Voice-leading distance between two chords (Tymoczko's taxicab metric). The smallest total semitone movement that turns *source* into *target*, minimised over every way of assigning source notes to target notes. When the chords have different sizes, notes of the smaller chord may be doubled (every note of both chords takes part — nothing is dropped). :param source: Pitches of the first chord. :param target: Pitches of the second chord. :param pitch_classes: When True (default), pitches are reduced mod 12 and each voice moves by the shortest path around the pitch-class circle (a tritone is 6). When False, pitches are absolute MIDI notes and each voice moves by its literal semitone interval — use this to score concrete voicings rather than abstract chords. :returns: Total semitone movement (an int). .. rubric:: Example .. code-block:: python vl_distance([0, 4, 7], [9, 0, 4]) # C → Am: G moves to A, distance 2 vl_distance([0, 4, 7], [5, 9, 0]) # C → F: distance 3 .. py:function:: warp_stack(value: float, amounts: List[Union[float, List[float]]]) -> float warp_stack(value: List[float], amounts: List[Union[float, List[float]]]) -> List[float] Apply several density knobs to ``value`` so they compound. Folds :func:`density_warp` over ``amounts``: it starts from ``value`` and warps by each knob in turn. Because :func:`density_warp` adds log-odds, stacking knobs equals one warp whose knobs sum in log-odds, so the order of ``amounts`` does not matter and the neutral knob ``0.5`` is a no-op. Each knob may itself be a single value or a per-step list (it inherits :func:`density_warp`'s broadcasting), so you can mix a global knob with per-step envelopes. An empty ``amounts`` list returns ``value`` unchanged. Stacking saturates fast: every knob above ``0.5`` pushes harder toward ``1.0`` (and below toward ``0.0``), so a few strong knobs can pin a sequence almost fully on or off. Treat it like gain-staging — prefer a few gentle knobs, or blend control layers first with :func:`combine_densities` and apply the consensus as one knob. :param value: A probability/density in ``[0, 1]``, or a list of them. :param amounts: The knobs to compound, each a value in ``[0, 1]`` (``0.5`` neutral, ``>0.5`` denser, ``<0.5`` sparser) or a per-step list. :returns: A float when ``value`` and every knob are floats, otherwise a list. .. rubric:: Example .. code-block:: python # Compound a global swell, a humanised drift, and a per-step accent. accent = subsequence.easing.ramp(16, 0.4, 0.8, "ease_in") probs = subsequence.sequence_utils.warp_stack(0.5, [0.7, 0.55, accent]) .. py:function:: weighted_choice(options: List[Tuple[T, float]], rng: random.Random) -> T Pick one item from a list of (value, weight) pairs. Weights are relative - they don't need to sum to 1.0. Higher weight means higher probability of selection. :param options: List of ``(value, weight)`` tuples :param rng: Random number generator instance .. rubric:: Example .. code-block:: python density = subsequence.sequence_utils.weighted_choice([ (3, 0.5), # 3 hits: 50% (5, 0.3), # 5 hits: 30% (7, 0.2), # 7 hits: 20% ], p.rng) p.euclidean("snare", pulses=density)