Closed26

IFCのデータ構造を理解したい

kiyukakiyuka

IFCファイルのデータ構造がよくわからないので、IfcOpenShellとかIFC.jsを使ってどういう作りになっているのかを解析したい。その自分用のメモ書き。多分ある程度理解したところでまとめる。

あとスクラップ初めて使うので色々試したい。

kiyukakiyuka

IFCはデータ的にはおそらくIfcProjectがトップにあって、そこに建物情報とかがぶら下がっているのだと思う。IFC.jsのドキュメントを見ての推測。(IFC仕様のドキュメントのどこを見たらそういうのがわかるのかわからない...)
ただ、IfcProjectのtreeにぶら下がっているのではなくて、IfcRelAggregatesとかIfcRelContainedInSpatialStructureとかのリレーションデータと合わせて見る必要がありそう。これもIFC.js(web-ifc-three)のソースコードを読んでの推測。

ktaroabobonktaroabobon

IFCのデータ構造はIfcRootがトップにあり、その下にIfcObjectDefinition(IfcWallやIfcColumnなど)、IfcPropertyDefinition(オブジェクトの属性情報)、IfcRelationship(オブジェクトや属性情報の関係を表す情報)があります。また、そこからさらに...という形で階層構造になっています。

以下はIFCの公式ドキュメントのリンクです!

kiyukakiyuka

IFCの各クラス?(正しい呼び名がわからない...Ifc~のことを勝手にIFCクラスと言っている)のトップはIfcRootですね。PythonでいうObjectクラスに当たるものと言うか、すべてを包含するクラスというか。
ただ、ここで言いたかったトップと言うのは、IFCファイル的に一番上というか、ファイルを作るときに最初に作られるというか、そういう位置づけにくるものがIfcProjectだということが言いたかったんです(説明下手くそ)。

あと、正しいドキュメントのリンクありがとうございます!!
(と言うかどこからdevelopmentのドキュメント持ってきていたんだろうか...?)

ktaroabobonktaroabobon

あ、なるほどです~

あと、IFCでのクラスみたいなのはエンティティと呼ばれてます!

kiyukakiyuka

なるほど!エンティティなんですね。ありがとうございます!
ドキュメントの Formal representation の記述とかがDBっぽいなーとはなんとなく思ってましたが納得できました。

kiyukakiyuka

リレーションはデータ同士の関係を結びつけるもの。web-ifc-threeのPropertyManagerで使用していそうなのはこの5つ。

  • IfcRelAggregates
    • 構造の関係情報。建物には1階と2階があるとか、壁はXXとYYで構成されているとか
  • IfcRelContainedInSpatialStructure
    • 建物の特定の階層と関連付けられている構造物。1階には家具がある、のような
  • IfcRelDefinesByProperties
  • IfcRelAssociatesMaterial
  • IfcRelDefinesByType

web-ifc-viewerのサンプルにあるspatial-treeのviewer.IFC.getSpatialStructureを見ると
IfcProjectを取得してそのリレーションのIfcRelAggregates、IfcRelContainedInSpatialStructureからtreeを作成しているっぽい。

kiyukakiyuka

IfcRelDefinesByTypeは、IfcSlabとIfcSlabTypeなどのIfcElementとIfcElementTypeの紐づけをしている。IfcRelDefinesByPropertiesはプロパティ(なんだこれ?)のひもづけ、IfcRelAssociatesMaterialはマテリアルとのひもづけをしているっぽい。
そのため建物のある部材に関する情報を取得したい場合、このあたりのリレーション情報も参照して取得する必要がありそう。

web-ifc-viewerだとviewer.getPropertiesでもろもろ取得できそう(確認してない)。
内部的には IFCLoader.js の WebIfcPropertyManagergetPropertySets, getTypeProperties, getMaterialsProperties で取得している。

kiyukakiyuka

IFCの構造理解するためならIFC.jsよりIfcOpenShellのほうがいい気がしてきた。前に調べたときはIfcOpenShellのドキュメントが出てこなかったからIFC.jsを先に使ってたけど、今調べたら普通に見つかった。
IfcOpenShellドキュメント

あとこことかこことかも参考にしつつ、色々使い方とか調べてみる。

kiyukakiyuka

IfcOpenShellの by_type は指定したクラスを取得するわけではないらしい?
IfcProductのサブクラスを取得しないようにするとからの配列が返ってくる。
IfcProduct のときだけリレーション取得しているっぽいからこれだけ違う処理してる感じなのかな?

import ifcopenshell
ifc = ifcopenshell.open('01.ifc')
ifc.by_type('IfcProduct', include_subtypes=False) # []

