subsequence.sequence_utils

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

class subsequence.sequence_utils.Sieve(predicate: Callable[[int], bool])[source]

A composable Xenakis sieve — residual classes under & | ~.

The full algebra layer over sieve(): build a Sieve with residual_class() (alias rc) and combine with union (|), intersection (&), and complement (~), then evaluate() over a range. Membership is decided per integer, so complement is well-defined without a range until evaluation.

Example

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 residual_class() to make one).

evaluate(hi: int, lo: int = 0) List[int][source]

The sorted integers in [lo, hi) that belong to this sieve.

subsequence.sequence_utils.branch_sequence(pitches: List[int], depth: int = 2, path: int = 0, mutation: float = 0.0, rng: random.Random | None = None) List[int][source]

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.

Parameters:
  • pitches – The trunk — absolute MIDI pitches. All variations derive from this.

  • depth – Branching levels (2 ** depth variations).

  • path – Which variation (0-based; wraps modulo 2 ** depth).

  • mutation – Probability per step of substituting a random trunk pitch on top of the deterministic branching.

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

subsequence.sequence_utils.build_metric_weights(time_signature: Tuple[int, int] = (4, 4), grid: int = 16) List[float][source]

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

Parameters:
  • time_signature(beats_per_bar, beat_unit) — only the beat count shapes the table.

  • grid – Number of equal steps across the bar.

Returns:

grid floats in step order.

Example

build_metric_weights((4, 4), grid=8)
# → [1.0, 0.25, 0.5, 0.25, 0.75, 0.25, 0.5, 0.25]
subsequence.sequence_utils.choke(sequence: List[T], against: Sequence[Any] | None = None, steps: Iterable[int] | None = None, floor: Any = 0) List[T][source]

Suppress the steps where a selector is active, keeping the rest.

The complement of 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 mask().

choke(seq, against=other) is the same as masking by the complement of other.

Parameters:
  • sequence – The per-step data to gate.

  • against – A parallel part; steps are suppressed where it is truthy. Mutually exclusive with steps.

  • steps – Step indices to suppress. Mutually exclusive with against.

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

Example

# 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)
subsequence.sequence_utils.clamp(value: float, low: float = 0.0, high: float = 1.0) float[source]
subsequence.sequence_utils.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 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.

Parameters:
  • value – A number to bound, or a list of them.

  • low – The lower bound (default 0.0).

  • high – The upper bound (default 1.0).

Returns:

A float when value is a float, otherwise a list of floats.

Example

# 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)
subsequence.sequence_utils.combine_densities(layers: List[float | List[float]], strategy: str = 'geomean') float | List[float][source]

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 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 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 [].

Parameters:
  • 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.

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

Example

# 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)
subsequence.sequence_utils.constrained_walk(graph: subsequence.weighted_graph.WeightedGraph[T], start: T, length: int, rng: random.Random, pins: Dict[int, T] | None = None, end: T | None = None, avoid: Collection[T] | None = None, weight_modifier: Callable[[T, T, int], float] | None = None, before_choice: Callable[[T], None] | None = None, after_choice: Callable[[T], None] | None = None) List[T][source]

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.

Parameters:
  • graph – The transition graph.

  • start – The node at position 1.

  • length – Total walk length, including start.

  • rng – Random stream for the weighted draws.

  • pins{position: node} — the node that MUST sound at a 1-based position.

  • end – The node at the final position — sugar for pins[length].

  • avoid – Nodes excluded from every chosen position. Naming a node the graph does not contain is allowed (trivially satisfied).

  • weight_modifierfn(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.

  • 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).

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

subsequence.sequence_utils.cseg(pitches: Sequence[float]) List[int][source]

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.

Example

cseg([60, 67, 64])   # → [0, 2, 1]
cseg([5, 5, 3])      # → [1, 1, 0]
subsequence.sequence_utils.csim(a: Sequence[float], b: Sequence[float]) float[source]

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

