0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

産業用太陽光発電シミュレーターをMCP対応にした

0
Posted at

——validate→simulateの2段階プロトコルと、モード別のP-IRR開示制御を実装した

何をしたか

産業用PV+蓄電池シミュレーター(pv-sim-biz)を
MCPサーバーとして公開した。

姉妹プロジェクトのpv-sim-fip(FIP転+系統用蓄電池)・
pv-sim-gh(住宅用)に続く3件目のMCP化で、
「validateしてからsimulateする」2段階プロトコル
fip発の設計をそのまま踏襲している(gh側にも
validate_residential_paramssimulate_residential_pvとして
同じパターンが実装済み)。

産業用ならではの追加ポイントは2つある。

  1. 産業用は住宅用よりパラメータが多く(複数施設合算・
    高圧/特別高圧・事業モデル・マイクログリッドなど)、
    validate側の検証ルールが厚くなっている
  2. 事業モデルによってP-IRRの開示/非開示を切り替える設計にした
    (後述)。これは他の2プロジェクトにはない、bizだけの設計判断だ

エンドポイントはここだ。

https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/

公開しているツール一覧

ツール名 機能
list_stations 対応地点一覧(NEDO METPV-20収録の50地点)を返す
estimate_pv_generation 発電量のみの簡易試算
validate_industrial_params パラメータの妥当性チェック・正規化・警告提示
simulate_industrial_pv 需給・デマンド・蓄電池最適化・経済性の全計算

なぜ2段階にしたか

産業用シミュレーターのパラメータは40項目を超える
(PV面設定、複数施設の需要合算、高圧・特別高圧の別、
蓄電池のSOC上限・下限、補助金率、リース/PPAの事業モデル、
目標IRR、マイクログリッド、両面パネル……)。

AIエージェントに直接simulate_industrial_pv
叩かせることもできるが、それだと
入力ミスや単位の取り違えがあった場合に
気づかないまま計算結果だけが返ってきてしまう。

そこでvalidate_industrial_paramsを先に呼び、

  • 正規化されたパラメータ(単位・型を揃えた値)
  • 警告(値は有効だが注意が必要なケース)
  • エラー(存在しないキー等、計算不能なケース)

をAIエージェントが人間に提示してから、
本計算のsimulate_industrial_pvに進む設計にした
(この2段階自体はfip・ghと共通のパターン。産業用は
検証項目の数が多い分、この設計が効いてくる場面が増える)。

実際に動かしてみる

MCP対応クライアントに以下を貼り付けると、
2段階プロトコルに沿って動く。

pv-sim-biz(産業用高圧・特別高圧太陽光発電+蓄電池シミュレーター)の
MCPサーバーに接続してテストしたい。

1. 以下のリモートMCPサーバーを追加してください:
   名前: pv-sim-biz
   URL: https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/

2. 接続できたら、利用可能なツール一覧を確認してください
   (list_stations, estimate_pv_generation,
     validate_industrial_params, simulate_industrial_pv
     の4つがあるはずです)

3. まず list_stations を呼び、
   福岡(station_no="82182")が一覧に含まれることを確認してください。

4. 次の条件で validate_industrial_params を呼び、
   正規化されたパラメータと警告(あれば)を提示してください:
   - station_no: "82182"(福岡)
   - faces: [{"ppeak_kw": 800.0, "tilt_deg": 30.0,
             "azimuth_deg": 180.0, "pcs_limit_kw": 800.0}]
   - facilities: [{"building_type": "hospital",
                   "floor_area_m2": 22400.0, "building_count": 1}]
   - contract_type: "high_voltage"
   - sell_mode: "surplus_export"
   - sell_scheme: "fit"
   - fit_elapsed_years: 3
   - battery_enabled: true
   - battery_mode: "lp_optimized"
   - battery_capacity_kwh: 300.0
   - battery_efficiency_pct: 95.0
   - battery_max_charge_kw: 150.0
   - battery_max_discharge_kw: 150.0
   - battery_soc_min_pct: 15.0
   - battery_soc_max_pct: 95.0
   - subsidy_enabled: true
   - subsidy_pv_pct: 10.0
   - subsidy_bat_pct: 20.0
   - business_model: "lease"
   - contract_years: 15
   - target_irr_pct: 12.0

5. 検証結果(normalized_params・warnings)を私に見せて
   確認を求めてから、同じ条件で simulate_industrial_pv を
   実行してください(validateで確認→simulateで本計算、
   の2段階プロトコルを必ず踏んでください)。

