python 3.15 adds math.isnormal() and math.issubnormal(). I was wondering if we should add cmath.isnormal() and cmath.issubnormal() too.
There are 2 possible implementations:
.NET:
def isnormal(z):
return math.isnormal(z.real) and (z.imag == 0 or math.isnormal(z.imag))
def issubnormal(z):
return math.issubnormal(z.real) or math.issubnormal(z.imag)
Swift:
def isnormal(z):
return isfinite(z) and (math.isnormal(z.real) or math.isnormal(z.imag))
def issubnormal(z):
return math.issubnormal(z.real) and math.issubnormal(z.imag)
If you have complex numbers with subnormal components, you lose performance and accuracy:
performance:
$ python -m timeit -s "a = 2+0j; b = 5e-308+5e-308j" "a * b"
5000000 loops, best of 5: 49.1 nsec per loop
$ python -m timeit -s "a = 2+0j; b = 5e-308+5e-324j" "a * b"
5000000 loops, best of 5: 88.3 nsec per loop
$ python -m timeit -s "a = 2+0j; b = 5e-324+5e-324j" "a * b"
2000000 loops, best of 5: 124 nsec per loop
I don’t have any personal need for these functions, but it seems to me that if there’s no standard definition for what they should do on complex numbers, it’s better to let the user write their own than to pick a definition which might not match what the user is expecting…
As we have more than two definitions — that means it’s not about mathematics. (In fact, we could invent even more: e.g. permit zeros in either component of the “normal” complex number with the second component — being a normal float.)
These helpers will be just “short-cuts”, that now are possible, using math’s functions on components of the complex number. The questions are:
are they useful like isnan/isinf?
which one definition we should chose?
I think isnormal/issubnormal notions don’t make sense in usual complex arithmetic (pair of IEEE floating-point numbers, etc). Maybe in some positional numeral system with imaginary base like 2j.
Are there real world examples of people using this kind of function? In those examples, which definition is useful?
The fact that there are competing definitions could indicate that this is something tricky to get right, so it might be useful to provide in the stdlib, assuming that one definition is clearly more correct than the other.
That’s tricky to figure out, checking for this is not very common. I mostly brought this up for being able to switch from math to cmath and because this wasn’t discussed yet.
A user can already write, issubnormal(abs(z)) and have well-defined obvious code. But if cmath adds these functions, a reader has to guess how it it interpreted, perhaps issubnormal(z.real) or issubnormal(z.imag). Note, this is the same reason that complex numbers are not orderable; we could invent an ordering but that doesn’t make it obvious or useful.
Also, the case for adding math.issubnormal() was already weak. It wasn’t done because our users needed it; it was done because C did it. Mostly, these functions will just clutter the math library but not be used (do you have a needed, not invented, use case?). Let’s not double down by also extending this to cmath.
Can you point to any real world examples of where a project needed this? Have any complex number libraries implemented this?
ISTM that this thread was inspired by “math has this so cmath should too” rather than any demonstrated need. That is why I think this would just be clutter.
As a practical matter, testing for subnormals is very rare for apps using float64 (or wider). The need is far more acute when using float32, because their dynamic range is so much narrower and they have so much less precision to begin with.
Now Python is well served by supplying float.subnormal() (etc) anyway, because they’re defined by standard C and so very widely implemented. Even if they are essentially useless in real life . There’s practical use for them when working with float32, and C always supplies all floating-point math functions for all widths, for “consistency”.
But best I can tell there is no respected standard that even tries to define what they “should mean” for a complex type. Python should be a follower here, not a leader,
That said, .NET’s definition makes more sense to me than Swift’s. “Subnormal” means “I’ve lost full precision, but not yet enough to underflow to 0” - not necessarily fatal but an alarm bell. For a complex type full precision has been lost if either component has lost precision. That’s an alarm bell.
So my preferences:
Don’t add them unless/until C does.
If we “have to” add them, follow .NET rather than Swift.
isnormal() is not symmetric. isnormal(complex(x, y)) != isnormal(complex(y, x)).
issubnormal() can return true for infinity and NaN, while isnormal() always return false for them.
The Swift implementation is at least more consistent. BTW, the real Swift implementation of issubnormal() contains explicit condition that the argument is not complex zero. It is only needed if isnormal() for floats in Swift differs from isnormal() in C.
The other consistent implementation is:
def isnormal(z):
return math.isnormal(z.real) and math.isnormal(z.imag)
def issubnormal(z):
return isfinite(z) and (math.issubnormal(z.real) or math.issubnormal(z.imag))
But we have no good reasons to prefer one or other. We do not even have good reasons to reject infinity and NaN, or to not make a special case for zero, so there are much more possible implementations.
Net implementation looks slightly different than announced in this thread:
public static bool IsNormal(Complex value)
{
// much as IsFinite requires both part to be finite, we require both
// part to be "normal" (finite, non-zero, and non-subnormal) to be true
return double.IsNormal(value.m_real)
&& ((value.m_imaginary == 0.0) || double.IsNormal(value.m_imaginary));
}
I have only a vague idea of what isnormal and issubnormal mean, and reading the docs gives two very brief and circular definitions. If someone wants to, it would be great to update the docs to be a little more helpful, even by linking to some external authority about what these functions are for.
Are you talking only about table entries? I appreciate better descriptions, but documentation texts don’t look circular for me:
isnormal(x): Return True if x is a normal number, that is a finite nonzero number that is not a subnormal (see issubnormal()). Return False otherwise.
issubnormal(x): Return True if x is a subnormal number, that is a finite nonzero number with a magnitude smaller than the smallest positive normal number, see sys.float_info.min. Return False otherwise.
Perhaps it’s hard to see how these paragraphs read to someone who doesn’t know what a “normal” number is. isnormal is defined as something that isn’t subnormal. issubnormal refers to normal numbers. The reference is to sys.float_info.min which says “The minimum representable positive normalized float.”
Where is the actual definition of what a normalized float is? Sure, I can use a search engine to look up the terms, but it could be helpful to briefly explain what it is and why I might want to know if a number is normal or not.
To someone curious about what is in the math module, reading the current descriptions sound like, “if you have to ask what it means, you don’t need it.”
Sorry, I’m not a native English speaker and maybe you can rephrase the issubnormal() description better.
Though, I don’t see how it refers to the normal number notion. Rather, it refers to “the smallest positive normal number, see sys.float_info.min”. Perhaps, instead of “see” I should use “i.e.”. But the intention was: a reference to emphasized term (which was explained after comma), not to “normal number”.
Which, alas, is largely the truth! It can’t be explained usefully without knowing “a lot” about how floating-point numbers are represented, and no newbie has that background to build on. We could say, e.g., that “normal” means it’s “a finite non-zero float stored with maximum possible machine precision”, but then that relies on ever more “if you have to ask ,” mysteries.
Reference docs aren’t meant to be tutorials, and explaining basic concepts of fp representation would be as out of place as, e.g., bloating the docs with explanations about what math.gamma() does. 'If you have to ask …" applies there too.