8 oct. 2022

[Dynamo += Python] Visualiser un IFC avec CPython3 et... IronPython

 




Est si on utilisait Dynamo pour visualiser un IFC? Et si on profitait des avantages des 2 moteurs Python ?

if this article is not in your language, use the Google Translate widget (bottom of page for Mobile version) ⬈

Bien que Dynamo ne soit pas un viewer Ifc, l'idée de charger la géométrie d'un Ifc complet dans Dynamo m'a toujours trotté dans la tête.


  • Récupérer les données géométriques d'un IFC

Pour exemple, reprenons l'IFC 2x3 du projet RAC_basic_sample, nous récupérons les géométries (coordonnées des triangles) et les couleurs RGB des éléments via la librairie ifcopenshell et le moteur CPython3.

Ici, nous profitons du multiprocessing que permet la libraire ifcopenshell pour récupérer les informations géométriques (shape de chaque Element Ifc)

Ensuite, pour tracer les triangles (convertis en surfaces), on utilisera IronPython.

Pour transférer les donnes du nœud CPython3 au nœud IronPython, la solution la plus simple aurait été d'utiliser une liste, cependant cela représenterait plusieurs centaines de milliers d'items.

liste avec + de 2 millions items


  • Interopérabilité entre les moteurs Pythons (CPython3 et IronPython)

