pythonnet icon indicating copy to clipboard operation
pythonnet copied to clipboard

Numerical accuracy when converting from Single to float

Open m-rossi opened this issue 3 years ago • 4 comments

Environment

  • Pythonnet version: 3.0.0rc4
  • Python version: 3.10.5
  • Operating System: Windows 10 20H2
  • .NET Runtime: 6.0.6

Details

  • Describe what you were trying to get done.

    I'm sorry it's me again with some weird low-level issue. I use a DLL where a function returns a System.Single. Its return value is 0.01. Before pythonnet version 3.0 this got converted to a Python float also of value 0.01. With Pythonnet 3.0 I get a Python float 0.009999999776482582 instead. I can reproduce this issue by creating System.Single objects.

  • What commands did you run to trigger this issue? If you can provide a Minimal, Complete, and Verifiable example this will help us understand the issue.

    import clr
    import System
    
    # Parse a string to avoid conversion from Python float to dotnet
    print(System.Single.Parse('1e-2'))
    # Create a pure dotnet object without conversion back to Python (I think .ToString() gets called here?)
    print(System.Single(0.01))
    # Create a pure dotnet object without conversion back to Python and convert back to Python
    print(System.Single(0.01).MemberwiseClone())
    
    0.009999999776482582
    0.01
    0.009999999776482582
    

    With pythonnet 2.5.2 this will return

    0.01
    0.01
    0.01
    

m-rossi avatar Aug 03 '22 15:08 m-rossi

Always check your types )

The three expressions perhaps surprisingly give values of two different types: the 1st and 3rd are Python float (which in CPython is a float64, so same as System.Double), and the 2nd is System.Single (float32). And the values are actually equal. The reason you see different strings has to do with how precision of the number type affects rounding.

Sadly, it looks like the current behavior deserves its place in the famous JavaScript WAT video.

The cause for type to be different is that in most cases any System.Single value returned to Python from a call to .NET is automatically widened without loss of precision to Python float (again, which is == float64 == System.Double) similarly to how System.Int32 is widened to Python int - for convenience. The direct invocation of System.Single "constructor" is AFAIK the only exception because it is used to explicitly convert type to System.Single in order to choose a .NET overload when necessary.

So I guess that WAT-like behavior is by design, and we won't change it in 3.0.0

Though while playing with your code I noticed that in 3.0 (at least) float(System.Single(42)) fails, and so does any attempt to use int() or float() on instances of primitive .NET types, which looks like a bug, but it also might be intentional.

@filmor thoughts on the float(System.Single(42)) issue?

My experiments:

>>> parsed = System.Single.Parse('1e-2')
>>> from_py = System.Single(0.01)
>>> cloned = from_py.MemberwiseClone()
>>> parsed
0.009999999776482582
>>> from_py
<System.Single object at 0x000001E25534B580>
>>> cloned
0.009999999776482582
>>> from_py.ToString()
'0.01'
>>> float(from_py)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Single'
>>> parsed == from_py
True
>>> float(from_py)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Single'
>>> from_py2 = System.Single(0.02)
>>> parsed == from_py2
False
>>> System.Double(from_py)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: System.Single value cannot be converted to System.Double
>>> System.Array[int](10).Length
10
>>> type(System.Array[int](10).Length)
<class 'int'>
>>> float(System.Int32(42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: float() argument must be a string or a real number, not 'Int32'
>>> int(System.Int32(42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'Int32'

lostmsu avatar Aug 03 '22 19:08 lostmsu

Thank you for that detailed explanation!

The reason you see different strings has to do with how precision of the number type affects rounding.

I guessed it's something related to this. That's why I chose that title with accuracy. Floats can be evil sometimes.

Sadly, it looks like the current behavior deserves its place in the famous JavaScript WAT video.

Thank you, I did not know the video. I laughed out loud and can't wait to send it to colleagues tomorrow. 😂

Though while playing with your code I noticed that in 3.0 (at least) float(System.Single(42)) fails, and so does any attempt to use int() or float() on instances of primitive .NET types, which looks like a bug, but it also might be intentional.

That's reassuring, I thought I was just too stupid to make it a Python datatype again and therefore came up with

System.Single(0.01).MemberwiseClone()

m-rossi avatar Aug 03 '22 20:08 m-rossi

I guess float(Single(42)) used to work because we implicitly converted Single to Python's float. We currently don't implement __float__ or __int__ on any .NET types, that is what's missing here.

filmor avatar Aug 10 '22 07:08 filmor

As a workaround I can use numpy for the rescue and its float32-datatype

import clr
import numpy as np
import System

s = System.Single(0.01).MemberwiseClone()
print(s)
n = np.float32(s)
print(n)
0.009999999776482582
0.01

m-rossi avatar Aug 10 '22 10:08 m-rossi