[Python-ideas] Trigonometry in degrees

375 views
Skip to first unread message

Robert Vanden Eynde

unread,
Jun 7, 2018, 4:07:56 PM6/7/18
to python-ideas
I suggest adding degrees version of the trigonometric functions in the math module.

- Useful in Teaching and replacing calculators by python, importing something is seen by the young students much more easy than to define a function.

- Special values could be treated, aka when the angle is a multiple of 90, young students are often surprise to see that cos(pi/2) != 0

Testing for a special value Isn't very costly (x % 90 == 0) but it could be pointed out that there is a small overhead using the "degrees" equivalent of trig function because of the radians to degrees conversion And the special values testing.

- Standard names will be chosen so that everyone will use the same name convention. I suggest adding a "d" like sind, cosd, tand, acosd, asind, atand, atan2d.

Another option would be to add "deg" or prepend "d" or "deg" however the name should be short.

sind, dsin, sindeg or degsin ?

We can look in other languages what they chose.

Creating a new package like 'from math.degrees import cos' however I would not recommend that because "cos" in the source code would mean to lookup the import to know if it's in degrees or radians (and that leads to very filthy bugs). Also "degrees" is already so the name would have to change the name of the package.

- Also in the cmath module. Even though the radians make more sense in the complex plane. The same functions sin cos tan, asin acos atan, alongside with phase and polar.

Here's my current implementation :

