Skip to content

Merging two IFC files with different length units (mm and m) #1247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Krande opened this issue Jan 7, 2021 · 24 comments
Open

Merging two IFC files with different length units (mm and m) #1247

Krande opened this issue Jan 7, 2021 · 24 comments

Comments

@Krande
Copy link
Contributor

Krande commented Jan 7, 2021

Hi,

I am looking into merging ifc files with length units (millimeters and meters). My initial thought was to simply scale all length units for all physical elements of one of the ifc files with the appropriate scale factor (and of course change the length unit definition in the ifc) prior to merge.

Unfortunately I haven't found any existing methods or functions in IfcOpenshell that does this specifically, nor any specific examples for how to go about this in particular. However, I have found some related topics (#237, #1000) and a recipe that might be extended to do what I am after.

I guess ultimately my question is if there exists such a functionality already within ifcopenshell that does this?

If not, I could contribute with a recipe for this. My first thought would be to iterate all elements that have a IfcLengthMeasure attribute and scale with the correct scale factor (IfcCartesianPoint, geometry lengths, heights, etc..). Any hints on how to proceed (if this path is necessary) is as always much appreciated.

Best Regards
Kristoffer

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

In fact, I was just going to embark on this myself. I'm rewriting the BlenderBIM Add-on to incrementally edit the IFC as you change things in Blender instead of an export process, and so if the user togges the length unit in Blender, it also needs to push the change back to IFC... not a trivial task as you've guessed. If you solve it, I can reuse it in the BlenderBIM Add-on, both as an IfcPatch and also within the Add-on.

Have you seen this? https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.6.0/src/ifcopenshell-python/ifcopenshell/util/unit.py the convert function may be useful.

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

Theoretically although length units are the most noticable thing, it may be good to consider different area / volume units as well, which may require you to check things like property set and quantity set unit types... quite a nasty little issue.

@Krande
Copy link
Contributor Author

Krande commented Jan 7, 2021

Wow that was quick :) Yes, I was actually playing around with the code in unit.py! My first test was to see if I can somehow traverse all referenced attributes on a given IfcProduct and look for a IfcLengthMeasure and scale it. I agree it would definitively be worthwhile to add conversion of other quantity types.

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

Maybe instead of traversing, which maybe complex, how about this really dumb method instead?

import ifcopenshell
s = ifcopenshell.ifcopenshell_wrapper.schema_by_name("IFC4")
classes_to_modify = {}
for d in s.declarations():
    if not hasattr(d, "all_attributes") or "IfcLength" not in str(d.all_attributes()):
        continue
    attributes_to_modify = []
    for attribute in d.all_attributes():
        if "IfcLength" in str(attribute):
            attributes_to_modify.append(attribute.name())
    classes_to_modify[d.name()] = attributes_to_modify
    
print(classes_to_modify)

for ifc_class, attributes in classes_to_modify.items():
    for element in ifc_file.by_type(ifc_class):
        for attribute in attributes:
            pass # setattr(element, attribute, unit.convert ...

@Krande
Copy link
Contributor Author

Krande commented Jan 7, 2021

That's a great suggestion. Okay I have gotten this far based on your suggestion:

import ifcopenshell
from ifcopenshell.util.unit import get_prefix_multiplier, convert

ifc_file = ifcopenshell.open(ifc_file_path)


def calculate_unit_scale(file):
    units = file.by_type("IfcUnitAssignment")[0]
    unit_scale = 1
    for unit in units.Units:
        if not hasattr(unit, "UnitType") or unit.UnitType != "LENGTHUNIT":
            continue
        while unit.is_a("IfcConversionBasedUnit"):
            unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue
            unit = unit.ConversionFactor.UnitComponent
        if unit.is_a("IfcSIUnit"):
            unit_scale *= get_prefix_multiplier(unit.Prefix)
    return unit_scale


s = ifcopenshell.ifcopenshell_wrapper.schema_by_name("IFC4")
classes_to_modify = {}
for d in s.declarations():
    if not hasattr(d, "all_attributes") or "IfcLength" not in str(d.all_attributes()):
        continue
    attributes_to_modify = []
    for attribute in d.all_attributes():
        if "IfcLength" in str(attribute):
            attributes_to_modify.append(attribute.name())
    classes_to_modify[d.name()] = attributes_to_modify

print(classes_to_modify)

scale_factor = calculate_unit_scale(ifc_file)


