tonal-tools 8.8.3 → 8.9.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d17a66bfeffdbb9b385962d18767b8750d67c36ebf6757ca048f7640e905ebaa
4
- data.tar.gz: 74e415a1db46af3cb9345280cb7bc56cfebfba8ea223a445785f2d5637507dbd
3
+ metadata.gz: ff37192dbedd21f500728f844d7d684053161434a23dbe5625fcf0f7de39da4e
4
+ data.tar.gz: 94670893a608e9fd758a769b34513d31fc8cfa7c19220dd77d887b2d504be7b5
5
5
  SHA512:
6
- metadata.gz: 9001c74c1d40419f25f6f63061c72bec1535a947e050495bdb2ceaf15253637b9b1bf752fa221e868e0a76ae7139133981579318277c5d7d205ba8a2df4c04d8
7
- data.tar.gz: 6bd3ceb1427d2ba7be06155dbe40e9731e2f66752b5ffdac08c45316ce152de7305a3b4d88d83879edca49ddd649da789c05c3ebfd8663f4968bd1ec8969a39f
6
+ metadata.gz: e2d30aa302feae75b7efd5e225a7a3363a6dd1fe3b04929c5ceaeaec6daf69dfbc703af3c8710525ddc062145a10a7cae35e72d815d2bd996315445538892242
7
+ data.tar.gz: f57a435920ae18d8541e5eea3053f56c18d61468383b5a9eb006f9bb35595c06941c5442b7d6146fbb2de33efd05a87700285b902096ec32959b6581e202edb4
@@ -38,7 +38,7 @@ class Tonal::Ratio
38
38
  Set.new(ratio: ratio) do |ratios|
39
39
  ContinuedFraction.new(antecedent.to_f/consequent, conv_limit).convergents.each do |convergent|
40
40
  ratio2 = ratio.class.new(convergent.numerator,convergent.denominator)
41
- ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && (ratio2.max_prime <= max_prime) && (ratio2.min_prime >= min_prime)
41
+ ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && max_prime_at_most?(ratio2, max_prime) && !prime_factor_at_most?(ratio2, min_prime - 1)
42
42
  break if ratios.length >= depth
43
43
  end
44
44
  end
@@ -61,7 +61,7 @@ class Tonal::Ratio
61
61
  FractionTree.node(to_f).path.each do |node|
62
62
  next if node.number.infinite?
63
63
  ratio2 = ratio.class.new(node.number)
64
- ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && (ratio2.max_prime <= max_prime) && (ratio2.min_prime >= min_prime)
64
+ ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && max_prime_at_most?(ratio2, max_prime) && !prime_factor_at_most?(ratio2, min_prime - 1)
65
65
  break if ratios.length >= depth
66
66
  end
67
67
  end
@@ -78,13 +78,15 @@ class Tonal::Ratio
78
78
  # @param superpart if the superior part is the numerator or denominator
79
79
  #
80
80
  def by_superparticular(cents_tolerance: Tonal::Cents::TOLERANCE, depth: DEFAULT_SUPERPART_DEPTH, max_prime: DEFAULT_MAX_PRIME, min_prime: DEFAULT_MIN_PRIME, superpart: :upper)
81
+ # One of n and n+1 is even, so every candidate has 2 as a factor and none can meet a higher min_prime
82
+ return Set.new(ratio: ratio) if min_prime > 2
81
83
  self_in_cents = to_cents
82
84
  within = cents_tolerance.kind_of?(Tonal::Cents) ? cents_tolerance : Tonal::Cents.new(cents: cents_tolerance)
83
85
  Set.new(ratio: ratio) do |ratios|
84
86
  n = 1
85
87
  while true do
86
88
  ratio2 = ratio.class.superparticular(n, factor: ratio.to_r, superpart:)
87
- ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && (ratio2 != ratio) && (ratio2.max_prime <= max_prime) && (ratio2.min_prime <= min_prime)
89
+ ratios << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && (ratio2 != ratio) && max_prime_at_most?(ratio2, max_prime) && !prime_factor_at_most?(ratio2, min_prime - 1)
88
90
  break if ratios.length >= depth
89
91
  n += 1
90
92
  end
@@ -113,7 +115,7 @@ class Tonal::Ratio
113
115
  while boundary <= max_boundary
114
116
  vacinity = ratio.respond_to?(:to_basic_ratio) ? to_basic_ratio.scale(scale) : ratio.scale(scale)
115
117
  self.class.neighbors(away: boundary, vacinity: vacinity).each do |neighbor|
116
- ratios << neighbor if (neighbor != ratio) && ratio.class.within_cents?(self_in_cents, neighbor.to_cents, within) && (neighbor.max_prime <= max_prime) && (neighbor.min_prime >= min_prime)
118
+ ratios << neighbor if (neighbor != ratio) && ratio.class.within_cents?(self_in_cents, neighbor.to_cents, within) && max_prime_at_most?(neighbor, max_prime) && !prime_factor_at_most?(neighbor, min_prime - 1)
117
119
  end