def cosd(x):
    if x % 90 == 0:
        return (1, 0, -1, 0)[int(x // 90) % 4]
    else:
        return cos(radians(x))

def sind(x):
    if x % 90 == 0:
        return (0, 1, 0, -1)[int(x // 90) % 4]
    else:
        return sin(radians(x))

def tand(x):
    if x % 90 == 0:
        return (0, float('inf'), 0, float('-inf'))[int(x // 90) % 4]
    else:
        return tan(radians(x))

The infinity being positive of negative is debatable however, here I've chosen the convention lim tan(x) as x approaches ±90° from 0

def acosd(x):
    if x == 1: return 0
    if x == 0: return 90
    if x == -1: return 180
    return degrees(acos(x))

def asind(x):
    if x == 1: return 90
    if x == 0: return 0
    if x == -1: return -90
    return degrees(asin(x))

However, currently [degrees(acos(x)) for x in (1,0,-1)] == [0, 90, 180] on my machine so maybe the test isn't necessary.

Testing for Special values of abs(x) == 0.5 could be an idea but I don't think the overhead is worth the effort.

Probably this has already been discussed but I don't know how to check that.

Ryan Gonzalez

unread,
Jun 7, 2018, 4:23:02 PM6/7/18
to Robert Vanden Eynde, python-ideas

You could always do e.g. math.sin(math.degress(radians)) and so forth...

_______________________________________________
Python-ideas mailing list
Python...@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

Rob Speer

unread,
Jun 7, 2018, 4:46:55 PM6/7/18
to Ryan Gonzalez, python-ideas
You meant math.radians(degrees), and Robert already mentioned the problem with this:

>>> math.cos(math.radians(90))
6.123233995736766e-17

Yuval Greenfield

unread,
Jun 7, 2018, 5:03:10 PM6/7/18
to robertva...@hotmail.com, python-ideas
On Thu, Jun 7, 2018 at 1:07 PM Robert Vanden Eynde <robertva...@hotmail.com> wrote:
I suggest adding degrees version of the trigonometric functions in the math module.


You can create a pypi package that suits your needs. If it becomes popular it could considered for inclusion in the standard library.

Would that work for you?

Yuval

Hugh Fisher

unread,
Jun 7, 2018, 6:18:05 PM6/7/18
to python...@python.org
> Date: Thu, 7 Jun 2018 12:33:29 +0000
> From: Robert Vanden Eynde <robertva...@hotmail.com>
> To: python-ideas <python...@python.org>
> Subject: [Python-ideas] Trigonometry in degrees
> Message-ID:
> >
> I suggest adding degrees version of the trigonometric functions in the math module.
>
> - Useful in Teaching and replacing calculators by python, importing something is seen by the young students much more easy than to define a function.

I agree that degrees are useful for teaching. They are also very
useful for graphics
programming, especially with my favourite OpenGL API. But I think that
the use of
radians in programming language APIs is more prevalent, so the initial advantage
of easy learning will be outweighed by the long term inconvenience of
adjusting to
what everyone else is doing.

Writing degrees(x) and radians(x) is a little inconvenient, but it
does make it clear
what units are being used. And even if your proposal is adopted, there
is still going
to be a lot of code around that uses the older math routines. With the
current API
it is a least safe to assume that angles are radians unless stated otherwise.

> - Special values could be treated, aka when the angle is a multiple of 90, young students are often surprise to see that cos(pi/2) != 0
>
> Testing for a special value Isn't very costly (x % 90 == 0) but it could be pointed out that there is a small overhead using the "degrees" equivalent of trig function because of the radians to degrees conversion And the special values testing.

Not just young students :-) I agree with this, but I would prefer the
check to be in
the implementation of the existing functions as well. Any sin/cos very
close to 0
becomes 0, any close to 1 becomes 1.

> - Standard names will be chosen so that everyone will use the same name convention. I suggest adding a "d" like sind, cosd, tand, acosd, asind, atand, atan2d.

Not "d". In the OpenGL 3D API, and many 3D languages/APIs since, appending "d"
means "double precision". It's even sort of implied by the C math
library which has
sinf and friends for single precision.

>
> Creating a new package like 'from math.degrees import cos' however I would not recommend that because "cos" in the source code would mean to lookup the import to know if it's in degrees or radians (and that leads to very filthy bugs). Also "degrees" is already so the name would have to change the name of the package.

Agree, not a good idea.

--

cheers,
Hugh Fisher

Robert Vanden Eynde

unread,
Jun 7, 2018, 7:10:09 PM6/7/18
to python-ideas
- I didn't know there were sinf in C (that's since C99), I was aware of the 'd' postfix in opengl.

So yeah, sind would be a bad idea, but sindeg or degsin would be too long, hmm, and I can settle for the Pre or Post fix. sindeg(90) degsin(90) are both pretty, the first emphasize on the "degree" part and the second on the "sin(90)" part. I feel I prefer sindeg, cosdeg, atandeg, atan2deg, phasedeg, rectdeg hmhm

By the way I've seen a stackoverflow answer using Sin and Cos with a capital letter, doesn't seem very explicit to me.

- I could do a pypi for it for sure, I didn't know it was that easy to create a repo actually. degreesmath (and degreesmath.cmath ?) would be a good package name but again I don't want to name the functions sin, cos. People could rename them on import anyway (let the fools be fools as long as they don't hurt anyone).

- I agree radians should be the default, but is it especially Because sin/cos must be in radians ? And because it's more efficient ? The problem arrises when Mixing units in the same program.

However, should everyone use m³ and not Liters because they're the SI units ? That's more a problems of "mixing units and not sticking to one convention". I've seen lot of libraries using degrees (and not just good old glRotate).

Let's notice there are some libraries that wrap units so that one can mix them safely (and avoid to add meters to seconds).

Let's be honest, radians are useful only when converting arc length, areas or dealing with derivatives, signals, or complex numbers (engineering stuffs), and efficiency of sin/cos implementations. When doing simple 2D/3D applications, angles are just angles and nobody needs to know that derivative of sin(ax) is a·cos(ax) if x is in radians.

- Integers are more convenient than float, you could add 1 degree every frame at 60fps to a counter and after 60 frames you'll do a full turn, adding tau/360 doesn't add so well (floating point representation). Having exact representation for multiple of 90 degrees is a big plus. Another advantage is also being able to check if the angle is particular (multiple of 30 or 90 for example). Especially python Integers with infinite precision.

- Everyone knows degrees, whereas radians are known only by people that had math in 10th grade. I know it's easy to say "just convert" but trust me, not everyone is confident with unit conversions, when you ask "what's the unit of angle ?" people will think of degrees.

- Changing the behavior for current cos/sin function to have cos(pi/2) being exact is a bad idea in my opinion, the good old sin/cos from C exist for a long time and people rely on the behaviors. That would break too much existing code for no much trouble. And would slow Current applications relying on the efficiency of the C functions.

- I totally agree writing degrees(...) and radians(...) make it clear and explicit. That's why I strongly discourage people defining their own "sin" function that'd take degrees, therefore I look for a new function name (sindeg).

Richard Damon

unread,
Jun 7, 2018, 10:40:24 PM6/7/18
to python...@python.org
First I feel the need to point out that radians are actually fairly
fundamental in trigonometry, so there is good reasons for the base
functions to be based on radians. The fact that the arc length of the
angle on the unit circle is the angle in radians actually turns out to
be a fairly basic property.

To make it so that sindeg/cosdeg of multiples of 90 come out exact is
probably easiest to do by doing the angle reduction in degrees (so the
nice precise angles stay as nice precise angles) and then either adjust
the final computation formulas for degrees, or convert the angle to
radians and let the fundamental routine do the small angle computation.

While we are at it, it might be worth thinking if it might make sense to
also define a set of functions using circles as a unit (90 degrees =
0.25, one whole revolution = 1)

--
Richard Damon

Greg Ewing

unread,
Jun 8, 2018, 1:27:38 AM6/8/18
to python...@python.org
Richard Damon wrote:
> First I feel the need to point out that radians are actually fairly
> fundamental in trigonometry,

Even more so in calculus, since the derivative of sin(x)
is cos(x) if and only if x is in radians.

--
Greg

Stephen J. Turnbull

unread,
Jun 8, 2018, 1:38:36 AM6/8/18
to Richard Damon, python...@python.org
Richard Damon writes:

> To make it so that sindeg/cosdeg of multiples of 90 come out exact is
> probably easiest to do by doing the angle reduction in degrees (so the
> nice precise angles stay as nice precise angles) and then either adjust
> the final computation formulas for degrees, or convert the angle to
> radians and let the fundamental routine do the small angle
> computation.

You would still need some sort of correction for many angles because
of the nature of floating point computation. The modern approach is
that floating point is exact computation but some numbers can't be
exactly represented. Since Pi is irrational, Pi/4 is too, so it
definitely cannot be represented. Making a correction to a number
that "looks like" Pi/4 is against this philosophy. So you need
separate functions (a "high school mode" argument would be frowned
upon, I think).

> While we are at it, it might be worth thinking if it might make sense to
> also define a set of functions using circles as a unit (90 degrees =
> 0.25, one whole revolution = 1)

While 1/4 is no problem, 1/6 is not exactly representable as a binary
floating point number, and that's kind of an important angle for high
school trigonometry (which is presumably what we're talking about here
-- a symbolic math program would not represent Pi by a floating point
number, but rather as a symbol with special properties as an argument
to a trigonometric function!)

My bias is that people who want to program this kind of thing just
need to learn about floating point numbers and be aware that they're
going to have to accept that

>>> from math import cos, radians
>>> cos(radians(90))
6.123233995736766e-17
>>>

is good enough for government work, including at the local public high
school.

Of course, I admit that's a bias, not a scientific fact. :-)


--
Associate Professor Division of Policy and Planning Science
http://turnbull.sk.tsukuba.ac.jp/ Faculty of Systems and Information
Email: turn...@sk.tsukuba.ac.jp University of Tsukuba
Tel: 029-853-5175 Tennodai 1-1-1, Tsukuba 305-8573 JAPAN

Yuval Greenfield

unread,
Jun 8, 2018, 1:45:25 AM6/8/18
to turnbull....@u.tsukuba.ac.jp, python-ideas
On Thu, Jun 7, 2018 at 10:38 PM Stephen J. Turnbull <turnbull....@u.tsukuba.ac.jp> wrote:

6.123233995736766e-17
>>>

is good enough for government work, including at the local public high
school.


There probably is room for a library like "fractions" that represents multiples of pi or degrees precisely. I'm not sure how complicated or valuable of an endeavor that would be. But while I agree that floating point is good enough, we probably can do better.
 

Steven D'Aprano

unread,
Jun 8, 2018, 1:49:47 AM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 08:17:02AM +1000, Hugh Fisher wrote:

> But I think that the use of
> radians in programming language APIs is more prevalent, so the initial advantage
> of easy learning will be outweighed by the long term inconvenience of
> adjusting to what everyone else is doing.

But why would you need to?

If we had a degrees API, why wouldn't people just use it?


> Writing degrees(x) and radians(x) is a little inconvenient, but it
> does make it clear what units are being used.

I never know what conversion function to use. I always expect something
like deg2rad and rad2deg. I never remember whether degrees(x) expects an
angle in degrees or returns an angle in degrees.

So I would disagree that it is clear.


> And even if your proposal is adopted, there is still going
> to be a lot of code around that uses the older math routines.

Why would that be a problem?

When the iterator protocol was introduced, that didn't require lists and
sequences to be removed.


> Not just young students :-) I agree with this, but I would prefer the
> check to be in the implementation of the existing functions as well.
> Any sin/cos very close to 0 becomes 0, any close to 1 becomes 1.

Heavens no! That's a terrible idea -- that means that functions which
*ought to return 0.9999987 (say) will suddenly become horribly
inaccurate and return 1.

The existing trig functions are as close to accurate as is practical to
expect with floating point maths. (Although some platform's maths
libraries are less accurate than others.) We shouldn't make them *less*
accurate just because some people don't care for more than three decimal
places.


> > - Standard names will be chosen so that everyone will use the same
> > name convention. I suggest adding a "d" like sind, cosd, tand,
> > acosd, asind, atand, atan2d.
>
> Not "d". In the OpenGL 3D API, and many 3D languages/APIs since, appending "d"
> means "double precision".

Python floats are already double precision.

What advantage is there for reserving a prefix/suffix because some
utterly unrelated framework in another language uses it for a completely
different purpose?

Like mathematicians, we use the "h" suffix for hyperbolic sin, cos and
tan; should we have done something different because C uses ".h" for
header files, or because the struct module uses "h" as the format code
for short ints?

Julia provides a full set of trigonometric functions in both radians and
degrees:

https://docs.julialang.org/en/release-0.4/manual/mathematical-operations/#trigonometric-and-hyperbolic-functions

They use sind, cosd, tand etc for the variants expecting degrees. I
think that's much more relevant than OpenGL.

Although personally I prefer the look of d as a prefix:

dsin, dcos, dtan

That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
"tanned" etc.


--
Steve

Chris Angelico

unread,
Jun 8, 2018, 1:58:24 AM6/8/18
to python-ideas
On Fri, Jun 8, 2018 at 3:45 PM, Steven D'Aprano <st...@pearwood.info> wrote:
> Although personally I prefer the look of d as a prefix:
>
> dsin, dcos, dtan
>
> That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
> "tanned" etc.

Having it as a suffix does have one advantage. The math module would
need a hyperbolic sine function which accepts an argument in; and
then, like Charles Napier [1], Python would finally be able to say "I
have sindh".

ChrisA

[1] Apocryphally, alas.

Steven D'Aprano

unread,
Jun 8, 2018, 2:15:01 AM6/8/18
to python...@python.org
On Thu, Jun 07, 2018 at 10:39:06PM -0400, Richard Damon wrote:

> First I feel the need to point out that radians are actually fairly
> fundamental in trigonometry, so there is good reasons for the base
> functions to be based on radians. The fact that the arc length of the
> angle on the unit circle is the angle in radians actually turns out to
> be a fairly basic property.

People managed to use trigonometry for *literally* millennia before
radians were invented and named by James Thomson in 1873.

Just because they are, *in some sense*, mathematically fundamental
doesn't mean we ought to be using them for measurements. We don't write
large numbers using powers of e instead of powers of 10, just because
exponentiation to base e is in some sense more fundamental than other
powers.

Even the fact that we talk about sine, cosine and tangent as distinct
functions is mathematically unnecessary, since both cosine and tangent
can be expressed in terms of sine.

> While we are at it, it might be worth thinking if it might make sense to
> also define a set of functions using circles as a unit (90 degrees =
> 0.25, one whole revolution = 1)

Hardly anyone still uses grads, and even fewer people use revolutions
as the unit of angles. But if you did need revolutions, conveniently
many simple fractions of a revolution come out to be whole numbers of
degrees, thanks to 360 having lots of factors. All of these fractions of
a revolution are exact whole numbers of degrees:

1/2, 1/3, 1/4, 1/5, 1/6, 1/8, 1/9, 1/10, 1/12, 1/15, 1/18

so I don't believe we need a third set of trig functions for
revolutions.


--
Steve

Steven D'Aprano

unread,
Jun 8, 2018, 2:23:42 AM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 02:37:33PM +0900, Stephen J. Turnbull wrote:

> My bias is that people who want to program this kind of thing just
> need to learn about floating point numbers and be aware that they're
> going to have to accept that
>
> >>> from math import cos, radians
> >>> cos(radians(90))
> 6.123233995736766e-17
> >>>
>
> is good enough for government work, including at the local public high
> school.

In Australia, most secondary schools recommend or require CAS
calculators from about Year 10, sometimes even from Year 9. Most (all?)
state curricula for Year 11 and 12 mandate CAS calculators. Even
old-school scientific calcuators without the fancy CAS symbolic maths
are capable of having cos(90) return zero in degree mode.

It is quite common for high school students to expect cos(90°) to come
out as exactly zero. And why not? It's the 21st century, not 1972 when
four-function calculators were considered advanced technology :-)

To my mind, the question is not "should we have trig functions that take
angles in degrees" -- that's a no-brainer, of course we should. The only
questions in my mind are whether or not such a library is (1)
appropriate for the stdlib and (2) ready for the stdlib.


--
Steve

Greg Ewing

unread,
Jun 8, 2018, 2:35:28 AM6/8/18
to python...@python.org
Stephen J. Turnbull wrote:
> Since Pi is irrational, Pi/4 is too, so it
> definitely cannot be represented. Making a correction to a number
> that "looks like" Pi/4 is against this philosophy.

I'm not sure what all the fuss is about:

>>> from math import pi, sin
>>> sin(pi/2)
1.0
>>> sin(pi/2 + 2 * pi)
1.0
>>> sin(pi/2 + 4 * pi)
1.0
>>> sin(pi/2 + 8 * pi)
1.0
>>> sin(pi/2 + 16 * pi)
1.0
>>> sin(pi/2 + 32 * pi)
1.0

Seems to be more than good enough for most angle ranges
that your average schoolkid is going to be plugging
into it.

In fact you have to go quite a long way before expectations
start to break down:

>>> sin(pi/2 + 10000000 * pi)
1.0
>>> sin(pi/2 + 100000000 * pi)
0.9999999999999984

--
Greg

Greg Ewing

unread,
Jun 8, 2018, 2:50:04 AM6/8/18
to python-ideas
Chris Angelico wrote:
> The math module would
> need a hyperbolic sine function which accepts an argument in;

Except that the argument to hyperbolic trig functions is
not an angle in any normal sense of the word, so expressing
it in degrees makes little sense.

(However I do like the idea of a function called "tanhd"
and pronounced "tanned hide". :-)

--
Greg

Greg Ewing

unread,
Jun 8, 2018, 2:58:15 AM6/8/18
to python...@python.org
Steven D'Aprano wrote:
> Even
> old-school scientific calcuators without the fancy CAS symbolic maths
> are capable of having cos(90) return zero in degree mode.

FWIW, my Casio fx-100 (over 30 years old) produces exactly 1 for
both sin(90°) and sin(pi/2) for its version of pi.

--
Greg

Jacco van Dorp

unread,
Jun 8, 2018, 3:07:51 AM6/8/18
to python-ideas
Or when students get stuff e-17 out of a function, you teach them what
floating point numbers are and what gotcha's they can expect. The
simple version is "value is stored as a float, and a float gets
rounding errors below e-16", or for the more inquisitive minds you
give them nice places like
https://docs.python.org/3/tutorial/floatingpoint.html .

If they're really going to use what they learn, they're going to run
into it sooner or later. So having a bit of base knowledge about
floats is a lot more useful than having to google "why does sin()
return weird values python". At the very least, they'll isntead google
"float limitations", which is going to get them a lot closer to the
real information a lot faster.

That said, I wouldn't be that opposed to a dedicated type to remember
things about pi. Lets say....

class pi(Numeric):
"""Represents numbers that represent some function of pi"""

def __init__(self, mul=1):
self.multiplier = mul

def __mul__(self, other):
if isinstance(other, Numeric):
return self.__class__(self.multiplier*other)

(similar with the other special methods)
(Please consider the idea, not the exact code. I dont even know if
i spelled the numeric superclass right. Let alone making this type
hashable and immutable, which it should be.)

It's probably not a good idea to use that for performance-critical
parts, but for the more trivial applications, it could allow for more
clarity. Also, in a lot of common angles, it'd be far easier to
actually recognize special cases. you could also make it __repr__ like
f"Pi*{self.multiplier}", so you get a neat exact answer if you print
it..

Steven D'Aprano

unread,
Jun 8, 2018, 3:12:40 AM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 03:55:34PM +1000, Chris Angelico wrote:
> On Fri, Jun 8, 2018 at 3:45 PM, Steven D'Aprano <st...@pearwood.info> wrote:
> > Although personally I prefer the look of d as a prefix:
> >
> > dsin, dcos, dtan
> >
> > That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
> > "tanned" etc.
>
> Having it as a suffix does have one advantage. The math module would
> need a hyperbolic sine function which accepts an argument in; and
> then, like Charles Napier [1], Python would finally be able to say "I
> have sindh".

Ha ha, nice pun, but no, the hyperbolic trig functions never take
arguments in degrees. Or radians for that matter. They are "hyperbolic
angles", which some electrical engineering text books refer to as
"hyperbolic radians", but all the maths text books I've seen don't call
them anything other than a real number. (Or sometimes a complex number.)

But for what it's worth, there is a correspondence of a sort between the
hyperbolic angle and circular angles. The circular angle going between 0
to 45° corresponds to the hyperbolic angle going from 0 to infinity.

https://en.wikipedia.org/wiki/Hyperbolic_angle

https://en.wikipedia.org/wiki/Hyperbolic_function


> [1] Apocryphally, alas.

Don't ruin a good story with facts ;-)



--
Steve

Adam Bartoš

unread,
Jun 8, 2018, 5:02:45 AM6/8/18
to python...@python.org
Wouldn't sin(45 * DEG) where DEG = 2 * math.pi / 360 be better that sind(45)? This way we woudn't have to introduce new functions. (The problem with nonexact results for nice angles is a separate issue.)

Regards,
Adam Bartoš

Steven D'Aprano

unread,
Jun 8, 2018, 7:56:18 AM6/8/18
to python...@python.org

But that's not a separate issue, that's precisely one of the motives for
having dedicated trig functions for degrees.

sind(45) (or dsin(45), as I would prefer) could (in principle) return
the closest possible float to sqrt(2)/2, which sin(45*DEG) does not do:

py> DEG = 2 * math.pi / 360
py> math.sin(45*DEG) == math.sqrt(2)/2
False

Likewise, we'd expect cosd(90) to return zero, not something not-quite
zero:

py> math.cos(90*DEG)
6.123031769111886e-17

That's how it works in Julia:

julia> sind(45) == sqrt(2)/2
true

julia> cosd(90)
0.0


and I'd expect no less here. If we can't do that, there probably
wouldn't be much point in the exercise.

Steven D'Aprano

unread,
Jun 8, 2018, 8:29:34 AM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 06:34:25PM +1200, Greg Ewing wrote:

> I'm not sure what all the fuss is about:
>
> >>> from math import pi, sin
> >>> sin(pi/2)
> 1.0

Try cos(pi/2) or sin(pi/6).

Or try:

sin(pi/4) == sqrt(2)/2
tan(pi/4) == 1
tan(pi/3) == sqrt(3)

And even tan(pi/2), which ought to be an error, but isn't.

These are, of course, limitations due to the finite precision of floats.

But Julia gets the equivalent degree-based calculations all correct,
except for tan(90) where it returns Inf (NAN would be better, as the
limit from below and the limit from above are different).


--
Steve

Hugh Fisher

unread,
Jun 8, 2018, 9:20:04 AM6/8/18
to python...@python.org
> Date: Fri, 8 Jun 2018 15:45:31 +1000
> From: Steven D'Aprano <st...@pearwood.info>
> To: python...@python.org
> Subject: Re: [Python-ideas] Trigonometry in degrees
> Message-ID: <20180608054...@ando.pearwood.info>
> Content-Type: text/plain; charset=us-ascii
>
> On Fri, Jun 08, 2018 at 08:17:02AM +1000, Hugh Fisher wrote:
>
>> But I think that the use of
>> radians in programming language APIs is more prevalent, so the initial advantage
>> of easy learning will be outweighed by the long term inconvenience of
>> adjusting to what everyone else is doing.
>
> But why would you need to?
>
> If we had a degrees API, why wouldn't people just use it?

Is this going to be backported all the way to Python 2.7?

More generally, there is a huge body of code in C, C++, Java, JavaScript,
etc etc where angles are always passed as radians. Python programmers
will almost certainly have to read, and often write, such code. If everybody
else is doing something in a particular way then there is a strong case for
doing the same thing.

It's not as if Python programmers cannot use degrees. The built in
conversion functions make it easier to do so than most other languages.

> I never know what conversion function to use. I always expect something
> like deg2rad and rad2deg. I never remember whether degrees(x) expects an
> angle in degrees or returns an angle in degrees.
>
> So I would disagree that it is clear.

The degrees and radian functions follow the Python idiom for converting
values, eg str(x) is interpreted as converting x into a str. However the
analogy breaks down because str(x) is a NOP if x is already a string,
while degrees(x) can't tell whether x is already in degrees or not.

Maybe rad2deg would have been better, but the current solution is good
enough - and as noted above, much better than what you get in C or
JavaScript.

>> And even if your proposal is adopted, there is still going
>> to be a lot of code around that uses the older math routines.
>
> Why would that be a problem?

See above. Why do Python subscripts start from zero? Because most
programmers expect them to.

>> Not just young students :-) I agree with this, but I would prefer the
>> check to be in the implementation of the existing functions as well.
>> Any sin/cos very close to 0 becomes 0, any close to 1 becomes 1.
>
> Heavens no! That's a terrible idea -- that means that functions which
> *ought to return 0.9999987 (say) will suddenly become horribly
> inaccurate and return 1.
>
> The existing trig functions are as close to accurate as is practical to
> expect with floating point maths. (Although some platform's maths
> libraries are less accurate than others.) We shouldn't make them *less*
> accurate just because some people don't care for more than three decimal
> places.

But I want them to be more accurate. I didn't make myself clear. Like you,
I want cos(90 degrees) to be 0, not some small number. Other people have
pointed out the problem with trying to guess the result from the argument
value, so I am suggesting that the functions should instead look at the
calculated result and if it is sufficiently close to 0.0 or 1.0, assume that the
argument value was 90 degrees or some multiple thereof.

>> Not "d". In the OpenGL 3D API, and many 3D languages/APIs since, appending "d"
>> means "double precision".
>
> Python floats are already double precision.
>
> What advantage is there for reserving a prefix/suffix because some
> utterly unrelated framework in another language uses it for a completely
> different purpose?

Well I program using the OpenGL API in Python, so there's at least one
person who will find the d suffix confusing for that reason.

And the d suffix is used for types and functions in OpenGL shading language,
C, ARM/Intel assembler. The intersection with Python programmers may not
be very large, but again it is at least 1. So no they are not utterly unrelated.

> Like mathematicians, we use the "h" suffix for hyperbolic sin, cos and
> tan; should we have done something different because C uses ".h" for
> header files, or because the struct module uses "h" as the format code
> for short ints?

Is d used as a suffix by mathematicians though? The h works because the
context makes it clear which sense is being used, mathematics, C, or struct
module. Here we are discussing functions in the same module. Whether to
use d or deg is an arbitrary choice for mathematicians (AFAIK), so either
would work equally well. Since d can be confusing for others, to me that
would make deg preferable. But see below where I change my mind.

> Julia provides a full set of trigonometric functions in both radians and
> degrees:
>
> https://docs.julialang.org/en/release-0.4/manual/mathematical-operations/#trigonometric-and-hyperbolic-functions
>
> They use sind, cosd, tand etc for the variants expecting degrees. I
> think that's much more relevant than OpenGL.

OK, that's interesting, I did not know that. And a quick google shows that
Matlab also has sind and similar variants for degrees.

> Although personally I prefer the look of d as a prefix:
>
> dsin, dcos, dtan
>
> That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
> "tanned" etc.

If Julia and Matlab are sufficiently well known, I would prefer d as suffix
rather than prefix.

--

cheers,
Hugh Fisher

Kyle Lahnakoski

unread,
Jun 8, 2018, 9:53:59 AM6/8/18
to python...@python.org
Yes, I agree with making a module (called `rational_trig`?), that defines some Angle constants, and defines trig functions that accept Angle objects. Using angle objects will prevent the explosion of unit-specific variations on the trig functions (sin, sindeg, singrad, etc).  Like mentioned above, the Angle object is probably best implemented as a Rational of 2*pi, which will allow our favorite angles to be represented without floating point error.  We can define `degrees` and `radians` constants which can be used as units; then trig looks something like:

from rational_trig import cos

if cos(90*degrees) == 0:
    print("yay!")

It is probably slow as molasses, but maybe good enough for a teaching environment?


Jacco van Dorp

unread,
Jun 8, 2018, 9:55:59 AM6/8/18
to python-ideas
2018-06-08 15:19 GMT+02:00 Hugh Fisher <hugo....@gmail.com>:

>> Julia provides a full set of trigonometric functions in both radians and
>> degrees:
>>
>> https://docs.julialang.org/en/release-0.4/manual/mathematical-operations/#trigonometric-and-hyperbolic-functions
>>
>> They use sind, cosd, tand etc for the variants expecting degrees. I
>> think that's much more relevant than OpenGL.
>
> OK, that's interesting, I did not know that. And a quick google shows that
> Matlab also has sind and similar variants for degrees.
>
>> Although personally I prefer the look of d as a prefix:
>>
>> dsin, dcos, dtan
>>
>> That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
>> "tanned" etc.
>
> If Julia and Matlab are sufficiently well known, I would prefer d as suffix
> rather than prefix.

I graduated less than a year ago - Matlab at the very least is quite
well-known, we got lessons about it (although I was the one kid who
used python for his matlab assignments...you can make do, with
matplotlib, google, opencv, and numpy.). I also believe they give
free/heavily discounted licenses to schools, hoping that after
graduation, those students are used to matlab and will try to make
their employers buy full-priced versions.

I don't know about Julia, though.

Adam Bartoš

unread,
Jun 8, 2018, 5:12:55 PM6/8/18
to python...@python.org
Steven D'Aprano wrote:
> On Fri, Jun 08, 2018 at 10:53:34AM +0200, Adam Bartoš wrote:
>> Wouldn't sin(45 * DEG) where DEG = 2 * math.pi / 360 be better that
>> sind(45)? This way we woudn't have to introduce new functions. (The problem
>> with nonexact results for nice angles is a separate issue.)
>
> But that's not a separate issue, that's precisely one of the motives for
> having dedicated trig functions for degrees.
>
> sind(45) (or dsin(45), as I would prefer) could (in principle) return
> the closest possible float to sqrt(2)/2, which sin(45*DEG) does not do:
>
> py> DEG = 2 * math.pi / 360
> py> math.sin(45*DEG) == math.sqrt(2)/2
> False
>
> Likewise, we'd expect cosd(90) to return zero, not something not-quite
> zero:
>
> py> math.cos(90*DEG)
> 6.123031769111886e-17
>
>
>
> That's how it works in Julia:
>
> julia> sind(45) == sqrt(2)/2
> true
>
> julia> cosd(90)
> 0.0
>
>
> and I'd expect no less here. If we can't do that, there probably
> wouldn't be much point in the exercise.

But if there are both sin and dsin, and you ask about the difference between them, the obvious answer would be that one takes radians and the other takes degrees. The point that the degrees version is additionally exact on special values is an extra benefit. It would be nice to also fix the original sin, or more precisely to provide a way to give it a fractional multiple of pi. How about a special class PiMultiple that would represent a fractional multiple of pi?

PI = PiMultiple(1)
assert PI / 2 == PiMultiple(1, 2)
assert cos(PI / 2) == 0
DEG = 2 * PI / 360
assert sin(45 * DEG) == sqrt(2) / 2

Best regards,
Adam Bartoš


Richard Damon

unread,
Jun 8, 2018, 8:06:43 PM6/8/18
to python...@python.org
In one sense that is why I suggest a Circle based version of the trig
functions. In effect that is a multiple of Tau (= 2*pi) routine.

Richard Damon

Robert Vanden Eynde

unread,
Jun 8, 2018, 8:16:32 PM6/8/18
to python-ideas
- Thanks for pointing out a language (Julia) that already had a name convention. Interestingly they don't have a atan2d function. Choosing the same convention as another language is a big plus.

- Adding trig function using floats between 0 and 1 is nice, currently one needs to do sin(tau * t) which is not so bad (from math import tau, tau sounds like turn).

- Julia has sinpi for sin(pi*x), one could have sintau(x) for sin(tau*x) or sinturn(x).

Grads are in the idea of turns but with more problems, as you guys said, grads are used by noone, but turns are more useful. sin(tau * t) For The Win.

- Even though people mentionned 1/6 not being exact, so that advantage over radians isn't that obvious ?

from math import sin, tau
from fractions import Fraction
sin(Fraction(1,6) * tau)
sindeg(Fraction(1,6) * 360)

These already work today by the way.

- As you guys pointed out, using radians implies knowing a little bit about floating point arithmetic and its limitations. Integer are more simple and less error prone. Of course it's useful to know about floats but in many case it's not necessary to learn about it right away, young students just want their player in the game move in a straight line when angle = 90.

- sin(pi/2) == 1 but cos(pi/2) != 0 and sin(3*pi/2) != 1 so sin(pi/2) is kind of an exception.

Steven D'Aprano

unread,
Jun 8, 2018, 9:07:05 PM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 08:45:54AM +0000, Robert Vanden Eynde wrote:

> from math import sin, tau
> from fractions import Fraction
> sin(Fraction(1,6) * tau)
> sindeg(Fraction(1,6) * 360)
>
> These already work today by the way.

You obviously have a different understanding of the words "already work"
than I do:

py> sindeg(Fraction(1,6) * 360)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sindeg' is not defined

Since tau is a float, writing Fraction(1,6) * tau instead of tau/6 is a
waste of time. Also, Fraction(1,6) * 360 is also a waste of time, since
360/6 is not only exact, but can be done at compile-time.

Steven D'Aprano

unread,
Jun 8, 2018, 9:13:36 PM6/8/18
to python...@python.org
On Fri, Jun 08, 2018 at 11:11:09PM +0200, Adam Bartoš wrote:

> But if there are both sin and dsin, and you ask about the difference
> between them, the obvious answer would be that one takes radians and the
> other takes degrees. The point that the degrees version is additionally
> exact on special values is an extra benefit.

No, that's not an extra benefit, it is the only benefit!

If we can't make it exact for the obvious degree angles, there would be
no point in doing this. We'd just tell people to write their own
two-line functions:

def sindeg(angle):
return math.sin(math.radians(angle))


The only reason to even consider making this a standard library function
is if we can do better than that.


> It would be nice to also fix the original sin,

The sin function is not broken and does not need fixing.

(Modulo quirks of individual platform maths libraries.)


> or more precisely to provide a way to give it a
> fractional multiple of pi. How about a special class PiMultiple that would
> represent a fractional multiple of pi?

What is the point of that? When you pass it to math.sin, it still needs
to be converted to a float before sin can operate on it.

Unless you are proposing a series of dunder methods __sin__ __cos__ and
__tan__ to allow arbitrary classes to be passed to sin, cos and tan, the
following cannot work:

> PI = PiMultiple(1)
> assert PI / 2 == PiMultiple(1, 2)
> assert cos(PI / 2) == 0

Without a __cos__ dunder method that allows PiMultiple objects to
customise the result of cos(), that last line has to fail, because
cos(math.pi/2) == 0 fails.

> DEG = 2 * PI / 360
> assert sin(45 * DEG) == sqrt(2) / 2

Likewise.

Wes Turner

unread,
Jun 8, 2018, 10:10:21 PM6/8/18
to robertva...@hotmail.com, Python-Ideas
# Python, NumPy, SymPy, mpmath, sage trigonometric functions

## Python math module
- degrees(radians): Float degrees
- radians(degrees): Float degrees

## NumPy
- degrees(radians) : List[float] degrees
- rad2deg(radians): List[float] degrees
- radians(degrees) : List[float] radians
- deg2rad(degrees): List[float] radians



## SymPy
http://docs.sympy.org/latest/modules/functions/elementary.html#trionometric-functions

- sympy.mpmath.degrees(radians): Float degrees
- sympy.mpmath.radians(degrees): Float radians

  - cosd, sind

    > Let x, theta, phi, etc. be Symbols representing quantities in radians. Keep a list of these symbols: angles = [x, theta, phi]. Then, at the very end, use y.subs([(angle, angle*pi/180) for angle in angles]) to change the meaning of the symbols to degrees"


## mpmath
- sympy.mpmath.degrees(radians): Float degrees
- sympy.mpmath.radians(degrees): Float radians


## Sage

Robert Kern

unread,
Jun 8, 2018, 11:44:22 PM6/8/18
to python...@python.org
On 6/8/18 01:45, Robert Vanden Eynde wrote:
> - Thanks for pointing out a language (Julia) that already had a name convention.
> Interestingly they don't have a atan2d function. Choosing the same convention as
> another language is a big plus.

For what it's worth, scipy calls them sindg, cosdg, tandg, cotdg.

https://docs.scipy.org/doc/scipy-1.1.0/reference/special.html

--
Robert Kern

"I have come to believe that the whole world is an enigma, a harmless enigma
that is made terrible by our own mad attempt to interpret it as though it had
an underlying truth."
-- Umberto Eco

Wes Turner

unread,
Jun 8, 2018, 11:54:58 PM6/8/18
to rober...@gmail.com, Python-Ideas
On Fri, Jun 8, 2018 at 11:44 PM Robert Kern <rober...@gmail.com> wrote:
On 6/8/18 01:45, Robert Vanden Eynde wrote:
> - Thanks for pointing out a language (Julia) that already had a name convention.
> Interestingly they don't have a atan2d function. Choosing the same convention as
> another language is a big plus.

For what it's worth, scipy calls them sindg, cosdg, tandg, cotdg.

https://docs.scipy.org/doc/scipy-1.1.0/reference/special.html

Adam Bartoš

unread,
Jun 9, 2018, 5:23:34 AM6/9/18
to python...@python.org
Steven D'Arpano wrote:

> On Fri, Jun 08, 2018 at 11:11:09PM +0200, Adam Bartoš wrote: > >> But if there are both sin and dsin, and you ask about the difference >> between them, the obvious answer would be that one takes radians and the >> other takes degrees. The point that the degrees version is additionally >> exact on special values is an extra benefit. > > No, that's not an extra benefit, it is the only benefit! > > If we can't make it exact for the obvious degree angles, there would be > no point in doing this. We'd just tell people to write their own > two-line functions: > > def sindeg(angle): > return math.sin(math.radians(angle)) > > > The only reason to even consider making this a standard library function > is if we can do better than that.

I agree completely, I just think it doesn't look obvious. 

>> It would be nice to also fix the original sin, > > The sin function is not broken and does not need fixing. > > (Modulo quirks of individual platform maths libraries.) > > >> or more precisely to provide a way to give it a >> fractional multiple of pi. How about a special class PiMultiple that would >> represent a fractional multiple of pi?
 >
> What is the point of that? When you pass it to math.sin, it still needs 
> to be converted to a float before sin can operate on it.
> 
> Unless you are proposing a series of dunder methods __sin__ __cos__ and 
> __tan__ to allow arbitrary classes to be passed to sin, cos and tan, the 
> following cannot work.

The idea was that the functions could handle the PiMultiple instances in a special way and fall back to float only when a special value is not detected. It would be like the proposed dsin functionality, but with a magic class instead of a new set of functions, and without a particular choice of granularity (360 degrees).
But maybe it isn't worth it. Also what about acos(0)? Should it return PiMultiple(1, 2) and confuse people or just 1.5707963267948966 and loose exactness?

Best regards,
Adam Bartoš

Michael Selik

unread,
Jun 9, 2018, 10:47:48 AM6/9/18
to Adam Bartoš, python...@python.org
On Sat, Jun 9, 2018 at 2:22 AM Adam Bartoš <dre...@gmail.com> wrote:
The idea was that the functions could handle the PiMultiple instances in a special way and fall back to float only when a special value is not detected. It would be like the proposed dsin functionality, but with a magic class instead of a new set of functions, and without a particular choice of granularity (360 degrees).
But maybe it isn't worth it. Also what about acos(0)? Should it return PiMultiple(1, 2) and confuse people or just 1.5707963267948966 and loose exactness?
That'd be the only module in the standard library with such a specialized class and behavior. You could argue that pathlib creates a sort of Path preference with str fallback, but the Path type has a large collection of methods. In contrast, this PiMultiple type would be only used as an input. That's very unusual style for Python.

Robert Vanden Eynde

unread,
Jun 9, 2018, 2:55:12 PM6/9/18
to python-ideas
Indeed what we need for exact math for multiple of 90 (and 30) is ideas from the symbolic libraries (sympy, sage).

Of course the symbolic lib can do more like :

sage: k = var('k', domain='integer')
sage: cos(1 + 2*k*pi)
cos(1)
sage: cos(k*pi)
cos(pi*k)
sage: cos(pi/3 + 2*k*pi)
1/2

But that would concern symbolic lib only I think.

For the naming convention, scipy using sindg (therefore Nor sind nor sindeg) will make the sind choice less obvious. However if Matlab and Julia chooses sind that's a good path to go, Matlab is pretty popular, as other pointed out, with Universities giving "free" licences and stuff. With that regards, scipy wanting to "be a replacement to Matlab in python and open source" it's interesting they chose sindg and not the Matlab name sind.

For the "d" as suffix that would mean "d" as "double" like in opengl. Well, let's remember that in Python there's only One floating type, that's a double, and it's called float... So python programmers will not think "sind means it uses a python float and not a python float32 that C99 sinf would". Python programmers would be like "sin takes float in radians, sind takes float in degrees or int, because int can be converted to float when there's no overflow".

Neil Girdhar

unread,
Jun 10, 2018, 6:50:45 AM6/10/18
to python-ideas
I think this suggestion should result in a library on PyPi, which can then be considered for the standard library if it sees a lot of use.

Also, modern OpenGL does this just like Python does: all of the trigonometric functions take radians and a "radians" function is provided.

Best,

Neil

Stephan Houben

unread,
Jun 10, 2018, 10:45:11 AM6/10/18
to Robert Vanden Eynde, python-ideas
2018-06-09 8:18 GMT+02:00 Robert Vanden Eynde <robertva...@hotmail.com>:
For the naming convention, scipy using sindg (therefore Nor sind nor sindeg) will make the sind choice less obvious. However if Matlab and Julia chooses sind that's a good path to go, Matlab is pretty popular, as other pointed out, with Universities giving "free" licences and stuff. With that regards, scipy wanting to "be a replacement to Matlab in python and open source" it's interesting they chose sindg and not the Matlab name sind.


I would suggest that compatibility with a major Python library such as SciPy is more important than compatibility
with other programming languages.

I would go even further and argue that scipy.special.sindg and its friends cosdg and tandg
can serve as the reference implementation for this proposal.

Stephan

Robert Vanden Eynde

unread,
Jun 10, 2018, 4:00:10 PM6/10/18
to python-ideas
I agree that a big python library is more close to the standard python lib than matlab. However, helping transition from matlab is a great concern in the python scientific community, because matlab is used is a lot of engineering classes at University.

That's a tough call hmmm.

I'll look at the implementation of scipy.special.sindg and friends to see if/how they have optimisations for exact values.

Terry Reedy

unread,
Jun 10, 2018, 5:41:54 PM6/10/18
to python...@python.org
On 6/10/2018 10:44 AM, Stephan Houben wrote:

> I would suggest that compatibility with a major Python library such as
> SciPy is more important than compatibility
> with other programming languages.
>
> I would go even further and argue that scipy.special.sindg and its
> friends cosdg and tandg
> can serve as the reference implementation for this proposal.

Or we could decide that we don't need to duplicate scipy.


--
Terry Jan Reedy

Neil Girdhar

unread,
Jun 10, 2018, 7:54:04 PM6/10/18
to python...@googlegroups.com, python...@python.org
On Sun, Jun 10, 2018 at 5:41 PM Terry Reedy <tjr...@udel.edu> wrote:
On 6/10/2018 10:44 AM, Stephan Houben wrote:

> I would suggest that compatibility with a major Python library such as
> SciPy is more important than compatibility
> with other programming languages.
>
> I would go even further and argue that scipy.special.sindg and its
> friends cosdg and tandg
> can serve as the reference implementation for this proposal.

Or we could decide that we don't need to duplicate scipy.


Copying scipy's and numpy's interface makes vectorizing code a lot simpler.  tensorflow also initially made the mistake of varying from numpy's interface only to then deprecate the variations and adopt the standard.
 

--
Terry Jan Reedy

_______________________________________________
Python-ideas mailing list
Python...@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

--

---
You received this message because you are subscribed to a topic in the Google Groups "python-ideas" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/python-ideas/-NauPA0ZckE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to python-ideas...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Terry Reedy

unread,
Jun 11, 2018, 12:11:20 AM6/11/18
to python...@python.org
On 6/10/2018 7:53 PM, Neil Girdhar wrote:
>
>
> On Sun, Jun 10, 2018 at 5:41 PM Terry Reedy
> <tjr...@udel.edu
> <mailto:tjr...@udel.edu>> wrote:
>
> On 6/10/2018 10:44 AM, Stephan Houben wrote:
>
> > I would suggest that compatibility with a major Python library
> such as
> > SciPy is more important than compatibility
> > with other programming languages.
> >
> > I would go even further and argue that scipy.special.sindg and its
> > friends cosdg and tandg
> > can serve as the reference implementation for this proposal.
>
> Or we could decide that we don't need to duplicate scipy.
>
>
> Copying scipy's and numpy's interface makes vectorizing code a lot
> simpler.  tensorflow also initially made the mistake of varying from
> numpy's interface only to then deprecate the variations and adopt the
> standard.

What I meant is to not add functions that already exist in scipy. Core
devs do not need the burden of keeping up with whatever improvements are
made to the scipy functions.

Chris Barker via Python-ideas

unread,
Jun 11, 2018, 1:09:51 AM6/11/18
to Robert Vanden Eynde, python-ideas
On Sun, Jun 10, 2018 at 11:26 AM, Robert Vanden Eynde <robertva...@hotmail.com> wrote:
I agree that a big python library is more close to the standard python lib than matlab. However, helping transition from matlab is a great concern in the python scientific community, because matlab is used is a lot of engineering classes at University.

That's a tough call hmmm.

not really -- if you are moving from matlab to python, you are going to be using numpy and scipy -- we really don't need to spell similar functionality differently than scipy does.

In regard to the "special values", and exact results -- a good math lib should return results that are "exact" in all but maybe the last digit stored. So you could check inputs and outputs with, e.g. math.isclose() to give people the "exact" results. -- and keep it all in floating point.

-CHB


--

Christopher Barker, Ph.D.
Oceanographer

Emergency Response Division
NOAA/NOS/OR&R            (206) 526-6959   voice
7600 Sand Point Way NE   (206) 526-6329   fax
Seattle, WA  98115       (206) 526-6317   main reception

Chris....@noaa.gov

Steven D'Aprano

unread,
Jun 11, 2018, 1:49:39 AM6/11/18
to Chris Barker via Python-ideas
On Sun, Jun 10, 2018 at 10:01:09PM -0700, Chris Barker via Python-ideas wrote:

> In regard to the "special values", and exact results -- a good math lib
> should return results that are "exact" in all but maybe the last digit
> stored. So you could check inputs and outputs with, e.g. math.isclose() to
> give people the "exact" results. -- and keep it all in floating point.

I wish Uncle Timmy or Mark Dickinson were around to give a definite
answer, but in their absence I'll have a go. I'm reasonably sure
that's wrong.

The problem with trig functions is that they suffer from "the
table maker's dilemma", so it is very hard to guarantee a correctly
rounded result without going to ludicrous extremes:

http://perso.ens-lyon.fr/jean-michel.muller/Intro-to-TMD.htm

So I think that there's no guarantee given for trancendental functions
like sine, cosine etc.

But even if they were, using isclose() is the wrong solution. Suppose
sin(x) returns some number y, such that isclose(y, 0.0) say. You have no
way of knowing that y is an inaccurate result that ought to be zero, or
whether the answer should be non-zero and y is correct. You cannot
assume that "y is close to zero, therefore it ought to be zero".

It's not just zero, the same applies for any value. That's just moving
rounding errors from one input to a slightly different input.

# current situation
sine of x returns y, but the mathematical exact result is exactly z

# suggested "fix"
sine of x ± a tiny bit returns exactly z, but ought to return y

Guessing what sin or cos "ought to" return based on either the inexact
input or inexact output is not a good approach.

Remember, because π is irrational, we cannot actually call sin or cos on
any rational multiple of π. We can only operate on multiples of pi,
which is *close to* but not the same as π. That's why it is okay that
tan(pi/2) returns a huge number instead of infinity or NAN. That's
because the input is every so slightly smaller than π/2. That's exactly
the behaviour you want when x is ever so slightly smaller than π/2.

Jacco van Dorp

unread,
Jun 11, 2018, 3:19:54 AM6/11/18
to Chris Barker via Python-ideas
> Remember, because π is irrational, we cannot actually call sin or cos on
> any rational multiple of π. We can only operate on multiples of pi,
> which is *close to* but not the same as π. That's why it is okay that
> tan(pi/2) returns a huge number instead of infinity or NAN. That's
> because the input is every so slightly smaller than π/2. That's exactly
> the behaviour you want when x is ever so slightly smaller than π/2.


This would basically be the reason for a PiMultiple class - you can
special case it. You'd know sin(PiMultiple(0.5)) == 0. You'd know
cos(PiMultiple(0.5)) == -1 and tan(PiMultiple(0.5)) == nan. This could
let you remember as much angles as possible into multiples of pi, and
as long as you're in multiples of pi, you're exact.
PiMultiple(Fraction(1, 6)) would be exact and could give the right
sin() and cos() behaviour.

And because it'd be a numeric type, you could still use it with all
other numeric types and add/multiply etc it. When you add and subtract
it with another numeric type, it'd lose the special status, but even
with multiples and divisions you can preserve it's specialness.

And if it gets weird values, you can always fall back on converting it
to a float, therefore never giving worse results.

It also gives a reason -against- degrees. if you have PiMultiple or
TauMultiple, it's rather easy to give common angles, and students can
learn to properly learn radians for angles as they should.(because,
lets be honest, they're objectively better measures of angles than
degrees, or even *shiver* grads. )

We SHOULD make it easy to code exact and the right way, and I think a
PiMultiple class could help that a lot. That said, it does need a
better name.

Ronald Oussoren

unread,
Jun 11, 2018, 4:01:40 AM6/11/18
to Jacco van Dorp, Chris Barker via Python-ideas

What is the real world advantage of such a class? So far I’ve only seen examples where the current behavior is said to be confusing for students. In most cases where I have used math.sin the angle wasn’t a constant and wasn’t an exact mulltiple of pi.

Ronald

Jacco van Dorp

unread,
Jun 11, 2018, 6:09:06 AM6/11/18
to Ronald Oussoren, Chris Barker via Python-ideas
2018-06-11 10:00 GMT+02:00 Ronald Oussoren <ronaldo...@mac.com>:
>> [me suggestion PiMultiple class]

>
> What is the real world advantage of such a class? So far I’ve only seen examples where the current behavior is said to be confusing for students. In most cases where I have used math.sin the angle wasn’t a constant and wasn’t an exact mulltiple of pi.
>
> Ronald

Im assuming the current math.pi would be converted to PiMultiple(1)

When learning, it's rather easy to write and read the following:

>>> from math import sin, pi, asin, cos

>>> myangle = pi / 2
>>> sin(myangle)
1
>>> asin(1)
"0.5π" # Currently: 1.5707963267948966
>>> cos(pi / 2)
0 # Currently: 6.123233995736766e-17

It helps clarity and understanding when you're coming to python from a
math background. In universities, half of your values when working
with angles are written as some multiple of pi (of some weird fraction
involving it). Also, for the common angles, we'll gain accuracy (and
perhaps we can avoid the infinite series for certain computations.
That would be a win.).

