Class: HeadMusic::Style::CompositeAssessment
- Inherits:
-
Object
- Object
- HeadMusic::Style::CompositeAssessment
- Defined in:
- lib/head_music/style/composite_assessment.rb
Overview
Several guides applied to one voice: the combined grade, and the per-guide assessments it was computed from.
Answers the same protocol as GuideAssessment, which is the point – a consumer
walks assessments and never asks which of the two it is holding. The shared
example group in spec/support is what keeps the two from drifting apart.
Instance Attribute Summary collapse
-
#guide ⇒ Object
readonly
Returns the value of attribute guide.
-
#voice ⇒ Object
readonly
Returns the value of attribute voice.
Instance Method Summary collapse
-
#adherent? ⇒ Boolean
-
#assessable? ⇒ Boolean
-
#assessments ⇒ Object
Memoized because everything else here is derived from it: without this, each of fitness, messages, and guide_item_assessments re-runs the whole member analysis, and
assessments.firstis a different object every time. -
#fitness ⇒ Object
Geometric rather than arithmetic, because a species grade is two grades that must both hold.
-
#fitness_by_category ⇒ Object
Grouped rather than keyed, so a composite of two guides sharing a category reports both instead of silently dropping one.
-
#gate_factor ⇒ Object
-
#geometric_mean(values) ⇒ Object
private
Exact where exactness is required, which the log-sum form would not be: [1.0, 1.0] is exactly 1.0, which adherent? depends on, and any zero member is exactly 0.0.
-
#guide_item_assessments ⇒ Object
-
#initialize(guide, voice) ⇒ CompositeAssessment
constructor
A new instance of CompositeAssessment.
-
#member_fitness(assessment) ⇒ Object
private
-
#messages ⇒ Object
Constructor Details
#initialize(guide, voice) ⇒ CompositeAssessment
Returns a new instance of CompositeAssessment.
13 14 15 16 17 18 19 20 |
# File 'lib/head_music/style/composite_assessment.rb', line 13 def initialize(guide, voice) unless guide.respond_to?(:composite?) && guide.composite? raise ArgumentError, "guide must be a composite (got #{guide.inspect})" end @guide = guide @voice = voice end |
Instance Attribute Details
#guide ⇒ Object (readonly)
Returns the value of attribute guide.
11 12 13 |
# File 'lib/head_music/style/composite_assessment.rb', line 11 def guide @guide end |
#voice ⇒ Object (readonly)
Returns the value of attribute voice.
11 12 13 |
# File 'lib/head_music/style/composite_assessment.rb', line 11 def voice @voice end |
Instance Method Details
#adherent? ⇒ Boolean
37 38 39 |
# File 'lib/head_music/style/composite_assessment.rb', line 37 def adherent? assessments.all?(&:adherent?) end |
#assessable? ⇒ Boolean
41 42 43 |
# File 'lib/head_music/style/composite_assessment.rb', line 41 def assessable? assessments.all?(&:assessable?) end |
#assessments ⇒ Object
Memoized because everything else here is derived from it: without this, each
of fitness, messages, and guide_item_assessments re-runs the whole member
analysis, and assessments.first is a different object every time.
25 26 27 |
# File 'lib/head_music/style/composite_assessment.rb', line 25 def assessments @assessments ||= guide.guides.map { |member| member.assess(voice) } end |
#fitness ⇒ Object
Geometric rather than arithmetic, because a species grade is two grades that must both hold. A perfect melody against a half-graded harmony reads 0.707 rather than 0.75, and either half at zero takes the whole grade to zero. Acing the melody buys no relief from the harmony.
When a member is unassessable the rubric is left out of it entirely. This is GuideAssessment’s own rule lifted one level – “a voice failing a precondition has not earned a bad grade on the rest” – with the nouns raised: the composite has not earned a grade on the OTHER MEMBERS either. Without the branch, an assessable member’s rubric leaks into a grade that was never earned, moving it up or down according to material the guide declined to look at.
With today’s registry the branch changes no number, and that is not an argument against it. The members of a species composite share MinimumNotes.with(3) and fail it identically, and the only gate that distinguishes them, SetAgainstAnotherVoice, scores exactly 0 or 1 – so a solo voice grades 0.0 either way. The rules part company where the members’ gates differ and one scores a fraction, which MinimumNotes does. The rule is chosen for what a gate means, not for a number it currently moves.
65 66 67 68 69 |
# File 'lib/head_music/style/composite_assessment.rb', line 65 def fitness @fitness ||= geometric_mean( assessable? ? assessments.map(&:fitness) : assessments.map(&:gate_factor) ) end |
#fitness_by_category ⇒ Object
Grouped rather than keyed, so a composite of two guides sharing a category reports both instead of silently dropping one. Follows fitness into the gate-factor branch: a consumer showing melody, harmony, and total must not be handed three numbers that do not combine.
79 80 81 82 |
# File 'lib/head_music/style/composite_assessment.rb', line 79 def fitness_by_category assessments.group_by { |assessment| assessment.guide.category } .transform_values { |group| geometric_mean(group.map(&method(:member_fitness))) } end |
#gate_factor ⇒ Object
71 72 73 |
# File 'lib/head_music/style/composite_assessment.rb', line 71 def gate_factor @gate_factor ||= geometric_mean(assessments.map(&:gate_factor)) end |
#geometric_mean(values) ⇒ Object (private)
Exact where exactness is required, which the log-sum form would not be: [1.0, 1.0] is exactly 1.0, which adherent? depends on, and any zero member is exactly 0.0. The empty case cannot arise – CompositeGuide refuses fewer than two members – so it is left to raise rather than answering a silent 1.0.
94 95 96 |
# File 'lib/head_music/style/composite_assessment.rb', line 94 def geometric_mean(values) values.reduce(:*)**(1.0 / values.length) end |
#guide_item_assessments ⇒ Object
29 30 31 |
# File 'lib/head_music/style/composite_assessment.rb', line 29 def guide_item_assessments @guide_item_assessments ||= assessments.flat_map(&:guide_item_assessments) end |
#member_fitness(assessment) ⇒ Object (private)
86 87 88 |
# File 'lib/head_music/style/composite_assessment.rb', line 86 def member_fitness(assessment) assessable? ? assessment.fitness : assessment.gate_factor end |
#messages ⇒ Object
33 34 35 |
# File 'lib/head_music/style/composite_assessment.rb', line 33 def assessments.flat_map(&:messages) end |