- Add _is_adjacent() to check if pitches differ by ±1 in one dimension
- Add _compute_dim_diff() to compute prime * direction for dimension changes
- Add _find_ref_and_dim_diff() to find the ref voice and dim_diff for changed pitches
- Update output_chords_to_music_data() to compute ref and dim_diff by comparing consecutive chords
- Update format_dim_diff() to include dim_diff == 0 check and fix ref mapping for 4 voices
- Compute both c1→c2 and c2→c1 edges independently using _find_valid_edges()
- Fix _is_directly_tunable to properly reorder c2_transposed using movement map
- Clean up unused valid_pairings code in _build_movement_maps
- Add edge_data field to PathStep for debugging
- Convert chord data to LilyPond parts and PDF
- Generate part files for voices I, II, III
- Generate full score from template
- Call LilyPond to create PDF automatically
- CLI with --name, --fundamental, --template options
- Add preview mode (p key) to toggle between send/preview behavior
- Arrow keys for navigation with optional send
- Enter sends current chord (only when not in preview mode)
- Jump to chord by number + Enter
- k/K keys for kill soft (15.0) and kill hard (0.0) commands
- Display prev/current/next chords with frequencies to 2 decimals
- Add OSC test receiver for debugging
- Use arrow key escape sequences for left/right navigation
Fix to_fraction() in src/pitch.py to handle negative exponents
correctly without floating point precision loss.
Before: 2573485501354569/2251799813685248
After: 8/7
- Rename --voice-crossing to --allow-voice-crossing
- Change --direct-tuning to --disable-direct-tuning (defaults to require)
- Add explicit documentation for every CLI parameter
- Add detailed explanations for each factor
- Add more examples
- Move all _factor_* methods from pathfinder.py to path.py
- Add get_candidates() and compute_weights() to Path class
- Simplify step() to just commit chosen candidate
- Add normalized_scores field for consistent influence calculation
- Remove duplicate transposition/voice_map logic between get_candidates and step
- dca_voice_movement and target_range now use destination_chord directly
- Remove Candidate class, use PathStep for both hypothetical and actual steps
- Simplify Path.step() to accept a PathStep
- Fix DCA Hamiltonian to return visit_count directly instead of normalized score
- Tests pass and DCA properly discriminates
- Create src/dims.py with DIMS_4, DIMS_5, DIMS_7, DIMS_8 constants
- Update pitch.py to import from dims
- Update harmonic_space.py to import from dims
- Update io.py to import from dims
- Fix circular import issue
- harmonic_space.py: remove abs() to store signed cent differences
- graph.py: melodic threshold now uses abs(cents) for magnitude
- Change melodic power from 2 to 3 for sharper penalization
This fixes contrary_motion factor which was broken (always 0 due to absolute values)
- Add callback and interval parameters to find_stochastic_path() for adaptive weights
- Add get_influence() method to compute weighted score contribution per factor
- Rename graph_node/output_chord to source_node/destination_node/source_chord/destination_chord
- Rename voice_stay_count to sustain_count_before/after
- Rename node_visit_counts to last_visited_count_before/after
- Remove redundant internal state from Path - derive from steps
- Each PathStep now fully self-contained with before/after state
Changed voice stay count calculation in Path.step() to compare
by position (position i with position i) instead of tracking
voice identity. This makes dca_voice_movement factor behave
identically to master.
- Move transposition, voice mapping into Path.step()
- Add node_visit_counts and voice_stay_count to each PathStep
- Fix voice tracking to use voice identity, not position
- Clean up unused last_graph_nodes code
- Full encapsulation: Path handles all voice-leading state
- Add src/path.py with Path and PathStep classes
- Path stores initial_chord, steps, weights_config
- PathStep stores graph_node, output_chord, transposition, movements, scores, candidates
- Refactor find_stochastic_path to use candidates approach
- Separate _build_candidates (raw scores) from _compute_weights
- Simplify return type to Path only (graph_chords available via property)
- Update io.py to use new Path API
- Each soft factor is sum-normalized across all edge candidates
- Ensures factors compete equally regardless of native value ranges
- Skip factors with no variance (all candidates same value)
- Base weight of 1.0 ensures minimum probability
- Half of moving voices should go one direction, half opposite
- Weighted by closeness to ideal half split
- factor = 1.0 - (distance_from_half / half)
- Works with odd (near-half) and even (exact half) voices
- Analysis shows contrary_motion_steps, percent, and avg_score
- Save graph_path to output for accurate Hamiltonian tracking
- DCA analysis now shows avg/max voice stay counts
- Fix: use actual graph node hashes instead of rehashing transposed chords
- Add src/analyze.py: standalone analysis script
- Add --stats CLI flag to show stats after generation
- Analyze: melodic violations, target range %, voice changes
- Replace cumulative_trans with average cents of actual chord
- More accurate register targeting using current chord position
- Test with max_path=150 shows reaching ~400 Hz target (2 octaves)
- Replace harsh 1/(1+distance) with bounded relative scoring
- Factor > 1.0 when moving toward target, < 1.0 when moving away
- Minimum factor 0.1 to avoid zero weights
- Test with max_path=150 shows reaching ~90% of 2-octave target
- Add --target-range CLI option (in octaves)
- Implement target_range weight in PathFinder
- Add test for target range weight
- Add --max-path to README