IFCのデータ構造を理解したい
IFCファイルのデータ構造がよくわからないので、IfcOpenShellとかIFC.jsを使ってどういう作りになっているのかを解析したい。その自分用のメモ書き。多分ある程度理解したところでまとめる。
あとスクラップ初めて使うので色々試したい。
IFCはデータ的にはおそらくIfcProjectがトップにあって、そこに建物情報とかがぶら下がっているのだと思う。IFC.jsのドキュメントを見ての推測。(IFC仕様のドキュメントのどこを見たらそういうのがわかるのかわからない...)
ただ、IfcProjectのtreeにぶら下がっているのではなくて、IfcRelAggregatesとかIfcRelContainedInSpatialStructureとかのリレーションデータと合わせて見る必要がありそう。これもIFC.js(web-ifc-three)のソースコードを読んでの推測。
IFCのデータ構造はIfcRootがトップにあり、その下にIfcObjectDefinition(IfcWallやIfcColumnなど)、IfcPropertyDefinition(オブジェクトの属性情報)、IfcRelationship(オブジェクトや属性情報の関係を表す情報)があります。また、そこからさらに...という形で階層構造になっています。
以下はIFCの公式ドキュメントのリンクです!
IFCの各クラス?(正しい呼び名がわからない...Ifc~
のことを勝手にIFCクラスと言っている)のトップはIfcRootですね。PythonでいうObjectクラスに当たるものと言うか、すべてを包含するクラスというか。
ただ、ここで言いたかったトップと言うのは、IFCファイル的に一番上というか、ファイルを作るときに最初に作られるというか、そういう位置づけにくるものがIfcProjectだということが言いたかったんです(説明下手くそ)。
あと、正しいドキュメントのリンクありがとうございます!!
(と言うかどこからdevelopmentのドキュメント持ってきていたんだろうか...?)
あ、なるほどです~
あと、IFCでのクラスみたいなのはエンティティと呼ばれてます!
なるほど!エンティティなんですね。ありがとうございます!
ドキュメントの Formal representation の記述とかがDBっぽいなーとはなんとなく思ってましたが納得できました。
リレーションはデータ同士の関係を結びつけるもの。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を作成しているっぽい。
IfcRelDefinesByType
は、IfcSlabとIfcSlabTypeなどのIfcElementとIfcElementTypeの紐づけをしている。IfcRelDefinesByProperties
はプロパティ(なんだこれ?)のひもづけ、IfcRelAssociatesMaterial
はマテリアルとのひもづけをしているっぽい。
そのため建物のある部材に関する情報を取得したい場合、このあたりのリレーション情報も参照して取得する必要がありそう。
web-ifc-viewerだとviewer.getProperties
でもろもろ取得できそう(確認してない)。
内部的には IFCLoader.js の WebIfcPropertyManager
の getPropertySets
, getTypeProperties
, getMaterialsProperties
で取得している。
IFCの構造理解するためならIFC.jsよりIfcOpenShellのほうがいい気がしてきた。前に調べたときはIfcOpenShellのドキュメントが出てこなかったからIFC.jsを先に使ってたけど、今調べたら普通に見つかった。
IfcOpenShellドキュメント
IfcOpenShellの by_type
は指定したクラスを取得するわけではないらしい?
IfcProductのサブクラスを取得しないようにするとからの配列が返ってくる。
IfcProduct のときだけリレーション取得しているっぽいからこれだけ違う処理してる感じなのかな?
import ifcopenshell
ifc = ifcopenshell.open('01.ifc')
ifc.by_type('IfcProduct', include_subtypes=False) # []
違うわ。IfcProjectを取得しようとしたんだよ。IfcProductは親クラスのアブストラクトだからそりゃサブクラス取得しないようにしたら、からになるよ...
IfcProjectを指定したらちゃんと取得できました。
IfcOpenShellは内部で何やってるかわかんないなこれ。というか処理はC++で書かれているので読みたくない。ドキュメントも触り部分くらいしか書かれてないっぽいし、IFCの構造を理解するには使いづらいな。
ひとまずBIMVisionで表示される内容がどこから取ってきているのかを見ようと思う。
使うデータはここから
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))}
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
で頑張って取得するしかないんだろうか。
Locationタブの情報はSiteから取得したデータを表示しているのではなくて、下の階層のBuilding以下からのデータから計算したものを表示しているっぽい。たぶん。
Buildingは下記で取得できた。
基本的に get_info
と ifcopenshell.util.element.get_psets
で各種情報は取得できる感じ?
ifcopenshell.util.element.get_psets
は IfcPropertySet で紐づけられている情報を取得している感じかな
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}}
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
が間に入って関連付けを行っている(IfcWall
⇔ IfcRelDefinesByProperties
⇔ IfcPropertySet
)。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)}
ちなみに壁の窓部分などの穴の空いた部分の空間形状は 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')
ドキュメントの Attributes
の数字のついてない項目ってIFCファイル内に記載もないしなんだろうと思ってたんだけど、Relationで紐づけされる項目だったのか。
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)),)
IfcProjectからリレーションと子要素をたどれば、IFCの全行取得できるのかなと思ったけど余った。
余ったんだけど、余った行をIFCファイルから取り除いてBIMVisonで開いたらエラー出ずに開けた。
単純に定数要素で使用されてない感じかな?
使用されてない要素は、IfcDirection
, IfcSurfaceStyle
, IfcMaterialDefinitionRepresentation
, IfcColourRgb
, IfcPresentationLayerAssignment
とかだしそんなな気がする。
ただ、IfcPresentationLayerAssignment
はレイヤーだから単純にリレーションとかで紐づけされてないだけな気がする。
ところでなんで IfcColourRgb
はカラーがイギリス英語なの?
ちなみに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]
IfcProjectから辿れなかった要素も使われてそう。辿れなかった要素を削除してもBIMvisionで開けるけど色とか違ってた。
IfcGeometricRepresentationContext を見ると HasSubContexts
の属性を持っていてIfcGeometricRepresentationSubContext
から参照されるよう。ということは IfcRel~
以外にも参照される側の属性があるから、属性情報を全部確認するにはリレーションクラス以外にもある逆向きにたどる要素が必要になるのか。
IfcOpenShellだと get_inverse
で取得したり、逆属性情報も model.by_type('IfcProject')[0]
とかで取得できる ifcopenshell.entity_instance
のプロパティにあるから取得はできる。
逆属性に何があるかを取得するの正規の方法がわからなかったので、無理やり逆属性を取得する方法。
通常の?属性は 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 }
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)
ひとまずここまでのまとめ
- おそらくIFCファイルの構成上のトップにあるクラスは
IfcProject
- 各IFCクラスがそれぞれ参照し合うことでグラフ構造をしている
- 参照は属性情報として持っている場合と逆属性として持っている場合がある
- ドキュメントのAttributesの数字の入っている要素が順属性で、数字の入っていないのが逆属性(たぶん)
- 逆属性は
IfcRelationship
が代表的っぽいけど、それ以外のクラスもある
- 順属性はIFCファイルにも記載されている要素だけれども、逆属性はファイルに記載はない
-
IfcOpenShell
で読み込むと逆属性もプロパティとして持っている
あとはそれぞれのIFCクラスを個別に見ていかないとわからなさそうなので、いったんここで閉じます。
グラフ構造っぽいので次は可視化できたらなーと思ったり。