def scale_all(obj, sf):
    def serialize(obj):
        """Recursively walk object's hierarchy."""
        if isinstance(obj, (int, float)):
            return obj * sf
        elif isinstance(obj, list):
            return [serialize(item) for item in obj]
        elif isinstance(obj, tuple):
            return tuple(serialize([item for item in obj]))
        else:
            try:
                if obj.is_a('IfcLengthMeasure') is True:
                    obj.wrappedValue = obj.wrappedValue * sf
                    return obj
                elif obj.is_a('IfcReal') is True:
                    obj.wrappedValue = obj.wrappedValue * sf
                    return obj
                elif obj.is_a('IfcInteger') is True:
                    obj.wrappedValue = int(obj.wrappedValue * sf)
                    return obj
                elif obj.is_a('IfcPlaneAngleMeasure') is True:
                    obj.wrappedValue = obj.wrappedValue * sf
                    return obj
                elif obj.is_a('IfcText') is True:
                    return obj
                elif obj.is_a('IfcLogical') is True:
                    return obj
            except:
                pass

            raise ValueError(f'Unknown entity "{type(obj)}", "{obj}"')

    return serialize(obj)


for ifc_class, attributes in classes_to_modify.items():
    for element in ifc_file.by_type(ifc_class):
        for attribute in attributes:

            old_val = getattr(element, attribute)
            if old_val is None:
                continue
            setattr(element, attribute, scale_all(old_val, scale_factor))
            new_val = getattr(element, attribute)

It worked on my particular IFC file, but I see that I would need to add a lot of conditions to catch all the IFC real, integer etc... Any tip for a more elegant solution? :)

@aothms
Copy link
Member

aothms commented Jan 7, 2021

In C++ the conversion factor should be applied for length units as long as both files have a length unit and you're copying from one to the other. https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.6.0/src/ifcparse/IfcParse.cpp#L1657

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

Instead of adding a lot of conditions, how about this dirty trick...

setattr(element, attribute, ifc_file.create_entity(old_val.is_a(), old_val * sf))

(Note: will bork on lists)

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

@aothms awesome! So I guess just create a new IfcOpenShell file object, add a length unit assignment, then add() all elements from one file to another?

@aothms
Copy link
Member

aothms commented Jan 7, 2021

Yes, but you're right, this should be extended for other units. Now IfcLengthMeasure and LENGTHUNIT are hardcoded, but it should be pretty straightforward to extend this to all < measures unit > pairs. Might be a nice issue to get familiar with C++ ;)

@Krande
Copy link
Contributor Author

Krande commented Jan 7, 2021

I do have a plan to jump into C++, so I would love to give it a go. But at the moment I'm a bit short on time. Maybe I can start to look at it in a couple of weeks if no one else has given it a go :)

@Moult
Copy link
Contributor

Moult commented Jan 7, 2021

In the middle of a big refactor now on the Python side, but if @Krande doesn't beat me to it, I'll investigate this too :)

@aothms
Copy link
Member

aothms commented Jan 9, 2021

Really cool, feel free to ping me any time once any of you start to have a go at this.

@Krande
Copy link
Contributor Author

Krande commented Feb 4, 2021

ping @aothms.

I have some time to dive into c++. I've already started tackling the local compilation of ifcopenshell on windows with the hope of at least getting a local dev setup. Any help or hints with setting up a good local development setup is as always much appreciated:)

@aothms
Copy link
Member

aothms commented Feb 5, 2021

Very cool.

I've already started tackling the local compilation of ifcopenshell on windows with the hope of at least getting a local dev setup. Any help or hints with setting up a good local development setup is as always much appreciated:)

For windows the build-deps run-cmake things in the win/ folder do result in a good development setup by default. You can install the Visual Studio Community Edition for free. The scripts were recently updated by @ahladik to support 2019 (you cannot download versions I think for the free Community Edition) and I think the paths changed a bit wrt to the documentation, but fundamentally they're still the same. Let me know if you're stuck somehow.

@Krande
Copy link
Contributor Author

Krande commented Feb 5, 2021

The build-deps step seemed to work just fine. However I did run into an error with "run-cmake" step though.

The error seems to be this:

 but it set boost_system_FOUND to FALSE so package "boost_system" is
  considered to be NOT FOUND.  Reason given by package:

  No suitable build variant has been found.

  The following variants have been tried and rejected:

  * boost_system.lib (shared, Boost_USE_STATIC_LIBS=ON)