subsequence.sequence_utils.de_bruijn(k: int, n: int) List[int][source]

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.

Parameters:
  • k – Alphabet size (number of distinct symbols, e.g. 2 for binary or len(pitches) for a pitch list).

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

Example

# 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]
subsequence.sequence_utils.density_spread(value: float, amount: float, midpoint: float = 0.5) float[source]
subsequence.sequence_utils.density_spread(value: List[float], amount: float | List[float], midpoint: float = 0.5) List[float]
subsequence.sequence_utils.density_spread(value: float, amount: List[float], midpoint: float = 0.5) List[float]

Expand or contract a probability/density about a fixed anchor.

Where 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 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).

Parameters:
  • value – A probability/density in [0, 1], or a list of them.

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

  • 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).

Example

# 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.
subsequence.sequence_utils.density_to_steps(density: float, rng: random.Random, length: int) List[int][source]
subsequence.sequence_utils.density_to_steps(density: List[float], rng: random.Random, length: int | None = 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: threshold() is the deterministic gate, 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 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.

Parameters:
  • density – A per-step density list (floats in [0, 1]), or a single float applied to every step (then length is required).

  • rng – The seeded random generator — pass the pattern’s p.rng.

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

Example

# 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)
subsequence.sequence_utils.density_warp(value: float, amount: float) float[source]
subsequence.sequence_utils.density_warp(value: List[float], amount: float | List[float]) List[float]
subsequence.sequence_utils.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.

Parameters:
  • value – A probability/density in [0, 1], or a list of them.

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

Example

# 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.
subsequence.sequence_utils.displace(sequence: List[T], amount: int) List[T][source]

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 rotate(), which adds shift to the integer values of a step-index list; displace moves the positions within a value list.

Parameters:
  • sequence – The per-step pattern to phase-shift.

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

Example

# 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)
subsequence.sequence_utils.fibonacci_rhythm(steps: int, length: float = 4.0) List[float][source]

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.

Parameters:
  • steps – Number of beat positions to generate.

  • 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).

Example

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)
subsequence.sequence_utils.flip(value: float, low: float = 0.0, high: float = 1.0) float[source]
subsequence.sequence_utils.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 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.

Parameters:
  • value – A number to reflect, or a list of them.

  • low – The lower bound of the range (default 0.0).

  • high – The upper bound of the range (default 1.0).

Returns:

A float when value is a float, otherwise a list of floats.

Example

# 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)
subsequence.sequence_utils.generate_bresenham_sequence(steps: int, pulses: int) List[int][source]

Generate a rhythm using Bresenham’s line algorithm.

subsequence.sequence_utils.generate_bresenham_sequence_weighted(steps: int, weights: List[float]) List[int][source]

Generate a sequence that distributes weighted indices across steps.

subsequence.sequence_utils.generate_cellular_automaton_1d(steps: int, rule: int = 30, generation: int = 0, seed: int = 1) List[int][source]

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.

Parameters:
  • steps – Length of the output sequence.

  • rule – Wolfram rule number (0–255). Default 30.

  • generation – Number of generations to evolve from the initial state.

  • seed – Initial state as a bit field. Default 1 (single centre cell).

Returns:

Binary list of length steps (0s and 1s).

Example

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)
subsequence.sequence_utils.generate_cellular_automaton_2d(rows: int, cols: int, rule: str = 'B368/S245', generation: int = 0, seed: int | List[List[int]] = 1, density: float = 0.5) List[List[int]][source]

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.

Parameters:
  • rows – Number of rows (maps to pitches or instruments).

  • cols – Number of columns (maps to time steps / rhythm grid).

  • rule – Birth/Survival notation, e.g. "B3/S23" for Conway’s Life, "B368/S245" for Morley.

  • generation – Number of evolution steps to run from the initial seed.

  • seed – Initial grid state. 1 places a single live cell at the centre. Any other int seeds a random.Random and fills cells with probability density. A list[list[int]] provides an explicit starting grid (must be rows × cols).

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

