作品一覧へ戻る

AI ツール · A1

解析結果のバッチ処理と熱抵抗計算

熱解析の表形式エクスポートファイルをまとめて読み込み、接合部–冷却液間の熱抵抗とその層別内訳を計算し、異常行を検出し、設計案と冷却条件で比較して、集計表・グラフ・レポートを出力する。

個人プロジェクト(自主制作) · 2026 年 10 月に作成した公開用デモ · サンプルデータは本サイトの汎用モデルで生成し、雇用主の資料は含まない

分担手法と要件の定義、結果の確認は本人が担当し、コードは AI コーディングツールで実装して本人が確認した。本事例は 2026 年 10 月に作成した公開用デモであり、汎用モデルと自作データを用いている。業務で使用している同種のツールは機密のため公開していない。

Python ツールの実測(サンプルデータ)

エクスポートファイル
3
データ行
38
有効な解析ケース
36
検出した異常行
2
読み込み・計算・チェック
1.4ms

リポジトリの output/run.json(サンドボックスで実際に 1 回実行した記録)の値。グラフとレポートの出力にさらに 0.44 s かかった。

処理の流れ

  1. エクスポートファイル

    設計案ごとにパラメトリックスイープ 1 回分を 1 つのテキスト表として出力する。% で始まるヘッダー行と列名の後に数値行が続く。

  2. 読み込み

    ヘッダーから設計案名と説明を読み取る。列名の別名で h、P、冷却液温度、Tj,max を特定し、単位を換算する(K → °C、kW → W)。

  3. 計算とチェック

    Rth = (Tj,max − T_cool) / P。各層の下面温度があれば層別に分解する。非数値、Rth ≤ 0、層別の合計、同じ h での P に対する単調性、Rth の外れ値、接合部温度の上限を確認する。

  4. 比較

    同じ h で平均 Rth により順位を付け、最良案との差を示す。隣り合う h の間で Rth の変化率と dRth/d(ln h) を求める。

  5. グラフとレポート

    summary・flagged・comparison・sensitivity の 4 つの CSV、3 枚の SVG グラフ、Markdown レポートを出力する。

ブラウザで実行

このページにはサンプルのエクスポートファイル 3 つ(設計案 A・B・C、計 38 行。うち 2 行は意図的に入れた異常)を収録している。実行すると、Python ツールと同じ読み込み・計算・チェックをブラウザ内の TypeScript で再現する。

ローカルファイルはブラウザ内でのみ読み込んで計算し、アップロードしない。形式は下の「制約」と README を参照。

サンプルファイルdesign-A_al2o3-0.38mm.txtdesign-B_si3n4-0.32mm.txtdesign-C_aln-0.63mm.txt

Python ツール

tools/sim-batch-rth のコマンドラインツール。batch_rth.py は Python 標準ライブラリのみで読み込み・計算・チェック・比較を行い、run_batch.py は matplotlib でグラフを描いてレポートを書く。pytest の単体テストとバージョン固定の requirements.txt を含む。

サンドボックスでの実際の 1 回の実行(python run_batch.py examples --out output)
エクスポートファイル3
読み込んだ行38
有効 / 異常 / 接合部温度の注意36 / 2 / 6
読み込み・計算・チェック0.0014 s
グラフとレポート0.444 s
スクリプト全体(matplotlib の読み込みを含む)0.794 s
環境Python 3.13.16 · matplotlib 3.11.2
Python ツールが出力した層別熱抵抗のグラフ(output/rth_breakdown.svg、h = 10000 W/(m²·K))。
Python ツールが出力した層別熱抵抗のグラフ(output/rth_breakdown.svg、h = 10000 W/(m²·K))。

主要な関数

parse_export:ヘッダー行と列名の読み込み
def split_name_unit(label: str) -> tuple[str, str]:
    """'h (W/(m^2*K))' -> ('h', 'W/(m^2*K)'); nested parentheses in the unit are kept."""
    label = label.strip()
    if not label.endswith(")"):
        return label, ""
    depth = 0
    for index in range(len(label) - 1, -1, -1):
        depth += {")": 1, "(": -1}.get(label[index], 0)
        if depth == 0:
            return label[:index].strip(), label[index + 1:-1].strip()
    return label, ""


@dataclass
class Export:
    file: str
    design: str
    meta: dict[str, str]
    columns: list[tuple[str, str]]
    rows: list[tuple[int, list[str]]]  # (1-based line number, raw cells)