118
120
  boundary += 1
119
121
  end
@@ -140,6 +142,22 @@ class Tonal::Ratio
140
142
  end.flatten).to_a
141
143
  end
142
144
 
145
+ # Filters approximations without fully factoring them (a Float's convergents
146
+ # can have huge prime factors). Float::INFINITY skips the factoring.
147
+ def max_prime_at_most?(candidate, max_prime)
148
+ return true if max_prime.infinite?
149
+ Prime.each(max_prime).inject([candidate.antecedent, candidate.consequent]) do |(ante, cons), p|
150
+ ante /= p while (ante % p).zero?
151
+ cons /= p while (cons % p).zero?
152
+ [ante, cons]
153
+ end.all?{|n| n == 1 }
154
+ end
155
+
156
+ def prime_factor_at_most?(candidate, prime)
157
+ Prime.each(prime).any?{|p| (candidate.antecedent % p).zero? || (candidate.consequent % p).zero? }
158
+ end
159
+ private :max_prime_at_most?, :prime_factor_at_most?
160
+
143
161
  # @return [Array] an array of Tonal::Ratio neighbors in the scaled ratio's grid neighborhood
144
162
  # @example
145
163
  # Tonal::Ratio::Approximation.neighbors(vacinity: (3/2r).ratio(reduced:false).scale(256), away: 1)
@@ -1,4 +1,4 @@
1
1
  module Tonal
2
2
  TOOLS_PRODUCER = "mTonal"
3
- TOOLS_VERSION = "8.8.3"
3
+ TOOLS_VERSION = "8.9.0"
4
4
  end
@@ -180,28 +180,32 @@ class Numeric
180
180
  # @example
181
181
  # (3/2r).prime_vector => Vector[-1, 1]
182
182
  # @param reduced
183
+ # @param limit [Integer] the largest prime to factor by, see Tonal::Ratio#prime_divisions
183
184
  #
184
- def prime_vector(reduced: false) = ratio(reduced:).prime_vector
185
+ def prime_vector(reduced: false, limit: Tonal::Ratio::PRIME_LIMIT) = ratio(reduced:).prime_vector(limit:)
185
186
  alias :monzo :prime_vector
186
187
  alias :prime_exponent_vector :prime_vector
187
188
 
188
- # @return [Array], self decomposed into its prime factors
189
+ # @return [Array, nil], self decomposed into its prime factors, or nil if self has a prime factor above limit
189
190
  # @example
190
191
  # (31/30r).prime_divisions => [[[31, 1]], [[2, 1], [3, 1], [5, 1]]]
192
+ # @param limit [Integer] the largest prime to factor by, see Tonal::Ratio#prime_divisions
191
193
  #
192
- def prime_divisions = [self.numerator.prime_division, self.denominator.prime_division]
194
+ def prime_divisions(limit: Tonal::Ratio::PRIME_LIMIT) = ratio.prime_divisions(limit:)
193
195
 
194
- # @return [Integer] the maximum prime factor of self
196
+ # @return [Integer, nil] the maximum prime factor of self, or nil if self has a prime factor above limit
195
197
  # @example
196
198
  # (31/30r).max_prime => 31
199
+ # @param limit [Integer] the largest prime to factor by, see Tonal::Ratio#prime_divisions
197
200
  #
198
- def max_prime = prime_divisions.flatten(1).map(&:first).max
201
+ def max_prime(limit: Tonal::Ratio::PRIME_LIMIT) = ratio.max_prime(limit:)
199
202
 
200
- # @return [Integer] the minimum prime factor of self
203
+ # @return [Integer, nil] the minimum prime factor of self, or nil if self has a prime factor above limit
201
204
  # @example
202
205
  # (31/30r).min_prime => 2
206
+ # @param limit [Integer] the largest prime to factor by, see Tonal::Ratio#prime_divisions
203
207
  #
204
- def min_prime = prime_divisions.flatten(1).map(&:first).min
208
+ def min_prime(limit: Tonal::Ratio::PRIME_LIMIT) = ratio.min_prime(limit:)
205
209
 
206
210
  # @return [Vector], self expressed as a Vector
207
211
  # @example
@@ -271,18 +275,6 @@ class Integer
271
275
  #
272
276
  def ed(modulo, equave: 2/1r) = Tonal::ReducedRatio.ed(self, modulo, equave:)
273
277
 
274
- # @return [Integer] the maximum prime factor of self
275
- # @example
276
- # 72.max_prime => 3
277
- #
278
- def max_prime = self.prime_division.map(&:first).max
279
-
280
- # @return [Integer] the minimum prime factor of self
281
- # @example
282
- # 72.min_prime => 2
283
- #
284
- def min_prime = self.prime_division.map(&:first).min
285
-
286
278
  # @return [Integer] the factorial of self