ma première idée (avec peu d'espoir) était d'utiliser une classe Python3 pour stocker la liste pour ensuite transférer un objet/instance vers le nœud IronPython, et comme je m'en doutais un peu, cela ne fonctionne pas.




Une solution de contournement est d'utiliser un objet .Net et non Itérable / Énumérable pour éviter qu'il soit converti en Liste par le wrapper Dynamo. 


    • Solution n°1 : une Classe .Net (CLR)




import sys
import clr
import System
from System.Collections.Generic import List

class ExampleClrClass(System.Object):
    __namespace__ = "MyNameSpace_"
    def __init__(self, lst):
        super().__init__()
        self.__lst = lst
        
    @clr.clrmethod(List[System.Int32], [int])
    def Multiply(self, x):
        return List[System.Int32]([n * x for n in self.__lst])

    @clr.clrproperty(List[System.Int32])
    def GetList(self):
        return List[System.Int32](self.__lst)
              
a = ExampleClrClass(list(range(10000)))

OUT = a
Néanmoins, sous Dynamo, pour une raison que j'ignore, PythonNet 2.5.x ne parvient pas à libérer toutes les ressources (Memory Leak) pour certains objets, les classes Net/CLR en font partie.


    • Solution n°2  : une DataTable

Une DataTable est un objet de type Table de données dont la structure est un peu similaire à un DataFrame Pandas ou une Table SQL, elle peut stocker plus de 16 millions de lignes.

Ce sera ici la solution retenue, on évite ainsi que la sortie du nœud Python (Dynamo wrapper) ait à traiter une immense liste (5 à 10 secondes de gagner).

Le code CPython3


import clr
import sys
import re
import System
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import  Autodesk.DesignScript.Geometry as DS

clr.AddReference('System.Data')
from System.Data import *

clr.AddReference('Python.Included')
import Python.Included as pyInc
path_py3_lib = pyInc.Installer.EmbeddedPythonHome
sys.path.append(path_py3_lib + r'\Lib\site-packages')
import time
import os
import traceback
import ifcopenshell
import ifcopenshell.geom
from ifcopenshell.util.placement import get_local_placement
import numpy as np
import multiprocessing
import math


class IfcUtils():
    def __init__(self, file_path):
        self._file_path = file_path
        self.ifc_file = ifcopenshell.open(self._file_path)
        self.errors = []
    
    def ToGroupVerices(self):
        """ search Cluster by location """
        settings = ifcopenshell.geom.settings()
        settings.set(settings.USE_WORLD_COORDS, True)
        settings.set(settings.APPLY_DEFAULT_MATERIALS, True)
        # get unit length 
        global_unit_assignments = self.ifc_file.by_type("IfcUnitAssignment")
        # The global context defines 0 or more unit sets, each containing IFC unit definitions (using list comprehension):
        global_length_unit_definition = [u for ua in global_unit_assignments for u in ua.Units if u.is_a() in ('IfcSIUnit', 'IfcConversionBasedUnit') and u.UnitType=='LENGTHUNIT'][-1]
        #
        products = self.ifc_file.by_type('IfcProduct')
        triangles = []
        #
        ### Start Processing ####
        print (("nbr_processor", multiprocessing.cpu_count()))
        t1 = time.time()
        iterator = ifcopenshell.geom.iterator(settings, self.ifc_file, multiprocessing.cpu_count())
        print("elapsed_time_iterator", time.time() - t1)
        counter = 0
        dt_triangle = DataTable("ifc_data")
        dt_triangle.Columns.Add("r", int)
        dt_triangle.Columns.Add("g", int)
        dt_triangle.Columns.Add("b", int)
        dt_triangle.Columns.Add("ptaX", float)
        dt_triangle.Columns.Add("ptaY", float)
        dt_triangle.Columns.Add("ptaZ", float)
        dt_triangle.Columns.Add("ptbX", float)
        dt_triangle.Columns.Add("ptbY", float)
        dt_triangle.Columns.Add("ptbZ", float)
        dt_triangle.Columns.Add("ptcX", float)
        dt_triangle.Columns.Add("ptcY", float)
        dt_triangle.Columns.Add("ptcZ", float)
        if iterator.initialize():
            while True:
                counter += 1
                shape = iterator.get()
                # METHOD TO GET MATRIX if you don't use USE_WORLD_COORDS
                # https://github.com/IfcOpenShell/IfcOpenShell/issues/1440
                matrix_data = np.array(shape.transformation.matrix.data).reshape((3,4)).T
                element = self.ifc_file.by_guid(shape.guid)
                if not element.is_a('IfcSite') and not element.is_a('IfcOpeningElement') :
                    faces = shape.geometry.faces # Indices of vertices per triangle face e.g. [f1v1, f1v2, f1v3, f2v1, f2v2, f2v3, ...]
                    verts = shape.geometry.verts # X Y Z of vertices in flattened list e.g. [v1x, v1y, v1z, v2x, v2y, v2z, ...]
                    materials = shape.geometry.materials # Material names and colour style information that are relevant to this shape
                    material_ids = shape.geometry.material_ids # Indices of material applied per triangle face e.g. [f1m, f2m, ...]
                    # Since the lists are flattened, you may prefer to group them per face like so depending on your geometry kernel
                    grouped_verts = [[verts[i], verts[i + 1], verts[i + 2]] for i in range(0, len(verts), 3)]
                    #print(grouped_verts)
                    grouped_faces = [[faces[i], faces[i + 1], faces[i + 2]] for i in range(0, len(faces), 3)]
                    # get color
                    rgb_color = [200,200,200]
                    if len(materials) == 1:
                        elementMaterial = materials[0]
                        if elementMaterial.hasDiffuse():
                            rgb_color = [math.floor(i * 255) for i in elementMaterial.diffuse]
                        elif elementMaterial.hasSpecular():
                            rgb_color = [math.floor(i * 255) for i in elementMaterial.specular]
                        else:pass
                    #
                    for idx_a, idx_b, idx_c in grouped_faces:
                        coorda = grouped_verts[idx_a]
                        coordb = grouped_verts[idx_b]
                        coordc = grouped_verts[idx_c]
                        dt_triangle.Rows.Add(*rgb_color, *coorda, *coordb, *coordc)
                        # 
                if not iterator.next():
                    break
        return dt_triangle

file_path = IN[0]
#
objIfc = IfcUtils(file_path)
dt_triangle = objIfc.ToGroupVerices()

OUT = dt_triangle

  • Traitement et Affichage des Géométries IFC.
Pour optimiser ce dernier processus, j'utilise ici du traitement parallèle (parallélisme) sur les lignes de la DataTable (via PLINQ). Cela est rendu possible avec le moteur IronPython2 qui l'avantage d'être .Net natif.

Pour rappel Revit ne supporte pas le multiprocessing

le code IronPython2 (avec parallélisme PLINQ)

import clr
import time
import System
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *
import  Autodesk.DesignScript.Geometry as DS

clr.AddReference('GeometryColor')
from Modifiers import GeometryColor
clr.AddReference('DSCoreNodes') 
import DSCore
from DSCore import Color as DSColor

clr.AddReference('System.Data')
clr.AddReference('System.Data.DataSetExtensions')
from System.Data import DataSet,DataTable,DataRow
from System.Data.DataTableExtensions import *
clr.ImportExtensions(System.Data.DataTableExtensions)

from System.Threading.Tasks import *
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
import traceback


def to_Surface(data_row):
	colorRGB, pta, ptb, ptc, plg = None, None, None, None, None
	try:
		ds_color = DSColor.ByARGB(255, data_row['r'], data_row['g'], data_row['b'])
		pta = DS.Point.ByCoordinates(data_row['ptaX'], data_row['ptaY'], data_row['ptaZ'])
		ptb = DS.Point.ByCoordinates(data_row['ptbX'], data_row['ptbY'], data_row['ptbZ'])
		ptc = DS.Point.ByCoordinates(data_row['ptcX'], data_row['ptcY'], data_row['ptcZ'])
		#
		plg = DS.Polygon.ByPoints([pta, ptb, ptc])
		surface = plg.Patch()
		pta.Dispose()
		ptb.Dispose()
		ptc.Dispose()
		plg.Dispose()
		return GeometryColor.ByGeometryColor(surface, ds_color)
	except Exception as ex:
		for i in [pta, ptb, ptc, plg]:
			if i is not None:
				i.Dispose()
		return None

dt_triangle = IN[0]
threadResult = dt_triangle.Rows.AsParallel()\
				.WithDegreeOfParallelism(System.Environment.ProcessorCount)\
				.Select(lambda l: to_Surface(l))\
				.ToList()
OUT = threadResult


🛈  Malgré toutes ces optimisations, un viewer IFC restera tout de même toujours plus rapide. 

Résultat en vidéo


alors CPython3 ou IronPython2 ? les deux, mon capitaine

À ce jour les 2 moteurs Python ont chacun des avantages, ils ne sont pas concurrents... 




3 commentaires:

  1. Hi Cyril, This is a great piece of art. Would it also be possible to import only geometry of a chosen IFC-category?

    RépondreSupprimer
    Réponses
    1. Ce commentaire a été supprimé par l'auteur.

      Supprimer
    2. Hi, replace this line

      if not element.is_a('IfcSite') and not element.is_a('IfcOpeningElement') :

      by

      if element.is_a() in ['IfcSlab', 'IfcDoor']:

      Supprimer