Also, you're countering the "confusing to students" part with your own
production environment experience. Those aren't alike. And if this was
done, your float-based production code wouldn't get slower or care any
other way. What you implement and test with PiMultiple would work
perfectly fine with any random float as well, just without the extra
advantages.

Steven D'Aprano

unread,
Jun 11, 2018, 6:51:37 AM6/11/18
to python...@python.org
On Mon, Jun 11, 2018 at 09:18:22AM +0200, Jacco van Dorp wrote:
> > Remember, because π is irrational, we cannot actually call sin or cos on
> > any rational multiple of π. We can only operate on multiples of pi,
> > which is *close to* but not the same as π. That's why it is okay that
> > tan(pi/2) returns a huge number instead of infinity or NAN. That's
> > because the input is every so slightly smaller than π/2. That's exactly
> > the behaviour you want when x is ever so slightly smaller than π/2.
>
>
> This would basically be the reason for a PiMultiple class - you can
> special case it. You'd know sin(PiMultiple(0.5)) == 0.

Not when I went to school it wasn't. sin(π/2) = sin(90°) = 1.

Perhaps you meant cos?

In any case, the sin function doesn't work that way. Unless we add a
special dunder method to defer to, it cannot be expected to magically
know how to deal with these "PiMultiple" objects, except by converting
them to floats.