Example

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)
subsequence.sequence_utils.generate_euclidean_sequence(steps: int, pulses: int) List[int][source]

Generate a Euclidean rhythm using Bjorklund’s algorithm.

subsequence.sequence_utils.generate_legato_durations(hits: List[int]) List[int][source]

Convert a hit list into per-step legato durations.

subsequence.sequence_utils.generate_van_der_corput_sequence(n: int, base: int = 2) List[float][source]

Generate a sequence of n numbers using the van der Corput sequence.

subsequence.sequence_utils.logistic_map(r: float, steps: int, x0: float = 0.5) List[float][source]

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

Parameters:
  • r – Growth rate, typically 0.0–4.0. Values outside [0, 4] will cause x to diverge; clamp externally if needed.

  • steps – Number of values to generate.

  • x0 – Seed value in the open interval (0, 1). Default 0.5.

Example

# 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)
subsequence.sequence_utils.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]][source]

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.

Parameters:
  • steps – Number of points to generate.

  • dt – Integration time step. Smaller values produce smoother trajectories. Default 0.01.

  • sigma – Lorenz σ parameter. Default 10.0.

  • rho – Lorenz ρ parameter. Default 28.0 (classic chaotic regime).

  • beta – Lorenz β parameter. Default 8/3.

  • x0 – Initial x position. Small changes diverge over time.

  • y0 – Initial y position.

  • z0 – Initial z position.

Returns:

List of (x, y, z) tuples, each component in [0.0, 1.0].

Example

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)
subsequence.sequence_utils.lsystem_expand(axiom: str, rules: Dict[str, str | List[Tuple[str, float]]], generations: int, rng: random.Random | None = None) str[source]

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.

Parameters:
  • axiom – Initial string (e.g. "A").

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

  • generations – Number of rewriting iterations.

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

Example

# 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,
)
subsequence.sequence_utils.mask(sequence: List[T], against: Sequence[Any] | None = None, steps: Iterable[int] | None = None, zero: Any = 0) List[T][source]

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.

Parameters:
  • sequence – The per-step data to gate.

  • against – A parallel part, active where truthy. Mutually exclusive with steps.

  • steps – Step indices at which the selector is active. Mutually exclusive with against.

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

Example

# 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])
subsequence.sequence_utils.offbeatness(onsets: Sequence[int], grid: int) int[source]

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

Parameters:
  • onsets – 0-based onset indices (reduced modulo grid, de-duplicated).

  • grid – Pulses in the cycle.

Returns:

The count of distinct onsets on off-beat (coprime) pulses.

Example

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
subsequence.sequence_utils.perlin_1d(x: float, seed: int = 0) float[source]

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.

Parameters:
  • 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.

  • seed – Seed for the hash function. Different seeds produce different but equally smooth noise fields.

Example

# 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)
subsequence.sequence_utils.perlin_1d_sequence(start: float, spacing: float, count: int, seed: int = 0) List[float][source]

Generate a sequence of smooth 1D noise values.

Equivalent to calling perlin_1d() count times at evenly-spaced positions, but expressed as a single call. Every value is in [0.0, 1.0].

Parameters:
  • 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.

  • 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).

  • count – Number of values to return.

  • seed – Noise field seed. Same seed as a matching perlin_1d() call produces identical values at the same positions.

Example

# 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
]
subsequence.sequence_utils.perlin_2d(x: float, y: float, seed: int = 0) float[source]

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.

Parameters:
  • x – Position along the X axis of the noise field.

  • y – Position along the Y axis of the noise field.

  • seed – Seed for the hash function. Different seeds produce different but equally smooth noise fields.

Example

# 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)
subsequence.sequence_utils.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]][source]

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 perlin_2d() for every (x, y) position in the grid.