It might be unrelated, but for some reason I see references to my system python interpreter (even though I build a new python in the build-deps step.

CMake Error at C:/Anaconda3_x64/Library/lib/cmake/Boost-1.74.0/BoostConfig.cmake:141 (find_package):
  Found package configuration file:

The full output is shown below

C:\code\krande-IfcOpenShell\win>run-cmake.bat

Generator not passed, but GEN_SHORTHAND=vs2019-x64 read from BuildDepsCache

GENERATOR:           ["Visual Studio 16 2019"]
VS_VER:              [2019]
VS_PLATFORM:         [x64]
VS_TOOLSET:          []
VC_VER:              [14.2]
ARCH_BITS:           [64]
TARGET_ARCH:         [x64]
BOOST_BOOTSTRAP_VER: [vc142]
BOOST_TOOLSET:       [msvc-14.2]
BOOST_WIN_API:       []

Script configuration:
  Generator    = "Visual Studio 16 2019"
  Architecture = x64
  Toolset      =
  Arguments    =

Dependency Environment Variables for IfcOpenShell:
   BOOST_ROOT              = C:\code\krande-IfcOpenShell\_deps\boost_1_74_0
   BOOST_LIBRARYDIR        = C:\code\krande-IfcOpenShell\_deps\boost_1_74_0\stage\vs2019-x64\lib
   OCC_INCLUDE_DIR         = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\inc
   OCC_LIBRARY_DIR         = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib
   OPENCOLLADA_INCLUDE_DIR = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\OpenCOLLADA\include\opencollada
   OPENCOLLADA_LIBRARY_DIR = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\OpenCOLLADA\lib\opencollada
   LIBXML2_INCLUDE_DIR     = C:\code\krande-IfcOpenShell\_deps\OpenCOLLADA\Externals\LibXML\include
   LIBXML2_LIBRARIES       = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\OpenCOLLADA\lib\opencollada\xml.lib
   PYTHONHOME              = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\Python34
   PYTHON_INCLUDE_DIR      = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\Python34\include
   PYTHON_LIBRARY          = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\Python34\libs\python34.lib
   PYTHON_EXECUTABLE       = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\Python34\python.exe
   SWIG_DIR                = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\swigwin
   JSON_INCLUDE_DIR        = C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\json

   CMAKE_INSTALL_PREFIX    = C:\code\krande-IfcOpenShell\installed-vs2019-x64

   
Running CMake for IfcOpenShell.
-- BINDIR: C:/code/krande-IfcOpenShell/installed-vs2019-x64/bin
-- INCLUDEDIR: C:/code/krande-IfcOpenShell/installed-vs2019-x64/include
-- LIBDIR: C:/code/krande-IfcOpenShell/installed-vs2019-x64/lib
CMake Warning (dev) at CMakeLists.txt:180 (FIND_PACKAGE):
  Policy CMP0074 is not set: find_package uses <PackageName>_ROOT variables.
  Run "cmake --help-policy CMP0074" for policy details.  Use the cmake_policy
  command to set the policy and suppress this warning.

  Environment variable Boost_ROOT is set to:

    C:\code\krande-IfcOpenShell\_deps\boost_1_74_0

  For compatibility, CMake is ignoring the variable.
This warning is for project developers.  Use -Wno-dev to suppress it.

CMake Error at C:/Anaconda3_x64/Library/lib/cmake/Boost-1.74.0/BoostConfig.cmake:141 (find_package):
  Found package configuration file:

    C:/Anaconda3_x64/Library/lib/cmake/boost_system-1.74.0/boost_system-config.cmake

  but it set boost_system_FOUND to FALSE so package "boost_system" is
  considered to be NOT FOUND.  Reason given by package:

  No suitable build variant has been found.

  The following variants have been tried and rejected:

  * boost_system.lib (shared, Boost_USE_STATIC_LIBS=ON)

Call Stack (most recent call first):
  C:/Anaconda3_x64/Library/lib/cmake/Boost-1.74.0/BoostConfig.cmake:258 (boost_find_component)
  C:/Program Files/CMake/share/cmake-3.19/Modules/FindBoost.cmake:460 (find_package)
  CMakeLists.txt:180 (FIND_PACKAGE)


-- Configuring incomplete, errors occurred!
See also "C:/code/krande-IfcOpenShell/_build-vs2019-x64/CMakeFiles/CMakeOutput.log".

An error occurred

I see there has been a lot of recent activity on the windows build process, so I am trying to read through the various issues to see if I can catch something that might help. But if you have any suggestions I would appreciate any help :)

Best Regards
Kristoffer

@aothms
Copy link
Member

aothms commented Feb 5, 2021

I think it's the same as #1273 (but that only addressed it for the nix build script). Can you try and add that same -DBoost_NO_BOOST_CMAKE=On to run-cmake and try again (please submit a PR if it works :))

@Krande
Copy link
Contributor Author

Krande commented Feb 5, 2021

Using run-cmake.bat vs2019-x64 -DBoost_NO_BOOST_CMAKE=On worked!

I thought I would start by working my way through the c++ example files and just debug and step through some of the more basic operations to get a feel for how everything is tied together. However, when trying to build, I ran into this:

