====================================================================
依頼 v11 の確かめ B：同じ種で2回の数字がずれる元を調べた（直していません）
2026-10-25 17:45
====================================================================

比べた回：KIOXIA service_sim/unity/
  20261025_split_check_cs2_fam_p1_r1_s1    （1回め・実時間 654.5 秒）
  20261025_split_check_cs2_fam_p1_r1_s1_2  （2回め・実時間 601.1 秒）
どちらも ServiceSim/cs:2・83 組・全員が店を出た・店の中 179.2 分 / 182.0 分。

結論を先に書きます。

  ・**人（スタッフと客）だけが Unity の NavMeshAgent 自身に動かされていて、
    その行き先の道の計算が「非同期」です。** 道が引けるまでに何コマかかるかは、
    Mac の忙しさで変わります。
  ・その「引けていない間」が、**よけるかどうかの判断と、着いたかどうかの判断を
    その場で打ち切る**作りになっています（3 か所）。
    だから道の計算が1〜数コマ遅れると、よける／着くが数コマずれ、そこから連鎖します。
  ・ロボットは updatePosition を切って自分の script で動かし、
    道も同期の NavMesh.CalculatePath で引いているので、ロボット側は決まっています。
    最初にずれたのがスタッフ（Staff03）の譲りだったのと合います。
  ・実時間の値（plan_ms・wall_seconds）は**1回めの往復から違っていました**が、
    どちらも記録だけで、動きには一切使っていません。

直す案は 4 章に 5 つ並べました。**どれを選ぶかで基準データ v2 が使えるかが変わります。**


====================================================================
1. ずれ方（データで確かめた）
====================================================================

最初に中身が違った所を、ファイルごとに1行ずつ突き合わせました。

  ファイル        最初に違う所                         中身
  ------------------------------------------------------------------------
  trips.csv       2 行目（1回めの往復）                plan_ms_max 3.2536 → 2.8529
                                                       **これだけ。ほかの列は同じ**
  yields.csv      21 行目のデータ（t_s 1546）          Staff03 の譲り
                                                         t_s      1546.253 → 1546.453
                                                         x        5.0317   → 5.0491
                                                         z        4.119    → 4.1142
                                                         waited_s 0.4      → 0.2
  stops.csv       16 行目のデータ（t_s 1548.05）        1号機が Staff03 とすれ違った止まり
                                                         duration_s 1.05 → 0.95
                                                         blocked_s  0.1  → 0
  timeline.csv    43 行目（t_s 2460）                  board_serve 1 → 0
  groups.csv      2 行目以降                           来方（arrive_s・seated_s）は同じ

  ずれの大きさは 0.2 秒 = **コマ 4 つぶん**（1 コマ 0.05 秒）。
  stops の duration_s / blocked_s の差 0.1 秒 = コマ 2 つぶん。

  ・trips.csv の plan_ms_max は Stopwatch で測った「道を引くのにかかった実時間」で、
    1回めの往復から違います。**これは動きに使っていません**（4 章 (0)）。
    つまり「実時間は最初から違うが、動きは 1546 秒まで完全に同じ」でした。
  ・1546 秒までは yields も stops も小数4桁まで一致していました。
    毎コマ少しずつ狂っていく類（浮動小数の積み上げ・スレッドの足し算の順番）ではなく、
    **ある瞬間に数コマ分ずれる類**です。これが下の原因と合います。


====================================================================
2. ずれる元（全部挙げる）
====================================================================