You don't suddenly get accurate results by waving a magic wand over the
float 0.5 and saying "You're a multiple of pi". You still have to code a
separate algorithm for this, and that's hard work. (Why do you think the
decimal module doesn't support trig functions?)

Is every function that takes float arguments now supposed to recognise
PiMultiple objects and treat them specially? How do they integrate in
the numeric tower and interact with ordinary floats?

But let's say you do this:

def sin(arg):
if isinstance(arg, PiMultiple):
# does this mean it needs to be a builtin class?
call internal sinpi(arg) function
else:
call regular sin(arg) function

This isn't Java, and not everything needs to be a class. If we go to the
trouble of writing separate sinpi() etc implementations, why hide one of
them behind a class (and not even in a proper object-oriented interface)
when we can just call the functions directly?

sinpi(1.5)

sin(PiMultiple(1.5))

I know which I'd rather use.


> It also gives a reason -against- degrees. if you have PiMultiple or
> TauMultiple, it's rather easy to give common angles,

What about the uncommon angles?

The whole point of this proposal is to make it easy to give angles in
degrees without the need to convert to radians, introducing rounding
errors over and above those introduced by the trig function itself.


> and students can
> learn to properly learn radians for angles as they should.(because,
> lets be honest, they're objectively better measures of angles than
> degrees, or even *shiver* grads. )

No they are not objectively better measures of angles. With radians, you
have to divide a right angle into an irrational number of radians, one
which cannot be expressed in a finite number of decimal places.

In a very real sense, it is impossible to measure exactly 1 radian.

I know that in practical terms, this makes no difference, we can get
close enough, but physical measurements are limited to rational numbers.
A measurement system based on irrational numbers, especially one as
difficult as π, is not objectively better.

Its not just because of tradition that nobody uses radians in civil
engineering, astronomy, architecture, etc. Radians shine when we're
doing pure maths and some branches of physics, but they're a PITA to use
in most practical circumstances.

E,g, the tip of your little finger at arms length is close enough to 1°
or 0.017 radian. Who wants to measure angles in multiples of 0.017?

https://www.timeanddate.com/astronomy/measuring-the-sky-by-hand.html

Using radians, these heuristics stink.


--
Steve

Steven D'Aprano

unread,
Jun 11, 2018, 7:43:28 AM6/11/18
to python...@python.org
On Mon, Jun 11, 2018 at 12:08:07PM +0200, Jacco van Dorp wrote:

> >>> asin(1)
> "0.5π" # Currently: 1.5707963267948966

I think that if you expect the stdlib math library to change to symbolic
maths for trig functions, you are going to be extremely disappointed.

Aside from everything else, this is such a massive backward-
compatibility break that it probably wouldn't even have been allowed in
Python 3.0, let alone in 3.8.


> It helps clarity and understanding when you're coming to python from a
> math background.

What about the other 95% of Python programmers who don't have a maths
background and don't give two hoots about the mathematical elegance of
being able to differentiate sin(x) without a multiplicative factor?


--
Steve

Chris Barker via Python-ideas

unread,
Jun 11, 2018, 1:06:58 PM6/11/18
to Ronald Oussoren, Chris Barker via Python-ideas
On Mon, Jun 11, 2018 at 1:00 AM, Ronald Oussoren <ronaldo...@mac.com> wrote:

What is the real world advantage of such a class?  So far I’ve only seen examples where the current behavior is said to be confusing for students. 

EXACTLY!

In [49]: math.sin(math.pi)

Out[49]: 1.2246467991473532e-16


If the difference between 1.2246467991473532e-16 and zero is important to you, you've got bigger issues to deal with, and you'd better have a decent grasp of floating point computation's limitations.

This is not that different than using Decimal, because it is confusing or aesthetically unpleasing to get something other than 1 (for example) when you add 0.1 up ten times:

In [25]: x = 0.0


In [26]: for i in range(10): x += 0.1


In [27]: x

Out[27]: 0.9999999999999999


But:

In [28]: 1.0 - x

Out[28]: 1.1102230246251565e-16


i.e.  x is  within one decimal unit in the last place stored by a float to 1.0.

Which is to say -- there is no practical difference within the abilities of floating point, and Decimal, while it would present this particular result exactly, isn't any more "accurate" in general (unless you use more precision, which is a result of variable precision, not decimal arithmetic per se)

So -- If there is a nifty way to specify that I want, say, the sin of "exactly pi", then the code could special case that, and return exactly zero. But what if you happen to pass in a value just a tiny bit larger than pi? then you have a potential discontinuity at pi, because you'd still have to use the regular FP computation for any non-exact multiple of pi.

All this means that you will get a very similar result by rounding your outputs to a digit less than full FP precision:

In [46]: math.sin(math.pi)

Out[46]: 1.2246467991473532e-16


In [47]: round(math.sin(math.pi), 15)

Out[47]: 0.0


But anyway:

There *may* be a nice use-case for a more "friendly" trig package, particularly in education, but I don't think it would ever belong in the std lib. (though maybe I'd change my mind if it saw really wide use)


However simiply adding a few names like:

sindeg, cosdeg, etc,


to save folks from having to type:

math.sin(math.degree(something))

is a fine idea -- and it may make it a bit more clear, when people go looking for the "Sine" function, that they don't want to use degrees with the regular one...

-CHB

Michael Selik

unread,
Jun 11, 2018, 1:27:47 PM6/11/18
to Chris Barker, Chris Barker via Python-ideas
Would sind and cosd make Euler's formula work correctly?

sind(x) + i * sind(x) == math.e ** (i * x)

I suspect that adding these functions is kind of like those cartoons where the boat is springing leaks and the character tried to plug them with their fingers. Floating point is a leaky abstraction.

Perhaps you'd prefer an enhancement to the fractions module that provides real (not float) math?

Michael Selik

unread,
Jun 11, 2018, 1:35:14 PM6/11/18
to Chris Barker, Chris Barker via Python-ideas
Whoops, it turns out Euler's formula does work! I expected imprecision, but at least one test matched.

x = 42
cos(x) + 1j * sin(x) == e ** (1j * x)

I suppose that's because it's radians.

Stephan Houben

unread,
Jun 11, 2018, 2:05:32 PM6/11/18
to Michael Selik, Chris Barker via Python-ideas
2018-06-11 19:33 GMT+02:00 Michael Selik <mi...@selik.org>:
Whoops, it turns out Euler's formula does work! I expected imprecision, but at least one test matched.

x = 42
cos(x) + 1j * sin(x) == e ** (1j * x)

I think you will find it holds for any x (except inf, -inf and nan).
The boat is less leaky than you think; IEEE floating-point arithmetic goes
out of its way to produce exact answers whenever possible.
(To great consternation of hardware designers who felt that
requiring 1.0*x == x was too expensive.)
 
I suppose that's because it's radians.

Well, the formula obviously only holds in exact arithmetic
if cos and sin are the versions taking radians.

Stephan
 