def parse_export(name: str, text: str) -> Export:
    """COMSOL-style table export: '% Key: value' header lines, the last '%' line names the
    columns (separated by two or more spaces or tabs), then whitespace-separated numbers."""
    meta: dict[str, str] = {}
    header_lines: list[str] = []
    rows: list[tuple[int, list[str]]] = []
    last_key = None
    for number, line in enumerate(text.splitlines(), start=1):
        if line.startswith("%"):
            content = line[1:].strip()
            header_lines.append(content)
            match = re.match(r"^([A-Za-z][\w ]*?):\s+(.*)$", content)
            if match and not re.search(r"\s{2,}\S+\s*\(", match[2]):
                last_key = match[1].strip()
                meta[last_key] = match[2].strip()
            elif last_key and content and not re.search(r"\S\s{2,}\S", content):
                meta[last_key] += " " + content
        elif line.strip():
            rows.append((number, line.split()))
    if not header_lines:
        raise ValueError("no '%' header lines")
    columns = [split_name_unit(cell) for cell in re.split(r"\t+|\s{2,}", header_lines[-1]) if cell]
    stem = Path(name).stem
    return Export(file=name, design=meta.get("Design", stem), meta=meta, columns=columns, rows=rows)
cases_from_export:熱抵抗と層別内訳の計算
def cases_from_export(export: Export) -> list[Case]:
    """Rth = (Tj_max - T_cool) / P for each row; with T_<layer>_bot probes, each layer's share is
    (T_top - T_bottom) / P, where a layer's top is the bottom of the layer above (chip top = Tj)."""
    cols = export.columns
    index = {role: _find(cols, role) for role in ("h", "power", "t_cool", "tj")}
    layer_columns = [(i, m[1]) for i, (name, _u) in enumerate(cols) if (m := LAYER_PATTERN.match(name))]
    cases = []
    for line, cells in export.rows:
        if len(cells) != len(cols):
            case = Case(export.design, export.file, line, math.nan, math.nan, math.nan, math.nan)
            case.errors.append("column_count")
            cases.append(case)
            continue
        value = lambda i: _number(cells[i])  # noqa: E731
        temperature = lambda i: _to_celsius(value(i), cols[i][1])  # noqa: E731
        case = Case(
            design=export.design, file=export.file, line=line,
            h=value(index["h"]), power=_to_watts(value(index["power"]), cols[index["power"]][1]),
            t_cool=temperature(index["t_cool"]), tj=temperature(index["tj"]),
        )
        case.rth = (case.tj - case.t_cool) / case.power if case.power else math.nan
        top = case.tj
        for column, layer in layer_columns:
            bottom = temperature(column)
            case.layers.append((layer, (top - bottom) / case.power if case.power else math.nan))
            top = bottom
        if layer_columns:
            case.layers.append(("convection", (top - case.t_cool) / case.power if case.power else math.nan))
        cases.append(case)
    return cases
check_cases:物理的な妥当性のチェック
def check_cases(cases: list[Case], tj_limit: float = TJ_LIMIT_C, rth_tolerance: float = RTH_TOLERANCE) -> None:
    """Row checks first; group checks (same design and h) only among rows that passed them.
    Errors exclude a row from the comparison; warnings keep it."""
    for case in cases:
        if case.errors:
            continue
        numbers = [case.h, case.power, case.t_cool, case.tj, *(r for _n, r in case.layers)]
        if not all(math.isfinite(v) for v in numbers):
            case.errors.append("non_finite")
            continue
        if case.power <= 0 or case.h <= 0:
            case.errors.append("nonpositive_input")
            continue
        if case.rth <= 0:
            case.errors.append("nonpositive_rth")
            continue
        if case.layers and abs(sum(r for _n, r in case.layers) - case.rth) > LAYER_SUM_TOLERANCE * case.rth:
            case.errors.append("layer_sum")
    groups: dict[tuple[str, float], list[Case]] = {}
    for case in cases:
        if not case.errors:
            groups.setdefault((case.design, case.h), []).append(case)
    for group in groups.values():
        group.sort(key=lambda c: (c.power, c.line))
        for previous, current in zip(group, group[1:]):
            if current.power == previous.power:
                current.errors.append("duplicate")
            elif current.tj <= previous.tj:
                current.errors.append("power_monotonicity")
        median = _median([c.rth for c in group])
        for case in group:
            if abs(case.rth - median) > rth_tolerance * median:
                case.errors.append("rth_outlier")
    for case in cases:
        if not case.errors and case.tj > tj_limit:
            case.warnings.append("tj_limit")
compare / sensitivity:設計案の順位付けと h に対する感度
def compare(cases: list[Case]) -> list[Comparison]:
    """Mean Rth per design and h over valid power steps; rank 1 = lowest Rth at that h."""
    groups: dict[tuple[str, float], list[float]] = {}
    for case in cases:
        if not case.errors:
            groups.setdefault((case.design, case.h), []).append(case.rth)
    rows = [
        Comparison(design, h, len(values), sum(values) / len(values),
                   (max(values) - min(values)) / (sum(values) / len(values)))
        for (design, h), values in sorted(groups.items(), key=lambda item: (item[0][1], item[0][0]))
    ]
    for h in sorted({row.h for row in rows}):
        same_h = sorted((row for row in rows if row.h == h), key=lambda row: (row.rth, row.design))
        for rank, row in enumerate(same_h, start=1):
            row.rank = rank
            row.delta_vs_best = row.rth / same_h[0].rth - 1
    return sorted(rows, key=lambda row: (row.design, row.h))


def sensitivity(rows: list[Comparison]) -> list[dict]:
    """Change in Rth between successive h levels, and dRth/d(ln h) in K/W per e-fold of h."""
    result = []
    for design in sorted({row.design for row in rows}):
        series = sorted((row for row in rows if row.design == design), key=lambda row: row.h)
        for low, high in zip(series, series[1:]):
            result.append({
                "design": design, "h_from": low.h, "h_to": high.h,
                "rth_from": low.rth, "rth_to": high.rth,
                "change": high.rth / low.rth - 1,
                "slope": (high.rth - low.rth) / math.log(high.h / low.h),
            })
    return result

検証

  • ブラウザと Python の一致:単体テストで本ページの TypeScript 実装に同じサンプルファイルを処理させ、summary・flagged・comparison・sensitivity の 4 つの CSV がリポジトリ内の Python 出力とバイト単位で一致することを確認している。
  • サンプルデータの再現性:model:lab:verify が本サイト S1 の等価熱抵抗回路網から 3 つのエクスポートファイルを再生成し、リポジトリ内のファイルとバイト単位で一致することを確認している。
  • Python の単体テストは、単位換算、入れ子の括弧を含む単位、各種の異常、読み込めないファイルのスキップ、下の 2 つの手計算を対象とする。

手計算による確認(設計案 A、h = 10000 W/(m²·K)、P = 90 W)

対流熱抵抗
R = 1 / (h · A) = 1 / (10000 × 0.030²) = 0.1111 K/Wツールの出力:0.111111 K/W
セラミック層(45° 拡がり)
R = t / (k · a_in · a_out) = 0.38 mm / (24 × 5.6 mm × 6.36 mm) = 0.4446 K/Wツールの出力:0.444557 K/W

制約

  • サンプルデータは本サイト S1 の等価熱抵抗回路網(物性一定、線形)から生成したため、Rth は電力によらない。温度依存の物性を含む実際の COMSOL 結果では Rth が電力で変わるため、外れ値の許容幅(既定 ±5 %)を実情に合わせて調整する必要がある。
  • ファイルは COMSOL の表エクスポートのテキスト形式に倣っているが、COMSOL の実際の出力ではない。実際の形式はバージョンとエクスポート設定(区切り文字、列名、単位、パラメータ列の有無)で異なり、認識できない列名は別名表に追加する必要がある。
  • 層別の内訳には各層下面のプローブ温度(T_<層>_bot 列)が必要で、Tj,max のみの場合は全体の熱抵抗だけを計算する。定常のスカラー結果のみを扱い、過渡の Zth 曲線や場のデータは扱わない。
  • 雇用主のデータは一切使用していない。業務で使用している同種のツールは機密のため公開していない。

プロジェクトの要点

問い
パラメトリック熱解析のエクスポート結果から、比較・追跡できる熱抵抗を自動で求め、問題のある行を拾い出すにはどうするか。
手法
列名と単位でエクスポートファイルを読み込み、Rth = (Tj,max − T_cool) / P とその層別内訳を計算する。物理的な妥当性をチェックした後、h ごとに設計案を順位付けし、h に対する感度を求める。Python ツールとブラウザの TypeScript 実装の出力一致は単体テストで確認している。
結果
サンプル 3 ファイル 38 行のうち有効 36 件、異常 2 行を検出した。すべての h で順位は C(AlN 0.63 mm)< B(Si3N4 0.32 mm)< A(Al2O3 0.38 mm)。h = 10000 W/(m²·K) での Rth はそれぞれ 0.602、0.648、0.983 K/W。