6. 結果から以下の項目を抜き出して報告してください:
   - annual.generation_kwh(年間発電量)
   - annual.self_consumption_rate_pct(自家消費率)
   - annual.battery_charge_kwh / battery_discharge_kwh
   - annual.co2_reduction_t_per_year(CO2削減量)
   - electricity_cost.contract_power_reduction_kw(契約電力削減)
   - electricity_cost.annual_economic_merit_yen(年間経済メリット)
   - investment.net_investment_yen / simple_payback_years
   - business.required_lease_yen_per_year(必要リース料)
   - business.customer_annual_benefit_yen と
     business.proposal_viable(提案可能か)
   - caveats に記載されている免責事項の内容

7. 追加で、異常系の挙動も確認してください:
   facilities の building_type に存在しないキー
   (例: "warehouse")を指定して validate_industrial_params を呼び、
   errors に分かりやすい日本語メッセージが返ることを確認してください。

8. 最後に、sell_mode を "no_export"(逆潮流禁止)に変えて
   同条件で simulate_industrial_pv を実行し、
   export_kwh が 0 になり curtailed_kwh(出力抑制量)が
   正の値になることを確認してください。

エラーメッセージも日本語で返す

building_typeに存在しないキー("warehouse"等)を渡すと、
errorsに日本語でメッセージが返る設計にしている。
実際にCodex(OpenAI)から呼び出して確認した、生のレスポンスがこれだ。

{
  "valid": false,
  "normalized_params": {
    "facilities": [],
    "...": "(無効な施設は取り除かれた状態で残りのパラメータは正規化されて返る)"
  },
  "warnings": [],
  "errors": [
    "facilities[0].building_type は ['office', 'primary_school', 'secondary_school', 'hospital', 'hotel', 'retail'] から選択してください"
  ]
}

AIエージェント経由で使われることを想定すると、
エラーメッセージが英語のスタックトレースのままだと
AIが人間にうまく伝え直せないことがある。
「どのパラメータが」「何が問題で」「何を渡せばいいか」
(有効な選択肢そのもの)まで日本語で返すことで、
エージェント側が追加の推測をせずにそのまま
ユーザーへ伝え返せるようにしている。

逆潮流禁止(no_export)の挙動確認

sell_modeno_exportに変更すると、
系統への逆潮流がゼロになり、
余剰電力は出力抑制(curtailment)扱いになる。
先の実行例(福岡・病院22,400m²・PV800kW・蓄電池300kWh)で
surplus_exportno_exportを切り替えた実測値がこれだ。

項目 surplus_export no_export
売電量 export_kwh 33,613 kWh 0 kWh
出力抑制量 curtailed_kwh 0 kWh 33,613 kWh
年間売電収入 638,655円 0円
年間経済メリット 21,575,289円 20,936,634円

自営線・小規模系統など系統連系条件が厳しい施設を
想定したモードで、surplus_exportとの切り替えで
「捨てる電力がどれだけ出て、経済メリットがいくら
下がるか」をAIエージェント経由でも確認できる。

なぜ産業用でこの設計にしたか

住宅用はパラメータが少なく、
誤入力があっても被害が小さい。

産業用は違う。simulate_industrial_pvの結果は
リース料・PPA単価・提案可否判断に直結する。
AIエージェントが誤ったパラメータのまま
本計算を走らせて、それをそのまま
提案書の数字として使ってしまうリスクがある。

validateを挟むことで、
AIエージェントに「一度立ち止まって
人間に確認を求める」動作を強制している。

モードによってP-IRRの開示・非開示を切り替える

産業用シミュレーターには2つの事業モードがある。

  • モードA(自家消費型): リース/PPAで需要家に提案する
    営業ツール。需要家が見るのは「電気代がいくら下がるか」で、
    事業者側の収益構造(目標P-IRR)は見せたくない
  • モードB(マイクログリッド事業): 複数施設をまとめて
    自営線で結び、網内売電する事業そのものの投資判断ツール。
    こちらはP-IRRの算出自体がゴール

このビジネス上の非対称性を、MCPの出力スキーマでも
そのまま再現した。

business_model: "lease"(モードA)で呼ぶと、
target_irr_pctは資本回収係数(CRF)で
リース料を逆算するための入力として使われるだけで、
出力側に「実現P-IRR」のような算出値は一切出てこない。

{
  "business": {
    "business_model": "lease",
    "required_lease_yen_per_year": 10386428,
    "customer_annual_benefit_yen": 954693,
    "proposal_viable": true
  }
}

一方mg_enabled: true(モードB)にすると、
microgridセクションが追加され、
project_irr_pctがそのまま返ってくる。

{
  "microgrid": {
    "mg_total_investment_yen": 186400000,
    "bundling_merit_yen_per_year": 1333009,
    "project_irr_pct": 2.34,
    "project_irr_note": "モードB(マイクログリッド事業)ではP-IRRの算出自体が事業者向けのゴールのため、モードA(lease/ppa)の目標P-IRRとは異なりそのまま開示しています"
  }
}

AIエージェントは「今どちらのモードで呼ばれているか」を
意識する必要がなく、ツール側が返す構造化データの
形そのものが「この情報は開示してよい/よくない」を
表現している。エージェントに「P-IRRは言わないでください」
と自然言語で指示するより、そもそも出力に含めない方が
情報漏洩のリスクを構造的に防げる。

ハマった実装上の罠:python app.py起動時の二重import

MCP化にあたって、計算ロジック本体(app.py)を
別ファイルのmcp_tools.pyから呼び出す構成にした。
ここで一つ罠がある。

HF Spacesはpython app.pyでアプリを起動するため、
このときPythonはappモジュールを"__main__"という
名前でロードする。この状態でmcp_tools.py内から
素朴にimport appすると、Gradioが動いている裏で
app.pyがまるごと二度目の実行をしてしまい

build_ui()がリクエスト処理スレッド内で再度走って
Gradioがクラッシュする(実際に姉妹プロジェクトの
pv-sim-fipで本番事故として発生した)。

対策は、すでにロード済みのモジュールを
sys.modulesから探して再利用すること。

def _get_app():
    import sys
    for name in ("app", "__main__"):
        mod = sys.modules.get(name)
        if mod is not None and hasattr(mod, "optimize_battery"):
            return mod
    import app
    return app

この問題は、Pythonの関数を直接呼ぶユニットテストでは
絶対に再現しないpython app.pyで実際にサーバーを
起動し、/gradio_api/call/<ツール名>をHTTP経由で
叩いて初めて検出できる。ローカルで動いたから本番も
大丈夫、とはならないタイプのバグだった。

3つのAIエージェント経由で数値が完全一致するか検証した

MCPは標準プロトコルなので、理屈の上では
どのAIエージェントから呼んでも同じ結果になるはずだ。
これを実際に確かめた。

福岡・病院22,400m²・PV800kW・蓄電池300kWh・
リース事業モデルという同一条件で、

  1. Pythonからmcp_tools.simulate_industrial_pv()を直接呼ぶ
  2. ローカルでpython app.pyを起動し、HTTP経由で呼ぶ
  3. 本番のHugging Face Spacesに対してHTTP経由で呼ぶ
  4. OpenAI Codexにこの記事と同じプロンプトを渡して呼ばせる

の4パターンを比較したところ、発電量・自家消費率・
蓄電池充放電量・CO2削減量・年間経済メリット・
必要リース料・需要家メリット……全項目が1件単位で
完全一致
した。丸め誤差すら出なかった。

異常系(building_typeに存在しないキーを渡す)でも、
Codexが正規化エラーをそのままユーザーに提示してから
処理を止める、という設計どおりの挙動を確認できた。

まとめ

  • fip発のvalidate→simulateの2段階プロトコルを踏襲しつつ、
    産業用ならではの検証ルールを厚めに実装した
  • 事業モデル(自家消費/リース/PPA)ではP-IRRを非開示、
    マイクログリッド事業ではP-IRRをそのまま開示——出力
    スキーマ自体でビジネス上の情報非対称性を表現した
  • エラーメッセージは日本語で「何が問題で、何を渡せば
    いいか」まで返し、AIエージェントが人間にそのまま
    伝え返せるようにした
  • 逆潮流禁止モードの出力抑制量・経済メリットの低下も
    MCP経由で確認できる
  • python app.py起動時の二重import問題への対策
    _get_app()パターン)と、直接呼び出し・ローカル
    HTTP・本番・Codex経由の4パターンで数値が完全一致する
    ことを検証した
  • MCPエンドポイント:
    https://hachinai-pv-sim-biz.hf.space/gradio_api/mcp/

住宅用(pv-sim-gh)に続き、
FIP転(pv-sim-fip)のMCP対応記事も書く予定だ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?