Skip to content

Commit 1f2a8b2

Browse files
author
Simon Holliday
committed
- New sequence_utils.constrained_walk(): backward feasibility (unsatisfiable raises before any draw) + forward sampling through the engine's real history-dependent weights; stall fallback to bare weights; unconstrained walks are draw-identical to the plain engine
- Progression.generate(style=, bars=, beats=, pins=, end=, avoid=) with full engine parameter pass-through; the progression() factory style path delegates to it - Keyless generation emits a KEY-RELATIVE value (scale-proof major-relative romans, new RomanChord.major_relative) - prints romans unbound, resolves at bind; key= still yields concrete chords - freeze(end=, pins=, avoid=): hybrid constraints on the live engine; bar 1 stays the journey's current chord; unconstrained freeze routes through the kernel with unchanged seeded output - WeightedGraph.nodes(); style→scale inference for int constraints (end=1)
1 parent 781cebb commit 1f2a8b2

10 files changed

Lines changed: 1036 additions & 80 deletions

api-cheatsheet.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The top-level controller for a musical piece.
2121
| `form_jump(section_name) -> None` | Jump the form to a named section immediately. |
2222
| `form_next(section_name) -> None` | Queue the next section — takes effect when the current section ends. |
2323
| `form_state *(property)*` | The active ``subsequence.form_state.FormState``, or ``None`` if ``form()`` has not been called. |
24-
| `freeze(bars) -> 'Progression'` | Capture a chord progression from the live harmony engine. |
24+
| `freeze(bars, end, pins, avoid) -> 'Progression'` | Capture a chord progression from the live harmony engine. |
2525
| `get_tweaks(name) -> Dict[str, Any]` | Return a copy of the current tweaks for a running pattern. |
2626
| `harmonic_state *(property)*` | The active ``HarmonicState``, or ``None`` if ``harmony()`` has not been called. |
2727
| `harmony(style, cycle_beats, dominant_7th, gravity, nir_strength, minor_turnaround_weight, root_diversity, reschedule_lookahead, progression) -> None` | Configure the harmonic logic and chord change intervals. |
@@ -219,6 +219,7 @@ A frozen sequence of :class:`ChordSpan` — the governing harmony value.
219219
| `describe(key, scale) -> str` | A readable, one-chord-per-line summary. |
220220
| `events() -> Tuple[subsequence.progressions.ChordEvent, ...]` | The realised timeline as a tuple (iteration, materialised). |
221221
| `extend(*extensions, only) -> 'Progression'` | Add chord extensions (``7``/``9``/``11``/``13``/``"sus4"``/...) to every span. |
222+
| `generate(style, bars, beats, key, scale, seed, rng, pins, end, avoid, dominant_7th, gravity, nir_strength, minor_turnaround_weight, root_diversity) -> 'Progression'` | Generate a progression from a chord-graph walk — the hybrid generator. |
222223
| `inversions(spec) -> 'Progression'` | Set chord inversions — a single int for all spans, or a list cycled per span. |
223224
| `is_concrete *(property)*` | True when every span is key-independent (no romans/degrees). |
224225
| `length *(property)*` | Total length in beats (the sum of span lengths). |
@@ -335,7 +336,7 @@ A sequence of Motifs with segmentation preserved.
335336
| `between(low, high, step) -> subsequence.harmonic_rhythm.HarmonicRhythm` | A harmonic rhythm that varies *between* two lengths (in beats). |
336337
| `parse_chord(name) -> subsequence.chords.Chord` | Parse a chord name like ``"Cm7"`` or ``"Dbmaj7"`` into a :class:`Chord`. |
337338
| `register_chord_quality(name, intervals, suffix) -> None` | Register a custom chord quality for use everywhere chords are used. |
338-
| `progression(source, beats, style, bars, key, seed, rng, dominant_7th, gravity, nir_strength) -> subsequence.progressions.Progression` | Build a :class:`Progression` — the lowercase factory. |
339+
| `progression(source, beats, style, bars, key, scale, seed, rng, pins, end, avoid, dominant_7th, gravity, nir_strength, minor_turnaround_weight, root_diversity) -> subsequence.progressions.Progression` | Build a :class:`Progression` — the lowercase factory. |
339340
| `motif(degrees, beats, velocities, durations, probabilities, length) -> subsequence.motifs.Motif` | The lowercase shortcut: a melody as 1-based scale degrees. |
340341
| `vl_distance(source, target, pitch_classes) -> int` | Voice-leading distance between two chords (Tymoczko's taxicab metric). |
341342
| `branch_sequence(pitches, depth, path, mutation, rng) -> List[int]` | Navigate a fractal tree of pitch-sequence transforms and return one variation. |
@@ -351,6 +352,7 @@ Functions for generating and transforming sequences.
351352
|---|---|
352353
| `branch_sequence(pitches, depth, path, mutation, rng) -> List[int]` | Navigate a fractal tree of pitch-sequence transforms and return one variation. |
353354
| `build_metric_weights(time_signature, grid) -> List[float]` | Per-step metric weights for one bar — how "strong" each grid position is. |
355+
| `constrained_walk(graph, start, length, rng, pins, end, avoid, weight_modifier, before_choice, after_choice) -> List[~T]` | Walk a weighted graph under constraints — the shared hybrid kernel. |
354356
| `de_bruijn(k, n) -> List[int]` | Generate a de Bruijn sequence B(k, n). |
355357
| `fibonacci_rhythm(steps, length) -> List[float]` | Generate beat positions spaced by the golden ratio (Fibonacci spiral). |
356358
| `generate_bresenham_sequence(steps, pulses) -> List[int]` | Generate a rhythm using Bresenham's line algorithm. |

subsequence/composition.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import subsequence.pattern
2929
import subsequence.pattern_builder
3030
import subsequence.progressions
31+
import subsequence.sequence_utils
3132
import subsequence.sequencer
3233
import subsequence.voicings
3334
import subsequence.web_ui
@@ -1561,7 +1562,13 @@ def harmony (
15611562
# A re-call invalidates whatever the horizon had planned.
15621563
self._harmony_horizon.invalidate_future()
15631564

1564-
def freeze (self, bars: int) -> "Progression":
1565+
def freeze (
1566+
self,
1567+
bars: int,
1568+
end: typing.Optional[typing.Any] = None,
1569+
pins: typing.Optional[typing.Dict[int, typing.Any]] = None,
1570+
avoid: typing.Optional[typing.Sequence[typing.Any]] = None,
1571+
) -> "Progression":
15651572

15661573
"""Capture a chord progression from the live harmony engine.
15671574
@@ -1573,21 +1580,36 @@ def freeze (self, bars: int) -> "Progression":
15731580
continuing compositional journey so section progressions feel like parts
15741581
of a whole rather than isolated islands.
15751582
1583+
The hybrid constraints compile into the walk: ``end=`` fixes the last
1584+
bar ("end on V at bar 8"), ``pins=`` fix any 1-based bar, ``avoid=``
1585+
excludes chords throughout. Specs follow the progression-element
1586+
grammar (ints where diatonic, roman/name strings where chromatic) and
1587+
resolve against the composition key and scale. A backward
1588+
feasibility pass guarantees satisfiability before any chord is drawn;
1589+
the forward walk keeps the engine's real history-dependent weighting.
1590+
Bar 1 is always the engine's current chord — the journey continues —
1591+
so ``pins={1: ...}`` may only name it redundantly.
1592+
15761593
Parameters:
15771594
bars: Number of chords to capture (one per harmony cycle).
1595+
end: The chord at the final bar — ``end="V"`` is the cadential
1596+
major dominant in minor.
1597+
pins: ``{bar: chord}`` — 1-based fiat positions.
1598+
avoid: Chords excluded from the walk.
15781599
15791600
Returns:
15801601
A :class:`Progression` with the captured chords and trailing
15811602
history for NIR continuity.
15821603
15831604
Raises:
1584-
ValueError: If :meth:`harmony` has not been called first.
1605+
ValueError: If :meth:`harmony` has not been called first, or the
1606+
constraints are contradictory or unsatisfiable.
15851607
15861608
Example::
15871609
15881610
composition.harmony(style="functional_major", cycle_beats=4)
1589-
verse = composition.freeze(8) # 8 chords, engine advances
1590-
chorus = composition.freeze(4) # next 4 chords, continuing on
1611+
verse = composition.freeze(8, end="V") # the verse sets up the chorus
1612+
chorus = composition.freeze(4) # next 4 chords, continuing on
15911613
composition.section_chords("verse", verse)
15921614
composition.section_chords("chorus", chorus)
15931615
"""
@@ -1597,6 +1619,23 @@ def freeze (self, bars: int) -> "Progression":
15971619
if bars < 1:
15981620
raise ValueError("bars must be at least 1")
15991621

1622+
scale = self.scale or "ionian"
1623+
key_pc = subsequence.chords.key_name_to_pc(self.key) if self.key is not None else hs.key_root_pc
1624+
1625+
resolved_pins = {
1626+
position: subsequence.progressions.resolve_constraint(spec, key_pc, scale, f"pins[{position}]")
1627+
for position, spec in (pins or {}).items()
1628+
}
1629+
resolved_end = subsequence.progressions.resolve_constraint(end, key_pc, scale, "end") if end is not None else None
1630+
resolved_avoid = [subsequence.progressions.resolve_constraint(spec, key_pc, scale, "avoid") for spec in (avoid or [])]
1631+
1632+
if 1 in resolved_pins and resolved_pins[1] != hs.current_chord:
1633+
raise ValueError(
1634+
f"pins[1]={resolved_pins[1].name()} conflicts with the engine's current chord "
1635+
f"({hs.current_chord.name()}) — bar 1 of a freeze continues the journey; "
1636+
"pin a later bar, or use pin_chord() for playback fiat"
1637+
)
1638+
16001639
# Per-call salted stream (freeze:1, freeze:2, ...): each call's draws
16011640
# are independent of every other consumer, so frozen progressions are
16021641
# reproducible WITHOUT play() and adding a call cannot shift a
@@ -1611,11 +1650,23 @@ def freeze (self, bars: int) -> "Progression":
16111650
hs.rng = stream
16121651

16131652
try:
1614-
collected: typing.List[subsequence.chords.Chord] = [hs.current_chord]
1615-
1616-
for _ in range(bars - 1):
1617-
hs.step()
1618-
collected.append(hs.current_chord)
1653+
# The kernel with the engine's own hooks is draw-for-draw the old
1654+
# step() loop when unconstrained — one walk path for both.
1655+
def _commit (chosen: subsequence.chords.Chord) -> None:
1656+
hs.current_chord = chosen
1657+
1658+
collected = subsequence.sequence_utils.constrained_walk(
1659+
hs.graph,
1660+
hs.current_chord,
1661+
bars,
1662+
rng = hs.rng,
1663+
pins = resolved_pins,
1664+
end = resolved_end,
1665+
avoid = resolved_avoid,
1666+
weight_modifier = hs._transition_weight,
1667+
before_choice = hs._record_transition_source,
1668+
after_choice = _commit,
1669+
)
16191670

16201671
# Advance past the last captured chord so the next freeze() call or
16211672
# live playback does not duplicate it.

subsequence/harmonic_state.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,25 @@ def _transition_weight (
264264

265265
return (1.0 + boost) * nir_score * diversity
266266

267+
def _record_transition_source (self, chord: subsequence.chords.Chord) -> None:
268+
269+
"""History bookkeeping for one transition: the outgoing chord enters history.
270+
271+
The first half of :meth:`step` — exposed so a constrained walk can
272+
interleave it with its own draws (``before_choice``) and the NIR
273+
weighting sees exactly the context it would live.
274+
"""
275+
276+
self.history.append(chord)
277+
if len(self.history) > 4:
278+
self.history.pop(0)
279+
267280
def step (self) -> subsequence.chords.Chord:
268281

269282
"""Advance to the next chord based on the transition graph."""
270283

271284
# Update history before choosing next (so structure tracks the path)
272-
self.history.append(self.current_chord)
273-
if len(self.history) > 4:
274-
self.history.pop(0)
285+
self._record_transition_source(self.current_chord)
275286

276287
# Decision path: chord changes occur here; key changes are not automatic.
277288
self.current_chord = self.graph.choose_next(self.current_chord, self.rng, weight_modifier=self._transition_weight)
@@ -291,9 +302,7 @@ def plan_next (self) -> subsequence.chords.Chord:
291302

292303
saved_history = list(self.history)
293304

294-
self.history.append(self.current_chord)
295-
if len(self.history) > 4:
296-
self.history.pop(0)
305+
self._record_transition_source(self.current_chord)
297306

298307
try:
299308
return self.graph.choose_next(self.current_chord, self.rng, weight_modifier=self._transition_weight)
@@ -310,9 +319,7 @@ def commit_chord (self, chord: subsequence.chords.Chord) -> subsequence.chords.C
310319
``step()`` records it).
311320
"""
312321

313-
self.history.append(self.current_chord)
314-
if len(self.history) > 4:
315-
self.history.pop(0)
322+
self._record_transition_source(self.current_chord)
316323

317324
self.current_chord = chord
318325

0 commit comments

Comments
 (0)