Soloing
ci / checks (push) Successful in 1h9m55s

This commit is contained in:
khodowany
2026-10-01 08:35:12 -05:00
parent eae432aa83
commit 75655786e9
16 changed files with 1015 additions and 161 deletions
+75
View File
@@ -4,6 +4,8 @@ import dataclasses
import pathlib
import types
import generator.solo.policy
import generator.model
import generator.musicxml
@@ -20,9 +22,46 @@ class Sequence:
offsets = self.offsets()
return max(map(abs, self.steps)), max(offsets) - min(offsets), sum(a * b < 0 for a, b in zip(self.steps, self.steps[1:])), self.id
@dataclasses.dataclass(frozen=True)
class Path:
steps: tuple[int, ...]
sources: tuple[str, ...]
def offsets(self):
result = [0]
for step in self.steps:
result.append(result[-1] + step)
return tuple(result)
def family(steps):
if not steps or all(s == 0 for s in steps):
return 'repeated'
turns = sum(a * b < 0 for a, b in zip(steps, steps[1:]))
if not turns:
return 'rising' if sum(steps) > 0 else 'falling'
if max(map(abs, steps)) >= 4:
return 'leap_return'
if sum(steps) == 0 and max(map(abs, steps)) == 1:
return 'neighbor'
return ('arch' if steps[0] > 0 else 'valley') if turns == 1 else 'wave'
def diverse(items, rank, category, limit):
"""Round-robin eligible musical families before a destructive cutoff."""
groups = {}
for item in sorted(items, key=rank):
groups.setdefault(category(item), []).append(item)
result, index = [], 0
while len(result) < limit:
layer = [values[index] for values in groups.values() if len(values) > index]
if not layer:
break
result.extend(sorted(layer, key=rank)[:limit - len(result)])
index += 1
return tuple(result)
@dataclasses.dataclass(frozen=True, init=False)
class Library:
paths: types.MappingProxyType
chains: dict
def __init__(self, sequences: tuple[Sequence, ...]):
paths, identities = {}, set()
for sequence in sequences:
@@ -31,8 +70,44 @@ class Library:
identities.add(sequence.id)
paths.setdefault(len(sequence.steps) + 1, []).append(sequence)
object.__setattr__(self, 'paths', types.MappingProxyType({count: tuple(sorted(items, key=lambda s: s.rank())) for count, items in paths.items()}))
object.__setattr__(self, 'chains', {})
def connecting(self, count: int, displacement: int | None = None) -> tuple[Sequence, ...]:
return tuple(s for s in self.paths.get(count, ()) if displacement is None or s.offsets()[-1] == displacement)
def chained(self, count):
return self.retrieve(count)
def retrieve(self, count, displacement=0, direction=0):
key = (count, displacement, direction)
if key in self.chains:
return self.chains[key]
if not 1 <= count <= 16:
return ()
states, remaining = (Path((), ()),), count - 1
while remaining:
size = min(3, remaining)
options = self.connecting(size + 1)
if not options:
size = next((n for n in range(size - 1, 0, -1) if self.connecting(n + 1)), 0)
if not size:
return ()
options = self.connecting(size + 1)
desired = displacement * (count - 1 - remaining + size) / max(1, count - 1)
def rank(path):
offsets = path.offsets()
motion = offsets[-1]
return abs(motion - desired) + (2 if direction and motion * direction < 0 else 0), max(offsets) - min(offsets), max(map(abs, path.steps), default=0), path.sources
# Retrieve from the entire authored vocabulary, conditioned on each prefix.
following = []
for prior in states:
if count > 4:
local = self.retrieve(size + 1, round(desired - prior.offsets()[-1]), direction)
paths = (Path(prior.steps + s.steps, prior.sources + s.sources) for s in local)
else:
paths = (Path(prior.steps + s.steps, prior.sources + (s.id,)) for s in options)
following.extend(diverse(paths, rank, lambda p: family(p.steps), generator.solo.policy.DEFAULT.retrieval))
states = diverse(following, rank, lambda p: (family(p.steps), (p.offsets()[-1] > 0) - (p.offsets()[-1] < 0)), generator.solo.policy.DEFAULT.retrieval)
remaining -= size
self.chains[key] = states
return states
def read(path: pathlib.Path, identifier: str, count: int) -> Sequence:
document = generator.musicxml.load_musicxml(path)