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 aSievewithresidual_class()(aliasrc) and combine with union (|), intersection (&), and complement (~), thenevaluate()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).
- 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
pitchessequence is the trunk. At each ofdepthlevels two transforms are assigned deterministically (derived from the pitch content itself, so the tree is identical for the same trunk regardless of any seed), andpathselects left or right per level —2 ** depthvariations 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 toMotif.notes()for a storable variation, or letp.branch()place it directly.- Parameters:
pitches – The trunk — absolute MIDI pitches. All variations derive from this.
depth – Branching levels (
2 ** depthvariations).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
mutationdraws 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:
gridfloats 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(): replacessequence[i]withfloorwhere 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 ofagainst(a parallel part, active where truthy) orsteps(a collection of active step indices), with the same repeat-last / ignore-out-of-range / empty rules asmask().choke(seq, against=other)is the same as masking by the complement ofother.- 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
againstandstepsare 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 fromscale_clamp(), which rescales from one range to another before clamping. Defaults to[0, 1], the usual density range; passlow/highfor any other scale. Assumeslow <= high.valuemay be a single float or a list; the return matches that shape.lowandhighare 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
valueis 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 theamountofdensity_warp().The
strategypicks 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
layersis empty, orstrategyis 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.
avoidapplies 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_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.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
ValueErrorfor 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
kwith windownis a cyclic sequence in which every possible subsequence of lengthnappears exactly once. The output length isk ** 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 theksymbols appears exactly once in the cyclic output.
- Returns:
List of integers in
[0, k-1]of lengthk ** 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 anchormidpoint— the contrast twin of the warp.amount = 0.5is 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). Avalueequal tomidpointnever 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 asodds(out) = odds(value)**k * odds(midpoint)**(1-k). Handy points:amount = 0.75triples the spread (k = 3),0.25thirds it (k = 1/3);amountand1 - amountare 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 atamountnear 0 or 1).valuemay be a single float or a list; the return matches that shape.amountmay 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.midpointis 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.5is identity,>0.5expands,<0.5contracts. A float spreads every element uniformly; a list spreads per step.midpoint – The fixed anchor in
(0, 1)that the spread pivots around (default0.5). Values stay on their own side of it.
- Returns:
A float when
valueandamountare both floats, otherwise a list of floats.- Raises:
ValueError – If
midpointis 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=...)orhit_stepsand 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, anddensity_to_stepstakes a pure density profile (floats in[0, 1]) and emits the sparse set of survivors as indices — no[1] * nbase and nosequence_to_indices()bridge needed.densitymay be a per-step list (its length sets the grid) or a single float applied uniformly, in which caselengthis required — a bare probability has no grid of its own. A density at or above1.0always fires; at or below0.0never fires. Pass a seededrandom.Random(or the pattern’sp.rng) for reproducible output.- Parameters:
density – A per-step density list (floats in
[0, 1]), or a single float applied to every step (thenlengthis required).rng – The seeded random generator — pass the pattern’s
p.rng.length – The grid size when
densityis 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
densityis a scalar andlengthis 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
valuein[0, 1]toward 1.0 (denser) or toward 0.0 (sparser) with one knobamountin[0, 1].amount = 0.5is the identity and returnsvalueunchanged; 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 oflogit(value) + logit(amount). Because the log-odds add, warps stack: two warps equal one whose knobs combine by summing their log-odds.amount = 1forces a full1.0andamount = 0a full0.0for anyvaluestrictly between 0 and 1.valuemay be a single float or a list; the return matches that shape.amountmay 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.5is identity,>0.5denser,<0.5sparser. 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
sequencealong byamountpositions and wraps the steps that fall off one end back round to the other — the classic rhythm necklace rotation (metric displacement). A positiveamountpushes the pattern later (to the right): the hit at step 0 in[1, 0, 0, 0]lands on step 1. A negativeamountpulls it earlier.amountis 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 addsshiftto the integer values of a step-index list;displacemoves 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[]whensequenceis 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
stepsfloat 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, mirroringvalueto the opposite side of the range[low, high]. With the default[0, 1]this is1 - 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)is27, mirroring a velocity within the MIDI range.flipis its own inverse and assumesvaluelies within[low, high]; it does not clamp (compose withclamp()if the input may stray).valuemay be a single float or a list; the return matches that shape.lowandhighare 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
valueis 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.cycleas 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.
1places a single live cell at the centre. Any otherintseeds arandom.Randomand fills cells with probability density. Alist[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
rcontrols behaviour from stability to chaos:r < 3.0converges to a fixed point;r3.0–3.57 produces periodic oscillations (period-2, -4, -8…);r > 3.57enters chaos. Atr ≈ 3.83a 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 intohit_stepsprobability 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
xto 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
rulespass 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 requirerngto be provided.Note
String length can grow exponentially. A doubling rule applied for 30 generations produces ~1 billion characters. Keep
generationsto 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
generationsiterations.- Raises:
ValueError – If stochastic rules are present but
rngisNone.
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
sequenceagainst a selector, keepingsequence[i]where the selector is active and replacing every other step withzero. Give the selector as exactly one of:against— a parallel part, active whereagainst[i]is truthy(non-zero). Shorter than
sequenceit repeats its last value; an empty one is inactive everywhere.
steps— a collection of step indices, active at exactly thosepositions. 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(default0); passzero=0.0to 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
againstandstepsare 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 * scalewherescalecontrols 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 * scaleto anchor the sequence to an absolute position in the piece.spacing – Distance between consecutive samples. Matches the
scalefactor used in single calls — e.g.0.1gives the same per-sample change asperlin_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_countrows, each containingx_countvalues in [0.0, 1.0]. Access asgrid[row][col]. Equivalent to callingperlin_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) andlogistic_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
stepfrom the previous, clamped to[low, high]. Similar to Max/MSP’sdrunkobject.- 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
widthfloats 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 aSieve.The atom of sieve algebra (Xenakis’s notation
modulus @ residue). Combine with&|~and callSieve.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 distanced). 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).Falsereturns 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
nintegers 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
urnobject.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 — everyxin[lo, hi)withx % modulus == residuefor 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 (seeSieve).- Parameters:
classes –
(modulus, residue)pairs.modulusmust 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_pulsefrom the metric-weight table (so onsets on the downbeat add nothing, onsets on the weakest pulses add the most), normalised by the onset count. Usesbuild_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
1wheresequence[i] > cutoff(strict) and0otherwise — the deterministic counterpart toprobability_gate()’s random roll, matching the idiomcutoff < x. Pair withsequence_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/1ints the length ofsequence.
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 ini. 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
sequenceend-to-end and truncates so the result is exactlylengthitems 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] * 3reads 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
lengthitems, or[]whenlength <= 0.- Raises:
ValueError – If
sequenceis empty andlength > 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
valueso they compound.Folds
density_warp()overamounts: it starts fromvalueand warps by each knob in turn. Becausedensity_warp()adds log-odds, stacking knobs equals one warp whose knobs sum in log-odds, so the order ofamountsdoes not matter and the neutral knob0.5is 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 emptyamountslist returnsvalueunchanged.Stacking saturates fast: every knob above
0.5pushes harder toward1.0(and below toward0.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 withcombine_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.5neutral,>0.5denser,<0.5sparser) or a per-step list.
- Returns:
A float when
valueand 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)tuplesrng – 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)