On Mon, Jun 11, 2018, 10:24 AM Michael Selik <mi...@selik.org> wrote:
Would sind and cosd make Euler's formula work correctly?

sind(x) + i * sind(x) == math.e ** (i * x)

I suspect that adding these functions is kind of like those cartoons where the boat is springing leaks and the character tried to plug them with their fingers. Floating point is a leaky abstraction.

Perhaps you'd prefer an enhancement to the fractions module that provides real (not float) math?

Steven D'Aprano

unread,
Jun 11, 2018, 2:39:10 PM6/11/18
to python...@python.org
On Mon, Jun 11, 2018 at 10:24:42AM -0700, Michael Selik wrote:
> Would sind and cosd make Euler's formula work correctly?
>
> sind(x) + i * sind(x) == math.e ** (i * x)

No, using degrees makes Euler's identity *not* work correctly, unless
you add in a conversion factor from degrees to radians:

https://math.stackexchange.com/questions/1368049/eulers-identity-in-degrees

Euler's Identity works fine in radians:

py> from cmath import exp
py> exp(1j*math.pi)
(-1+1.2246063538223773e-16j)

which is close enough to -1 given the usual rounding issues with floats.
(Remember, math.pi is not π, but a number close to it. There is no way
to represent the irrational number π in less than an infinite amount of
memory without symbolic maths.)


[...]


> Perhaps you'd prefer an enhancement to the fractions module that provides
> real (not float) math?

I should think not. Niven's Theorem tells us that for rational angles
between 0° and 90° (that is, angles which can be represented as
fractions), there are only THREE for which sine (and cosine) are
themselves rational:

https://en.wikipedia.org/wiki/Niven's_theorem

Every value of sin(x) except for those three angles is an irrational
number, which means they cannot be represented exactly as fractions or

in a finite number of decimal places.

What that means is that if we tried to implement real (not float)
trigonometric functions on fractions, we'd need symbolic maths capable
of returning ever-more complicated expressions involving surds.

For example, the exact value of sin(7/2 °) involves a triple nested
square root:

1/2 sqrt(2 - sqrt(2 + sqrt(3)))

and that's one of the relatively pretty ones. sin(3°) is:

-1/2 (-1)^(29/60) ((-1)^(1/60) - 1) (1 + (-1)^(1/60))

http://www.wolframalpha.com/input/?i=exact+value+of+sine%2815%2F2+degrees%29
http://www.wolframalpha.com/input/?i=exact+value+of+sine%283+degrees%29

This proposal was supposed to *simplify* the trig functions for
non-mathematicians, not make them mind-bogglingly complicated.

--
Steve

Clément Pit-Claudel

unread,
Jun 11, 2018, 3:59:25 PM6/11/18
to Stephan Houben, Michael Selik, Chris Barker via Python-ideas
On 2018-06-11 14:04, Stephan Houben wrote:
> 2018-06-11 19:33 GMT+02:00 Michael Selik <mi...@selik.org <mailto:mi...@selik.org>>:

>
> Whoops, it turns out Euler's formula does work! I expected imprecision, but at least one test matched.
>
> x = 42
> cos(x) + 1j * sin(x) == e ** (1j * x)
>
>
> I think you will find it holds for any x (except inf, -inf and nan).
> The boat is less leaky than you think; IEEE floating-point arithmetic goes
> out of its way to produce exact answers whenever possible.
> (To great consternation of hardware designers who felt that
> requiring 1.0*x == x was too expensive.)

In fact, 1.0*x == x is almost all that this test exercises. If I'm looking in the right place, this is C the implementation of a ** b, omitting in a few special cases:

vabs = hypot(a.real,a.imag);
len = pow(vabs,b.real);
at = atan2(a.imag, a.real);
phase = at*b.real;
if (b.imag != 0.0) {
len /= exp(at*b.imag);
phase += b.imag*log(vabs);
}
r.real = len*cos(phase);
r.imag = len*sin(phase);

This means that (e ** ...) is essentially implemented in terms of the formula above. Indeed, in the special case of e ** (1j * x), we have a.real = e, a.imag = 0.0, b.real = 0.0, and b.imag = 1.0, so concretely the code simplifies to this:

vabs = e
len = 1.0
at = 0.0
phase = 0.0
if (b.imag != 0.0) {
len = 1.0;
phase = x; // requires log(e) == 1.0 and x * 1.0 == x
}
r.real = cos(phase); // requires 1.0 * x == x
r.imag = sin(phase);

Thus, it shouldn't be too surprising that the formula holds :)

Clément.

Chris Barker via Python-ideas

unread,
Jun 11, 2018, 4:45:10 PM6/11/18
to Michael Selik, Chris Barker via Python-ideas
On Mon, Jun 11, 2018 at 10:24 AM, Michael Selik <mi...@selik.org> wrote:
Would sind and cosd make Euler's formula work correctly?

Not trying to pick on you, but this question shows a key misunderstanding:

There is nothing inherently more accurate in using degrees rather than radians for trigonometry. IT's nice that handy values like "one quarter of a circle" can be exactly represented, but that's really only an asthetic thing.

And every computer math lib I've even seen uses floating point radians for trig functions, so unless you're really going to implement trig from degrees from scratch, then you are going to go to floating point radians  (and floating point pi) anyway.

Oh, and radians are the more "natural" units (in fact unitless) for math, and the only way that things like the Euler identity work. Which is why computational math libs use them.

So there are two orthogonal ideas on the table here:

1) Have trig functions that take degrees for convenience for when folks are working in degrees already.

2) Have trig functions that produce exact values (i.e what is "expected") for the special cases.

It seems the OP is interested in a package that combines both of these -- which is a fine idea as a third party lib.

Perhaps you'd prefer an enhancement to the fractions module that provides real (not float) math?

Isn't that exactly what the fractions module does? or are you suggesting that it be extended with trig functions ?

Michael Selik

unread,
Jun 11, 2018, 4:59:31 PM6/11/18
to Chris Barker, Chris Barker via Python-ideas
On Mon, Jun 11, 2018, 1:18 PM Chris Barker <chris....@noaa.gov> wrote:
On Mon, Jun 11, 2018 at 10:24 AM, Michael Selik <mi...@selik.org> wrote:
Would sind and cosd make Euler's formula work correctly?

There is nothing inherently more accurate in using degrees rather than radians for trigonometry.

That's actually what I was trying to say. Shouldn't have tried to be round-about. Not only did the point get muddled, but I wrote something false as well!

Perhaps you'd prefer an enhancement to the fractions module that provides real (not float) math?

Isn't that exactly what the fractions module does? or are you suggesting that it be extended with trig functions?

The latter. However, to Steven's point about irrationals, perhaps this should be an entirely separate module designed to handle various irrationalities accurately. ... Like SymPy.

Chris Barker via Python-ideas

unread,
Jun 11, 2018, 5:13:56 PM6/11/18
to Steven D'Aprano, Chris Barker via Python-ideas
On Sun, Jun 10, 2018 at 10:48 PM, Steven D'Aprano <st...@pearwood.info> wrote:

> In regard to the "special values", and exact results -- a good math lib
> should return results that are "exact" in all but maybe the last digit
> stored. So you could check inputs and outputs with, e.g. math.isclose() to
> give people the "exact" results. -- and keep it all in floating point.

I wish Uncle Timmy or Mark Dickinson were around to give a definite
answer, but in their absence I'll have a go. I'm reasonably sure
that's wrong.

hmm -- I'm no numerical analyst, but I could have sworn I learned (from Kahan himself) that the trig functions could (and were, at least in the HP calculators :-) ) be computed to one digit of accuracy. He even proved how many digits of pi you'd have to store to do that (though I can't say I understood the proof) -- I think you needed all those digits of pi because the trig functions are defined on the range 0 -- pi/2, and any larger value needs to be mapped to that domain -- if someone asks for the sin(e100), you need to know pretty exactly what x % pi/4 is. 

The problem with trig functions is that they suffer from "the
table maker's dilemma", so it is very hard to guarantee a correctly
rounded result without going to ludicrous extremes:

http://perso.ens-lyon.fr/jean-michel.muller/Intro-to-TMD.htm

So I think that there's no guarantee given for trancendental functions
like sine, cosine etc.

so -- if that's the case, I still think we know that while the last digit may not be the best rounded value to the real one, the second to last digit is correct. And if not, then there's nothing we could do other than implement the math lib :-)

But even if they were, using isclose() is the wrong solution. Suppose
sin(x) returns some number y, such that isclose(y, 0.0) say. You have no
way of knowing that y is an inaccurate result that ought to be zero, or
whether the answer should be non-zero and y is correct. You cannot
assume that "y is close to zero, therefore it ought to be zero".

no, but you can say "y is as close to zero as I care about" we are already restricted to not knowing the distinction within less than an eps -- so making the "effective eps" a bit larger would result in more esthetically pleasing results.

It's not just zero, the same applies for any value. That's just moving
rounding errors from one input to a slightly different input.

# current situation
sine of x returns y, but the mathematical exact result is exactly z

# suggested "fix"
sine of x ± a tiny bit returns exactly z, but ought to return y

Guessing what sin or cos "ought to" return based on either the inexact
input or inexact output is not a good approach.

I don't think that's what it would be -- rather, it would be returning a bit less precision in exchange for more esthetically pleasing results :-)

Note that there is no way I would advocate using this for the stdlib trig functions -- only for a purpose library.

I'm also suggesting that it would result in equally good results to what is being proposed: using integer degrees, or a pi or tau based units. 

If you used integer degrees, then you'd have exactly, say pi (180 degrees), but a precision of only pi/180 -- much less than the 15 digits or so you'd get if you rounded the regular floating point results.

And if you used tau based units, you'd be back to the same thing -- 0.5 tau could be exact, but what would you do for a bit bigger or smaller than that? use FP :-)
 
We can only operate on multiples of pi,
which is *close to* but not the same as π. That's why it is okay that
tan(pi/2) returns a huge number instead of infinity or NAN. That's
because the input is every so slightly smaller than π/2. That's exactly
the behavior you want when x is ever so slightly smaller than π/2.

I suppose so, but is there a guarantee that the FP representation of π/2 is a tiny bit less than the exact value, rather than a tiny bit more? which would result in VERY different answers:

In [10]: math.tan(math.pi / 2.0)

Out[10]: 1.633123935319537e+16


In [11]: math.tan(math.pi / 2.0 + 2e-16)

Out[11]: -6218431163823738.0


(though equally "correct")

Also -- 1.6 e+16 is actually pretty darn small compared to FP range.

So a library that wants to produce "expected" results may want to do something with that -- something like:

In [119]: def pretty_tan(x):

     ...:     tol = 3e-16

     ...:     diff = x % (pi / 2)

     ...:     if abs(diff) < tol:

     ...:         return float("-Inf")          

     ...:     elif (pi /2) - diff < tol:

     ...:         return float("Inf")

     ...:     return math.tan(x)

     ...: 

     ...:     


In [120]: x = pi / 2 - 5e-16


In [121]: for i in range(10):

     ...:     val = x + i * 1e-16

     ...:     print val, pretty_tan(val)

     ...:     

1.57079632679 1.9789379661e+15

1.57079632679 1.9789379661e+15

1.57079632679 inf

1.57079632679 inf

1.57079632679 -inf

1.57079632679 -inf

1.57079632679 -inf

1.57079632679 -inf

1.57079632679 -2.61194216074e+15

1.57079632679 -2.61194216074e+15


You'd want to tweak that tolerance value to be as small as possible, and do somethign to make it more symmetric, but you get the idea.

The goal is that if you have an input that is about as close as you can get to pi/2, you get inf or -inf as a result.

This does mean you are tossing away a tiny bit of precision -- you could get a "correct" value for those values really close to pi/2, but it would be prettier...

-CHB












 


--
Steve
_______________________________________________
Python-ideas mailing list
Python...@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/



Greg Ewing

unread,
Jun 11, 2018, 6:49:35 PM6/11/18
to Chris Barker via Python-ideas
Michael Selik wrote:
> Whoops, it turns out Euler's formula does work! I expected imprecision,
> but at least one test matched.

That might be because the implememtation of e ** x where x
is complex is using Euler's formula...

--
Greg

Greg Ewing

unread,
Jun 11, 2018, 6:58:22 PM6/11/18
to python...@python.org
Steven D'Aprano wrote:
> sin(3°) is:
>
> -1/2 (-1)^(29/60) ((-1)^(1/60) - 1) (1 + (-1)^(1/60))
>
> This proposal was supposed to *simplify* the trig functions for
> non-mathematicians, not make them mind-bogglingly complicated.

I don't think anyone is going to complain about sin(3°) not
being exact, whatever units are being used. This discussion
is only about the rational values.

I wonder whether another solution would be to provide a
set of "newbie math" functions that round their results.

>>> round(cos(pi/2), 15)
0.0
>>> round(sin(pi/6), 15)
0.5

Yes, I know, this just pushes the surprises somewhere
else, but so does every solution.

--
Greg

Steven D'Aprano

unread,
Jun 11, 2018, 8:49:38 PM6/11/18
to python...@python.org
On Tue, Jun 12, 2018 at 10:57:18AM +1200, Greg Ewing wrote:
> Steven D'Aprano wrote:
> >sin(3°) is:
> >
> >-1/2 (-1)^(29/60) ((-1)^(1/60) - 1) (1 + (-1)^(1/60))
> >
> >This proposal was supposed to *simplify* the trig functions for
> >non-mathematicians, not make them mind-bogglingly complicated.
>
> I don't think anyone is going to complain about sin(3°) not
> being exact, whatever units are being used. This discussion
> is only about the rational values.

Precisely. They are the values I'm talking about.

If you're serious about only supporting only the rational values, then
the implementation is trivial and we can support both degrees and
radians easily.

def sin(angle): # radians
if angle == 0:
return 0
raise ValueError("sorry, no rational sine for that angle")

def sind(angle): # degrees
angle %= 360
rational_values = {
0: 0, 30: 0.5, 90: 1, 150: 0.5,
180: 0, 210: -0.5, 270: -1, 330: -0.5
}
if angle in rational_values:
return rational_values[angle]
raise ValueError("sorry, no rational sine for that angle")


Exceedingly simple, and exceedingly useless. Which was my point:
supporting only the rational values is pointless, because there are only
a handful of them. We either have to support full-blown symbolic
results, or we have rational APPROXIMATIONS to the true value.

I'm responding to a proposal that explicitly suggested using fractions
to do "real (not float) math", which I read as "no approximations". I
imagine that Michael thought that by using fractions, we can calculate
exact rational results for sine etc without floating point rounding
errors. No we cannot, except for the values above.

If "not float" is serious, then with the exception of the above values,
*none* of the values will be rational and we either can't return a value
(except for the above) or we have to use symbolic maths.

Using fractions is not a magic panacea that lets you calculate exact
answers just by swapping floats to fractions.


> I wonder whether another solution would be to provide a
> set of "newbie math" functions that round their results.

[...]


> Yes, I know, this just pushes the surprises somewhere
> else, but so does every solution.

No, that's really not true of every solution.

The initial proposal is fine: a separate set of trig functions that take
their arguments in degrees would have no unexpected surprises (only the
expected ones). With a decent implementation i.e. not this one:

# don't do this
def sind(angle):
return math.sin(math.radians(angle))

and equivalent for cos, we ought to be able to get correctly rounded
values for nearly all the "interesting" angles of the circle, without
those pesky rounding issues caused by π not being exactly representable
as a float.

