==================================================================== 依頼 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) ServiceOps._carrying / _lastBatch / (Dictionary) _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) に当たる直しです。**入れていません。**