(A) ★ 本命：人の道の計算が非同期（NavMeshAgent.SetDestination）

  人（スタッフ・客・待合の客）は **NavMeshAgent 自身に動かされています**。
  updatePosition を切っていないのは人だけで、行き先は agent.SetDestination で渡します
  （People/PeopleNav.cs:106-107）。SetDestination は **非同期**で、
  道が引けるまで agent.pathPending が true のままです。
  引けるまでに何コマかかるかは、Unity が毎コマ道探しに使える分と、
  そのとき何人ぶんの注文が溜まっているかで決まります。

  その「引けていない間」を見ている所が 3 か所あり、**どれも「このコマは何もしない」**
  という作りです。

    1. People/PersonRobotYield.cs:774   AisleAboutToEnter()
         if (_agent.pathPending) return -1;
       → -1 は「これから細い通路に入らない」の意味なので、
         UpdateClear() の「1. これから入る細い通路にロボットがいたら入らない」が
         丸ごと飛ばされ、**そのコマは譲らない**（260 行）。
       → 今回ずれたのは、まさにこの譲り（Staff03・waited_s 0.4 → 0.2）です。

    2. People/StaffAgent.cs:189         着いたかの判定
         if (_agent.pathPending) return;
       → 着いた判定が遅れ、dwell（会計 90 秒・片付け 40+40 秒）の始まりがずれる。

    3. People/GuestAgent.cs:175         着いたかの判定（同じ形）
       → ドリンクバー・トイレ・退店のタイミングがずれる。

(B) 人の局所回避（Unity の RVO）が入っている

  PeopleSpec の avoidanceQuality = 3（GoodQualityObstacleAvoidance）、
  avoidancePriority = 50。ロボットは NoObstacleAvoidance で切ってあります
  （ServingRobotController.cs:612）。
  Unity の局所回避はワーカースレッドで解くので、スレッドの数や割り当てで
  わずかに変わりうる所です。
  ただし今回は 1546 秒まで小数4桁まで一致していたので、
  **毎コマ効く形では出ていません**。可能性としては残りますが、今回の主因ではありません。

(C) 物理の問い合わせの、返ってくる順番と「あふれ」

  Robot/FrontSensor.cs:70        Physics.OverlapBoxNonAlloc（配列 32 個）
  ServingRobotController.cs:1728 Physics.OverlapSphereNonAlloc
  ServingRobotController.cs:1844 Physics.RaycastNonAlloc

  ・FrontSensor は 32 個を超えると `if (n > MaxHits) n = MaxHits;` で切り捨てます。
    あふれたとき **どれが落ちるかは返る順番で決まる**ので、順番が変われば結果が変わります。
  ・近いものを選ぶ所は `if (d >= r.nearestDistance) continue;`（厳密に近い方が勝つ）なので、
    ぴったり同距離のときだけ、返る順番で勝ち負けが変わります。
  → 今回のずれの説明にはなりません（ロボットより先に人がずれている）。
    ただし「3本同時」で順番が変わる余地は残っています。

(D) 実時間を見ている所（動きには使っていないが、記録に出る）

  Robot/GridPlanner.cs:446,605,675  Stopwatch → LastPlanMs
    → ServingRobotController:2209 で PlanMsMax に入り、trips.csv の plan_ms_max と
      run_info の plan_ms に出るだけ。**打ち切りには使っていません**
      （Plan() の中を読みました。経過時間で抜ける枝はありません）。
  Experiment/ExperimentRunner.cs:186,518  Time.realtimeSinceStartup → _wallSeconds
    → 段階Bの「1回 40 分」の打ち切りにだけ使う。超えなければ動きは変わりません。
      今回は 654 秒・601 秒なので超えていません。
  Robot/ServingRobotController.cs:1805  _open.startTimeS = Time.time
    → stops.csv の t_s に出るだけ（下の (E)）。

(E) 時計が2系統あって、ずれていく（記録の話。今回の原因ではない）

  ・stops.csv の t_s は `Time.time - _timeOffset`（ExperimentRunner:746）。
    Time.captureDeltaTime = 0.05 が効いているので、**ちょうど 0.05 の倍数**になります
    （観測値 1548.05・1562.15 と合う）。
  ・ほかの記録（yields・groups・give_ups・summary の total_minutes）は ops.NowS。
    こちらは `NowS += dt` と float で足し続けているので、だんだんずれます。
    実際に float32 で 0.05 を積んで測りました:

        30,925 コマ（本当 1546.25 秒）→ NowS 1546.58 秒   ずれ +0.33 秒
       100,000 コマ（本当 5000.00 秒）→ NowS 4999.28 秒   ずれ −0.72 秒
       215,000 コマ（本当 10750.0 秒）→ NowS 10726.8 秒   ずれ −23.2 秒

    今回の回は 179 分（約 215,000 コマ）なので、**終わりごろには 20 秒以上**
    2つの時計が食い違います。どちらの回でも同じようにずれるので
    「2回がずれる」原因ではありませんが、記録として直した方がよい所です。

(F) 見つからなかったもの（念のため確かめた）

  ・種を使わない乱数（UnityEngine.Random.Range / .value など）… **1か所もなし**
    （客の中身・来方は Group.rng と _arrivalRng で種から作っている。依頼 v8b の B）
  ・コルーチン・フレームをまたぐ待ち（StartCoroutine / yield return / async）… なし
  ・Time.frameCount で分岐する所 … なし
  ・FindObjectsOfType で順番に頼る所 … 回している間は使っていない（下ごしらえだけ）
  ・Time.captureDeltaTime は両方の回で 0.05 で効いていた（run_info.json）


====================================================================
3. Mac が忙しい（3本同時）とどちらに効くか
====================================================================

  元                      3本同時にすると            結果がどちらに動くか
  --------------------------------------------------------------------------------
  (A) 道の計算が非同期    1コマに回せる道探しが減り   ★ 効く。向きも読める:
                          pathPending が **長くなる**   ・人が譲るのが遅れる →
                                                          ロボットの止まりが増える・長くなる
                                                        ・着いた判定が遅れる →
                                                          会計・片付けの始まりが遅れ、
                                                          卓が回らなくなる → 入店待ちが伸びる
                                                        ・どちらも「Unity の作り」の
                                                          判定線（止まり・卓を空けた）を
                                                          踏みやすい向き
  (B) 局所回避（RVO）     スレッドの割り当てが変わる  効きうるが向きは読めない。
                                                      今回は主因ではない
  (C) 物理の返る順番      変わりうる                  32 個を超えたときだけ。
                                                      向きは読めない
  (D) plan_ms             **大きくなる**              記録だけ。動きは変わらない
  (D) wall_seconds        **大きくなる**              ★ 40 分の打ち切りに当たると
                                                      status が stopped になる。
                                                      今回 11 分なので 3 本でも 33 分の見込みで、
                                                      余裕は少ない。⑦2台のような重い条件は
                                                      当たる恐れあり
  (E) 時計のずれ          変わらない                  record。同じようにずれる

  まとめると、**忙しくすると「人の反応が鈍る」向きに偏ります。**
  夜に3本同時で回した結果を、昼に1本で回した基準データ v2 と比べると、
  Unity の作りが悪くなったように見える恐れがあります。
  （判定 judge_night.py は「種の中央値が基準の最大を超えたら Unity の作り」なので、
   この偏りがそのまま印になりえます）


====================================================================
4. 直す案と、動きがどれだけ変わりそうか
====================================================================

(0) 先に：記録だけの直し（動きは1つも変わらない。基準データ v2 はそのまま使える）

  ・plan_ms_max / wall_seconds は実時間なので、台帳や判定で「同じか」を比べる列から外す。
    （依頼 v11 の 3 B で「実時間と timestamp を除いて並べる」と書いてあるのと同じ扱いを、
      plan_ms_max にも広げる）
  ・stops.csv の t_s を ops.NowS に合わせる（いまは Time.time 由来）。
  ・ops.NowS を float の足し算から「コマ数 × 0.05」に変える（double かコマ数で持つ）。
    → (E) の 20 秒の食い違いが消えます。
  いちばん安くて、確かめの土台が固まります。

(1) 人の道の計算を同期にする（本命の直し）

  PeopleNav.Go で agent.SetDestination をやめ、
  NavMesh.CalculatePath（同期）＋ agent.SetPath に変える。
  → pathPending が true になる場面が無くなり、(A) の 3 か所が
    「このコマは何もしない」をしなくなります。

  動きの変わり方：**小さいが確かに変わります。**
    いままで道が引けるまで 1〜数コマ「何もしない」でいた分が無くなるので、
    人は少し早く動き出し、少し早く着き、少し早く譲ります。
    今回のずれ（4 コマ = 0.2 秒）と同じ大きさの前倒しが、
    1回の中で人の動きのたびに起きます。
  → **基準データ v2 はそのままでは使えなくなります**（18回の回し直しが必要）。
  → 1回あたりの計算は重くなります（同期で引く分、コマの実時間が伸びる）。
    どれだけ重くなるかは測らないと分かりません。

