Pythonで工学計算ツールを作る|油圧シリンダ推力計算の総合演習

「Pythonで工学計算ツールを作る|油圧シリンダ推力計算の総合演習」の内容を表す技術イラスト

Python入門の総合演習として、油圧シリンダの理論推力計算ツールを作ります。入力・検証・計算・出力を分離し、dataclass、CSV、JSON、例外、型ヒント、unittestを統合します。

目次

本文

Pythonの構文を個別に学んだ後は、それらを小さな道具へまとめる段階です。

今回作るのは、CSVから油圧シリンダの諸元を読み込み、押し側と引き側の推力を計算してJSONへ保存するツールです。

ただし、単に式を1行書くだけではありません。実務へ発展させやすいように、処理を次の4段階へ分けます。

  1. 入力:CSVから文字列を読み込む
  2. 検証:数値、有限性、寸法関係、効率の範囲を確認する
  3. 計算:SI単位へ変換して推力を求める
  4. 出力:計算結果をJSONへ保存する

この構成なら、将来CSVを画面入力やAPIへ変更しても、計算関数を再利用できます。

今日の到達点

この記事を読み終えると、次のことができるようになります。

  • 小さなプログラムを入力・検証・計算・出力へ分割できる
  • dataclassで入力条件と計算結果を表現できる
  • CSVの文字列を検証して数値へ変換できる
  • mmとMPaをSI単位へ変換して計算できる
  • 例外へ行番号と原因を残せる
  • 計算結果をJSONへ出力できる
  • 正常値、境界値、異常値、入出力をunittestで検証できる

ツールの入出力

入力ファイルcylinders.csvには、次の列を用意します。

列意味単位・範囲
name識別名文字列
bore_mmシリンダ内径mm、0より大きい
rod_mmロッド径mm、0以上かつ内径未満
pressure_mpa使用圧力MPa、0より大きい
efficiency効率0より大きく1以下

入力例です。

name,bore_mm,rod_mm,pressure_mpa,efficiency
Cylinder A,80,40,14,0.90
Cylinder B,50,28,7,0.85

出力するJSONには、名称、押し側推力、引き側推力を保存します。

[
  {
    "name": "Cylinder A",
    "extension_force_kn": 63.334507896370226,
    "retraction_force_kn": 47.500880922277666
  }
]

JSONには計算値を丸めずに保存し、画面表示だけを小数点以下2桁にします。表示用の丸めと計算用データを分けるためです。

計算式と単位

押し側では、ピストン全面が受圧します。\[ A_p=\frac{\pi D^2}{4} \]

引き側では、ピストン面積からロッド断面積を引いた環状面積が受圧します。\[ A_a=\frac{\pi(D^2-d^2)}{4} \]

推力は次の式で求めます。\[ F=pA\eta \]

  • \(D\):シリンダ内径 m
  • \(d\):ロッド径 m
  • \(A_p\):ピストン面積 m²
  • \(A_a\):環状面積 m²
  • \(p\):圧力 Pa
  • \(\eta\):効率、無次元
  • \(F\):推力 N

CSVでは寸法をmm、圧力をMPaで受け取ります。計算直前にSI単位へ変換します。\[ D[\mathrm{m}]=\frac{D[\mathrm{mm}]}{1000} \]\[ p[\mathrm{Pa}]=p[\mathrm{MPa}]\times10^6 \]

この計算は静的な単純モデルです。実機選定では、シール摩擦、背圧、圧力損失、始動条件、座屈、取付方法、安全率などを別途検討します。

ファイル構成

最小構成は次の3ファイルです。

hydraulic_force_tool.py
test_hydraulic_force_tool.py
cylinders.csv

実行するとforce_results.jsonが生成されます。

データモデルを定義する

入力条件と結果をdataclassで表します。

from dataclasses import dataclass
import math


