Skip to content

Numerical accuracy when converting from Single to float #1896

Closed

Description

@m-rossi

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
    

Activity

  1. lostmsu commented on Aug 3, 2022

    @lostmsu
    Member

    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'
  2. m-rossi commented on Aug 3, 2022

    @m-rossi
    ContributorAuthor

    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()
  3. filmor commented on Aug 10, 2022

    @filmor
    Member

    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.

  4. m-rossi commented on Aug 10, 2022

    @m-rossi
    ContributorAuthor

    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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions