IronLanguages / ironpython2

Implementation of the Python programming language for .NET Framework; built on top of the Dynamic Language Runtime (DLR).
http://ironpython.net
Apache License 2.0
1.08k stars 229 forks source link

Equivalent code in IronPython from C# code #777

Closed Marco-Pellegrino closed 3 years ago

Marco-Pellegrino commented 3 years ago

Hi guys, is anyone able to help translating this C# piece of code for IronPython? Is it even possible?

I am developing a .NET application that requires the use of Iron Python to read an HDF5 file format and I have found a piece of code written in C# that solve my problem. I have tried to translate the code in Iron Python but with no luck.

I attached a "recorder.hdf5" in case it is helpful. Download: https://drive.google.com/file/d/1SAKkZf0VGHRfbdPKabyiEPzpEXie4VzC/view?usp=sharing I am using the HDF5-CSharp library https://github.com/LiorBanai/HDF5-CSharp but I am struggling to find a solution.

public class Coordinate
{
       [Hdf5EntryName("COORDINATES")] public double[,] COORDINATES { get; set; }
}

string filename = @"recorder.hdf5"; \\full path
long fileId = -1;
try
{
        fileId = Hdf5.OpenFile(filename, true);
        var result = Hdf5.ReadObject<Coordinate>(fileId, "/MODEL_STAGE[1]/MODEL/NODES");
}
 finally
 {
        if (fileId > 0)
           {
             Hdf5.CloseFile(fileId);
            }
 }

Best, Marco

slozier commented 3 years ago

I'm not sure, off the top of my head, how to create the Coordinate class in IronPython, but perhaps it's doable using clrtype. So I cheated in injected the class into the scope (alternatively you could import it from your DLL on the Python side).

using HDF5CSharp.DataTypes;
using IronPython.Hosting;

var engine = Python.CreateEngine();
var paths = engine.GetSearchPaths();
paths.Add(@"C:\Program Files\IronPython 2.7\Lib");
engine.SetSearchPaths(paths);
var scope = engine.CreateScope();
scope.SetVariable("Coordinate", typeof(Coordinate));
engine.ExecuteFile("test.py", scope);

public class Coordinate
{
       [Hdf5EntryName("COORDINATES")] public double[,] COORDINATES { get; set; }
}

test.py:

import clr

clr.AddReference(r"HDF5CSharp")

from HDF5CSharp import Hdf5

fileId = 0
try:
    fileId = Hdf5.OpenFile(r"recorder.hdf5", True)
    result = Hdf5.ReadObject[Coordinate](fileId, "/MODEL_STAGE[1]/MODEL/NODES")
    print(result.COORDINATES[0,0])
except:
    if fileId > 0:
        Hdf5.CloseFile(fileId)
Marco-Pellegrino commented 3 years ago

thanks @slozier ! It is an interesting solution but I am not able to make it working. How can I run all the code in IronPython? I am a structural engineer who try to code but it seems a pretty advance topic to me :/

slozier commented 3 years ago

Ok, looks like the Hdf5EntryName attribute is not necessary if the name of the property already matches, this simplifies things a bit:

import System

import clr
import clrtype

clr.AddReference(r"HDF5CSharp")

from HDF5CSharp import Hdf5

type_2d = type(System.Array.CreateInstance(float, 1, 1))

class Coordinate(object):
    __metaclass__ = clrtype.ClrClass

    @property
    @clrtype.accepts()
    @clrtype.returns(type_2d)
    def COORDINATES(self): return self.__coords

    @COORDINATES.setter
    @clrtype.accepts(type_2d)
    @clrtype.returns()
    def COORDINATES(self, value): self.__coords = value    

fileId = 0
try:
    fileId = Hdf5.OpenFile(r"recorder.hdf5", True)
    result = Hdf5.ReadObject[Coordinate](fileId, "/MODEL_STAGE[1]/MODEL/NODES")
    print(result.COORDINATES[0,0])
finally:
    if fileId > 0:
        Hdf5.CloseFile(fileId)
Marco-Pellegrino commented 3 years ago

@slozier Marvellous! It is really good stuff. I would never get close to this solution.

There is only a small issue with this approach. If we try to run the script twice time, we get an error.

Message: Duplicate type name within an assembly.

Traceback:
  line 537, in __clrtype__, "C:\Program Files\Rhino 6\Plug-ins\IronPython\Lib\clrtype.py"
  line 12, in <module>, "C:\Users\FORMAT\Desktop\hdf5Test\test.py"

do you have a solution for this? I can use my brute-force approach and change the name in order to have a different one every time I run the script.

Is there anyway to offer you a beer by paypal? I appreciate your help

testHDF5

thimolangbehn commented 3 years ago

You could extract the type definitions into a module, however that would require the module load path to be set up correctly. As an in-code workaround, you could check if the class is already defined:

import System import clr import clrtype

clr.AddReference(r"HDF5CSharp")

from HDF5CSharp import Hdf5

if "Coordinate" not in globals(): type_2d = type(System.Array.CreateInstance(float, 1, 1))

class Coordinate(object):
    __metaclass__ = clrtype.ClrClass

    @property
    @clrtype.accepts()
    @clrtype.returns(type_2d)
    def COORDINATES(self): return self.__coords

    @COORDINATES.setter
    @clrtype.accepts(type_2d)
    @clrtype.returns()
    def COORDINATES(self, value): self.__coords = value

fileId = 0 try: fileId = Hdf5.OpenFile(r"recorder.hdf5", True) result = Hdf5.ReadObjectCoordinate print(result.COORDINATES[0,0]) finally: if fileId > 0: Hdf5.CloseFile(fileId)

Marco-Pellegrino commented 3 years ago

Hi @thimolangbehn ! Thanks for your help but I am still getting the same error as per @slozier approach.

have you tested your script in your application?

slozier commented 3 years ago

I have no idea how Rhino uses IronPython, but assuming it's just reusing the same engine you could maybe stash it away in a module:

my_module_name = "some_unique_name" # replace this with something unique that doesn't collide with a real module!
my_module = sys.modules.setdefault(my_module_name, type(sys)(my_module_name))

try:
    Coordinate = my_module.Coordinate
except AttributeError:
    type_2d = type(System.Array.CreateInstance(float, 1, 1))

    class Coordinate(object):
        __metaclass__ = clrtype.ClrClass

        @property
        @clrtype.accepts()
        @clrtype.returns(type_2d)
        def COORDINATES(self): return self.__coords

        @COORDINATES.setter
        @clrtype.accepts(type_2d)
        @clrtype.returns()
        def COORDINATES(self, value): self.__coords = value

    my_module.Coordinate = Coordinate

You could also put it in an existing module (like clrtype).

Marco-Pellegrino commented 3 years ago

@slozier , you are my hero! It is finally working and I should be able to read also the other "attributes" from the data base. Thanks you so much! You made me happy!

The tool that I am trying to use is called "Alpaca4d". The aim is to allow user to design for earthquake within a parametric environment. https://github.com/Alpaca4d/Alpaca

Marco-Pellegrino commented 3 years ago

Hi @slozier , I wanted to give you a little update and show you what it has been possible with your help! I can finally read the coordinates and displacement from a HDF5 file and it looks great! Do you have any idea on how can I read different "attribute"? I would like to select a specific STEP_X as input.

the number of Step are different from each analyses. Do you have any idea or suggestion?

For example: n = number of step

result.STEP_n

result = Hdf5.ReadObject[Displacementss](fileId, "/MODEL_STAGE[1]/RESULTS/ON_NODES/DISPLACEMENT/DATA")

try:
    for i in range(1000):
        a.append( result.STEP_n[i,0])
        b.append( result.STEP_n[i,1])
        c.append( result.STEP_n[i,2])
    Hdf5.CloseFile(fileId)
except:
    pass

testHDF5_Displacement

Marco-Pellegrino commented 3 years ago

They have suggest me something like this but I am not sure that it can work with ItonPython

        public class Steps
        {
            [Hdf5EntryName("STEP_0")] public double[,] STEP0 { get; set; }
            [Hdf5EntryName("STEP_1")] public double[,] STEP1 { get; set; }

        }
                fileId = Hdf5.OpenFile(filename, true);
                Assert.IsTrue(fileId > 0);
                var step = "/MODEL_STAGE[1]/RESULTS/ON_NODES/DISPLACEMENT/DATA";
                var result2 = Hdf5.ReadObject<
thimolangbehn commented 3 years ago

It seems you want to build attribute-names in code.

step_n = getattr(result, 'STEP_{}'.format(n))

Using this, the following examples are equivalent:

# manual:
a = []
a.append(result.STEP_0)
a.append(result.STEP_1)
a.append(result.STEP_2)

# in a loop:
a = []
for i in range(3):
    step_i = getattr(result, 'STEP_{}'.format(i))
    a.append(step_i)

This is common python, not an IronPython specific process,

slozier commented 3 years ago

It looks like HDF5CSharp might work on fields too. Here's a simplification of my previous code:

try:
    Coordinate = my_module.Coordinate
except AttributeError:
    type_2d = type(System.Array.CreateInstance(float, 1, 1))

    class Coordinate(object):
        __metaclass__ = clrtype.ClrClass
        _clrfields = {"COORDINATES": type_2d}

    my_module.Coordinate = Coordinate

and then by extension, you could do:

try:
    Steps = my_module.Steps
except AttributeError:
    type_2d = type(System.Array.CreateInstance(float, 1, 1))

    class Steps(object):
        __metaclass__ = clrtype.ClrClass
        _clrfields = {"STEP_{}".format(i): type_2d for i in range(100)} # change 100 to your max

    my_module.Steps = Steps
Marco-Pellegrino commented 3 years ago

@slozier @thimolangbehn you are rock stars! I should be able to extend your concept for also the other values (Forces, Stress, ect ect) I need to study a little bit what it is happening with clr, clrtype and what happen to the module with a unique name but I am happy.

I share my latest code and a .gif Thanks a lot!

import sys
import System
import clr
import clrtype
import Rhino.Geometry as rg

my_module_name = "aaa_one_other_unique_name" # replace this with something unique that doesn't collide with a real module!
my_module = sys.modules.setdefault(my_module_name, type(sys)(my_module_name))
clr.AddReferenceToFileAndPath(r"C:\Users\FORMAT\Desktop\hdf5Test\HDF5CSharp.dll")

from HDF5CSharp import Hdf5

try:
    Steps = my_module.Steps
except AttributeError:
    type_2d = type(System.Array.CreateInstance(float, 1, 1))

    class Steps(object):
        __metaclass__ = clrtype.ClrClass
        _clrfields = {"STEP_{}".format(i): type_2d for i in range(1000)} # it will work for model with less than 1000 steps

    my_module.Steps = Steps

fileId = Hdf5.OpenFile(r"C:\Users\FORMAT\Desktop\hdf5Test\recorder.hdf5", True)
numberOfPoints = 304 #

displacementResult = Hdf5.ReadObject[Steps](fileId, "/MODEL_STAGE[1]/RESULTS/ON_NODES/DISPLACEMENT/DATA")

displacement = []

for i in range(numberOfPoints):
    x = getattr(displacementResult, 'STEP_{}'.format(step))[i,0]
    y = getattr(displacementResult, 'STEP_{}'.format(step))[i,1]
    z = getattr(displacementResult, 'STEP_{}'.format(step))[i,2]
    displacement.append(rg.Vector3d(x,y,z))

rotationResult = Hdf5.ReadObject[Steps](fileId, "/MODEL_STAGE[1]/RESULTS/ON_NODES/ROTATION/DATA")

rotation = []

for i in range(numberOfPoints):
    x = getattr(rotationResult, 'STEP_{}'.format(step))[i,0]
    y = getattr(rotationResult, 'STEP_{}'.format(step))[i,1]
    z = getattr(rotationResult, 'STEP_{}'.format(step))[i,2]
    rotation.append(rg.Vector3d(x,y,z))

testHDF5_Displacement_Step