(2) 毎コマの道探しの枠を大きくする（NavMesh.pathfindingIterationsPerFrame）

  いまは触っていないので Unity の既定（100）です。これを大きく（例 5000）すると、
  ほとんどの注文が 1 コマで片付き、pathPending の長さのばらつきが小さくなります。
  → 1行足すだけ。動きの変わり方も (1) より小さい。
  → ただし **「必ず同じ」の保証にはなりません**（混んだときは依然ずれます）。
  → これも「少し早く道が引ける」方向なので、基準データ v2 とは少しずれます。

(3) よけの判断だけ、pathPending で打ち切らない

  PersonRobotYield の AisleAboutToEnter が、道が引けていないときは
  -1（入らない）ではなく「前に引けていた道」を使うようにする。
  → 今回出た症状（譲りのずれ）には直接効きます。
  → 着いた判定（StaffAgent・GuestAgent）のずれは残るので、
    「必ず同じ」にはなりません。
  → 動きの変わり方は (1) より小さいが、やはり基準データ v2 とは変わります。

(4) 人の局所回避を切る（おすすめしません）

  avoidanceQuality を NoObstacleAvoidance にすると (B) が消えますが、
  人が人をよけなくなるので、人どうしの詰まり（people_jams）・すれ違い・
  通路の混み方がまるごと変わります。
  → **動きが大きく変わります。基準データ v2 は使えません。**
    サービスロボットの話として、人が人をよけないのは実感に合わないので、
    これは選ばない方がよいと思います。

(5) 「必ず同じ」を目指さず、ばらつきとして扱う（動きを変えない案）

  いまのまま回し、同じ種の1回を取り直しても同じにならない前提で、
  **種を10個にして中央値で比べる**（設計 v0 の 1 章で決めた通り）。
  → 動きは1つも変わらないので、**基準データ v2 がそのまま使えます**。
  → ただし 3 章の「忙しいと人の反応が鈍る」偏りは残るので、
    夜（3本同時）と昼（1本）の結果を混ぜて比べられません。
    判定するなら「夜の条件で取り直した基準」と比べる必要があります。
    （設計 v0 の 4 章で、最初の夜の60回を基準データ v3 にすると決めてあるので、
      そこで「夜の条件の基準」に置き換わります。筋は通ります）

(6) 選ぶときに知っておいてほしいこと

  ・(1)〜(4) はどれも「基準データ v2 の 18 回を回し直す」が付いてきます。
  ・(0) と (5) だけなら回し直しは要りません。
  ・v12（夜の仕組み）に進むうえで必要なのは「同じ結果が出ること」ではなく
    「忙しさで結果が偏らないこと」です。(5) だけだと偏りは残るので、
    (0) ＋ (2) ＋ (5) の組み合わせ（安く・偏りを小さくし・ばらつきは認める）が
    いちばん釣り合いがよいと思います。
    「同じ種なら必ず同じ」まで欲しいなら (0) ＋ (1) ＋ (2) で、
    そのうえで 18 回を回し直す形です。


====================================================================
5. 決める前にやれる、安い確かめ（提案）
====================================================================

どれが効いているかを切り分けるのに、同じ ③1台・種1 を2回ずつで足ります。

  試し1  (2) だけ入れる（pathfindingIterationsPerFrame を 5000 に）
         → 2回が一致するか。しなければ (A) 以外の元も残っていると分かる
  試し2  (1) を入れる（人の道を同期にする）
         → 2回が一致するか。ここで一致すれば、元は (A) だけと確定できる
  試し3  試し2 の状態で、わざと重くして回す（ほかに2本回しながら1本）
         → 忙しさで結果が動かないかを見る。v12 で本当に欲しいのはこれ

  どれも1回 11 分なので、2回ずつ3通りで 70 分ほどです。
  (1) を入れた版での基準データの回し直しは、試し2・試し3 の結果を見てから決める形に
  できます。