@dataclass(frozen=True)
class CylinderSpec:
    name: str
    bore_mm: float
    rod_mm: float
    pressure_mpa: float
    efficiency: float = 1.0

    def __post_init__(self) -> None:
        values = (
            self.bore_mm,
            self.rod_mm,
            self.pressure_mpa,
            self.efficiency,
        )

        if not all(
            math.isfinite(value)
            for value in values
        ):
            raise ValueError(
                "all numeric values must be finite"
            )

        if self.bore_mm <= 0:
            raise ValueError(
                "bore_mm must be positive"
            )

        if not 0 <= self.rod_mm < self.bore_mm:
            raise ValueError(
                "rod_mm must satisfy "
                "0 <= rod_mm < bore_mm"
            )

        if self.pressure_mpa <= 0:
            raise ValueError(
                "pressure_mpa must be positive"
            )

        if not 0 < self.efficiency <= 1:
            raise ValueError(
                "efficiency must satisfy "
                "0 < efficiency <= 1"
            )


@dataclass(frozen=True)
class ForceResult:
    name: str
    extension_force_kn: float
    retraction_force_kn: float

frozen=Trueにより、作成後のフィールド代入を防ぎます。計算条件を途中で意図せず変更しにくくするためです。ただし、Pythonのすべての状態を完全に不変にする機能ではありません。

__post_init__()では型変換後の値を検証します。型ヒントだけでは、負数やNaNを自動的に拒否できません。

計算処理を独立させる

計算関数はファイルを読み書きせず、CylinderSpecを受け取ってForceResultを返します。

def calculate_forces(
    spec: CylinderSpec,
) -> ForceResult:
    bore_m = spec.bore_mm / 1000
    rod_m = spec.rod_mm / 1000
    pressure_pa = spec.pressure_mpa * 1_000_000

    piston_area_m2 = math.pi * bore_m ** 2 / 4
    rod_area_m2 = math.pi * rod_m ** 2 / 4
    annulus_area_m2 = piston_area_m2 - rod_area_m2

    extension_force_n = (
        pressure_pa
        * piston_area_m2
        * spec.efficiency
    )
    retraction_force_n = (
        pressure_pa
        * annulus_area_m2
        * spec.efficiency
    )

    return ForceResult(
        name=spec.name,
        extension_force_kn=extension_force_n / 1000,
        retraction_force_kn=retraction_force_n / 1000,
    )

入力元がCSVでもWebフォームでも、この計算関数は変更せずに利用できます。また、ファイルを準備しなくても数値だけでテストできます。

CSV行を検証して変換する

csv.DictReaderで読み込んだ各値は文字列です。数値へ変換し、CylinderSpecを作ります。

def spec_from_row(
    row: dict[str, str],
    row_no: int,
) -> CylinderSpec:
    try:
        return CylinderSpec(
            name=row["name"].strip(),
            bore_mm=float(row["bore_mm"]),
            rod_mm=float(row["rod_mm"]),
            pressure_mpa=float(
                row["pressure_mpa"]
            ),
            efficiency=float(
                row.get("efficiency", "1.0")
            ),
        )
    except (KeyError, TypeError, ValueError) as error:
        raise ValueError(
            f"row {row_no}: {error}"
        ) from error

from errorにより、元の例外を原因として残します。たとえば3行目のbore_mmが数値でなければ、どの行で変換に失敗したか追跡できます。

CSV全体を読み込む関数です。

from pathlib import Path
import csv


def load_specs(
    csv_path: Path,
) -> list[CylinderSpec]:
    specs: list[CylinderSpec] = []

    with csv_path.open(
        "r",
        encoding="utf-8",
        newline="",
    ) as file:
        reader = csv.DictReader(file)

        for row_no, row in enumerate(
            reader,
            start=2,
        ):
            specs.append(
                spec_from_row(row, row_no)
            )

    return specs

start=2とするのは、1行目がヘッダーだからです。newline=""は、csvモジュールへ改行処理を任せるために指定します。

JSONへ結果を出力する

dataclasses.asdict()で結果を辞書へ変換し、json.dump()で保存します。

from dataclasses import asdict
import json


def write_results_json(
    results: list[ForceResult],
    json_path: Path,
) -> None:
    data = [
        asdict(result)
        for result in results
    ]

    with json_path.open(
        "w",
        encoding="utf-8",
    ) as file:
        json.dump(
            data,
            file,
            ensure_ascii=False,
            indent=2,
        )
        file.write("\n")

ensure_ascii=Falseにより、日本語名を使った場合もUnicodeエスケープへ強制変換せずに保存できます。JSONの数値は文字列ではなく数値として出力されます。