There will still be rounding errors, because we're dealing with numbers
like sqrt(2) etc, but with IEEE-754 maths, they'll be correctly
rounded.


--
Steve

Steven D'Aprano

unread,
Jun 11, 2018, 9:10:25 PM6/11/18
to python...@python.org
On Mon, Jun 11, 2018 at 01:18:10PM -0700, Chris Barker via Python-ideas wrote:
> On Mon, Jun 11, 2018 at 10:24 AM, Michael Selik <mi...@selik.org> wrote:
>
> > Would sind and cosd make Euler's formula work correctly?
>
>
> Not trying to pick on you, but this question shows a key misunderstanding:
>
> There is nothing inherently more accurate in using degrees rather than
> radians for trigonometry.

Actually there is: using radians, the only "nice" angle that can be
represented exactly is 0. With degrees, we can represent a whole lot of
"nice" angles exactly. "Nice", is of course subjective, but most of us
would recognise that 36° represented as exactly 36.0 is nice but being
*approximately* represented as 0.6283185307179586 is not.

Using radians, we have two sources of rounding error:

- π cannot be represented exactly as a float, so we have to use a
number pi which is ever-so-slightly off;

- plus the usual round-off error in the algorithm;

while using degrees, we only have the second one (since 180° *can* be
represented exactly, as the float 180.0).

And with the degrees implementation, we should be able to use correctly
rounded roots for many of our "nice" angles.


> And every computer math lib I've even seen uses floating point radians for
> trig functions, so unless you're really going to implement trig from
> degrees from scratch

Well that's the whole point of the discussion.


> Oh, and radians are the more "natural" units (in fact unitless) for math,

Degrees are unit-less too. 180° = π radians. That's just a scaling
factor difference.

Unless you're doing symbolic maths, differentiating or integrating trig
functions, or certain geometric formulae which are "neater" in radians
than in degrees, there's no real advantage to radians.

Degrees are simply a much more practical unit of angle for practical
work.


> and the only way that things like the Euler identity work.

Its not the *only* way.

https://math.stackexchange.com/questions/1368049/eulers-identity-in-degrees


> Which is why computational math libs use them.

Actually, more maths libraries than you might guess offer trig functions
in degrees, or scaled by pi, e.g:

https://docs.oracle.com/cd/E19957-01/806-3568/ncg_lib.html

Julia and Matlab provide sind etc, and although I cannot find a
reference right now, I seem to recall the latest revision to the IEEE
754 standard suggesting them as optional functions.

--
Steve

Steven D'Aprano

unread,
Jun 11, 2018, 9:19:19 PM6/11/18
to python...@python.org
On Mon, Jun 11, 2018 at 02:12:06PM -0700, Chris Barker wrote:

> no, but you can say "y is as close to zero as I care about"

Of course you can.

But we (the std lib) should not make that decision for everybody. For
some people 0.001 is "close enough to zero". For others, 1e-16 is not.
We're not in the position to decide for everyone.

Tim Peters

unread,
Jun 12, 2018, 1:54:44 AM6/12/18
to Steven D'Aprano, Python-Ideas
[Steven D'Aprano]
... 
The initial proposal is fine: a separate set of trig functions that take
their arguments in degrees would have no unexpected surprises (only the
expected ones). With a decent implementation i.e. not this one:

    # don't do this
    def sind(angle):
        return math.sin(math.radians(angle))

But that's good enough for almost all real purposes.  Indeed, except for argument reduction, it's essentially how scipy's sindg and cosdg _are_ implemented:

 
and equivalent for cos, we ought to be able to get correctly rounded
values for nearly all the "interesting" angles of the circle, without
those pesky rounding issues caused by π not being exactly representable
as a float.

If people are overly ;-) worried about tiny rounding errors, just compute things with some extra bits of precision to absorb them.  For example, install `mpmath` and use this:

     def sindg(d):
        import math, mpmath
        d = math.fmod(d, 360.0)
        if abs(d) == 180.0:
            return 0.0
        with mpmath.extraprec(12):
            return float(mpmath.sin(mpmath.radians(d)))

Then, e.g,

>>> for x in (0, 30, 90, 150, 180, 210, 270, 330, 360):
...     print(x, sindg(x))
0 0.0
30 0.5
90 1.0
150 0.5
180 0.0
210 -0.5
270 -1.0
330 -0.5
360 0.0

Notes:

1. Python's float "%" is unsuitable for argument reduction; e.g.,

>>> -1e-14 % 360.0
360.0

`math.fmod` is suitable, because it's exact:

>>> math.fmod(-1e-14, 360.0)
-1e-14

2. Using a dozen extra bits of precision make it very likely you'll get the correctly rounded 53-bit result; it will almost certainly (barring bugs in `mpmath`) always be good to less than 1 ULP.

3. Except for +-180.  No matter how many bits of float precision (including the number of bits used to approximate pi) are used, converting that to radians can never yield the mathematical `pi`; and sin(pi+x) is approximately equal to -x for tiny |x|; e.g., here with a thousand bits:

>>> mpmath.mp.prec = 1000
>>> float(mpmath.sin(mpmath.radians(180)))
1.2515440597544546e-301

So +-180 is special-cased.  For cosdg, +-{90. 270} would need to be special-cased for the same reason.

Greg Ewing

unread,
Jun 12, 2018, 2:41:27 AM6/12/18
to Python-Ideas
Tim Peters wrote:

> 1. Python's float "%" is unsuitable for argument reduction; e.g.,
>
> >>> -1e-14 % 360.0
> 360.0
>
> `math.fmod` is suitable, because it's exact:
>
> >>> math.fmod(-1e-14, 360.0)
> -1e-14

So why doesn't float % use math.fmod?

--
Greg

Matt Arcidy

unread,
Jun 12, 2018, 2:46:26 AM6/12/18
to Tim Peters, python-ideas
Sorry for top posting, but these aren't really opinions for the
debate, just information. I haven't seen them mentioned, and none
grouped nicely under someone's reply.

Number representation::
IEEE-754 doubles cannot represent pi correctly at any bit-depth (i
mean, obviously, but more seriously).
input pi = 3.14159265358979323846264338327
output pi = 3.14159265358979311599796346854

The cutoff is the value everyone uses, which is what is in math.pi
among other places.
3.141592653589793

Any conversion using this value of pi is already wrong, and the input
to the function is already wrong. The function doesn't matter. It's
always underestimating if using pi. I'm clarifying this because it's
separate from any discussion of functions. More accurate or different
functions cannot use a better input pi.

Calculator: http://www.binaryconvert.com/convert_double.html
input pi source: https://www.piday.org/million/

Libraries:
Boost implements a cos_pi function, it's algorithm will probably be
useful to look at.

glibc implements cos/sin as a look-up table, which is most likely
where any other implementation will end up, as it is a common
endpoint. I've seen this on different architectures and libraries.
(sincostab.h if you want to google glibc's table). Maybe there is a
hilarious index lookup off-by-1 there, I didn't look.

Tables are the basis for the calculation in many libraries in march
architectures, so I wanted to point that out to any function
designers. A new implementation may come down to calculating right
index locations.
For a degree based implementation, the same algorithm can be used with
a different table input that is calibrated to degrees.
Likewise, a pi based implementation, the same but for the pi scale
factor input.
Nothing needs to be done designed other than a new lookup table
calibrated for the "base" of a degree, radian, or pi.. it will have
the same output precision issues calculating index values, but at
least the input will be cleaner.

This is not me saying what should be done, just giving information
that may hopefully be useful

Small note about python's math:
The python math library does not implement algorithms, it exposes the
c functions. You can see C/C++ has this exact issue performing the
calculation there. As that is "the spec," the spec is defined with
the error. The math library is technically correct given it's stated
purpose and result.
Technically correct, the best kind of correct.
https://www.youtube.com/watch?v=hou0lU8WMgo

Chris Angelico

unread,
Jun 12, 2018, 2:52:01 AM6/12/18
to Python-Ideas
On Tue, Jun 12, 2018 at 4:40 PM, Greg Ewing <greg....@canterbury.ac.nz> wrote:
> Tim Peters wrote:
>
>> 1. Python's float "%" is unsuitable for argument reduction; e.g.,
>>
>> >>> -1e-14 % 360.0
>> 360.0
>>
>> `math.fmod` is suitable, because it's exact:
>>
>> >>> math.fmod(-1e-14, 360.0)
>> -1e-14
>
>
> So why doesn't float % use math.fmod?

https://docs.python.org/3/reference/expressions.html#binary-arithmetic-operations
https://docs.python.org/3/reference/expressions.html#id17
https://docs.python.org/3/reference/expressions.html#id18

(the latter two being footnotes from the section in the first link)

With real numbers, divmod (and thus the // and % operators) would
always return values such that:

div, mod = divmod(x, y):
1) div*y + mod == x
2) sign(mod) == sign(y)
3) 0 <= abs(mod) < abs(y)

But with floats, you can't guarantee all three of these. The divmod
function focuses on the first, guaranteeing the fundamental arithmetic
equality, but to do so, it sometimes has to bend the third one and
return mod==y.

There are times when it's better to sacrifice one than the other, and
there are other times when it's the other way around. We get the two
options.

ChrisA

Stephan Houben

unread,
Jun 12, 2018, 3:03:50 AM6/12/18
to Chris Angelico, Python-Ideas
Hi all,

I wrote a possible implementation of sindg:


This code first reduces the angle to the [0,90] interval.
After doing so, it can be observed that the simple implementation
  math.sin(math.radians(angle))
produces exact results for 0 and 90, and a result already rounded to nearest for
60.

For 30 and 45, this simple implementation is one ulp too low.
So I special-case those to return the correct/correctly-rounded value instead.
Note that this does not affect monotonicity around those values.

So I am still unsure if this belong in the stdlib, but if so, this is how it could be done.

Stephan

Nathaniel Smith

unread,
Jun 12, 2018, 6:42:29 AM6/12/18
to Stephan Houben, Python-Ideas
On Tue, Jun 12, 2018, 00:03 Stephan Houben <steph...@gmail.com> wrote:
Hi all,

I wrote a possible implementation of sindg:


This code first reduces the angle to the [0,90] interval.
After doing so, it can be observed that the simple implementation
  math.sin(math.radians(angle))
produces exact results for 0 and 90, and a result already rounded to nearest for
60.

You observed this on your system, but math.sin uses the platform libm, which might do different things on other people's systems.


For 30 and 45, this simple implementation is one ulp too low.
So I special-case those to return the correct/correctly-rounded value instead.
Note that this does not affect monotonicity around those values.

Again, monotonicity is preserved on your system, but it might not be on others. It's not clear that this matters, but then it's not clear that any of this matters...

-n

Wes Turner

unread,
Jun 12, 2018, 8:28:56 AM6/12/18
to Robert Vanden Eynde, Python-Ideas
Sym: SymPy, SymEngine, PySym, SymCXX, Diofant

(re: \pi, symbolic computation and trigonometry
instead of surprisingly useful piecewise optimizations)

On Fri, Jun 8, 2018 at 10:09 PM Wes Turner <wes.t...@gmail.com> wrote:
# Python, NumPy, SymPy, mpmath, sage trigonometric functions

## Python math module
- degrees(radians): Float degrees
- radians(degrees): Float degrees

## NumPy
- degrees(radians) : List[float] degrees
- rad2deg(radians): List[float] degrees
- radians(degrees) : List[float] radians
- deg2rad(degrees): List[float] radians



# Symbolic computation
 
    > Let x, theta, phi, etc. be Symbols representing quantities in radians. Keep a list of these symbols: angles = [x, theta, phi]. Then, at the very end, use y.subs([(angle, angle*pi/180) for angle in angles]) to change the meaning of the symbols to degrees"





from diofant import symbols, pi
x,y,z,_pi = symbols('x y z _pi')
expr =  pi**x # TODO: see diofant/tests/test_wester.py#L511
expr.subs(x, 1e11)
print(operator.sub(
      expr.subs(pi, 3.14),
      expr.subs(pi, 3.14159265)))
assert expr.subs(pi, 3.14) != expr.subs(pi, 3.14159265)
print(expr.subs(pi, 3.14159).evalf(70))

- CAS capability tests:

> """ Tests from Michael Wester's 1999 paper "Review of CAS mathematical
> capabilities".
> See also http://math.unm.edu/~wester/cas_review.html for detailed output of
> each tested system.
"""

https://github.com/diofant/diofant/blob/79ae584e949a08/diofant/tests/test_wester.py#L511

# I. Trigonometry
@pytest.mark.xfail
def test_I1():
    assert tan(7*pi/10) == -sqrt(1 + 2/sqrt(5))
@pytest.mark.xfail
def test_I2():
    assert sqrt((1 + cos(6))/2) == -cos(3)
def test_I3():
    assert cos(n*pi) + sin((4*n - 1)*pi/2) == (-1)**n - 1
def test_I4():
    assert cos(pi*cos(n*pi)) + sin(pi/2*cos(n*pi)) == (-1)**n - 1
@pytest.mark.xfail
def test_I5():
    assert sin((n**5/5 + n**4/2 + n**3/3 - n/30) * pi) == 0
 
diofant.sin.eval() has a number of interesting conditionals in there:

The tests for diofant.functions.elementary.trigonometric likely have a number of helpful tests for implementing methods dealing with pi and trigonometric identities:
https://github.com/diofant/diofant/blob/master/diofant/functions/elementary/tests/test_trigonometric.py


## mpmath
- sympy.mpmath.degrees(radians): Float degrees
- sympy.mpmath.radians(degrees): Float radians


## Sage



On Friday, June 8, 2018, Robert Vanden Eynde <robertva...@hotmail.com> wrote:
- Thanks for pointing out a language (Julia) that already had a name convention. Interestingly they don't have a atan2d function. Choosing the same convention as another language is a big plus.

- Adding trig function using floats between 0 and 1 is nice, currently one needs to do sin(tau * t) which is not so bad (from math import tau, tau sounds like turn).

- Julia has sinpi for sin(pi*x), one could have sintau(x) for sin(tau*x) or sinturn(x).

Grads are in the idea of turns but with more problems, as you guys said, grads are used by noone, but turns are more useful. sin(tau * t) For The Win.

- Even though people mentionned 1/6 not being exact, so that advantage over radians isn't that obvious ?

from math import sin, tau
from fractions import Fraction
sin(Fraction(1,6) * tau)
sindeg(Fraction(1,6) * 360)

These already work today by the way.

- As you guys pointed out, using radians implies knowing a little bit about floating point arithmetic and its limitations. Integer are more simple and less error prone. Of course it's useful to know about floats but in many case it's not necessary to learn about it right away, young students just want their player in the game move in a straight line when angle = 90.

- sin(pi/2) == 1 but cos(pi/2) != 0 and sin(3*pi/2) != 1 so sin(pi/2) is kind of an exception.




Le ven. 8 juin 2018 à 09:11, Steven D'Aprano <st...@pearwood.info> a écrit :
On Fri, Jun 08, 2018 at 03:55:34PM +1000, Chris Angelico wrote:
> On Fri, Jun 8, 2018 at 3:45 PM, Steven D'Aprano <st...@pearwood.info> wrote:
> > Although personally I prefer the look of d as a prefix:
> >
> > dsin, dcos, dtan
> >
> > That's more obviously pronounced "d(egrees) sin" etc rather than "sined"
> > "tanned" etc.
>
> Having it as a suffix does have one advantage. The math module would
> need a hyperbolic sine function which accepts an argument in; and
> then, like Charles Napier [1], Python would finally be able to say "I
> have sindh".

