poker-mcp 1.9.8 → 1.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/package.json +3 -1
- package/src/mcp/handlers/calculationHandlers.js +7 -3
- package/src/mcp/middleware/errorHandler.js +19 -1
- package/tools/gen_input.py +717 -0
- package/tools/scatter.py +474 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ YAML-based input file management tool for radiation-shielding calculation code P
|
|
|
4
4
|
|
|
5
5
|
## 📋 クイック情報
|
|
6
6
|
|
|
7
|
-
- **バージョン**: 1.9.
|
|
7
|
+
- **バージョン**: 1.9.9
|
|
8
8
|
- **ツール数**: 36メソッド
|
|
9
9
|
- **プロトコル**: MCP (Model Context Protocol) 1.0.0 完全準拠
|
|
10
10
|
- **メインサーバー**: `src/mcp_server_stdio_v4.js`
|
|
@@ -12,6 +12,35 @@ YAML-based input file management tool for radiation-shielding calculation code P
|
|
|
12
12
|
- **核種データ**: `${POKER_INSTALL_PATH}/LIB/ICRP-07.NDX` を直接参照
|
|
13
13
|
- **実行方式**: STDIO通信(MCPプロトコル標準)
|
|
14
14
|
|
|
15
|
+
## 🐛 バージョン1.9.9の修正
|
|
16
|
+
|
|
17
|
+
npm で入れた環境(`npx poker-mcp`)でだけ現れる不具合と、失敗の伝え方の不具合を
|
|
18
|
+
直しました。まっさらな Windows に npm パッケージから入れた環境で、CAD 連携を含む
|
|
19
|
+
計算結果がリポジトリ上の実行と全行一致することを確認しています。
|
|
20
|
+
|
|
21
|
+
### `poker_generateInput` が npm 版で動かなかった
|
|
22
|
+
|
|
23
|
+
`tools/gen_input.py` と、それが読み込む `tools/scatter.py` が `package.json` の
|
|
24
|
+
`files` から漏れていました。リポジトリから動かすと問題なく、npm から入れた環境で
|
|
25
|
+
だけ `No module named 'gen_input'` で失敗していました。
|
|
26
|
+
|
|
27
|
+
### 失敗が成功として返っていた
|
|
28
|
+
|
|
29
|
+
CAD 系などのハンドラが返す `{ content, isError }` を、共通の処理がもう一度包んで
|
|
30
|
+
いたため、`isError: true` が本文の中に埋もれ、MCP の上では成功として伝わって
|
|
31
|
+
いました。AI クライアントは失敗に気づかないまま次の手順へ進みます。
|
|
32
|
+
|
|
33
|
+
すでに MCP の応答形式になっている戻り値はそのまま返し、`{ success: false, ... }`
|
|
34
|
+
を返すハンドラには `isError: true` を付けるようにしました。立体・ゾーン・線源など
|
|
35
|
+
の検証エラーも、MCP の上で失敗として伝わります。
|
|
36
|
+
|
|
37
|
+
### 計算エラーの処理そのものが落ちていた
|
|
38
|
+
|
|
39
|
+
`poker_executeCalculation` が計算エラーを返す途中で
|
|
40
|
+
`Cannot read properties of undefined (reading '_isRecoverableError')` になり、
|
|
41
|
+
本来のエラー内容が隠れていました。ハンドラがオブジェクトから切り離して呼ばれる
|
|
42
|
+
ため `this` が未定義になっていたのが原因です。
|
|
43
|
+
|
|
15
44
|
## 🆕 バージョン1.9.8の新機能
|
|
16
45
|
|
|
17
46
|
### 📜 `poker_getSchema` — ファイル書式を JSON Schema で取得
|
|
@@ -659,6 +688,11 @@ POKER_MCP_HOME/
|
|
|
659
688
|
|
|
660
689
|
## 📝 更新履歴
|
|
661
690
|
|
|
691
|
+
### v1.9.9 (2026-10)
|
|
692
|
+
- 🐛 `tools/gen_input.py` と `tools/scatter.py` を npm パッケージに同梱(`poker_generateInput` が npm 版で動かなかった)
|
|
693
|
+
- 🐛 ツールの失敗が `isError: true` として伝わるよう修正(応答の二重包みを解消、`success: false` に `isError` を付与)
|
|
694
|
+
- 🐛 `poker_executeCalculation` の計算エラー処理が `this` 未定義で落ちていた問題を修正
|
|
695
|
+
|
|
662
696
|
### v1.9.8 (2026-10)
|
|
663
697
|
- ✨ `poker_getSchema`(入力YAML・`.paths`・`.summary`・`.dose` の書式を JSON Schema で取得)
|
|
664
698
|
- 🐛 `poker_getDoseMap` が `.dose` の列構成を `information` から読むようになり、線量種別の増加に追随
|
|
@@ -749,6 +783,6 @@ obtained separately and licensed separately. poker-mcp は `poker_cui` を外部
|
|
|
749
783
|
|
|
750
784
|
---
|
|
751
785
|
|
|
752
|
-
**🎯 Poker MCP Server v1.9.
|
|
786
|
+
**🎯 Poker MCP Server v1.9.9**
|
|
753
787
|
**プロトコル**: MCP 1.0.0 完全準拠
|
|
754
788
|
**作者**: Yoshihiro Hirao | **ライセンス**: MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poker-mcp",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.9",
|
|
4
4
|
"description": "YAML-based input file management tool for radiation shielding calculation code POKER with MCP support - Enhanced with collision detection, daughter nuclide auto-completion, and environment variable support",
|
|
5
5
|
"main": "src/mcp_server_stdio_v4.js",
|
|
6
6
|
"type": "module",
|
|
@@ -56,7 +56,9 @@
|
|
|
56
56
|
},
|
|
57
57
|
"files": [
|
|
58
58
|
"src/",
|
|
59
|
+
"tools/gen_input.py",
|
|
59
60
|
"tools/gen_paths.py",
|
|
61
|
+
"tools/scatter.py",
|
|
60
62
|
"tools/ray_trace_tri.py",
|
|
61
63
|
"tools/poker_lib.py",
|
|
62
64
|
"tools/audit_mfp.py",
|
|
@@ -10,7 +10,9 @@ import { parseDoseMap } from '../../utils/doseMapParser.js';
|
|
|
10
10
|
export function createCalculationHandlers(taskManager) {
|
|
11
11
|
const calculationService = new CalculationService();
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// server.js はハンドラを handler(args) の形でオブジェクトから切り離して呼ぶため、
|
|
14
|
+
// 内部の補助関数は this ではなくこの変数経由で参照する。
|
|
15
|
+
const handlers = {
|
|
14
16
|
async getDoseMap(args) {
|
|
15
17
|
try {
|
|
16
18
|
if (!args.detector_name) {
|
|
@@ -231,8 +233,8 @@ export function createCalculationHandlers(taskManager) {
|
|
|
231
233
|
mcp_error_code: error.mcpErrorCode,
|
|
232
234
|
details: {
|
|
233
235
|
category: 'calculation_error',
|
|
234
|
-
recoverable:
|
|
235
|
-
suggestions:
|
|
236
|
+
recoverable: handlers._isRecoverableError(error.code),
|
|
237
|
+
suggestions: handlers._getErrorSuggestions(error.code, error.context)
|
|
236
238
|
}
|
|
237
239
|
};
|
|
238
240
|
}
|
|
@@ -343,4 +345,6 @@ export function createCalculationHandlers(taskManager) {
|
|
|
343
345
|
}
|
|
344
346
|
}
|
|
345
347
|
};
|
|
348
|
+
|
|
349
|
+
return handlers;
|
|
346
350
|
}
|
|
@@ -37,11 +37,29 @@ export function handleMcpError(error, context = {}) {
|
|
|
37
37
|
return new McpError(ErrorCode.InternalError, `内部エラー: ${error.message}`);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* ハンドラの戻り値を MCP の CallToolResult に整える。
|
|
42
|
+
* - すでに CallToolResult の形(content 配列を持つ)ならそのまま返す。
|
|
43
|
+
* 包み直すと isError が text の中に埋もれ、失敗が成功として伝わってしまう。
|
|
44
|
+
* - { success: false, ... } を返すハンドラは失敗として isError: true を付ける。
|
|
45
|
+
* AI クライアントは本文を読まずに isError で成否を判断することがあるため。
|
|
46
|
+
*/
|
|
47
|
+
export function toCallToolResult(result) {
|
|
48
|
+
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
const wrapped = { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
52
|
+
if (result && typeof result === 'object' && result.success === false) {
|
|
53
|
+
wrapped.isError = true;
|
|
54
|
+
}
|
|
55
|
+
return wrapped;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export function safeExecute(fn, context = {}) {
|
|
41
59
|
return async (request) => {
|
|
42
60
|
try {
|
|
43
61
|
const result = await fn(request);
|
|
44
|
-
return
|
|
62
|
+
return toCallToolResult(result);
|
|
45
63
|
} catch (error) {
|
|
46
64
|
throw handleMcpError(error, context);
|
|
47
65
|
}
|
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# gen_input.py -- CAD モデルから POKER の YAML 入力を組み立てる
|
|
2
|
+
#
|
|
3
|
+
# CAD が正本という前提。線源・検出器・材質・分割数を CAD のカスタムプロパティに
|
|
4
|
+
# 持たせ、そこから YAML を生成する。人が YAML を書く必要をなくすのが目的。
|
|
5
|
+
#
|
|
6
|
+
# 使い方:
|
|
7
|
+
# freecadcmd -c "import gen_input; gen_input.main('spec.json')"
|
|
8
|
+
#
|
|
9
|
+
# spec.json:
|
|
10
|
+
# { "fcstd": "...", "out": "poker.yaml", "poker_dir": "C:/Poker" }
|
|
11
|
+
#
|
|
12
|
+
# CAD 側のカスタムプロパティ:
|
|
13
|
+
#
|
|
14
|
+
# 全オブジェクト共通
|
|
15
|
+
# PokerRole shield(既定) / source / detector
|
|
16
|
+
#
|
|
17
|
+
# 遮蔽体 (PokerRole=shield)
|
|
18
|
+
# PokerMaterial 材質名(Iron, Concrete, ...)
|
|
19
|
+
# PokerDensity 密度の上書き [g/cm3](省略可)
|
|
20
|
+
#
|
|
21
|
+
# 線源 (PokerRole=source)
|
|
22
|
+
# PokerNuclides "Co60:3.7e10, Cs137:1.0e11"
|
|
23
|
+
# PokerDivision "8 16 30"(軸の順は形状で決まる。省略時は既定値)
|
|
24
|
+
# PokerCutoff 打ち切り率(省略時 1e-4)
|
|
25
|
+
#
|
|
26
|
+
# 検出器 (PokerRole=detector)
|
|
27
|
+
# PokerDivision "5 5"(2D)/ "5 5 5"(3D)。省略時は各方向 5
|
|
28
|
+
# グリッドの次元は形状から決まる:
|
|
29
|
+
# 球・小さい立体 -> 点検出器
|
|
30
|
+
# 薄い直方体 -> 2D(面線量マップ)
|
|
31
|
+
# 直方体 -> 3D(体積線量マップ)
|
|
32
|
+
# PokerShowPathTrace True/False(省略時 False)
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
|
|
38
|
+
import FreeCAD as App
|
|
39
|
+
|
|
40
|
+
import poker_lib
|
|
41
|
+
import scatter
|
|
42
|
+
|
|
43
|
+
# CAD(mm) -> POKER(cm)
|
|
44
|
+
# FreeCAD の内部表現は常に mm で、表示の単位系設定(UserSchema)は Shape の
|
|
45
|
+
# 座標に影響しない。UnitsSchemaInternal の定義に「FreeCAD uses a mm/kg/deg
|
|
46
|
+
# scala」と明記されており、UserSchema(SI1/SI2/Imperial/Centimeters 等)は
|
|
47
|
+
# 表示の切り替えにすぎない。実測でも Quantity("1 m").Value == 1000.0。
|
|
48
|
+
# よってこの係数は固定でよい。
|
|
49
|
+
SCALE = 0.1
|
|
50
|
+
|
|
51
|
+
# 線源の分割数を寸法から決めるときの基準。
|
|
52
|
+
#
|
|
53
|
+
# 1 区画の厚さが DIV_MFP mfp 以下になるよう分割する。
|
|
54
|
+
#
|
|
55
|
+
# これは出発点であって最適値ではない。本来は分割数を変えて線量の収束を
|
|
56
|
+
# 確認し、それで決めるもの。ここでの既定値は「収束解析を始める前の、
|
|
57
|
+
# ひとまず妥当な値」という位置づけ。利用者は PokerDivision で上書きできる。
|
|
58
|
+
#
|
|
59
|
+
# 2 mfp としたのは、実運用の分割(キャスクで r 方向 1.9 mfp、z 方向 2.7 mfp)
|
|
60
|
+
# に近いため。1 mfp だと点数が現実的でなくなる(キャスクで半径 16 分割、
|
|
61
|
+
# 高さ 81 分割)。
|
|
62
|
+
DIV_MFP = 2.0
|
|
63
|
+
|
|
64
|
+
# 軸ごとの上限は設けない。2 mfp という基準を軸によって変える理由がなく、
|
|
65
|
+
# 細長い線源では z 方向に、扁平な線源では r 方向に、自然に多く配分される。
|
|
66
|
+
#
|
|
67
|
+
# 周方向(phi)だけは寸法で決まらないので固定値を使う。円周を何分割するかは
|
|
68
|
+
# 半径の mfp とは別の話で、方位角方向の線量分布をどれだけ解像したいかで決まる。
|
|
69
|
+
DIV_PHI = 16
|
|
70
|
+
DIV_MIN = 2
|
|
71
|
+
|
|
72
|
+
# 点数が過大になったときに気づけるよう、この数を超えたら警告する。
|
|
73
|
+
# 止めはしない(利用者が意図して細かくしている場合もある)。
|
|
74
|
+
DIV_WARN_POINTS = 20000
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _div_from_size(size_cm, mu):
|
|
78
|
+
# size_cm を DIV_MFP mfp 以下に刻むのに必要な分割数
|
|
79
|
+
if mu is None or mu <= 0:
|
|
80
|
+
return DIV_MIN
|
|
81
|
+
n = int(size_cm * mu / DIV_MFP + 0.999)
|
|
82
|
+
return max(DIV_MIN, n)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _num(v, default=None):
|
|
86
|
+
try:
|
|
87
|
+
return float(v)
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
return default
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _xyz(v, scale=SCALE):
|
|
93
|
+
# 重心計算などで 1e-17 のような値が出るので、実質ゼロは 0 に丸める。
|
|
94
|
+
# POKER の入力に指数表記の微小値が並ぶと読みにくい。
|
|
95
|
+
def f(x):
|
|
96
|
+
x = x * scale
|
|
97
|
+
return 0.0 if abs(x) < 1e-9 else x
|
|
98
|
+
return '%g %g %g' % (f(v.x), f(v.y), f(v.z))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def role_of(obj):
|
|
102
|
+
r = (getattr(obj, 'PokerRole', None) or 'shield').strip().lower()
|
|
103
|
+
return r if r in ('shield', 'source', 'detector') else 'shield'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_nuclides(text):
|
|
107
|
+
# "Co60:3.7e10, Cs137:1.0e11" -> [("Co60", 3.7e10), ("Cs137", 1.0e11)]
|
|
108
|
+
out = []
|
|
109
|
+
for part in re.split(r'[,;]', text or ''):
|
|
110
|
+
part = part.strip()
|
|
111
|
+
if not part:
|
|
112
|
+
continue
|
|
113
|
+
m = re.match(r'^([A-Za-z]+[-_]?\d+m?)\s*[:=]\s*([\d.eE+-]+)$', part)
|
|
114
|
+
if not m:
|
|
115
|
+
raise SystemExit(
|
|
116
|
+
'PokerNuclides の書式が不正です: %r\n'
|
|
117
|
+
' 例: "Co60:3.7e10, Cs137:1.0e11"' % part)
|
|
118
|
+
out.append((m.group(1).replace('-', '').replace('_', ''), float(m.group(2))))
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def surface_types(sh):
|
|
123
|
+
# 面の種類を数える。OCC は Surface の型で曲面種別が分かる。
|
|
124
|
+
# Cylinder / Sphere / Cone / Toroid / Plane / BSplineSurface ...
|
|
125
|
+
from collections import Counter
|
|
126
|
+
return Counter(type(f.Surface).__name__ for f in sh.Faces)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def classify(sh):
|
|
130
|
+
# ソリッドの形から POKER の立体型を判定する。
|
|
131
|
+
#
|
|
132
|
+
# 面の数だけで判定すると誤る。三角柱も五面体も「平面だけ」だが、
|
|
133
|
+
# 面数は形によって変わる。曲面の種別で分ける方が確実。
|
|
134
|
+
#
|
|
135
|
+
# 返り値: ('RCC'|'SPH'|'BOX'|None, 補足情報)
|
|
136
|
+
t = surface_types(sh)
|
|
137
|
+
n_plane = t.get('Plane', 0)
|
|
138
|
+
n_face = len(sh.Faces)
|
|
139
|
+
|
|
140
|
+
# 球: 球面1枚だけ
|
|
141
|
+
if t.get('Sphere', 0) == 1 and n_face == 1:
|
|
142
|
+
s = sh.Faces[0].Surface
|
|
143
|
+
return 'SPH', {'center': s.Center, 'radius': s.Radius}
|
|
144
|
+
|
|
145
|
+
# 円柱: 円柱面1枚 + 平面2枚(上下の蓋)
|
|
146
|
+
if t.get('Cylinder', 0) == 1 and n_plane == 2 and n_face == 3:
|
|
147
|
+
cyl = [f for f in sh.Faces if type(f.Surface).__name__ == 'Cylinder'][0]
|
|
148
|
+
s = cyl.Surface
|
|
149
|
+
ax = s.Axis
|
|
150
|
+
# 底面と上面を軸方向の位置で見分ける
|
|
151
|
+
planes = [f for f in sh.Faces if type(f.Surface).__name__ == 'Plane']
|
|
152
|
+
ts = sorted(((f.CenterOfMass - s.Center).dot(ax), f.CenterOfMass)
|
|
153
|
+
for f in planes)
|
|
154
|
+
h = ts[-1][0] - ts[0][0]
|
|
155
|
+
return 'RCC', {'bottom': ts[0][1], 'axis': ax, 'height': h,
|
|
156
|
+
'radius': s.Radius}
|
|
157
|
+
|
|
158
|
+
# 直方体: 平面6枚で、法線が3方向(各2枚ずつ)
|
|
159
|
+
if n_plane == 6 and n_face == 6:
|
|
160
|
+
axes = []
|
|
161
|
+
for f in sh.Faces:
|
|
162
|
+
a = f.Surface.Axis
|
|
163
|
+
if not any(abs(abs(a.dot(b)) - 1.0) < 1e-6 for b in axes):
|
|
164
|
+
axes.append(a)
|
|
165
|
+
if len(axes) == 3:
|
|
166
|
+
# 直交しているか(斜方体でないか)確認
|
|
167
|
+
ok = (abs(axes[0].dot(axes[1])) < 1e-6 and
|
|
168
|
+
abs(axes[1].dot(axes[2])) < 1e-6 and
|
|
169
|
+
abs(axes[0].dot(axes[2])) < 1e-6)
|
|
170
|
+
if ok:
|
|
171
|
+
return 'BOX', {'axes': axes}
|
|
172
|
+
|
|
173
|
+
return None, {'faces': n_face, 'types': dict(t)}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def read_source(obj, nuclide_override=None, mu_src=None):
|
|
177
|
+
# mu_src: 線源材質の線減弱係数 [1/cm]。分割数を寸法から決めるのに使う。
|
|
178
|
+
# 線源自身の自己遮蔽が効くので、1区画が厚すぎると精度が落ちる。
|
|
179
|
+
# 形状から線源の型と幾何を決める。核種と分割は プロパティから。
|
|
180
|
+
sh = obj.Shape
|
|
181
|
+
kind, geo = classify(sh)
|
|
182
|
+
bb = sh.BoundBox
|
|
183
|
+
|
|
184
|
+
nuc = parse_nuclides(getattr(obj, 'PokerNuclides', None) or '')
|
|
185
|
+
# 子孫核種を補完した結果が spec で渡されていればそちらを使う。
|
|
186
|
+
# Cs137 は β 崩壊のみで光子をほぼ出さず、0.662 MeV は娘核種 Ba137m から
|
|
187
|
+
# 出る。忘れると線量が 1/3 になるため、poker_mcp 側で補完した結果を渡す。
|
|
188
|
+
if nuclide_override and obj.Name in nuclide_override:
|
|
189
|
+
nuc = [(d['nuclide'], float(d['radioactivity']))
|
|
190
|
+
for d in nuclide_override[obj.Name]]
|
|
191
|
+
if not nuc:
|
|
192
|
+
raise SystemExit(
|
|
193
|
+
'線源 %s に PokerNuclides が設定されていません\n'
|
|
194
|
+
' 例: "Co60:3.7e10"' % obj.Name)
|
|
195
|
+
|
|
196
|
+
src = {
|
|
197
|
+
'name': obj.Name,
|
|
198
|
+
'inventory': [{'nuclide': n, 'radioactivity': a} for n, a in nuc],
|
|
199
|
+
'cutoff_rate': _num(getattr(obj, 'PokerCutoff', None), 1e-4),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
div_text = (getattr(obj, 'PokerDivision', None) or '').strip()
|
|
203
|
+
div = [int(x) for x in div_text.split()] if div_text else None
|
|
204
|
+
|
|
205
|
+
# 体積が無視できるほど小さければ点線源として扱う(印として置いた球など)
|
|
206
|
+
if sh.Volume * (SCALE ** 3) < 1.0: # 1 cm3 未満
|
|
207
|
+
src['type'] = 'POINT'
|
|
208
|
+
src['position'] = _xyz(sh.CenterOfMass)
|
|
209
|
+
return src
|
|
210
|
+
|
|
211
|
+
if kind == 'RCC':
|
|
212
|
+
# 軸は円柱面から取る。バウンディングボックスから作ると傾いた円柱に
|
|
213
|
+
# 対応できず、横倒しの燃料棒や斜めの配管がまったく違う形になる。
|
|
214
|
+
b, ax, h = geo['bottom'], geo['axis'], geo['height']
|
|
215
|
+
src['type'] = 'RCC'
|
|
216
|
+
src['geometry'] = {
|
|
217
|
+
'bottom_center': _xyz(b),
|
|
218
|
+
'height_vector': '%g %g %g' % (ax.x * h * SCALE,
|
|
219
|
+
ax.y * h * SCALE,
|
|
220
|
+
ax.z * h * SCALE),
|
|
221
|
+
'radius': geo['radius'] * SCALE,
|
|
222
|
+
}
|
|
223
|
+
d = div or [
|
|
224
|
+
_div_from_size(geo['radius'] * SCALE, mu_src),
|
|
225
|
+
DIV_PHI,
|
|
226
|
+
_div_from_size(abs(h) * SCALE, mu_src),
|
|
227
|
+
]
|
|
228
|
+
src['division'] = {
|
|
229
|
+
'r': {'number': d[0], 'type': 'UNIFORM'},
|
|
230
|
+
'phi': {'number': d[1], 'type': 'UNIFORM'},
|
|
231
|
+
'z': {'number': d[2], 'type': 'UNIFORM'},
|
|
232
|
+
}
|
|
233
|
+
return src
|
|
234
|
+
|
|
235
|
+
if kind == 'SPH':
|
|
236
|
+
# POKER の SPH 線源。分割は r/phi/theta。
|
|
237
|
+
src['type'] = 'SPH'
|
|
238
|
+
src['geometry'] = {
|
|
239
|
+
'center': _xyz(geo['center']),
|
|
240
|
+
'radius': geo['radius'] * SCALE,
|
|
241
|
+
}
|
|
242
|
+
d = div or [
|
|
243
|
+
_div_from_size(geo['radius'] * SCALE, mu_src),
|
|
244
|
+
DIV_PHI,
|
|
245
|
+
DIV_PHI,
|
|
246
|
+
]
|
|
247
|
+
src['division'] = {
|
|
248
|
+
'r': {'number': d[0], 'type': 'UNIFORM'},
|
|
249
|
+
'phi': {'number': d[1], 'type': 'UNIFORM'},
|
|
250
|
+
'theta': {'number': d[2], 'type': 'UNIFORM'},
|
|
251
|
+
}
|
|
252
|
+
return src
|
|
253
|
+
|
|
254
|
+
if kind == 'BOX':
|
|
255
|
+
src['type'] = 'BOX'
|
|
256
|
+
src['geometry'] = {
|
|
257
|
+
'vertex': '%g %g %g' % (bb.XMin * SCALE, bb.YMin * SCALE, bb.ZMin * SCALE),
|
|
258
|
+
'edge_1': '%g 0 0' % (bb.XLength * SCALE),
|
|
259
|
+
'edge_2': '0 %g 0' % (bb.YLength * SCALE),
|
|
260
|
+
'edge_3': '0 0 %g' % (bb.ZLength * SCALE),
|
|
261
|
+
}
|
|
262
|
+
d = div or [
|
|
263
|
+
_div_from_size(bb.XLength * SCALE, mu_src),
|
|
264
|
+
_div_from_size(bb.YLength * SCALE, mu_src),
|
|
265
|
+
_div_from_size(bb.ZLength * SCALE, mu_src),
|
|
266
|
+
]
|
|
267
|
+
src['division'] = {
|
|
268
|
+
'edge_1': {'number': d[0], 'type': 'UNIFORM'},
|
|
269
|
+
'edge_2': {'number': d[1], 'type': 'UNIFORM'},
|
|
270
|
+
'edge_3': {'number': d[2], 'type': 'UNIFORM'},
|
|
271
|
+
}
|
|
272
|
+
return src
|
|
273
|
+
|
|
274
|
+
# 判定できない形(円錐、トーラス、自由曲面、フィレット付きなど)。
|
|
275
|
+
# 黙って外接直方体で近似すると、利用者が気づかないまま違う線源分布で
|
|
276
|
+
# 計算することになる。明示的に拒否し、対処法を示す。
|
|
277
|
+
raise SystemExit(
|
|
278
|
+
'線源 %s の形状を判定できません(面: %s)\n'
|
|
279
|
+
' POKER の線源は POINT / RCC(円柱) / SPH(球) / BOX(直方体) です。\n'
|
|
280
|
+
' この形状を線源に使うには、外接する円柱か直方体に置き換えるか、\n'
|
|
281
|
+
' YAML を手で書いてください。' % (obj.Name, geo))
|
|
282
|
+
|
|
283
|
+
if kind == 'RCC':
|
|
284
|
+
# 円柱。軸は最も長い辺の方向とみなす
|
|
285
|
+
src['type'] = 'RCC'
|
|
286
|
+
src['geometry'] = {
|
|
287
|
+
'bottom_center': '%g %g %g' % (
|
|
288
|
+
(bb.XMin + bb.XMax) / 2 * SCALE,
|
|
289
|
+
(bb.YMin + bb.YMax) / 2 * SCALE,
|
|
290
|
+
bb.ZMin * SCALE),
|
|
291
|
+
'height_vector': '0 0 %g' % (bb.ZLength * SCALE),
|
|
292
|
+
'radius': max(bb.XLength, bb.YLength) / 2 * SCALE,
|
|
293
|
+
}
|
|
294
|
+
d = div or [
|
|
295
|
+
_div_from_size(geo['radius'] * SCALE, mu_src),
|
|
296
|
+
DIV_PHI,
|
|
297
|
+
_div_from_size(abs(h) * SCALE, mu_src),
|
|
298
|
+
]
|
|
299
|
+
src['division'] = {
|
|
300
|
+
'r': {'number': d[0], 'type': 'UNIFORM'},
|
|
301
|
+
'phi': {'number': d[1], 'type': 'UNIFORM'},
|
|
302
|
+
'z': {'number': d[2], 'type': 'UNIFORM'},
|
|
303
|
+
}
|
|
304
|
+
elif kind == 'BOX':
|
|
305
|
+
src['type'] = 'BOX'
|
|
306
|
+
src['geometry'] = {
|
|
307
|
+
'vertex': '%g %g %g' % (bb.XMin * SCALE, bb.YMin * SCALE, bb.ZMin * SCALE),
|
|
308
|
+
'edge_1': '%g 0 0' % (bb.XLength * SCALE),
|
|
309
|
+
'edge_2': '0 %g 0' % (bb.YLength * SCALE),
|
|
310
|
+
'edge_3': '0 0 %g' % (bb.ZLength * SCALE),
|
|
311
|
+
}
|
|
312
|
+
d = div or [
|
|
313
|
+
_div_from_size(bb.XLength * SCALE, mu_src),
|
|
314
|
+
_div_from_size(bb.YLength * SCALE, mu_src),
|
|
315
|
+
_div_from_size(bb.ZLength * SCALE, mu_src),
|
|
316
|
+
]
|
|
317
|
+
src['division'] = {
|
|
318
|
+
'edge_1': {'number': d[0], 'type': 'UNIFORM'},
|
|
319
|
+
'edge_2': {'number': d[1], 'type': 'UNIFORM'},
|
|
320
|
+
'edge_3': {'number': d[2], 'type': 'UNIFORM'},
|
|
321
|
+
}
|
|
322
|
+
else:
|
|
323
|
+
# 判定できない形は外接直方体で近似する。
|
|
324
|
+
# 線源の分割点は幾何の中に収まればよく、遮蔽体ほど形状に厳密さを要さない。
|
|
325
|
+
src['type'] = 'BOX'
|
|
326
|
+
src['geometry'] = {
|
|
327
|
+
'vertex': '%g %g %g' % (bb.XMin * SCALE, bb.YMin * SCALE, bb.ZMin * SCALE),
|
|
328
|
+
'edge_1': '%g 0 0' % (bb.XLength * SCALE),
|
|
329
|
+
'edge_2': '0 %g 0' % (bb.YLength * SCALE),
|
|
330
|
+
'edge_3': '0 0 %g' % (bb.ZLength * SCALE),
|
|
331
|
+
}
|
|
332
|
+
d = div or [
|
|
333
|
+
_div_from_size(bb.XLength * SCALE, mu_src),
|
|
334
|
+
_div_from_size(bb.YLength * SCALE, mu_src),
|
|
335
|
+
_div_from_size(bb.ZLength * SCALE, mu_src),
|
|
336
|
+
]
|
|
337
|
+
src['division'] = {
|
|
338
|
+
'edge_1': {'number': d[0], 'type': 'UNIFORM'},
|
|
339
|
+
'edge_2': {'number': d[1], 'type': 'UNIFORM'},
|
|
340
|
+
'edge_3': {'number': d[2], 'type': 'UNIFORM'},
|
|
341
|
+
}
|
|
342
|
+
return src
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def read_detector(obj):
|
|
346
|
+
# 検出器の形状からグリッドを決める。
|
|
347
|
+
# 球や小さな立体 -> 点検出器
|
|
348
|
+
# 薄い直方体 -> 2D グリッド(面線量マップ)
|
|
349
|
+
# 直方体 -> 3D グリッド(体積線量マップ)
|
|
350
|
+
#
|
|
351
|
+
# 従来は PokerGrid に "120 0 0 / 0 120 0 / 5 5" と書かせていたが、
|
|
352
|
+
# 書式を間違えやすく、CAD で形を見ながら決められない。形状から読めば
|
|
353
|
+
# 「この面の線量マップが欲しい」という意図がそのまま伝わる。
|
|
354
|
+
sh = obj.Shape
|
|
355
|
+
kind, geo = classify(sh)
|
|
356
|
+
bb = sh.BoundBox
|
|
357
|
+
|
|
358
|
+
det = {
|
|
359
|
+
'name': obj.Name,
|
|
360
|
+
'origin': _xyz(sh.CenterOfMass),
|
|
361
|
+
'show_path_trace': bool(getattr(obj, 'PokerShowPathTrace', False)),
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# 分割数。"5 5" なら 2 方向、"5 5 5" なら 3 方向。
|
|
365
|
+
div_text = (getattr(obj, 'PokerDivision', None) or '').strip()
|
|
366
|
+
div = [int(x) for x in div_text.split()] if div_text else None
|
|
367
|
+
|
|
368
|
+
# 点検出器とみなす条件: 球、または十分小さい立体
|
|
369
|
+
if kind == 'SPH' or sh.Volume * (SCALE ** 3) < 1.0:
|
|
370
|
+
return det
|
|
371
|
+
|
|
372
|
+
if kind != 'BOX':
|
|
373
|
+
# グリッドは直方体でしか作れない。判定できない形は点として扱い、
|
|
374
|
+
# 重心を評価点にする(黙って外接箱にすると意図と違う点を評価する)。
|
|
375
|
+
return det
|
|
376
|
+
|
|
377
|
+
# 辺の長さで次元を決める。最も短い辺が他の 1/20 未満なら平板とみなす。
|
|
378
|
+
L = sorted([(bb.XLength, 0), (bb.YLength, 1), (bb.ZLength, 2)])
|
|
379
|
+
edges = [
|
|
380
|
+
('%g 0 0' % (bb.XLength * SCALE), bb.XLength),
|
|
381
|
+
('0 %g 0' % (bb.YLength * SCALE), bb.YLength),
|
|
382
|
+
('0 0 %g' % (bb.ZLength * SCALE), bb.ZLength),
|
|
383
|
+
]
|
|
384
|
+
thin = L[0][0] < L[2][0] / 20.0
|
|
385
|
+
|
|
386
|
+
if thin:
|
|
387
|
+
# 2D: 薄い方向を除く 2 辺
|
|
388
|
+
use = [k for _, k in L[1:]]
|
|
389
|
+
n = div or [5, 5]
|
|
390
|
+
else:
|
|
391
|
+
use = [0, 1, 2]
|
|
392
|
+
n = div or [5, 5, 5]
|
|
393
|
+
|
|
394
|
+
if len(n) < len(use):
|
|
395
|
+
raise SystemExit(
|
|
396
|
+
'検出器 %s の PokerDivision は %d 個の数が必要です(指定: %r)'
|
|
397
|
+
% (obj.Name, len(use), div_text))
|
|
398
|
+
|
|
399
|
+
# 原点はグリッドの隅(POKER の grid は origin から edge 方向に伸びる)
|
|
400
|
+
det['origin'] = '%g %g %g' % (bb.XMin * SCALE, bb.YMin * SCALE, bb.ZMin * SCALE)
|
|
401
|
+
det['grid'] = [{'edge': edges[k][0], 'number': n[i]} for i, k in enumerate(use)]
|
|
402
|
+
return det
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_yaml(doc, materials, sources, detectors, mfp_order=None, lib_density=None, buildup_equiv=None):
|
|
406
|
+
# materials: {材質名: 密度 or None}(CAD の PokerDensity)
|
|
407
|
+
# lib_density: {材質名: カタログ密度}(poker_lib から)
|
|
408
|
+
# mfp_order: [材質名, ...] mfp の大きい順。None なら材質名順。
|
|
409
|
+
lib_density = lib_density or {}
|
|
410
|
+
buildup_equiv = buildup_equiv or {}
|
|
411
|
+
|
|
412
|
+
# 分割点の総数(最大の線源)と、検出器の評価点の総数を数える
|
|
413
|
+
n_src_points = 1
|
|
414
|
+
for s_ in sources:
|
|
415
|
+
n = 1
|
|
416
|
+
for d_ in (s_.get('division') or {}).values():
|
|
417
|
+
n *= d_['number']
|
|
418
|
+
n_src_points = max(n_src_points, n)
|
|
419
|
+
n_det_points = 1
|
|
420
|
+
for d_ in detectors:
|
|
421
|
+
n = 1
|
|
422
|
+
for g in d_.get('grid', []):
|
|
423
|
+
n *= g['number']
|
|
424
|
+
n_det_points = max(n_det_points, n)
|
|
425
|
+
|
|
426
|
+
L = []
|
|
427
|
+
L.append('# このファイルは gen_input.py が CAD モデルから生成したものです。')
|
|
428
|
+
L.append('# 正本は CAD モデルです。形状を変えたら再生成してください。')
|
|
429
|
+
L.append('')
|
|
430
|
+
# サマリーの出力量。既定では分割点も評価点も間引かれるため、.paths の生成に
|
|
431
|
+
# 必要な全点が得られない。入力の実数に合わせて設定する。
|
|
432
|
+
L.append('thinnedindices:')
|
|
433
|
+
L.append(' sourcepoint: %d' % max(10, n_src_points))
|
|
434
|
+
L.append(' pseudosourcepoint: 10')
|
|
435
|
+
L.append(' detectorgrid: %d' % max(10, n_det_points))
|
|
436
|
+
L.append(' detectorevaluation: %d' % max(10, n_det_points))
|
|
437
|
+
L.append(' pathtrace: 5')
|
|
438
|
+
L.append(' buildupenergy: 3')
|
|
439
|
+
L.append(' buildupmfp: 3')
|
|
440
|
+
L.append('')
|
|
441
|
+
L.append('unit:')
|
|
442
|
+
L.append(' length: cm')
|
|
443
|
+
L.append(' angle: degree')
|
|
444
|
+
L.append(' density: g/cm3')
|
|
445
|
+
L.append(' radioactivity: Bq')
|
|
446
|
+
L.append('')
|
|
447
|
+
|
|
448
|
+
# --- body / zone ---
|
|
449
|
+
# --path-input で計算する場合、幾何は .paths から来るのでここの立体は
|
|
450
|
+
# 使われない。ただし POKER の入力規約上、ゾーンには実在する立体が要り、
|
|
451
|
+
# 密度もここから引かれるため、材質ごとにダミーを1つ置く。
|
|
452
|
+
L.append('# [ダミー] 以下の body / zone は --path-input では幾何として使われません。')
|
|
453
|
+
L.append('# 幾何は .paths から読み込まれます。ここに置くのは、POKER の入力規約上')
|
|
454
|
+
L.append('# ゾーンに実在する立体が必要なことと、材質の密度をここから引くためです。')
|
|
455
|
+
L.append('# 立体どうしが重ならないよう x 方向に並べてあります。')
|
|
456
|
+
L.append('body:')
|
|
457
|
+
names = mfp_order or sorted(materials)
|
|
458
|
+
for i, m in enumerate(names):
|
|
459
|
+
x = i * 10
|
|
460
|
+
L.append(' - name: REF_%s' % m)
|
|
461
|
+
L.append(' type: RPP')
|
|
462
|
+
L.append(' min: %d -1 -1' % x)
|
|
463
|
+
L.append(' max: %d 1 1' % (x + 1))
|
|
464
|
+
L.append('')
|
|
465
|
+
L.append('zone:')
|
|
466
|
+
L.append(' - body_name: ATMOSPHERE')
|
|
467
|
+
L.append(' material: VOID')
|
|
468
|
+
for m in names:
|
|
469
|
+
L.append(' - body_name: REF_%s' % m)
|
|
470
|
+
L.append(' material: %s' % m)
|
|
471
|
+
# 密度は省略できない。CAD で PokerDensity が指定されていればその値、
|
|
472
|
+
# 無ければ材料ライブラリのカタログ密度を書く。
|
|
473
|
+
d = materials.get(m)
|
|
474
|
+
if not d:
|
|
475
|
+
d = lib_density.get(m)
|
|
476
|
+
if d:
|
|
477
|
+
L.append(' density: %g' % d)
|
|
478
|
+
L.append('')
|
|
479
|
+
L.append('transform: []')
|
|
480
|
+
L.append('')
|
|
481
|
+
|
|
482
|
+
# --- buildup_factor ---
|
|
483
|
+
# POKER は並び順でビルドアップ材料を選ぶため、透過線上で最も効く材質
|
|
484
|
+
# (mfp 合計が大きい材質) を先頭に置く。
|
|
485
|
+
L.append('# 並び順で選択されるビルドアップ材料が変わります。')
|
|
486
|
+
if mfp_order:
|
|
487
|
+
L.append('# 全経路の mfp 合計が大きい順に並べてあります。')
|
|
488
|
+
L.append('buildup_factor:')
|
|
489
|
+
for m in names:
|
|
490
|
+
if m in ('VOID', 'Air'):
|
|
491
|
+
continue
|
|
492
|
+
# ビルドアップ係数データがある材質に読み替える。
|
|
493
|
+
# lib_setting.dat の buildup_material に列挙された材質しか使えない。
|
|
494
|
+
# SUS_A や Source_Dry のようなカスタム材料は、実効原子番号が近い
|
|
495
|
+
# 標準材料(SUS_A なら Iron)に読み替える。POKER 側も同じ表を見る。
|
|
496
|
+
bu = buildup_equiv.get(m, m)
|
|
497
|
+
L.append(' - material: %s' % bu)
|
|
498
|
+
if bu != m:
|
|
499
|
+
L.append(' # %s の等価材料として指定' % m)
|
|
500
|
+
L.append(' use_slant_correction: false')
|
|
501
|
+
L.append(' use_finite_medium_correction: false')
|
|
502
|
+
L.append('')
|
|
503
|
+
|
|
504
|
+
# --- source ---
|
|
505
|
+
L.append('source:')
|
|
506
|
+
for s in sources:
|
|
507
|
+
if s.get('scatter'):
|
|
508
|
+
# 散布線源: 点線源の集まりとして展開する。
|
|
509
|
+
# 汚染密度から各点の強度を決めてあるので、ここでは並べるだけ。
|
|
510
|
+
L.append(' # %s: %s(%d 点, 合計 %.4g Bq)'
|
|
511
|
+
% (s['name'], s.get('kind', '散布'),
|
|
512
|
+
len(s['points']), s.get('total_bq', 0.0)))
|
|
513
|
+
for k, (x, y, z, inv) in enumerate(s['points']):
|
|
514
|
+
L.append(' - name: %s_%04d' % (s['name'], k + 1))
|
|
515
|
+
L.append(' type: POINT')
|
|
516
|
+
L.append(' position: %g %g %g' % (x, y, z))
|
|
517
|
+
L.append(' inventory:')
|
|
518
|
+
for nu, bq in inv:
|
|
519
|
+
L.append(' - nuclide: %s' % nu)
|
|
520
|
+
L.append(' radioactivity: %g' % bq)
|
|
521
|
+
L.append(' cutoff_rate: %g' % s.get('cutoff_rate', 1e-4))
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
L.append(' - name: %s' % s['name'])
|
|
525
|
+
L.append(' type: %s' % s['type'])
|
|
526
|
+
L.append(' inventory:')
|
|
527
|
+
for inv in s['inventory']:
|
|
528
|
+
L.append(' - nuclide: %s' % inv['nuclide'])
|
|
529
|
+
L.append(' radioactivity: %g' % inv['radioactivity'])
|
|
530
|
+
L.append(' cutoff_rate: %g' % s['cutoff_rate'])
|
|
531
|
+
if s['type'] == 'POINT':
|
|
532
|
+
L.append(' position: %s' % s['position'])
|
|
533
|
+
else:
|
|
534
|
+
L.append(' geometry:')
|
|
535
|
+
for k, v in s['geometry'].items():
|
|
536
|
+
L.append(' %s: %s' % (k, v))
|
|
537
|
+
L.append(' division:')
|
|
538
|
+
for ax, d in s['division'].items():
|
|
539
|
+
L.append(' %s:' % ax)
|
|
540
|
+
L.append(' number: %d' % d['number'])
|
|
541
|
+
L.append(' type: %s' % d['type'])
|
|
542
|
+
L.append('')
|
|
543
|
+
|
|
544
|
+
# --- detector ---
|
|
545
|
+
L.append('detector:')
|
|
546
|
+
for d in detectors:
|
|
547
|
+
L.append(' - name: %s' % d['name'])
|
|
548
|
+
L.append(' origin: %s' % d['origin'])
|
|
549
|
+
if 'grid' in d:
|
|
550
|
+
L.append(' grid:')
|
|
551
|
+
for g in d['grid']:
|
|
552
|
+
L.append(' - edge: %s' % g['edge'])
|
|
553
|
+
L.append(' number: %d' % g['number'])
|
|
554
|
+
L.append(' show_path_trace: %s' % ('true' if d['show_path_trace'] else 'false'))
|
|
555
|
+
L.append('')
|
|
556
|
+
return '\n'.join(L)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _nearest_buildup(mat, lib, avail):
|
|
560
|
+
# ビルドアップ係数データがある材質のうち、実効原子番号が最も近いものを返す。
|
|
561
|
+
# SUS_A -> Iron、Source_Dry -> Tungsten のような読み替え。
|
|
562
|
+
# POKER の等価材料テーブル(lib_equivalent.dat)があればそれを優先する。
|
|
563
|
+
try:
|
|
564
|
+
rho, comp = lib.materials[mat]
|
|
565
|
+
except Exception:
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
def zeff(composition):
|
|
569
|
+
# 実効原子番号(光電効果の m=2.94 乗則)
|
|
570
|
+
num = den = 0.0
|
|
571
|
+
for z, w in composition.items():
|
|
572
|
+
num += w * (z ** 2.94)
|
|
573
|
+
den += w
|
|
574
|
+
return (num / den) ** (1.0 / 2.94) if den > 0 else 0.0
|
|
575
|
+
|
|
576
|
+
target = zeff(comp)
|
|
577
|
+
best, best_d = None, None
|
|
578
|
+
for cand in avail:
|
|
579
|
+
if cand in ('VOID', 'Air'):
|
|
580
|
+
continue
|
|
581
|
+
try:
|
|
582
|
+
_, c2 = lib.materials[cand]
|
|
583
|
+
except Exception:
|
|
584
|
+
continue
|
|
585
|
+
d = abs(zeff(c2) - target)
|
|
586
|
+
if best_d is None or d < best_d:
|
|
587
|
+
best, best_d = cand, d
|
|
588
|
+
return best
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _source_mu(obj, lib, spec):
|
|
592
|
+
# 線源材質の線減弱係数。PokerMaterial が設定されていればその材質、
|
|
593
|
+
# 無ければ既定として Water を使う(多くの線源は水と同程度の減衰)。
|
|
594
|
+
#
|
|
595
|
+
# 参照エネルギーは spec.mu_energy、無ければ 0.662 MeV(Cs-137)。
|
|
596
|
+
# ここでは分割数を決めるだけなので、厳密なエネルギーは要らない。
|
|
597
|
+
mat = getattr(obj, 'PokerMaterial', None) or 'Water'
|
|
598
|
+
energy = float(spec.get('mu_energy', 0.6617))
|
|
599
|
+
try:
|
|
600
|
+
return lib.mu(mat, energy)
|
|
601
|
+
except Exception:
|
|
602
|
+
try:
|
|
603
|
+
return lib.mu('Water', energy)
|
|
604
|
+
except Exception:
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def main(spec_path):
|
|
609
|
+
spec = json.load(open(spec_path, encoding='utf-8-sig'))
|
|
610
|
+
fcstd = spec['fcstd']
|
|
611
|
+
out = spec.get('out') or os.path.splitext(fcstd)[0] + '.yaml'
|
|
612
|
+
|
|
613
|
+
name = os.path.splitext(os.path.basename(fcstd))[0]
|
|
614
|
+
if name in App.listDocuments():
|
|
615
|
+
App.closeDocument(name)
|
|
616
|
+
doc = App.openDocument(fcstd)
|
|
617
|
+
|
|
618
|
+
# mfp 順は .paths 生成後にしか分からないので、spec で渡された場合のみ使う
|
|
619
|
+
# カタログ密度を読む。CAD 側の PokerDensity が無い材質に使う。
|
|
620
|
+
lib = poker_lib.PokerLib(spec.get('poker_dir', r'C:\Poker'))
|
|
621
|
+
lib_density = dict((m, v[0]) for m, v in lib.materials.items())
|
|
622
|
+
|
|
623
|
+
# 散布線源のピッチを決めるのに検出器の位置が要るので、先に集める。
|
|
624
|
+
# 点線源近似では線源片の大きさが検出器までの距離に対して十分小さい
|
|
625
|
+
# 必要があり、既定ピッチはその距離から決まる。
|
|
626
|
+
det_positions = []
|
|
627
|
+
for o in doc.Objects:
|
|
628
|
+
if (getattr(o, 'PokerRole', None) or '').strip().lower() != 'detector':
|
|
629
|
+
continue
|
|
630
|
+
sh = getattr(o, 'Shape', None)
|
|
631
|
+
if sh is None or sh.isNull():
|
|
632
|
+
continue
|
|
633
|
+
c = sh.CenterOfMass
|
|
634
|
+
det_positions.append((c.x, c.y, c.z))
|
|
635
|
+
|
|
636
|
+
warnings = []
|
|
637
|
+
|
|
638
|
+
materials, sources, detectors = {}, [], []
|
|
639
|
+
for o in doc.Objects:
|
|
640
|
+
sh = getattr(o, 'Shape', None)
|
|
641
|
+
if sh is None or sh.isNull() or not sh.Solids:
|
|
642
|
+
continue
|
|
643
|
+
r = role_of(o)
|
|
644
|
+
if r == 'source':
|
|
645
|
+
# 散布指定(体積汚染・表面汚染)があるか
|
|
646
|
+
# PokerSourceType=volume、または PokerInner*/PokerOuter* が設定
|
|
647
|
+
# されていれば、点線源の集まりとして展開する。
|
|
648
|
+
# 散布線源のオブジェクトが材質を持つなら遮蔽体としても登録する。
|
|
649
|
+
# 表面汚染した配管の管壁は、線源であると同時に遮蔽体でもある。
|
|
650
|
+
# これを登録しないと管壁が透明になり、自己遮蔽が効かない。
|
|
651
|
+
m = getattr(o, 'PokerMaterial', None)
|
|
652
|
+
if m:
|
|
653
|
+
d = _num(getattr(o, 'PokerDensity', None))
|
|
654
|
+
if m not in materials or (d and not materials[m]):
|
|
655
|
+
materials[m] = d
|
|
656
|
+
|
|
657
|
+
sc = scatter.build_scatter_sources(o, doc, SCALE, det_positions, warnings)
|
|
658
|
+
if sc:
|
|
659
|
+
for s in sc:
|
|
660
|
+
s['scatter'] = True
|
|
661
|
+
s['cutoff_rate'] = _num(getattr(o, 'PokerCutoff', None), 1e-4)
|
|
662
|
+
s['kind'] = ('表面汚染' if s['name'].endswith(('_inner', '_outer'))
|
|
663
|
+
else '体積汚染')
|
|
664
|
+
sources.append(s)
|
|
665
|
+
else:
|
|
666
|
+
sources.append(read_source(o, spec.get('nuclides'), _source_mu(o, lib, spec)))
|
|
667
|
+
elif r == 'detector':
|
|
668
|
+
detectors.append(read_detector(o))
|
|
669
|
+
else:
|
|
670
|
+
m = getattr(o, 'PokerMaterial', None)
|
|
671
|
+
if not m:
|
|
672
|
+
raise SystemExit(
|
|
673
|
+
'遮蔽体 %s に PokerMaterial が設定されていません\n'
|
|
674
|
+
' 材質を設定するか、PokerRole を source/detector にしてください'
|
|
675
|
+
% o.Name)
|
|
676
|
+
d = _num(getattr(o, 'PokerDensity', None))
|
|
677
|
+
if m not in materials or (d and not materials[m]):
|
|
678
|
+
materials[m] = d
|
|
679
|
+
|
|
680
|
+
if not sources:
|
|
681
|
+
raise SystemExit('PokerRole=source のオブジェクトがありません')
|
|
682
|
+
if not detectors:
|
|
683
|
+
raise SystemExit('PokerRole=detector のオブジェクトがありません')
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# ビルドアップ係数データがある材質への読み替え表を作る。
|
|
687
|
+
# lib_setting.dat の buildup_material に無い材質(SUS_A, Source_Dry など)は
|
|
688
|
+
# そのままでは POKER が受け付けない。実効原子番号が近い標準材料に読み替える。
|
|
689
|
+
# spec.buildup_equivalent で明示指定もできる。
|
|
690
|
+
buildup_equiv = dict(spec.get('buildup_equivalent', {}))
|
|
691
|
+
try:
|
|
692
|
+
avail = set(lib.single)
|
|
693
|
+
except Exception:
|
|
694
|
+
avail = set()
|
|
695
|
+
for m in materials:
|
|
696
|
+
if m in buildup_equiv or m in avail or m in ('VOID', 'Air'):
|
|
697
|
+
continue
|
|
698
|
+
e = _nearest_buildup(m, lib, avail)
|
|
699
|
+
if e:
|
|
700
|
+
buildup_equiv[m] = e
|
|
701
|
+
|
|
702
|
+
text = build_yaml(doc, materials, sources, detectors,
|
|
703
|
+
spec.get('mfp_order'), lib_density, buildup_equiv)
|
|
704
|
+
open(out, 'w', encoding='utf-8').write(text)
|
|
705
|
+
|
|
706
|
+
rep = {
|
|
707
|
+
'out': out,
|
|
708
|
+
'materials': sorted(materials),
|
|
709
|
+
'sources': [s['name'] for s in sources],
|
|
710
|
+
'detectors': [d['name'] for d in detectors],
|
|
711
|
+
'scatter_sources': [{'name': s['name'], 'points': len(s['points']),
|
|
712
|
+
'total_bq': s['total_bq']}
|
|
713
|
+
for s in sources if s.get('scatter')],
|
|
714
|
+
'warnings': warnings,
|
|
715
|
+
}
|
|
716
|
+
print(json.dumps(rep, ensure_ascii=False, indent=2))
|
|
717
|
+
return rep
|
package/tools/scatter.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# scatter.py -- 任意形状への点線源散布
|
|
2
|
+
#
|
|
3
|
+
# POKER の立体型(POINT / SPH / RCC / RPP / BOX)で表現できない形状を線源として
|
|
4
|
+
# 扱う。形状の内部(または面上)に点線源を格子状に配置し、指定した汚染密度と
|
|
5
|
+
# 矛盾しない強度を配分する。
|
|
6
|
+
#
|
|
7
|
+
# 用途:
|
|
8
|
+
# 体積汚染 不定形の廃棄物、スラッジ、曲がった配管内の流体 Bq/cm3
|
|
9
|
+
# 表面汚染 床の汚染、配管内面の付着、機器表面 Bq/cm2
|
|
10
|
+
#
|
|
11
|
+
# 決定論的な格子配置を使う。MCNP/PHITS は棄却法(乱数)で点を撒くが、点減衰核
|
|
12
|
+
# では各点の強度を確定値として決める必要がある。乱数だと同じ入力で答えが変わり、
|
|
13
|
+
# 収束解析ができない。
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 点数がこれを超えたら警告する。止めはしない(利用者が意図している場合もある)。
|
|
19
|
+
SCATTER_WARN_POINTS = 10000
|
|
20
|
+
|
|
21
|
+
# ピッチの既定値を決めるときの基準。
|
|
22
|
+
# 最も近い検出器までの距離の何分の1にするか。
|
|
23
|
+
# 点線源近似では、線源の大きさが距離に対して十分小さい必要がある。
|
|
24
|
+
# 1/10 なら立体角の誤差が 1% 程度に収まる。
|
|
25
|
+
PITCH_DISTANCE_RATIO = 10.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _bbox_grid(bb, pitch_mm):
|
|
29
|
+
# 外接直方体を pitch で刻んだ格子点(セル中心)を返す
|
|
30
|
+
nx = max(1, int(math.ceil(bb.XLength / pitch_mm)))
|
|
31
|
+
ny = max(1, int(math.ceil(bb.YLength / pitch_mm)))
|
|
32
|
+
nz = max(1, int(math.ceil(bb.ZLength / pitch_mm)))
|
|
33
|
+
dx, dy, dz = bb.XLength / nx, bb.YLength / ny, bb.ZLength / nz
|
|
34
|
+
for i in range(nx):
|
|
35
|
+
for j in range(ny):
|
|
36
|
+
for k in range(nz):
|
|
37
|
+
yield (bb.XMin + dx * (i + 0.5),
|
|
38
|
+
bb.YMin + dy * (j + 0.5),
|
|
39
|
+
bb.ZMin + dz * (k + 0.5))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def scatter_volume(shape, pitch_mm, total_bq, scale):
|
|
43
|
+
"""ソリッドの内部に点を散布する。
|
|
44
|
+
|
|
45
|
+
各点の強度は総放射能の等分。セルごとの内部体積を厳密に求めるには
|
|
46
|
+
ブーリアン演算が要り重いので、等分で済ませる。総量は Shape.Volume から
|
|
47
|
+
出すので必ず合う。縁のセルで位置がずれるのが誤差だが、ピッチを細かく
|
|
48
|
+
すれば小さくなる。
|
|
49
|
+
|
|
50
|
+
返り値: [(x, y, z, bq), ...] 座標は POKER 単位
|
|
51
|
+
"""
|
|
52
|
+
import FreeCAD as App
|
|
53
|
+
bb = shape.BoundBox
|
|
54
|
+
pts = []
|
|
55
|
+
for (x, y, z) in _bbox_grid(bb, pitch_mm):
|
|
56
|
+
if shape.isInside(App.Vector(x, y, z), 1e-7, True):
|
|
57
|
+
pts.append((x, y, z))
|
|
58
|
+
if not pts:
|
|
59
|
+
return []
|
|
60
|
+
a = total_bq / len(pts)
|
|
61
|
+
return [(x * scale, y * scale, z * scale, a) for (x, y, z) in pts]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cell_area(surf, ua, ub, va, vb):
|
|
65
|
+
# パラメータ空間のセルが面上で占める面積を、4隅から概算する
|
|
66
|
+
p00 = surf.value(ua, va)
|
|
67
|
+
p10 = surf.value(ub, va)
|
|
68
|
+
p01 = surf.value(ua, vb)
|
|
69
|
+
p11 = surf.value(ub, vb)
|
|
70
|
+
a1 = (p10 - p00).cross(p01 - p00).Length / 2.0
|
|
71
|
+
a2 = (p10 - p11).cross(p01 - p11).Length / 2.0
|
|
72
|
+
return a1 + a2
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def scatter_surface(face, pitch_mm, total_bq, scale):
|
|
76
|
+
"""面の上に点を散布する。
|
|
77
|
+
|
|
78
|
+
パラメータ空間を等間隔に刻むが、面上で等間隔になるとは限らない。
|
|
79
|
+
実測では球面で セル面積が 4.23 倍ばらつく(極で潰れる)。等分すると
|
|
80
|
+
極付近に強度が集中するので、セル面積で重み付けする。
|
|
81
|
+
|
|
82
|
+
平面と円筒では重みが一様になるので、実務で多い床や配管では面積等分と
|
|
83
|
+
同じ結果になる。
|
|
84
|
+
|
|
85
|
+
返り値: [(x, y, z, bq), ...]
|
|
86
|
+
"""
|
|
87
|
+
surf = face.Surface
|
|
88
|
+
u0, u1, v0, v1 = face.ParameterRange
|
|
89
|
+
|
|
90
|
+
# パラメータ空間の刻み数を決める。
|
|
91
|
+
# u, v 方向それぞれが面上で何 mm に相当するかを、複数点で測って平均する。
|
|
92
|
+
# 1 本の線分で測ると、円筒の u(角度 0〜2π)や自由曲面で破綻する。
|
|
93
|
+
def span(fix_v):
|
|
94
|
+
# v を固定して u 方向の弧長を折れ線で概算
|
|
95
|
+
m, L = 8, 0.0
|
|
96
|
+
prev = surf.value(u0, fix_v)
|
|
97
|
+
for t in range(1, m + 1):
|
|
98
|
+
cur = surf.value(u0 + (u1 - u0) * t / m, fix_v)
|
|
99
|
+
L += (cur - prev).Length
|
|
100
|
+
prev = cur
|
|
101
|
+
return L
|
|
102
|
+
|
|
103
|
+
def span_v(fix_u):
|
|
104
|
+
m, L = 8, 0.0
|
|
105
|
+
prev = surf.value(fix_u, v0)
|
|
106
|
+
for t in range(1, m + 1):
|
|
107
|
+
cur = surf.value(fix_u, v0 + (v1 - v0) * t / m)
|
|
108
|
+
L += (cur - prev).Length
|
|
109
|
+
prev = cur
|
|
110
|
+
return L
|
|
111
|
+
|
|
112
|
+
# 3 箇所で測って平均(曲率が場所で変わる面に備える)
|
|
113
|
+
du = sum(span(v0 + (v1 - v0) * t) for t in (0.25, 0.5, 0.75)) / 3.0
|
|
114
|
+
dv = sum(span_v(u0 + (u1 - u0) * t) for t in (0.25, 0.5, 0.75)) / 3.0
|
|
115
|
+
nu = max(1, int(math.ceil(du / pitch_mm)))
|
|
116
|
+
nv = max(1, int(math.ceil(dv / pitch_mm)))
|
|
117
|
+
|
|
118
|
+
cells = []
|
|
119
|
+
for i in range(nu):
|
|
120
|
+
ua = u0 + (u1 - u0) * i / nu
|
|
121
|
+
ub = u0 + (u1 - u0) * (i + 1) / nu
|
|
122
|
+
uc = (ua + ub) / 2.0
|
|
123
|
+
for j in range(nv):
|
|
124
|
+
va = v0 + (v1 - v0) * j / nv
|
|
125
|
+
vb = v0 + (v1 - v0) * (j + 1) / nv
|
|
126
|
+
vc = (va + vb) / 2.0
|
|
127
|
+
# トリムされた面では、パラメータ範囲内でも面の外側があり得る
|
|
128
|
+
try:
|
|
129
|
+
if not face.isPartOfDomain(uc, vc):
|
|
130
|
+
continue
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
w = _cell_area(surf, ua, ub, va, vb)
|
|
134
|
+
if w <= 0:
|
|
135
|
+
continue
|
|
136
|
+
p = surf.value(uc, vc)
|
|
137
|
+
cells.append((p, w))
|
|
138
|
+
|
|
139
|
+
if not cells:
|
|
140
|
+
return []
|
|
141
|
+
wsum = sum(w for _, w in cells)
|
|
142
|
+
return [(p.x * scale, p.y * scale, p.z * scale, total_bq * w / wsum)
|
|
143
|
+
for (p, w) in cells]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def default_pitch(shape, detectors_mm):
|
|
147
|
+
"""ピッチの既定値。
|
|
148
|
+
|
|
149
|
+
最も近い検出器までの距離の 1/PITCH_DISTANCE_RATIO とする。点線源近似では
|
|
150
|
+
線源片の大きさが距離に対して十分小さい必要があり、1/10 なら立体角の誤差が
|
|
151
|
+
1% 程度に収まる。
|
|
152
|
+
|
|
153
|
+
ただし形状より粗くしても点が入らないので、形状の最短辺の 1/4 を上限とする
|
|
154
|
+
(最短辺に最低 4 点は入るようにする)。細長い配管では直径方向が支配する。
|
|
155
|
+
|
|
156
|
+
detectors_mm: [(x, y, z), ...] CAD 単位(mm)の検出器位置
|
|
157
|
+
"""
|
|
158
|
+
import FreeCAD as App
|
|
159
|
+
bb = shape.BoundBox
|
|
160
|
+
try:
|
|
161
|
+
c = shape.CenterOfMass
|
|
162
|
+
except AttributeError:
|
|
163
|
+
c = App.Vector(bb.XMin + bb.XLength / 2,
|
|
164
|
+
bb.YMin + bb.YLength / 2,
|
|
165
|
+
bb.ZMin + bb.ZLength / 2)
|
|
166
|
+
|
|
167
|
+
# 形状の最短辺から上限を決める。ゼロ厚(面)の方向は除く。
|
|
168
|
+
sides = [s for s in (bb.XLength, bb.YLength, bb.ZLength) if s > 1e-6]
|
|
169
|
+
cap = max(1.0, min(sides) / 4.0) if sides else 1.0
|
|
170
|
+
|
|
171
|
+
dmin = None
|
|
172
|
+
for (x, y, z) in (detectors_mm or []):
|
|
173
|
+
d = (App.Vector(x, y, z) - c).Length
|
|
174
|
+
if dmin is None or d < dmin:
|
|
175
|
+
dmin = d
|
|
176
|
+
|
|
177
|
+
if dmin is None or dmin <= 0:
|
|
178
|
+
return cap
|
|
179
|
+
return max(1.0, min(cap, dmin / PITCH_DISTANCE_RATIO))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def clip_face(face, region_shape):
|
|
183
|
+
"""面を領域立体で切り取る。
|
|
184
|
+
|
|
185
|
+
自由曲面の一部だけが汚染している場合、その範囲を面として作るのは GUI では
|
|
186
|
+
難しい。汚染範囲を単純な立体(直方体など)で囲めば、その共通部分として
|
|
187
|
+
切り出せる。MCNP のクッキーカッター(サンプリング領域を別セルで制限する
|
|
188
|
+
手法)と同じ考え方。
|
|
189
|
+
|
|
190
|
+
返り値: 切り取られた面のリスト。共通部分が無ければ空。
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
cut = face.common(region_shape)
|
|
194
|
+
except Exception:
|
|
195
|
+
return []
|
|
196
|
+
return [f for f in cut.Faces if f.Area > 1e-9]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def clip_solid(shape, region_shape):
|
|
200
|
+
"""ソリッドを領域立体で切り取る。体積汚染の範囲指定に使う。"""
|
|
201
|
+
try:
|
|
202
|
+
cut = shape.common(region_shape)
|
|
203
|
+
except Exception:
|
|
204
|
+
return None
|
|
205
|
+
return cut if (cut.Solids and cut.Volume > 1e-9) else None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def pick_faces(shape, which):
|
|
209
|
+
"""ソリッドから汚染面を選ぶ。
|
|
210
|
+
|
|
211
|
+
which:
|
|
212
|
+
'inner' 内面(法線が重心を向く面)
|
|
213
|
+
'outer' 外面
|
|
214
|
+
'top' 上面(法線の Z 成分が正で最大)
|
|
215
|
+
'bottom' 下面
|
|
216
|
+
'all' すべての面
|
|
217
|
+
数字 Face のインデックス(1 始まり)
|
|
218
|
+
|
|
219
|
+
内外の判定は、面の中心から法線方向に少し進んだ点がソリッドの内部かで見る。
|
|
220
|
+
"""
|
|
221
|
+
import FreeCAD as App
|
|
222
|
+
faces = list(shape.Faces)
|
|
223
|
+
if not faces:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
w = (which or 'all').strip().lower()
|
|
227
|
+
|
|
228
|
+
if w.isdigit():
|
|
229
|
+
i = int(w) - 1
|
|
230
|
+
return [faces[i]] if 0 <= i < len(faces) else []
|
|
231
|
+
|
|
232
|
+
if w == 'all':
|
|
233
|
+
return faces
|
|
234
|
+
|
|
235
|
+
if w in ('top', 'bottom'):
|
|
236
|
+
sign = 1.0 if w == 'top' else -1.0
|
|
237
|
+
best, best_z = None, None
|
|
238
|
+
for f in faces:
|
|
239
|
+
try:
|
|
240
|
+
u0, u1, v0, v1 = f.ParameterRange
|
|
241
|
+
n = f.normalAt((u0 + u1) / 2, (v0 + v1) / 2)
|
|
242
|
+
except Exception:
|
|
243
|
+
continue
|
|
244
|
+
if n.z * sign < 0.7: # ほぼ真上/真下を向く面だけ
|
|
245
|
+
continue
|
|
246
|
+
z = f.CenterOfMass.z * sign
|
|
247
|
+
if best_z is None or z > best_z:
|
|
248
|
+
best, best_z = f, z
|
|
249
|
+
return [best] if best else []
|
|
250
|
+
|
|
251
|
+
if w in ('inner', 'outer'):
|
|
252
|
+
# 管状の形状の内面/外面を分ける。
|
|
253
|
+
#
|
|
254
|
+
# 曲がった配管には直線の中心軸が無いので、軸からの距離では判定
|
|
255
|
+
# できない(実測で誤判定した)。法線の向きも使えない。OCC は各面の
|
|
256
|
+
# 表側が外部を向くよう向き付けするので、内面も外面も「法線の逆側が
|
|
257
|
+
# 形状の内部」で同じになる。
|
|
258
|
+
#
|
|
259
|
+
# 面から法線の逆方向にレイを飛ばし、形状と交わる回数で判定する。
|
|
260
|
+
# 外面から内向きに飛ばす -> 管壁を抜けて中空に出て、反対側の壁を
|
|
261
|
+
# 通り、外へ抜ける
|
|
262
|
+
# 内面から内向き(=中空側)に飛ばす -> すぐ中空で、反対側の壁だけ
|
|
263
|
+
# 交点の数が違うので分けられるが、接線方向で不安定になる。
|
|
264
|
+
#
|
|
265
|
+
# 実用上は「外面の方が広い」で足りる。同じ長さの管なら半径が大きい
|
|
266
|
+
# 分だけ外面が広く、曲がっていれば外側の曲率でさらに広がる。
|
|
267
|
+
# 端面は法線が隣接面の法線とほぼ直交するので、それで除く。
|
|
268
|
+
cand = []
|
|
269
|
+
for f in faces:
|
|
270
|
+
try:
|
|
271
|
+
u0, u1, v0, v1 = f.ParameterRange
|
|
272
|
+
uc, vc = (u0 + u1) / 2.0, (v0 + v1) / 2.0
|
|
273
|
+
f.normalAt(uc, vc)
|
|
274
|
+
except Exception:
|
|
275
|
+
continue
|
|
276
|
+
cand.append(f)
|
|
277
|
+
if len(cand) < 2:
|
|
278
|
+
return cand
|
|
279
|
+
# 面積の大きい2面を内外とみなす。端面は管壁より小さい。
|
|
280
|
+
cand.sort(key=lambda f: -f.Area)
|
|
281
|
+
big = cand[:2]
|
|
282
|
+
big.sort(key=lambda f: -f.Area)
|
|
283
|
+
return [big[0]] if w == 'outer' else [big[1]]
|
|
284
|
+
|
|
285
|
+
return faces
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# CAD プロパティの読み取り
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def parse_nuclide_values(text):
|
|
294
|
+
"""'Cs137:1.0e6, Co60:2.0e5' -> [('Cs137', 1.0e6), ('Co60', 2.0e5)]
|
|
295
|
+
|
|
296
|
+
値の意味(濃度か総放射能か)は呼び出し側の文脈で決まる。
|
|
297
|
+
"""
|
|
298
|
+
import re
|
|
299
|
+
out = []
|
|
300
|
+
for part in re.split(r'[,;]', text or ''):
|
|
301
|
+
part = part.strip()
|
|
302
|
+
if not part:
|
|
303
|
+
continue
|
|
304
|
+
m = re.match(r'^([A-Za-z]+[-_]?\d+m?)\s*[:=]\s*([\d.eE+-]+)$', part)
|
|
305
|
+
if not m:
|
|
306
|
+
raise SystemExit(
|
|
307
|
+
'核種の書式が不正です: %r\n'
|
|
308
|
+
' 例: "Cs137:1.0e6, Co60:2.0e5"' % part)
|
|
309
|
+
out.append((m.group(1).replace('-', '').replace('_', ''), float(m.group(2))))
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def resolve_inventory(obj, amount, prefix=''):
|
|
314
|
+
"""CAD のプロパティから核種と量を決める。
|
|
315
|
+
|
|
316
|
+
3 つの指定方法を許す:
|
|
317
|
+
(a) PokerNuclides = 'Cs137:1.0e6, Co60:2.0e5'
|
|
318
|
+
核種ごとの濃度(または総放射能)を直接指定
|
|
319
|
+
(b) PokerComposition = 'Cs137:5, Co60:1' と PokerConcentration = '1.0e6'
|
|
320
|
+
組成比と総量。比率の合計で正規化して配分する。
|
|
321
|
+
組成が既知で濃度だけ違う場合に使う
|
|
322
|
+
(c) PokerActivity = 'Co60:3.7e10'
|
|
323
|
+
総放射能を直接
|
|
324
|
+
|
|
325
|
+
amount: 濃度指定のときに掛ける量(体積 cm3 または面積 cm2)。
|
|
326
|
+
総放射能指定のときは使わない。
|
|
327
|
+
prefix: 面ごとに別の濃度を与える場合の接頭辞('Inner' / 'Outer')
|
|
328
|
+
|
|
329
|
+
返り値: [(核種, Bq), ...]
|
|
330
|
+
"""
|
|
331
|
+
def get(name):
|
|
332
|
+
key = (prefix or 'Poker') + name
|
|
333
|
+
return (getattr(obj, key, None) or '').strip()
|
|
334
|
+
|
|
335
|
+
# (c) 総放射能
|
|
336
|
+
act = get('Activity')
|
|
337
|
+
if act:
|
|
338
|
+
return parse_nuclide_values(act)
|
|
339
|
+
|
|
340
|
+
# (b) 組成比 + 総濃度
|
|
341
|
+
comp = get('Composition')
|
|
342
|
+
conc = get('Concentration')
|
|
343
|
+
if comp and conc:
|
|
344
|
+
pairs = parse_nuclide_values(comp)
|
|
345
|
+
total_ratio = sum(v for _, v in pairs)
|
|
346
|
+
if total_ratio <= 0:
|
|
347
|
+
raise SystemExit('%s の組成比の合計が 0 です' % obj.Name)
|
|
348
|
+
try:
|
|
349
|
+
c = float(conc)
|
|
350
|
+
except ValueError:
|
|
351
|
+
raise SystemExit('%s の濃度が数値ではありません: %r' % (obj.Name, conc))
|
|
352
|
+
return [(n, c * amount * v / total_ratio) for n, v in pairs]
|
|
353
|
+
|
|
354
|
+
# (a) 核種ごとの濃度
|
|
355
|
+
nuc = get('Nuclides')
|
|
356
|
+
if nuc:
|
|
357
|
+
pairs = parse_nuclide_values(nuc)
|
|
358
|
+
return [(n, v * amount) for n, v in pairs]
|
|
359
|
+
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def find_region(obj, doc):
|
|
365
|
+
"""この線源の汚染範囲を制限する立体を探す。
|
|
366
|
+
|
|
367
|
+
別オブジェクトに PokerRegionFor でこの線源を指させる。
|
|
368
|
+
App::PropertyLink を使うので、オブジェクト名を変えてもリンクは保たれる
|
|
369
|
+
(名前の一致に頼ると壊れる)。
|
|
370
|
+
"""
|
|
371
|
+
for o in doc.Objects:
|
|
372
|
+
tgt = getattr(o, 'PokerRegionFor', None)
|
|
373
|
+
if tgt is None:
|
|
374
|
+
continue
|
|
375
|
+
# リンクなら Object そのもの、文字列なら名前で比較
|
|
376
|
+
name = getattr(tgt, 'Name', None) or str(tgt)
|
|
377
|
+
if name == obj.Name:
|
|
378
|
+
sh = getattr(o, 'Shape', None)
|
|
379
|
+
if sh is not None and not sh.isNull():
|
|
380
|
+
return sh
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def build_scatter_sources(obj, doc, scale, detectors_mm, warn):
|
|
385
|
+
"""CAD オブジェクトから散布線源を作る。
|
|
386
|
+
|
|
387
|
+
返り値: [{'name':..., 'points':[(x,y,z,{核種:Bq}), ...]}, ...]
|
|
388
|
+
線源の種類ごとに 1 つ(体積/内面/外面)
|
|
389
|
+
|
|
390
|
+
warn: 警告を受け取るリスト
|
|
391
|
+
"""
|
|
392
|
+
sh = obj.Shape
|
|
393
|
+
region = find_region(obj, doc)
|
|
394
|
+
pitch = _num(getattr(obj, 'PokerPointSpacing', None)) or default_pitch(sh, detectors_mm)
|
|
395
|
+
|
|
396
|
+
out = []
|
|
397
|
+
|
|
398
|
+
# --- 体積汚染 ---
|
|
399
|
+
vol_spec = (getattr(obj, 'PokerNuclides', None) or
|
|
400
|
+
getattr(obj, 'PokerComposition', None) or
|
|
401
|
+
getattr(obj, 'PokerActivity', None))
|
|
402
|
+
stype = (getattr(obj, 'PokerSourceType', None) or '').strip().lower()
|
|
403
|
+
if stype == 'volume' and vol_spec:
|
|
404
|
+
target = clip_solid(sh, region) if region is not None else sh
|
|
405
|
+
if target is None:
|
|
406
|
+
warn.append('%s: 領域との共通部分がありません' % obj.Name)
|
|
407
|
+
else:
|
|
408
|
+
vol_cm3 = target.Volume * (scale ** 3)
|
|
409
|
+
inv = resolve_inventory(obj, vol_cm3)
|
|
410
|
+
if inv:
|
|
411
|
+
total = sum(a for _, a in inv)
|
|
412
|
+
pts = scatter_volume(target, pitch, total, scale)
|
|
413
|
+
if pts:
|
|
414
|
+
out.append(_mk(obj.Name, pts, inv, total))
|
|
415
|
+
else:
|
|
416
|
+
warn.append('%s: 点が 1 つも入りませんでした(ピッチ %.1f mm)'
|
|
417
|
+
% (obj.Name, pitch))
|
|
418
|
+
|
|
419
|
+
# --- 表面汚染(内面・外面を別々に)---
|
|
420
|
+
for side, prefix in (('inner', 'PokerInner'), ('outer', 'PokerOuter')):
|
|
421
|
+
spec = (getattr(obj, prefix + 'Nuclides', None) or
|
|
422
|
+
getattr(obj, prefix + 'Composition', None) or
|
|
423
|
+
getattr(obj, prefix + 'Activity', None))
|
|
424
|
+
if not spec:
|
|
425
|
+
continue
|
|
426
|
+
faces = pick_faces(sh, side)
|
|
427
|
+
if region is not None:
|
|
428
|
+
clipped = []
|
|
429
|
+
for f in faces:
|
|
430
|
+
clipped += clip_face(f, region)
|
|
431
|
+
faces = clipped
|
|
432
|
+
if not faces:
|
|
433
|
+
warn.append('%s: %s 面が見つかりません' % (obj.Name, side))
|
|
434
|
+
continue
|
|
435
|
+
area_cm2 = sum(f.Area for f in faces) * (scale ** 2)
|
|
436
|
+
inv = resolve_inventory(obj, area_cm2, prefix=prefix)
|
|
437
|
+
if not inv:
|
|
438
|
+
continue
|
|
439
|
+
total = sum(a for _, a in inv)
|
|
440
|
+
pts = []
|
|
441
|
+
for f in faces:
|
|
442
|
+
a = f.Area * (scale ** 2)
|
|
443
|
+
pts += scatter_surface(f, pitch, total * a / area_cm2, scale)
|
|
444
|
+
if pts:
|
|
445
|
+
out.append(_mk('%s_%s' % (obj.Name, side), pts, inv, total))
|
|
446
|
+
else:
|
|
447
|
+
warn.append('%s: %s 面に点が入りませんでした' % (obj.Name, side))
|
|
448
|
+
|
|
449
|
+
# 点数の警告
|
|
450
|
+
n = sum(len(s['points']) for s in out)
|
|
451
|
+
if n > SCATTER_WARN_POINTS:
|
|
452
|
+
warn.append('%s: 点線源が %d 個になります(ピッチ %.1f mm)。'
|
|
453
|
+
'計算時間が長くなるので PokerPointSpacing での調整を検討してください'
|
|
454
|
+
% (obj.Name, n, pitch))
|
|
455
|
+
return out
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _mk(name, pts, inv, total):
|
|
459
|
+
"""散布点を線源の形にする。核種の比率は全点で同じ。"""
|
|
460
|
+
ratio = [(nu, a / total) for nu, a in inv] if total > 0 else []
|
|
461
|
+
return {
|
|
462
|
+
'name': name,
|
|
463
|
+
'pitch_points': len(pts),
|
|
464
|
+
'total_bq': total,
|
|
465
|
+
'points': [(x, y, z, [(nu, bq * r) for nu, r in ratio])
|
|
466
|
+
for (x, y, z, bq) in pts],
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _num(v):
|
|
471
|
+
try:
|
|
472
|
+
return float(v)
|
|
473
|
+
except (TypeError, ValueError):
|
|
474
|
+
return None
|