counterpoint-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ """
2
+ counterpoint_engine — Species counterpoint as constraint satisfaction.
3
+
4
+ Proves the core thesis: counterpoint = constraint satisfaction = Laman rigidity.
5
+
6
+ Each rule returns SAT or UNSAT.
7
+ Each voice is a vertex in a Laman graph.
8
+ Each contrapuntal constraint is an edge.
9
+ """
10
+
11
+ from .rules import (
12
+ no_parallel_fifths,
13
+ no_parallel_octaves,
14
+ proper_resolution,
15
+ max_leap_seventh,
16
+ consonant_interval,
17
+ voice_independence,
18
+ SAT,
19
+ UNSAT,
20
+ Satisfiability,
21
+ )
22
+ from .laman_counterpoint import (
23
+ CounterpointGraph,
24
+ henneberg_construct,
25
+ verify_rigidity,
26
+ )
27
+ from .generator import (
28
+ CounterpointGenerator,
29
+ CounterpointResult,
30
+ Species,
31
+ VoiceRange,
32
+ Scale,
33
+ )
34
+ from .tensor_output import (
35
+ TensorMIDIEvent,
36
+ voices_to_tensor_events,
37
+ voice_leading_to_sidechannels,
38
+ )
39
+ from .exceptions import (
40
+ CounterpointError,
41
+ ConstraintViolationError,
42
+ ParallelFifthsError,
43
+ ParallelOctavesError,
44
+ VoiceCrossingError,
45
+ RangeViolationError,
46
+ ResolutionError,
47
+ LeapViolationError,
48
+ InvalidInputError,
49
+ GenerationError,
50
+ )
51
+
52
+ __version__ = "0.1.0"
53
+
54
+ __all__ = [
55
+ # Rules
56
+ "no_parallel_fifths",
57
+ "no_parallel_octaves",
58
+ "proper_resolution",
59
+ "max_leap_seventh",
60
+ "consonant_interval",
61
+ "voice_independence",
62
+ "SAT",
63
+ "UNSAT",
64
+ "Satisfiability",
65
+ # Laman
66
+ "CounterpointGraph",
67
+ "henneberg_construct",
68
+ "verify_rigidity",
69
+ # Generator
70
+ "CounterpointGenerator",
71
+ "CounterpointResult",
72
+ "Species",
73
+ "VoiceRange",
74
+ "Scale",
75
+ # Tensor output
76
+ "TensorMIDIEvent",
77
+ "voices_to_tensor_events",
78
+ "voice_leading_to_sidechannels",
79
+ # Exceptions
80
+ "CounterpointError",
81
+ "ConstraintViolationError",
82
+ "ParallelFifthsError",
83
+ "ParallelOctavesError",
84
+ "VoiceCrossingError",
85
+ "RangeViolationError",
86
+ "ResolutionError",
87
+ "LeapViolationError",
88
+ "InvalidInputError",
89
+ "GenerationError",
90
+ # Meta
91
+ "__version__",
92
+ ]
@@ -0,0 +1,213 @@
1
+ """Custom exceptions for counterpoint-specific errors.
2
+
3
+ These exceptions provide structured error information for constraint
4
+ violations (parallel fifths/octaves, voice crossing, range violations)
5
+ and input validation failures.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional, Sequence, Tuple
11
+
12
+
13
+ class CounterpointError(Exception):
14
+ """Base exception for all counterpoint-engine errors."""
15
+
16
+ def __init__(self, message: str, *, detail: str = "") -> None:
17
+ self.detail = detail
18
+ super().__init__(message)
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Constraint violations
23
+ # ---------------------------------------------------------------------------
24
+
25
+ class ConstraintViolationError(CounterpointError):
26
+ """A contrapuntal constraint was violated during generation or checking."""
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ *,
32
+ constraint: str = "",
33
+ beat: int = -1,
34
+ voices: Optional[Sequence[int]] = None,
35
+ detail: str = "",
36
+ ) -> None:
37
+ self.constraint = constraint
38
+ self.beat = beat
39
+ self.voices = list(voices) if voices is not None else []
40
+ super().__init__(message, detail=detail)
41
+
42
+
43
+ class ParallelFifthsError(ConstraintViolationError):
44
+ """Parallel perfect fifths detected between two voices."""
45
+
46
+ def __init__(
47
+ self,
48
+ voice_a: Sequence[int],
49
+ voice_b: Sequence[int],
50
+ beat_prev: int,
51
+ beat_curr: int,
52
+ ) -> None:
53
+ self.voice_a = list(voice_a)
54
+ self.voice_b = list(voice_b)
55
+ self.beat_prev = beat_prev
56
+ self.beat_curr = beat_curr
57
+ super().__init__(
58
+ f"Parallel fifths between beats {beat_prev} and {beat_curr}",
59
+ constraint="no_parallel_fifths",
60
+ beat=beat_curr,
61
+ detail=(
62
+ f"voice_a[{beat_prev}]={voice_a[beat_prev]}, "
63
+ f"voice_a[{beat_curr}]={voice_a[beat_curr]}, "
64
+ f"voice_b[{beat_prev}]={voice_b[beat_prev]}, "
65
+ f"voice_b[{beat_curr}]={voice_b[beat_curr]}"
66
+ ),
67
+ )
68
+
69
+
70
+ class ParallelOctavesError(ConstraintViolationError):
71
+ """Parallel perfect octaves detected between two voices."""
72
+
73
+ def __init__(
74
+ self,
75
+ voice_a: Sequence[int],
76
+ voice_b: Sequence[int],
77
+ beat_prev: int,
78
+ beat_curr: int,
79
+ ) -> None:
80
+ self.voice_a = list(voice_a)
81
+ self.voice_b = list(voice_b)
82
+ self.beat_prev = beat_prev
83
+ self.beat_curr = beat_curr
84
+ super().__init__(
85
+ f"Parallel octaves between beats {beat_prev} and {beat_curr}",
86
+ constraint="no_parallel_octaves",
87
+ beat=beat_curr,
88
+ detail=(
89
+ f"voice_a[{beat_prev}]={voice_a[beat_prev]}, "
90
+ f"voice_a[{beat_curr}]={voice_a[beat_curr]}, "
91
+ f"voice_b[{beat_prev}]={voice_b[beat_prev]}, "
92
+ f"voice_b[{beat_curr}]={voice_b[beat_curr]}"
93
+ ),
94
+ )
95
+
96
+
97
+ class VoiceCrossingError(ConstraintViolationError):
98
+ """Voice crossing: a lower-numbered voice is above a higher-numbered one."""
99
+
100
+ def __init__(
101
+ self,
102
+ voice_upper: int,
103
+ voice_lower: int,
104
+ beat: int,
105
+ pitch_upper: int,
106
+ pitch_lower: int,
107
+ ) -> None:
108
+ self.voice_upper_idx = voice_upper
109
+ self.voice_lower_idx = voice_lower
110
+ self.pitch_upper = pitch_upper
111
+ self.pitch_lower = pitch_lower
112
+ super().__init__(
113
+ f"Voice crossing at beat {beat}: voice {voice_upper} "
114
+ f"({pitch_upper}) below voice {voice_lower} ({pitch_lower})",
115
+ constraint="voice_crossing",
116
+ beat=beat,
117
+ )
118
+
119
+
120
+ class RangeViolationError(ConstraintViolationError):
121
+ """A pitch falls outside the allowed range for its voice."""
122
+
123
+ def __init__(
124
+ self,
125
+ pitch: int,
126
+ min_pitch: int,
127
+ max_pitch: int,
128
+ voice_index: int = -1,
129
+ beat: int = -1,
130
+ ) -> None:
131
+ self.pitch = pitch
132
+ self.min_pitch = min_pitch
133
+ self.max_pitch = max_pitch
134
+ self.voice_index = voice_index
135
+ super().__init__(
136
+ f"Pitch {pitch} out of range [{min_pitch}, {max_pitch}]"
137
+ + (f" for voice {voice_index}" if voice_index >= 0 else "")
138
+ + (f" at beat {beat}" if beat >= 0 else ""),
139
+ constraint="voice_range",
140
+ beat=beat,
141
+ )
142
+
143
+
144
+ class ResolutionError(ConstraintViolationError):
145
+ """Leading tone failed to resolve to the tonic."""
146
+
147
+ def __init__(
148
+ self,
149
+ voice: Sequence[int],
150
+ beat: int,
151
+ leading_tone: int,
152
+ tonic: int,
153
+ ) -> None:
154
+ self.leading_tone = leading_tone
155
+ self.tonic = tonic
156
+ super().__init__(
157
+ f"Leading tone at beat {beat - 1} ({voice[beat - 1]}) "
158
+ f"did not resolve to tonic; got {voice[beat]} at beat {beat}",
159
+ constraint="proper_resolution",
160
+ beat=beat,
161
+ )
162
+
163
+
164
+ class LeapViolationError(ConstraintViolationError):
165
+ """Melodic leap exceeds the maximum allowed interval."""
166
+
167
+ def __init__(
168
+ self,
169
+ prev_pitch: int,
170
+ curr_pitch: int,
171
+ max_leap: int,
172
+ beat: int,
173
+ ) -> None:
174
+ self.prev_pitch = prev_pitch
175
+ self.curr_pitch = curr_pitch
176
+ self.actual_leap = abs(curr_pitch - prev_pitch)
177
+ self.max_leap = max_leap
178
+ super().__init__(
179
+ f"Leap of {self.actual_leap} semitones at beat {beat} "
180
+ f"exceeds maximum {max_leap}",
181
+ constraint="max_leap_seventh",
182
+ beat=beat,
183
+ )
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Input validation errors
188
+ # ---------------------------------------------------------------------------
189
+
190
+ class InvalidInputError(CounterpointError):
191
+ """Raised when generator input fails validation."""
192
+
193
+ def __init__(self, parameter: str, value: object, reason: str) -> None:
194
+ self.parameter = parameter
195
+ self.value = value
196
+ super().__init__(f"Invalid {parameter}={value!r}: {reason}")
197
+
198
+
199
+ class GenerationError(CounterpointError):
200
+ """Raised when counterpoint generation fails entirely."""
201
+
202
+ def __init__(
203
+ self,
204
+ message: str,
205
+ *,
206
+ species: int = 0,
207
+ n_voices: int = 0,
208
+ cantus_length: int = 0,
209
+ ) -> None:
210
+ self.species = species
211
+ self.n_voices = n_voices
212
+ self.cantus_length = cantus_length
213
+ super().__init__(message)