///|
pub(all) enum CandidateConstraint {
Range
Consonance
Leap
Motion
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ConstraintEvidence {
constraint : CandidateConstraint
passed : Bool
score : Int
detail : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct CandidateEvaluation {
candidate : PitchCandidate
constraints : Array[ConstraintEvidence]
score : Int
accepted : Bool
} derive(Debug, Eq, ToJson)
///|
/// Evaluate a candidate as transparent constraint evidence instead of one opaque score.
pub fn evaluate_candidate(
cantus_before : Note,
cantus_after : Note,
counter_before : Note,
counter_after : Note,
candidate : Note,
policy : CandidatePolicy,
) -> CandidateEvaluation {
let vertical = harmonic_interval(cantus_after, candidate)
let melodic = melodic_interval(counter_before, candidate)
let reference_delta = absolute_melodic_interval(counter_before, counter_after)
let motion_value = candidate_motion(
cantus_before, cantus_after, counter_before, candidate,
)
let absolute_leap = absolute_melodic_interval(counter_before, candidate)
let range_ok = policy.is_valid() &&
candidate.midi >= policy.low &&
candidate.midi <= policy.high
let consonance_ok = !policy.require_consonance || is_consonance(vertical)
let leap_ok = absolute_leap <= policy.max_leap
let motion_ok = !policy.prefer_contrary ||
motion_value == Contrary ||
motion_value == Oblique
let constraints = [
{
constraint: Range,
passed: range_ok,
score: if range_ok {
25
} else {
0
},
detail: if range_ok {
"candidate is inside the configured register"
} else {
"candidate is outside the configured register"
},
},
{
constraint: Consonance,
passed: consonance_ok,
score: if consonance_ok {
25
} else {
0
},
detail: if consonance_ok {
"vertical interval is consonant"
} else {
"vertical interval is dissonant"
},
},
{
constraint: Leap,
passed: leap_ok,
score: if leap_ok {
25
} else {
0
},
detail: "candidate leap=\{absolute_leap}, reference leap=\{reference_delta}",
},
{
constraint: Motion,
passed: motion_ok,
score: if motion_ok {
25
} else {
0
},
detail: if motion_ok {
"motion is contrary or oblique"
} else {
"policy prefers contrary or oblique motion"
},
},
]
let mut score = 0
for evidence in constraints {
score += evidence.score
}
let accepted = range_ok && consonance_ok && leap_ok && motion_ok
let pitch_candidate = {
note: candidate,
vertical_interval: vertical,
melodic_interval: melodic,
consonant: is_consonance(vertical),
motion: motion_value,
score,
}
{ candidate: pitch_candidate, constraints, score, accepted }
}
///|
pub fn candidate_motion_name(motion_value : MotionKind) -> String {
match motion_value {
Parallel => "parallel"
Similar => "similar"
Contrary => "contrary"
Oblique => "oblique"
Static => "static"
}
}
///|
pub fn candidate_constraint_name(constraint : CandidateConstraint) -> String {
match constraint {
Range => "range"
Consonance => "consonance"
Leap => "leap"
Motion => "motion"
}
}
///|
pub fn candidate_evaluation_count(evaluation : CandidateEvaluation) -> Int {
evaluation.constraints.length()
}
///|
pub fn candidate_evaluation_has(
evaluation : CandidateEvaluation,
constraint : CandidateConstraint,
) -> Bool {
for evidence in evaluation.constraints {
if evidence.constraint == constraint {
return true
}
}
false
}
///|
pub fn candidate_evaluation_passes(
evaluation : CandidateEvaluation,
constraint : CandidateConstraint,
) -> Bool {
for evidence in evaluation.constraints {
if evidence.constraint == constraint {
return evidence.passed
}
}
false
}
///|
pub fn candidate_evaluation_failed(
evaluation : CandidateEvaluation,
) -> Array[CandidateConstraint] {
let result : Array[CandidateConstraint] = []
for evidence in evaluation.constraints {
if !evidence.passed {
result.push(evidence.constraint)
}
}
result
}
///|
pub fn candidate_evaluation_summary(evaluation : CandidateEvaluation) -> String {
let failed = candidate_evaluation_failed(evaluation)
let failures = failed.map(candidate_constraint_name).join(",")
"pitch=\{evaluation.candidate.note.midi} score=\{evaluation.score} accepted=\{evaluation.accepted} failed=\{failures}"
}
///|
pub fn candidate_evaluation_markdown(
evaluation : CandidateEvaluation,
) -> String {
let lines : Array[String] = [
"### Candidate \{evaluation.candidate.note.midi}",
"",
"- Score: \{evaluation.score}",
"- Accepted: \{evaluation.accepted}",
"",
"| Constraint | Status | Detail |",
"| --- | --- | --- |",
]
for evidence in evaluation.constraints {
lines.push(
"| \{candidate_constraint_name(evidence.constraint)} | \{evidence.passed} | \{evidence.detail} |",
)
}
lines.join("\n")
}
///|
pub fn evaluate_candidates(
cantus_before : Note,
cantus_after : Note,
counter_before : Note,
counter_after : Note,
candidates : Array[Note],
policy : CandidatePolicy,
) -> Array[CandidateEvaluation] {
let result : Array[CandidateEvaluation] = []
for candidate in candidates {
result.push(
evaluate_candidate(
cantus_before, cantus_after, counter_before, counter_after, candidate, policy,
),
)
}
result
}
///|
pub fn best_evaluation(
evaluations : Array[CandidateEvaluation],
) -> CandidateEvaluation? {
if evaluations.length() == 0 {
return None
}
let mut best = evaluations[0]
for evaluation in evaluations {
if evaluation.accepted && !best.accepted {
best = evaluation
} else if evaluation.accepted == best.accepted &&
evaluation.score > best.score {
best = evaluation
}
}
Some(best)
}