Claude CodeでifcopenshellからIFCの部屋面積を検算する
IFCモデルのIfcSpaceから床面積をifcopenshellで取り出し、面積表のCSVと突き合わせるスクリプトをClaude Codeに書かせます。数量セットの欠落と単位の取り違えを検算で拾う手順です。
IFCモデルの部屋面積は、IfcSpaceに付いた数量セットから取り出せます。Pythonライブラリのifcopenshellを使えば数行です。問題は、取り出した値を面積表と突き合わせる段で、数量セットの欠落、部屋名の表記ゆれ、単位の取り違えが数字を静かにずらすことです。ここでは、取り出しと突合をClaude Codeにスクリプトとして書かせ、差分を機械的に洗い出す手順を示します。
例に使うIFCと面積表は、手順確認のために作った仮のデータです。実在の建物のものではありません。
ifcopenshellで読めるもの
ifcopenshellはIFCの読み書きライブラリで、PyPIから入ります。
python3 -m venv .venv
.venv/bin/pip install ifcopenshell
.venv/bin/python -c "import ifcopenshell; print(ifcopenshell.version)"手元ではバージョン0.9.0.post1が入りました。公式の入門ページは、ファイルを開いてスキーマ版を確かめ、クラス名で要素を集める流れを紹介しています。model.schemaはIFC2X3、IFC4、IFC4X3のいずれかを返し、model.by_type('IfcWall')のようにクラス名を渡すと該当要素の一覧が返ります。部屋ならIfcSpaceです。
import ifcopenshell
model = ifcopenshell.open("building.ifc")
print(model.schema)
spaces = model.by_type("IfcSpace")
print(len(spaces))入門ページは、要素に紐づくプロパティと数量をifcopenshell.util.element.get_psetsでまとめて読めるとも説明しています。util.elementのAPIリファレンスには引数の記載があり、qtos_only=Trueを付けると数量セットだけ、psets_only=Trueならプロパティセットだけになります。
面積の置き場所と取り出し方
床面積はIfcSpaceの属性ではなく、数量セットの中にあります。標準の数量セット名はQto_SpaceBaseQuantitiesで、正味の床面積はNetFloorArea、総床面積はGrossFloorAreaです。
取り出し方の選択肢
get_psets で全部読む
数量セットの名前も中身も分からないときの下見に使います。セット名と値の辞書が返ります。
get_pset で一点指定する
セット名とプロパティ名が分かっているなら、こちらのほうが効率的です。公式の説明も、名前が決まっているならget_psetのほうが効率的としています。
一点指定の形は次のとおりです。
from ifcopenshell.util.element import get_pset
area = get_pset(space, "Qto_SpaceBaseQuantities", "NetFloorArea")数量セットが無い部屋では、手元の確認でNoneが返りました。ゼロではなくNoneなので、「面積が0の部屋」と「面積が入っていない部屋」を区別できます。検算では、この区別をそのまま差分の種類にします。
単位を換算してから比べる
IFCの数値は、モデルが宣言した単位で入っています。ifcopenshellには、プロジェクトの単位とSI単位の換算係数を返すifcopenshell.util.unit.calculate_unit_scaleがあります。第2引数のunit_typeの既定値は長さ(LENGTHUNIT)です。面積を扱うなら、第2引数を"AREAUNIT"にして面積用の係数を取ります。
ここに罠があります。APIで作った小さなIFCを調べたところ、unit.assign_unitを引数なしで呼んだファイルでは、長さの単位がミリメートル、面積の単位が平方メートルでした。
.venv/bin/python -c "
import ifcopenshell, ifcopenshell.util.unit as U
m = ifcopenshell.open('demo.ifc')
print(U.calculate_unit_scale(m))
print(U.calculate_unit_scale(m, 'AREAUNIT'))
"出力は0.001と1.0でした。長さの係数を2乗して面積に掛けると、面積はすでに平方メートルなのに100万分の1になります。面積の値は面積の単位で読む。これを依頼文とコードの両方に書いておくと、長さの係数を流用する誤りを避けられます。
落とし穴
長さの換算係数を面積に流用すると桁が消える
LENGTHUNITがミリメートルでも、AREAUNITが平方メートルのファイルがあります。面積はAREAUNITの係数で換算し、長さの係数を2乗して使い回さないでください。
ただし、これは手元で作ったファイルでの観察です。他のBIMソフトが書き出したIFCで面積の単位がどうなるかは、ファイルごとにIfcUnitAssignmentを見て確かめる必要があります。
面積表との突合スクリプト
突き合わせる相手は、部屋名と面積を持つCSVとします。実務の面積表はExcelのことが多いので、CSVに書き出してから渡します。
name,area_m2
101 事務室,24.00
102 会議室,13.50
103 倉庫,8.00
104 廊下,6.20Claude Codeへの依頼文は、結果の形まで指定します。「IFCのIfcSpaceと面積表を突き合わせ、差があった部屋だけを出力して」と頼むだけでは、差の種類が混ざります。分けて欲しい種類を先に書いておきます。
- IFCに部屋が無い
- 面積表に部屋が無い
- 数量セットが無く面積がNone
- 面積の差が許容差を超える
許容差は0.01平方メートルとしました。表の小数点以下の桁数に合わせた仮の値なので、実際の表に合わせて変えてください。出来上がるスクリプトの一例は次のとおりです。
import csv
import sys
import ifcopenshell
import ifcopenshell.util.element as el
import ifcopenshell.util.unit as unit
QTO = "Qto_SpaceBaseQuantities"
TOL = 0.01 # 許容差(平方メートル)
def ifc_areas(path):
model = ifcopenshell.open(path)
scale = unit.calculate_unit_scale(model, "AREAUNIT") # 平方メートルへの換算
out = {}
for sp in model.by_type("IfcSpace"):
area = el.get_pset(sp, QTO, "NetFloorArea")
out[sp.Name] = None if area is None else area * scale # 数量セットが無ければ None
return model.schema, out
def table_areas(path):
with open(path, encoding="utf-8", newline="") as f:
return {r["name"]: float(r["area_m2"]) for r in csv.DictReader(f)}
schema, ifc = ifc_areas(sys.argv[1])
tbl = table_areas(sys.argv[2])
print(f"schema={schema} IfcSpace={len(ifc)} 面積表={len(tbl)}")
ng = 0
for name in sorted(set(ifc) | set(tbl)):
a, b = ifc.get(name), tbl.get(name)
if name not in ifc:
status = "IFCに無い"
elif name not in tbl:
status = "面積表に無い"
elif a is None:
status = "数量なし"
elif abs(a - b) > TOL:
status = f"差 {a - b:+.2f}"
else:
continue
ng += 1
print(f"NG {name}: ifc={a} 表={b} -> {status}")
tot_i = sum(v for v in ifc.values() if v is not None)
print(f"合計 ifc={tot_i:.2f} 表={sum(tbl.values()):.2f}")
sys.exit(1 if ng else 0)面積表をExcelから書き出すときは、読み込み側の癖にも気を付けます。文字コードを「CSV UTF-8」で保存すると先頭にBOMが付き、最初の列名が\ufeffnameとして読まれてnameのキー参照が失敗します。この場合はopenのencodingをutf-8-sigにします。また、面積の浮動小数は2進数で近似されるため、差が0かどうかの==比較は避け、許容差との大小で判定します。上のスクリプトが許容差方式なのはこのためです。
手元のIFCに対して実行した結果です。IFCには3室、面積表には4室が入れてあります。
.venv/bin/python areacheck.py demo.ifc menseki.csvschema=IFC4 IfcSpace=3 面積表=4
NG 102 会議室: ifc=12.5 表=13.5 -> 差 -1.00
NG 103 倉庫: ifc=None 表=8.0 -> 数量なし
NG 104 廊下: ifc=None 表=6.2 -> IFCに無い
合計 ifc=36.50 表=51.70ここでの要点は、差の合計だけを見ないことです。合計の差は15.20平方メートルですが、原因は面積の差が1.0、数量なしが8.0、廊下の欠落が6.2と三つに分かれます。合計だけで追うと、どの部屋から手を付けるかが分かりません。
階ごとの小計はget_aggregateで取る
面積表は階ごとに小計を持つことが多く、合計だけでなく階別でも合わせたくなります。ここで、部屋が所属する階の取り方に注意が要ります。公式のAPI説明では、get_containerは空間構造の包含関係をたどって要素の入れ物を返す関数です。一方、IfcSpaceは階に対して包含ではなく集約(aggregate)で結び付くのが通常の構成です。
手元のファイルで両方を呼ぶと、結果が分かれました。
.venv/bin/python -c "
import ifcopenshell, ifcopenshell.util.element as E
m = ifcopenshell.open('demo.ifc')
for s in m.by_type('IfcSpace'):
print(s.Name, E.get_container(s), E.get_aggregate(s).Name)
"3室ともget_containerはNone、get_aggregateは階(1F)を返しました。階別の集計をget_containerで書くと、全部屋が「階なし」に落ちます。Claude Codeへの依頼文には、階の取得はget_aggregateを使い、Noneの部屋は別枠で数えるよう書いておきます。ほかのソフトが書き出したIFCでは、部屋が階に包含で結ばれている場合もあるので、先に数室ぶん両方を出して構成を確かめるのが確実です。
部屋名で突き合わせる限界
このスクリプトは部屋名をキーにしています。部屋名が「101事務室」と「101事務室」のように揺れるだけで、両側とも「無い」扱いになります。室番号の列があるなら、名前でなく室番号で結合するほうが安全です。IFCの側では、室番号がIfcSpaceのどの属性に入るかが書き出すソフトで異なります。Claude Codeに、先にget_psetsで数部屋ぶんの全属性を出させ、番号が入っている場所を見てから結合キーを決めさせると、推測で書かせずに済みます。
数量セットの値は、書き出したソフトが計算して入れた値です。ifcopenshellが形状から面積を再計算しているわけではありません。この手順で確かめられるのは「IFCに入っている数字が面積表と一致するか」までで、「その数字が図面の寸法から正しく出ているか」は別の検算です。形状側の検算は、構造解析ならCalculiXの片持ち梁を手計算で検算する手順、3DモデルならCadQueryの寸法アサーションでSTEP出力を確かめる手順のように、手計算や期待値と突き合わせる型になります。法令の数量計算でも、危険物の指定数量の倍数を検算する手順が同じ考え方です。
検算をClaude Codeの終了条件にする
Claude Codeに書かせっぱなしにすると、実行して一度通ったところで終わりにしがちです。終了時にスクリプトを走らせるStopフックを置くと、差分が残っている間は作業が続きます。公式のフックの説明では、Stopフックが終了コード2で終わると、Claudeは作業に戻ります。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".venv/bin/python areacheck.py building.ifc menseki.csv >&2"
}
]
}
]
}
}これを.claude/settings.jsonに置きます。スクリプトの終了コードは差があるときに1を返す作りにしてあるため、そのままではブロックになりません。ブロックさせたいなら、スクリプト側を差ありのとき終了コード2にします。標準エラー出力の内容が、Claudeに戻る理由として渡されます。
sys.exit(2 if ng else 0)落とし穴は、Claudeの側で直せない差分が残る場合です。他のソフトが書き出したIFCで、数量セットそのものが入っていないときがその例です。終了コード2のままだと、Stopのたびにブロックされて作業が終わらなくなります。フックの入力JSONにはstop_hook_activeがあり、Stopフックによる再開の最中ならtrueになります。フックのドキュメントは、解決しない条件でブロックし続けないよう、この値を確かめて早めに抜けることを勧めています。ドキュメントには、Claudeがツールを呼ばないまま8回続けてブロックされた場合にフックを無効にする上限の記載もあります。
import json
if json.load(sys.stdin).get("stop_hook_active"):
sys.exit(0) # 二度目以降はブロックせず、差分は出力だけして終えるこの冒頭の判定を、上のスクリプトでngを数える前に入れます。二度目のStopでは通すかわりに、直せなかった差分は最終報告に残すよう、CLAUDE.mdに書いておきます。
フックの置き場所はユーザー全体の~/.claude/settings.jsonと、プロジェクトの.claude/settings.jsonがあり、後者はリポジトリに入れてチームで共有できます。IFCと面積表をリポジトリで管理しているなら、プロジェクト側に置くのが自然です。
CLAUDE.mdには、フックでは書きにくい前提を残します。
- 面積は Qto_SpaceBaseQuantities の NetFloorArea を使う
- 面積の換算は AREAUNIT の係数で行い、長さの係数を2乗して使わない
- 数量セットが無い部屋は 0 ではなく「数量なし」として報告する
- 許容差は 0.01 平方メートルまとめ
IFCの部屋面積の検算は、取り出しよりも差の分類で価値が出ます。部屋の欠落、数量なし、数値の差を別々に報告させれば、確認の順番が決まります。単位は面積用の係数で読み、数量セットの値は書き出し元ソフトの計算結果にすぎない点を前提に置いておけば、面積表との一致は一つの根拠として使えます。