287
279
  # @example
288
280
  # 5.factorial => 120
data/lib/tonal/ratio.rb CHANGED
@@ -6,6 +6,7 @@ class Tonal::Ratio
6
6
 
7
7
  PRECISION = 2
8
8
  DEFAULT_EQUAVE = 2/1r
9
+ PRIME_LIMIT = 10_000
9
10
 
10
11
  attr_reader :antecedent, :consequent, :equave, :reduced_antecedent, :reduced_consequent
11
12
 
@@ -356,21 +357,25 @@ class Tonal::Ratio
356
357
  Math.atan2(consequent, antecedent).round(round)
357
358
  end
358
359
 
359
- # @return [Array], self decomposed into its prime factors
360
+ # @return [Array, nil], self decomposed into its prime factors, or nil if self has a prime factor above limit (e.g. an irrational ratio built from a Float)
360
361
  # @example
361
362
  # Tonal::Ratio.new(31/30r).prime_divisions => [[[31, 1]], [[2, 1], [3, 1], [5, 1]]]
363
+ # @param limit [Integer] the largest prime to factor by
362
364
  #
363
- def prime_divisions
364
- [antecedent.prime_division, consequent.prime_division]
365
+ def prime_divisions(limit: PRIME_LIMIT)
366
+ ante = prime_division_within(antecedent, limit)
367
+ cons = prime_division_within(consequent, limit)
368
+ [ante, cons] if ante && cons
365
369
  end
366
370
 
367
- # @return [Vector], self represented as a prime vector
371
+ # @return [Vector, nil], self represented as a prime vector, or nil if self is 1/1 or has a prime factor above limit
368
372
  # @example
369
373
  # Tonal::Ratio.new(3/2r).prime_vector => Vector[-1, 1]
374
+ # @param limit [Integer] the largest prime to factor by
370
375
  #
371
- def prime_vector
372
- pds = prime_divisions
373
- return nil if pds.all?(&:empty?)
376
+ def prime_vector(limit: PRIME_LIMIT)
377
+ pds = prime_divisions(limit:)
378
+ return nil if pds.nil? || pds.all?(&:empty?)
374
379
 
375
380
  max = [pds.first.max{|p| p.first}, pds.last.max{|p| p.first}].compact.max.first
376
381
 
@@ -384,20 +389,22 @@ class Tonal::Ratio
384
389
  alias :monzo :prime_vector
385
390
  alias :prime_exponent_vector :prime_vector
386
391
 
387
- # @return [Integer] the maximum prime factor of self
392
+ # @return [Integer, nil] the maximum prime factor of self, or nil if self has a prime factor above limit
388
393
  # @example
389
394
  # Tonal::Ratio.new(31/30r).max_prime => 31
395
+ # @param limit [Integer] the largest prime to factor by
390
396
  #
391
- def max_prime
392
- prime_divisions.flatten(1).map(&:first).max
397
+ def max_prime(limit: PRIME_LIMIT)
398
+ prime_divisions(limit:)&.flatten(1)&.map(&:first)&.max
393
399
  end
394
400
 
395
- # @return [Integer] the minimum prime factor of self
401
+ # @return [Integer, nil] the minimum prime factor of self, or nil if self has a prime factor above limit
396
402
  # @example
397
403
  # Tonal::Ratio.new(31/30r).min_prime => 2
404
+ # @param limit [Integer] the largest prime to factor by
398
405
  #
399
- def min_prime
400
- prime_divisions.flatten(1).map(&:first).min
406
+ def min_prime(limit: PRIME_LIMIT)
407
+ prime_divisions(limit:)&.flatten(1)&.map(&:first)&.min
401
408
  end
402
409
 
403
410
  # =================================
@@ -684,6 +691,24 @@ class Tonal::Ratio
684
691
  @approximation = Approximation.new(ratio: self)
685
692
  end
686
693
 
694
+ # Trial-divides n by the primes up to limit, so a Float's huge binary
695
+ # fraction is rejected without a full (and very slow) factorization.
696
+ # A cofactor left below limit**2 has no factor up to limit, so it is prime.
697
+ def prime_division_within(n, limit)
698
+ division = []
699
+ Prime.each(limit) do |prime|
700
+ break if n <= 1
701
+ exponent = 0
702
+ while (n % prime).zero?
703
+ n /= prime
704
+ exponent += 1
705
+ end
706
+ division << [prime, exponent] if exponent > 0
707
+ end
708
+ return division if n <= 1
709
+ division << [n, 1] if n < limit**2
710
+ end
711
+
687
712
  def raise_if_negative(*args)
688
713
  raise ArgumentError, "Arguments must be greater than zero" if args.any?{|i| i < 0 }
689
714
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tonal-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.8.3
4
+ version: 8.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Hales-Garcia