Severity	Code	Description	Project	File	Line	Suppression State
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\OpenCOLLADA\lib\opencollada\xmld.lib'	IfcParseExamples	C:\code\krande-IfcOpenShell\_build-vs2019-x64\examples\LINK	1	
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib\TKerneld.lib'	IfcOpenHouse	C:\code\krande-IfcOpenShell\_build-vs2019-x64\examples\LINK	1	
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib\TKerneld.lib'	IfcAdvancedHouse	C:\code\krande-IfcOpenShell\_build-vs2019-x64\examples\LINK	1	
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib\TKerneld.lib'	IfcGeomServer	C:\code\krande-IfcOpenShell\_build-vs2019-x64\LINK	1	
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib\TKerneld.lib'	_ifcopenshell_wrapper	C:\code\krande-IfcOpenShell\_build-vs2019-x64\ifcwrap\LINK	1	
Error	LNK1104	cannot open file 'C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\opencascade-7.3.0p3\win64\lib\TKerneld.lib'	IfcConvert	C:\code\krande-IfcOpenShell\_build-vs2019-x64\LINK	1	

Any ideas?

I noticed it looks for xmld.lib in C:\code\krande-IfcOpenShell\_deps-vs2019-x64-installed\OpenCOLLADA\lib\opencollada\ but when I look in that folder I find xml.lib which is close, but not quite right :).

Another thing I am a bit curious about (and I guess this question is more of a general c++\python type of question) is if it is possible to attach and debug a running instance of python using ifcopenshell and step through portion of the c++ code that seemingly generates segmentation errors? Or if I have to recreate the examples in c++ and debug in "pure c++"?

Anyways, thanks for the help so far!

Best Regards
Kristoffer

@aothms
Copy link
Member

aothms commented Feb 5, 2021

-DBoost_NO_BOOST_CMAKE=On worked!

Thanks, I'll add it to run-cmake then.

I ran into this:

I think the default of the build script is RelWithDebInfo, but Microsoft VS will always open a solution in Debug mode. So you need to switch that to RelWithDebInfo to be consistent with the built libraries (the _d prefix is for debug).

is possible to attach and debug a running instance of python using ifcopenshell and step through portion of the c++ code that seemingly generates segmentation errors?

yes this is what I tend to do as well. Make sure that the .pyd and .pdb (msvc debugging archive) match and are both in the ifcopenshell py module directory and it matches the msvc solution you have open. Then in msvc process [Ctrl] [Alt] [P] for attach to process (it's also somewhere in the menus) and select your python process. Then you can simply set a breakpoint in the code.

@Maozerhouni
Copy link

Did you find a satisfying solution ?
How do you solve the cases where the attribute can be IfcLengthMeasure (but is not necessarily IfcLengthMeasure). (Case is for IfcMetric). From what I am understanding in the code above, your code will still scale whatever value it is provided, eventhough it is not an ifcLengthMeasure

@aothms
Copy link
Member

aothms commented Dec 29, 2022

can be IfcLengthMeasure (but is not necessarily IfcLengthMeasure)

I assume you mean so called select types. Like IfcMetricValueSelect/IfcValue. I assume they are handled automatically on the ifcopenshell.file level. In IfcOpenShell they are basically the same as instances with 1 single attribute called wrappedValue. The code above does seem to have some assumptions you need to adjust for your own case.

@Maozerhouni
Copy link

Maozerhouni commented Dec 29, 2022

I tried to generalize what have been said and done before, to get a global conversion, for all measurement + point the assumptions.

Classes concerned by measurement can be calculated one per schema. I thought it would be better to just write the resulting dictionnary instead of recalculating it each time.

I tried to minimize file parsing by only parsing classes that are actually concerned by resizing.

It seems to work.

If there are test IFCs (especially ones using non-geometrical entities), I would be glad to test them

PS : I am having trouble copying the code here. How can I do so ?
MergeIfcModels.txt

@aothms
Copy link
Member

aothms commented Jan 28, 2023

@Maozerhouni this looks really cool. Sorry for the delay in replying. The best way to "copying" the code is to submit a Pull Request so that the code gets bundled with IfcOpenShell. Would you be so kind?

In later versions of ifcopenshell some of the directionaries are not strictly necessary anymore.

"IFC2X3": {
'IfcAsymmetricIShapeProfileDef': [('OverallWidth', 'IfcLengthMeasure'),

>>> ifcopenshell.ifcopenshell_wrapper.schema_by_name('IFC2X3').declaration_by_name('IfcAsymmetricIShapeProfileDef').attributes()[0].type_of_attribute().declared_type().declared_type().declared_type().name()
'IfcLengthMeasure'

But a pull request in the current form is also already a good addition to the library :)

@Moult
Copy link
Contributor

Moult commented Feb 23, 2023

Bump @Maozerhouni

@Maozerhouni
Copy link

Hello, sorry for the late answer
yes I will try to merge it and then improve it.
I am not very familiar with Git, so I will try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants