Fractions, SimpleNumber, Magnitude

Hi good folks!

Whats going on here?

(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) // 2.0
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)).asInteger // 1
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)).ceil.asInteger // 2
(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) == 2.0 // false
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)).ceil == 2.0 // true
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)) + 1.0 == 3.0 // true
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)) + 0.2 == 2.2 // false
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)) + 0.3 == 2.3 // true
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)) + 0.201 == 2.201 // false
((1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)) + 0.202 == 2.202 // true
(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) == (6/3) // false
(1/3) + (1/3) == 0.66666666666667 // false
(1/3) + (1/3) == (2/3) // true

Its all float imperfections?
In lisp fractions will stay fractions until they are made rationals. (1/3) + (1/3) equals 2/3 not a rounded 0.66666666666667. So in sclang a fraction becomes a rational immediately? Or they are really just a syntactical representation of a rational? So where and why exactly does this rounding error happen?

Its is puzzling to me how the language can return

(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3)

as 2.0, when it is not really 2.0 until some procedural magic has rounded it up/down and made it 2.0

1 Like
(1/3).class -> Float

As was discussed in the thread below the representation of Floats in the post-window is not precise. Should SuperCollider Show Full Floating-Point Precision? Time for a Change?

(1/3).asFraction -> [1, 3]

but this is an Array.

1 Like

I have good news: there is a quark that implements rational numbers. Whatever is possible to implement as a quark is there. However, since it’s not part of the std lib, you may need to convert it to a Float at the end.

More here: Rational Quark update

2 Likes

By the way, I made some tiny changes so we can use it directly with Patterns. (For this use case, I still need to work a bit more, and test it)

Rational durations now work cleanly inside Pbind, including additive meters, polyrhythms, and probability-weighted rhythms. Here’s a small example and a longer ā€œthangā€ (just wrote that, may need polishing):


// Quarks.install("https://github.com/smoge/Rational")

(
var pattern = [2%/8, 2%/8, 3%/8];
pattern.sum;

Pbind(
	\degree, Pseq([0, 2, 4], inf),
	\dur,    Pseq(pattern, inf)
).play;
)

// --- piece ---

(
s.waitForBoot {

	var pitchRatios = (
		unison: 1%/1, major2: 9%/8, minor3: 6%/5, major3: 5%/4,
		fourth: 4%/3, fifth: 3%/2, major6: 5%/3,
		minor7: 9%/5, octave: 2%/1
	);

	var ratios = pitchRatios.values;
	var root = 207.5;
	var ratioToFreq = { |r| root * r.asFloat };

	SynthDef(\ping, { |out=0, freq=440, amp=0.1, pan=0|
		var env, sig, mod, body;
		env = EnvGen.kr(
			Env.perc(0.004, 0.35, curve: -5),
			doneAction: 2
		);
		mod = SinOsc.kr(5, Rand(0, 2pi)).range(0.997, 1.003);
		sig = SinOsc.ar(freq * mod);
		body = SinOsc.ar(freq * 2.01, 0, 0.2);
		sig = sig + body;
		sig = LPF.ar(sig, freq * 3.5);
		sig = sig * env * amp;
		sig = Splay.ar(sig, 0.15);
		Out.ar(out, Balance2.ar(sig[0], sig[1], pan));
	}).add;

	SynthDef(\bell, { |out=0, freq=440, amp=0.1, pan=0, decay=3|
		var env, exc, sig, modes;
		env = EnvGen.kr(
			Env.perc(0.01, decay, curve: -6),
			doneAction: 2
		);
		exc = PinkNoise.ar(0.12)
		* EnvGen.kr(Env.perc(0.002, 0.04));
		modes = [1, 2.02, 3.01, 4.12, 5.3];
		sig = Mix.fill(modes.size, { |i|
			Ringz.ar(
				exc,
				freq * modes[i],
				decay * (0.9 - (i * 0.12))
			)
		});
		sig = LPF.ar(sig, 8000);
		sig = sig * env * amp;
		Out.ar(out, Pan2.ar(sig, pan));
	}).add;

	s.sync;

	~voiceOne = Pbind(
		\instrument, \ping,
		\dur, Prand((1..13).collect { |n| n %/ 51 }, inf),
		\freq, Pseq([
			ratioToFreq.(pitchRatios.fifth),
			Pfunc { ratioToFreq.(ratios.choose) },
			ratioToFreq.(pitchRatios.octave)
		], inf) * Prand([0.5, 1, 2], inf),
		\amp, Pwhite(0.01, 0.1),
		\pan, Pwhite(0.4, 0.7)
	);

	~voiceTwo = Pbind(
		\instrument, \bell,
		\dur, Pseq( { rrand(1, 21) %/ 37 }!13, inf),
		\freq, Pfunc { ratioToFreq.(ratios.choose) },
		\amp, Pwhite(0.01, 0.07),
		\pan, Pwhite(0.1, 0.9)
	);

	~piece = Ppar([~voiceOne, ~voiceTwo]).play;
};
)
3 Likes

Yes, it’s all floating point rounding error. The numbers are rounded for posting (conversion to string) but internally they keep as much precision as double allows. But since 1/3 is an infinitely repeating binary fraction, there is some rounding, hence the results aren’t exact.

hjh

2 Likes

This is essentially correct, with just a thing worth clarifying.

What happens is that the value is approximated at the moment it’s created, because it has to be represented in binary using floating-point. Internally, the system then works with that binary approximation.

When the value is later converted to a string for display, there’s another rounding step involved, but this doesn’t introduce a new error — it simply makes the existing approximation visible.