違うわ。IfcProjectを取得しようとしたんだよ。IfcProductは親クラスのアブストラクトだからそりゃサブクラス取得しないようにしたら、からになるよ...
IfcProjectを指定したらちゃんと取得できました。

kiyukakiyuka

IfcOpenShellは内部で何やってるかわかんないなこれ。というか処理はC++で書かれているので読みたくない。ドキュメントも触り部分くらいしか書かれてないっぽいし、IFCの構造を理解するには使いづらいな。

ひとまずBIMVisionで表示される内容がどこから取ってきているのかを見ようと思う。

使うデータはここから
https://github.com/IFCjs/test-ifc-files

kiyukakiyuka

rootのIfcProjectをまず見る。

FileHeaderの部分はIfcOpenShellで取ってこれるのかわからなかったけど、ファイルのヘッダー部分に記載されている。

ElementSpecificの部分は下記で情報を取れる。

product = model.by_type('IfcProject')[0]
product.get_info()
# {'id': 119,
#  'type': 'IfcProject',
#  'GlobalId': '0w984V0GL6yR4z75XVLWOq',
#  'OwnerHistory': #41=IfcOwnerHistory(#38,#5,$,.NOCHANGE.,$,$,$,1606175882),
#  'Name': '0001',
#  'Description': None,
#  'ObjectType': None,
#  'LongName': 'Nombre de proyecto',
#  'Phase': 'Estado de proyecto',
#  'RepresentationContexts': (#111=IfcGeometricRepresentationContext($,'Model',3,1.00000000000000E-5,#108,#109),),
#  'UnitsInContext': #106=IfcUnitAssignment((#42,#43,#44,#48,#49,#52,#55,#57,#58,#60,#64,#69,#71,#72,#73,#74,#75,#76,#77,#82,#86,#88,#92,#98,#104))}
kiyukakiyuka

Site

取得できてそう。

site = model.by_type('IfcSite')[0]
site.get_info()
# {'id': 148,
#  'type': 'IfcSite',
#  'GlobalId': '0w984V0GL6yR4z75XVLWOs',
#  'OwnerHistory': #41=IfcOwnerHistory(#38,#5,$,.NOCHANGE.,$,$,$,1606175882),
#  'Name': 'Default',
#  'Description': None,
#  'ObjectType': None,
#  'ObjectPlacement': #147=IfcLocalPlacement($,#146),
#  'Representation': None,
#  'LongName': None,
#  'CompositionType': 'ELEMENT',
#  'RefLatitude': (40, 25, 13, 9643),
#  'RefLongitude': (-3, -42, -20, -772056),
#  'RefElevation': 0.0,
#  'LandTitleNumber': None,
#  'SiteAddress': None}

ところでIfcOpenShellで階層構造取ってくるのどうやるんだろう? get_inverse で頑張って取得するしかないんだろうか。

kiyukakiyuka

Locationタブの情報はSiteから取得したデータを表示しているのではなくて、下の階層のBuilding以下からのデータから計算したものを表示しているっぽい。たぶん。

kiyukakiyuka

Buildingは下記で取得できた。
基本的に get_infoifcopenshell.util.element.get_psets で各種情報は取得できる感じ?

ifcopenshell.util.element.get_psetsIfcPropertySet で紐づけられている情報を取得している感じかな

building = model.by_type('IfcBuilding')[0]
building.get_info()
# {'id': 129,
#  'type': 'IfcBuilding',
#  'GlobalId': '0w984V0GL6yR4z75XVLWOr',
#  'OwnerHistory': #41=IfcOwnerHistory(#38,#5,$,.NOCHANGE.,$,$,$,1606175882),
#  'Name': '',
#  'Description': None,
#  'ObjectType': None,
#  'ObjectPlacement': #32=IfcLocalPlacement(#147,#31),
#  'Representation': None,
#  'LongName': '',
#  'CompositionType': 'ELEMENT',
#  'ElevationOfRefHeight': None,
#  'ElevationOfTerrain': None,
#  'BuildingAddress': #125=IfcPostalAddress($,$,$,$,('Introduzca dirección aquí'),$,'Madrid','Community','of Madrid','Spain')}

ifcopenshell.util.element.get_psets(building)
# {'Pset_BuildingCommon': {'NumberOfStoreys': 2,
#  'IsLandmarked': 'UNKNOWN',
#  'id': 24544}}
kiyukakiyuka

IfcWall を見てるんだけどなんとなく構造がわかってきたような。

基本的な情報は IfcWallの属性情報からたどると取得できる。IfcOpenShellであればget_infoで取得できる情報がそれにあたる。