処理を組み合わせる

入力、計算、出力をrun()で接続します。

def run(
    csv_path: Path,
    json_path: Path,
) -> list[ForceResult]:
    specs = load_specs(csv_path)
    results = [
        calculate_forces(spec)
        for spec in specs
    ]
    write_results_json(results, json_path)
    return results

スクリプトとして実行する部分です。

if __name__ == "__main__":
    results = run(
        Path("cylinders.csv"),
        Path("force_results.json"),
    )

    for result in results:
        print(
            f"{result.name}: "
            f"extension="
            f"{result.extension_force_kn:.2f} kN, "
            f"retraction="
            f"{result.retraction_force_kn:.2f} kN"
        )

実行コマンドです。

python hydraulic_force_tool.py

実行結果です。

Cylinder A: extension=63.33 kN, retraction=47.50 kN
Cylinder B: extension=11.68 kN, retraction=8.02 kN

このコードはPython 3.12.14で実行確認済みです。

unittestで計算と入出力を確認する

計算式だけでなく、検証規則とCSV→JSONの一連処理もテストします。

from pathlib import Path
from tempfile import TemporaryDirectory
import json
import math
import unittest

from hydraulic_force_tool import (
    CylinderSpec,
    calculate_forces,
    run,
)


class TestHydraulicForceTool(unittest.TestCase):
    def test_forces_without_loss(self) -> None:
        spec = CylinderSpec(
            "Cylinder A",
            80.0,
            40.0,
            14.0,
        )
        result = calculate_forces(spec)

        expected_extension_kn = (
            14_000_000
            * math.pi
            * 0.08 ** 2
            / 4
            / 1000
        )

        self.assertAlmostEqual(
            result.extension_force_kn,
            expected_extension_kn,
        )

    def test_efficiency_is_applied(self) -> None:
        ideal = calculate_forces(
            CylinderSpec(
                "Ideal",
                80.0,
                40.0,
                14.0,
                1.0,
            )
        )
        actual = calculate_forces(
            CylinderSpec(
                "Actual",
                80.0,
                40.0,
                14.0,
                0.9,
            )
        )

        self.assertAlmostEqual(
            actual.extension_force_kn,
            ideal.extension_force_kn * 0.9,
        )

    def test_invalid_rod_diameter_raises(self) -> None:
        with self.assertRaises(ValueError):
            CylinderSpec(
                "Invalid",
                80.0,
                80.0,
                14.0,
            )

    def test_non_finite_value_raises(self) -> None:
        with self.assertRaises(ValueError):
            CylinderSpec(
                "Invalid",
                math.nan,
                40.0,
                14.0,
            )

    def test_csv_to_json(self) -> None:
        with TemporaryDirectory() as directory:
            csv_path = Path(directory) / "input.csv"
            json_path = Path(directory) / "output.json"

            csv_path.write_text(
                "name,bore_mm,rod_mm,"
                "pressure_mpa,efficiency\n"
                "Cylinder A,80,40,14,0.9\n",
                encoding="utf-8",
            )

            results = run(csv_path, json_path)
            data = json.loads(
                json_path.read_text(encoding="utf-8")
            )

            self.assertEqual(len(results), 1)
            self.assertEqual(
                data[0]["name"],
                "Cylinder A",
            )


if __name__ == "__main__":
    unittest.main()

実行コマンドです。

python -m unittest -v test_hydraulic_force_tool.py

実行結果です。

test_csv_to_json ... ok
test_efficiency_is_applied ... ok
test_forces_without_loss ... ok
test_invalid_rod_diameter_raises ... ok
test_non_finite_value_raises ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.002s

OK

全5件をPython 3.12.14で実行し、成功を確認しました。

テストで確認していること

分類入力・条件確認内容
正常値内径80 mm、ロッド40 mm、14 MPaSI単位の式と一致する
正常値効率0.9理論値へ0.9が反映される
境界値ロッド径=内径ValueErrorになる
異常値内径がNaNValueErrorになる
入出力CSV 1行JSONへ1件出力される

境界条件をコードとテストの両方へ残すことで、後から検証規則を変更したときに影響を確認できます。

設計上のポイント

入出力と計算を分離する