Ha ha, nice pun, but no, the hyperbolic trig functions never take
arguments in degrees. Or radians for that matter. They are "hyperbolic
angles", which some electrical engineering text books refer to as
"hyperbolic radians", but all the maths text books I've seen don't call
them anything other than a real number. (Or sometimes a complex number.)

But for what it's worth, there is a correspondence of a sort between the
hyperbolic angle and circular angles. The circular angle going between 0
to 45° corresponds to the hyperbolic angle going from 0 to infinity.

https://en.wikipedia.org/wiki/Hyperbolic_angle

https://en.wikipedia.org/wiki/Hyperbolic_function


> [1] Apocryphally, alas.

Don't ruin a good story with facts ;-)



--
Steve

Tim Peters

unread,
Jun 12, 2018, 11:52:49 AM6/12/18
to Python-Ideas

[Tim]
>> 1. Python's float "%" is unsuitable for argument reduction; e.g.,
>>
>>  >>> -1e-14 % 360.0
>> 360.0
>>
>> `math.fmod` is suitable, because it's exact:
>>
>>  >>> math.fmod(-1e-14, 360.0)
>> -1e-14
 
[Greg Ewing]
> So why doesn't float % use math.fmod?

[Chris Angelico] 
https://docs.python.org/3/reference/expressions.html#binary-arithmetic-operations
https://docs.python.org/3/reference/expressions.html#id17
https://docs.python.org/3/reference/expressions.html#id18

(the latter two being footnotes from the section in the first link)

With real numbers, divmod (and thus the // and % operators) would
always return values such that:

div, mod = divmod(x, y):
1) div*y + mod == x
2) sign(mod) == sign(y)
3) 0 <= abs(mod) < abs(y)

But with floats, you can't guarantee all three of these. The divmod
function focuses on the first, guaranteeing the fundamental arithmetic
equality, but to do so, it sometimes has to bend the third one and
return mod==y.

It's more that #2 is viewed as fundamental (because that's most useful for positive integer y), and _given that_ sometimes results are fiddled to keep #1 approximately true, and strict inequality in #3 may be sacrificed.  For `fmod`, sign(mod) == sign(x) instead.

>>> -2 % 3
1
 >>> -2.0 % 3.0
1.0
>>> math.fmod(-2.0, 3.0)
-2.0

All mod functions, m(x, y), strive to return a result that's mathematically exactly equal to x-n*y (for some mathematical integer `n` that may not even be representable in the programming language).  `fmod()` is exact in that sense, but Python's floating "%" may not be. and no float scheme such that sign(m(x, y)) = sign(y) can be (see the original example at the top:  the only mathematical integer `n` such that the mathematical   -1e-14 - n*360.0 is exactly representable as a double is n==0).

The most useful mod function for floats _as floats_ would actually satisfy

    abs(m(x, y)) <= abs(y) / 2

That can be done exactly too - but then the sign of the result has approximately nothing to do with the signs of the arguments.

Robert Vanden Eynde

unread,
Jun 13, 2018, 1:39:09 AM6/13/18
to python-ideas
As mentioned, with complex numbers the radians make more sense and of course cmath.sind(x) + 1j * cmath.sind(x) != cmath.exp(1j * x).

However, adding degrees version for cmath (import cmath) is still useful, cmath.rectd, cmath.phased, cmath.polard etc.

Stephan Houben

unread,
Jun 13, 2018, 3:52:22 AM6/13/18
to Nathaniel Smith, Python-Ideas
Op di 12 jun. 2018 12:41 schreef Nathaniel Smith <n...@pobox.com>:
On Tue, Jun 12, 2018, 00:03 Stephan Houben <steph...@gmail.com> wrote:
Hi all,

I wrote a possible implementation of sindg:


This code first reduces the angle to the [0,90] interval.
After doing so, it can be observed that the simple implementation
  math.sin(math.radians(angle))
produces exact results for 0 and 90, and a result already rounded to nearest for
60.

You observed this on your system, but math.sin uses the platform libm, which might do different things on other people's systems.


Ok, I updated the code to treat all the values 0, 30, 45, 60 and 90 specially.

Stephan

Robert Vanden Eynde

unread,
Jun 13, 2018, 6:01:49 AM6/13/18
to python-ideas
What was wrong with my initial implementation with a lookup table ? :D