walls = model.by_type('IfcWall')
wall = walls[0]
wall.get_info()
# {'id': 186,
#  'type': 'IfcWallStandardCase',
#  'GlobalId': '2idC0G3ezCdhA9WVjWemc$',
#  'OwnerHistory': #41=IfcOwnerHistory(#38,#5,$,.NOCHANGE.,$,$,$,1606175882),
#  'Name': 'Muro básico:Partición con capa de yeso:163541',
#  'Description': None,
#  'ObjectType': 'Muro básico:Partición con capa de yeso',
#  'ObjectPlacement': #155=IfcLocalPlacement(#136,#154),
#  'Representation': #182=IfcProductDefinitionShape($,$,(#161,#179)),
#  'Tag': '163541'}

ただ、それでわかるのはtree構造になっている表面のみなので、形状情報は属性情報の ObjectPlacement, Representation から階層を潜る必要がある。IfcOpenShellであればtraverseを使うとtree構造以下の情報を取得できる。(ただしtreeとしては取得してくれずフラットな配列で取得される。)

model.traverse(wall, max_levels=None)

それとは別に付属している情報(共通してかならずあるわけではない情報)は、IfcRel~~ のクラスからたどることで取得できる。
たとえば IfcPropertySet であれば IfcWall と紐づけるために IfcRelDefinesByProperties が間に入って関連付けを行っている(IfcWallIfcRelDefinesByPropertiesIfcPropertySet)。IfcOpenShell であれば ifcopenshell.util.element.get_psets がおそらくそれに当たる。

ifcopenshell.util.element.get_psets(wall)
# {'Pset_QuantityTakeOff': {'Reference': 'Partición con capa de yeso',
#   'id': 250},
#  'Pset_ReinforcementBarPitchOfWall': {'Reference': 'Partición con capa de yeso',
#   'id': 253},
#  'Pset_WallCommon': {'Reference': 'Partición con capa de yeso',
#   'IsExternal': False,
#   'id': 257,
#   'ExtendToStructure': False,
#   'LoadBearing': False}}

それ以外の IfcRel~~ を取得するのは get_inverse を使用すればできる(IfcRel以外も取得されるけど)。

model.get_inverse(wall)
# {#24466=IfcRelContainedInSpatialStructure('29TFpHBYXAnw12ni$37b4C',#41,$,$,(#186,#294,#338,#6518,#6563,#6595,#6627,#6659,#6691,#6723,#6755,#6787,#7792,#18774,#18799,#18819,#18839,#18859,#18879,#18899,#18919,#18939,#18959,#18979,#18999,#19019,#19039,#19059,#19079,#19099,#19119,#19139,#19159,#19179,#19199,#19219,#19239,#19259,#19279,#19299,#19319,#19339,#19359,#19379,#19399,#19419,#19439,#22492,#22551,#22655),#138),
#  #24550=IfcRelAssociatesMaterial('0iid6qgCHFVBHUJh6LLQoC',#41,$,$,(#186),#231),
#  #24754=IfcRelDefinesByType('16JJBpj4rDy8X3VIaBPrGe',#41,$,$,(#186,#294,#338),#232),
#  #24835=IfcRelConnectsPathElements('3cvoYjiYL8MQrc9LqoS_uf',#41,'2idC0G3ezCdhA9WVjWemc$|2idC0G3ezCdhA9WVjWemcy','Structural',$,#186,#294,(),(),.ATEND.,.ATSTART.),
#  #24850=IfcRelConnectsPathElements('3Lsua5Ym94OPPHKLJtppMB',#41,'2idC0G3ezCdhA9WVjWemc$|1s5utE$rDDfRKgzV6jUJ1G','Structural',$,#186,#22655,(),(),.ATEND.,.ATPATH.),
#  #24873=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$V2',#41,$,$,#186,#24868),
#  #24900=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$Pp',#41,$,$,#186,#24897),
#  #24926=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$PM',#41,$,$,#186,#24923),
#  #24952=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$O_',#41,$,$,#186,#24949),
#  #24978=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$O$',#41,$,$,#186,#24975),
#  #25004=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$OA',#41,$,$,#186,#25001),
#  #25030=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$OB',#41,$,$,#186,#25027),
#  #25056=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$O8',#41,$,$,#186,#25053),
#  #25082=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$O9',#41,$,$,#186,#25079),
#  #259=IfcRelDefinesByProperties('31l5wO6B5EtxzDnt2ExlHU',#41,$,$,(#186),#250),
#  #263=IfcRelDefinesByProperties('0lTpqqly56Th0lbuXkrcRj',#41,$,$,(#186),#253),
#  #266=IfcRelDefinesByProperties('2mk5om_obBp8gFwoZvbuBD',#41,$,$,(#186),#257)}
kiyukakiyuka