Parameters:
  • x_start – Starting X position.

  • y_start – Starting Y position.

  • x_step – Spacing between columns.

  • y_step – Spacing between rows.

  • x_count – Number of columns (samples along X).

  • y_count – Number of rows (samples along Y).

  • seed – Noise field seed.

Example

# 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))
subsequence.sequence_utils.pink_noise(steps: int, sources: int = 16, seed: int = 0) List[float][source]

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 perlin_1d() (smooth, predictable) and logistic_map() (chaos, controllable order-to-randomness): use pink noise when you want statistically “natural” variation without tuning octave weights manually.

Parameters:
  • steps – Number of output samples.

  • sources – Number of independent random sources. More sources extend the low-frequency range. Default 16 is a good general value.

  • seed – RNG seed. Same seed always produces the same sequence.

Returns:

List of floats in [0.0, 1.0].

Example

# 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)
subsequence.sequence_utils.probability_gate(sequence: List[int], probability: float | List[float], rng: random.Random) List[int][source]

Filter a binary sequence by probability.

Each active step (value > 0) is kept with the given probability. Inactive steps (value == 0) are never promoted.

Parameters:
  • sequence – Binary sequence (0s and 1s)

  • probability – Chance of keeping each hit (0.0–1.0). A single float applies uniformly; a list assigns per-step probability.

  • rng – Random number generator instance

Example

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))
subsequence.sequence_utils.random_walk(n: int, low: int, high: int, step: int, rng: random.Random, start: int | None = None) List[int][source]

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.

Parameters:
  • n – Number of values to generate

  • low – Minimum value (inclusive)

  • high – Maximum value (inclusive)

  • step – Maximum change per step

  • rng – Random number generator instance

  • start – Starting value (default: midpoint of range)

Example

# Wandering velocity for 16 hi-hat steps
walk = subsequence.sequence_utils.random_walk(16, low=50, high=110, step=15, rng=p.rng)
subsequence.sequence_utils.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][source]

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.

Parameters:
  • width – Number of cells (maps to the pattern’s step grid).

  • steps – Number of simulation iterations. More steps = more developed pattern. Default 1000.

  • feed_rate – Rate at which U is replenished. Default 0.055.

  • kill_rate – Rate at which V is removed. Default 0.062.

  • du – Diffusion rate for U. Default 0.16.

  • dv – Diffusion rate for V. Default 0.08.

Returns:

List of width floats in [0.0, 1.0] representing final V concentration.

Example

# 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)
subsequence.sequence_utils.residual_class(modulus: int, residue: int) Sieve[source]

A single residual class {x : x % modulus == residue} as a Sieve.

The atom of sieve algebra (Xenakis’s notation modulus @ residue). Combine with & | ~ and call Sieve.evaluate().

subsequence.sequence_utils.rhythmic_evenness(onsets: Sequence[int], grid: int, normalize: bool = True) float[source]

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.

Parameters:
  • onsets – 0-based onset indices (duplicates and out-of-range values are reduced modulo grid and de-duplicated).

  • grid – Pulses in the cycle.

  • 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).

Example

rhythmic_evenness([0, 3, 6], 8)     # tresillo — near 1.0
rhythmic_evenness([0, 1, 2], 8)     # clustered — much lower
subsequence.sequence_utils.rotate(indices: List[int], shift: int, length: int) List[int][source]

Circularly rotate step indices by the specified amount, wrapping at length.

subsequence.sequence_utils.scale_clamp(value: float, in_min: float, in_max: float, out_min: float = 0.0, out_max: float = 1.0) float[source]

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

Parameters:
  • value – The number to scale and clamp.

  • in_min – The start of the input range.

  • in_max – The end of the input range.

  • out_min – The start of the target output range (default: 0.0).

  • out_max – The end of the target output range (default: 1.0).

Example

# 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)
subsequence.sequence_utils.self_avoiding_walk(n: int, low: int, high: int, rng: random.Random, start: int | None = None) List[int][source]

