Cleanup
This commit is contained in:
+39
-109
@@ -7,22 +7,20 @@ normal composer; accepting injected pitches is not a construction proof.
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import bisect
|
||||
from collections import Counter
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
import hashlib
|
||||
import heapq
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from audition.reference_review import read
|
||||
from audition.solo_compare import events, rests
|
||||
from generator import accompaniment, model, musicxml, parts, production, resources, score, vocabulary
|
||||
from audition.solo_compare import events
|
||||
from generator import accompaniment, musicxml, parts, production, resources, score, vocabulary
|
||||
from generator.arrange import form, generate
|
||||
from generator.scales import selection, timeline
|
||||
from generator.solo import analysis, construction, harmonic, profiles, report, types
|
||||
from generator.solo import analysis, construction, harmonic, profiles
|
||||
|
||||
|
||||
def build(source_path,role,data_dir,cache_dir):
|
||||
@@ -54,122 +52,50 @@ def canonical(notes):
|
||||
|
||||
|
||||
def derive(builder,target):
|
||||
"""Search grammar derivations constrained to the exact supplied events.
|
||||
"""Stateful target-constrained coverage probe; NEVER production success.
|
||||
|
||||
This is a development diagnostic, not normal production inference. Failure
|
||||
is recorded at its earliest uncovered event. No target action is injected.
|
||||
Separate histories carry actual pending contracts. This bounded diagnostic
|
||||
can report exhaustion, not prove a phrase impossible. No notes are injected.
|
||||
"""
|
||||
encoded=tuple((F(n['start']),F(n['end']),int(n['pitch'])) for n in events(target))
|
||||
starts=tuple(n[0] for n in encoded);encoded_set=set(encoded)
|
||||
queue=[builder.start];paths={builder.start:()};seen=set();furthest=builder.start
|
||||
queue=[builder.start];chart={builder.start:[construction.State()]};queued={builder.start}
|
||||
furthest=builder.start;last=construction.State();cuts=0
|
||||
while queue:
|
||||
at=heapq.heappop(queue)
|
||||
if at in seen:continue
|
||||
seen.add(at);furthest=max(furthest,at)
|
||||
at=heapq.heappop(queue);states=chart.pop(at);furthest=max(furthest,at)
|
||||
unique={builder.identity(s):s for s in states};states=list(unique.values())
|
||||
cuts+=max(0,len(states)-16);states=states[:16]
|
||||
if at==builder.end:
|
||||
path=paths[at]
|
||||
checked=builder.harmony.evaluate(tuple(n for a in path for n in a.notes),expressions=tuple(c for a in path for c in a.contracts),complete=True,until=at)
|
||||
return path,{'complete':checked.valid,'covered_until':str(at),'actions':len(path),
|
||||
'status':'constructible' if checked.valid else 'rejected_by_full_context','error':checked.error}
|
||||
for state in states:
|
||||
checked=builder.harmony.evaluate(state.notes,expressions=tuple(c for a in state.actions for c in a.contracts),complete=True,until=at)
|
||||
if checked.valid and canonical(state.notes)==encoded:
|
||||
return state.actions,{'complete':True,'covered_until':str(at),'actions':len(state.actions),
|
||||
'status':'statefully_constructible','diagnostic_only':True,'pruned_histories':cuts}
|
||||
left=bisect.bisect_left(starts,at)
|
||||
def matches(action):
|
||||
if any((c.target.start,c.target.end,c.target.pitch.number()) not in encoded_set for c in action.contracts):return False
|
||||
expected=encoded[left:bisect.bisect_left(starts,action.end)]
|
||||
return not (left and encoded[left-1][1]>at) and canonical(action.notes)==expected
|
||||
first=encoded[left] if left<len(encoded) else None
|
||||
for action in builder.operators.at(at,eligible=matches,first=first):
|
||||
if action.end not in paths:
|
||||
paths[action.end]=(*paths[at],action);heapq.heappush(queue,action.end)
|
||||
actions=builder.operators.at(at,eligible=matches,first=first)
|
||||
for state in states:
|
||||
last=state
|
||||
for action in actions:
|
||||
next_state=builder.advance(state,None,action)
|
||||
if next_state is None:continue
|
||||
chart.setdefault(action.end,[]).append(next_state)
|
||||
if action.end not in queued:heapq.heappush(queue,action.end);queued.add(action.end)
|
||||
next_event=next((n for n in encoded if n[0]>=furthest),None)
|
||||
reasons=[]
|
||||
if next_event:
|
||||
start,end,pitch=next_event;region=builder.harmony.at(start)
|
||||
reasons=[{'pitch':pitch,'start':str(start),'end':str(end),
|
||||
'source_chord':region.chord if region else None,'mapped':bool(region and region.scale and pitch%12 in region.scale.pitches),
|
||||
'basic':bool(region and pitch%12 in region.roles.basic),
|
||||
'function':builder.harmony.function(pitch,region) if region else 'missing_harmony'}]
|
||||
return paths.get(furthest,()),{'complete':False,'covered_until':str(furthest),
|
||||
'status':'not_proposed_or_rejected_by_rule','next_events':reasons,
|
||||
'budget':'Exhaustive derivation through available unpruned grammar actions, not unrestricted music search.',
|
||||
'action_coverage':dict(builder.operators.coverage)}
|
||||
return last.actions,{'complete':False,'covered_until':str(furthest),
|
||||
'status':'coverage_search_exhausted' if cuts else 'not_proposed_or_rejected_by_rule',
|
||||
'next_event':tuple(map(str,next_event)) if next_event else None,
|
||||
'pruned_histories':cuts,'diagnostic_only':True,'action_coverage':dict(builder.operators.coverage)}
|
||||
|
||||
|
||||
def fit(examples,passes,on_checkpoint=None):
|
||||
"""Integer early-update structured perceptron on shared musical features.
|
||||
|
||||
Each roll-in is normal source-only ranking until its first wrong action.
|
||||
Only the development command finds a compatible positive action and updates
|
||||
coefficients. Production never receives the positive prefix. A zero-error
|
||||
pass means every unrestricted roll-in produced the entire expected stream.
|
||||
"""
|
||||
weights={r:dict(b.selector.weights) for _,r,b,_ in examples};history=[];best=None;tables={}
|
||||
# Whole actions include pickup+hold and compound groups (e.g. 17/4).
|
||||
# Eight beats on the supported twelfth-beat lattice need numerators up to
|
||||
# 96. The previous LCM 1..16 covered note values, not complete actions.
|
||||
normalization=math.lcm(*range(1,97))
|
||||
def table(name,b,at,state):
|
||||
key=(name,at,canonical(state.recent),canonical(state.home),state.last_kind,state.pending)
|
||||
if key in tables:return tables[key]
|
||||
rows=[]
|
||||
actions=b.operators.at(at)
|
||||
if state.home and state.last_kind!='return':actions+=b.operators.related(at,state.home,'return')+b.operators.related(at,state.home,'answer')
|
||||
for action in actions:
|
||||
if b.advance(state,None,action) is None:continue
|
||||
features,prior=b.selector.features(action,state.recent,state.home,state.last_kind)
|
||||
factor=F(normalization)/(action.end-action.start)
|
||||
if factor.denominator!=1:raise ValueError('Unsupported training normalization span')
|
||||
factor=int(factor)
|
||||
rows.append((action,tuple((k,v*factor) for k,v in features.items() if v),
|
||||
sum(v*b.selector_default[k] for k,v in prior.items())*factor))
|
||||
if len(tables)>=24:tables.pop(next(iter(tables)))
|
||||
tables[key]=tuple(rows);return tables[key]
|
||||
from generator.solo.selection import DEFAULT
|
||||
for _,_,b,_ in examples:b.selector_default=DEFAULT
|
||||
for iteration in range(passes):
|
||||
updates={r:Counter() for r in weights};records=[];errors=0;covered=0
|
||||
for name,role,b,gold in examples:
|
||||
b.selector.weights=weights[role];state=construction.State();at=b.start
|
||||
encoded=canonical(tuple(n for a in gold for n in a.notes));encoded_set=set(encoded);starts=tuple(n[0] for n in encoded)
|
||||
while at<b.end:
|
||||
rows=table(name,b,at,state)
|
||||
rank=lambda row:(sum(weights[role].get(k,0)*v for k,v in row[1])+row[2],row[0].key)
|
||||
if not rows:raise ValueError('No feasible action during development roll-in')
|
||||
predicted=min(rows,key=rank)
|
||||
left=bisect.bisect_left(starts,at)
|
||||
def matches(row):
|
||||
action=row[0]
|
||||
return canonical(action.notes)==encoded[left:bisect.bisect_left(starts,action.end)] and all((c.target.start,c.target.end,c.target.pitch.number()) in encoded_set for c in action.contracts)
|
||||
if not matches(predicted):
|
||||
positives=sorted((row for row in rows if matches(row)),key=rank)
|
||||
# An alternate valid derivation can be learnable even when the
|
||||
# cheapest positive shares every feature with the wrong action.
|
||||
positive=next((row for row in positives if dict(row[1])!=dict(predicted[1])),positives[0] if positives else None)
|
||||
if positive is None:
|
||||
records.append({'case':name,'first_error':str(at),'status':'coverage_lost_after_alternate_derivation'})
|
||||
errors+=1;break
|
||||
updates[role].update(dict(predicted[1]));updates[role].subtract(dict(positive[1]))
|
||||
records.append({'case':name,'first_error':str(at),'predicted':predicted[0].kind,'positive':positive[0].kind,
|
||||
'identical_features':dict(predicted[1])==dict(positive[1]),'status':'selection_error'})
|
||||
errors+=1;break
|
||||
action=predicted[0]
|
||||
state=b.advance(state,b.selector.evaluate(action,state.recent,state.home,state.last_kind),action)
|
||||
at=action.end
|
||||
covered+=bisect.bisect_left(starts,at)
|
||||
if at==b.end:records.append({'case':name,'status':'exact_rollout','events':len(encoded)})
|
||||
complexity=sum(abs(v) for w in weights.values() for v in w.values())
|
||||
history.append({'pass':iteration+1,'incorrect_rollouts':errors,'matching_prefix_attacks':covered,'cases':records})
|
||||
checkpoint=(errors,-covered,complexity)
|
||||
if best is None or checkpoint<best[0]:best=(checkpoint,json.loads(json.dumps(weights)))
|
||||
if on_checkpoint:on_checkpoint({'weights':best[1],'history':history,'complete':errors==0})
|
||||
print(f'fit pass {iteration+1}: {errors} incorrect rollouts; {covered} matching prefix attacks',flush=True)
|
||||
if not errors:break
|
||||
for role,difference in updates.items():
|
||||
for feature,delta in difference.items():weights[role][feature]=weights[role].get(feature,0)+delta
|
||||
weights[role]={k:v for k,v in weights[role].items() if v}
|
||||
return {'weights':best[1],'history':history,'complete':history[-1]['incorrect_rollouts']==0,
|
||||
'method':'integer early-update structured perceptron, fixed case order and 200-pass maximum',
|
||||
'checkpoint_metric':'incorrect rollouts, then longest correct prefixes, then coefficient magnitude; full-output exact gate remains separate',
|
||||
'limitation':'Training examples are not unseen evaluation. Runtime independence does not imply development independence.'}
|
||||
def fit(examples,passes,on_checkpoint=None,resources=None):
|
||||
# Imported only by this explicitly requested development operation.
|
||||
from audition.structured_learning import fit_complete
|
||||
return fit_complete(examples,passes,on_checkpoint,resources)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -182,8 +108,12 @@ def main():
|
||||
p.add_argument('--data-dir',type=Path,default=Path('data'))
|
||||
p.add_argument('--cache-dir',type=Path)
|
||||
p.add_argument('--output',type=Path,required=True)
|
||||
p.add_argument('--passes',type=int,default=200)
|
||||
p.add_argument('--passes',type=int,default=8)
|
||||
p.add_argument('--solver-path',type=Path,help='Development-only pinned solver installation directory')
|
||||
args=p.parse_args()
|
||||
if args.solver_path:
|
||||
import sys
|
||||
sys.path.insert(0,str(args.solver_path.resolve()))
|
||||
if not 1<=args.passes<=200:p.error('passes must be 1–200')
|
||||
pairs=lambda values:dict(v.split('=',1) for v in values)
|
||||
sources,refs,roles=map(pairs,(args.source,args.reference,args.role));starts=pairs(args.form_start)
|
||||
@@ -199,7 +129,7 @@ def main():
|
||||
'derivation':[{'kind':a.kind,'start':str(a.start),'end':str(a.end),'events':[(str(n.start),str(n.end),n.pitch.number()) for n in a.notes],'rules':[c.rule for c in a.contracts]} for a in path]}
|
||||
if args.operation=='fit' and result['complete']:examples.append((name,roles[name],b,path))
|
||||
print(name,result['status'],result['covered_until'],flush=True)
|
||||
result={'schema':'shared-rule-coverage-1','development_only':True,'coverage_seconds':time.monotonic()-began,'cases':results}
|
||||
result={'schema':'shared-phrase-learning-2','development_only':True,'coverage_seconds':time.monotonic()-began,'cases':results}
|
||||
if args.operation=='fit':
|
||||
if not all(r['complete'] for r in results.values()):result['fit_status']='blocked_by_construction_coverage'
|
||||
else:
|
||||
@@ -207,7 +137,7 @@ def main():
|
||||
path=args.output/'checkpoint.json';temporary=path.with_suffix('.tmp')
|
||||
temporary.write_text(json.dumps(value,indent=2)+'\n');temporary.replace(path)
|
||||
fitting_start=time.monotonic()
|
||||
result['fit']=fit(examples,args.passes,checkpoint)
|
||||
result['fit']=fit(examples,args.passes,checkpoint,data)
|
||||
result['fit_seconds']=time.monotonic()-fitting_start
|
||||
result['total_seconds']=time.monotonic()-began
|
||||
(args.output/'coverage.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Export source-only complete candidates; references are not accepted here."""
|
||||
import argparse
|
||||
import copy
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
import cProfile
|
||||
|
||||
from audition.fit_selection import build
|
||||
from generator import model, musicxml_writer, score, vocabulary
|
||||
from generator.solo import notation, placement
|
||||
|
||||
|
||||
def export(builder,document,output):
|
||||
output.mkdir(parents=True,exist_ok=True)
|
||||
source=score.source_staff(document)['measures']
|
||||
start_index=score.body_start_measure_index(source)
|
||||
if builder.start<0:start_index-=1
|
||||
length=F();selected=[]
|
||||
for m in source[start_index:]:
|
||||
if length>=builder.end-builder.start:break
|
||||
selected.append(m);length+=model.fraction(m['duration'])
|
||||
if length!=builder.end-builder.start:raise ValueError('Diagnostic export requires complete source measures')
|
||||
records=[]
|
||||
for index,state in enumerate(builder.completed,1):
|
||||
played=placement.place(state.notes,builder.analysis.tempo,profile=builder.harmony.profile)
|
||||
if played is None:raise ValueError('Completed candidate is unplayable')
|
||||
shifted=tuple(dataclasses.replace(n,start=n.start-builder.start,end=n.end-builder.start) for n in played[1])
|
||||
part=copy.deepcopy(document);staff=part['staves'][0]
|
||||
part['staves']=[staff];staff['measures']=notation.measures(selected,shifted,'candidate')
|
||||
staff['tuplets']=notation.tuplets(staff['measures']);staff['tablature']=True
|
||||
for key in ('scale_spans','octave_shifts','slurs','wedges'):staff.pop(key,None)
|
||||
path=output/f'candidate-{index}.xml'
|
||||
musicxml_writer.write_musicxml(path,part,tablature=True)
|
||||
records.append({'file':path.name,'automatic_winner':index==1,'evaluation':dataclasses.asdict(state.evaluation),
|
||||
'home':[(str(n.start),str(n.end),n.pitch.number()) for n in state.home],
|
||||
'events':[(str(n.start),str(n.end),n.pitch.number()) for n in state.notes],
|
||||
'actions':[{'kind':a.kind,'start':str(a.start),'end':str(a.end),'contracts':[c.rule for c in a.contracts]} for a in state.actions]})
|
||||
value={'source_only':True,'automatic_winner':'candidate-1.xml','start':str(builder.start),'end':str(builder.end),
|
||||
'candidates':records,'frontiers':builder.frontiers,'timings':dict(builder.timings),
|
||||
'note':'Diagnostic candidates, not publication or evidence of exact reproduction.'}
|
||||
(output/'candidates.json').write_text(json.dumps(value,indent=2,default=str)+'\n')
|
||||
return value
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--source',type=Path,required=True);p.add_argument('--role',choices=('acoustic_solo','electric_solo'),required=True)
|
||||
p.add_argument('--data-dir',type=Path,default=Path('data'));p.add_argument('--output',type=Path,required=True)
|
||||
p.add_argument('--cache-dir',type=Path);p.add_argument('--bars',type=int,default=4)
|
||||
p.add_argument('--profile',action='store_true')
|
||||
args=p.parse_args()
|
||||
if args.bars<1:p.error('--bars must be positive')
|
||||
with vocabulary.using(vocabulary.load(args.data_dir/'composition'),args.role):
|
||||
b,document=build(args.source,args.role,args.data_dir,args.cache_dir)
|
||||
ends=[x-b.analysis.body_offset for x in b.analysis.boundaries if x-b.analysis.body_offset>0]
|
||||
b.end=min(b.end,ends[min(args.bars-1,len(ends)-1)]);b.operators.end=b.end
|
||||
started=time.monotonic();seen=set()
|
||||
def progress(stage,at,state,proposed,retained):
|
||||
if stage=='frontier' and at.denominator==1 and at not in seen:
|
||||
seen.add(at);print(f'frontier {at}: {len(proposed)} histories, {len(retained)} retained; {time.monotonic()-started:.1f}s',flush=True)
|
||||
b.observe=progress
|
||||
args.output.mkdir(parents=True,exist_ok=True)
|
||||
profiler=cProfile.Profile() if args.profile else None
|
||||
if profiler:profiler.enable()
|
||||
try:b.compose()
|
||||
finally:
|
||||
if profiler:profiler.disable();profiler.dump_stats(args.output/'search.prof')
|
||||
export(b,document,args.output)
|
||||
print(json.dumps({'completed':len(b.completed),'seconds':b.timings['action_search_seconds'],'winner':str(b.winner_evaluation.total)}))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,2 @@
|
||||
# Development only. Runtime composition must not import this solver.
|
||||
z3-solver==4.13.3.0
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Development-only shared integer ranking from COMPLETE ordinary rollouts.
|
||||
|
||||
The returned coefficients depend on the training examples. Target event arrays,
|
||||
source identifiers and diagnostic oracle paths are never production artifacts.
|
||||
"""
|
||||
from collections import Counter
|
||||
from fractions import Fraction as F
|
||||
import itertools
|
||||
import math
|
||||
from generator import vocabulary
|
||||
from generator.solo import selection
|
||||
|
||||
|
||||
def contextual_candidates(selector,notes):
|
||||
"""Induce only whitelisted two/three-condition event contexts.
|
||||
|
||||
These counts are training-dependent. No title, time index, song key, event
|
||||
array, fingerprint or arbitrary predicate can appear in the rule schema.
|
||||
"""
|
||||
values=Counter()
|
||||
for context,ticks in selector.contexts(notes):
|
||||
terms=sorted((k,v) for k,v in context.items() if v in selection.CONDITIONS[k])
|
||||
for size in (2,3):
|
||||
for terms_ in itertools.combinations(terms,size):
|
||||
values['joint/'+'&'.join(k+'='+v for k,v in terms_)]+=ticks
|
||||
return values
|
||||
|
||||
|
||||
def vector(builder,actions,home=()):
|
||||
e=builder.selector.complete(actions,home,start=builder.start,end=builder.end)
|
||||
# Equal spans: clear normalization without rounding any musical time.
|
||||
span=builder.end-builder.start
|
||||
features={k:F(v)*span for k,v in e.raw.items() if not k.startswith('prior/') and v}
|
||||
prior=sum(F(v)*span for k,v in e.contributions.items() if k.startswith('prior/'))
|
||||
return features,prior
|
||||
|
||||
|
||||
def constraint(role,gold,other,case):
|
||||
left,lp=gold;right,rp=other
|
||||
delta={k:left.get(k,0)-right.get(k,0) for k in left.keys()|right.keys()}
|
||||
delta={k:v for k,v in delta.items() if v}
|
||||
selection.validate_weights(dict.fromkeys(delta,0))
|
||||
return {'role':role,'delta':delta,'prior':lp-rp,'case':case}
|
||||
|
||||
|
||||
def solve(rows,bound=1024):
|
||||
"""One shared coefficient table per role; deterministic pinned integer solver."""
|
||||
import z3
|
||||
if z3.get_version_string()!='4.13.3':raise ValueError('Use pinned z3-solver==4.13.3.0')
|
||||
z3.set_param('smt.random_seed',0)
|
||||
# Exact zero/one-coefficient cases need no combinatorial cardinality search.
|
||||
# Checking every feature's integer interval proves the minimum support and
|
||||
# L1 objective when all independent roles have such a solution. This does
|
||||
# not claim the resulting sparse model reproduces any complete melody.
|
||||
simple={}
|
||||
for role in sorted({r['role'] for r in rows}):
|
||||
subset=[r for r in rows if r['role']==role]
|
||||
if all(r['prior']<=-1 for r in subset):simple[role]={};continue
|
||||
choices=[]
|
||||
for key in sorted({k for r in subset for k in r['delta']}):
|
||||
low,high=-bound,bound
|
||||
for r in subset:
|
||||
slope=F(r['delta'].get(key,0));limit=-1-F(r['prior'])
|
||||
if slope>0:high=min(high,math.floor(limit/slope))
|
||||
elif slope<0:low=max(low,math.ceil(limit/slope))
|
||||
elif limit<0:low,high=1,0;break
|
||||
if low<=high:
|
||||
value=low if low>0 else high if high<0 else 0
|
||||
choices.append((abs(value),key,value))
|
||||
if not choices:break
|
||||
_,key,value=min(choices);simple[role]={key:value}
|
||||
if set(simple)=={r['role'] for r in rows}:
|
||||
for weights in simple.values():selection.validate_weights(weights)
|
||||
return {'status':'solved','weights':simple,'bound':bound,
|
||||
'nonzero':sum(len(w) for w in simple.values()),'l1':sum(abs(v) for w in simple.values() for v in w.values()),
|
||||
'method':'exhaustive zero/one-coefficient integer intervals; minimum support and L1 within that support proven'}
|
||||
names=sorted({(r['role'],k) for r in rows for k in r['delta']})
|
||||
variables={n:z3.Int('w_'+str(i)) for i,n in enumerate(names)}
|
||||
limits=[v>=-bound for v in variables.values()]+[v<=bound for v in variables.values()]
|
||||
rational=lambda v:z3.RealVal(str(F(v)))
|
||||
constraints=[z3.Sum([variables[(r['role'],k)]*rational(v) for k,v in r['delta'].items()])+rational(r['prior'])<=-1 for r in rows]
|
||||
check=z3.Solver();check.set(rlimit=2000000);check.add(*limits)
|
||||
for i,c in enumerate(constraints):check.assert_and_track(c,'comparison_'+str(i))
|
||||
status=check.check()
|
||||
if status==z3.unknown:return {'status':'solver_unknown','stage':'feasibility','reason':check.reason_unknown()}
|
||||
if status==z3.unsat:
|
||||
return {'status':'incompatible_with_current_features_and_bounds','bound':bound,
|
||||
'conflicts':[str(x) for x in check.unsat_core()],
|
||||
'identical_feature_pairs':[i for i,r in enumerate(rows) if not r['delta'] and r['prior']>=0]}
|
||||
opt=z3.Optimize();opt.add(*limits,*constraints)
|
||||
# Deterministic solver work budget. Exhaustion yields no proposed model;
|
||||
# elapsed wall time never chooses a musical winner.
|
||||
opt.set(rlimit=2000000)
|
||||
opt.minimize(z3.Sum([z3.If(v==0,0,1) for v in variables.values()]))
|
||||
opt.minimize(z3.Sum([z3.If(v<0,-v,v) for v in variables.values()]))
|
||||
if opt.check()!=z3.sat:return {'status':'solver_unknown','reason':opt.reason_unknown()}
|
||||
model=opt.model();weights={r:{} for r,_ in names}
|
||||
for (role,key),v in variables.items():
|
||||
value=model.eval(v,model_completion=True).as_long()
|
||||
if value:weights[role][key]=value
|
||||
for w in weights.values():selection.validate_weights(w)
|
||||
return {'status':'solved','weights':weights,'bound':bound,
|
||||
'nonzero':sum(len(w) for w in weights.values()),'l1':sum(abs(v) for w in weights.values() for v in w.values())}
|
||||
|
||||
|
||||
def sounded(actions):
|
||||
from generator.solo.types import sounding_notes
|
||||
return tuple((n.start,n.end,n.pitch.number()) for n in sounding_notes(tuple(n for a in actions for n in a.notes)))
|
||||
|
||||
|
||||
class CoverageTrace:
|
||||
"""Read-only observer. Targets diagnose cuts, never supply search decisions."""
|
||||
def __init__(self,gold):
|
||||
self.gold=sounded(gold);self.records=[]
|
||||
def matches(self,notes,end):
|
||||
# These are committed whole actions, not a truncated playback preview.
|
||||
# A quarter-note attack cannot reproduce a half-note target merely because
|
||||
# its first quarter sounds the same. Future actions do not reopen it.
|
||||
from generator.solo.types import sounding_notes
|
||||
actual=tuple((n.start,n.end,n.pitch.number()) for n in sounding_notes(notes))
|
||||
expected=tuple((a,b,p) for a,b,p in self.gold if a<end)
|
||||
return actual==expected
|
||||
def __call__(self,stage,at,state,proposed,retained):
|
||||
if stage=='successors':
|
||||
if not self.matches(state.notes,at):return
|
||||
eligible=[a for a in proposed if self.matches((*state.notes,*a.notes),a.end)]
|
||||
survivors=[a for a in retained if self.matches((*state.notes,*a.notes),a.end)]
|
||||
else:
|
||||
eligible=[s for s in proposed if self.matches(s.notes,s.action.end if s.action else at)]
|
||||
survivors=[s for s in retained if self.matches(s.notes,s.action.end if s.action else at)]
|
||||
if eligible and not survivors:
|
||||
self.records.append({'time':str(at),'status':'pruned_at_'+stage,'matching_proposals':len(eligible)})
|
||||
elif stage=='successors' and not eligible:
|
||||
self.records.append({'time':str(at),'status':'not_proposed_in_actual_incoming_state'})
|
||||
|
||||
|
||||
def fit_complete(examples,passes,on_checkpoint=None,resources=None):
|
||||
weights={role:dict(b.selector.weights) for _,role,b,_ in examples}
|
||||
rows=[];history=[];last_solver={};exact=False;comparisons=[];induced=[]
|
||||
for iteration in range(passes):
|
||||
cases=[];errors=0
|
||||
for name,role,b,gold in examples:
|
||||
b.selector.weights=weights[role]
|
||||
trace=CoverageTrace(gold);b.observe=trace
|
||||
# Honor the explicitly selected development data directory during lazy
|
||||
# operator expansion too, not just when constructing the builder.
|
||||
with vocabulary.using(resources or vocabulary.current()['data'],role):
|
||||
b.compose() # No target argument, prefix, candidate injection or oracle.
|
||||
b.observe=None
|
||||
state=__import__('generator.solo.construction',fromlist=['State']).State()
|
||||
for a in gold:state=b.advance(state,None,a)
|
||||
positive=vector(b,gold,state.home)
|
||||
correct=sounded(b.selected_actions)==sounded(gold)
|
||||
cases.append({'case':name,'exact_complete_output':correct,
|
||||
'completed_candidates':len(b.completed),'winner':str(b.winner_evaluation.total),
|
||||
'earliest_observed_failures':trace.records[:8],
|
||||
'target_in_completed_pool':any(sounded(s.actions)==sounded(gold) for s in b.completed)})
|
||||
if not correct:errors+=1
|
||||
for s in b.completed:
|
||||
if sounded(s.actions)!=sounded(gold):
|
||||
comparisons.append((name,role,b,gold,state.home,s.actions,s.home))
|
||||
rows.append(constraint(role,positive,vector(b,s.actions,s.home),name))
|
||||
print(name,'exact' if correct else 'complete-output mismatch',flush=True)
|
||||
history.append({'iteration':iteration+1,'incorrect_complete_outputs':errors,'cases':cases})
|
||||
if not errors:exact=True
|
||||
result={'weights':weights,'history':history,'complete':exact,'solver':last_solver,
|
||||
'induced_rules':induced,
|
||||
'training_dependency':'Coefficients learned from supplied authored examples; runtime target-free is not training independence.',
|
||||
'release_gate':'Ordinary cold full-song outputs must independently pass exact comparison; derivation is not success.'}
|
||||
if on_checkpoint:on_checkpoint(result)
|
||||
if exact:break
|
||||
last_solver=solve(rows)
|
||||
if last_solver['status']!='solved':
|
||||
# Add a context only for a demonstrated feature collision. General
|
||||
# ranking failure is not permission to expand vocabulary indiscriminately.
|
||||
collisions=last_solver.get('identical_feature_pairs',[])
|
||||
if collisions and len(induced)<8 and iteration+1<passes:
|
||||
i=collisions[0];name,role,b,gold,gh,other,oh=comparisons[i]
|
||||
g=contextual_candidates(b.selector,tuple(n for a in gold for n in a.notes))
|
||||
o=contextual_candidates(b.selector,tuple(n for a in other for n in a.notes))
|
||||
keys=sorted((k for k in g.keys()|o.keys() if g.get(k,0)!=o.get(k,0) and k not in weights[role]),key=lambda k:(k.count('&'),k))
|
||||
if keys:
|
||||
key=keys[0]
|
||||
for r in weights:weights[r][key]=0
|
||||
induced.append({'rule':key,'reason':'complete-candidate feature collision','training_case':name,
|
||||
'schema':'two or three whitelisted chord/relative-motion/rhythm/source conditions; no song identity'})
|
||||
rows=[]
|
||||
for name,r,b,gold,gh,other,oh in comparisons:
|
||||
b.selector.weights=weights[r]
|
||||
rows.append(constraint(r,vector(b,gold,gh),vector(b,other,oh),name))
|
||||
continue
|
||||
result['solver']=last_solver;break
|
||||
if iteration+1<passes:weights={r:last_solver['weights'].get(r,{}) for r in weights}
|
||||
# Do not call an untested final solver update a successful checkpoint.
|
||||
result['solver']=last_solver
|
||||
result['constraints']=[{**r,'delta':{k:str(v) for k,v in r['delta'].items()},'prior':str(r['prior'])} for r in rows]
|
||||
return result
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Fit shared ranking from completed ordinary generation, never prefix roll-ins.
|
||||
|
||||
This development command reads explicit targets and saved complete alternatives.
|
||||
It writes a *proposal*, not a released model. A staged data copy may be passed to
|
||||
the ordinary generator for a separate target-free cold reproduction test.
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
from fractions import Fraction as F
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from audition.fit_selection import build,derive
|
||||
from audition.reference_review import read
|
||||
from audition.structured_learning import vector,constraint,solve,sounded,CoverageTrace
|
||||
from generator import production,score,vocabulary
|
||||
from generator.solo import construction,selection,policy
|
||||
|
||||
|
||||
def evaluation_vector(value):
|
||||
if value['version']!=selection.VERSION:raise ValueError('Completed output uses another evaluator version')
|
||||
span=F(value['denominators']['span'])
|
||||
features={k:F(v)*span for k,v in value['raw'].items() if not k.startswith('prior/') and F(v)}
|
||||
selection.validate_weights(dict.fromkeys(features,0))
|
||||
prior=sum(F(v)*span*selection.DEFAULT[k.removeprefix('prior/')] for k,v in value['raw'].items() if k.startswith('prior/'))
|
||||
return features,prior
|
||||
|
||||
|
||||
def training_context(data):
|
||||
"""Changing learned weights is allowed between rounds; musical rules are not."""
|
||||
musical=copy.deepcopy(data)
|
||||
musical['roles'].pop('provenance',None)
|
||||
for role in musical['roles']['roles'].values():role.pop('selection_weights',None)
|
||||
return {'evaluator':selection.VERSION,'algorithm':policy.DEFAULT.composition_version,
|
||||
'fixed_priors':selection.DEFAULT,'musical_data':vocabulary.fingerprint(musical)}
|
||||
|
||||
|
||||
def previous_constraints(evidence,cases,context):
|
||||
if evidence.get('schema')!='complete-candidate-training-1' or evidence.get('context')!=context:
|
||||
raise ValueError('Previous round has incompatible evaluation or musical data')
|
||||
earlier=evidence['training_dependency']
|
||||
if earlier.keys()!=cases.keys():raise ValueError('Previous round has different training cases')
|
||||
for name in cases:
|
||||
for key in ('source_sha256','target_sha256','role'):
|
||||
if cases[name][key]!=earlier[name][key]:raise ValueError('Previous round changed '+name+'/'+key)
|
||||
rows=[]
|
||||
for row in evidence['constraints']:
|
||||
if row['case'] not in cases or row['role']!=cases[row['case']]['role']:
|
||||
raise ValueError('Previous constraint has a mismatched case/role')
|
||||
selection.validate_weights(dict.fromkeys(row['delta'],0))
|
||||
rows.append({**row,'delta':{k:F(v) for k,v in row['delta'].items()},'prior':F(row['prior'])})
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
for name in ('source','reference','role'):p.add_argument('--'+name,action='append',required=True,metavar='CASE=VALUE')
|
||||
p.add_argument('--form-start',action='append',default=[])
|
||||
p.add_argument('--data-dir',type=Path,default=Path('data'))
|
||||
p.add_argument('--completed-dir',type=Path,required=True,help='Completed ordinary assets directory containing analysis/*.roles')
|
||||
p.add_argument('--output',type=Path,required=True);p.add_argument('--cache-dir',type=Path)
|
||||
p.add_argument('--solver-path',type=Path,required=True)
|
||||
p.add_argument('--stage-data',action='store_true',help='Write an isolated data copy for ordinary inference; does not publish it')
|
||||
p.add_argument('--previous-round',type=Path,action='append',default=[],help='Accumulate complete-output constraints from a compatible training.json; never a production input')
|
||||
args=p.parse_args();sys.path.insert(0,str(args.solver_path.resolve()))
|
||||
pairs=lambda values:dict(v.split('=',1) for v in values)
|
||||
sources,refs,roles=map(pairs,(args.source,args.reference,args.role));starts=pairs(args.form_start)
|
||||
if sources.keys()!=refs.keys() or sources.keys()!=roles.keys():p.error('Cases must agree')
|
||||
args.output.mkdir(parents=True,exist_ok=True);rows=[];cases={};contexts=[];data=vocabulary.load(args.data_dir/'composition')
|
||||
for name in sorted(sources):
|
||||
with vocabulary.using(data,roles[name]):
|
||||
b,document=build(sources[name],roles[name],args.data_dir,args.cache_dir)
|
||||
target=read(refs[name],name,int(starts[name]) if name in starts else None,'guitar-written')
|
||||
gold,coverage=derive(b,target)
|
||||
if not coverage['complete']:raise ValueError(name+': '+coverage['status'])
|
||||
s=construction.State()
|
||||
for a in gold:s=b.advance(s,None,a)
|
||||
positive=vector(b,gold,s.home)
|
||||
contexts.append((name,roles[name],b,gold))
|
||||
path=args.completed_dir/'analysis'/(score.slugify(Path(sources[name]).stem)+'.roles')
|
||||
generated=json.loads(path.read_text())[roles[name]]
|
||||
if generated['trace']['source']!=production.SourceAnalysis.read(document).musical_key:
|
||||
raise ValueError(name+': completed candidates belong to different canonical source music')
|
||||
if generated['trace']['role']!=roles[name]:raise ValueError(name+': wrong completed role')
|
||||
if generated['trace']['vocabulary']!=vocabulary.fingerprint(production.dependencies(roles[name],data)):
|
||||
raise ValueError(name+': completed candidates use different production data/settings')
|
||||
candidates=generated['details']['selection_trace']['completed_candidates']
|
||||
negatives=0;exact=False
|
||||
for c in candidates:
|
||||
stream=tuple((F(a),F(z),int(n)) for a,z,n in c['events'])
|
||||
same=stream==sounded(gold)
|
||||
if c['rank']==1:exact=same
|
||||
if not same:rows.append(constraint(roles[name],positive,evaluation_vector(c['evaluation']),name));negatives+=1
|
||||
cases[name]={'source':sources[name],'reference':refs[name],'role':roles[name],
|
||||
'source_sha256':hashlib.sha256(Path(sources[name]).read_bytes()).hexdigest(),
|
||||
'target_sha256':target['sha256'],'completed_artifact_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
'coverage':coverage,'ordinary_winner_exact':exact,'complete_negative_candidates':negatives}
|
||||
print(name,exact,negatives,flush=True)
|
||||
context=training_context(data);parents=[]
|
||||
for path in args.previous_round:
|
||||
raw=path.read_bytes();rows.extend(previous_constraints(json.loads(raw),cases,context))
|
||||
parents.append({'path':str(path),'sha256':hashlib.sha256(raw).hexdigest()})
|
||||
unique={}
|
||||
for row in rows:
|
||||
key=(row['role'],row['case'],tuple(sorted(row['delta'].items())),row['prior'])
|
||||
unique[key]=row
|
||||
rows=[unique[key] for key in sorted(unique)]
|
||||
result=solve(rows)
|
||||
evidence={'schema':'complete-candidate-training-1','training_dependency':cases,'solver':result,
|
||||
'context':context,'previous_rounds':parents,
|
||||
'constraints':[{**r,'delta':{k:str(v) for k,v in r['delta'].items()},'prior':str(r['prior'])} for r in rows],
|
||||
'released':False,'acceptance':'Exact four full outputs from ordinary cold target-free generation, plus transfer; not satisfied by this training solve.'}
|
||||
if result['status']=='solved':
|
||||
# A zero here proves this proposed model cannot reproduce the full output.
|
||||
# A nonzero result proves neither full retention nor exact generation.
|
||||
# This is read-only diagnosis, not prefix fitting or candidate injection.
|
||||
cuts={}
|
||||
for name,role,b,gold in contexts:
|
||||
with vocabulary.using(data,role):
|
||||
b.selector.weights=result['weights'].get(role,{})
|
||||
trace=CoverageTrace(gold);b.observe=trace
|
||||
retained=b.candidates(b.start,construction.State());b.observe=None
|
||||
count=sum(trace.matches(a.notes,a.end) for _,a in retained)
|
||||
cuts[name]={'matching_first_successors':count,'observed_failures':trace.records,
|
||||
'full_reproduction_already_excluded':count==0}
|
||||
evidence['post_fit_cutoff_diagnosis']=cuts
|
||||
(args.output/'training.json').write_text(json.dumps(evidence,indent=2)+'\n')
|
||||
if result['status']=='solved':
|
||||
(args.output/'proposed-weights.json').write_text(json.dumps(result['weights'],indent=2)+'\n')
|
||||
if args.stage_data:
|
||||
staged=args.output/'data'
|
||||
if staged.exists():raise ValueError('Refusing to overwrite a previous staged data proposal')
|
||||
shutil.copytree(args.data_dir,staged)
|
||||
path=staged/'composition'/'roles.json';value=json.loads(path.read_text())
|
||||
for role,weights in result['weights'].items():value['roles'][role]['selection_weights']=weights
|
||||
value['provenance']='Shared bounded coefficients learned from explicit authored training examples; see adjacent development training.json. No runtime target reads or stored phrases.'
|
||||
path.write_text(json.dumps(value,indent=2)+'\n')
|
||||
print(json.dumps(result,indent=2))
|
||||
return 0 if result['status']=='solved' else 2
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
Reference in New Issue
Block a user