ちなみに壁の窓部分などの穴の空いた部分の空間形状は IfcWall には情報としてはなくて、IfcRelVoidsElement -> IfcOpeningElement として別に定義されているよう。
基本的に壁はシンプルな平たい直方体で、ドアとか窓とかでその中になにか物体があるなら別で定義する形にしているらしい。

ちなみにデータとしては下記のように紐づいている。(IfcRelVoidsElementが #186,#24868 を紐づけ)

#186=IfcWallStandardCase('2idC0G3ezCdhA9WVjWemc$',#41,'Muro básico:Partición con capa de yeso:163541',$,'Muro básico:Partición con capa de yeso',#155,#182,'163541')
#24873=IfcRelVoidsElement('2idC0G3ezCdhA9WUzWe$V2',#41,$,$,#186,#24868)
#24868=IfcOpeningElement('2idC0G3ezCdhA9WUXWe$V2',#41,'Ventana simple:100 x 100 cm:164008:1',$,'Opening',#24866,#24861,'164008')
kiyukakiyuka

ドキュメントの Attributes の数字のついてない項目ってIFCファイル内に記載もないしなんだろうと思ってたんだけど、Relationで紐づけされる項目だったのか。

kiyukakiyuka

IfcOpenSellでも属性情報として取得できるようになってる。

project = model.by_type('IfcProject')[0]
dir(project)
# ['Decomposes',
#  'Description',
#  'GlobalId',
#  'HasAssignments',
#  'HasAssociations',
#  'IsDecomposedBy',
#  'IsDefinedBy',
#  'LongName',
#  'Name',
#  'ObjectType',
#  'OwnerHistory',
#  'Phase',
#  'RepresentationContexts',
#  'UnitsInContext',
#  ...

project.IsDecomposedBy
# (#24529=IfcRelAggregates('0NKZLLXJr0zxVju2bOK15M',#41,$,$,#119,(#148)),)
kiyukakiyuka

IfcProjectからリレーションと子要素をたどれば、IFCの全行取得できるのかなと思ったけど余った。
余ったんだけど、余った行をIFCファイルから取り除いてBIMVisonで開いたらエラー出ずに開けた。
単純に定数要素で使用されてない感じかな?
使用されてない要素は、IfcDirection, IfcSurfaceStyle, IfcMaterialDefinitionRepresentation, IfcColourRgb, IfcPresentationLayerAssignment とかだしそんなな気がする。
ただ、IfcPresentationLayerAssignment はレイヤーだから単純にリレーションとかで紐づけされてないだけな気がする。

ところでなんで IfcColourRgbはカラーがイギリス英語なの?

kiyukakiyuka

ちなみにIfcProjectからリレーションと子要素をたどるのに使用したコード

# リレーション一覧
# entity_instance => { related: [], relating: [] }
relationmap = defaultdict(lambda: dict(related=[], relating=[]))
relationship = model.by_type('IfcRelationship')
for rel in relationship:
    for attstr in dir(rel):
        # IfcRelAssigns の非推奨要素は一応除く
        if attstr.startswith('Related') and attstr != 'RelatedObjectsType':
            att = getattr(rel, attstr)
            if isinstance(att, tuple):
                for at in att:
                    relationmap[at]['related'].append(rel) 
            else:
                relationmap[att]['related'].append(rel) 
            
        if attstr.startswith('Relating'):
            att = getattr(rel, attstr)
            if isinstance(att, tuple):
                for at in att:
                    relationmap[at]['relating'].append(rel) 
            else:
                relationmap[att]['relating'].append(rel) 
# 全行取得(もうちょっとかしこい方法ないの?)
lines = []
for i in range(model.wrapped_data.getMaxId() + 1):
    try:
        line = model.by_id(i)
        if line.is_a() == 'IfcProject':
            project = line
        lines.append(line)
    except:
        pass
def get_element(item):
    # project から子要素取得
    children = []
    for k, v in item.get_info().items():
        if isinstance(v, ifcopenshell.entity_instance):
            children.append(v)
        elif isinstance(v, tuple) and len(v) > 0:
            if isinstance(v[0], ifcopenshell.entity_instance):
                children += v

    element = dict(
        children=children,
        **relationmap[item],
    )
    return element
# IfcProject から順に要素をたどる
ids = {}
for line in lines:
    ids[line] = False

element = get_element(project)
ids[project] = element

queue_element = [element]
while len(queue_element) > 0:
    element = queue_element.pop(0)
    print(element)
    for item in element['children'] + element['related'] + element['relating']:
        if ids.get(item): continue
        element = get_element(item)
        ids[item] = element
        queue_element.append(element)

# 取得できなかった要素
[k for k, v in ids.items() if not v]
kiyukakiyuka

IfcProjectから辿れなかった要素も使われてそう。辿れなかった要素を削除してもBIMvisionで開けるけど色とか違ってた。

kiyukakiyuka

IfcGeometricRepresentationContext を見ると HasSubContexts の属性を持っていてIfcGeometricRepresentationSubContext から参照されるよう。ということは IfcRel~ 以外にも参照される側の属性があるから、属性情報を全部確認するにはリレーションクラス以外にもある逆向きにたどる要素が必要になるのか。
IfcOpenShellだと get_inverse で取得したり、逆属性情報も model.by_type('IfcProject')[0] とかで取得できる ifcopenshell.entity_instance のプロパティにあるから取得はできる。

kiyukakiyuka

逆属性に何があるかを取得するの正規の方法がわからなかったので、無理やり逆属性を取得する方法。
通常の?属性は get_info で取得できるけどそれに対応する方法ってないのだろうか?
get_inverse は自身を参照している要素がすべて取得されるので、ドキュメント定義上にある属性以外の要素も取ってくる)

