============================
外部 Mesh Morphing 連携
============================

外部のmesh morphingツールへ体積メッシュを渡し、変形後の節点座標をEMSolutionへ返すための
連携機能です。Python APIの一般一覧とは分離して、このページでデータ契約、実行順序、制約を
説明します。

.. warning::

   この機能はschema version 1/2のPoC段階です。外部ツールとの結合試験および互換性方針が
   確定するまでは、安定APIとしては扱いません。外部ツール側はschema versionを確認し、
   未対応のversionを受け取った場合は処理を中止してください。

概要
----

EMSolutionは、DEFORM領域について、外部ツールが必要とする静的な体積メッシュと、各ステップの
節点状態をPythonへ渡します。外部ツールはRIGID運動を反映済みの**グローバル絶対座標**を返します。
EMSolutionはその座標を使って変形行列を組み立てます。

- 座標単位は常に ``m``。
- 座標はグローバル直交XYZ座標系。
- 返却値は基準座標からの変位量ではなく、変形後の絶対座標。
- 外部payloadを渡したstepでは、既存の ``set_deformed_node_position()`` は重ねて適用されません。

実行の流れ
------------

初期化直後に静的トポロジーを一度だけ取得して外部ツール側でキャッシュし、各stepでは現在の
変形状態を取得してpayloadを返します。ライフサイクル管理のため、module-level関数ではなく
``CoupledSession`` を使用してください。

.. code-block:: python

   import pyemsol

   with pyemsol.CoupledSession(input_json, str(run_directory)) as session:
       # セッション中に変わらない体積メッシュ。最初のstepより前に1回だけ取得する。
       topology = session.describe_deformation_mesh()

       for end_time, delta_time, motion_state in time_steps:
           description = session.describe_deformation()
           deformation = morph_deformation(
               description, topology, motion_state, time=end_time
           )
           result = session.step(
               time=end_time,
               delta_time=delta_time,
               deformation=deformation,
           )

``describe_deformation()`` の ``current_coordinates`` はstep後に更新されます。静的な
``topology`` は ``immutable_during_session`` が ``True`` であれば、セッション終了まで
再取得する必要はありません。

利用するPython API
-------------------

.. py:class:: CoupledSession(inputJson, run_directory)

   外部step解析のセッションです。``with`` 文を使用すると、終了時に ``close()`` が呼ばれます。
   同一プロセスで複数のactiveな解析を実行することはサポートしません。

.. py:method:: CoupledSession.describe_deformation() -> dict

   各DEFORM領域の参照座標、既存線形変形の目標座標、現在座標を返します。schema versionは現在 ``1``
   です。

.. py:method:: CoupledSession.describe_deformation_mesh() -> dict

   外部morphing用の静的体積メッシュと境界roleを返します。schema versionは現在 ``2`` です。

.. py:method:: CoupledSession.step(time, delta_time, inputs={}, deformation=None) -> dict

   1つの過渡stepを実行します。``time`` は直前終了時刻 + ``delta_time``、``delta_time`` は
   正の有限値でなければなりません。電気連携を併用する場合は ``inputs`` にポート入力を指定します。

.. py:function:: linear_deformation(description, position, *, mesh_revision=None) -> dict

   既存の線形変形規則と同じabsolute-coordinate payloadを作る参照helperです。外部ツールへの
   接続確認には利用できますが、一般のmesh morphingでは外部ツールのadapterに置き換えます。
   領域が1つなら ``position`` は数値、複数なら ``{deform_id: position}`` を渡します。

静的メッシュトポロジー（schema version 2）
---------------------------------------------

``describe_deformation_mesh()`` のトップレベル構造です。

.. code-block:: python

   {
       "schema_version": 2,
       "enabled": True,
       "mesh_revision": 0,
       "unit": "m",
       "coordinate_frame": "global",
       "immutable_during_session": True,
       "regions": [...],
   }

- ``regions`` の各要素は ``deform_id``、``motion_id``、基準・目標motion位置、
  ``return_node_ids``、``mesh`` を持ちます。
- ``return_node_ids`` はstep payloadへ返す節点IDです。``describe_deformation()`` の
  同じ領域の ``node_ids`` と同じ順序です。
- ``mesh.nodes`` は ``ids``、``reference_coordinates``、``roles`` を持ち、同じ添字が
  同じ節点を表します。
- ``mesh.volume_elements`` は ``element_id``、``element_type_id``、``element_type``、
  ``material_id``、``role``、``node_ids`` を持ちます。``node_ids`` はEMSolutionの局所節点順を
  保持します。

節点roleは次の3種類です。

- ``deformable``: 変形する体積領域（内部の ``MOTION_DEFORM``）。
- ``rigid``: 形状を保って移動する領域（内部の ``MOTION_RIGID``）。
- ``fixed``: 外側の固定領域と共有するため既存処理で動かさない境界節点。