Generate a self-avoiding random walk on an integer lattice.

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

Parameters:
  • n – Number of values to generate.

  • low – Minimum value (inclusive).

  • high – Maximum value (inclusive).

  • rng – Random number generator instance.

  • start – Starting value. Defaults to the midpoint of [low, high].

Returns:

List of n integers in [low, high].

Example

# 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)
subsequence.sequence_utils.sequence_to_indices(sequence: List[int]) List[int][source]

Extract step indices where hits occur in a binary sequence.

subsequence.sequence_utils.shuffled_choices(pool: List[T], n: int, rng: random.Random) List[T][source]

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.

Parameters:
  • pool – Items to choose from

  • n – Number of items to return

  • rng – Random number generator instance

Example

# 16 velocity values with no adjacent repeats
vels = subsequence.sequence_utils.shuffled_choices([70, 85, 100, 110], 16, p.rng)
subsequence.sequence_utils.sieve(classes: Sequence[Tuple[int, int]], hi: int, lo: int = 0) List[int][source]

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 residual_class() objects with &, |, ~ and evaluate the result (see Sieve).

Parameters:
  • classes(modulus, residue) pairs. modulus must be ≥ 1; the residue is taken modulo the modulus.

  • hi – Exclusive upper bound.

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

Example

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
subsequence.sequence_utils.syncopation(onsets: Sequence[int], grid: int, time_signature: Tuple[int, int] = (4, 4), weights: Sequence[float] | None = None) float[source]

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 build_metric_weights() by default; pass a custom per-pulse weights list for additive or non-isochronous meters.

Parameters:
  • onsets – 0-based onset indices (reduced modulo grid, de-duplicated).

  • grid – Pulses in the cycle.

  • time_signature – Shapes the default weight table.

  • 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).

Example

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
subsequence.sequence_utils.threshold(sequence: List[float], cutoff: float = 0.5) List[int][source]

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 probability_gate()’s random roll, matching the idiom cutoff < x. Pair with 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.

Parameters:
  • sequence – A per-step field (e.g. a density profile in [0, 1]).

  • cutoff – The exclusive threshold; steps strictly above it fire (default 0.5).

Returns:

A list of 0 / 1 ints the length of sequence.

Example

# 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)
subsequence.sequence_utils.thue_morse(n: int) List[int][source]

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.

Parameters:

n – Number of values to generate.

Returns:

Binary list of length n (0s and 1s).

Example

# 16-step Thue-Morse rhythm — first 8 values: 0 1 1 0 1 0 0 1
seq = subsequence.sequence_utils.thue_morse(16)
subsequence.sequence_utils.tile(sequence: List[T], length: int) List[T][source]

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.

Parameters:
  • sequence – The pattern to repeat. Must be non-empty when length > 0.

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

Example

# Stretch a three-step kick cell across a 16-step bar.
bar = subsequence.sequence_utils.tile([1, 0, 0], 16)
subsequence.sequence_utils.vl_distance(source: Sequence[int], target: Sequence[int], pitch_classes: bool = True) int[source]

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

Parameters:
  • source – Pitches of the first chord.

  • target – Pitches of the second chord.

  • 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).

Example

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
subsequence.sequence_utils.warp_stack(value: float, amounts: List[float | List[float]]) float[source]
subsequence.sequence_utils.warp_stack(value: List[float], amounts: List[float | List[float]]) List[float]

Apply several density knobs to value so they compound.

Folds density_warp() over amounts: it starts from value and warps by each knob in turn. Because 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 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 combine_densities() and apply the consensus as one knob.

Parameters:
  • value – A probability/density in [0, 1], or a list of them.

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

Example

# 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])
subsequence.sequence_utils.weighted_choice(options: List[Tuple[T, float]], rng: random.Random) T[source]

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.

Parameters:
  • options – List of (value, weight) tuples

  • rng – Random number generator instance

Example

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)