calculate_forces()はCSVやJSONを知りません。そのため、将来入力元がデータベースやWeb APIになっても再利用できます。

単位を変数名へ含める

bore_mm、pressure_mpa、force_nのように単位を名前へ含め、計算内部ではSI単位へ変換しています。Pythonは物理単位の取り違えを自動検出しません。

変換前と変換後を混ぜない

CSVから得た文字列をそのまま計算へ渡さず、floatへ変換し、検証済みのCylinderSpecへまとめます。

例外を握りつぶさない

不正な行を空の結果へ置き換えるのではなく、行番号を含むValueErrorとして通知します。大量処理で不正行をスキップする場合も、行番号と理由をログや別ファイルへ残す設計が必要です。

出力丸めを計算へ戻さない

コンソールでは読みやすく2桁表示しますが、JSONには元の浮動小数点値を保存しています。表示用の丸め値で次の計算を行わないためです。

このツールの制約

このプログラムは学習用の最小モデルです。次の要素は含みません。

  • シール摩擦の速度・圧力依存性
  • 背圧と戻り側圧力
  • 配管・弁の圧力損失
  • 始動圧とスティックスリップ
  • ロッド座屈
  • 横荷重と取付部強度
  • 動的な加減速と衝撃
  • 温度による油特性の変化
  • 機器仕様に基づく許容圧力判定

したがって、出力値だけで実機を選定してはいけません。ツールの責任範囲と前提条件を、計算式と一緒に管理してください。

よくある失敗

MPaとPaを混ぜる

14 MPaを14 PaとしてSI式へ入れると、結果が100万分の1になります。変数名と変換処理を明示します。

引き側でも全面積を使う

片ロッドシリンダの引き側では、ロッド断面積を差し引いた環状面積を使います。

CSVを読み込めたので安全だと思う

CSVのセルは文字列です。数値変換に成功しても、NaN、負数、寸法関係などを追加で検証する必要があります。

すべてを一つの関数へ書く

読込、検証、計算、出力を一つへ詰め込むと、計算だけをテストしにくくなります。処理の境界を関数で表現します。

テストの期待値を実装からコピーする

実装と同じ誤りをテストへコピーすると、誤った式でも成功します。重要な計算では、手計算、別式、メーカー資料など独立した根拠と照合します。

練習問題

推力差を出力する

押し側と引き側の推力差をForceResultへ追加してください。

解答例です。

@dataclass(frozen=True)
class ForceResult:
    name: str
    extension_force_kn: float
    retraction_force_kn: float
    force_difference_kn: float

calculate_forces()で次を設定します。

force_difference_kn = (
    extension_force_n - retraction_force_n
) / 1000

空の名称を拒否する

nameが空文字列の場合にValueErrorを発生させてください。

解答例です。

if self.name.strip() == "":
    raise ValueError("name must not be empty")

テストではCylinderSpec("", 80, 40, 14)がValueErrorになることを確認します。

無限大のテストを追加する

pressure_mpaへ無限大を渡し、拒否されることを確認してください。

解答例です。

def test_infinite_pressure_raises(self) -> None:
    with self.assertRaises(ValueError):
        CylinderSpec(
            "Invalid",
            80.0,
            40.0,
            math.inf,
        )

まとめ

  • 小さなツールでも入力・検証・計算・出力を分離する
  • dataclassで検証済み入力と計算結果を表現できる
  • CSVから読み込んだ値は文字列なので型変換が必要
  • 数値へ変換できても、有限性、符号、範囲、寸法関係を検証する
  • mmとMPaをmとPaへ変換し、SI単位で式を評価する
  • 押し側はピストン全面積、引き側は環状面積を使う
  • 例外には行番号と元の原因を残す
  • JSON出力と表示用丸めを分ける
  • 正常値、境界値、異常値、入出力をテストする
  • 計算ツールの前提と対象外を明記する

このツールは巨大なアプリケーションではありません。しかし、入力形式を変更でき、計算を単独で再利用でき、異常値を拒否し、結果を機械可読形式で保存し、テストで変更を検証できます。

Pythonを実務のEngineering Knowledge Assetへ発展させるときに重要なのは、コード量ではなく、根拠、単位、入力条件、出力形式、検証方法が分離され、再利用できることです。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次