要素には ``deformable`` と ``rigid`` だけを含めます。モデル全体のfixed要素は渡しません。
ただし、外部ツールが固定条件を認識できるよう、共有境界節点は ``fixed`` roleとして含まれます。
``element_type`` はEMSolutionの短縮名（例: ``H1``、``PR1``）です。外部ツール固有の要素種別へ
変換する場合は、``element_type_id``、節点数、局所節点順を合わせてadapter内で対応付けてください。

動的な変形記述（schema version 1）
------------------------------------

``describe_deformation()`` は各stepで利用する節点状態を返します。

.. code-block:: python

   {
       "schema_version": 1,
       "enabled": True,
       "mode": "absolute",
       "unit": "m",
       "coordinate_frame": "global_after_rigid",
       "regions": [
           {
               "deform_id": 1,
               "motion_id": 1,
               "position_pre_geom": 0.0,
               "position_deform_mesh": 0.24,
               "node_ids": [10, 11, 12],
               "reference_coordinates": [[...], [...], [...]],
               "target_coordinates": [[...], [...], [...]],
               "current_coordinates": [[...], [...], [...]],
           },
       ],
   }

``node_ids``、``reference_coordinates``、``target_coordinates``、``current_coordinates`` は、
同じ添字が同じ節点を表します。``target_coordinates`` は既存 ``deform_mesh`` による線形変形の
目標値であり、全節点の拘束条件ではありません。motion位置は外部solver、Pythonで評価した
時間関数、または上位の連成制御コードから外部ツールへ渡してください。

外部ツールから返すpayload
----------------------------

外部ツールは、全DEFORM領域を1回ずつ含む ``regions`` 形式を返します。

.. code-block:: python

   deformation = {
       "mode": "absolute",
       "unit": "m",
       "coordinate_frame": "global_after_rigid",
       "mesh_revision": 25,  # 任意。結果へそのまま返される。
       "regions": [
           {
               "deform_id": 1,
               "node_ids": [10, 11, 12],
               "coordinates": [
                   [-0.15, 0.50, 0.00],
                   [-0.09, 0.51, 0.00],
                   [-0.03, 0.50, 0.00],
               ],
           },
       ],
   }

``coordinates`` は ``(N, 3)`` のグローバル絶対座標です。``node_ids[i]`` と
``coordinates[i]`` は同じ節点を表します。入力順序は任意ですが、デバッグ性と互換性のため
``description`` が返した順序を維持してください。部分更新、節点IDの振り直し、変位量の返却は
サポートしていません。

NumPyを内部で利用しても、pyemsolへ渡す直前にPython組み込み型へ変換してください。

.. code-block:: python

   region_payload = {
       "deform_id": int(deform_id),
       "node_ids": node_ids.astype(int).tolist(),
       "coordinates": coordinates.astype(float).tolist(),
   }

結果と検証
------------

成功時、``step()`` の戻り値には次のような変形情報が含まれます。

.. code-block:: python

   result["deformation"] == {
       "mode": "absolute",
       "coordinate_frame": "global_after_rigid",
       "region_count": 1,
       "node_count": 548,
       "mesh_revision": 25,
   }

EMSolutionは全領域を検証してから節点座標を更新します。以下の場合は座標および解析時刻を進めずに
例外を返します。

- DEFORM領域がない、または領域数・``deform_id`` が一致しない。
- ``mode``、``unit``、``coordinate_frame`` が指定値と異なる。
- ``node_ids`` または ``coordinates`` の長さ、対象ID、XYZ成分数が正しくない。
- 節点IDの重複・不足・不明ID、NaN/Infを含む座標、Python組み込み型以外の値。
- gap要素を含むモデル、またはSlide motionと外部Deformを同時に使用するモデル。

外部ツールの受け入れ確認
--------------------------

adapter実装では、少なくとも次を確認してください。

1. 受け取った ``description`` / ``topology`` を破壊的に変更しない。
2. 全 ``deform_id`` について、全 ``return_node_ids`` を欠落・重複なく返す。
3. 座標は有限な ``m`` 単位のグローバル絶対座標で、RIGID運動後の境界と固定境界を満たす。
4. 同じ入力に対して決定的な結果を返す。
5. 基準状態で期待する形状を返し、``Deform_sample`` による40stepの結合試験を完走する。

step後にもう一度 ``describe_deformation()`` を呼び、``current_coordinates`` と送信した
``coordinates`` を比較すると、座標の受け渡しを確認できます。

現在の制約
------------

- 表面要素、周期境界・スライド面、要素品質指標はトポロジーAPIに含まれません。
- 外部座標について、要素反転・負Jacobian・体積変化は検査しません。
- solver計算失敗時の完全rollback、および非線形morphingの後処理再生用座標履歴は未実装です。
- 毎step、対象となる全節点の座標を返す必要があります。
