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
The Hamiltonian weight was comparing transposed output chords
against untransposed graph nodes, so they never matched.
Now tracks graph_path separately for correct Hamiltonian check.
- Sort pitches by frequency after projection in generate_connected_sets
- This ensures all chords in graph have bass-to-soprano ordering
- Simplified voice crossing check: verify destination is sorted after movement
- Since source is sorted, just check if adjacent pairs remain in order
- Fixed movement map indexing bug in precomputed voice crossing detection
- Replace index-based voice crossing check with pitch ordering comparison
- Now properly detects when voice ordering changes between chords
- This was causing edges with voice crossing to be incorrectly allowed
- Deduplicate transpositions in _find_valid_edges using set comprehension
to avoid processing same transposition multiple times
- Edge count now matches notebook (1414 vs 2828)
- Rename expand() to project() for clarity (project to [1,2) range)
- Fix SyntaxWarnings in docstrings (escape backslashes)
- Add _is_directly_tunable method to check if changing pitches are adjacent to staying pitch
- Modify _find_valid_edges to compute and return edge properties
- Store all properties in graph edges at build time
- Simplify _calculate_edge_weights to read from edge data
- Rename voice_crossing config to voice_crossing_allowed (False = reject crossing)
- Change movement map from {pitch: {destination, cents}} to {src_idx: dest_idx}
- Track voice mapping cumulatively in pathfinder
- Reorder output pitches according to voice mapping
- Update weight calculation to compute cent_diffs from index mapping
- Melodic threshold now correctly filters edges based on actual movements