(An alternative, mentioned in this thread, is a better convertion method into/from string, but that’s another thread)

So you can think of it as two stages:

  • the approximation needed to store the number in binary
  • the purely presentational rounding as decimal text
1 Like

Sure the post window is not exact. But what happens here?

(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) + 0.201 == 2.201 // false
(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) + 0.202 == 2.202 // true
(1/3) + (1/3) + (1/3) + (1/3) + (1/3) + (1/3) + 0.2021 == 2.2021 // false
// here staying in "fraction land" as long as possible
a=((1/3).asFraction!6).sum;[a[0],a[1]/6][0..1].asFloat.reduce('/') + 0.2021 == 2.2021 // true
1 Like

0.202 happens to land on a binary value whose rounding compensates for the earlier error.

It is an accident.

1 Like

Thanks everyone I was in a fog :fog: now its clear sky :sunny:

1 Like

Just a little bit about the many ways in which lisp and supercollider are different…

In supercollider we don’t do any optimisations on the code (like constant folding). Everything (sort of) is an object, and all you can do (sort of) is send messages, which are always dispatched and unknown to the compiler (sort of).

Your code looks like this to the compiler…

1.perform('/', 3).perform('+', 1.perform('/', 3))

The compiler isn’t allowed to know that 1/3 is a ā€˜rational’. It is up to the 1 to decide what / means, and what + means… This means you can change the definition of + and / if you like!

If you want to construct a ā€˜fraction’ like object, you need to construct one with the syntax Fraction(1, 3). It would then become the Fraction object’s responsibility to implement addition.

Converting to a fraction using .asFraction doesn’t really work because you have already send messages to floats.

(1/3).asFraction === 1.perform('/', 3).asFraction

Lisp’s compiler is able to get around this because it assumes that 1+1 is in fact 2… supercollider does not do this.

1 Like

fantastic insight! thank you

1 Like

In simple words, sclang has no concept of rational numbers at all. In Lisp, Haskell, Perl 6 (Raku), for example, expressions like (1/3) produce exact rational numbers, and arithmetic on them remains exact unless explicitly converted to floating point.

In SuperCollider, numeric operators are just messages sent to objects (and can be even be redefined, which the Rational quark does hehe), which return a Float. No rational number ever exists in the process. The compiler doesn’t even try to ā€œunderstandā€ algebraic identities.

The source of the mismatch is that musical time should be computed in a different algebra than signal time, and SuperCollider, by design, is optimized for the latter.

EDIT: rational numbers will always be a bit more expensive because of gcd operations. I tried my best in sclang, but it can still be improved with a binary gcd algorithm (using shifts). That would need to modify sclang C++ code to make a difference.

1 Like

This thread has inspired a GitHub issue for further discussion:

2 Likes

I tried once. The interesting thing is that the code not only comes with property-based testing, but it is also fast enough for granular synthesis, with 64-bit precision, even supporting Patterns, which covers all the weirdest edge cases.

The hard part about completing the integration with Patterns is that the only way is to overwrite too many things from std. Also, enhancing the bottleneck (gcd) would not even be considered, I think. (I mean, the quark implementation gives me better results than the primitive)

There are other things about it. It is safer than much code, like the official gcd code, which is naive and does not implement cross-reduction, which prevents overflow a lot. Without cross-reduction, the intermediate representation can easily explode. (Not C++ std lib, of course. The one on sc sourcecode)

The property-based tests large values and passes every single property of rational numbers (not arbitrarily big, but much better). Besides, its complete arithmetic implementation, it is a proper numeric type heavily tested.

My implementation of rational numbers is not a second-class citizen. Proved by testing, not by comments on the internet.

Constructive criticism is welcome, for me it always went far beyond constructive criticism, more like somebody whacked out my stuff, made with love, and reviewed by a scammer or a self-pimped up person. The reason? I don’t know. If I have the right to go to this level and say what I think, I don’t know either. I don’t know where those people come from. Maybe be a problem with my Double Nested Array, or something I said in another context. I don’t really care, does it make a difference?

@smoge I have removed the expletive from your last post, since you didn’t.

General reply:

I’m not going to be consistently available for the next 18 hours or so (and for whatever reason, this hotel is blocking my VPN, which I need to access github, so I’m not able to weigh in over there). However I’m getting a bit concerned that this discussion may go off the rails, and I won’t be able to monitor the forum closely through the evening.

So 1/ other moderators, please keep an eye on this.

2/ I’m torn between the wish not to discourage contributions by meeting them with immediate skepticism vs several other recognitions, such as that the class library is already a bit bloated. Clearly a negative vote on this PR has caused some offense (and perhaps I’m a bit sensitive to that, since there was a case awhile back where I was one of those who was perceived as shooting down an enhancement and I probably should have been more receptive to it) – but it’s essential that we as a community be able to evaluate the maintenance-cost vs user-benefit ratio rationally without fear of emotionally loaded replies if someone comes up with a different evaluation from one’s own.

So the one flagged reply, I’m going to honor that flag. The other, unflagged post… I’ll leave it for now? But let’s please rein it in.

hjh

1 Like

I got exactly the same message about ā€œ32bitā€ today.

It is funny, because I wrote that in 2017, it has testing and documentation.

Yo
Didn’t you listen to the last round,?
Pay attention, you’re saying the same stuff that he said
Matter fact, dawg, here’s a pencil

1 Like

@salkin-mada @mstep

I believe the following post on one of my PRs may be of interest to you:

1 Like