project = model.by_type('IfcProject')[0]
invs = set(dir(project)) - set(dir(ifcopenshell.entity_instance)) - set(project.get_info().keys())
invinfo = { inv: getattr(project, inv) for inv in invs }
kiyukakiyuka

IfcProject から逆属性含む属性を辿るコード。
これで辿れなかった要素を削除してBIMvision開いたら、削除前と表示は変わっていなさそう。

def get_element(item):
    # item から子要素取得
    children = []
    for k, v in item.get_info().items():
        if isinstance(v, ifcopenshell.entity_instance):
            children.append(v)
        elif isinstance(v, tuple) and len(v) > 0:
            if isinstance(v[0], ifcopenshell.entity_instance):
                children += v
    
    # 逆属性取得
    inverse = []
    invs = set(dir(item)) - set(dir(ifcopenshell.entity_instance)) - set(item.get_info().keys())
    for inv in invs:
        v = getattr(item, inv)
        if isinstance(v, ifcopenshell.entity_instance):
            inverse.append(v)
        elif isinstance(v, tuple) and len(v) > 0:
            if isinstance(v[0], ifcopenshell.entity_instance):
                inverse += v
    
    element = dict(
        id=item.id(),
        type=item.is_a(),
        children=children,
        inverse=inverse,
    )
    return element

IfcProject から順に要素をたどる

ids = {}
for line in lines:
    ids[line] = False

element = get_element(project)
ids[project] = element
    
queue_element = [element]
bar = tqdm(total=len(lines))
while len(queue_element) > 0:
    element = queue_element.pop(0)
    for item in element['children'] + element['inverse']:
        if ids.get(item): continue
        element = get_element(item)
        ids[item] = element
        queue_element.append(element)
        
    bar.update(1)
bar.close()

辿れなかった要素を削除してファイル保存

with open(path, 'r', encoding='utf-8') as f:
    ifc = f.read()

pat = re.compile(r'^#(' + '|'.join([f'{k.id()}' for k, v in ids.items() if not v]) + ')(?=[^\d]).*\n', re.MULTILINE)
ifcrem = pat.sub('', ifc)

with open(path, 'w', encoding='utf-8', newline='') as f:
    f.write(ifcrem)
kiyukakiyuka

ひとまずここまでのまとめ

  • おそらくIFCファイルの構成上のトップにあるクラスは IfcProject
  • 各IFCクラスがそれぞれ参照し合うことでグラフ構造をしている
  • 参照は属性情報として持っている場合と逆属性として持っている場合がある
    • ドキュメントのAttributesの数字の入っている要素が順属性で、数字の入っていないのが逆属性(たぶん)
    • 逆属性は IfcRelationship が代表的っぽいけど、それ以外のクラスもある
  • 順属性はIFCファイルにも記載されている要素だけれども、逆属性はファイルに記載はない
  • IfcOpenShell で読み込むと逆属性もプロパティとして持っている

あとはそれぞれのIFCクラスを個別に見ていかないとわからなさそうなので、いったんここで閉じます。
グラフ構造っぽいので次は可視化できたらなーと思ったり。

このスクラップは2023/02/06にクローズされました