Modifiying IFCCARTESIANPOINT with delta from initial point and base point and rewriting a large IFC file

Hi,

I’ve managed to create a quick Dynamo graph with a python script but i’m bugged now

Some IFC files can have large amounts of text lines and can take up long time to rewrite the text file.

Anyone has an idea how i could optimize my process and it takes less time ?

It’s a big killer for deploy that .Dyn the time it takes

import clr
import sys

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager 

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *

def replace_ifc_cartesian_point(ifc_text, offset_text):
    result_lines = []
    found = False
    
    for line in ifc_text.splitlines():
        if not found and "IFCCARTESIANPOINT" in line:
            parts = line.split("=")
            if len(parts) == 2 and "IFCCARTESIANPOINT" in parts[1]:
                result_lines.append(f"{parts[0].strip()}= IFCCARTESIANPOINT(({offset_text}));")
                found = True
            else:
                result_lines.append(line)
        else:
            result_lines.append(line)
    
    return "\n".join(result_lines)

# Example usage:
ifc_text = IN[0]
doc = DocumentManager.Instance.CurrentDBDocument

bp = BasePoint.GetProjectBasePoint(doc).SharedPosition
io = InternalOrigin.Get(doc).SharedPosition

delta_x = round((bp[0] - io[0]) * 0.3048, 3)
delta_y = round((bp[1] - io[1]) * 0.3048, 3)

if delta_x == 0 and delta_y == 0:
    result = ifc_text
else:
    offset_text = f"{delta_x},{delta_y},0.0"
    print(offset_text)
    result = replace_ifc_cartesian_point(ifc_text, offset_text)

OUT = result

As of now i think doing a massive search and replace in Notepad is much quicker (Dyn take more than 15 minutes - Notepad takes 30 secondes)

Instead of File Path 1 > FileSystem.ReadText > Python > FileSystem.WriteText, why not just go File Path 1 > Python?

The later is more similar to what your notepad method does. Currently you are opening the entire file and putting it to memory (first node), copying it to memory again in the Python node, moving it to memory again by splitting the single string into lines, copying one line at a time into memory to edit them, copying each of the lines into memory again to put them in the results list, then copying it to memory again as one string, then copying the final string to the Dynamo environment, passing the string into memory for the watch node, passing the display of all that text into memory, and finally moving the string into the written text node.

Instead open the file with Python, read the stream as lines, and edit the line if the magic string is present, and then write the edited stream to disc. Takes the RAM from far more than can be expected to a single action composed using a tool which is built with speedy string manipulation as a primary use instead of one built for low entry computational design.

1 Like

Hi @jacob.small

Ill give it dive and redo my approach by rewriting it in python directly.

Sometimes you loose persepective while writing and especially when your starting to have fun in programming

Will keep you posted

1 Like

Wow that update make the graph run like it’s day and night.

Took less than a second to generate :

import clr
import sys
import gc

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager 

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *

def replace_ifc_cartesian_point(ifc_text, offset_text):
    result_lines = []
    found = False
    lines = ifc_text.splitlines()
    
    for i, line in enumerate(lines):
        if not found and "IFCCARTESIANPOINT" in line:
            parts = line.split("=")
            if len(parts) == 2 and "IFCCARTESIANPOINT" in parts[1]:
                result_lines.append(f"{parts[0].strip()}= IFCCARTESIANPOINT(({offset_text}));")
                found = True
                # Append the remaining lines
                result_lines.extend(lines[i+1:])
                break
        else:
            result_lines.append(line)
    
    return "\n".join(result_lines)

# Example usage:
input_filepath = IN[0]  # Path to the input IFC file
output_filepath = IN[1]  # Path to the output IFC file

# Read the IFC file
with open(input_filepath, 'r') as file:
    ifc_text = file.read()

# Get document information from Revit
doc = DocumentManager.Instance.CurrentDBDocument

bp = BasePoint.GetProjectBasePoint(doc).SharedPosition
io = InternalOrigin.Get(doc).SharedPosition

delta_x = round((bp[0] - io[0]) * 0.3048, 3)
delta_y = round((bp[1] - io[1]) * 0.3048, 3)

if delta_x == 0 and delta_y == 0:
    result = ifc_text
else:
    offset_text = f"{delta_x},{delta_y},0.0"
    print(offset_text)
    result = replace_ifc_cartesian_point(ifc_text, offset_text)

# Write the result to the output file
with open(output_filepath, 'w') as file:
    file.write(result)

# Clear cache memory by forcing garbage collection
gc.collect()

OUT = "IFC file processed and saved to " + output_filepath

1 Like

Great work!

Next up: display those points in the Dynamo environment. :smiley:

1 Like

Oh and how can i complete that ? haha

You could do this by reading into memory with file.readlines() then updating in-place using a regular expression substitution and then writing out.
Since you’re using CPython3 pathlib is a handy way to do file operations.

import re
from pathlib import Path

in_file = Path("C:/temp/Snowdon Towers Sample Architectural.ifc") # or IN[0]
out_file = Path("C:/temp/Snowdon Towers Sample Architectural_update.ifc") # or IN[1]

update = "123.,456.,789."  # Example

# Regular expression to find coordinates in IFC format
# \d+ is any number of digits (at least one)
# \. a decimal point
# (\d+)? optional decimals
coords = re.compile("\d+\.(\d+)?,\d+\.(\d+)?,\d+\.(\d+)?")

with in_file.open(mode="r") as f_in:
    data = f_in.readlines()
    for i, line in enumerate(data):
        if "IFCCARTESIANPOINT" in line:
            newline = coords.sub(update, line) # Substitute update for matched text 
            data[i] = newline  # Replace in data
            break

with out_file.open(mode="w") as f_out:
    f_out.writelines(data)
1 Like

Hi,

for info, you can use ifcopenshell and this utility class

here is an example of adaptation

# IfcPatch Patcher Modified by Cyril POUPIN
#
# IfcPatch - IFC patching utiliy
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcPatch.
# https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.6.0/src/ifcpatch/ifcpatch/recipes/OffsetObjectPlacements.py
#
# IfcPatch is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcPatch is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.

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

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 ifcopenshell
import math

class Patcher:
    def __init__(self, file, outfile, args=None):
        self.outfile = outfile
        self.file = ifcopenshell.open(file)
        global_unit_assignments = self.file.by_type("IfcUnitAssignment")
        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]
        self.args = args
        if global_length_unit_definition.Prefix == "MILLI":
            for i in range(3):
                self.args[i] *= 1000

    def patch(self):
        absolute_placements = []

        for product in self.file.by_type('IfcProduct'):
            if not product.ObjectPlacement:
                continue
            absolute_placement = self.get_absolute_placement(product.ObjectPlacement)
            if absolute_placement.is_a('IfcLocalPlacement'):
                absolute_placements.append(absolute_placement)
        absolute_placements = set(absolute_placements)

        for placement in absolute_placements:
            offset_location = (
                placement.RelativePlacement.Location.Coordinates[0] + float(self.args[0]),
                placement.RelativePlacement.Location.Coordinates[1] + float(self.args[1]),
                placement.RelativePlacement.Location.Coordinates[2] + float(self.args[2])
            )

            relative_placement = self.file.createIfcAxis2Placement3D(
                self.file.createIfcCartesianPoint(offset_location))

            if placement.RelativePlacement.Axis:
                relative_placement.Axis = placement.RelativePlacement.Axis
            if placement.RelativePlacement.RefDirection:
                relative_placement.RefDirection = placement.RelativePlacement.RefDirection

            angle = float(self.args[3])
            if not angle:
                placement.RelativePlacement = relative_placement
                continue

            rotation_matrix = self.z_rotation_matrix(math.radians(angle))
            relative_placement.Location.Coordinates = self.multiply_by_matrix(offset_location, rotation_matrix)

            if placement.RelativePlacement.Axis:
                z_axis = placement.RelativePlacement.Axis.DirectionRatios
                relative_placement.Axis = self.file.createIfcDirection(
                    self.multiply_by_matrix(z_axis, rotation_matrix))

            if placement.RelativePlacement.RefDirection:
                x_axis = placement.RelativePlacement.RefDirection.DirectionRatios
            else:
                x_axis = (1., 0., 0.)
            relative_placement.RefDirection = self.file.createIfcDirection(
                self.multiply_by_matrix(x_axis, rotation_matrix))

            placement.RelativePlacement = relative_placement
        self.file.write(self.outfile)

    def get_absolute_placement(self, object_placement):
        if object_placement.PlacementRelTo:
            return self.get_absolute_placement(object_placement.PlacementRelTo)
        return object_placement

    def z_rotation_matrix(self, angle):
        return [
            [math.cos(angle), -math.sin(angle), 0.],
            [math.sin(angle), math.cos(angle), 0.],
            [0., 0., 1.]
        ]

    def multiply_by_matrix(self, v, m):
        return [
            v[0]*m[0][0] + v[1]*m[0][1] + v[2]*m[0][2],
            v[0]*m[1][0] + v[1]*m[1][1] + v[2]*m[1][2],
            v[0]*m[2][0] + v[1]*m[2][1] + v[2]*m[2][2]
        ]


input = r"D:\Project Example 2024\IFC\CLOV_LAU_EXE_CME_B1_SOL.ifc"
output = r"D:\Project Example 2024\IFC\CLOV_LAU_EXE_CME_B1_SOLv4.ifc"

# transform coordinates in meters and degre [X, Y, Z, Angle]
arguments = [-10, -10, 0, 0]

o = Patcher(input, output, args=arguments)
o.patch()
2 Likes