subsequence 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. subsequence/__init__.py +231 -0
  2. subsequence/__main__.py +24 -0
  3. subsequence/assets/web/index.html +345 -0
  4. subsequence/cadences.py +113 -0
  5. subsequence/chord_graphs/__init__.py +100 -0
  6. subsequence/chord_graphs/aeolian_minor.py +158 -0
  7. subsequence/chord_graphs/chromatic_mediant.py +113 -0
  8. subsequence/chord_graphs/diminished.py +97 -0
  9. subsequence/chord_graphs/dorian_minor.py +127 -0
  10. subsequence/chord_graphs/functional_major.py +102 -0
  11. subsequence/chord_graphs/hooktheory_major.py +88 -0
  12. subsequence/chord_graphs/lydian_major.py +130 -0
  13. subsequence/chord_graphs/mixolydian.py +98 -0
  14. subsequence/chord_graphs/phrygian_minor.py +76 -0
  15. subsequence/chord_graphs/suspended.py +109 -0
  16. subsequence/chord_graphs/turnaround_global.py +157 -0
  17. subsequence/chord_graphs/whole_tone.py +77 -0
  18. subsequence/chords.py +419 -0
  19. subsequence/composition.py +6099 -0
  20. subsequence/conductor.py +238 -0
  21. subsequence/constants/__init__.py +24 -0
  22. subsequence/constants/durations.py +37 -0
  23. subsequence/constants/instruments/__init__.py +13 -0
  24. subsequence/constants/instruments/gm_cc.py +46 -0
  25. subsequence/constants/instruments/gm_drums.py +53 -0
  26. subsequence/constants/instruments/gm_instruments.py +32 -0
  27. subsequence/constants/instruments/roland_tr8s.py +320 -0
  28. subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
  29. subsequence/constants/midi_notes.py +29 -0
  30. subsequence/constants/pulses.py +17 -0
  31. subsequence/constants/velocity.py +22 -0
  32. subsequence/definitions.py +232 -0
  33. subsequence/display.py +617 -0
  34. subsequence/easing.py +347 -0
  35. subsequence/event_emitter.py +109 -0
  36. subsequence/form_state.py +665 -0
  37. subsequence/forms.py +257 -0
  38. subsequence/groove.py +323 -0
  39. subsequence/harmonic_rhythm.py +83 -0
  40. subsequence/harmonic_state.py +352 -0
  41. subsequence/harmony.py +197 -0
  42. subsequence/held_notes.py +91 -0
  43. subsequence/helpers/__init__.py +0 -0
  44. subsequence/helpers/network.py +58 -0
  45. subsequence/helpers/wing.py +430 -0
  46. subsequence/intervals.py +436 -0
  47. subsequence/keystroke.py +249 -0
  48. subsequence/link_clock.py +128 -0
  49. subsequence/live_client.py +187 -0
  50. subsequence/live_reloader.py +298 -0
  51. subsequence/live_server.py +161 -0
  52. subsequence/melodic_state.py +483 -0
  53. subsequence/midi.py +97 -0
  54. subsequence/midi_utils.py +329 -0
  55. subsequence/mini_notation.py +164 -0
  56. subsequence/motifs.py +2356 -0
  57. subsequence/osc.py +194 -0
  58. subsequence/pattern.py +363 -0
  59. subsequence/pattern_algorithmic.py +2010 -0
  60. subsequence/pattern_builder.py +2589 -0
  61. subsequence/pattern_midi.py +1208 -0
  62. subsequence/progressions.py +1913 -0
  63. subsequence/py.typed +0 -0
  64. subsequence/roles.py +63 -0
  65. subsequence/sequence_utils.py +3123 -0
  66. subsequence/sequencer.py +2086 -0
  67. subsequence/tuning.py +453 -0
  68. subsequence/voicings.py +151 -0
  69. subsequence/web_ui.py +337 -0
  70. subsequence/weighted_graph.py +156 -0
  71. subsequence-0.6.4.dist-info/METADATA +208 -0
  72. subsequence-0.6.4.dist-info/RECORD +78 -0
  73. subsequence-0.6.4.dist-info/WHEEL +5 -0
  74. subsequence-0.6.4.dist-info/entry_points.txt +2 -0
  75. subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
  76. subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
  77. subsequence-0.6.4.dist-info/scm_version.json +8 -0
  78. subsequence-0.6.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3123 @@
1
+ """Utility functions for generating and transforming step sequences.
2
+
3
+ Provides algorithms for rhythm generation (Euclidean, Bresenham, van der Corput),
4
+ sequence manipulation (rotate, legato, probability gate), and general-purpose
5
+ generative helpers (random walk, weighted choice, shuffled choices, scale/clamp).
6
+ """
7
+
8
+ import itertools
9
+ import math
10
+ import random
11
+ import typing
12
+
13
+ import subsequence.easing
14
+ import subsequence.weighted_graph
15
+
16
+ T = typing.TypeVar("T")
17
+
18
+
19
+ def generate_euclidean_sequence (steps: int, pulses: int) -> typing.List[int]:
20
+
21
+ """
22
+ Generate a Euclidean rhythm using Bjorklund's algorithm.
23
+ """
24
+
25
+ if pulses < 0:
26
+ raise ValueError(f"Pulses must be zero or positive — got {pulses}")
27
+
28
+ if pulses == 0:
29
+ return [0] * steps
30
+
31
+ if pulses > steps:
32
+ raise ValueError(f"Pulses ({pulses}) cannot be greater than steps ({steps})")
33
+
34
+ sequence = []
35
+ counts = []
36
+ remainders = []
37
+ divisor = steps - pulses
38
+
39
+ remainders.append(pulses)
40
+ level = 0
41
+
42
+ while True:
43
+ counts.append(divisor // remainders[level])
44
+ remainders.append(divisor % remainders[level])
45
+ divisor = remainders[level]
46
+ level += 1
47
+ if remainders[level] <= 1:
48
+ break
49
+
50
+ counts.append(divisor)
51
+
52
+ def build (level: int) -> None:
53
+ if level == -1:
54
+ sequence.append(0)
55
+ elif level == -2:
56
+ sequence.append(1)
57
+ else:
58
+ for i in range(counts[level]):
59
+ build(level - 1)
60
+ if remainders[level] != 0:
61
+ build(level - 2)
62
+
63
+ build(level)
64
+ i = sequence.index(1)
65
+ return sequence[i:] + sequence[:i]
66
+
67
+
68
+ def generate_bresenham_sequence (steps: int, pulses: int) -> typing.List[int]:
69
+
70
+ """
71
+ Generate a rhythm using Bresenham's line algorithm.
72
+ """
73
+
74
+ if pulses < 0:
75
+ raise ValueError(f"Pulses must be zero or positive — got {pulses}")
76
+
77
+ sequence = [0] * steps
78
+ error = 0
79
+
80
+ # The accumulator starts at 0, so each group of steps fills up before it
81
+ # overflows — placing hits LATE in each group BY DESIGN. This is the
82
+ # documented difference from generate_euclidean_sequence, which rotates
83
+ # its result to start on a hit.
84
+ for i in range(steps):
85
+ error += pulses
86
+ if error >= steps:
87
+ sequence[i] = 1
88
+ error -= steps
89
+
90
+ return sequence
91
+
92
+
93
+ def generate_bresenham_sequence_weighted (steps: int, weights: typing.List[float]) -> typing.List[int]:
94
+
95
+ """
96
+ Generate a sequence that distributes weighted indices across steps.
97
+ """
98
+
99
+ if steps <= 0:
100
+ raise ValueError("Steps must be positive")
101
+
102
+ if not weights:
103
+ raise ValueError("Weights cannot be empty")
104
+
105
+ acc = [0.0] * len(weights)
106
+ sequence: typing.List[int] = []
107
+
108
+ for _ in range(steps):
109
+
110
+ for i, weight in enumerate(weights):
111
+ acc[i] += weight
112
+
113
+ chosen = max(range(len(weights)), key=lambda i: acc[i])
114
+ sequence.append(chosen)
115
+ acc[chosen] -= 1.0
116
+
117
+ return sequence
118
+
119
+
120
+ def generate_van_der_corput_sequence (n: int, base: int = 2) -> typing.List[float]:
121
+
122
+ """
123
+ Generate a sequence of n numbers using the van der Corput sequence.
124
+ """
125
+
126
+ # base 1 never shrinks k (infinite loop) and base 0 divides by zero.
127
+ if base < 2:
128
+ raise ValueError(f"van der Corput base must be at least 2 — got {base}")
129
+
130
+ sequence = []
131
+
132
+ for i in range(n):
133
+ value = 0.0
134
+ f = 1.0 / base
135
+ k = i
136
+ while k > 0:
137
+ value += (k % base) * f
138
+ k //= base
139
+ f /= base
140
+ sequence.append(value)
141
+
142
+ return sequence
143
+
144
+
145
+ def sequence_to_indices (sequence: typing.List[int]) -> typing.List[int]:
146
+
147
+ """Extract step indices where hits occur in a binary sequence."""
148
+
149
+ return [i for i, v in enumerate(sequence) if v]
150
+
151
+
152
+ def rotate (indices: typing.List[int], shift: int, length: int) -> typing.List[int]:
153
+
154
+ """Circularly rotate step indices by the specified amount, wrapping at *length*."""
155
+
156
+ return [(i + shift) % length for i in indices]
157
+
158
+
159
+ def displace (sequence: typing.List[T], amount: int) -> typing.List[T]:
160
+
161
+ """Phase-shift a per-step pattern by a whole number of steps, wrapping.
162
+
163
+ Moves every element of ``sequence`` along by ``amount`` positions and wraps
164
+ the steps that fall off one end back round to the other — the classic rhythm
165
+ *necklace* rotation (metric displacement). A **positive** ``amount`` pushes
166
+ the pattern **later** (to the right): the hit at step 0 in ``[1, 0, 0, 0]``
167
+ lands on step 1. A negative ``amount`` pulls it earlier. ``amount`` is taken
168
+ modulo the length, so a whole revolution (or zero) returns the pattern
169
+ unchanged and an over-length shift simply wraps.
170
+
171
+ Works on any per-step data — a 0/1 rhythm, a density profile, a velocity or
172
+ note list — since it only reorders the existing values. This is a different
173
+ operation from :func:`rotate`, which adds ``shift`` to the integer *values*
174
+ of a step-index list; ``displace`` moves the *positions* within a value list.
175
+
176
+ Parameters:
177
+ sequence: The per-step pattern to phase-shift.
178
+ amount: Steps to move later (positive) or earlier (negative); reduced
179
+ modulo ``len(sequence)``.
180
+
181
+ Returns:
182
+ A new list the same length as ``sequence`` (the input is never mutated),
183
+ or ``[]`` when ``sequence`` is empty.
184
+
185
+ Example:
186
+ ```python
187
+ # Push a Euclidean kick one step later in the bar.
188
+ kick = subsequence.sequence_utils.generate_euclidean_sequence(8, 3)
189
+ delayed = subsequence.sequence_utils.displace(kick, 1)
190
+ steps = subsequence.sequence_utils.sequence_to_indices(delayed)
191
+ ```
192
+ """
193
+
194
+ if not sequence:
195
+ return []
196
+
197
+ k = amount % len(sequence)
198
+ return sequence[-k:] + sequence[:-k]
199
+
200
+
201
+ def generate_legato_durations (hits: typing.List[int]) -> typing.List[int]:
202
+
203
+ """
204
+ Convert a hit list into per-step legato durations.
205
+ """
206
+
207
+ if not hits:
208
+ return []
209
+
210
+ note_on_indices = [idx for idx, hit in enumerate(hits) if hit]
211
+ note_on_indices.append(len(hits))
212
+
213
+ durations = [0] * len(hits)
214
+
215
+ for idx, next_idx in zip(note_on_indices[:-1], note_on_indices[1:]):
216
+ durations[idx] = max(1, next_idx - idx)
217
+
218
+ return durations
219
+
220
+
221
+ def tile (sequence: typing.List[T], length: int) -> typing.List[T]:
222
+
223
+ """Cycle a sequence to an exact length.
224
+
225
+ Repeats ``sequence`` end-to-end and truncates so the result is exactly
226
+ ``length`` items long — ``tile([1, 0, 0], 8)`` gives ``[1, 0, 0, 1, 0, 0, 1,
227
+ 0]``. Reach for it when the target length is not a whole multiple of the
228
+ pattern; for an exact multiple, plain ``[1, 0, 0] * 3`` reads more clearly.
229
+
230
+ Works on any per-step data — a rhythm, a density profile, notes, velocities.
231
+
232
+ Parameters:
233
+ sequence: The pattern to repeat. Must be non-empty when ``length > 0``.
234
+ length: The exact length of the result.
235
+
236
+ Returns:
237
+ A new list of exactly ``length`` items, or ``[]`` when ``length <= 0``.
238
+
239
+ Raises:
240
+ ValueError: If ``sequence`` is empty and ``length > 0`` — there is
241
+ nothing to cycle.
242
+
243
+ Example:
244
+ ```python
245
+ # Stretch a three-step kick cell across a 16-step bar.
246
+ bar = subsequence.sequence_utils.tile([1, 0, 0], 16)
247
+ ```
248
+ """
249
+
250
+ if length <= 0:
251
+ return []
252
+
253
+ if not sequence:
254
+ raise ValueError("cannot tile an empty sequence")
255
+
256
+ return [sequence[i % len(sequence)] for i in range(length)]
257
+
258
+
259
+ def _select_mask (
260
+ sequence: typing.List[T],
261
+ against: typing.Optional[typing.Sequence[typing.Any]],
262
+ steps: typing.Optional[typing.Iterable[int]],
263
+ off: typing.Any,
264
+ keep_active: bool,
265
+ ) -> typing.List[T]:
266
+
267
+ """Gate ``sequence`` against an active selector (the shared mask/choke core).
268
+
269
+ Exactly one of ``against`` (a parallel part, active where ``against[i]`` is
270
+ truthy) or ``steps`` (a collection of step indices, active at those positions)
271
+ must be given. With ``keep_active`` True each step is kept where the selector
272
+ is active and set to ``off`` elsewhere; with False the sense is inverted. An
273
+ ``against`` part shorter than ``sequence`` repeats its last value; an empty one
274
+ is inactive everywhere; indices in ``steps`` outside the sequence are ignored.
275
+ """
276
+
277
+ if (against is None) == (steps is None):
278
+ raise ValueError("pass exactly one of against= or steps=")
279
+
280
+ if against is not None:
281
+ active = (
282
+ [bool(against[i]) if i < len(against) else bool(against[-1]) for i in range(len(sequence))]
283
+ if against else [False] * len(sequence)
284
+ )
285
+ else:
286
+ index_set = set(steps if steps is not None else [])
287
+ active = [i in index_set for i in range(len(sequence))]
288
+
289
+ return [value if (flag == keep_active) else off for value, flag in zip(sequence, active)]
290
+
291
+
292
+ def mask (
293
+ sequence: typing.List[T],
294
+ against: typing.Optional[typing.Sequence[typing.Any]] = None,
295
+ steps: typing.Optional[typing.Iterable[int]] = None,
296
+ zero: typing.Any = 0,
297
+ ) -> typing.List[T]:
298
+
299
+ """Keep the steps where a selector is active, zeroing the rest.
300
+
301
+ Gates ``sequence`` against a selector, keeping ``sequence[i]`` where the
302
+ selector is **active** and replacing every other step with ``zero``. Give the
303
+ selector as **exactly one** of:
304
+
305
+ - ``against`` — a parallel part, active where ``against[i]`` is truthy
306
+ (non-zero). Shorter than ``sequence`` it repeats its last value; an
307
+ empty one is inactive everywhere.
308
+ - ``steps`` — a collection of step indices, active at exactly those
309
+ positions. Indices outside the sequence are ignored.
310
+
311
+ Works on any per-step data — densities, 0/1 gates, notes, velocities — since
312
+ the kept steps keep their value. Off steps take ``zero`` (default ``0``); pass
313
+ ``zero=0.0`` to keep a float density profile float.
314
+
315
+ Parameters:
316
+ sequence: The per-step data to gate.
317
+ against: A parallel part, active where truthy. Mutually exclusive with
318
+ ``steps``.
319
+ steps: Step indices at which the selector is active. Mutually exclusive
320
+ with ``against``.
321
+ zero: The value written to inactive steps (default ``0``).
322
+
323
+ Returns:
324
+ A new list the length of ``sequence``.
325
+
326
+ Raises:
327
+ ValueError: If neither or both of ``against`` and ``steps`` are given.
328
+
329
+ Example:
330
+ ```python
331
+ # Keep the open hat only on the backbeat accents.
332
+ accent = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
333
+ open_hat = subsequence.sequence_utils.mask(open_hat_density, against=accent)
334
+
335
+ # Or keep only the downbeats, by step index.
336
+ downs = subsequence.sequence_utils.mask(open_hat_density, steps=[0, 4, 8, 12])
337
+ ```
338
+ """
339
+
340
+ return _select_mask(sequence, against, steps, zero, keep_active=True)
341
+
342
+
343
+ def choke (
344
+ sequence: typing.List[T],
345
+ against: typing.Optional[typing.Sequence[typing.Any]] = None,
346
+ steps: typing.Optional[typing.Iterable[int]] = None,
347
+ floor: typing.Any = 0,
348
+ ) -> typing.List[T]:
349
+
350
+ """Suppress the steps where a selector is active, keeping the rest.
351
+
352
+ The complement of :func:`mask`: replaces ``sequence[i]`` with ``floor`` where
353
+ the selector is **active**, and keeps it everywhere else. This is the classic
354
+ drum *choke* — one voice silences another on the steps it sounds. Give the
355
+ selector as **exactly one** of ``against`` (a parallel part, active where
356
+ truthy) or ``steps`` (a collection of active step indices), with the same
357
+ repeat-last / ignore-out-of-range / empty rules as :func:`mask`.
358
+
359
+ ``choke(seq, against=other)`` is the same as masking by the complement of
360
+ ``other``.
361
+
362
+ Parameters:
363
+ sequence: The per-step data to gate.
364
+ against: A parallel part; steps are suppressed where it is truthy.
365
+ Mutually exclusive with ``steps``.
366
+ steps: Step indices to suppress. Mutually exclusive with ``against``.
367
+ floor: The value written to suppressed steps (default ``0``).
368
+
369
+ Returns:
370
+ A new list the length of ``sequence``.
371
+
372
+ Raises:
373
+ ValueError: If neither or both of ``against`` and ``steps`` are given.
374
+
375
+ Example:
376
+ ```python
377
+ # Closed hat chokes the open hat wherever the closed hat sounds.
378
+ open_hat = subsequence.sequence_utils.choke(open_hat_density, against=closed_hat_density)
379
+
380
+ # Drum 2 ducks out on the steps drum 1 already fired.
381
+ drum_2 = subsequence.sequence_utils.choke(drum_2_density, steps=fired_steps)
382
+ ```
383
+ """
384
+
385
+ return _select_mask(sequence, against, steps, floor, keep_active=False)
386
+
387
+
388
+ def weighted_choice (options: typing.List[typing.Tuple[T, float]], rng: random.Random) -> T:
389
+
390
+ """Pick one item from a list of (value, weight) pairs.
391
+
392
+ Weights are relative - they don't need to sum to 1.0. Higher weight means
393
+ higher probability of selection.
394
+
395
+ Parameters:
396
+ options: List of ``(value, weight)`` tuples
397
+ rng: Random number generator instance
398
+
399
+ Example:
400
+ ```python
401
+ density = subsequence.sequence_utils.weighted_choice([
402
+ (3, 0.5), # 3 hits: 50%
403
+ (5, 0.3), # 5 hits: 30%
404
+ (7, 0.2), # 7 hits: 20%
405
+ ], p.rng)
406
+ p.euclidean("snare", pulses=density)
407
+ ```
408
+ """
409
+
410
+ if not options:
411
+ raise ValueError("Options list cannot be empty")
412
+
413
+ values, weights = zip(*options)
414
+ total = sum(weights)
415
+
416
+ if total <= 0:
417
+ raise ValueError("Total weight must be positive")
418
+
419
+ threshold = rng.random() * total
420
+ cumulative = 0.0
421
+
422
+ for value, weight in options:
423
+ cumulative += weight
424
+ if cumulative >= threshold:
425
+ return value
426
+
427
+ return options[-1][0]
428
+
429
+
430
+ def shuffled_choices (pool: typing.List[T], n: int, rng: random.Random) -> typing.List[T]:
431
+
432
+ """Choose N items from a pool with no immediate repetition.
433
+
434
+ Within each pass through the pool, every item appears before any repeats.
435
+ Across reshuffles, the last item of one pass is never the first of the next.
436
+ Similar to Max/MSP's ``urn`` object.
437
+
438
+ The no-immediate-repeat guarantee assumes the pool values are distinct —
439
+ a pool containing duplicates (e.g. ``[100, 100, 80]``) can still place
440
+ equal values back to back.
441
+
442
+ Parameters:
443
+ pool: Items to choose from
444
+ n: Number of items to return
445
+ rng: Random number generator instance
446
+
447
+ Example:
448
+ ```python
449
+ # 16 velocity values with no adjacent repeats
450
+ vels = subsequence.sequence_utils.shuffled_choices([70, 85, 100, 110], 16, p.rng)
451
+ ```
452
+ """
453
+
454
+ if not pool:
455
+ raise ValueError("Pool cannot be empty")
456
+
457
+ if n <= 0:
458
+ return []
459
+
460
+ result: typing.List[T] = []
461
+ last: typing.Optional[T] = None
462
+
463
+ while len(result) < n:
464
+
465
+ deck = list(pool)
466
+ rng.shuffle(deck)
467
+
468
+ # Prevent adjacent repeat across reshuffles.
469
+ if last is not None and len(deck) > 1 and deck[0] == last:
470
+ # Swap with a random later position.
471
+ swap_idx = rng.randint(1, len(deck) - 1)
472
+ deck[0], deck[swap_idx] = deck[swap_idx], deck[0]
473
+
474
+ for item in deck:
475
+
476
+ if len(result) >= n:
477
+ break
478
+
479
+ result.append(item)
480
+ last = item
481
+
482
+ return result
483
+
484
+
485
+ def random_walk (n: int, low: int, high: int, step: int, rng: random.Random, start: typing.Optional[int] = None) -> typing.List[int]:
486
+
487
+ """Generate values that drift by small steps within a range.
488
+
489
+ Each value moves up or down by at most ``step`` from the previous,
490
+ clamped to ``[low, high]``. Similar to Max/MSP's ``drunk`` object.
491
+
492
+ Parameters:
493
+ n: Number of values to generate
494
+ low: Minimum value (inclusive)
495
+ high: Maximum value (inclusive)
496
+ step: Maximum change per step
497
+ rng: Random number generator instance
498
+ start: Starting value (default: midpoint of range)
499
+
500
+ Example:
501
+ ```python
502
+ # Wandering velocity for 16 hi-hat steps
503
+ walk = subsequence.sequence_utils.random_walk(16, low=50, high=110, step=15, rng=p.rng)
504
+ ```
505
+ """
506
+
507
+ if n <= 0:
508
+ return []
509
+
510
+ if low > high:
511
+ raise ValueError(f"low ({low}) must be <= high ({high})")
512
+
513
+ if start is not None:
514
+ current = max(low, min(high, start))
515
+ else:
516
+ current = (low + high) // 2
517
+
518
+ result = [current]
519
+
520
+ for _ in range(n - 1):
521
+ delta = rng.randint(-step, step)
522
+ current = max(low, min(high, current + delta))
523
+ result.append(current)
524
+
525
+ return result
526
+
527
+
528
+ def probability_gate (sequence: typing.List[int], probability: typing.Union[float, typing.List[float]], rng: random.Random) -> typing.List[int]:
529
+
530
+ """Filter a binary sequence by probability.
531
+
532
+ Each active step (value > 0) is kept with the given probability.
533
+ Inactive steps (value == 0) are never promoted.
534
+
535
+ Parameters:
536
+ sequence: Binary sequence (0s and 1s)
537
+ probability: Chance of keeping each hit (0.0–1.0). A single float applies
538
+ uniformly; a list assigns per-step probability. Steps beyond the
539
+ end of a short list are always kept (probability 1.0).
540
+ rng: Random number generator instance
541
+
542
+ Example:
543
+ ```python
544
+ seq = subsequence.sequence_utils.generate_euclidean_sequence(16, 7)
545
+ gated = subsequence.sequence_utils.probability_gate(seq, 0.7, p.rng)
546
+ p.hit_steps("hh", subsequence.sequence_utils.sequence_to_indices(gated))
547
+ ```
548
+ """
549
+
550
+ result: typing.List[int] = []
551
+
552
+ for i, value in enumerate(sequence):
553
+
554
+ if value == 0:
555
+ result.append(0)
556
+ continue
557
+
558
+ if isinstance(probability, list):
559
+ prob = probability[i] if i < len(probability) else 1.0
560
+ else:
561
+ prob = probability
562
+
563
+ result.append(value if rng.random() < prob else 0)
564
+
565
+ return result
566
+
567
+
568
+ @typing.overload
569
+ def density_to_steps (density: float, rng: random.Random, length: int) -> typing.List[int]: ...
570
+ @typing.overload
571
+ def density_to_steps (density: typing.List[float], rng: random.Random, length: typing.Optional[int] = None) -> typing.List[int]: ...
572
+
573
+
574
+ def density_to_steps (
575
+ density: typing.Union[float, typing.List[float]],
576
+ rng: random.Random,
577
+ length: typing.Optional[int] = None,
578
+ ) -> typing.List[int]:
579
+
580
+ """Roll each step against its density and return the fired step indices.
581
+
582
+ Walks the grid and, for each step, draws a fresh random number and keeps
583
+ that step when the draw falls below the step's density — an independent
584
+ weighted coin per step. Returns the **list of fired step indices**, ready
585
+ to feed ``p.sequence(steps=...)`` or ``hit_steps`` and per-step
586
+ comprehensions over those indices. This is the named form of the
587
+ hand-written ``[i for i in range(n) if rng.random() < density[i]]``.
588
+
589
+ It is the *stochastic* member of the density-gate family: :func:`threshold`
590
+ is the deterministic gate, :func:`probability_gate` thins an already-binary
591
+ sequence and returns a parallel 0/1 list, and ``density_to_steps`` takes a
592
+ pure density profile (floats in ``[0, 1]``) and emits the sparse set of
593
+ survivors as indices — no ``[1] * n`` base and no :func:`sequence_to_indices`
594
+ bridge needed.
595
+
596
+ ``density`` may be a per-step list (its length sets the grid) or a single
597
+ float applied uniformly, in which case ``length`` is **required** — a bare
598
+ probability has no grid of its own. A density at or above ``1.0`` always
599
+ fires; at or below ``0.0`` never fires. Pass a seeded ``random.Random`` (or
600
+ the pattern's ``p.rng``) for reproducible output.
601
+
602
+ Parameters:
603
+ density: A per-step density list (floats in ``[0, 1]``), or a single
604
+ float applied to every step (then ``length`` is required).
605
+ rng: The seeded random generator — pass the pattern's ``p.rng``.
606
+ length: The grid size when ``density`` is a scalar. Ignored for a list
607
+ density (the list's own length is used).
608
+
609
+ Returns:
610
+ The fired step indices in ascending order — possibly empty. An empty
611
+ result is normal: a sparse profile may fire nothing on a given cycle.
612
+
613
+ Raises:
614
+ ValueError: If ``density`` is a scalar and ``length`` is not given —
615
+ there is no grid to roll against.
616
+
617
+ Example:
618
+ ```python
619
+ # A per-step kick profile rolled into concrete hits.
620
+ profile = [0.9, 0.1, 0.5, 0.1, 0.8, 0.1, 0.4, 0.1]
621
+ steps = subsequence.sequence_utils.density_to_steps(profile, p.rng)
622
+ velocities = [40 + int(profile[i] * 30) for i in steps]
623
+ p.sequence(steps=steps, pitches="kick", velocities=velocities)
624
+
625
+ # A uniform 30% across a 16-step bar needs an explicit length.
626
+ ghosts = subsequence.sequence_utils.density_to_steps(0.3, p.rng, length=16)
627
+ ```
628
+ """
629
+
630
+ if isinstance(density, list):
631
+ return [i for i in range(len(density)) if rng.random() < density[i]]
632
+
633
+ if length is None:
634
+ raise ValueError("density_to_steps needs a length when density is a scalar")
635
+
636
+ return [i for i in range(length) if rng.random() < density]
637
+
638
+
639
+ def _density_warp_scalar (p: float, d: float) -> float:
640
+
641
+ """Warp a single probability ``p`` by knob ``d`` (the scalar core).
642
+
643
+ Returns ``(p*d) / (p*d + (1-p)*(1-d))`` with hard guards at the edges so the
644
+ warp reaches exact ``0.0`` / ``1.0`` and never divides by zero.
645
+ """
646
+
647
+ if p <= 0.0:
648
+ return 0.0
649
+
650
+ if p >= 1.0:
651
+ return 1.0
652
+
653
+ if d <= 0.0:
654
+ return 0.0
655
+
656
+ if d >= 1.0:
657
+ return 1.0
658
+
659
+ pd = p * d
660
+ return pd / (pd + (1.0 - p) * (1.0 - d))
661
+
662
+
663
+ @typing.overload
664
+ def density_warp (value: float, amount: float) -> float: ...
665
+ @typing.overload
666
+ def density_warp (value: typing.List[float], amount: typing.Union[float, typing.List[float]]) -> typing.List[float]: ...
667
+ @typing.overload
668
+ def density_warp (value: float, amount: typing.List[float]) -> typing.List[float]: ...
669
+
670
+
671
+ def density_warp (
672
+ value: typing.Union[float, typing.List[float]],
673
+ amount: typing.Union[float, typing.List[float]],
674
+ ) -> typing.Union[float, typing.List[float]]:
675
+
676
+ """Warp a probability/density by a single denser/sparser knob.
677
+
678
+ Pushes a probability ``value`` in ``[0, 1]`` toward 1.0 (denser) or toward
679
+ 0.0 (sparser) with one knob ``amount`` in ``[0, 1]``. ``amount = 0.5`` is
680
+ the identity and returns ``value`` unchanged; above 0.5 thickens, below 0.5
681
+ thins. The output is always in ``[0, 1]``.
682
+
683
+ The map is ``W = (value*amount) / (value*amount + (1-value)*(1-amount))`` —
684
+ the logistic of ``logit(value) + logit(amount)``. Because the log-odds add,
685
+ warps **stack**: two warps equal one whose knobs combine by summing their
686
+ log-odds. ``amount = 1`` forces a full ``1.0`` and ``amount = 0`` a full
687
+ ``0.0`` for any ``value`` strictly between 0 and 1.
688
+
689
+ ``value`` may be a single float or a list; the return matches that shape.
690
+ ``amount`` may likewise be a single float (applied to every element) or a
691
+ per-step list (e.g. a Perlin density field). The result is a list whenever
692
+ either argument is a list; when both are lists of unequal length the result
693
+ has the length of the longer, the shorter extended by repeating its last
694
+ value. An empty list yields an empty list.
695
+
696
+ Parameters:
697
+ value: A probability/density in ``[0, 1]``, or a list of them.
698
+ amount: The warp knob in ``[0, 1]`` — ``0.5`` is identity, ``>0.5``
699
+ denser, ``<0.5`` sparser. A float warps every element uniformly;
700
+ a list warps per step.
701
+
702
+ Returns:
703
+ A float when both arguments are floats, otherwise a list of floats.
704
+
705
+ Example:
706
+ ```python
707
+ # A per-step kick profile, thickened by one density knob.
708
+ profile = [0.9, 0.1, 0.5, 0.1, 0.8, 0.1, 0.4, 0.1]
709
+ dense = subsequence.sequence_utils.density_warp(profile, 0.7)
710
+ # dense is a list in [0, 1] — use as per-step probabilities or velocities.
711
+ ```
712
+ """
713
+
714
+ if isinstance(value, list):
715
+
716
+ if isinstance(amount, list):
717
+
718
+ # Both lists: an empty operand yields []; otherwise pad the shorter
719
+ # by repeating its last element (the _expand_sequence_param rule).
720
+ if not value or not amount:
721
+ return []
722
+
723
+ n = max(len(value), len(amount))
724
+ vs = value if len(value) == n else value + [value[-1]] * (n - len(value))
725
+ amt = amount if len(amount) == n else amount + [amount[-1]] * (n - len(amount))
726
+ return [_density_warp_scalar(v, a) for v, a in zip(vs, amt)]
727
+
728
+ return [_density_warp_scalar(v, amount) for v in value]
729
+
730
+ if isinstance(amount, list):
731
+ return [_density_warp_scalar(value, a) for a in amount]
732
+
733
+ return _density_warp_scalar(value, amount)
734
+
735
+
736
+ def _sigmoid (x: float) -> float:
737
+
738
+ """Logistic sigmoid, evaluated so ``exp`` only ever sees a non-positive argument.
739
+
740
+ Splitting on the sign of ``x`` keeps the exponent ``<= 0`` in both branches,
741
+ so the result saturates gracefully to ``1.0`` / ``0.0`` at large magnitude
742
+ instead of raising ``OverflowError`` or producing ``nan``.
743
+ """
744
+
745
+ if x >= 0.0:
746
+ z = math.exp(-x)
747
+ return 1.0 / (1.0 + z)
748
+
749
+ z = math.exp(x)
750
+ return z / (1.0 + z)
751
+
752
+
753
+ def _density_spread_scalar (p: float, amount: float, m: float) -> float:
754
+
755
+ """Contrast a single probability ``p`` about anchor ``m`` by knob ``amount`` (the scalar core).
756
+
757
+ Maps ``out = sigmoid(logit(m) + k*(logit(p) - logit(m)))`` with
758
+ ``k = amount/(1-amount)``, so ``odds(out) = odds(p)**k * odds(m)**(1-k)``.
759
+ Hard guards keep the result in ``[0, 1]`` and avoid log/exp blow-ups;
760
+ ``amount = 0.5`` returns ``p`` exactly (the general path round-trips through
761
+ ``log``/``exp`` and would otherwise drift by up to one ULP).
762
+ """
763
+
764
+ if p <= 0.0:
765
+ return 0.0
766
+
767
+ if p >= 1.0:
768
+ return 1.0
769
+
770
+ if amount <= 0.0: # k = 0 -> collapse onto the anchor
771
+ return m
772
+
773
+ if amount >= 1.0: # k -> inf -> push to the nearest rail across m
774
+ if p < m:
775
+ return 0.0
776
+ if p > m:
777
+ return 1.0
778
+ return m
779
+
780
+ if amount == 0.5: # k = 1 -> exact identity, no log/exp round-trip
781
+ return p
782
+
783
+ k = amount / (1.0 - amount)
784
+ lm = math.log(m / (1.0 - m))
785
+ lp = math.log(p / (1.0 - p))
786
+ return _sigmoid(lm + k * (lp - lm))
787
+
788
+
789
+ @typing.overload
790
+ def density_spread (value: float, amount: float, midpoint: float = 0.5) -> float: ...
791
+ @typing.overload
792
+ def density_spread (value: typing.List[float], amount: typing.Union[float, typing.List[float]], midpoint: float = 0.5) -> typing.List[float]: ...
793
+ @typing.overload
794
+ def density_spread (value: float, amount: typing.List[float], midpoint: float = 0.5) -> typing.List[float]: ...
795
+
796
+
797
+ def density_spread (
798
+ value: typing.Union[float, typing.List[float]],
799
+ amount: typing.Union[float, typing.List[float]],
800
+ midpoint: float = 0.5,
801
+ ) -> typing.Union[float, typing.List[float]]:
802
+
803
+ """Expand or contract a probability/density about a fixed anchor.
804
+
805
+ Where :func:`density_warp` *shifts* a value denser or sparser, this *scales*
806
+ the spread of values around an anchor ``midpoint`` — the contrast twin of the
807
+ warp. ``amount = 0.5`` is the identity; above 0.5 **expands** the spread
808
+ (pushes values away from the anchor, toward 0 and 1); below 0.5 **contracts**
809
+ it (pulls values toward the anchor). A ``value`` equal to ``midpoint`` never
810
+ moves, and the output is always in ``[0, 1]``.
811
+
812
+ The map scales each value's log-odds deviation from the anchor by a gain
813
+ ``k = amount/(1-amount)``, so the odds compound as
814
+ ``odds(out) = odds(value)**k * odds(midpoint)**(1-k)``. Handy points:
815
+ ``amount = 0.75`` triples the spread (``k = 3``), ``0.25`` thirds it
816
+ (``k = 1/3``); ``amount`` and ``1 - amount`` are exact inverses (contract
817
+ then expand-by-complement returns the input).
818
+
819
+ The spread is even in log-odds, not in linear distance: it stretches the
820
+ mid-range most and barely moves values already near 0 or 1, and a strong
821
+ expand pins near-rail values onto the rails (the same saturation
822
+ :func:`density_warp` has at ``amount`` near 0 or 1).
823
+
824
+ ``value`` may be a single float or a list; the return matches that shape.
825
+ ``amount`` may likewise be a single float (applied to every element) or a
826
+ per-step list. The result is a list whenever either is a list; on unequal
827
+ lengths the shorter repeats its last value, and an empty list yields an empty
828
+ list. ``midpoint`` is a single anchor in the open interval ``(0, 1)``.
829
+
830
+ Parameters:
831
+ value: A probability/density in ``[0, 1]``, or a list of them.
832
+ amount: The spread knob in ``[0, 1]`` — ``0.5`` is identity, ``>0.5``
833
+ expands, ``<0.5`` contracts. A float spreads every element
834
+ uniformly; a list spreads per step.
835
+ midpoint: The fixed anchor in ``(0, 1)`` that the spread pivots around
836
+ (default ``0.5``). Values stay on their own side of it.
837
+
838
+ Returns:
839
+ A float when ``value`` and ``amount`` are both floats, otherwise a list
840
+ of floats.
841
+
842
+ Raises:
843
+ ValueError: If ``midpoint`` is not strictly between 0 and 1 (an anchor on
844
+ a rail has no defined log-odds).
845
+
846
+ Example:
847
+ ```python
848
+ # Sharpen the contrast of a per-step accent profile around 0.5.
849
+ accents = [0.55, 0.3, 0.7, 0.45, 0.8, 0.25, 0.6, 0.4]
850
+ sharp = subsequence.sequence_utils.density_spread(accents, 0.75)
851
+ # values above 0.5 pushed up, below pushed down — same centre, wider spread.
852
+ ```
853
+ """
854
+
855
+ if not 0.0 < midpoint < 1.0:
856
+ raise ValueError(f"midpoint must be strictly between 0 and 1, got {midpoint}")
857
+
858
+ if isinstance(value, list):
859
+
860
+ if isinstance(amount, list):
861
+
862
+ # Both lists: an empty operand yields []; otherwise pad the shorter
863
+ # by repeating its last element (the _expand_sequence_param rule).
864
+ if not value or not amount:
865
+ return []
866
+
867
+ n = max(len(value), len(amount))
868
+ vs = value if len(value) == n else value + [value[-1]] * (n - len(value))
869
+ amt = amount if len(amount) == n else amount + [amount[-1]] * (n - len(amount))
870
+ return [_density_spread_scalar(v, a, midpoint) for v, a in zip(vs, amt)]
871
+
872
+ return [_density_spread_scalar(v, amount, midpoint) for v in value]
873
+
874
+ if isinstance(amount, list):
875
+ return [_density_spread_scalar(value, a, midpoint) for a in amount]
876
+
877
+ return _density_spread_scalar(value, amount, midpoint)
878
+
879
+
880
+ def _combine_geomean (values: typing.List[float]) -> float:
881
+
882
+ """Geometric mean of ``values`` — the Nth root of their product."""
883
+
884
+ return math.prod(values) ** (1.0 / len(values))
885
+
886
+
887
+ _combine_reducers: typing.Dict[str, typing.Callable[[typing.List[float]], float]] = {
888
+ "geomean": _combine_geomean,
889
+ "min": min,
890
+ "mean": lambda values: sum(values) / len(values),
891
+ "product": math.prod,
892
+ }
893
+
894
+
895
+ def combine_densities (
896
+ layers: typing.List[typing.Union[float, typing.List[float]]],
897
+ strategy: str = "geomean",
898
+ ) -> typing.Union[float, typing.List[float]]:
899
+
900
+ """Blend several density layers into one consensus density.
901
+
902
+ Takes a list of density ``layers`` — each a single value in ``[0, 1]`` or a
903
+ per-step list in ``[0, 1]`` — and reduces them, step by step, to one density
904
+ value or list. This is the "agree on how busy this bar should be" stage; its
905
+ output is meant to feed the ``amount`` of :func:`density_warp`.
906
+
907
+ The ``strategy`` picks the reducer (all preserve ``[0, 1]`` for valid inputs,
908
+ so no clamping is applied):
909
+
910
+ - ``"geomean"`` (default) — the geometric mean, ``prod(values) ** (1/N)``.
911
+ A gentle consensus where any near-zero layer pulls the result down.
912
+ - ``"min"`` — the most restrictive layer wins (a strict gate).
913
+ - ``"mean"`` — the plain arithmetic average (a balanced vote).
914
+ - ``"product"`` — multiply them all (stacks toward sparse fast).
915
+
916
+ Broadcasting generalises the rule in :func:`density_warp`: if every layer is
917
+ a single value the result is a single value; if any layer is a list the
918
+ result is a list as long as the longest layer, with scalar layers applied to
919
+ every step and shorter lists extended by repeating their last value. An
920
+ empty list layer yields ``[]``.
921
+
922
+ Parameters:
923
+ layers: The density layers to blend. Each entry is a value in
924
+ ``[0, 1]`` or a per-step list of such values. Must be non-empty.
925
+ strategy: Reducer name — ``"geomean"`` (default), ``"min"``, ``"mean"``,
926
+ or ``"product"``.
927
+
928
+ Returns:
929
+ A float when every layer is a float, otherwise a list of floats.
930
+
931
+ Raises:
932
+ ValueError: If ``layers`` is empty, or ``strategy`` is unknown.
933
+
934
+ Example:
935
+ ```python
936
+ # Blend a metric accent curve, an intensity envelope, and a global knob
937
+ # into a consensus, then warp a base probability by it.
938
+ accent = subsequence.sequence_utils.build_metric_weights((4, 4), 16)
939
+ envelope = subsequence.easing.ramp(16, 0.2, 0.9, "ease_in_out")
940
+ consensus = subsequence.sequence_utils.combine_densities(
941
+ [accent, envelope, 0.6], strategy="geomean")
942
+ probs = subsequence.sequence_utils.density_warp(0.5, consensus)
943
+ ```
944
+ """
945
+
946
+ if not layers:
947
+ raise ValueError("Layers cannot be empty")
948
+
949
+ if strategy not in _combine_reducers:
950
+ available = ", ".join(f'"{k}"' for k in sorted(_combine_reducers))
951
+ raise ValueError(
952
+ f"Unknown combine strategy {strategy!r}. Available strategies: {available}"
953
+ )
954
+
955
+ reduce = _combine_reducers[strategy]
956
+
957
+ if all(not isinstance(layer, list) for layer in layers):
958
+ return reduce([layer for layer in layers if not isinstance(layer, list)])
959
+
960
+ lengths = [len(layer) for layer in layers if isinstance(layer, list)]
961
+
962
+ if 0 in lengths:
963
+ return []
964
+
965
+ n = max(lengths)
966
+ result: typing.List[float] = []
967
+
968
+ for i in range(n):
969
+
970
+ step: typing.List[float] = []
971
+
972
+ for layer in layers:
973
+ if isinstance(layer, list):
974
+ step.append(layer[i] if i < len(layer) else layer[-1])
975
+ else:
976
+ step.append(layer)
977
+
978
+ result.append(reduce(step))
979
+
980
+ return result
981
+
982
+
983
+ @typing.overload
984
+ def warp_stack (value: float, amounts: typing.List[float]) -> float: ...
985
+ @typing.overload
986
+ def warp_stack (value: float, amounts: typing.List[typing.Union[float, typing.List[float]]]) -> typing.Union[float, typing.List[float]]: ...
987
+ @typing.overload
988
+ def warp_stack (value: typing.List[float], amounts: typing.List[typing.Union[float, typing.List[float]]]) -> typing.List[float]: ...
989
+
990
+
991
+ def warp_stack (
992
+ value: typing.Union[float, typing.List[float]],
993
+ amounts: typing.Sequence[typing.Union[float, typing.List[float]]],
994
+ ) -> typing.Union[float, typing.List[float]]:
995
+
996
+ """Apply several density knobs to ``value`` so they compound.
997
+
998
+ Folds :func:`density_warp` over ``amounts``: it starts from ``value`` and
999
+ warps by each knob in turn. Because :func:`density_warp` adds log-odds,
1000
+ stacking knobs equals one warp whose knobs sum in log-odds, so the order of
1001
+ ``amounts`` does not matter and the neutral knob ``0.5`` is a no-op.
1002
+
1003
+ Each knob may itself be a single value or a per-step list (it inherits
1004
+ :func:`density_warp`'s broadcasting), so you can mix a global knob with
1005
+ per-step envelopes. An empty ``amounts`` list returns ``value`` unchanged.
1006
+
1007
+ Stacking saturates fast: every knob above ``0.5`` pushes harder toward
1008
+ ``1.0`` (and below toward ``0.0``), so a few strong knobs can pin a sequence
1009
+ almost fully on or off. Treat it like gain-staging — prefer a few gentle
1010
+ knobs, or blend control layers first with :func:`combine_densities` and apply
1011
+ the consensus as one knob.
1012
+
1013
+ Parameters:
1014
+ value: A probability/density in ``[0, 1]``, or a list of them.
1015
+ amounts: The knobs to compound, each a value in ``[0, 1]`` (``0.5``
1016
+ neutral, ``>0.5`` denser, ``<0.5`` sparser) or a per-step list.
1017
+
1018
+ Returns:
1019
+ A float when ``value`` and every knob are floats, otherwise a list.
1020
+
1021
+ Example:
1022
+ ```python
1023
+ # Compound a global swell, a humanised drift, and a per-step accent.
1024
+ accent = subsequence.easing.ramp(16, 0.4, 0.8, "ease_in")
1025
+ probs = subsequence.sequence_utils.warp_stack([0.5] * 16, [0.7, 0.55, accent])
1026
+ ```
1027
+ """
1028
+
1029
+ result = value
1030
+
1031
+ for amount in amounts:
1032
+ result = density_warp(result, amount)
1033
+
1034
+ return result
1035
+
1036
+
1037
+ def scale_clamp (value: float, in_min: float, in_max: float, out_min: float = 0.0, out_max: float = 1.0) -> float:
1038
+
1039
+ """Scale a value from an input range to an output range and clamp the result.
1040
+
1041
+ Maps a value from [in_min, in_max] to [out_min, out_max]. If the result
1042
+ falls outside the output range, it is clamped to the nearest bound.
1043
+ Correctly handles reversed ranges (where min > max).
1044
+
1045
+ Parameters:
1046
+ value: The number to scale and clamp.
1047
+ in_min: The start of the input range.
1048
+ in_max: The end of the input range.
1049
+ out_min: The start of the target output range (default: 0.0).
1050
+ out_max: The end of the target output range (default: 1.0).
1051
+
1052
+ Example:
1053
+ ```python
1054
+ # Scale sensor data (0-1023) to a probability (0.0-1.0)
1055
+ prob = subsequence.sequence_utils.scale_clamp(sensor_val, 0, 1023)
1056
+
1057
+ # Invert a MIDI CC (0-127) to a volume multiplier (1.0-0.0)
1058
+ vol = subsequence.sequence_utils.scale_clamp(cc_val, 0, 127, 1.0, 0.0)
1059
+ ```
1060
+ """
1061
+
1062
+ if in_min == in_max:
1063
+
1064
+ raise ValueError(f"Input range cannot be zero-width ({in_min} == {in_max})")
1065
+
1066
+ percentage = (value - in_min) / (in_max - in_min)
1067
+ scaled = out_min + percentage * (out_max - out_min)
1068
+
1069
+ # Handle regular and reversed ranges
1070
+ if out_min < out_max:
1071
+ return max(out_min, min(out_max, scaled))
1072
+ else:
1073
+ return max(out_max, min(out_min, scaled))
1074
+
1075
+
1076
+ @typing.overload
1077
+ def flip (value: float, low: float = 0.0, high: float = 1.0) -> float: ...
1078
+ @typing.overload
1079
+ def flip (value: typing.List[float], low: float = 0.0, high: float = 1.0) -> typing.List[float]: ...
1080
+
1081
+
1082
+ def flip (
1083
+ value: typing.Union[float, typing.List[float]],
1084
+ low: float = 0.0,
1085
+ high: float = 1.0,
1086
+ ) -> typing.Union[float, typing.List[float]]:
1087
+
1088
+ """Reflect a value within a range — its complement about the mid-point.
1089
+
1090
+ Returns ``low + high - value``, mirroring ``value`` to the opposite side of the
1091
+ range ``[low, high]``. With the default ``[0, 1]`` this is ``1 - value`` — the
1092
+ density/probability complement, and a logical NOT for a 0/1 list. Being
1093
+ range-aware it also flips other scales: ``flip(100, 0, 127)`` is ``27``,
1094
+ mirroring a velocity within the MIDI range.
1095
+
1096
+ ``flip`` is its own inverse and assumes ``value`` lies within ``[low, high]``;
1097
+ it does **not** clamp (compose with :func:`clamp` if the input may stray).
1098
+
1099
+ ``value`` may be a single float or a list; the return matches that shape.
1100
+ ``low`` and ``high`` are single scalars applied to every element. An empty
1101
+ list yields an empty list.
1102
+
1103
+ Parameters:
1104
+ value: A number to reflect, or a list of them.
1105
+ low: The lower bound of the range (default ``0.0``).
1106
+ high: The upper bound of the range (default ``1.0``).
1107
+
1108
+ Returns:
1109
+ A float when ``value`` is a float, otherwise a list of floats.
1110
+
1111
+ Example:
1112
+ ```python
1113
+ # Turn a kick density into its "everywhere the kick isn't" field.
1114
+ gaps = subsequence.sequence_utils.flip(kick_density)
1115
+
1116
+ # Mirror a velocity within the MIDI range.
1117
+ soft = subsequence.sequence_utils.flip(100, 0, 127)
1118
+ ```
1119
+ """
1120
+
1121
+ if isinstance(value, list):
1122
+ return [low + high - v for v in value]
1123
+
1124
+ return low + high - value
1125
+
1126
+
1127
+ @typing.overload
1128
+ def clamp (value: float, low: float = 0.0, high: float = 1.0) -> float: ...
1129
+ @typing.overload
1130
+ def clamp (value: typing.List[float], low: float = 0.0, high: float = 1.0) -> typing.List[float]: ...
1131
+
1132
+
1133
+ def clamp (
1134
+ value: typing.Union[float, typing.List[float]],
1135
+ low: float = 0.0,
1136
+ high: float = 1.0,
1137
+ ) -> typing.Union[float, typing.List[float]]:
1138
+
1139
+ """Bound a value (or list) to the range ``[low, high]``.
1140
+
1141
+ Returns ``max(low, min(high, value))`` element-wise. This is the plain,
1142
+ list-aware clamp — distinct from :func:`scale_clamp`, which *rescales* from one
1143
+ range to another before clamping. Defaults to ``[0, 1]``, the usual
1144
+ density range; pass ``low``/``high`` for any other scale. Assumes
1145
+ ``low <= high``.
1146
+
1147
+ ``value`` may be a single float or a list; the return matches that shape.
1148
+ ``low`` and ``high`` are single scalars. An empty list yields an empty list.
1149
+
1150
+ Parameters:
1151
+ value: A number to bound, or a list of them.
1152
+ low: The lower bound (default ``0.0``).
1153
+ high: The upper bound (default ``1.0``).
1154
+
1155
+ Returns:
1156
+ A float when ``value`` is a float, otherwise a list of floats.
1157
+
1158
+ Example:
1159
+ ```python
1160
+ # Keep a hand-scaled density field inside [0, 1] after arithmetic.
1161
+ safe = subsequence.sequence_utils.clamp([d * 1.4 for d in kick_density])
1162
+
1163
+ # Bound a computed velocity to the MIDI range.
1164
+ vel = subsequence.sequence_utils.clamp(raw_velocity, 0, 127)
1165
+ ```
1166
+ """
1167
+
1168
+ if isinstance(value, list):
1169
+ return [max(low, min(high, v)) for v in value]
1170
+
1171
+ return max(low, min(high, value))
1172
+
1173
+
1174
+ def threshold (sequence: typing.List[float], cutoff: float = 0.5) -> typing.List[int]:
1175
+
1176
+ """Gate a per-step field into a deterministic 0/1 sequence.
1177
+
1178
+ Returns ``1`` where ``sequence[i] > cutoff`` (strict) and ``0`` otherwise — the
1179
+ deterministic counterpart to :func:`probability_gate`'s random roll, matching
1180
+ the idiom ``cutoff < x``. Pair with :func:`sequence_to_indices` to turn the
1181
+ result into the firing step indices.
1182
+
1183
+ This is a sequence operation: it takes a list and returns a list of ints. An
1184
+ empty sequence yields an empty list.
1185
+
1186
+ Parameters:
1187
+ sequence: A per-step field (e.g. a density profile in ``[0, 1]``).
1188
+ cutoff: The exclusive threshold; steps strictly above it fire (default
1189
+ ``0.5``).
1190
+
1191
+ Returns:
1192
+ A list of ``0`` / ``1`` ints the length of ``sequence``.
1193
+
1194
+ Example:
1195
+ ```python
1196
+ # Place closed-hat hits wherever the density clears the half-way line.
1197
+ gate = subsequence.sequence_utils.threshold(hh_closed_density, 0.5)
1198
+ steps = subsequence.sequence_utils.sequence_to_indices(gate)
1199
+ ```
1200
+ """
1201
+
1202
+ return [1 if value > cutoff else 0 for value in sequence]
1203
+
1204
+
1205
+ def perlin_1d (x: float, seed: int = 0) -> float:
1206
+
1207
+ """Generate smooth 1D noise at position *x*.
1208
+
1209
+ Returns a value in [0.0, 1.0] that varies smoothly as *x* changes.
1210
+ Same *x* and *seed* always produce the same output. Use to drive
1211
+ density, velocity, or probability parameters that should wander
1212
+ organically over time — the "parameter wandering within boundaries"
1213
+ quality of generative electronic music systems.
1214
+
1215
+ Parameters:
1216
+ x: Position along the noise field. Use ``bar * scale`` where
1217
+ ``scale`` controls the rate of change (smaller = slower).
1218
+ 0.05–0.1 is good for bar-level wandering.
1219
+ seed: Seed for the hash function. Different seeds produce
1220
+ different but equally smooth noise fields.
1221
+
1222
+ Example:
1223
+ ```python
1224
+ # Smooth density that wanders over bars
1225
+ density = subsequence.sequence_utils.perlin_1d(p.cycle * 0.08, seed=42)
1226
+ p.bresenham("snare_1", pulses=max(1, round(density * 6)),
1227
+ velocity=35, no_overlap=True)
1228
+ ```
1229
+ """
1230
+
1231
+ x0 = math.floor(x)
1232
+ x1 = x0 + 1
1233
+ t = x - x0
1234
+
1235
+ def _grad (pos: int) -> float:
1236
+ # Hash function using Linear Congruential Generator (LCG) constants.
1237
+ # The "magic numbers" (e.g. 1103515245) distribute bits evenly and are
1238
+ # drawn from standard C library rand() implementations to ensure high
1239
+ # quality pseudo-randomness quickly.
1240
+ h = ((pos * 1103515245 + seed * 374761393 + 12345) & 0x7FFFFFFF)
1241
+ return (h / 0x3FFFFFFF) - 1.0
1242
+
1243
+ fade = subsequence.easing.s_curve(t)
1244
+
1245
+ d0 = _grad(x0) * t
1246
+ d1 = _grad(x1) * (t - 1.0)
1247
+
1248
+ value = d0 + fade * (d1 - d0)
1249
+
1250
+ # Shift and clamp into [0, 1]. In practice the output occupies only the
1251
+ # middle of that range (roughly [0.22, 0.77]) and rarely nears the extremes,
1252
+ # so rescale with map_value / scale_clamp if you need the full 0–1 span.
1253
+ return max(0.0, min(1.0, value + 0.5))
1254
+
1255
+
1256
+ def perlin_2d (x: float, y: float, seed: int = 0) -> float:
1257
+
1258
+ """Generate smooth 2D noise at position *(x, y)*.
1259
+
1260
+ Returns a value in [0.0, 1.0] that varies smoothly as *x* and *y* change.
1261
+ Same coordinates and *seed* always produce the same output. Use to drive
1262
+ correlated parameters that should weave around each other organically over time,
1263
+ or for spatialized parameter wandering.
1264
+
1265
+ Parameters:
1266
+ x: Position along the X axis of the noise field.
1267
+ y: Position along the Y axis of the noise field.
1268
+ seed: Seed for the hash function. Different seeds produce
1269
+ different but equally smooth noise fields.
1270
+
1271
+ Example:
1272
+ ```python
1273
+ # Two parameters wandering smoothly but with related movement.
1274
+ # By locking X to time and slightly separating Y, the two values
1275
+ # will move in a correlated, organic dance over the bars.
1276
+ density = subsequence.sequence_utils.perlin_2d(p.cycle * 0.08, 0.0, seed=42)
1277
+ velocity = subsequence.sequence_utils.perlin_2d(p.cycle * 0.08, 0.5, seed=42)
1278
+ ```
1279
+ """
1280
+
1281
+ x0 = math.floor(x)
1282
+ y0 = math.floor(y)
1283
+ x1 = x0 + 1
1284
+ y1 = y0 + 1
1285
+
1286
+ tx = x - x0
1287
+ ty = y - y0
1288
+
1289
+ def _grad (pos_x: int, pos_y: int) -> float:
1290
+ # Note: The math here (smootherstep fade and hash function) is deliberately
1291
+ # duplicated from perlin_1d rather than extracted into helper functions.
1292
+ # This avoids Python function call overhead, maximizing execution speed for
1293
+ # dense sequences. See perlin_1d for details on the LCG hash constants.
1294
+ h = ((pos_x * 1103515245 + pos_y * 741103597 + seed * 374761393 + 12345) & 0x7FFFFFFF)
1295
+ # 4 diagonal gradients
1296
+ h4 = h & 3
1297
+ dx = x - pos_x
1298
+ dy = y - pos_y
1299
+ if h4 == 0: return dx + dy
1300
+ if h4 == 1: return -dx + dy
1301
+ if h4 == 2: return dx - dy
1302
+ return -dx - dy
1303
+
1304
+ fadex = subsequence.easing.s_curve(tx)
1305
+ fadey = subsequence.easing.s_curve(ty)
1306
+
1307
+ d00 = _grad(x0, y0)
1308
+ d10 = _grad(x1, y0)
1309
+ d01 = _grad(x0, y1)
1310
+ d11 = _grad(x1, y1)
1311
+
1312
+ # Interpolate along x
1313
+ ix0 = d00 + fadex * (d10 - d00)
1314
+ ix1 = d01 + fadex * (d11 - d01)
1315
+
1316
+ # Interpolate along y
1317
+ value = ix0 + fadey * (ix1 - ix0)
1318
+
1319
+ # Shift and clamp into [0, 1]. In practice the output occupies only the
1320
+ # middle of that range (roughly [0.23, 0.82]); rescale with map_value /
1321
+ # scale_clamp if you need the full 0–1 span.
1322
+ return max(0.0, min(1.0, (value + 1.0) / 2.0))
1323
+
1324
+
1325
+ def perlin_1d_sequence (start: float, spacing: float, count: int, seed: int = 0) -> typing.List[float]:
1326
+
1327
+ """Generate a sequence of smooth 1D noise values.
1328
+
1329
+ Equivalent to calling :func:`perlin_1d` *count* times at evenly-spaced
1330
+ positions, but expressed as a single call. Every value is in [0.0, 1.0].
1331
+
1332
+ Parameters:
1333
+ start: Position of the first sample in the noise field.
1334
+ Typically ``p.bar * p.grid * scale`` to anchor the sequence
1335
+ to an absolute position in the piece.
1336
+ spacing: Distance between consecutive samples. Matches the
1337
+ ``scale`` factor used in single calls — e.g. ``0.1`` gives
1338
+ the same per-sample change as ``perlin_1d(i * 0.1, seed)``.
1339
+ count: Number of values to return.
1340
+ seed: Noise field seed. Same seed as a matching :func:`perlin_1d`
1341
+ call produces identical values at the same positions.
1342
+
1343
+ Example:
1344
+ ```python
1345
+ # 16 smoothly-varying velocities for hi-hat ghost notes
1346
+ noise = subsequence.sequence_utils.perlin_1d_sequence(
1347
+ start = p.bar * p.grid * 0.1,
1348
+ spacing = 0.1,
1349
+ count = p.grid,
1350
+ seed = 10
1351
+ )
1352
+ hat_velocities = [
1353
+ int(subsequence.easing.map_value(n, out_min=50, out_max=75, shape="ease_in"))
1354
+ for n in noise
1355
+ ]
1356
+ ```
1357
+ """
1358
+
1359
+ return [perlin_1d(start + i * spacing, seed) for i in range(count)]
1360
+
1361
+
1362
+ def perlin_2d_grid (
1363
+ x_start: float,
1364
+ y_start: float,
1365
+ x_step: float,
1366
+ y_step: float,
1367
+ x_count: int,
1368
+ y_count: int,
1369
+ seed: int = 0
1370
+ ) -> typing.List[typing.List[float]]:
1371
+
1372
+ """Generate a 2D grid of smooth noise values.
1373
+
1374
+ Returns a list of ``y_count`` rows, each containing ``x_count`` values
1375
+ in [0.0, 1.0]. Access as ``grid[row][col]``. Equivalent to calling
1376
+ :func:`perlin_2d` for every *(x, y)* position in the grid.
1377
+
1378
+ Parameters:
1379
+ x_start: Starting X position.
1380
+ y_start: Starting Y position.
1381
+ x_step: Spacing between columns.
1382
+ y_step: Spacing between rows.
1383
+ x_count: Number of columns (samples along X).
1384
+ y_count: Number of rows (samples along Y).
1385
+ seed: Noise field seed.
1386
+
1387
+ Example:
1388
+ ```python
1389
+ # 4x4 noise grid — rows are bars, columns are steps
1390
+ grid = subsequence.sequence_utils.perlin_2d_grid(
1391
+ x_start = p.bar * 0.1,
1392
+ y_start = 0.0,
1393
+ x_step = 0.1,
1394
+ y_step = 0.25,
1395
+ x_count = 4,
1396
+ y_count = 4,
1397
+ seed = 5,
1398
+ )
1399
+ # e.g. drive density of four voices independently
1400
+ for row, voice in enumerate(["kick", "snare", "hi_hat_closed", "clap"]):
1401
+ density = sum(grid[row]) / len(grid[row])
1402
+ p.ghost_fill(voice, density=density, velocity=(20, 50))
1403
+ ```
1404
+ """
1405
+
1406
+ return [
1407
+ [perlin_2d(x_start + xi * x_step, y_start + yi * y_step, seed) for xi in range(x_count)]
1408
+ for yi in range(y_count)
1409
+ ]
1410
+
1411
+
1412
+
1413
+ def logistic_map (r: float, steps: int, x0: float = 0.5) -> typing.List[float]:
1414
+
1415
+ """Generate a deterministic chaos sequence using the logistic map.
1416
+
1417
+ A single parameter ``r`` controls behaviour from stability to chaos:
1418
+ ``r < 3.0`` converges to a fixed point; ``r`` 3.0–3.57 produces
1419
+ periodic oscillations (period-2, -4, -8…); ``r > 3.57`` enters chaos.
1420
+ At ``r ≈ 3.83`` a stable period-3 window briefly returns.
1421
+
1422
+ Complements :func:`perlin_1d` — use Perlin for smooth organic
1423
+ wandering and logistic_map when you need controllable order-to-chaos
1424
+ behaviour. Feeding logistic_map values into ``hit_steps`` probability
1425
+ or ghost note velocity gives ghost notes that are "the same but never
1426
+ exactly the same."
1427
+
1428
+ Parameters:
1429
+ r: Growth rate, typically 0.0–4.0. Values outside [0, 4] will
1430
+ cause ``x`` to diverge; clamp externally if needed.
1431
+ steps: Number of values to generate.
1432
+ x0: Seed value in the open interval (0, 1). Default 0.5.
1433
+
1434
+ Example:
1435
+ ```python
1436
+ # Ghost snare density that hovers at the edge of chaos
1437
+ chaos = subsequence.sequence_utils.logistic_map(r=3.7, steps=16)
1438
+ for i, v in enumerate(chaos):
1439
+ if v > 0.5:
1440
+ p.hit_steps("snare_2", [i], velocity=round(30 + 50 * v), no_overlap=True)
1441
+ ```
1442
+ """
1443
+
1444
+ if steps <= 0:
1445
+ return []
1446
+
1447
+ x = x0
1448
+ result: typing.List[float] = []
1449
+
1450
+ for _ in range(steps):
1451
+ x = r * x * (1.0 - x)
1452
+ result.append(x)
1453
+
1454
+ return result
1455
+
1456
+
1457
+ def pink_noise (steps: int, sources: int = 16, seed: int = 0) -> typing.List[float]:
1458
+
1459
+ """Generate a 1/f (pink) noise sequence using the Voss-McCartney algorithm.
1460
+
1461
+ Pink noise has equal energy per octave — it contains both slow drift
1462
+ and fast jitter in a single signal, matching how musical parameters
1463
+ naturally vary. Voss and Clarke (1978) showed that pitch and loudness
1464
+ fluctuations in real music follow 1/f statistics.
1465
+
1466
+ Sits between :func:`perlin_1d` (smooth, predictable) and
1467
+ :func:`logistic_map` (chaos, controllable order-to-randomness): use
1468
+ pink noise when you want statistically "natural" variation without
1469
+ tuning octave weights manually.
1470
+
1471
+ Parameters:
1472
+ steps: Number of output samples.
1473
+ sources: Number of independent random sources. More sources extend
1474
+ the low-frequency range. Default 16 is a good general value.
1475
+ seed: RNG seed. Same seed always produces the same sequence.
1476
+
1477
+ Returns:
1478
+ List of floats in [0.0, 1.0].
1479
+
1480
+ Example:
1481
+ ```python
1482
+ # Humanise hi-hat velocity with pink noise
1483
+ noise = subsequence.sequence_utils.pink_noise(steps=p.grid, seed=p.bar)
1484
+ for i, level in enumerate(noise):
1485
+ if level > 0.3:
1486
+ p.hit_steps("hi_hat_closed", [i], velocity=round(40 + 50 * level), no_overlap=True)
1487
+ ```
1488
+ """
1489
+
1490
+ if steps <= 0:
1491
+ return []
1492
+
1493
+ if sources <= 0:
1494
+ raise ValueError(f"pink_noise() needs at least one random source to sum — got sources={sources}")
1495
+
1496
+ rng = random.Random(seed)
1497
+
1498
+ source_values = [rng.random() for _ in range(sources)]
1499
+ total = sum(source_values)
1500
+
1501
+ result: typing.List[float] = []
1502
+
1503
+ for i in range(steps):
1504
+ # Count trailing zeros of i+1 to select which source to update.
1505
+ # This distributes updates so lower-indexed sources change less
1506
+ # frequently, creating the 1/f spectral slope.
1507
+ counter = i + 1
1508
+ trailing = 0
1509
+ while counter & 1 == 0 and trailing < sources - 1:
1510
+ trailing += 1
1511
+ counter >>= 1
1512
+
1513
+ old_val = source_values[trailing]
1514
+ new_val = rng.random()
1515
+ source_values[trailing] = new_val
1516
+ total = total - old_val + new_val
1517
+
1518
+ result.append(total / sources)
1519
+
1520
+ # Normalise to [0.0, 1.0].
1521
+ lo = min(result)
1522
+ hi = max(result)
1523
+ if hi > lo:
1524
+ result = [(v - lo) / (hi - lo) for v in result]
1525
+
1526
+ return result
1527
+
1528
+
1529
+ def lsystem_expand (
1530
+ axiom: str,
1531
+ rules: typing.Dict[str, typing.Union[str, typing.List[typing.Tuple[str, float]]]],
1532
+ generations: int,
1533
+ rng: typing.Optional[random.Random] = None,
1534
+ ) -> str:
1535
+
1536
+ """Expand an L-system string by applying production rules.
1537
+
1538
+ An L-system rewrites every symbol in the current string simultaneously,
1539
+ each generation replacing symbols according to ``rules``. After enough
1540
+ generations the string exhibits self-similarity: its large-scale structure
1541
+ mirrors its small-scale structure — the same property found in natural
1542
+ music, where motifs recur at phrase, section, and movement level.
1543
+
1544
+ Symbols not present in ``rules`` pass through unchanged (identity rule).
1545
+ Symbols are single characters; each character in the string is one symbol.
1546
+
1547
+ Rules may be deterministic (a single replacement string) or stochastic
1548
+ (a list of ``(replacement, weight)`` pairs). Stochastic rules require
1549
+ ``rng`` to be provided.
1550
+
1551
+ .. note::
1552
+ String length can grow exponentially. A doubling rule applied for
1553
+ 30 generations produces ~1 billion characters. Keep ``generations``
1554
+ to 3–8 for practical use.
1555
+
1556
+ Parameters:
1557
+ axiom: Initial string (e.g. ``"A"``).
1558
+ rules: Production rules. Deterministic: ``{"A": "AB", "B": "A"}``.
1559
+ Stochastic: ``{"A": [("AB", 3), ("BA", 1)]}`` — weights are
1560
+ relative and do not need to sum to 1.
1561
+ generations: Number of rewriting iterations.
1562
+ rng: Random number generator. Required when any rule is stochastic;
1563
+ ignored for fully deterministic rule sets.
1564
+
1565
+ Returns:
1566
+ Expanded string after ``generations`` iterations.
1567
+
1568
+ Raises:
1569
+ ValueError: If stochastic rules are present but ``rng`` is ``None``.
1570
+
1571
+ Example:
1572
+ ```python
1573
+ # Fibonacci rhythm — hits distributed at golden-ratio spacing
1574
+ expanded = subsequence.sequence_utils.lsystem_expand(
1575
+ axiom="A", rules={"A": "AB", "B": "A"}, generations=6
1576
+ )
1577
+ # expanded is "ABAABABAABAABABAABABA" (length 21, the gen-6 Fibonacci word)
1578
+
1579
+ # Stochastic — different output each bar
1580
+ expanded = subsequence.sequence_utils.lsystem_expand(
1581
+ axiom="A",
1582
+ rules={"A": [("AB", 3), ("BA", 1)]},
1583
+ generations=4,
1584
+ rng=rng,
1585
+ )
1586
+ ```
1587
+ """
1588
+
1589
+ # Validate: stochastic rules need an rng.
1590
+ for production in rules.values():
1591
+ if isinstance(production, list):
1592
+ if rng is None:
1593
+ raise ValueError(
1594
+ "lsystem_expand: rng is required when rules contain stochastic productions"
1595
+ )
1596
+ break
1597
+
1598
+ current = axiom
1599
+
1600
+ for _ in range(generations):
1601
+ parts: typing.List[str] = []
1602
+
1603
+ for symbol in current:
1604
+ if symbol not in rules:
1605
+ parts.append(symbol)
1606
+ continue
1607
+
1608
+ production = rules[symbol]
1609
+
1610
+ if isinstance(production, str):
1611
+ parts.append(production)
1612
+ else:
1613
+ # Stochastic: pick one replacement weighted by the float weights.
1614
+ chosen = weighted_choice(production, rng) # type: ignore[arg-type]
1615
+ parts.append(chosen)
1616
+
1617
+ current = "".join(parts)
1618
+
1619
+ return current
1620
+
1621
+
1622
+ _ca_1d_cache: typing.Dict[typing.Tuple[int, int, int], typing.Tuple[int, typing.List[int]]] = {}
1623
+
1624
+
1625
+ def _ca_1d_initial_state (steps: int, seed: int) -> typing.List[int]:
1626
+
1627
+ """Build the generation-0 row for an elementary CA (``seed=1`` → centre cell)."""
1628
+
1629
+ state = [0] * steps
1630
+
1631
+ if seed == 1:
1632
+ state[steps // 2] = 1
1633
+ else:
1634
+ for i in range(min(steps, seed.bit_length())):
1635
+ if seed & (1 << i):
1636
+ state[i] = 1
1637
+
1638
+ return state
1639
+
1640
+
1641
+ def _ca_1d_step (state: typing.List[int], rule: int, steps: int) -> typing.List[int]:
1642
+
1643
+ """Advance an elementary-CA row by one generation (toroidal neighbourhood)."""
1644
+
1645
+ new_state = [0] * steps
1646
+
1647
+ for i in range(steps):
1648
+ left = state[(i - 1) % steps]
1649
+ center = state[i]
1650
+ right = state[(i + 1) % steps]
1651
+ neighborhood = (left << 2) | (center << 1) | right
1652
+ new_state[i] = (rule >> neighborhood) & 1
1653
+
1654
+ return new_state
1655
+
1656
+
1657
+ def generate_cellular_automaton_1d (steps: int, rule: int = 30, generation: int = 0, seed: int = 1) -> typing.List[int]:
1658
+
1659
+ """Generate a binary sequence using an elementary cellular automaton.
1660
+
1661
+ Evolves a 1D CA from an initial state for the specified number of
1662
+ generations, returning the final state as a binary rhythm. Each
1663
+ generation the pattern evolves — use ``p.cycle`` as the generation
1664
+ to get a rhythm that changes every bar.
1665
+
1666
+ Rule 30 produces "structured chaos" — patterns that look random but
1667
+ have hidden self-similarity. Rule 90 produces fractal (Sierpiński
1668
+ triangle) patterns. Rule 110 is Turing-complete.
1669
+
1670
+ Parameters:
1671
+ steps: Length of the output sequence.
1672
+ rule: Wolfram rule number (0–255). Default 30.
1673
+ generation: Number of generations to evolve from the initial state.
1674
+ seed: Initial state as a bit field. Default 1 (single centre cell).
1675
+
1676
+ Returns:
1677
+ Binary list of length *steps* (0s and 1s).
1678
+
1679
+ Example:
1680
+ ```python
1681
+ seq = subsequence.sequence_utils.generate_cellular_automaton_1d(
1682
+ 16, rule=30, generation=p.cycle
1683
+ )
1684
+ indices = subsequence.sequence_utils.sequence_to_indices(seq)
1685
+ p.hit_steps("snare_1", indices, velocity=35)
1686
+ ```
1687
+ """
1688
+
1689
+ if steps <= 0:
1690
+ return []
1691
+
1692
+ # Memoise the evolution: the common idiom drives `generation` from `p.cycle`,
1693
+ # advancing one generation per bar, so without a cache each bar re-ran every
1694
+ # prior generation from scratch (cost growing without bound over a long set).
1695
+ # The cache holds the latest (generation, state) per (steps, rule, seed) and
1696
+ # advances incrementally; a request for an earlier generation than cached
1697
+ # recomputes from the initial state. Correct because the CA is Markovian —
1698
+ # generation N+1 depends only on generation N.
1699
+ cache_key = (steps, rule, seed)
1700
+ cached = _ca_1d_cache.get(cache_key)
1701
+
1702
+ if cached is not None and cached[0] <= generation:
1703
+ current_gen, state = cached[0], list(cached[1])
1704
+ else:
1705
+ current_gen, state = 0, _ca_1d_initial_state(steps, seed)
1706
+
1707
+ for _ in range(current_gen, generation):
1708
+ state = _ca_1d_step(state, rule, steps)
1709
+
1710
+ _ca_1d_cache[cache_key] = (generation, list(state))
1711
+
1712
+ return state
1713
+
1714
+
1715
+ def _parse_life_rule (rule: str) -> typing.Tuple[typing.Set[int], typing.Set[int]]:
1716
+
1717
+ """Parse a Life-like rule string in Birth/Survival notation.
1718
+
1719
+ Parameters:
1720
+ rule: Rule string in the form ``"B<digits>/S<digits>"``, e.g.
1721
+ ``"B3/S23"`` for Conway's Life or ``"B368/S245"`` for Morley.
1722
+
1723
+ Returns:
1724
+ ``(birth_set, survival_set)`` — sets of neighbour counts that
1725
+ trigger birth or survival respectively.
1726
+
1727
+ Raises:
1728
+ ValueError: If the rule string is not valid Birth/Survival notation.
1729
+ """
1730
+
1731
+ rule = rule.strip().upper()
1732
+ parts = rule.split("/")
1733
+
1734
+ if len(parts) != 2:
1735
+ raise ValueError(f"Invalid Life rule: {rule!r} — expected 'B.../S...' format")
1736
+
1737
+ birth_part, survival_part = parts
1738
+
1739
+ if not birth_part.startswith("B") or not survival_part.startswith("S"):
1740
+ raise ValueError(f"Invalid Life rule: {rule!r} — expected 'B.../S...' format")
1741
+
1742
+ try:
1743
+ birth_set: typing.Set[int] = {int(c) for c in birth_part[1:]}
1744
+ survival_set: typing.Set[int] = {int(c) for c in survival_part[1:]}
1745
+ except ValueError:
1746
+ raise ValueError(f"Invalid Life rule: {rule!r} — neighbour counts must be digits 0–8")
1747
+
1748
+ for n in birth_set | survival_set:
1749
+ if n > 8:
1750
+ raise ValueError(f"Invalid Life rule: {rule!r} — neighbour count {n} exceeds maximum of 8")
1751
+
1752
+ return birth_set, survival_set
1753
+
1754
+
1755
+ _ca_2d_cache: typing.Dict[typing.Tuple[int, int, str, int, float], typing.Tuple[int, typing.List[typing.List[int]]]] = {}
1756
+
1757
+
1758
+ def _ca_2d_initial_grid (rows: int, cols: int, seed: int, density: float) -> typing.List[typing.List[int]]:
1759
+
1760
+ """Build the generation-0 grid for an int-seeded 2D CA."""
1761
+
1762
+ if seed == 1:
1763
+ grid = [[0] * cols for _ in range(rows)]
1764
+ grid[rows // 2][cols // 2] = 1
1765
+ return grid
1766
+
1767
+ rng = random.Random(seed)
1768
+ return [[1 if rng.random() < density else 0 for _ in range(cols)] for _ in range(rows)]
1769
+
1770
+
1771
+ def _ca_2d_step (grid: typing.List[typing.List[int]], rows: int, cols: int, birth_set: typing.Set[int], survival_set: typing.Set[int]) -> typing.List[typing.List[int]]:
1772
+
1773
+ """Advance a Life-like 2D grid by one generation (toroidal Moore neighbourhood)."""
1774
+
1775
+ new_grid = [[0] * cols for _ in range(rows)]
1776
+
1777
+ for r in range(rows):
1778
+ for c in range(cols):
1779
+ neighbours = 0
1780
+
1781
+ for dr in (-1, 0, 1):
1782
+ for dc in (-1, 0, 1):
1783
+ if dr == 0 and dc == 0:
1784
+ continue
1785
+ neighbours += grid[(r + dr) % rows][(c + dc) % cols]
1786
+
1787
+ alive = grid[r][c]
1788
+
1789
+ if alive:
1790
+ new_grid[r][c] = 1 if neighbours in survival_set else 0
1791
+ else:
1792
+ new_grid[r][c] = 1 if neighbours in birth_set else 0
1793
+
1794
+ return new_grid
1795
+
1796
+
1797
+ def generate_cellular_automaton_2d (
1798
+ rows: int,
1799
+ cols: int,
1800
+ rule: str = "B368/S245",
1801
+ generation: int = 0,
1802
+ seed: typing.Union[int, typing.List[typing.List[int]]] = 1,
1803
+ density: float = 0.5,
1804
+ ) -> typing.List[typing.List[int]]:
1805
+
1806
+ """Generate a 2D cellular automaton grid using Life-like rules.
1807
+
1808
+ Evolves a 2D grid of cells from an initial state using Birth/Survival
1809
+ notation rules. The resulting grid maps rows to pitches or instruments
1810
+ and columns to time steps, producing polyphonic rhythmic patterns.
1811
+
1812
+ The default rule B368/S245 (Morley/"Move") produces chaotic, active
1813
+ patterns well-suited to generative music. B3/S23 is Conway's Life.
1814
+
1815
+ Parameters:
1816
+ rows: Number of rows (maps to pitches or instruments).
1817
+ cols: Number of columns (maps to time steps / rhythm grid).
1818
+ rule: Birth/Survival notation, e.g. ``"B3/S23"`` for Conway's Life,
1819
+ ``"B368/S245"`` for Morley.
1820
+ generation: Number of evolution steps to run from the initial seed.
1821
+ seed: Initial grid state. ``1`` places a single live cell at the
1822
+ centre. Any other ``int`` seeds a :class:`random.Random` and
1823
+ fills cells with probability *density*. A
1824
+ ``list[list[int]]`` provides an explicit starting grid (must be
1825
+ rows × cols).
1826
+ density: Fill probability when *seed* is a random int (0.0–1.0).
1827
+
1828
+ Returns:
1829
+ 2D grid as a list of lists (rows × cols), each cell 0 or 1.
1830
+
1831
+ Example:
1832
+ ```python
1833
+ grid = subsequence.sequence_utils.generate_cellular_automaton_2d(
1834
+ rows=4, cols=16, rule="B3/S23", generation=p.cycle, seed=42
1835
+ )
1836
+ for row_idx, pitch in enumerate([60, 62, 64, 67]):
1837
+ hits = [c for c, v in enumerate(grid[row_idx]) if v]
1838
+ p.hit_steps(pitch, hits, velocity=80)
1839
+ ```
1840
+ """
1841
+
1842
+ # A grid with no rows or no columns has no cells to evolve — return the
1843
+ # degenerate empty grid, matching the 1D kernel's steps<=0 no-op.
1844
+ if rows <= 0 or cols <= 0:
1845
+ return [[] for _ in range(max(0, rows))]
1846
+
1847
+ birth_set, survival_set = _parse_life_rule(rule)
1848
+
1849
+ # Memoise int-seeded evolutions incrementally, like the 1D version, so a
1850
+ # `generation`-per-bar idiom doesn't re-run every prior generation each call.
1851
+ # A list seed (an explicit one-off starting grid) is not cached.
1852
+ cache_key: typing.Optional[typing.Tuple[int, int, str, int, float]] = None
1853
+
1854
+ if isinstance(seed, list):
1855
+ current_gen = 0
1856
+ grid = [[int(bool(seed[r][c])) for c in range(cols)] for r in range(rows)]
1857
+ else:
1858
+ cache_key = (rows, cols, rule, seed, density)
1859
+ cached = _ca_2d_cache.get(cache_key)
1860
+ if cached is not None and cached[0] <= generation:
1861
+ current_gen, grid = cached[0], [row[:] for row in cached[1]]
1862
+ else:
1863
+ current_gen, grid = 0, _ca_2d_initial_grid(rows, cols, seed, density)
1864
+
1865
+ for _ in range(current_gen, generation):
1866
+ grid = _ca_2d_step(grid, rows, cols, birth_set, survival_set)
1867
+
1868
+ if cache_key is not None:
1869
+ _ca_2d_cache[cache_key] = (generation, [row[:] for row in grid])
1870
+
1871
+ return grid
1872
+
1873
+
1874
+ def thue_morse (n: int) -> typing.List[int]:
1875
+
1876
+ """
1877
+ Generate the Thue-Morse sequence.
1878
+
1879
+ The Thue-Morse sequence is an infinite aperiodic binary sequence defined
1880
+ by ``t(i) = popcount(i) mod 2`` — the parity of the number of 1-bits in
1881
+ ``i``. It is perfectly balanced (equal density of 0s and 1s over any
1882
+ power-of-two window) and overlap-free: no subsequence occurs three times
1883
+ consecutively. It is self-similar but never strictly periodic, making it
1884
+ useful for rhythmic patterns that feel structured yet avoid monotony.
1885
+
1886
+ Parameters:
1887
+ n: Number of values to generate.
1888
+
1889
+ Returns:
1890
+ Binary list of length ``n`` (0s and 1s).
1891
+
1892
+ Example:
1893
+ ```python
1894
+ # 16-step Thue-Morse rhythm — first 8 values: 0 1 1 0 1 0 0 1
1895
+ seq = subsequence.sequence_utils.thue_morse(16)
1896
+ ```
1897
+ """
1898
+
1899
+ if n <= 0:
1900
+ return []
1901
+
1902
+ return [bin(i).count("1") % 2 for i in range(n)]
1903
+
1904
+
1905
+ _MORSE_CODE = {
1906
+ "a": ".-", "b": "-...", "c": "-.-.", "d": "-..", "e": ".", "f": "..-.",
1907
+ "g": "--.", "h": "....", "i": "..", "j": ".---", "k": "-.-", "l": ".-..",
1908
+ "m": "--", "n": "-.", "o": "---", "p": ".--.", "q": "--.-", "r": ".-.",
1909
+ "s": "...", "t": "-", "u": "..-", "v": "...-", "w": ".--", "x": "-..-",
1910
+ "y": "-.--", "z": "--..",
1911
+ "0": "-----", "1": ".----", "2": "..---", "3": "...--", "4": "....-",
1912
+ "5": ".....", "6": "-....", "7": "--...", "8": "---..", "9": "----.",
1913
+ ".": ".-.-.-", ",": "--..--", "?": "..--..", "'": ".----.", "!": "-.-.--",
1914
+ "/": "-..-.", "(": "-.--.", ")": "-.--.-", "&": ".-...", ":": "---...",
1915
+ ";": "-.-.-.", "=": "-...-", "+": ".-.-.", "-": "-....-", "_": "..--.-",
1916
+ '"': ".-..-.", "$": "...-..-", "@": ".--.-.",
1917
+ }
1918
+
1919
+
1920
+ def morse_code (
1921
+ text: str,
1922
+ dot: float = 0.25,
1923
+ dash: float = 0.75,
1924
+ symbol_gap: float = 0.25,
1925
+ letter_gap: float = 0.75,
1926
+ word_gap: float = 1.75,
1927
+ ) -> typing.List[float]:
1928
+
1929
+ """
1930
+ Translate text into an International Morse Code rhythm.
1931
+
1932
+ Encodes ``text`` as Morse and lays it out as a per-step duration list — the
1933
+ same shape as :func:`generate_legato_durations`, where ``0.0`` marks a rest.
1934
+ Each dot becomes a note of length ``dot`` and each dash a note of length
1935
+ ``dash``; the gaps between elements, characters and words are spans of rests.
1936
+
1937
+ The ``dot`` is the base time unit: every element and gap occupies
1938
+ ``round(duration / dot)`` cells, so the genuine 1:3 dot/dash ratio and the
1939
+ 1/3/7-unit gaps fall straight out of the defaults. Place the result on a
1940
+ grid whose step equals ``dot`` for authentic timing — a dash then sustains
1941
+ across its three cells.
1942
+
1943
+ Pair it with :func:`sequence_to_indices` to get the note positions, then read
1944
+ the matching durations from the list. (Not to be confused with
1945
+ :func:`thue_morse`, the unrelated Thue-Morse mathematical sequence.)
1946
+
1947
+ The full International Morse alphabet is supported: A-Z, 0-9 and the standard
1948
+ punctuation. Input is normalised first — folded to one case (Morse is
1949
+ caseless), runs of whitespace collapsed to a single word gap and trimmed from
1950
+ the ends, and any unencodable character dropped — so arbitrary text is safe.
1951
+
1952
+ Parameters:
1953
+ text: The message to encode.
1954
+ dot: Note length of a dot, and the base time unit (default ``0.25``).
1955
+ dash: Note length of a dash (default ``0.75`` — three units).
1956
+ symbol_gap: Rest between elements within a character (default ``0.25``).
1957
+ letter_gap: Rest between characters (default ``0.75`` — three units).
1958
+ word_gap: Rest between words (default ``1.75`` — seven units).
1959
+
1960
+ Returns:
1961
+ A per-step duration list with ``0.0`` for rests, or ``[]`` when the text
1962
+ has no encodable characters.
1963
+
1964
+ Raises:
1965
+ ValueError: If ``dot`` is not positive — it is the base time unit.
1966
+
1967
+ Example:
1968
+ ```python
1969
+ # "sos" -> the canonical ...---... rhythm.
1970
+ rhythm = subsequence.sequence_utils.morse_code("sos")
1971
+ steps = subsequence.sequence_utils.sequence_to_indices(rhythm)
1972
+ durations = [rhythm[s] for s in steps]
1973
+ p.sequence(steps=steps, pitches=40, durations=durations, velocities=90)
1974
+ ```
1975
+ """
1976
+
1977
+ if dot <= 0:
1978
+ raise ValueError("morse_code: dot must be positive (it is the base time unit)")
1979
+
1980
+ normalised = " ".join(text.lower().split())
1981
+ if not normalised:
1982
+ return []
1983
+
1984
+ words: typing.List[typing.List[str]] = []
1985
+ for word in normalised.split(" "):
1986
+ codes = [_MORSE_CODE[ch] for ch in word if ch in _MORSE_CODE]
1987
+ if codes:
1988
+ words.append(codes)
1989
+
1990
+ if not words:
1991
+ return []
1992
+
1993
+ dash_cells = max(1, round(dash / dot))
1994
+ symbol_cells = max(0, round(symbol_gap / dot))
1995
+ letter_cells = max(0, round(letter_gap / dot))
1996
+ word_cells = max(0, round(word_gap / dot))
1997
+
1998
+ result: typing.List[float] = []
1999
+
2000
+ for w, codes in enumerate(words):
2001
+ if w > 0:
2002
+ result.extend([0.0] * word_cells)
2003
+ for c, code in enumerate(codes):
2004
+ if c > 0:
2005
+ result.extend([0.0] * letter_cells)
2006
+ for e, element in enumerate(code):
2007
+ if e > 0:
2008
+ result.extend([0.0] * symbol_cells)
2009
+ if element == "-":
2010
+ result.append(dash)
2011
+ # These 0.0 cells are occupied by the dash's sustain, not rests —
2012
+ # the duration-at-onset convention, as in generate_legato_durations.
2013
+ result.extend([0.0] * (dash_cells - 1))
2014
+ else:
2015
+ result.append(dot)
2016
+
2017
+ return result
2018
+
2019
+
2020
+ def de_bruijn (k: int, n: int) -> typing.List[int]:
2021
+
2022
+ """
2023
+ Generate a de Bruijn sequence B(k, n).
2024
+
2025
+ A de Bruijn sequence over an alphabet of size ``k`` with window ``n`` is a
2026
+ cyclic sequence in which every possible subsequence of length ``n`` appears
2027
+ exactly once. The output length is ``k ** n``.
2028
+
2029
+ Uses Martin's algorithm (Lyndon word decomposition), which produces a
2030
+ lexicographically minimal sequence with no external dependencies.
2031
+
2032
+ Parameters:
2033
+ k: Alphabet size (number of distinct symbols, e.g. 2 for binary or
2034
+ ``len(pitches)`` for a pitch list).
2035
+ n: Window size. Every ``n``-gram over the ``k`` symbols appears
2036
+ exactly once in the cyclic output.
2037
+
2038
+ Returns:
2039
+ List of integers in ``[0, k-1]`` of length ``k ** n``.
2040
+
2041
+ Example:
2042
+ ```python
2043
+ # Map each integer to a pitch index
2044
+ indices = subsequence.sequence_utils.de_bruijn(3, 2) # length 9
2045
+ pitches = [60, 62, 64]
2046
+ melody = [pitches[i] for i in indices]
2047
+ ```
2048
+ """
2049
+
2050
+ if k <= 0 or n <= 0:
2051
+ return []
2052
+
2053
+ sequence: typing.List[int] = []
2054
+ a = [0] * (n + 1)
2055
+
2056
+ def _db (t: int, p: int) -> None:
2057
+ if t > n:
2058
+ if n % p == 0:
2059
+ sequence.extend(a[1:p + 1])
2060
+ else:
2061
+ a[t] = a[t - p]
2062
+ _db(t + 1, p)
2063
+ for j in range(a[t - p] + 1, k):
2064
+ a[t] = j
2065
+ _db(t + 1, t)
2066
+
2067
+ _db(1, 1)
2068
+ return sequence
2069
+
2070
+
2071
+ def fibonacci_rhythm (steps: int, length: float = 4.0) -> typing.List[float]:
2072
+
2073
+ """
2074
+ Generate beat positions spaced by the golden ratio (Fibonacci spiral).
2075
+
2076
+ Uses the golden angle method: ``position_i = frac(i * φ) * length``, where
2077
+ ``φ = (1 + √5) / 2 ≈ 1.618``. The result is sorted into ascending order.
2078
+ This distributes events with the maximum possible spread — analogous to
2079
+ how sunflower seeds are arranged — producing a quasi-random but
2080
+ aesthetically pleasing timing distribution that is distinct from both
2081
+ even grids (Euclidean) and pure randomness.
2082
+
2083
+ Parameters:
2084
+ steps: Number of beat positions to generate.
2085
+ length: Total span in beats to fill. Defaults to 4.0 (one bar of 4/4).
2086
+
2087
+ Returns:
2088
+ Sorted list of ``steps`` float beat positions in ``[0.0, length)``.
2089
+
2090
+ Example:
2091
+ ```python
2092
+ beats = subsequence.sequence_utils.fibonacci_rhythm(8, length=4.0)
2093
+ for beat in beats:
2094
+ p.note(pitch=60, beat=beat, velocity=80, duration=0.2)
2095
+ ```
2096
+ """
2097
+
2098
+ if steps <= 0:
2099
+ return []
2100
+
2101
+ phi = (1.0 + math.sqrt(5.0)) / 2.0
2102
+
2103
+ # Take the *fractional* part of i·φ to get a low-discrepancy point in [0, 1),
2104
+ # then scale to the span. Applying ``% length`` directly to ``i·φ`` (φ ≈ 1.618,
2105
+ # typically < length) does not equidistribute — it clusters two notes near the
2106
+ # start — so the fractional part is what produces the documented sunflower spread.
2107
+ positions = sorted(((i * phi) % 1.0) * length for i in range(steps))
2108
+ return positions
2109
+
2110
+
2111
+ def lorenz_attractor (
2112
+ steps: int,
2113
+ dt: float = 0.01,
2114
+ sigma: float = 10.0,
2115
+ rho: float = 28.0,
2116
+ beta: float = 8.0 / 3.0,
2117
+ x0: float = 0.1,
2118
+ y0: float = 0.0,
2119
+ z0: float = 0.0,
2120
+ ) -> typing.List[typing.Tuple[float, float, float]]:
2121
+
2122
+ """
2123
+ Integrate the Lorenz attractor and return normalised (x, y, z) tuples.
2124
+
2125
+ The Lorenz system is a set of three coupled differential equations
2126
+ originally derived to model atmospheric convection. Its trajectories
2127
+ orbit a butterfly-shaped strange attractor: deterministic yet sensitive to
2128
+ initial conditions, never exactly repeating. The three axes provide
2129
+ independent but correlated modulation sources — ideal for simultaneously
2130
+ shaping pitch, velocity, and duration from a single generative process.
2131
+
2132
+ Integration uses the Euler method with step ``dt``. Each axis is
2133
+ independently min-max normalised to ``[0.0, 1.0]`` across the full
2134
+ trajectory so that outputs span the full musical range regardless of
2135
+ the chosen parameters.
2136
+
2137
+ Parameters:
2138
+ steps: Number of points to generate.
2139
+ dt: Integration time step. Smaller values produce smoother
2140
+ trajectories. Default 0.01.
2141
+ sigma: Lorenz σ parameter. Default 10.0.
2142
+ rho: Lorenz ρ parameter. Default 28.0 (classic chaotic regime).
2143
+ beta: Lorenz β parameter. Default 8/3.
2144
+ x0: Initial x position. Small changes diverge over time.
2145
+ y0: Initial y position.
2146
+ z0: Initial z position.
2147
+
2148
+ Returns:
2149
+ List of ``(x, y, z)`` tuples, each component in ``[0.0, 1.0]``.
2150
+
2151
+ Example:
2152
+ ```python
2153
+ points = subsequence.sequence_utils.lorenz_attractor(16, x0=p.cycle * 0.001)
2154
+ pitches = [60, 62, 64, 65, 67, 69, 71, 72]
2155
+ for i, (x, y, z) in enumerate(points):
2156
+ pitch = pitches[int(x * len(pitches)) % len(pitches)]
2157
+ vel = int(40 + y * 87)
2158
+ p.note(pitch=pitch, beat=i * 0.25, velocity=vel, duration=0.05 + z * 0.2)
2159
+ ```
2160
+ """
2161
+
2162
+ if steps <= 0:
2163
+ return []
2164
+
2165
+ x, y, z = x0, y0, z0
2166
+ xs: typing.List[float] = []
2167
+ ys: typing.List[float] = []
2168
+ zs: typing.List[float] = []
2169
+
2170
+ for _ in range(steps):
2171
+ dx = sigma * (y - x) * dt
2172
+ dy = (x * (rho - z) - y) * dt
2173
+ dz = (x * y - beta * z) * dt
2174
+ x += dx
2175
+ y += dy
2176
+ z += dz
2177
+ xs.append(x)
2178
+ ys.append(y)
2179
+ zs.append(z)
2180
+
2181
+ def _normalise (values: typing.List[float]) -> typing.List[float]:
2182
+ lo = min(values)
2183
+ hi = max(values)
2184
+ if hi == lo:
2185
+ return [0.5] * len(values)
2186
+ span = hi - lo
2187
+ return [(v - lo) / span for v in values]
2188
+
2189
+ return list(zip(_normalise(xs), _normalise(ys), _normalise(zs)))
2190
+
2191
+
2192
+ def reaction_diffusion_1d (
2193
+ width: int,
2194
+ steps: int = 1000,
2195
+ feed_rate: float = 0.055,
2196
+ kill_rate: float = 0.062,
2197
+ du: float = 0.16,
2198
+ dv: float = 0.08,
2199
+ ) -> typing.List[float]:
2200
+
2201
+ """
2202
+ Simulate a 1D Gray-Scott reaction-diffusion system.
2203
+
2204
+ The Gray-Scott model describes two interacting chemicals U and V
2205
+ on a 1D ring. V is introduced as a small seed in the centre; the
2206
+ simulation evolves until a stable spatial pattern forms. The resulting
2207
+ V-concentration profile — spots, stripes, or travelling waves depending
2208
+ on the feed/kill parameters — is returned as a normalised float sequence.
2209
+
2210
+ This is fundamentally different from cellular automata: the state is
2211
+ continuous, the update rule is a PDE (not a binary function), and the
2212
+ patterns are governed by diffusion rates rather than neighbour counts.
2213
+ The spatial structure maps naturally to a rhythm grid.
2214
+
2215
+ Parameters:
2216
+ width: Number of cells (maps to the pattern's step grid).
2217
+ steps: Number of simulation iterations. More steps = more developed
2218
+ pattern. Default 1000.
2219
+ feed_rate: Rate at which U is replenished. Default 0.055.
2220
+ kill_rate: Rate at which V is removed. Default 0.062.
2221
+ du: Diffusion rate for U. Default 0.16.
2222
+ dv: Diffusion rate for V. Default 0.08.
2223
+
2224
+ Returns:
2225
+ List of ``width`` floats in ``[0.0, 1.0]`` representing final V
2226
+ concentration.
2227
+
2228
+ Example:
2229
+ ```python
2230
+ # Use as a rhythm grid — threshold to place notes
2231
+ conc = subsequence.sequence_utils.reaction_diffusion_1d(16, steps=2000)
2232
+ hits = [i for i, v in enumerate(conc) if v > 0.5]
2233
+ p.hit_steps("kick_1", hits, velocity=90)
2234
+ ```
2235
+ """
2236
+
2237
+ if width <= 0:
2238
+ return []
2239
+
2240
+ u = [1.0] * width
2241
+ v = [0.0] * width
2242
+
2243
+ # Seed the centre half (width//4 to 3*width//4) with a small V concentration.
2244
+ seed_start = width // 4
2245
+ seed_end = 3 * width // 4
2246
+ for i in range(seed_start, seed_end):
2247
+ v[i] = 0.25
2248
+
2249
+ for _ in range(steps):
2250
+
2251
+ new_u = [0.0] * width
2252
+ new_v = [0.0] * width
2253
+
2254
+ for i in range(width):
2255
+ lap_u = u[(i - 1) % width] - 2.0 * u[i] + u[(i + 1) % width]
2256
+ lap_v = v[(i - 1) % width] - 2.0 * v[i] + v[(i + 1) % width]
2257
+ uvv = u[i] * v[i] * v[i]
2258
+ new_u[i] = u[i] + du * lap_u - uvv + feed_rate * (1.0 - u[i])
2259
+ new_v[i] = v[i] + dv * lap_v + uvv - (feed_rate + kill_rate) * v[i]
2260
+
2261
+ u = new_u
2262
+ v = new_v
2263
+
2264
+ lo = min(v)
2265
+ hi = max(v)
2266
+
2267
+ if hi == lo:
2268
+ return [0.0] * width
2269
+
2270
+ span = hi - lo
2271
+ return [(x - lo) / span for x in v]
2272
+
2273
+
2274
+ def self_avoiding_walk (
2275
+ n: int,
2276
+ low: int,
2277
+ high: int,
2278
+ rng: random.Random,
2279
+ start: typing.Optional[int] = None,
2280
+ ) -> typing.List[int]:
2281
+
2282
+ """
2283
+ Generate a self-avoiding random walk on an integer lattice.
2284
+
2285
+ Unlike :func:`random_walk`, which can revisit any position, this walk
2286
+ tracks all visited positions and avoids them. Each step moves ±1 to an
2287
+ unvisited neighbour. When the walk is trapped (all neighbours have been
2288
+ visited), the visited set is reset at the current position and the walk
2289
+ continues — this creates natural phrase boundaries with a fresh sense of
2290
+ direction after each reset.
2291
+
2292
+ The constraint guarantees pitch diversity: within each "phrase" (before a
2293
+ reset), no pitch is repeated and the melody explores the range in a
2294
+ continuous, step-wise manner.
2295
+
2296
+ Parameters:
2297
+ n: Number of values to generate.
2298
+ low: Minimum value (inclusive).
2299
+ high: Maximum value (inclusive).
2300
+ rng: Random number generator instance.
2301
+ start: Starting value. Defaults to the midpoint of ``[low, high]``.
2302
+
2303
+ Returns:
2304
+ List of ``n`` integers in ``[low, high]``.
2305
+
2306
+ Example:
2307
+ ```python
2308
+ # Self-avoiding bassline across a 2-octave range (MIDI 40–64)
2309
+ notes = subsequence.sequence_utils.self_avoiding_walk(16, low=40, high=64, rng=p.rng)
2310
+ ```
2311
+ """
2312
+
2313
+ if n <= 0:
2314
+ return []
2315
+
2316
+ if low > high:
2317
+ raise ValueError(f"low ({low}) must be <= high ({high})")
2318
+
2319
+ if start is not None:
2320
+ current = max(low, min(high, start))
2321
+ else:
2322
+ current = (low + high) // 2
2323
+
2324
+ visited: typing.Set[int] = {current}
2325
+ result: typing.List[int] = [current]
2326
+
2327
+ for _ in range(n - 1):
2328
+ candidates = [
2329
+ p for p in (current - 1, current + 1)
2330
+ if low <= p <= high and p not in visited
2331
+ ]
2332
+
2333
+ if not candidates:
2334
+ # Trapped: reset visited and pick any valid neighbour.
2335
+ visited = {current}
2336
+ candidates = [p for p in (current - 1, current + 1) if low <= p <= high]
2337
+
2338
+ if not candidates:
2339
+ # Range is a single value; stay put.
2340
+ result.append(current)
2341
+ continue
2342
+
2343
+ current = rng.choice(candidates)
2344
+ visited.add(current)
2345
+ result.append(current)
2346
+
2347
+ return result
2348
+
2349
+
2350
+ def _branch_retrograde (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2351
+
2352
+ """Reverse the pitch order (rhythm untouched — callers own timing)."""
2353
+
2354
+ return list(reversed(seq))
2355
+
2356
+
2357
+ def _branch_invert (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2358
+
2359
+ """Mirror each pitch around the root (the sequence's first note)."""
2360
+
2361
+ return [root + (root - p) for p in seq]
2362
+
2363
+
2364
+ def _branch_transpose (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2365
+
2366
+ """Shift all pitches by the interval between the first two notes."""
2367
+
2368
+ return [p + interval for p in seq]
2369
+
2370
+
2371
+ def _branch_rotate (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2372
+
2373
+ """Rotate the pitch order by one position (first note moves to the end)."""
2374
+
2375
+ if not seq:
2376
+ return seq
2377
+
2378
+ return seq[1:] + seq[:1]
2379
+
2380
+
2381
+ def _branch_compress (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2382
+
2383
+ """Halve every interval from the root, rounded to the nearest semitone."""
2384
+
2385
+ return [root + round((p - root) * 0.5) for p in seq]
2386
+
2387
+
2388
+ def _branch_expand (seq: typing.List[int], root: int, interval: int) -> typing.List[int]:
2389
+
2390
+ """Double every interval from the root."""
2391
+
2392
+ return [root + round((p - root) * 2.0) for p in seq]
2393
+
2394
+
2395
+ # Indexed by the tree RNG — the ORDER of this list is part of branch()'s
2396
+ # deterministic output contract; append new transforms, never reorder.
2397
+ _BRANCH_TRANSFORMS = [
2398
+ _branch_retrograde,
2399
+ _branch_invert,
2400
+ _branch_transpose,
2401
+ _branch_rotate,
2402
+ _branch_compress,
2403
+ _branch_expand,
2404
+ ]
2405
+
2406
+
2407
+ def branch_sequence (
2408
+ pitches: typing.List[int],
2409
+ depth: int = 2,
2410
+ path: int = 0,
2411
+ mutation: float = 0.0,
2412
+ rng: typing.Optional[random.Random] = None,
2413
+ ) -> typing.List[int]:
2414
+
2415
+ """
2416
+ Navigate a fractal tree of pitch-sequence transforms and return one variation.
2417
+
2418
+ The ``pitches`` sequence is the trunk. At each of ``depth`` levels two
2419
+ transforms are assigned deterministically (derived from the pitch content
2420
+ itself, so the tree is identical for the same trunk regardless of any
2421
+ seed), and ``path`` selects left or right per level — ``2 ** depth``
2422
+ variations before the index wraps. The transforms are order and interval
2423
+ operations (retrograde, inversion, transposition, rotation, interval
2424
+ compression/expansion): deliberately rhythm-free, so the caller owns
2425
+ timing. Feed the result to ``Motif.notes()`` for a storable variation,
2426
+ or let ``p.branch()`` place it directly.
2427
+
2428
+ Parameters:
2429
+ pitches: The trunk — absolute MIDI pitches. All variations derive
2430
+ from this.
2431
+ depth: Branching levels (``2 ** depth`` variations).
2432
+ path: Which variation (0-based; wraps modulo ``2 ** depth``).
2433
+ mutation: Probability per step of substituting a random trunk pitch
2434
+ on top of the deterministic branching.
2435
+ rng: Random source for ``mutation`` draws only (the tree itself is
2436
+ deterministic). Unseeded when omitted.
2437
+
2438
+ Returns:
2439
+ A pitch list the same length as the trunk.
2440
+ """
2441
+
2442
+ if not pitches:
2443
+ raise ValueError("pitches list cannot be empty")
2444
+
2445
+ # Tree RNG derived from the pitch content (never the caller's rng) so the
2446
+ # tree structure is always the same for the same trunk. hash() here is
2447
+ # deterministic across runs: PYTHONHASHSEED randomises only str/bytes
2448
+ # hashes, never ints or tuples of ints.
2449
+ branch_rng = random.Random(hash(tuple(pitches)))
2450
+
2451
+ path_index = path % (2 ** depth)
2452
+ sequence = list(pitches)
2453
+ root = pitches[0]
2454
+ interval = (pitches[1] - pitches[0]) if len(pitches) >= 2 else 0
2455
+
2456
+ for level in range(depth):
2457
+ left = _BRANCH_TRANSFORMS[branch_rng.randint(0, len(_BRANCH_TRANSFORMS) - 1)]
2458
+ right = _BRANCH_TRANSFORMS[branch_rng.randint(0, len(_BRANCH_TRANSFORMS) - 1)]
2459
+ direction = (path_index >> level) & 1
2460
+ sequence = left(sequence, root, interval) if direction == 0 else right(sequence, root, interval)
2461
+
2462
+ if mutation > 0.0:
2463
+
2464
+ if rng is None:
2465
+ rng = random.Random()
2466
+
2467
+ for i in range(len(sequence)):
2468
+ if rng.random() < mutation:
2469
+ sequence[i] = rng.choice(pitches)
2470
+
2471
+ return sequence
2472
+
2473
+
2474
+ def build_metric_weights (time_signature: typing.Tuple[int, int] = (4, 4), grid: int = 16) -> typing.List[float]:
2475
+
2476
+ """
2477
+ Per-step metric weights for one bar — how "strong" each grid position is.
2478
+
2479
+ The hierarchy: the downbeat is 1.0; the half-bar (even meters only) is
2480
+ 0.75; other beats are 0.5; off-beat eighths are 0.25; everything finer is
2481
+ 0.125. Derived from the time signature by default; pass a custom weight
2482
+ list instead wherever a metric table is accepted (additive and
2483
+ non-isochronous meters define their own strong beats).
2484
+
2485
+ Parameters:
2486
+ time_signature: ``(beats_per_bar, beat_unit)`` — only the beat count
2487
+ shapes the table.
2488
+ grid: Number of equal steps across the bar.
2489
+
2490
+ Returns:
2491
+ ``grid`` floats in step order.
2492
+
2493
+ Example:
2494
+ ```python
2495
+ build_metric_weights((4, 4), grid=8)
2496
+ # → [1.0, 0.25, 0.5, 0.25, 0.75, 0.25, 0.5, 0.25]
2497
+ ```
2498
+ """
2499
+
2500
+ if grid < 1:
2501
+ raise ValueError(f"grid must be at least 1 — got {grid}")
2502
+
2503
+ beats_per_bar = time_signature[0]
2504
+ weights = []
2505
+
2506
+ for i in range(grid):
2507
+
2508
+ numerator = i * beats_per_bar # beat position = numerator / grid
2509
+
2510
+ if i == 0:
2511
+ weights.append(1.0)
2512
+ elif beats_per_bar % 2 == 0 and 2 * i == grid:
2513
+ weights.append(0.75)
2514
+ elif numerator % grid == 0:
2515
+ weights.append(0.5)
2516
+ elif (2 * numerator) % grid == 0:
2517
+ weights.append(0.25)
2518
+ else:
2519
+ weights.append(0.125)
2520
+
2521
+ return weights
2522
+
2523
+
2524
+ def vl_distance (
2525
+ source: typing.Sequence[int],
2526
+ target: typing.Sequence[int],
2527
+ pitch_classes: bool = True,
2528
+ ) -> int:
2529
+
2530
+ """Voice-leading distance between two chords (Tymoczko's taxicab metric).
2531
+
2532
+ The smallest total semitone movement that turns *source* into *target*,
2533
+ minimised over every way of assigning source notes to target notes. When
2534
+ the chords have different sizes, notes of the smaller chord may be doubled
2535
+ (every note of both chords takes part — nothing is dropped).
2536
+
2537
+ Parameters:
2538
+ source: Pitches of the first chord.
2539
+ target: Pitches of the second chord.
2540
+ pitch_classes: When True (default), pitches are reduced mod 12 and each
2541
+ voice moves by the shortest path around the pitch-class circle (a
2542
+ tritone is 6). When False, pitches are absolute MIDI notes and
2543
+ each voice moves by its literal semitone interval — use this to
2544
+ score concrete voicings rather than abstract chords.
2545
+
2546
+ Returns:
2547
+ Total semitone movement (an int).
2548
+
2549
+ Example:
2550
+ ```python
2551
+ vl_distance([0, 4, 7], [9, 0, 4]) # C → Am: G moves to A, distance 2
2552
+ vl_distance([0, 4, 7], [5, 9, 0]) # C → F: distance 3
2553
+ ```
2554
+ """
2555
+
2556
+ if not source or not target:
2557
+ raise ValueError("vl_distance needs at least one pitch in each chord")
2558
+
2559
+ if pitch_classes:
2560
+ left = [pitch % 12 for pitch in source]
2561
+ right = [pitch % 12 for pitch in target]
2562
+ else:
2563
+ left = list(source)
2564
+ right = list(target)
2565
+
2566
+ def step (a: int, b: int) -> int:
2567
+ if pitch_classes:
2568
+ diff = abs(a - b) % 12
2569
+ return min(diff, 12 - diff)
2570
+ return abs(a - b)
2571
+
2572
+ # Orient so `small` is doubled into `large`: every large note is assigned
2573
+ # one small note, and every small note must be used at least once.
2574
+ small, large = (left, right) if len(left) <= len(right) else (right, left)
2575
+
2576
+ best: typing.Optional[int] = None
2577
+
2578
+ for assignment in itertools.product(range(len(small)), repeat = len(large)):
2579
+
2580
+ if len(set(assignment)) < len(small):
2581
+ continue # a small-chord note was dropped — not a voice leading
2582
+
2583
+ total = sum(step(small[index], large[position]) for position, index in enumerate(assignment))
2584
+
2585
+ if best is None or total < best:
2586
+ best = total
2587
+
2588
+ assert best is not None # guaranteed: the identity-style assignment always survives
2589
+ return best
2590
+
2591
+
2592
+ def _constraint_label (node: typing.Any) -> str:
2593
+
2594
+ """A readable label for a graph node in constraint error messages."""
2595
+
2596
+ name = getattr(node, "name", None)
2597
+
2598
+ if callable(name):
2599
+ return str(name())
2600
+
2601
+ return repr(node)
2602
+
2603
+
2604
+ def constrained_walk (
2605
+ graph: "subsequence.weighted_graph.WeightedGraph[T]",
2606
+ start: T,
2607
+ length: int,
2608
+ rng: random.Random,
2609
+ pins: typing.Optional[typing.Dict[int, T]] = None,
2610
+ end: typing.Optional[T] = None,
2611
+ avoid: typing.Optional[typing.Collection[T]] = None,
2612
+ weight_modifier: typing.Optional[typing.Callable[[T, T, int], float]] = None,
2613
+ before_choice: typing.Optional[typing.Callable[[T], None]] = None,
2614
+ after_choice: typing.Optional[typing.Callable[[T], None]] = None,
2615
+ ) -> typing.List[T]:
2616
+
2617
+ """Walk a weighted graph under constraints — the shared hybrid kernel.
2618
+
2619
+ A backward **feasibility** pass (boolean reachability of every pin, so
2620
+ unsatisfiability is known before a note is emitted) followed by a forward
2621
+ walk through the graph's **real, possibly history-dependent** weights
2622
+ masked to surviving candidates. This guarantees *satisfaction*, not an
2623
+ exact conditional distribution — history-dependent weighting (NIR,
2624
+ gravity, diversity) keeps its character rather than being flattened.
2625
+
2626
+ Positions are **1-based** (the musician count); position 1 is *start*,
2627
+ which is fixed — pins may name it only redundantly. ``avoid`` applies
2628
+ to the chosen positions (2..length); the start is exempt.
2629
+
2630
+ With no constraints, the walk consumes the RNG exactly as repeated
2631
+ ``graph.choose_next()`` calls would (one draw per step, same candidate
2632
+ order), so an unconstrained walk reproduces the unconstrained engine.
2633
+
2634
+ Parameters:
2635
+ graph: The transition graph.
2636
+ start: The node at position 1.
2637
+ length: Total walk length, including *start*.
2638
+ rng: Random stream for the weighted draws.
2639
+ pins: ``{position: node}`` — the node that MUST sound at a 1-based
2640
+ position.
2641
+ end: The node at the final position — sugar for ``pins[length]``.
2642
+ avoid: Nodes excluded from every chosen position. Naming a node
2643
+ the graph does not contain is allowed (trivially satisfied).
2644
+ weight_modifier: ``fn(source, target, weight) -> float`` — the
2645
+ engine's soft weighting, applied inside the feasible mask. If
2646
+ it suppresses every feasible candidate at some step (all
2647
+ modifiers <= 0), the step falls back to the unmodified weights
2648
+ (stall detection) — satisfaction beats character.
2649
+ before_choice: Called with the source node before each draw — the
2650
+ seam for history bookkeeping (append the source to history, as
2651
+ the live engine's ``step()`` does, so *weight_modifier* sees
2652
+ the same context it would live).
2653
+ after_choice: Called with each chosen node after its draw.
2654
+
2655
+ Returns:
2656
+ The walked nodes, ``[start, ...]``, exactly *length* long.
2657
+
2658
+ Raises:
2659
+ ValueError: If the constraints are contradictory or unsatisfiable —
2660
+ before any RNG draw, naming the failing position.
2661
+ """
2662
+
2663
+ if length < 1:
2664
+ raise ValueError(f"a walk needs at least one position, got length={length}")
2665
+
2666
+ pins = dict(pins) if pins else {}
2667
+
2668
+ if end is not None:
2669
+ if length in pins and pins[length] != end:
2670
+ raise ValueError(
2671
+ f"end={_constraint_label(end)} conflicts with pins[{length}]="
2672
+ f"{_constraint_label(pins[length])} — they name the same position"
2673
+ )
2674
+ pins[length] = end
2675
+
2676
+ for position in pins:
2677
+ if not 1 <= position <= length:
2678
+ raise ValueError(f"pin position {position} is outside the walk (1–{length})")
2679
+
2680
+ avoid_set = set(avoid) if avoid else set()
2681
+
2682
+ for position, node in pins.items():
2683
+ if node in avoid_set:
2684
+ raise ValueError(
2685
+ f"pins[{position}]={_constraint_label(node)} is also in avoid — "
2686
+ "a chord cannot be both required and forbidden"
2687
+ )
2688
+
2689
+ if 1 in pins and pins[1] != start:
2690
+ raise ValueError(
2691
+ f"pins[1]={_constraint_label(pins[1])} conflicts with the walk's start "
2692
+ f"({_constraint_label(start)}) — position 1 is where the walk begins"
2693
+ )
2694
+
2695
+ all_nodes = graph.nodes()
2696
+
2697
+ for position, node in pins.items():
2698
+ if node not in all_nodes and node != start:
2699
+ raise ValueError(
2700
+ f"pins[{position}]={_constraint_label(node)} is not in this graph's "
2701
+ "vocabulary — it can never sound"
2702
+ )
2703
+
2704
+ if length == 1:
2705
+ return [start]
2706
+
2707
+ # Backward feasibility: ok[i] = nodes allowed at position i from which a
2708
+ # valid completion exists. Position 1 is the fixed start.
2709
+ def allowed (position: int) -> typing.List[T]:
2710
+ if position in pins:
2711
+ return [pins[position]]
2712
+ return [node for node in all_nodes if node not in avoid_set]
2713
+
2714
+ ok: typing.Dict[int, typing.Set[T]] = {length: set(allowed(length))}
2715
+
2716
+ if not ok[length]:
2717
+ raise ValueError(f"no chord can satisfy the constraints at position {length}")
2718
+
2719
+ for position in range(length - 1, 1, -1):
2720
+ ok[position] = {
2721
+ node for node in allowed(position)
2722
+ if any(target in ok[position + 1] for target, _ in graph.get_transitions(node))
2723
+ }
2724
+ if not ok[position]:
2725
+ raise ValueError(
2726
+ f"no chord can satisfy the constraints at position {position} — "
2727
+ "there is no way through to the pins after it"
2728
+ )
2729
+
2730
+ if not any(target in ok[2] for target, _ in graph.get_transitions(start)):
2731
+ raise ValueError(
2732
+ f"no path from {_constraint_label(start)} satisfies the constraints "
2733
+ f"(pins at {sorted(pins)}, {len(avoid_set)} avoided) within {length} positions"
2734
+ )
2735
+
2736
+ # Forward walk: real weights, masked to the feasible sets.
2737
+ walk: typing.List[T] = [start]
2738
+
2739
+ for position in range(2, length + 1):
2740
+
2741
+ source = walk[-1]
2742
+
2743
+ if before_choice is not None:
2744
+ before_choice(source)
2745
+
2746
+ candidates: typing.List[typing.Tuple[T, float]] = []
2747
+ total = 0.0
2748
+
2749
+ for target, weight in graph.get_transitions(source):
2750
+
2751
+ if target not in ok[position]:
2752
+ continue
2753
+
2754
+ modifier = 1.0 if weight_modifier is None else float(weight_modifier(source, target, weight))
2755
+
2756
+ if modifier <= 0:
2757
+ continue
2758
+
2759
+ adjusted = float(weight) * modifier
2760
+ candidates.append((target, adjusted))
2761
+ total += adjusted
2762
+
2763
+ if total <= 0:
2764
+ # Stall: the soft weighting suppressed every feasible candidate.
2765
+ # Satisfaction beats character — fall back to the bare weights.
2766
+ candidates = [
2767
+ (target, float(weight))
2768
+ for target, weight in graph.get_transitions(source)
2769
+ if target in ok[position]
2770
+ ]
2771
+ total = sum(weight for _, weight in candidates)
2772
+
2773
+ roll = rng.uniform(0, total)
2774
+ accum = 0.0
2775
+ chosen = candidates[-1][0]
2776
+
2777
+ for target, adjusted in candidates:
2778
+ accum += adjusted
2779
+ if roll <= accum:
2780
+ chosen = target
2781
+ break
2782
+
2783
+ if after_choice is not None:
2784
+ after_choice(chosen)
2785
+
2786
+ walk.append(chosen)
2787
+
2788
+ return walk
2789
+
2790
+
2791
+ def cseg (pitches: typing.Sequence[float]) -> typing.List[int]:
2792
+
2793
+ """Contour segment: each pitch's rank within the line (Morris's CSEG).
2794
+
2795
+ The melodic shape abstracted from exact pitch: ``[60, 67, 64]`` and
2796
+ ``[50, 59, 55]`` both rank ``[0, 2, 1]``. Equal pitches share a rank.
2797
+
2798
+ Example:
2799
+ ```python
2800
+ cseg([60, 67, 64]) # → [0, 2, 1]
2801
+ cseg([5, 5, 3]) # → [1, 1, 0]
2802
+ ```
2803
+ """
2804
+
2805
+ if not pitches:
2806
+ return []
2807
+
2808
+ distinct = sorted(set(pitches))
2809
+
2810
+ return [distinct.index(pitch) for pitch in pitches]
2811
+
2812
+
2813
+ def csim (a: typing.Sequence[float], b: typing.Sequence[float]) -> float:
2814
+
2815
+ """Contour similarity between two equal-length lines (Marvin/Laprade CSIM).
2816
+
2817
+ The fraction of pairwise order relations (above/below/equal) the two
2818
+ contours share — 1.0 for identical shapes, regardless of exact pitch.
2819
+
2820
+ Raises ``ValueError`` for mismatched lengths (similarity between
2821
+ different-length contours is not defined here).
2822
+ """
2823
+
2824
+ if len(a) != len(b):
2825
+ raise ValueError(f"csim() compares equal-length lines — got {len(a)} and {len(b)}")
2826
+
2827
+ count = len(a)
2828
+
2829
+ if count < 2:
2830
+ return 1.0
2831
+
2832
+ def relation (x: float, y: float) -> int:
2833
+ return (x > y) - (x < y)
2834
+
2835
+ agreements = 0
2836
+ pairs = 0
2837
+
2838
+ for i in range(count):
2839
+ for j in range(i + 1, count):
2840
+ pairs += 1
2841
+ if relation(a[i], a[j]) == relation(b[i], b[j]):
2842
+ agreements += 1
2843
+
2844
+ return agreements / pairs
2845
+
2846
+
2847
+ # ---------------------------------------------------------------------------
2848
+ # Xenakis sieves — one integer-set kernel serving scales, pitch pools, rhythm
2849
+ # grids, and bar selection alike (the experimental composer's primitive).
2850
+ # ---------------------------------------------------------------------------
2851
+
2852
+
2853
+ def sieve (
2854
+ classes: typing.Sequence[typing.Tuple[int, int]],
2855
+ hi: int,
2856
+ lo: int = 0,
2857
+ ) -> typing.List[int]:
2858
+
2859
+ """Xenakis sieve: the sorted integers in ``[lo, hi)`` in any of the classes.
2860
+
2861
+ A sieve (Xenakis's *crible*) is a logical formula over **residual
2862
+ classes** that denotes a subset of the integers. This primary form takes
2863
+ a list of ``(modulus, residue)`` pairs and returns their **union** over a
2864
+ bounded range — every ``x`` in ``[lo, hi)`` with ``x % modulus == residue``
2865
+ for at least one class. The integers index *any* ordered parameter, so
2866
+ one kernel builds custom scales (over 0–11 semitones), non-octave pitch
2867
+ pools, rhythm grids, and bar-selection masks.
2868
+
2869
+ For intersection and complement, compose :func:`residual_class` objects
2870
+ with ``&``, ``|``, ``~`` and evaluate the result (see :class:`Sieve`).
2871
+
2872
+ Parameters:
2873
+ classes: ``(modulus, residue)`` pairs. ``modulus`` must be ≥ 1; the
2874
+ residue is taken modulo the modulus.
2875
+ hi: Exclusive upper bound.
2876
+ lo: Inclusive lower bound (default 0).
2877
+
2878
+ Returns:
2879
+ The sorted, de-duplicated integers in range that satisfy any class.
2880
+
2881
+ Raises:
2882
+ ValueError: If a modulus is below 1.
2883
+
2884
+ Example:
2885
+ ```python
2886
+ sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12)
2887
+ # → [0, 2, 4, 5, 7, 9, 11] — the major scale as a sieve
2888
+ sieve([(2, 0)], hi=12) # → [0, 2, 4, 6, 8, 10] — whole-tone
2889
+ sieve([(5, 0), (7, 1)], lo=60, hi=96) # a non-octave pitch pool
2890
+ ```
2891
+ """
2892
+
2893
+ for modulus, _residue in classes:
2894
+ if modulus < 1:
2895
+ raise ValueError(f"sieve modulus must be at least 1 — got {modulus}")
2896
+
2897
+ hits = {
2898
+ x
2899
+ for x in range(lo, hi)
2900
+ for modulus, residue in classes
2901
+ if x % modulus == residue % modulus
2902
+ }
2903
+
2904
+ return sorted(hits)
2905
+
2906
+
2907
+ class Sieve:
2908
+
2909
+ """A composable Xenakis sieve — residual classes under ``&`` ``|`` ``~``.
2910
+
2911
+ The full algebra layer over :func:`sieve`: build a :class:`Sieve` with
2912
+ :func:`residual_class` and combine with union (``|``), intersection
2913
+ (``&``), and complement (``~``), then :meth:`evaluate` over a range.
2914
+ Membership is decided per integer, so complement is well-defined
2915
+ without a range until evaluation.
2916
+
2917
+ Example:
2918
+ ```python
2919
+ # Multiples of 2 or 3, excluding steps one past a multiple of 4.
2920
+ evens = subsequence.sequence_utils.residual_class(2, 0)
2921
+ thirds = subsequence.sequence_utils.residual_class(3, 0)
2922
+ off_fours = subsequence.sequence_utils.residual_class(4, 1)
2923
+ ((evens | thirds) & ~off_fours).evaluate(hi=24)
2924
+ ```
2925
+ """
2926
+
2927
+ def __init__ (self, predicate: typing.Callable[[int], bool]) -> None:
2928
+
2929
+ """Wrap a membership predicate (use :func:`residual_class` to make one)."""
2930
+
2931
+ self._predicate = predicate
2932
+
2933
+ def __contains__ (self, value: int) -> bool:
2934
+
2935
+ """Membership test for a single integer."""
2936
+
2937
+ return self._predicate(value)
2938
+
2939
+ def __or__ (self, other: "Sieve") -> "Sieve":
2940
+
2941
+ """Union — in either sieve."""
2942
+
2943
+ return Sieve(lambda x: self._predicate(x) or other._predicate(x))
2944
+
2945
+ def __and__ (self, other: "Sieve") -> "Sieve":
2946
+
2947
+ """Intersection — in both sieves."""
2948
+
2949
+ return Sieve(lambda x: self._predicate(x) and other._predicate(x))
2950
+
2951
+ def __invert__ (self) -> "Sieve":
2952
+
2953
+ """Complement — every integer NOT in this sieve."""
2954
+
2955
+ return Sieve(lambda x: not self._predicate(x))
2956
+
2957
+ def evaluate (self, hi: int, lo: int = 0) -> typing.List[int]:
2958
+
2959
+ """The sorted integers in ``[lo, hi)`` that belong to this sieve."""
2960
+
2961
+ return [x for x in range(lo, hi) if self._predicate(x)]
2962
+
2963
+
2964
+ def residual_class (modulus: int, residue: int) -> Sieve:
2965
+
2966
+ """A single residual class ``{x : x % modulus == residue}`` as a :class:`Sieve`.
2967
+
2968
+ The atom of sieve algebra (Xenakis's notation ``modulus @ residue``).
2969
+ Combine with ``&`` ``|`` ``~`` and call :meth:`Sieve.evaluate`.
2970
+ """
2971
+
2972
+ if modulus < 1:
2973
+ raise ValueError(f"residual-class modulus must be at least 1 — got {modulus}")
2974
+
2975
+ reduced = residue % modulus
2976
+
2977
+ return Sieve(lambda x: x % modulus == reduced)
2978
+
2979
+
2980
+ # ---------------------------------------------------------------------------
2981
+ # Toussaint rhythm measures — evenness, off-beatness, syncopation (analysis
2982
+ # primitives from "The Geometry of Musical Rhythm").
2983
+ # ---------------------------------------------------------------------------
2984
+
2985
+
2986
+ def rhythmic_evenness (onsets: typing.Sequence[int], grid: int, normalize: bool = True) -> float:
2987
+
2988
+ """How evenly onsets are spread around the cycle (Toussaint's evenness).
2989
+
2990
+ Places the *grid* pulses as equally spaced points on a unit circle and
2991
+ sums the chord lengths between every pair of onsets (``2·sin(π·d/grid)``
2992
+ for a pulse distance ``d``). A maximally even rhythm (a regular polygon —
2993
+ the Euclidean rhythms) maximises this sum; clustered onsets minimise it.
2994
+
2995
+ Parameters:
2996
+ onsets: 0-based onset indices (duplicates and out-of-range values are
2997
+ reduced modulo *grid* and de-duplicated).
2998
+ grid: Pulses in the cycle.
2999
+ normalize: Divide by the maximally-even sum for *k* onsets, giving
3000
+ ``(0, 1]`` (1.0 = perfectly even). ``False`` returns the raw sum.
3001
+
3002
+ Returns:
3003
+ The evenness, in ``(0, 1]`` when normalised (1.0 for fewer than two
3004
+ onsets).
3005
+
3006
+ Example:
3007
+ ```python
3008
+ rhythmic_evenness([0, 3, 6], 8) # tresillo — near 1.0
3009
+ rhythmic_evenness([0, 1, 2], 8) # clustered — much lower
3010
+ ```
3011
+ """
3012
+
3013
+ if grid < 1:
3014
+ raise ValueError(f"grid must be at least 1 — got {grid}")
3015
+
3016
+ reduced = sorted({o % grid for o in onsets})
3017
+ k = len(reduced)
3018
+
3019
+ if k < 2:
3020
+ return 1.0
3021
+
3022
+ raw = sum(
3023
+ 2.0 * math.sin(math.pi * abs(reduced[a] - reduced[b]) / grid)
3024
+ for a in range(k)
3025
+ for b in range(a + 1, k)
3026
+ )
3027
+
3028
+ if not normalize:
3029
+ return raw
3030
+
3031
+ # The maximally-even arrangement of k points on the grid is the regular
3032
+ # k-gon: pairwise distances 2·sin(π·i/k) for each of the k rotations.
3033
+ maximum = sum(2.0 * math.sin(math.pi * i / k) * (k - i) for i in range(1, k))
3034
+
3035
+ return raw / maximum if maximum > 0 else 1.0
3036
+
3037
+
3038
+ def offbeatness (onsets: typing.Sequence[int], grid: int) -> int:
3039
+
3040
+ """How many onsets fall on intrinsically off-beat pulses (Toussaint).
3041
+
3042
+ The on-beat pulses are the vertices of every regular sub-polygon of the
3043
+ cycle (the divisor meters); the off-beat pulses are exactly those coprime
3044
+ to *grid*. Off-beatness counts onsets landing on coprime positions — a
3045
+ meter-independent syncopation flavour (high for rhythms that fight every
3046
+ even subdivision).
3047
+
3048
+ Parameters:
3049
+ onsets: 0-based onset indices (reduced modulo *grid*, de-duplicated).
3050
+ grid: Pulses in the cycle.
3051
+
3052
+ Returns:
3053
+ The count of distinct onsets on off-beat (coprime) pulses.
3054
+
3055
+ Example:
3056
+ ```python
3057
+ offbeatness([0, 4, 8, 12], 16) # 0 — all on the strong polygon
3058
+ offbeatness([0, 3, 6, 10, 13], 16) # 2 — bossa, off-beats on pulses 3 and 13
3059
+ ```
3060
+ """
3061
+
3062
+ if grid < 1:
3063
+ raise ValueError(f"grid must be at least 1 — got {grid}")
3064
+
3065
+ reduced = {o % grid for o in onsets}
3066
+
3067
+ # The downbeat (pulse 0) is always on-beat; the explicit guard also handles
3068
+ # grid==1, where gcd(0, 1) == 1 would otherwise misclassify it.
3069
+ return sum(1 for p in reduced if p != 0 and math.gcd(p, grid) == 1)
3070
+
3071
+
3072
+ def syncopation (
3073
+ onsets: typing.Sequence[int],
3074
+ grid: int,
3075
+ time_signature: typing.Tuple[int, int] = (4, 4),
3076
+ weights: typing.Optional[typing.Sequence[float]] = None,
3077
+ ) -> float:
3078
+
3079
+ """How much a rhythm pulls away from its metric strong points.
3080
+
3081
+ A weighted off-beat measure: each onset contributes ``max_weight −
3082
+ weight_at_its_pulse`` from the metric-weight table (so onsets on the
3083
+ downbeat add nothing, onsets on the weakest pulses add the most),
3084
+ normalised by the onset count. Uses :func:`build_metric_weights` by
3085
+ default; pass a custom per-pulse *weights* list for additive or
3086
+ non-isochronous meters.
3087
+
3088
+ Parameters:
3089
+ onsets: 0-based onset indices (reduced modulo *grid*, de-duplicated).
3090
+ grid: Pulses in the cycle.
3091
+ time_signature: Shapes the default weight table.
3092
+ weights: Optional explicit per-pulse weights (length *grid*).
3093
+
3094
+ Returns:
3095
+ Mean weight deficit per onset, in ``[0, max_weight − min_weight]``
3096
+ (0.0 only when every onset sits on the downbeat, or there are none;
3097
+ the downbeat alone carries the maximum weight, so even on-beat
3098
+ rhythms score a little above 0).
3099
+
3100
+ Example:
3101
+ ```python
3102
+ syncopation([0], 16) # 0.0 — the downbeat only
3103
+ syncopation([0, 4, 8, 12], 16) # low — on the beats, but beat 1 is strongest
3104
+ syncopation([3, 7, 11, 15], 16) # high — every onset on a weak pulse
3105
+ ```
3106
+ """
3107
+
3108
+ if grid < 1:
3109
+ raise ValueError(f"grid must be at least 1 — got {grid}")
3110
+
3111
+ table = list(weights) if weights is not None else build_metric_weights(time_signature, grid)
3112
+
3113
+ if len(table) != grid:
3114
+ raise ValueError(f"weights must have one value per grid step ({grid}) — got {len(table)}")
3115
+
3116
+ reduced = sorted({o % grid for o in onsets})
3117
+
3118
+ if not reduced:
3119
+ return 0.0
3120
+
3121
+ strongest = max(table)
3122
+
3123
+ return sum(strongest - table[p] for p in reduced) / len(reduced)