いまは何も直していません。どの案で進めるか決めてください。


====================================================================
6. 試し1・試し4 の結果と、エイイチの決定（2026-10-25 19:40 追記）
====================================================================

(1) 結果

  出力：KIOXIA service_sim/unity/20261025_det_check_cs2_fam_p1_r1_s1 と _2・_3・_4

    試し  切り替え                                        2回がずれたコマ
    ------------------------------------------------------------------------
    試し1  道探しの枠 5000 / 同期 切 / 回避 切            frame 19178（t_s 958.759）
    試し4  道探しの枠 5000 / 同期 入 / 回避 切る          frame 30537（t_s 1527.184）

  ★ **ずれ方が「コマのずれ」ではなく「小数の最下位」でした。**

    試し1 の最初のずれ   people_sum 191507181 → 191507198（差 17）
    試し4 の最初のずれ   people_sum 195291062 → 195289556（差 1506）

    指紋は x を 31 倍・z を 17 倍して足しているので、差 17 は
    **1人の z が 0.1 mm だけ違った**という意味です。
    コマが1つずれたなら 0.05 秒 × 歩く速さ ＝ 6 cm 以上（600 単位以上）動くので、
    前に見た 1546 秒の「4コマずれ」とは別の形です。
    どちらの回も、ずれたコマの path_pending は 0（道待ちの人はいない）でした。

  → 道の非同期（pathPending）は **原因から外れました**。
    局所回避（RVO）を切っても残るので、**RVO も単独の原因ではありません**。

(2) 物理の返る順番は、白と出ました

  run_info.json（4回ぶんぜんぶ）
    sensor_max_hits = 6 ／ sensor_overflows = 0

  前のセンサーの箱に入った当たりは最大 6 個で、枠の 32 個に遠く届いていません。
  **切り捨ては一度も起きていない**ので、2 章 (C) の「あふれたときにどれが落ちるか」は
  原因から外せます（残るのは「ぴったり同じ距離で並んだとき」だけ）。

(3) エイイチの決定（2026-10-25）

  **「同じ種なら必ず同じ」は今は追わない。ばらつきとして扱い、
    種10個の中央値で比べる（4 章の (5)）で進める。**

  ・(0) の記録の直しも入れない（下の (5)）
  ・試しの直しは元に戻す（(4)）
  ・残りの候補は直さず、別の課題にする（7 章）


====================================================================
7. 残っている候補の一覧（直していません。別の課題にします）
====================================================================

エイイチの挙げた「Play ごとに変わるインスタンス ID の順番に頼る所」を
ぜんぶ当たりました。**挙げられた3つは、どれも原因ではありませんでした。**

  候補                                        調べた結果
  --------------------------------------------------------------------------
  GetInstanceID()                             **1か所もなし**（ServiceSim 全体）
  Object を鍵にした Dictionary / HashSet       4 つあるが、**どれも回していない**
    JamWatcher._state                           （Dictionary<PersonBody, State>）
    ServiceOps._carrying / _lastBatch /          （Dictionary<ServingRobotController, …>）
                _stopDishes
    → ぜんぶ TryGetValue と鍵引きだけ。foreach は 0 か所。
      回し順が変わっても結果は変わりません
  FindObjectsOfType の並び                     回している間は使っていない。
    ExperimentRunner:195-199（下ごしらえで1つずつ拾う）
    ExperimentRunner:230 DisableCameras（ぜんぶ enabled=false にするので順番に意味なし）
    ほかは Editor の絵と札だけ
  NavMesh を焼く元（_sources）の並び            Unity の NavMeshBuilder.CollectSources
                                              （階層の順）＋ 店を建てた順。決まっています