def sind(x):
    if x % 90 == 0:
        return (0, 1, 0, -1)[int(x // 90) % 4]
    else:
        return sin(radians(x))

If you want to support multiples of 30, you can do % 30 and // 30.

_______________________________________________

Stephan Houben

unread,
Jun 13, 2018, 6:08:48 AM6/13/18
to Robert Vanden Eynde, python-ideas
2018-06-13 12:00 GMT+02:00 Robert Vanden Eynde <rober...@gmail.com>:
What was wrong with my initial implementation with a lookup table ? :D

def sind(x):
    if x % 90 == 0:
        return (0, 1, 0, -1)[int(x // 90) % 4]
    else:
        return sin(radians(x))

I kinda missed it, but now you ask:

1. It's better to reduce the angle while still in degrees since one of the advantages
   of degrees is that the reduction can be done exactly. Converting very large angles
   first to radians and then taking the sine can introduce a large error,

2. I used fmod instead of % on advice in this thread.

3. I also wanted to special case, 30, 45, and 60.
 

If you want to support multiples of 30, you can do % 30 and // 30.

Sure, but I also wanted to special-case 45.

Stephan

Robert Vanden Eynde

unread,
Jun 13, 2018, 6:13:34 AM6/13/18
to python-ideas
Then of you also want 45, you could do % 15 ? :D

Stephan Houben

unread,
Jun 13, 2018, 6:21:39 AM6/13/18
to Robert Vanden Eynde, python-ideas
2018-06-13 12:08 GMT+02:00 Robert Vanden Eynde <rober...@gmail.com>:
Then of you also want 45, you could do % 15 ? :D

Sure, but how the lookup is done in the Python reference code is
ultimately not so important, since it will need to be rewritten in C
if it is to be included in the math package (math is C-only).

And then we'll probably end up with a bunch of if-checks against
the common values.

Richard Damon

unread,
Jun 13, 2018, 7:13:13 AM6/13/18
to python...@python.org
My first comment is that special casing values like this can lead to
some very undesirable properties when you use the function for numerical
analysis. Suddenly your sind is no longer continuous (sind(x) is no
longer the limit of sind(x+d) as d goes to 0).

As I stated in my initial comment on this, if you are going to create a
sind function with the idea that you want 'nice' angles to return
'exact' results, then what you need to do is have the degree based trig
routines do the angle reduction in degrees, and only when you have a
small enough angle, either use the radians version on the small angle or
directly include an expansion in degrees.

Angle reduction would be based on the identity that sin(x+y) = sin(x) *
cos(y) + cos(x) * sin(y) and cos(x+y) = cos(x)*cos(y) - sin(x) * sin(y).

If you want to find sin(z) for an arbitrary value z, you can reduce it
to and x+y where x is some multiple of say 15 degrees, and y is in the
range -7.5 to 7.5 degrees. You can have stored exact values of sin/cos
of the 15 degree increments (and only really need them between 0 and 90)
and then compute the sin and cos of the y value.

On 6/13/18 6:07 AM, Stephan Houben wrote:
> 2018-06-13 12:00 GMT+02:00 Robert Vanden Eynde <rober...@gmail.com

> <mailto:rober...@gmail.com>>:


>
> What was wrong with my initial implementation with a lookup table
> ? :D
>
> def sind(x):
>     if x % 90 == 0:
>         return (0, 1, 0, -1)[int(x // 90) % 4]
>     else:
>         return sin(radians(x))
>
>
> I kinda missed it, but now you ask:
>
> 1. It's better to reduce the angle while still in degrees since one of
> the advantages
>    of degrees is that the reduction can be done exactly. Converting
> very large angles
>    first to radians and then taking the sine can introduce a large error,
>
> 2. I used fmod instead of % on advice in this thread.
>
> 3. I also wanted to special case, 30, 45, and 60.
>  
>
>
> If you want to support multiples of 30, you can do % 30 and // 30.
>
>
> Sure, but I also wanted to special-case 45.
>
> Stephan
>  
>
>
> Le mer. 13 juin 2018 à 09:51, Stephan Houben <steph...@gmail.com

> <mailto:steph...@gmail.com>> a écrit :


>
> Op di 12 jun. 2018 12:41 schreef Nathaniel Smith

> <n...@pobox.com <mailto:n...@pobox.com>>:

> Python...@python.org <mailto:Python...@python.org>
> https://mail.python.org/mailman/listinfo/python-ideas


> <https://mail.python.org/mailman/listinfo/python-ideas>
> Code of Conduct: http://python.org/psf/codeofconduct/
> <http://python.org/psf/codeofconduct/>
>
>
> _______________________________________________
> Python-ideas mailing list

> Python...@python.org <mailto:Python...@python.org>
> https://mail.python.org/mailman/listinfo/python-ideas


--
Richard Damon

Stephan Houben

unread,
Jun 13, 2018, 7:24:39 AM6/13/18
to Richard Damon, Python-Ideas


Op wo 13 jun. 2018 13:12 schreef Richard Damon <Ric...@damon-family.org>:
My first comment is that special casing values like this can lead to
some very undesirable properties when you use the function for numerical
analysis. Suddenly your sind is no longer continuous (sind(x) is no
longer the limit of sind(x+d) as d goes to 0).


The deviations introduced by the special casing are on the order of one ulp.

At that level of detail the sin wasn't continuous to begin with.

As I stated in my initial comment on this, if you are going to create a
sind function with the idea that you want 'nice' angles to return
'exact' results, then what you need to do is have the degree based trig
routines do the angle reduction in degrees, and only when you have a
small enough angle, either use the radians version on the small angle or
directly include an expansion in degrees.


Yes that is what my code does.
It reduces degrees to [0,90].

Angle reduction would be based on the identity that sin(x+y) = sin(x) *
cos(y) + cos(x) * sin(y) and cos(x+y) = cos(x)*cos(y) - sin(x) * sin(y).

If you want to find sin(z) for an arbitrary value z, you can reduce it
to and x+y where x is some multiple of say 15 degrees, and y is in the
range -7.5 to 7.5 degrees. You can have stored exact values of sin/cos
of the 15 degree increments (and only really need them between 0 and 90)
and then compute the sin and cos of the y value.

This is not how sine functions are calculated. They are calculated by reducing angle to some interval, then evaluating a polynomial which approximates the true sine within that interval.

Stephan

Tim Peters

unread,
Jun 13, 2018, 3:38:16 PM6/13/18
to Python-Ideas
[Richard Damon]
My first comment is that special casing values like this can lead to
some very undesirable properties when you use the function for numerical
analysis. Suddenly your sind is no longer continuous (sind(x) is no
longer the limit of sind(x+d) as d goes to 0).

As I stated in my initial comment on this, if you are going to create a
sind function with the idea that you want 'nice' angles to return
'exact' results, then what you need to do is have the degree based trig
routines do the angle reduction in degrees, and only when you have a
small enough angle, either use the radians version on the small angle or
directly include an expansion in degrees.
...

Either way, it's necessary to get the effect of working in greater than output precision, if it's desired that the best possible result be returned for cases well beyond just the handful of "nice integer inputs" people happen to be focused on today.

So I'll say again that the easiest way to do that is to use `mpmath` to get extra precision directly.

The following does that for sindg, cosdg, and tandg.

- There are no special cases.  Although tandg(90 + i*180) dies with ZeroDivisionError inside mpmath, and that could/should be fiddled to return an infinity instead.

- Apart from that, all functions appear to give the best possible double-precision result for all representable-as-a-double integer degree inputs (sindg(30), cosdg(-100000), doesn't matter).

- And for all representable inputs of the form `integer + j/32` for j in range(32).

- But not for all of the form `integer + j/64` for j in range(1, 64, 2).  A few of those suffer greater than 1/2 ULP error.  Setting EXTRAPREC to 16 is enough to repair those - but why bother? ;-)

- Consider the largest representable double less than 90:

>>> x
89.99999999999999
>>> x.hex()
'0x1.67fffffffffffp+6'

The code below gives the best possible tangent:

>>> tandg(x)
4031832051015932.0

Native precision is waaaaay off:

>>> math.tan(math.radians(x))
3530114321217157.5

It's not really the extra precision that saves the code below, but allowing argument reduction to reduce to the range [-pi/4, pi/4] radians, followed by exploiting trigonometric identities.  In this case, exploiting that tan(pi/2 + z) = -1/tan(z).  Then even native precision is good enough:

>>> -1 / math.tan(math.radians(x - 90))
4031832051015932.0

Here's the code:

    import mpmath
    from math import fmod
    # Return (n, x) such that:
    # 1. d degrees is equivalent to x + n*(pi/2) radians.
    # 2. x is an mpmath float in [-pi/4, pi/4].
    # 3. n is an integer in range(4).
    # There is one potential rounding error, when mpmath.radians() is
    # used to convert a number of degrees between -45 and 45.  This is
    # done using the current mpmath precision.
    def treduce(d):
        d = fmod(d, 360.0)
        n = round(d / 90.0)
        assert -4 <= n <= 4
        d -= n * 90.0
        assert -45.0 <= d <= 45.0
        return n & 3, mpmath.radians(d)

    EXTRAPREC = 14
    def sindg(d):
        with mpmath.extraprec(EXTRAPREC):
            n, x = treduce(d)
            if n & 1:
                x = mpmath.cos(x)
            else:
                x = mpmath.sin(x)
            if n >= 2:
                x = -x
            return float(x)

    def cosdg(d):
        with mpmath.extraprec(EXTRAPREC):
            n, x = treduce(d)
            if n & 1:
                x = mpmath.sin(x)
            else:
                x = mpmath.cos(x)
            if 1 <= n <= 2:
                x = -x
            return float(x)

    def tandg(d):
        with mpmath.extraprec(EXTRAPREC):
            n, x = treduce(d)
            x = mpmath.tan(x)
            if n & 1:
                x = -1.0 / x
            return float(x)

Greg Ewing

unread,
Jun 13, 2018, 8:24:59 PM6/13/18
to Python-Ideas
Stephan Houben wrote:
> Yes that is what my code does.
> It reduces degrees to [0,90].

Only for the special angles, though. Richard's point is that you
need to do tha for *all* angles to avoid discontinuities with
large angles.

> This is not how sine functions are calculated. They are calculated by
> reducing angle to some interval, then evaluating a polynomial which
> approximates the true sine within that interval.

That's what he suggested, with the interval being -7.5 to 7.5 degrees.

--
Greg

Tim Peters

unread,
Jun 14, 2018, 2:45:48 PM6/14/18
to Python-Ideas
I should note that numeric code "that works" is often much subtler than it appears at first glance.  So, for educational purposes, I'll point out some of what _wasn't_ said about this crucial function:

[Tim]
    import mpmath
    from math import fmod
    # Return (n, x) such that:
    # 1. d degrees is equivalent to x + n*(pi/2) radians.
    # 2. x is an mpmath float in [-pi/4, pi/4].
    # 3. n is an integer in range(4).
    # There is one potential rounding error, when mpmath.radians() is
    # used to convert a number of degrees between -45 and 45.  This is
    # done using the current mpmath precision.
    def treduce(d):
        d = fmod(d, 360.0)
        n = round(d / 90.0)
        assert -4 <= n <= 4
        d -= n * 90.0
        assert -45.0 <= d <= 45.0
        return n & 3, mpmath.radians(d)

How do we know there is at most one rounding error in that?  No, it's not obvious.

That `fmod` is exact is guaranteed by the relevant standards, but most people who write a libm don't get it right at first.  There is no "cheap" way to implement it correctly.  It requires getting the effect of doing exact integer division on, potentially, multi-thousand bit integers.  Assuming x > y > 0, a correct implementation of fmod(x, y) ends up in a loop that goes around a number of times roughly equal to log2(x/y), simulating one-bit-at-a-time long division  For example, here's glibc's implementation:


Don't expect that to be easy to follow either ;-)

Then how do we know that `d -= n * 90.0" is exact?  That's not obvious either.  It follows from the "Sterbenz lemma", one version of which:  if x and y are non-zero floats of the same sign within a factor of 2 of each other,

    1/2 <= x/y <= 2  (mathematically)

then x-y is exactly representable as a float too.  This is true regardless of the floating-point base, or of rounding mode in use.  In IEEE-754, it doesn't even need weasel words to exempt underflowing cases.

That lemma needs to be applied by cases, for each of the possible values of (the integer) `n`.  It gets closest to failing for |n| = 1.  For example, if d is a tiny bit larger than 45, n is 1, and then d/90 is (mathematically) very close to 1/2.

Which is another thing that needs to be shown:  "if d is a tiny bit larger than 45, n is 1".  Why?  It's certainly true if we were using infinite precision, but we're not.  The smallest representable double > 45 is 45 + 2**-47:

>>> d = 45 + 2**-47
>>> d
45.00000000000001
>>> _.hex()
'0x1.6800000000001p+5'

Then d/90.0 (the argument to round()) is, with infinite precision,

(45 + 2**-47)/90 =
0.5 + 2**-47/90

1 ULP with respect to 0.5 is 2**-53, so that in turn is equal to

0.5 + 2**-53/(90/64) =
0.5 + (64/90)*2**-53 =
0.5 + 0.71111111111... * 2**-53

Because the tail (0.711...) is greater than 0.5 ULP, it rounds up under nearest-even rounding, to

0.5 + 2**-53

>>> d / 90
0.5000000000000001
>>> 0.5 + 2**-53
0.5000000000000001

and so Python's round() rounds it up to 1:

>>> round(_)
1

Note that it would _not_ be true if truncating "rounding" were done, so round-nearest is a hidden assumption in the code.

Similar analysis needs to be done at values near the boundaries around all possible values of `n`.

That `assert -45.0 <= d <= 45.0` can't fall then follows from all of that.

In all, a proof that the code is correct is much longer than the code itself.  That's typical.  Alas, it's also typical that math library sources rarely point out the subtleties.

Richard Damon

unread,
Jun 15, 2018, 9:34:48 AM6/15/18
to Python-Ideas
On 6/13/18 7:21 AM, Stephan Houben wrote:
>
>
> Op wo 13 jun. 2018 13:12 schreef Richard Damon
> <Ric...@damon-family.org <mailto:Ric...@damon-family.org>>:

>
> My first comment is that special casing values like this can lead to
> some very undesirable properties when you use the function for
> numerical
> analysis. Suddenly your sind is no longer continuous (sind(x) is no
> longer the limit of sind(x+d) as d goes to 0).
>
>
>
> The deviations introduced by the special casing are on the order of
> one ulp.
>
> At that level of detail the sin wasn't continuous to begin with.
I would say the change isn't one ulp, changing a non-zero number to zero
is not one ulp (unless maybe you are on the verge of underflow). It may
be one ulp of 'full scale', but we aren't near the full scale point. It
might be the right answer for a less than one ulp change in the INPUT,
but if we thought that way we wouldn't have minded the non-zero result
in the first place. The fundamental motivation is that for 'nice angles'
we want the 'nice result' when possible, but the issue is that most of
the 'nice angles'  in radians are not representable exactly, so it isn't
surprising that we don't get the nice results out.

One property that we like to preserve in functional calculation is that
the following pseudo code

dp = x + delta
derivative = ( f(xp) - f(x) ) / (xp - x)

(or variations where you subtract delta or work at x+delta and x-delta)

should approximate well the derivative of the function, (which for sin
in radians should be cos), and that this improves as delta gets very
small until we hit the rounding error in the computation of f(x). (Note,
I don't divide by delta, but xp-x to remove the round off error in
computing xp which isn't the fault of the function f). Changing a point
because it is the closest to the nice number will cause this calculation
to spike due to the single point perturbation). Yes, this calculation
may start to 'blow up' f(xp) - f(x) is very small compared to f(x) and
we start to measure the round off error in the computation of the
function, near a zero of the function, (which if we are root finding is
common) we can do quite well.

And that is what my method did (as others have said). Virtual all
methods of computing sin and cos use the angle addition formula (and
even quadrant reduction is using it for the special case of using one of
the angles where sin/cos are valued in-1, 0, 1). The methods that least
use it that I know of reduces the angle to a quadrant (or octant) and
then selects one a number of expansions good for a limited range, or
table interpolation (but even that sort of uses it with the
approximation of sin(x) ~ x and cos(x) ~ 1 for very small x)

Steven D'Aprano

unread,
Jun 16, 2018, 9:28:26 AM6/16/18
to python...@python.org
On Thu, Jun 14, 2018 at 01:44:34PM -0500, Tim Peters wrote:
> I should note that numeric code "that works" is often much subtler than it
> appears at first glance. So, for educational purposes, I'll point out some
> of what _wasn't_ said about this crucial function:
[...]


Thanks Tim!

Reading your digressions on the minutia of floating point maths is
certainly an education. It makes algebra and real-valued mathematics
seem easy in comparison.

I still haven't got over Mark Dickinson's demonstration a few years
back that under Decimal floating point, but not binary, it is possible
for the ordinary arithmetic average (x+y)/2 to be outside of the
range [x, y]:


py> from decimal import getcontext, Decimal
py> getcontext().prec = 3
py> x = Decimal('0.516')
py> y = Decimal('0.518')
py> (x + y) / 2
Decimal('0.515')




--
Steve

Tim Peters

unread,
Jun 17, 2018, 1:58:42 AM6/17/18
to Steven D'Aprano, Python-Ideas
[Steven D'Aprano <st...@pearwood.info>]
Thanks Tim!

You're welcome ;-) 
 
Reading your digressions on the minutia of floating point maths is
certainly an education. It makes algebra and real-valued mathematics
seem easy in comparison.

Hard to say, really.  The problem with floating point is that it's so God-awful lumpy - special cases all over the place.  Signaling and quiet NaNs; signed infinities; signed zeroes; normal finites all with the same number of bits, but where the gap between numbers changes abruptly at power-of-2 boundaries; subnormals where the gap remains the same across power-of-2 boundaries, but the number of _bits_ changes abruptly; all "the rules" break down when you get too close to overflow or underflow; four rounding modes to worry about; and a whole pile of technically defined exceptional conditions and related traps & flags.

Ignoring all that, though, it's pretty easy ;-)  754 was dead serious about requiring results act is if a single rounding is done to the infinitely precise result, and that actually allows great simplification in reasoning.

The trend these days appears to be using automated theorem-proving systems to keep track of the mountain of interacting special cases.  Those have advanced enough that we may even be on the edge of getting provably-correctly-rounded transcendental functions with reasonable speed.  Although it's not clear people will be able to understand the proofs ;-)

I still haven't got over Mark Dickinson's demonstration a few years
back that under Decimal floating point, but not binary, it is possible
for the ordinary arithmetic average (x+y)/2 to be outside of the
range [x, y]:

py> from decimal import getcontext, Decimal
py> getcontext().prec = 3
py> x = Decimal('0.516')
py> y = Decimal('0.518')
py> (x + y) / 2
Decimal('0.515')

Ya, decimal fp doesn't really solve anything except the shallow surprise that decimal fractions generally aren't exactly representable as binary fractions.  Which is worth a whole lot for casual users, but doesn't address any of the deep problems (to the contrary, it makes those a bit worse).

I like to illustrate the above with 1-digit decimal fp, because it makes it more apparent at once that - unlike as in binary fp - multiplication and division by 2 may _not_ be exact in decimal fp.  We can't even average a number "with itself" reliably:

>>> import decimal
>>> decimal.getcontext().prec = 1
>>> x = y = decimal.Decimal(8); (x+y)/2 # 10 is much bigger than 8
Decimal('1E+1')
>>> x = y = decimal.Decimal(7); (x+y)/2 # 5 is much smaller than 7
Decimal('5')

But related things _can_ happen in binary fp too!  You have to be near the edge of representable non-zero finites though:

>>> x = y = 1e308
>>> x
1e+308
>>> (x+y)/2
inf

Oops.  So rewrite it:

>>> x/2 + y/2
1e+308

Better!  But then:

>>> x = y = float.fromhex("3p-1074")
>>> x
1.5e-323
>>> x/2 + y/2
2e-323

Oops.  A math library has to deal with everything "correctly".  Believe it or not, this paper

    "How do you compute the midpoint of an interval?"
    https://hal.archives-ouvertes.fr/hal-00576641v1/document

is solely concerned with computing the average of two IEEE doubles, yet runs to 29(!) pages.  Almost everything you try fails for _some_ goofy cases.

I personally write it as (x+y)/2 anyway ;-)

Chris Barker via Python-ideas

unread,
Jun 18, 2018, 10:25:38 PM6/18/18
to Tim Peters, Python-Ideas
On Sat, Jun 16, 2018 at 10:57 PM, Tim Peters <tim.p...@gmail.com> wrote:

Ya, decimal fp doesn't really solve anything except the shallow surprise that decimal fractions generally aren't exactly representable as binary fractions.  Which is worth a whole lot for casual users, but doesn't address any of the deep problems (to the contrary, it makes those a bit worse).

It's my suspicion that the story is the same with "degree-based" trig :-)

Which is why, if you want "nice-looking" results, it seems one could simply accept a decimal digit or so less precision, and use the "regular" FP trig functions, rounding to 14 or so decimal digits.

Though if someone really wants to implement trig in native degrees -- more power to 'em.

However -- if this is really such a good idea -- wouldn't someone have make a C lib that does it? Or has someone? Anyone looked?

-CHB


--

Christopher Barker, Ph.D.
Oceanographer

Emergency Response Division
NOAA/NOS/OR&R            (206) 526-6959   voice
7600 Sand Point Way NE   (206) 526-6329   fax
Seattle, WA  98115       (206) 526-6317   main reception

Chris....@noaa.gov

Matt Arcidy

unread,
Jun 19, 2018, 1:10:58 AM6/19/18
to Chris Barker - NOAA Federal, python-ideas


On Mon, Jun 18, 2018, 19:25 Chris Barker via Python-ideas <python...@python.org> wrote:
On Sat, Jun 16, 2018 at 10:57 PM, Tim Peters <tim.p...@gmail.com> wrote:

Ya, decimal fp doesn't really solve anything except the shallow surprise that decimal fractions generally aren't exactly representable as binary fractions.  Which is worth a whole lot for casual users, but doesn't address any of the deep problems (to the contrary, it makes those a bit worse).

It's my suspicion that the story is the same with "degree-based" trig :-)

Which is why, if you want "nice-looking" results, it seems one could simply accept a decimal digit or so less precision, and use the "regular" FP trig functions, rounding to 14 or so decimal digits.

Though if someone really wants to implement trig in native degrees -- more power to 'em.

However -- if this is really such a good idea -- wouldn't someone have make a C lib that does it? Or has someone? Anyone looked?


quite a few in fact, including cos(n*pi)




-CHB


--

Christopher Barker, Ph.D.
Oceanographer

Emergency Response Division
NOAA/NOS/OR&R            (206) 526-6959   voice
7600 Sand Point Way NE   (206) 526-6329   fax
Seattle, WA  98115       (206) 526-6317   main reception

Chris....@noaa.gov

Robert Kern

unread,
Jun 19, 2018, 2:38:15 AM6/19/18
to python...@python.org
On 6/18/18 19:23, Chris Barker via Python-ideas wrote:
> On Sat, Jun 16, 2018 at 10:57 PM, Tim Peters
> <tim.p...@gmail.com
> <mailto:tim.p...@gmail.com>> wrote:
>
> Ya, decimal fp doesn't really solve anything except the shallow surprise
> that decimal fractions generally aren't exactly representable as binary
> fractions.  Which is worth a whole lot for casual users, but doesn't address
> any of the deep problems (to the contrary, it makes those a bit worse).
>
>
> It's my suspicion that the story is the same with "degree-based" trig :-)
>
> Which is why, if you want "nice-looking" results, it seems one could simply
> accept a decimal digit or so less precision, and use the "regular" FP trig
> functions, rounding to 14 or so decimal digits.
>
> Though if someone really wants to implement trig in native degrees -- more power
> to 'em.
>
> However -- if this is really such a good idea -- wouldn't someone have make a C
> lib that does it? Or has someone? Anyone looked?

Certainly! scipy.special uses the functions implemented in the Cephes C library.

--
Robert Kern

"I have come to believe that the whole world is an enigma, a harmless enigma
that is made terrible by our own mad attempt to interpret it as though it had
an underlying truth."
-- Umberto Eco

Steven D'Aprano

unread,
Jun 19, 2018, 6:06:30 AM6/19/18
to python...@python.org
On Mon, Jun 18, 2018 at 07:23:50PM -0700, Chris Barker wrote:

> Though if someone really wants to implement trig in native degrees -- more
> power to 'em.
>
> However -- if this is really such a good idea -- wouldn't someone have make
> a C lib that does it? Or has someone? Anyone looked?

No, there's nothing magical about C. You can do it in pure Python. It
isn't as fast of course, but it works well enough.

When I get a Round Tuit, I'll pop the code up on PyPy.

Chris Barker - NOAA Federal via Python-ideas

unread,
Jun 23, 2018, 5:21:43 PM6/23/18
to Steven D'Aprano, python...@python.org
>> However -- if this is really such a good idea -- wouldn't someone have make
>> a C lib that does it? Or has someone? Anyone looked?
>
> No, there's nothing magical about C. You can do it in pure Python.

Sure, but there are a number of FP subtleties around the edge cases.

So wrapping (or translating) an existing (well thought out) lib might
be an easier way to go.

-CHB

Kyle Lahnakoski

unread,
May 8, 2019, 11:06:30 AM5/8/19
to python...@python.org


Maybe a library of trig functions that include an Angle type? This is related to the timespan units discussion we just had


def sin(angle):


On 2018-06-08 01:44, Yuval Greenfield wrote:
On Thu, Jun 7, 2018 at 10:38 PM Stephen J. Turnbull <turnbull....@u.tsukuba.ac.jp> wrote:

6.123233995736766e-17
>>>

is good enough for government work, including at the local public high
school.


There probably is room for a library like "fractions" that represents multiples of pi or degrees precisely. I'm not sure how complicated or valuable of an endeavor that would be. But while I agree that floating point is good enough, we probably can do better.
 
Reply all
Reply to author
Forward
0 new messages