理屈では残る所（**未確認**。小数の最下位を動かしうるもの）

  ア. Unity の NavMeshAgent 自身の動き（本命の疑い）
     人（スタッフ・客）は updatePosition を切っていないので、
     位置を決めているのは **Unity のエージェントの計算**です。
     これはワーカースレッドの仕事として解かれるので、
     どのスレッドがどの人を担当するか・どの順でまとめられるかが
     Play ごとに変わると、最下位の桁が変わりえます。
     回避を切っても残るのは、回避ではなく
     この「進める部分」が効いているからではないか、と見ています。
     → 確かめるには、人も updatePosition を切って自分の script で動かす形
       （ロボットと同じやり方）にして2回回すのが早いです。
       ただし人の動きは大きく変わるので、基準データは作り直しになります。

  イ. Play ごとに作り直す NavMesh と、作り直す通行者の種類
     1回ごとに Play を出入りするので、NavMesh は毎回焼き直しです。
     人用の面は NavMeshBaker が `NavMesh.CreateSettings()` で
     **その Play のための通行者の種類を1つ作って**焼きます
     （NavMeshBaker.cs:148-151）。
     焼き直した面のポリゴンの並びや種類の番号が前の Play と違うと、
     CalculatePath が返す角の座標が最下位の桁で変わりえます。
     → NavMeshAreas.asset は Humanoid 1種のまま増えていないので
       「残り続ける漏れ」はありません。変わるのは Play の中だけです。
     → 確かめるには、1回の Play の中で同じ条件を2回続けて回して
       （Play を出入りしない）比べると分かります。

  ウ. 同じ距離で並んだときの勝ち負け（可能性は低い）
     FrontSensor の「いちばん近いもの」は `d >= nearest` で切るので、
     **ぴったり同じ距離**のときだけ、返ってきた順で勝ちが変わります。
     HigherPriorityNear（`d > bestD` で切る）も同じ形です。
     小数がぴったり一致することはまず無いので、可能性は低いと見ています。

  エ. 使い回しの体の並び（PersonBody.All / StaffAgent.All）
     待合・立ち歩きの人は体を使い回すので、
     OnEnable / OnDisable の順で静的リストの並びが変わります。
     ただし、これを回している所は
       ・いちばん近い／いちばん小さいを選ぶ（順番に依らない）
       ・数を数える（順番に依らない）
     だけで、**float を足し込んでいる所はありませんでした**。
     指紋（det_frames）の people_sum は私が足した記録用で、動きには使っていません。

別の課題にするときの進め方（案）
  1. イ を先に確かめる（Play を出入りせず、同じ Play の中で2回）。安くて切り分けが進む
  2. イ でも残るなら ア。人の動かし方を変える話になるので、
     基準データの作り直しとセットで決める
  3. どちらも「ばらつきとして扱う」決定を覆すものではありません。
     種10個の中央値で比べる限り、0.1 mm のずれは判定に出ません


====================================================================
8. 既知のずれとして残すもの（直していません）
====================================================================

(1) 来客の締め切りが 9.3 秒ぶん長い

  ops.NowS は `NowS += dt` と float で足しているので、だんだん遅れます。
  来客を止める時刻だけは **絶対値で比べている**ため、ずれがそのまま効きます。

    ExperimentRunner.cs:602  if (ops.NowS >= _arrivalsEndS …)
    GroupManager.cs（Arrive） if (now >= arrivalsEndS) return;
      _arrivalsEndS = peak_min × 60 = 7200 秒（定数）

    本当の 7200 秒のとき NowS = 7190.68 秒（−9.32 秒）
    → **本当の 7209.3 秒まで来客を受け付けています**（9.3 秒ぶん長い）

  2回の回しで同じようにずれるので、ばらつきの原因ではありません。
  直すと来客が 9 秒ぶん減るので、基準データを作り直すときに一緒に決めます。

(2) 記録の時計が2系統ある

  stops.csv の t_s は `Time.time - _timeOffset`（ちょうど 0.05 の倍数）。
  ほかの記録（yields・groups・give_ups・summary の total_minutes）は ops.NowS。
  179 分の回では 20 秒以上食い違います。記録だけの話です。

(3) plan_ms_max は実時間

  trips.csv の plan_ms_max と run_info の plan_ms は Stopwatch で測った実時間で、
  **毎回必ず違います**（1回めの往復から違う）。動きには一切使っていません。
  台帳や判定で「同じか」を比べる列から外してください
  （実時間・timestamp と同じ扱い）。

  (1)(2)(3) はどれも 4 章の (0) に当たる直しです。**入れていません。**
