サービスロボット導入シミュレーション Log#13 | 隠れていた待ちが見えて、次は「どこで詰まるか」へ

配膳ロボットを入れたのに、お客さんが店に来てから席に着くまでの待ち時間(入店待ち)が、ロボットの台数でまったく変わらない。Unity(建物や人を3Dで置いて動かせるソフト)の中に作ったファミレスで、ロボット0台・1台・2台を回すと、そんな結果が出ていました。サイトに置いている計算の試算( https://pixelstomotors.com/sim/serving-robot )では、ロボットを入れるほど入店待ちは短くなります。どちらかが店の動きを取りこぼしています。

調べると、原因は2つでした。1つはレジです。Unity の店はレジの内側に店員が1人だけで、会計は1組ずつしかできません。一方、試算では会計を4人のスタッフのだれでも同時にできていました。もう1つは、Unity に入れていた逃がし弁です。片付けや会計が10分止まった卓を、片付けをしないまま空いたことにする仕組みで、だれもまだ手を付けていない「ふつうの片付け待ち」まで打ち切っていました。そのため、席は空いているのにお客さんが待たされている時間が、Unity では実際より小さく出ていました。

そこで、試算の方に「会計はレジ1つにつき1組ずつ」を入れ(試算 v2.8)、レジの数を1つ・2つから選べるようにしました。レジ1つ・会計90秒だと、1時間にさばけるのは40組までです。この店の「普通」の混み具合は1時間42組なので、ロボットの前にレジが詰まります。Unity の方は、逃がし弁がスタッフが仕事を取ってから止まった時間だけを数えるように直しました。

直したあと、③の店では、ロボットを入れると入店待ちが縮む向きが試算とそろいました(3.90分 → 3.30分 → 2.03分。試算は 3.30分 → 2.60分 → 1.18分)。一方で、店のどこで詰まるかは食い違ったままです。試算ではレジに列ができ、Unity では片付けが追いつかずに卓が空きません。スタッフ1人が1組に使う時間が、Unity の方が7〜18%長いためです。試算はピークでスタッフの手が95%ふさがっているので、1割に満たない差でも人手が足りなくなります。

やったことなぜ結果
Unity の18回の記録を読み直した
入店待ちがロボットの台数で変わらない原因を探すため
スタッフの仕事は軽くなかった。原因はレジと逃がし弁
試算に「会計はレジで1組ずつ」を入れ、レジの数を選べるようにした(v2.8)
実際の店はレジが1つで、Unity もそう作っているため
レジ1つだと、ロボットより先にレジが詰まる条件がはっきりした
会計を10分以上待つ条件に「レジが詰まる」の印を付けた
レジがせき止めると「店が回らない」がかえって減って見えたため
「店が回らない」とは別に、レジの詰まりを読めるようにした
Unity の逃がし弁を、スタッフが仕事を取ってから数えるようにした
ふつうの片付け待ちまで打ち切っていたため
卓を空けた回数 24 → 0。隠れていた片付け待ちが表に出た
時間帯ごとの記録を足して、18回を回し直した
どこで詰まるかを試算と並べるため
③は向きがそろった。詰まる場所は、試算がレジ・Unity が片付け

下の図で、ピークのあいだに「片付けが済んでいない卓」と「会計を待っている組」がどう増えるかを、試算と Unity で並べて見られます。店とロボットの台数を切り替えられます。

作業ログ ― この記事で扱う範囲と、Unity・試算の版

前の記事(LOG374)で、ロボットに店の地図を渡して道を自分で引かせる形にし、2つの店 × ロボット0・1・2台 × 来る客の並び3通り(種0・1・2)の18回を「基準データ v1」として区切りました。そのとき残ったいちばん大きいずれが、入店待ちがロボットの台数で変わらないことでした。この記事は、その原因を調べるところから、直した版で18回を回し直して「基準データ v2」を取るところまでです。

もの始め終わり
Unity のコード(Unity Version Control)
cs:143
cs:144(逃がし弁の数え方・試算 v2.8 を読む・時間帯ごとの記録・台帳の列)
試算(serving-robot.html)
v2.7
v2.8(会計はレジで1組ずつ。レジの数を1つ・2つから選べる。p2m-site の commit f26f27c)
Unity が読む条件(serving-robot-params.json)
v2.7
v2.8(Unity の6条件はレジ1つ)
店の形(p2m-site の public/sim/layouts/)
8a5b2f6a016a
8a5b2f6a016a(変えていない)
段階Bの18回
cs:143(基準データ v1)
cs:144(基準データ v2)

店は2つです。③は「効率重視・入口が角」(42卓)、⑦は「③の通路を95cmに」(42卓)。どちらもスタッフ4人、来客は1時間42組(普通)、ロボットは料理を運ぶだけ、1往復で最大3卓です。種は来る客の並びを決める番号で、種0・1・2で来る組は64・83・102組になります。試算の平均は約84組なので、試算と比べるときは種1(83組)を主に見ます。

この記事の「ねらい・変わったこと・見込める効果」のうち、効果の数字はすべて Unity と試算で実際に回して測った値です。

段階Bの18回(cs:143)を読み直した:スタッフの仕事は軽くなかった

LOG374 の時点では、Unity のスタッフの仕事が試算より軽いのではないかと疑っていました。ロボット0台でも、席は空いているのに待った時間が試算より短かったためです(種0・1で0.3〜0.7分、試算は3.36分)。18回の記録だけを読んで(Unity は動かさずに)、これを確かめました。

来た組が試算に近い種1(83組)で比べると、スタッフの手がふさがっている割合は ③0台で0.77(試算0.75)、⑦0台で0.82(試算0.74)です。スタッフの仕事は軽くありません。代わりに見つかったのが、次の2つです。

b5_register_and_valve.py(KIOXIA の service_sim/unity/stageB_cs143/)

# 段階Cの2回目(2026-10-25):入店待ちがロボットの台数で変わらない原因を調べる。
# 18回(cs:143)の記録だけを読む。Unity は触らない。
#  1) レジ:会計1組あたりのレジの時間と、1時間にさばける組の上限(Unity はレジの内側に1人だけ)
#  2) 逃がし弁「卓を空けた」(会計・片付けで600秒止まった卓を、片付けずに空ける)の数と、それで消えたスタッフの仕事
#  3) スタッフの手のふさがり(試算と比べる。来た組の数が近い種1で見る)
import csv, json, sys, os, statistics as st
B = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
P = json.load(open(sys.argv[2])) if len(sys.argv) > 2 else None
site = {}
if P:
    for u in P["unity_conditions"]:
        c = u["condition"]; site[(c["store"], c["robots"])] = u["site_result"]
print("店 台 種 組 | 手ふさがり 試算 | 回転(分) 試算 | 席空待ち 試算 | レジ1組(秒) 1時間の上限(組) | 卓を空けた 会計/片付け | 消えた仕事(スタッフ分) 全仕事に対して")
for stn in ("fam_p1", "fam_p5"):
    for r in (0, 1, 2):
        for s in (0, 1, 2):
            d = f"{B}/20261024_stageB_cs143_{stn}_r{r}_s{s}"
            j = json.load(open(d + "/summary.json"))
            m, sl, t = j["site_metrics"], j["sim_like"], j["staff_time_s"]
            pays = sl["register_pay_count"]; reg = t["会計"] / max(pays, 1)
            gu = list(csv.DictReader(open(d + "/give_ups.csv")))
            fp = sum(1 for g in gu if g["action"] == "卓を空けた" and g["job"] == "会計")
            fc = sum(1 for g in gu if g["action"] == "卓を空けた" and g["job"] == "片付け")
            clears_done = max(m["groups"] - fc, 1)
            per_clear = t["片付け"] / clears_done
            lost = (fc * per_clear + fp * reg) / 60
            work = sum(v for k, v in t.items() if k != "待機") / 60
            sr = site.get((stn, r), {})
            print(stn, r, s, m["groups"], "|", round(m["staff_util"], 3), sr.get("staff_util"), "|",
                  round(m["turn_mean"], 2), sr.get("turn_mean"), "|", round(m["staff_wait_mean"], 2), sr.get("staff_wait_mean"), "|",
                  round(reg), round(3600 / reg, 1), "|", fp, fc, "|", round(lost), f"{lost / work:.0%}")

b5_register_and_valve_output.txt

店 台 種 組 | 手ふさがり 試算 | 回転(分) 試算 | 席空待ち 試算 | レジ1組(秒) 1時間の上限(組) | 卓を空けた 会計/片付け | 消えた仕事(スタッフ分) 全仕事に対して
fam_p1 0 0 64 | 0.667 0.7454 | 3.81 9.7179 | 0.32 3.3554 | 95 37.8 | 0 0 | 0 0%
fam_p1 0 1 83 | 0.773 0.7454 | 4.95 9.7179 | 0.55 3.3554 | 95 37.9 | 7 17 | 51 10%
fam_p1 0 2 102 | 0.809 0.7454 | 5.99 9.7179 | 4.22 3.3554 | 93 38.7 | 8 29 | 81 13%
fam_p1 1 0 64 | 0.598 0.6428 | 3.09 4.2104 | 0.18 1.2201 | 98 36.7 | 1 0 | 2 0%
fam_p1 1 1 83 | 0.711 0.6428 | 3.85 4.2104 | 0.44 1.2201 | 95 37.7 | 7 1 | 13 3%
fam_p1 1 2 102 | 0.76 0.6428 | 4.57 4.2104 | 3.49 1.2201 | 94 38.3 | 13 15 | 57 9%
fam_p1 2 0 64 | 0.622 0.6257 | 4.38 3.7762 | 0.3 0.7468 | 101 35.6 | 2 3 | 12 3%
fam_p1 2 1 83 | 0.697 0.6257 | 3.74 3.7762 | 0.74 0.7468 | 102 35.4 | 10 6 | 30 6%
fam_p1 2 2 102 | 0.801 0.6257 | 5.37 3.7762 | 3.4 0.7468 | 97 37.1 | 12 24 | 86 14%
fam_p5 0 0 64 | 0.692 0.7444 | 6.15 9.9907 | 0.6 3.9497 | 95 37.8 | 1 11 | 34 7%
fam_p5 0 1 83 | 0.817 0.7444 | 5.95 9.9907 | 0.67 3.9497 | 93 38.7 | 6 19 | 60 11%
fam_p5 0 2 102 | 0.831 0.7444 | 6.58 9.9907 | 4.54 3.9497 | 93 38.6 | 8 47 | 159 24%
fam_p5 1 0 64 | 0.64 0.6682 | 4.87 4.7822 | 0.42 1.3606 | 99 36.5 | 1 8 | 29 6%
fam_p5 1 1 83 | 0.787 0.6682 | 5.22 4.7822 | 0.53 1.3606 | 94 38.2 | 6 13 | 48 8%
fam_p5 1 2 102 | 0.785 0.6682 | 5.94 4.7822 | 4.59 1.3606 | 94 38.3 | 10 30 | 97 15%
fam_p5 2 0 64 | 0.643 0.6208 | 3.98 3.845 | 0.3 0.8016 | 99 36.5 | 1 3 | 12 3%
fam_p5 2 1 83 | 0.767 0.6208 | 5.36 3.845 | 0.72 0.8016 | 94 38.3 | 6 14 | 51 9%
fam_p5 2 2 102 | 0.765 0.6208 | 5.37 3.845 | 4.38 0.8016 | 97 37.2 | 13 29 | 106 17%

原因1:Unity のレジは1時間に約38組しかさばけない

Unity の店は、レジの内側(客と反対側)に店員が1人だけ立つ作りです。会計は1組ずつで、1組あたり93〜102秒かかっています(会計の90秒と、レジまで歩く時間)。1時間にさばけるのは約36〜39組で、来客の1時間42組より少なくなります。ロボットは料理を運ぶだけなので、レジの詰まりは何台入れても減りません。試算の方は、会計(90秒)を4人のだれでも同時にできる作りでした。

原因2:逃がし弁「卓を空けた」が、ふつうの片付け待ちを打ち切っていた

逃がし弁は、会計・片付けが600秒(10分)止まった卓を、片付けをしないまま空いたことにする仕組みです。人が通れない店の作りで回し続けられなくなるのを防ぐために入れたものでした。ところが、だれもまだ取っていない片付けの仕事が板(スタッフがやる仕事の一覧)に載ったまま待っている時間まで数えていました。種1の③0台では、83組のうち24卓(会計7・片付け17)が空けられ、スタッフの仕事が約1割消えていました。試算ではロボット0台のとき、客が帰ってから卓が使えるまで平均9.7分かかるのがふつうです。Unity はそこを10分で打ち切るので、席は空いているのに待った時間が0.55分(試算3.36分)と小さく出ていました。

来る客の並び3通りは、平均84組に近いのが種1だけだった

来た組は種0・1・2で64・83・102組でした。試算の平均84組に近いのは種1だけで、種0と種2は大きく外れています。種2(102組)は満席で待つ時間が長く、入店待ちの平均を引き上げます。3回の平均で試算と比べるのはやめ、種1を主に見ることにしました。

決めたこと:会計は90秒のまま、レジは試算に入れる、逃がし弁は Unity を直す

調べた結果をもとに、次の3つを決めました。

問い決めたこと
ファミレスで、レジの会計は1組あたりどれくらいかかるか
90秒くらい(今の試算のまま)
レジの扱いはどちらに合わせるか
試算にレジを入れる。会計は1組ずつ(実際の店に近い。試算の条件をすべて回し直す)
逃がし弁の数え方
スタッフが仕事を取ってから止まった時間だけ数える(Unity の直し)

試算 v2.8:会計はレジで1組ずつにし、レジの数を選べるようにした

試算は SimPy(店の中の出来事を時間の順に計算する Python の道具)で書いています。会計の部分を、次のように変えました。

  • 食べ終えた組は、卓に座ったままレジの順番を待つ(その間、卓はふさがったまま)
  • 順番が来たら、スタッフの手が空くのを待って会計する(90秒。スタッフの手はこれまでどおりふさがる)
  • 食べ終えてから会計が始まるまでを、新しい数字 pay_wait_mean として出す

乱数の使い方は変えていません。レジの数を無限(999)にすると、v2.7 の数字と完全に一致することを確かめました。

fam_p1 0 999 3.99 3.3554 9.7179 0.521
fam_p1 0 1 3.301 0.9547 2.4872 12.278
fam_p1 0 2 3.7247 3.2872 8.9789 0.951
fam_p1 2 999 1.0758 0.7468 3.7762 0.293
fam_p1 2 1 1.1788 0.3853 2.0672 7.984
fam_p1 2 2 1.1018 0.7531 3.1456 0.712
32850 {'store': 'small', 'staff': 1, 'robots': 0, 'arrivals_per_h': 15, 'mode': 'serve', 'trays': 1, 'fragile_by': 'any', 'registers': 1}

(列は 店・ロボットの台数・レジの数・入店待ち・席は空いているのに待った・客が帰ってから卓が使えるまで・会計待ち。最後の行は全条件の数と最初の条件)

下書きで③⑦を回すと、レジ1つでは会計を8〜12分待った

全条件を回す前に、③⑦だけ下書きで回しました(スタッフ4人・上げ膳だけ)。

店 台 来客 | v2.7: 入店 席空待ち 会計待ち | レジ1つ: 入店 席空待ち 満席 会計待ち 回転
fam_p1 0 30 | 0.3165 0.1391 0.18 | 0.401 0.145 0.0783 2.49 2.1839
fam_p1 1 30 | 0.2683 0.0905 0.11 | 0.2512 0.0611 0.013 2.35 2.0755
fam_p1 2 30 | 0.2718 0.0933 0.12 | 0.2275 0.0483 0.0 2.2 2.0727
fam_p1 0 42 | 3.99 3.3554 0.52 | 3.301 0.9547 2.1671 12.28 2.4872
fam_p1 1 42 | 1.8213 1.2201 0.31 | 2.5992 0.572 1.8492 7.99 2.1338
fam_p1 2 42 | 1.0758 0.7468 0.29 | 1.1788 0.3853 0.6185 7.98 2.0672
fam_p1 0 54 | 12.8018 8.7527 0.58 | 13.0272 1.5483 11.3023 15.44 2.3904
fam_p1 1 54 | 11.4181 5.1923 0.46 | 11.9538 1.2742 10.5012 12.05 2.1086
fam_p1 2 54 | 7.5274 3.3804 0.4 | 11.1021 1.1728 9.7529 11.45 2.0594
fam_p5 0 30 | 0.3535 0.1741 0.19 | 0.2835 0.1041 0.0 2.55 2.1864
fam_p5 1 30 | 0.2764 0.094 0.11 | 0.232 0.0497 0.0 2.23 2.091
fam_p5 2 30 | 0.2756 0.0944 0.11 | 0.2212 0.0409 0.0 2.07 2.0712
fam_p5 0 42 | 4.7827 3.9497 0.51 | 2.7562 0.8929 1.6822 11.74 2.5036
fam_p5 1 42 | 2.142 1.3606 0.35 | 1.815 0.4851 1.1464 8.06 2.1404
fam_p5 2 42 | 1.1434 0.8016 0.27 | 1.9669 0.4479 1.3379 7.96 2.0967
fam_p5 0 54 | 13.7486 9.5684 0.59 | 12.3263 1.6042 10.5429 15.33 2.4291
fam_p5 1 54 | 11.0493 5.1199 0.45 | 13.7382 1.3233 12.2326 12.25 2.1291
fam_p5 2 54 | 9.0093 3.7496 0.42 | 10.9851 1.1374 9.6675 11.84 2.0793
fam_p1 0 レジ2つ 3.7247 3.2872 0.2584 0.95
fam_p1 1 レジ2つ 1.8796 1.0966 0.6048 0.67
fam_p1 2 レジ2つ 1.1018 0.7531 0.1716 0.71
fam_p5 0 レジ2つ 3.8334 3.2564 0.3962 1.02
fam_p5 1 レジ2つ 1.7417 1.0922 0.4659 0.84
fam_p5 2 レジ2つ 0.969 0.6526 0.134 0.69

レジ1つ・会計90秒だと、1時間にさばけるのは40組が上限です。③⑦の「普通」は1時間42組なので、レジがいちばんの詰まりになります。食べ終えた客が席で平均8〜12分会計を待ち、その間卓がふさがるので、入店待ちの中身が「片付けが追いつかない」から「満席」に入れ替わります。ロボットを入れても会計待ちはほとんど減りません。レジ2つなら、数字は v2.7 とほぼ同じに戻ります。平均8〜12分は長めに見えたので、全部の条件を回す前に進め方を決めました。

問い決めたこと
レジ1つ・90秒だと「普通」でもレジが詰まる。どう進めるか
レジの数を選べるようにする(1つ・2つ)。最初に見せるのは1つ
レジの決まりを入れる店
全部の店。大型店は最初からレジ2つ

32,850条件を、クラウドと Mac に分けて回した

レジの数が条件に加わったので、条件の数は16,425から32,850に倍になりました。1条件は40回の平均です。クラウドの作業環境(2コア)だけでは5時間ほどかかる見込みだったので、途中から Mac(4コア)にも分けて回しました。Mac 側は1回の呼び出しが3分までなので、160秒ずつ区切って回し、途中経過をファイルに書いて続きから回す形にしています(run_timed.py)。

重なって両方で回した37,581条件は、数字がすべて一致しました(クラウドは simpy 4.1.2、Mac は 4.1.1)。乱数の種で結果が決まる作りなので、どこで回しても同じになります。

レジ1つにすると「店が回らない」がかえって減った

全条件を回すと、レジ1つでは「店が回らない」の条件が2,956から1,937に減りました。「店が回らない」は、席は空いているのに片付けや案内が追いつかず、平均10分以上待つ条件です。レジが客の流れをせき止めるので、片付け・案内の手は追いつくようになり、そのぶん食べ終えた客が卓で会計を待つ形になるためです。

レジ1: 店が回らない 1937/16425(v2.7 2956)  入店待ちの差 平均+0.09 最小-15.06 最大+17.46  会計待ち 平均8.97 最大87.3
レジ2: 店が回らない 2907/16425(v2.7 2956)  入店待ちの差 平均-0.06 最小-4.85 最大+7.93  会計待ち 平均1.39 最大57.4
③ 2 人 0 台 入店 33.0 席空待ち 23.1 会計待ち 17.4 満席 9.7
③ 2 人 2 台 入店 14.8 席空待ち 8.3 会計待ち 24.2 満席 6.3
③ 3 人 0 台 入店 8.1 席空待ち 4.8 会計待ち 17.6 満席 3.2
③ 3 人 2 台 入店 3.3 席空待ち 0.9 会計待ち 13.2 満席 2.3
③ 4 人 0 台 入店 3.3 席空待ち 1.0 会計待ち 12.3 満席 2.2
③ 4 人 2 台 入店 1.2 席空待ち 0.4 会計待ち 8.0 満席 0.6
レジ 1 会計待ち10分以上なのに「回る」 3835 /会計待ちも入れた「回らない」 5772 / 16425
レジ 2 会計待ち10分以上なのに「回る」 3 /会計待ちも入れた「回らない」 2910 / 16425

いまの決め方のままだと、③の2人・ロボット2台も「回る」になります。会計を24分待たせている状態です。会計待ちを「店が回らない」に入れるか、別の印にするかを考え、別の印「レジが詰まる」を付けることにしました。

問い決めたこと
「店が回らない」の決め方に会計待ちを入れるか
入れない。会計待ちが平均10分以上なら、別の印「レジが詰まる」を出す。LP の「店舗が回る人数」も今の決め方のまま
  • ねらい:レジがせき止めて片付けが回って見える条件でも、会計で長く待たせていることが画面で分かるようにする
  • 変わったこと:試算のページの条件に「レジ 1つ/2つ」を足し、結果に「食べ終えてから会計が始まるまで」を足した。10分以上なら「レジが詰まる」と出る。レジ1つで5,263条件、レジ2つで493条件に印が付く
  • 見込める効果:レジを1つから2つにしたときに何が変わるかを、ロボットの台数と同じ画面で比べられる。③の4人・0台では、会計待ちが12.3分から1.0分になる
試算のページで③・スタッフ3人・ロボット2台・普通・レジ1つを選んだ画面。食べ終えてから会計が始まるまで13.2分に「レジが詰まる」の印が付いている

試算のページ・LP・Unity が読む条件を v2.8 にした

公開中の試算のページに、結果とレジの切り替えを入れるスクリプト(build_page_v28.py)を書き、全37,800画面を描いて、欠け・undefined・NaN がないことを確かめました。

rendered 37800 / レジが詰まるが出た画面 6955 / 回らないが出た画面 5944 / 5台以上の注意 10800

ページのデータは倍になり、圧縮したあとの重さは約0.6MBから約1.3MBになりました。LP( https://pixelstomotors.com/serving-robot-one-less-staff )は、試算のページからビルドのときに数字を読む作りなので、読み方にレジの数を足しました(src/lib/servingSim.ts)。LP の最初の画面の「店舗が回る人数」は、4人 → 3人 から 3人 → 2人 に変わりました。

Unity が読む条件のファイル(serving-robot-params.json)も v2.8 で書き出しました。Unity の6条件はレジ1つです。

/home/claude/p2m-site/public/sim/serving-robot-params.json 6 conditions / generation 8a5b2f6a016a
  {'store': 'fam_p1', 'staff': 4, 'robots': 0, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 1, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 3.301, 'staff_wait_mean': 0.9547, 'dish_wait_mean': 0.459, 'pay_wait_mean': 12.2785}
  {'store': 'fam_p1', 'staff': 4, 'robots': 1, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 3, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 2.5992, 'staff_wait_mean': 0.572, 'dish_wait_mean': 1.9487, 'pay_wait_mean': 7.9893}
  {'store': 'fam_p1', 'staff': 4, 'robots': 2, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 3, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 1.1788, 'staff_wait_mean': 0.3853, 'dish_wait_mean': 0.9488, 'pay_wait_mean': 7.9844}
  {'store': 'fam_p5', 'staff': 4, 'robots': 0, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 1, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 2.7562, 'staff_wait_mean': 0.8929, 'dish_wait_mean': 0.4478, 'pay_wait_mean': 11.7376}
  {'store': 'fam_p5', 'staff': 4, 'robots': 1, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 3, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 1.815, 'staff_wait_mean': 0.4851, 'dish_wait_mean': 1.9698, 'pay_wait_mean': 8.0624}
  {'store': 'fam_p5', 'staff': 4, 'robots': 2, 'arrivals_per_h': 42, 'mode': 'serve', 'trays': 3, 'fragile_by': 'any', 'registers': 1} {'entry_wait_mean': 1.9669, 'staff_wait_mean': 0.4479, 'dish_wait_mean': 0.9839, 'pay_wait_mean': 7.9555}

compare_v27_v28.txt(v2.7 と v2.8 の比べ)

v2.7(レジの数を数えない)と v2.8(会計はレジ1つにつき1組ずつ)の比較。2026-10-25
v2.7 の店が回らない 2956 / 16425 条件
レジ1つ:店が回らない 1937・レジが詰まる 5263 / 16425 条件 入店待ちの差 平均+0.09 最小-15.06 最大+17.46分 会計待ち 平均8.97分
レジ2つ:店が回らない 2907・レジが詰まる 493 / 16425 条件 入店待ちの差 平均-0.06 最小-4.85 最大+7.93分 会計待ち 平均1.39分
店ごと(真ん中の人数・普通・上げ膳だけ・最初に見せるレジ):入店待ち v2.7 0台→2台 / v2.8 0台→2台 / 会計待ち 0台→2台
  small(レジ1)2.78→3.32 / 3.12→2.64 / 1.6→1.3
  family(レジ1)8.58→3.16 / 7.86→2.56 / 10.2→6.7
  large(レジ2)19.68→7.38 / 17.52→7.20 / 3.4→2.4
  hotel(レジ1)2.90→1.44 / 3.87→0.84 / 4.2→2.2
  buffet(レジ1)9.22→1.65 / 8.47→1.84 / 3.5→1.4
  fam_p0(レジ1)2.74→1.76 / 2.68→1.60 / 1.3→1.3
  fam_p1(レジ1)3.99→1.08 / 3.30→1.18 / 12.3→8.0
  fam_p2(レジ1)10.76→3.66 / 9.72→3.17 / 4.2→4.9
  fam_p3(レジ1)7.56→2.15 / 5.44→2.40 / 18.0→11.2
  fam_p4(レジ1)7.80→3.49 / 6.31→2.34 / 11.7→7.7
  fam_p5(レジ1)4.78→1.14 / 2.76→1.97 / 11.7→8.0

Unity v10:逃がし弁は、スタッフが仕事を取ってから数える(cs:144)

Claude Code(Unity のコードを書く AI)に、逃がし弁の数え方を変える依頼(v10)を出しました。依頼文はこの記事の後ろに全文を載せています。

  • 数えるのは、その組の仕事をスタッフが取っている間だけ。板に載ったまま誰も取っていない間と、レジの列が空くのを席で待っている間は数えない
  • Unity が試算 v2.8 の条件を読み、試算と同じ会計待ち(pay_wait_mean)を出す
  • 試算の版が v2.8 でない、またはレジの数が1でない条件は回さない

確かめ:③・4人・0台・種1で、卓を空けたが24から0になった

直す前(cs:143)直したあと試算 v2.8
卓を空けた240−
入店待ち1.74分3.91分3.30分
席は空いているのに待った0.55分2.71分0.95分
客が帰ってから卓が使えるまで4.95分10.76分2.49分
食べ終えてから会計まで−4.71分12.28分
スタッフの手のふさがり0.770.780.72
  • ねらい:ふつうの片付け待ちを打ち切らず、試算と同じ意味で「席は空いているのに待った」を数える
  • 変わったこと:stuckS を、スタッフが仕事を取っている間だけ足す形にした(GroupManager.cs の1行)
  • 見込める効果:卓を空けた回数は 24 → 0。隠れていた片付け待ちが表に出て、片付けの仕事が取られるまで平均511秒・最長1,145秒、10分を超えたのが37回だった

入店待ちの大きさは試算に近づきました(3.91分と3.30分)。ただし詰まる場所が違います。試算 v2.8 はレジで詰まり(会計待ち12分・片付けは2.5分で回る)、Unity は片付けで詰まりました(卓が使えるまで10.8分・会計待ち4.7分)。Unity の形は、レジを入れる前の v2.7(卓が使えるまで9.7分)に近い形です。

v10b:台帳に会計待ちを足し、60秒ごとの記録 timeline.csv を出した

どの時間帯に、誰が何をしていて、何が板に残っていたかを試算と並べるため、記録を足す依頼(v10b)を出しました。店の回し方は1つも変えていません。

  • 段階Bの台帳(unity_ledger_v1.csv)に10列を足した(会計待ち・片付け待ち・客が帰ってから卓が使えるまでを、Unity と試算の両方の値で)。前の36行は書き換えず、台帳の1行目だけを差し替えた(差し替え前の写しは unity_ledger_v1_schema1_backup_20261025.csv)
  • 新しいファイル timeline.csv:店の中の時間で60秒ごとに1行。入口で待つ組・席で会計を待つ組・レジの列・片付けを誰も取っていない卓・板に残った仕事の数・スタッフ4人がそれぞれ何をしているか・走っているロボットの台数
  • 各行で、卓の合計が42、スタッフの合計が4になるかを、回すたびに自動で数える(summary.json の timeline_check)

18回の1回目(③・ロボット2台・種0)が終わった時点で、timeline_check を読みました。173行すべてで卓の合計が42、スタッフの合計が4で、合わない行は0でした。

timeline_check {'rows': 173, 'step_s': 60, 'tables': 42, 'staff': 4, 'bad_table_rows': 0, 'bad_staff_rows': 0, 'first_bad': '', 'note': 'timeline.csv の各行で、seated_groups + tables_free + tables_dirty_untaken + tables_clearing が tables と同じか、staff_* の合計が staff と同じかを数えたもの。合わない行があれば bad_* が 0 より大きくなり、Console にも出る'}

基準データ v2:段階Bの18回(cs:144)

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(22本)

01_c1_compare_output.txt ― c1_compare_output.txt(条件ごとの比べ)・4,349字

02_sec09.txt ― 10:04 v10 の報告(claude_code_reports/20261025_1004_v10_valve_v28.txt)・10,314字

03_sec09.txt ― 10:48 v10b の報告(claude_code_reports/20261025_1048_v10b_stageB_timeline.txt)・10,966字

04_summary.json ― v10 の確かめ B の summary.json(20261025_p1_efficient_corner_fam_p1_s4_r0_a42_s1_1013)・5,098字

05_summary.json ― 18回のうち③0台・種1の summary.json(20261025_stageB_cs144_fam_p1_r0_s1)・5,411字

06_timeline.csv ― 18回のうち③0台・種1の timeline.csv・13,020字

07_timeline.csv ― 18回のうち⑦0台・種1の timeline.csv・13,894字

08_timeline.csv ― 18回のうち③2台・種1の timeline.csv・13,466字

09_timeline.csv ― 18回のうち⑦2台・種1の timeline.csv・13,294字

10_unity_ledger_v1.csv ― 段階Bの台帳 unity_ledger_v1.csv(cs:140・143・144 の54行)・16,848字

11_unity_stageC_gaps_v1.md ― ずれの一覧 unity_stageC_gaps_v1.md・7,647字

12_simulation_v2_8_haizen_sim.py.txt ― simulation/v2_8/haizen_sim.py・32,373字

13_simulation_v2_8_build_page_v28.py.txt ― simulation/v2_8/build_page_v28.py・7,332字

14_simulation_v2_8_export_params.py.txt ― simulation/v2_8/export_params.py・3,708字

15_Assets_ServiceSim_Service_GroupManager.cs.txt ― Assets/ServiceSim/Service/GroupManager.cs・31,488字

16_Assets_ServiceSim_Service_ServiceOps.cs.txt ― Assets/ServiceSim/Service/ServiceOps.cs・62,929字

17_Assets_ServiceSim_Experiment_SimParams.cs.txt ― Assets/ServiceSim/Experiment/SimParams.cs・12,858字

18_Assets_ServiceSim_Experiment_TimelineLogger.cs.txt ― Assets/ServiceSim/Experiment/TimelineLogger.cs・11,872字

19_Assets_ServiceSim_Experiment_ExperimentRunner.cs.txt ― Assets/ServiceSim/Experiment/ExperimentRunner.cs・54,860字

20_Assets_ServiceSim_Experiment_TripLogger.cs.txt ― Assets/ServiceSim/Experiment/TripLogger.cs・81,530字

21_Assets_ServiceSim_Editor_ServiceSimLedger.cs.txt ― Assets/ServiceSim/Editor/ServiceSimLedger.cs・12,318字

22_Assets_ServiceSim_Editor_ServiceSimStageB.cs.txt ― Assets/ServiceSim/Editor/ServiceSimStageB.cs・36,373字

2026-10-25 の11時ごろから15時までで18回を回しました。18回とも最後まで回り切り、台帳に cs:144 の18行(台帳の版2・59列)が書かれています。

c1_compare.py(KIOXIA の service_sim/unity/stageB_cs144/)

# 基準データ v2(段階Bの18回・cs:144・逃がし弁を直し試算 v2.8 を読む版)を、cs:143(基準データ v1)と試算 v2.8 に並べる(2026-10-25)
import json, csv, sys, os, statistics as st, collections as C
B = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def run(cs, day, store, r, s):
    d = f"{B}/{day}_stageB_{cs}_{store}_r{r}_s{s}"
    j = json.load(open(d + "/summary.json")); m = j["site_metrics"]
    gu = list(csv.DictReader(open(d + "/give_ups.csv")))
    a = C.Counter(g["action"] for g in gu)
    return dict(m, freed=a["卓を空けた"], gave=a["あきらめた"], moved=a["移した"], site=j["site_result"],
                clear_max=(j.get("job_wait_max_s") or {}).get("片付け"), over=(j.get("job_wait_over_count") or {}).get("片付け", 0),
                stopped=json.load(open(d + "/run_info.json")).get("stopped_early"))
K = [("entry_wait_mean", "入店待ち"), ("staff_wait_mean", "席空待ち"), ("full_wait_mean", "満席"), ("turn_mean", "卓が使えるまで"),
     ("pay_wait_mean", "会計待ち"), ("dish_wait_mean", "料理の待ち"), ("robot_share", "ロボの割合"), ("staff_util", "手")]
for store in ("fam_p1", "fam_p5"):
    for r in (0, 1, 2):
        new = [run("cs144", "20261025", store, r, s) for s in (0, 1, 2)]
        old = [run("cs143", "20261024", store, r, s) for s in (0, 1, 2)]
        site = new[0]["site"]
        print(f"== {store} ロボット{r}台  組 {[x['groups'] for x in new]}  途中で止めた {[x['stopped'] for x in new]}")
        for k, lab in K:
            nv = [x.get(k) for x in new]; ov = [x.get(k) for x in old]
            f = lambda v: "—" if v is None else f"{v:.2f}"
            print(f"  {lab:6s} 種0・1・2 {' '.join(f(v) for v in nv)} |種1 cs143 {f(ov[1])} → cs144 {f(nv[1])} |試算 {f(site.get(k))}")
        print(f"  卓を空けた {[x['freed'] for x in old]}→{[x['freed'] for x in new]}  あきらめた {[x['gave'] for x in new]}  移した {[x['moved'] for x in new]}  片付けの板待ち最大 {[round(x['clear_max'] or 0) for x in new]}秒 600超 {[x['over'] for x in new]}")

c1_compare_output.txt(条件ごとの比べ)

01_c1_compare_output.txt4,349字・60行・text
== fam_p1 ロボット0台  組 [64, 83, 102]  途中で止めた [False, False, False]
  入店待ち   種0・1・2 1.56 3.90 13.13 |種1 cs143 1.74 → cs144 3.90 |試算 3.30
  席空待ち   種0・1・2 0.37 2.69 8.07 |種1 cs143 0.55 → cs144 2.69 |試算 0.95
  満席     種0・1・2 0.00 0.00 3.84 |種1 cs143 0.00 → cs144 0.00 |試算 2.17
  卓が使えるまで 種0・1・2 3.37 11.16 11.20 |種1 cs143 4.95 → cs144 11.16 |試算 2.49
  会計待ち   種0・1・2 2.24 4.63 5.46 |種1 cs143 — → cs144 4.63 |試算 12.28
  料理の待ち  種0・1・2 0.84 0.96 0.94 |種1 cs143 0.94 → cs144 0.96 |試算 0.46
  ロボの割合  種0・1・2 0.00 0.00 0.00 |種1 cs143 0.00 → cs144 0.00 |試算 0.00
別のタブで開く保存

③はロボットで入店待ちが縮む向きがそろい、⑦は外れた

種1(83組)の入店待ち(ロボット0台 → 1台 → 2台)を並べます。

Unity 直す前(cs:143)
Unity 直したあと(cs:144)
試算 v2.8
③
1.74 → 1.67 → 2.01分
3.90 → 3.30 → 2.03分
3.30 → 2.60 → 1.18分
⑦
1.87 → 1.75 → 2.04分
6.16 → 8.22 → 4.06分
2.76 → 1.81 → 1.97分

卓を空けた数は、どの条件も0〜4になりました(③0台の種0・1・2は 0・24・37 → 0・0・0)。③では、前回いちばん大きかったずれ(ロボットの台数で入店待ちが変わらない)が解けました。⑦は試算から大きく外れ、客が帰ってから卓が使えるまで16分かかっています。

ピーク後半の店の様子:試算はレジに列、Unity は片付けがたまる

来客が始まって60〜120分(ピークの後半)の平均を、Unity の timeline.csv から出しました。試算にも同じ見方の記録を付けて回し(40回の平均)、並べています。

c2_timeline_peak.py

# 基準データ v2 の timeline.csv から、ピークの後半(来客が始まって60〜120分)の平均を条件ごとに出す(2026-10-25)
import csv, sys, os, statistics as st
B = sys.argv[1]
cols = ["waiting_groups", "seated_groups", "pay_waiting_seat", "pay_in_line", "tables_free", "tables_dirty_untaken", "tables_clearing",
        "board_guide", "board_serve", "board_call", "board_pay", "board_clear",
        "staff_idle", "staff_guide", "staff_serve", "staff_call", "staff_pay", "staff_clear", "staff_rescue", "robots_busy"]
print("店 台 種 | " + " ".join(cols))
for store in ("fam_p1", "fam_p5"):
    for r in (0, 1, 2):
        for s in (0, 1, 2):
            T = list(csv.DictReader(open(f"{B}/20261025_stageB_cs144_{store}_r{r}_s{s}/timeline.csv")))
            t0 = float(T[0]["t_s"])
            P = [x for x in T if 3600 <= float(x["t_s"]) - t0 < 7200]
            print(store, r, s, "|", " ".join(f"{st.mean(float(x[c]) for x in P):.1f}" for c in cols))

c2_timeline_peak_output.txt(Unity・ピーク後半の平均)

店 台 種 | waiting_groups seated_groups pay_waiting_seat pay_in_line tables_free tables_dirty_untaken tables_clearing board_guide board_serve board_call board_pay board_clear staff_idle staff_guide staff_serve staff_call staff_pay staff_clear staff_rescue robots_busy
fam_p1 0 0 | 0.0 28.2 0.4 2.2 11.5 1.0 1.4 0.3 0.9 0.1 1.4 1.0 0.0 0.6 0.8 0.3 0.9 1.4 0.0 0.0
fam_p1 0 1 | 2.4 33.9 2.9 3.0 0.5 6.6 1.1 0.5 1.2 0.3 1.9 6.6 0.0 0.8 0.9 0.2 1.0 1.1 0.0 0.0
fam_p1 0 2 | 12.5 34.3 4.5 2.8 0.4 6.1 1.2 0.2 1.1 0.2 1.9 6.1 0.0 0.7 0.9 0.2 1.0 1.2 0.0 0.0
fam_p1 1 0 | 0.0 28.9 0.4 2.0 10.6 1.2 1.3 0.2 0.3 0.2 1.1 1.2 0.3 0.6 0.4 0.3 0.9 1.3 0.1 1.0
fam_p1 1 1 | 2.0 36.2 3.7 2.8 1.4 3.1 1.3 0.3 0.4 0.1 1.8 3.1 0.0 0.9 0.4 0.3 1.0 1.3 0.0 0.9
fam_p1 1 2 | 12.0 36.4 4.6 2.9 0.4 3.8 1.5 0.2 0.6 0.3 1.9 3.8 0.1 0.7 0.4 0.3 1.0 1.5 0.0 1.0
fam_p1 2 0 | 0.0 29.0 1.0 2.2 11.1 0.2 1.7 0.1 0.0 0.1 1.3 0.2 0.4 0.7 0.0 0.3 0.9 1.7 0.0 1.7
fam_p1 2 1 | 0.5 38.9 3.7 3.0 0.7 0.7 1.7 0.1 0.0 0.2 2.0 0.7 0.1 0.9 0.0 0.3 1.0 1.7 0.0 1.6
fam_p1 2 2 | 16.0 34.2 7.1 3.0 0.2 5.9 1.7 0.1 0.2 0.4 1.9 5.9 0.1 0.5 0.2 0.4 1.0 1.8 0.1 1.6
fam_p5 0 0 | 0.0 28.2 0.2 2.1 8.3 4.3 1.2 0.2 1.1 0.3 1.2 4.3 0.0 0.7 0.9 0.3 0.9 1.2 0.0 0.0
fam_p5 0 1 | 3.7 32.5 2.6 2.9 0.4 7.8 1.2 0.4 1.0 0.3 1.9 7.8 0.0 0.7 0.9 0.2 1.0 1.2 0.0 0.0
fam_p5 0 2 | 16.0 31.3 4.5 2.9 0.3 9.1 1.3 0.5 1.3 0.5 1.9 9.1 0.0 0.5 0.8 0.4 1.0 1.3 0.0 0.0
fam_p5 1 0 | 0.0 29.2 0.5 2.2 8.2 3.0 1.6 0.3 0.1 0.2 1.4 3.0 0.2 0.7 0.2 0.4 0.9 1.6 0.0 1.0
fam_p5 1 1 | 5.2 31.6 2.3 2.8 0.6 8.2 1.5 0.2 0.8 0.3 1.8 8.2 0.0 0.7 0.3 0.4 1.0 1.6 0.0 0.9
fam_p5 1 2 | 14.5 33.5 4.0 2.9 0.3 6.6 1.6 0.3 0.6 0.3 1.9 6.6 0.0 0.6 0.4 0.3 1.0 1.7 0.0 1.0
fam_p5 2 0 | 0.0 30.2 0.5 2.2 4.9 5.7 1.2 0.6 1.1 0.5 1.4 5.7 0.0 0.7 0.7 0.3 0.8 1.3 0.2 1.8
fam_p5 2 1 | 1.8 34.9 2.7 3.0 1.6 4.1 1.4 0.3 0.4 0.4 2.0 4.1 0.0 0.6 0.5 0.3 1.0 1.4 0.1 1.6
fam_p5 2 2 | 13.8 34.0 4.8 2.8 0.4 5.8 1.8 0.1 0.1 0.3 1.8 5.8 0.0 0.7 0.1 0.3 1.0 1.8 0.1 1.6

timeline_site.py(試算 v2.8 に同じ見方の記録を付ける)

# 試算 v2.8 にも Unity の timeline.csv と同じ見方の記録を付けて、ピークの後半(60〜120分)の平均を出す(2026-10-25)
# 数えるもの:入口で待つ組・卓にいる組・片付け待ちの卓(客が帰ってから片付けが終わるまで)・スタッフの手の空き・
# 板(スタッフの順番待ち)に残った仕事を優先度ごとに(1=案内・配膳・呼び出し・会計、2=片付け)・レジの列
import sys, statistics as st, simpy, random
import haizen_sim as h

def run(params, seed):
    store = h.STORES[params["store"]]; p = {**h.COMMON, **store, **params}
    env = simpy.Environment(); s = h.Store(env, random.Random(seed), p)
    env.process(s.arrivals()); rows = []
    def sampler():
        while True:
            q = s.staff.queue
            rows.append(dict(t=env.now, waiting=len(s.tables.queue), seated=s.tables.count - s.dirty, dirty=s.dirty,
                             staff_busy=s.staff.count, board_p1=sum(1 for r in q if r.priority == 1), board_p2=sum(1 for r in q if r.priority == 2),
                             reg_queue=len(s.register.queue), reg_busy=s.register.count))
            yield env.timeout(60)
    env.process(sampler()); env.run(until=10 * 3600)
    return [r for r in rows if 3600 <= r["t"] < 7200]

for store, r_, tr in (("fam_p1", 0, 1), ("fam_p1", 1, 3), ("fam_p1", 2, 3), ("fam_p5", 0, 1), ("fam_p5", 1, 3), ("fam_p5", 2, 3)):
    c = {"store": store, "staff": 4, "robots": r_, "arrivals_per_h": 42, "mode": "serve", "trays": tr, "fragile_by": "any", "registers": 1}
    R = [x for sd in range(40) for x in run(c, sd)]
    print(store, r_, " ".join(f"{k} {st.mean(x[k] for x in R):.1f}" for k in R[0] if k != "t"))

timeline_site_output.txt(試算・ピーク後半の平均。スタッフ4人・普通・レジ1つ)

fam_p1 0 waiting 3.4 seated 38.1 dirty 1.6 staff_busy 3.8 board_p1 1.3 board_p2 0.5 reg_queue 6.8 reg_busy 1.0
fam_p1 1 waiting 2.8 seated 36.9 dirty 1.3 staff_busy 3.4 board_p1 0.4 board_p2 0.1 reg_queue 4.3 reg_busy 0.9
fam_p1 2 waiting 1.2 seated 37.1 dirty 1.3 staff_busy 3.2 board_p1 0.2 board_p2 0.0 reg_queue 5.0 reg_busy 1.0
fam_p5 0 waiting 2.6 seated 37.4 dirty 1.6 staff_busy 3.8 board_p1 1.2 board_p2 0.5 reg_queue 6.7 reg_busy 1.0
fam_p5 1 waiting 1.9 seated 36.6 dirty 1.3 staff_busy 3.4 board_p1 0.4 board_p2 0.1 reg_queue 4.8 reg_busy 1.0
fam_p5 2 waiting 2.1 seated 36.4 dirty 1.3 staff_busy 3.2 board_p1 0.2 board_p2 0.0 reg_queue 4.9 reg_busy 0.9

③0台・種1で比べると、こうなります。

ピーク後半の平均Unity試算 v2.8
ふさがっているスタッフ(4人中)
4.0人3.8人
片付けが済んでいない卓
7.7卓(誰も取っていない6.6・片付け中1.1)
1.6卓
板に残った片付けの仕事6.6件0.5件
会計を待つ・レジに並ぶ組5.8組
7.8組(列6.8・会計中1.0)

試算ではスタッフに少し空きがあり、片付けは追いつき、レジに列ができます。Unity ではスタッフ4人がずっとふさがり、いちばん後回しの片付けがたまります。卓が空かないので客が席に着く流れが遅れ、レジの列はかえって短くなります。

仕事1件あたりのスタッフの時間が、Unity の方が7〜18%長い

1組あたりにスタッフが使った時間を、仕事ごとに並べました(種1・ロボット0台)。

fam_p1 0 組 83 皿 206 | 1件あたり(秒) 案内 73 配膳 37 会計 93 片付け 134 呼び出し(1組あたり) 24 | 合計/組 415
   スタッフの時間 {'待機': 169, '案内': 101, '料理': 126, '呼び出し': 34, '会計': 129, '片付け': 185}
fam_p5 0 組 83 皿 206 | 1件あたり(秒) 案内 77 配膳 41 会計 93 片付け 161 呼び出し(1組あたり) 25 | 合計/組 459
   スタッフの時間 {'待機': 156, '案内': 107, '料理': 142, '呼び出し': 35, '会計': 128, '片付け': 223}
d平均 10.65
案内 66.29285714285714 配膳1皿 31.292857142857144 片付け 122.58571428571429 会計 90
1件あたり(秒)試算 v2.8(③)Unity ③Unity ⑦
案内667377
配膳(1皿)313741
会計909393
片付け123134161
1組あたりの合計約388415(+7%)459(+18%)

試算は、案内も片付けも「受け渡し口から卓まで」の距離で歩く計算です。Unity は実際に、入口から卓へ、卓から洗い場へ、人をよけながら歩いています。⑦は通路が狭いので、その差が大きくなります。試算はピークでスタッフの手が95%ふさがっているので、1割に満たない差でも人手が足りなくなり、いちばん後回しの片付けがたまります。 店の回り方が、ほんの少しの歩く距離で別の形に切り替わる、ぎりぎりの所にいるということです。

決めたこと:歩く距離の差は、今は試算に入れない

問い決めたこと
スタッフが歩く距離の差をどう扱うか
今は試算に入れず、ずれとして記録だけにする。この18回を基準データ v2 として区切る

直すときの候補は2つ残しています。案内は入口から卓、片付けは卓から洗い場の距離を平面図から取る形と、人をよける分の遅れに Unity の実測を入れる形です。夜のあいだも条件を回し続ける仕組みは別に作っていて、その答え合わせは基準データ v2(KIOXIA の service_sim/unity/stageB_cs144/)と比べます。

データとコードの置き場所

もの場所
試算 v2.8 のコード・全32,850条件の結果・ページを作るスクリプト
iCloud の サービスロボット導入シミュレーション/simulation/v2_8/(README.md が入口)
試算のページ・Unity が読む条件
p2m-site の public/sim/serving-robot.html・serving-robot-params.json(commit f26f27c)
Unity の18回(基準データ v2)
KIOXIA の r2r2r-data/service_sim/unity/20261025_stageB_cs144_*(各回に timeline.csv)
18回の集計
KIOXIA の r2r2r-data/service_sim/unity/stageB_cs144/(c1・c2 と README.md)
18回(cs:143)を読み直した集計
KIOXIA の r2r2r-data/service_sim/unity/stageB_cs143/b5_register_and_valve.py
v10 の確かめ B
KIOXIA の r2r2r-data/service_sim/unity/20261025_p1_efficient_corner_fam_p1_s4_r0_a42_s1_1013/
段階Bの台帳
KIOXIA の r2r2r-data/service_sim/unity/unity_ledger_v1.csv
Claude Code への依頼文と報告
iCloud の サービスロボット導入シミュレーション/unity/ と unity/claude_code_reports/
ずれの一覧
iCloud の サービスロボット導入シミュレーション/unity/unity_stageC_gaps_v1.md

Claude Code への依頼文

依頼 v10:claude_code_request_v10_valve_v28.md

# 依頼 v10:逃がし弁「卓を空けた」の数え方を直し、試算 v2.8 を読む(2026-10-25)

段階Bの18回(cs:143)を調べると、入店待ちがロボットの台数で変わらない原因が2つあった(unity_stageC_gaps_v1.md の「段階Cの2回目」)。

1. レジ:Unity はレジの内側に店員1人で、会計は1組ずつ。試算はだれでも同時に会計できた → **試算の方を直した**(v2.8。会計はレジ1つにつき1組ずつ。公開済み)。Unity のレジはそのまま
2. 逃がし弁:会計・片付けで600秒たった卓を、片付けずに空けている。だれもまだ仕事を取っていない「ふつうの片付け待ち」も数えているので、③0台・種1では83組のうち24卓が空けられ、スタッフの仕事が約1割消えていた → **Unity を直す**(エイイチと決めた:スタッフが仕事を取ってから止まった時間だけ数える)

## 変えないもの

- レジの内側に1人・会計は1組ずつ(payAtRegisterByOne)。会計90秒・片付けの時間など ServiceSpec の値(サイトから読む)
- 600秒(tableStuckFreeSec)と、卓を空けるときの流れ(ForceFreeTable)
- 仕事の順番・料理はロボット優先・ロボットの動き(v9)・人のよけ方
- 店の形(版 8a5b2f6a016a のまま)

## 0. 試算 v2.8 の条件を写す

ネットから取らず、Mac のここから Experiment/serving-robot-params.json に写す。

```
~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/site_copy_v28/serving-robot-params.json
```

- model は v2.8、layout_generation は 8a5b2f6a016a(店の形は変わっていない)
- unity_conditions の6条件に `"registers": 1` が足してある。site_result に `pay_wait_mean`(食べ終えてから会計が始まるまで・分)が足してある
- 読むときに model が v2.8 で、6条件の registers が 1 であることを確かめる。違えば回さない

## 1. 逃がし弁の数え方(GroupManager)

いまは Paying・Clearing の間ずっと `g.stuckS += dt` している。これを次に変える。

- 数えるのは、その組の仕事(g.job)を**スタッフが取っている間だけ**(`g.job != null && g.job.taken`)
- 板に載ったまま誰も取っていない間(ふつうの片付け待ち)と、会計の並ぶ点が空くのを席で待っている間(g.job がまだない)は数えない
- スタッフがあきらめて仕事を板に戻したとき(AbandonJob)は、それまでに数えた分を0に戻さない(何度も着けない卓を見つけるため)
- 600秒に達したら今までどおり ForceFreeTable。give_ups.csv の waited_s は、この数え方の秒数にする

## 2. 試算と同じ数字を出す(TripLogger の site_metrics)

- `pay_wait_mean` を足す:組ごとに(会計が終わった時刻 − 食べ終えた時刻 − pay_s)÷60 の平均。groups.csv の pay_s 列(会計が終わった時刻)と eat_end_s を使う。卓を空けられた組は入れない。Unity はレジまで歩く時間が入るので、試算より少し長く出てよい
- summary.json に足す:片付けの仕事が板に載ってから取られるまでの最大(秒)と、そのうち600秒を超えた数。会計も同じ。直す前は「卓を空けた」で消えていた長い待ちが、ここに出るようになるはず

## 3. 確かめ

**A. Claude Code が見る**
- コンパイルが通る。直した所の前後を読み、数えない場面(板に載ったまま・並ぶ点待ち)で stuckS が増えないこと

**B. 回す(エイイチが押す)**
ServiceSim / Site Run:③ ロボット0台・種1 を1回。比べる相手は `20261024_stageB_cs143_fam_p1_r0_s1`(83組):
- 卓を空けた(直す前:会計7・片付け17)。0に近くなるはず
- 入店待ち・席は空いているのに待った・客が帰ってから卓が使えるまで・スタッフの手がふさがっている割合(直す前:1.74分・0.55分・4.95分・0.77)
- pay_wait_mean と、試算 v2.8 の同じ条件(入店待ち 3.30分・席は空いているのに待った 0.95分・回転 2.49分・会計待ち 12.3分)
- 2 の片付け・会計の待ちの最大と、600秒を超えた数
- 最後まで回り切る(全員が店を出る)。来た組は直す前と同じ83組

## 報告

- 直したファイルと、変えた所の説明
- 確かめ A・B の結果(数字つき)と、出力フォルダ名
- この依頼どおりにすると困る所があれば、直す前に書いて止める
- B が最後まで回り切ったら Check-in して番号を報告

段階Bの18回の回し直しは、私(Claude)が B の結果を見てから頼む。

依頼 v10b:claude_code_request_v10b_stageB_timeline.md

# 依頼 v10b:段階Bの台帳に会計待ちを足し、時間帯ごとの記録を出してから18回を回し直す(2026-10-25)

v10 の確かめ B(`20261025_p1_efficient_corner_fam_p1_s4_r0_a42_s1_1013`)で、逃がし弁は直った(卓を空けた 24 → 0)。入店待ちは試算 v2.8 に近づいた(3.91分・試算 3.30分)が、詰まる場所が違う。

- 試算 v2.8:レジで詰まる(会計待ち 12.3分・客が帰ってから卓が使えるまで 2.5分)
- Unity:片付けで詰まる(会計待ち 4.7分・卓が使えるまで 10.8分。片付けの仕事が取られるまで平均511秒)

仕事1件あたりの時間は試算とそろっているので、ピークのどの時間帯に、誰が何をしていて、何が板に残っていたかを、試算と並べて調べる。そのための記録を足し、逃がし弁を直した版で段階Bの18回を回し直す(基準データ v2)。

## 変えないもの

- v10 までの動き(逃がし弁の数え方・レジは1人で1組ずつ・ロボットの動き・人のよけ方・仕事の順番)。今回は記録を足すだけで、店の回し方は1つも変えない
- 段階Bの18回の中身(③⑦ × ロボット0・1・2台 × 種0・1・2)と順番、出力フォルダの名前の付け方
- 前の版の台帳の行(cs:140・cs:143 の36行)は消さない・書き換えない

## 1. 段階Bの台帳に列を足す(unity_ledger_v1.csv・ServiceSimStageB・ServiceSimLedger)

v10 の報告の 7 章 1 で相談のあった件。足すことに決めた。

- 足す列(いちばん後ろ、ledger_schema_version の前でなく後ろでもよいが、前の行と列の位置がずれないように):`pay_wait_mean`・`site_pay_wait_mean`・`blocked_wait_mean`・`site_blocked_wait_mean`・`turn_mean`・`site_turn_mean`・`clear_wait_mean_s`・`clear_wait_max_s`・`clear_wait_over_count`・`pay_job_wait_mean_s`
- ledger_schema_version を1つ上げる。前の行は新しい列が空のままでよい(書き換えない)
- 新しい列を読むのは私(Claude)の集計(KIOXIA の stageB_cs143/b1_collect.py の次の版)なので、Unity 側の集計は直さなくてよい

## 2. 時間帯ごとの記録(新しいファイル timeline.csv。1回の出力フォルダに1つ)

店の中の時間で60秒ごとに1行。来客が始まってから全員が店を出るまで。

| 列 | 中身 |
|---|---|
| t_s | 店の中の時刻(秒) |
| arrived | ここまでに来た組の数 |
| waiting_groups | 入口で待っている組 |
| seated_groups | 卓にいる組(食事中・会計待ちを含む) |
| pay_waiting_seat | 食べ終えて、レジの並ぶ点が空くのを席で待っている組 |
| pay_in_line | レジの並ぶ点に立っている組(会計中を含む) |
| tables_free | すぐ使える卓 |
| tables_dirty_untaken | 客が帰ったが、片付けの仕事をまだ誰も取っていない卓 |
| tables_clearing | 片付けの仕事をスタッフが取っている卓 |
| board_guide・board_serve・board_call・board_pay・board_clear | 板に載っていて誰も取っていない仕事の数(種類ごと) |
| staff_idle・staff_guide・staff_serve・staff_call・staff_pay・staff_clear・staff_rescue | いまその仕事をしているスタッフの人数(待機は idle) |
| robots_busy | 走っている・卓で待っているロボットの台数 |

- 数え方は、そのコマの状態をそのまま数える(平均にしない)
- 同じ記録を Site Run の1回でも出す(段階Bと同じ作り)

## 3. 確かめ

**A. Claude Code が見る**
- コンパイルが通る。timeline.csv の各行で、seated_groups + tables_free + tables_dirty_untaken + tables_clearing が卓の数(③42)と同じになること、スタッフの人数の合計が4になることを、v10 の B と同じ回の短い試し(Site Run・種1・店の中の時間30分で止めてよい)で確かめる

**B. 回す(エイイチが押す)**
1. Check-in して番号を報告(この番号が段階Bの unity_cs になる)
2. ServiceSim → Stage B → 押す前の確かめ(回さない)で、版が v2.8・店の形 8a5b2f6a016a・レジ1つで、18回とも「まだ」になっていること
3. ServiceSim → Stage B → Run All(18回)。夜にまとめて回してよい

## 報告

- 直したファイルと、変えた所の説明
- 確かめ A の結果(timeline.csv の最初の数行と、合計の確かめ)
- この依頼どおりにすると困る所があれば、直す前に書いて止める
- Check-in の番号

18回が終わったら、集計と試算との並べは私(Claude)がやる。

Claude Code の報告

10:04 v10 の報告(claude_code_reports/20261025_1004_v10_valve_v28.txt)

02_sec09.txt10,314字・281行・text
====================================================================
依頼 v10 の報告:逃がし弁「卓を空けた」の数え方を直し、試算 v2.8 を写した
2026-10-25 10:04
====================================================================

結論:0・1・2 章ぜんぶ入れました。組み立ては通っています(53 本・エラー 0)。
B(回す)はエイイチが押す番です。押す前に 1 つだけ決めてほしいことがあります
(7 章の「種1 で回すために .asset を 1 つ変えました」)。
別のタブで開く保存

10:48 v10b の報告(claude_code_reports/20261025_1048_v10b_stageB_timeline.txt)

03_sec09.txt10,966字・285行・text
====================================================================
依頼 v10b の報告:台帳に会計待ちを足し、時間帯ごとの記録 timeline.csv を足した
2026-10-25 10:48
====================================================================

結論:1・2 章を入れました。組み立ては通っています(54 本・エラー 0)。
Check-in は済みで、**番号は cs:144** です(これが段階Bの unity_cs になります)。
店の回し方は 1 つも変えていません。記録だけ足しました。
別のタブで開く保存

確かめと18回の出力

v10 の確かめ B の summary.json(20261025_p1_efficient_corner_fam_p1_s4_r0_a42_s1_1013)

04_summary.json5,098字・22行・json
{
  "run_name": "family_v0",
  "written_at": "2026-10-25 10:27:05",
  "note": "speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒と、スタッフを待った秒数(stuck_wait_s)、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。site_metrics はサイトの meta.metrics と同じ名前・同じ定義。入店待ちの分け方もサイトの試算と同じで、blocked_wait は片付け待ちの卓の数だけ待っている列の前から順に足した分、full_wait は seat_wait − blocked_wait、staff_wait は blocked_wait + guide_wait。pay_wait は(会計が終わった時刻 − 食べ終えた時刻 − pay_s)で、逃がし弁で卓を空けられた組は入れない。Unity は会計の人とスタッフがレジまで歩く時間が入るので、試算より少し長く出る。pauses と rescues は、サイト側が unity_measures の仮の値から出した数なので出どころが違う。",
  "sim_like": {"dish_wait_mean": 0.9597, "dish_wait_p90": 1.7137, "cold_share": 0.0049, "seat_wait_mean": 2.1373, "turn_mean": 10.7628, "robot_share": 0, "robot_dishes": 0, "staff_dishes": 206, "requeued_dishes": 0, "late_staff_dishes": 0, "late_staff_share": 0, "robot_first_wait_s": 120, "yields": 0, "yields_by_robot": 0, "yields_by_staff": 0, "yields_by_guest": 0, "yields_stepped_aside": 0, "yields_in_corner": 0, "yields_passed": 0, "yield_wait_s": 0, "yield_wait_mean_s": 0, "staff_yields": 0, "staff_yield_wait_s": 0, "staff_yield_mean_s": 0, "staff_yield_share_of_staff_time": 0, "staff_time_total_s": 44241.19, "corner_stops": 0, "corner_stop_s": 0, "corner_stop_count_share": 0, "corner_stop_time_share": 0, "corner_range": "x 0-0 / z 0-0", "groups": 83, "people_jams": 13, "robot_blocked_jams": 0, "robot_blocked_jam_share": 0, "chair_pulls": 175, "robot_contacts": 0, "robot_yield_waits": 0, "robot_yield_wait_s": 0, "robot_yield_detours": 0, "register_crowd_max": 10, "register_crowd_mean": 2.8425, "register_pay_count": 83, "register_pay_not_at_back": 0, "register_pay_off_spot": 0, "register_pay_offset_mean_m": 0, "register_pay_offset_max_m": 0, "register_back_intruders": 0, "register_back_intruder_s": 0, "drink_bar_queued": 9, "drink_bar_gave_up": 7, "drink_bar_wait_mean": 0.5519, "drink_bar_wait_p90": 0.8256, "gave_up": 7, "warped": 11, "table_freed": 0, "people_jam_total_s": 13.75, "walkers_mean": 9.5261, "measured_minutes": 184.2251},
  "site_metrics": {"entry_wait_mean": 3.9104, "full_wait_mean": 0, "staff_wait_mean": 2.7121, "blocked_wait_mean": 2.1373, "guide_wait_mean": 0.5747, "seat_wait_mean": 2.1373, "dish_wait_mean": 0.9597, "dish_wait_p90": 1.7137, "cold_share": 0.0049, "turn_mean": 10.7628, "pay_wait_mean": 4.7146, "pay_wait_groups": 83, "walk_km_per_staff": 3.2313, "staff_util": 0.7778, "robot_util": 0, "robot_share": 0, "rescues": 0, "pauses": 0, "groups": 83},
  "site_run": {"site_run": true, "condition_id": "fam_p1_s4_r0_a42", "arrivals_minutes": 120, "total_minutes": 184.2251, "everyone_left": true, "aisle_deferrals": 0, "staff_first_yield": true, "robot_stuck_detours": 0, "rescues_done": 0, "rescue_wait_s": 0, "gave_up": 7, "warped": 11, "table_freed": 0},
  "site_result": {"dish_wait_mean": 0.459, "dish_wait_p90": 0.9759, "cold_share": 0, "seat_wait_mean": 2.8998, "turn_mean": 2.4872, "walk_km_per_staff": 2.8126, "staff_util": 0.7219, "robot_util": 0, "robot_share": 0, "robot_bus_share": 0, "collects": 0, "rescues": 0, "pauses": 0, "fragile_robot": 0, "groups": 85.475, "entry_wait_mean": 3.301, "blocked_wait_mean": 0.7327, "guide_wait_mean": 0.222, "staff_wait_mean": 0.9547, "full_wait_mean": 2.1671, "pay_wait_mean": 12.2785},
別のタブで開く保存

18回のうち③0台・種1の summary.json(20261025_stageB_cs144_fam_p1_r0_s1)

05_summary.json5,411字・23行・json
{
  "run_name": "family_v0",
  "written_at": "2026-10-25 12:14:34",
  "note": "speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒と、スタッフを待った秒数(stuck_wait_s)、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。site_metrics はサイトの meta.metrics と同じ名前・同じ定義。入店待ちの分け方もサイトの試算と同じで、blocked_wait は片付け待ちの卓の数だけ待っている列の前から順に足した分、full_wait は seat_wait − blocked_wait、staff_wait は blocked_wait + guide_wait。pay_wait は(会計が終わった時刻 − 食べ終えた時刻 − pay_s)で、逃がし弁で卓を空けられた組は入れない。Unity は会計の人とスタッフがレジまで歩く時間が入るので、試算より少し長く出る。pauses と rescues は、サイト側が unity_measures の仮の値から出した数なので出どころが違う。",
  "sim_like": {"dish_wait_mean": 0.9613, "dish_wait_p90": 1.6996, "cold_share": 0.0049, "seat_wait_mean": 2.237, "turn_mean": 11.1557, "robot_share": 0, "robot_dishes": 0, "staff_dishes": 206, "requeued_dishes": 0, "late_staff_dishes": 0, "late_staff_share": 0, "robot_first_wait_s": 120, "yields": 0, "yields_by_robot": 0, "yields_by_staff": 0, "yields_by_guest": 0, "yields_stepped_aside": 0, "yields_in_corner": 0, "yields_passed": 0, "yield_wait_s": 0, "yield_wait_mean_s": 0, "staff_yields": 0, "staff_yield_wait_s": 0, "staff_yield_mean_s": 0, "staff_yield_share_of_staff_time": 0, "staff_time_total_s": 44625.41, "corner_stops": 0, "corner_stop_s": 0, "corner_stop_count_share": 0, "corner_stop_time_share": 0, "corner_range": "x 0-0 / z 0-0", "groups": 83, "people_jams": 37, "robot_blocked_jams": 0, "robot_blocked_jam_share": 0, "chair_pulls": 168, "robot_contacts": 0, "robot_yield_waits": 0, "robot_yield_wait_s": 0, "robot_yield_detours": 0, "register_crowd_max": 9, "register_crowd_mean": 2.7037, "register_pay_count": 83, "register_pay_not_at_back": 0, "register_pay_off_spot": 0, "register_pay_offset_mean_m": 0, "register_pay_offset_max_m": 0, "register_back_intruders": 0, "register_back_intruder_s": 0, "drink_bar_queued": 12, "drink_bar_gave_up": 7, "drink_bar_wait_mean": 0.5545, "drink_bar_wait_p90": 0.9257, "gave_up": 7, "warped": 10, "table_freed": 0, "people_jam_total_s": 38.7, "walkers_mean": 9.4199, "measured_minutes": 185.8255},
  "site_metrics": {"entry_wait_mean": 3.9048, "full_wait_mean": 0, "staff_wait_mean": 2.6872, "blocked_wait_mean": 2.237, "guide_wait_mean": 0.4502, "seat_wait_mean": 2.237, "dish_wait_mean": 0.9613, "dish_wait_p90": 1.6996, "cold_share": 0.0049, "turn_mean": 11.1557, "pay_wait_mean": 4.6305, "pay_wait_groups": 83, "walk_km_per_staff": 3.2327, "staff_util": 0.7722, "robot_util": 0, "robot_share": 0, "rescues": 0, "pauses": 0, "groups": 83},
  "site_run": {"site_run": true, "condition_id": "fam_p1_s4_r0_a42", "arrivals_minutes": 120, "total_minutes": 185.8255, "everyone_left": true, "aisle_deferrals": 0, "staff_first_yield": true, "robot_stuck_detours": 0, "rescues_done": 0, "rescue_wait_s": 0, "gave_up": 7, "warped": 10, "table_freed": 0},
  "timeline_check": {"rows": 186, "step_s": 60, "tables": 42, "staff": 4, "bad_table_rows": 0, "bad_staff_rows": 0, "first_bad": "", "note": "timeline.csv の各行で、seated_groups + tables_free + tables_dirty_untaken + tables_clearing が tables と同じか、staff_* の合計が staff と同じかを数えたもの。合わない行があれば bad_* が 0 より大きくなり、Console にも出る"},
別のタブで開く保存

18回のうち③0台・種1の timeline.csv

06_timeline.csv13,020字・187行・csv
condition_id,seed,t_s,arrived,waiting_groups,seated_groups,pay_waiting_seat,pay_in_line,tables_free,tables_dirty_untaken,tables_clearing,board_guide,board_serve,board_call,board_pay,board_clear,staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue,robots_busy
fam_p1_s4_r0_a42,1,0,0,0,0,0,0,42,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,60,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,120,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,180,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,240,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,300,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r0_a42,1,360,2,0,2,0,0,40,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
別のタブで開く保存

18回のうち⑦0台・種1の timeline.csv

07_timeline.csv13,894字・199行・csv
condition_id,seed,t_s,arrived,waiting_groups,seated_groups,pay_waiting_seat,pay_in_line,tables_free,tables_dirty_untaken,tables_clearing,board_guide,board_serve,board_call,board_pay,board_clear,staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue,robots_busy
fam_p5_s4_r0_a42,1,0,0,0,0,0,0,42,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,60,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,120,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,180,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,240,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,300,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r0_a42,1,360,2,0,2,0,0,40,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
別のタブで開く保存

18回のうち③2台・種1の timeline.csv

08_timeline.csv13,466字・194行・csv
condition_id,seed,t_s,arrived,waiting_groups,seated_groups,pay_waiting_seat,pay_in_line,tables_free,tables_dirty_untaken,tables_clearing,board_guide,board_serve,board_call,board_pay,board_clear,staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue,robots_busy
fam_p1_s4_r2_a42,1,0,0,0,0,0,0,42,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,60,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,120,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,180,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,240,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,300,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p1_s4_r2_a42,1,360,2,0,2,0,0,40,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
別のタブで開く保存

18回のうち⑦2台・種1の timeline.csv

09_timeline.csv13,294字・191行・csv
condition_id,seed,t_s,arrived,waiting_groups,seated_groups,pay_waiting_seat,pay_in_line,tables_free,tables_dirty_untaken,tables_clearing,board_guide,board_serve,board_call,board_pay,board_clear,staff_idle,staff_guide,staff_serve,staff_call,staff_pay,staff_clear,staff_rescue,robots_busy
fam_p5_s4_r2_a42,1,0,0,0,0,0,0,42,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,60,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,120,1,0,1,0,0,41,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,180,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,240,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,300,1,0,1,0,0,41,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0
fam_p5_s4_r2_a42,1,360,2,0,2,0,0,40,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0
別のタブで開く保存

段階Bの台帳 unity_ledger_v1.csv(cs:140・143・144 の54行)

10_unity_ledger_v1.csv16,848字・55行・csv
timestamp,run_folder,status,unity_cs,layout_generation,params_model,store,staff,robots,arrivals_per_h,mode,trays,fragile_by,seed,staff_first_yield,yield_order_enabled,one_robot_per_aisle,robot_first_wait_s,everyone_left,total_minutes,wall_seconds,entry_wait_mean,staff_wait_mean,dish_wait_mean,cold_share,robot_share,staff_util,robot_util,walk_km_per_staff,site_entry_wait_mean,site_staff_wait_mean,site_dish_wait_mean,site_robot_share,robot_speed_mps,pause_rate,pause_s_mean,stuck_rate,trips,timeouts,stuck_count,stop_s_total,corner_stop_time_share,gave_up,warped,table_freed,rescues,late_staff_dishes,staff_yield_share_of_staff_time,ledger_schema_version,pay_wait_mean,site_pay_wait_mean,blocked_wait_mean,site_blocked_wait_mean,turn_mean,site_turn_mean,clear_wait_mean_s,clear_wait_max_s,clear_wait_over_count,pay_job_wait_mean_s
2026-10-24T00:03:37,20261023_stageB_fam_p1_r2_s0,completed,cs:140,82023936eb99,v2.7,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,179.5543,787.0753,2.892,1.6328,4.2197,0.5507,0.7053,0.8095,0.7073,2.5281,0.8336,0.559,1.0102,0.9914,0.5734,,5.1598,,99,7,55,5118.954,0.2212,100,59,35,55,61,0.0616,1
2026-10-24T00:21:58,20261023_stageB_fam_p5_r2_s0,completed,cs:140,82023936eb99,v2.7,fam_p5,4,2,42,serve,3,any,0,1,1,1,120,1,216.8588,968.6135,15.0673,3.9569,3.9381,0.5882,0.7463,0.7428,0.7209,3.0054,1.5915,1.0166,1.0497,0.9898,0.5776,,4.794,,116,9,61,5484.75,0.1488,106,107,45,61,69,0.0676,1
2026-10-24T00:33:37,20261023_stageB_fam_p1_r0_s0,completed,cs:140,82023936eb99,v2.7,fam_p1,4,0,42,serve,1,any,0,1,1,1,120,1,179.9344,694.309,1.9311,0.7289,0.9608,0,0,0.742,0,3.0918,3.1814,2.8545,0.653,0,0,,0,,0,0,0,0,0,10,11,22,0,0,0,1
2026-10-24T00:42:09,20261023_stageB_fam_p1_r0_s1,completed,cs:140,82023936eb99,v2.7,fam_p1,4,0,42,serve,1,any,1,1,1,1,120,1,171.637,507.5364,2.2816,1.0721,0.8046,0,0,0.758,0,3.1089,3.1814,2.8545,0.653,0,0,,0,,0,0,0,0,0,9,11,22,0,0,0,1
2026-10-24T00:51:32,20261023_stageB_fam_p1_r0_s2,completed,cs:140,82023936eb99,v2.7,fam_p1,4,0,42,serve,1,any,2,1,1,1,120,1,181.8777,558.8922,3.9575,2.1577,0.913,0,0,0.7676,0,3.2595,3.1814,2.8545,0.653,0,0,,0,,0,0,0,0,0,7,14,22,0,0,0,1
2026-10-24T01:01:20,20261023_stageB_fam_p1_r1_s0,completed,cs:140,82023936eb99,v2.7,fam_p1,4,1,42,serve,3,any,0,1,1,1,120,1,190.4947,583.2244,3.5292,1.6372,3.8528,0.6376,0.5688,0.7286,0.7901,2.7416,1.6401,1.189,2.0398,0.7252,0.5796,,6.1226,,57,1,20,2254.078,0.0157,58,43,23,20,94,0.0446,1
2026-10-24T01:10:23,20261023_stageB_fam_p1_r1_s1,completed,cs:140,82023936eb99,v2.7,fam_p1,4,1,42,serve,3,any,1,1,1,1,120,1,179.0147,538.8367,1.5759,0.3513,3.1393,0.5056,0.75,0.6575,0.7625,2.5104,1.6401,1.189,2.0398,0.7252,0.5802,,4.3264,,70,0,5,1117.942,0.2593,37,37,6,5,45,0.0375,1
別のタブで開く保存

stageB_cs144/README.md(基準データ v2 の入口)

# 基準データ v2(段階Bの18回・cs:144)

逃がし弁を直し(スタッフが仕事を取ってから止まった時間だけ数える)、試算 v2.8(会計はレジで1組ずつ)を読む版。2026-10-25 11:00ごろ〜15:00 に Stage B / Run All(18回)で回した。18回とも最後まで回り切り、台帳 unity_ledger_v1.csv の unity_cs = cs:144 の18行(台帳の版2・59列)。各回に timeline.csv(60秒ごと)がある。

- c1_compare.py → c1_compare_output.txt:条件ごとに 種0・1・2、種1 の cs:143 → cs:144、試算 v2.8 を並べる
- c2_timeline_peak.py → c2_timeline_peak_output.txt:timeline.csv のピーク後半(来客が始まって60〜120分)の平均
- 試算 v2.8 の同じ見方の数字は、Claude のクラウドで haizen_sim.py に記録を付けて出した(下の「分かったこと」に書いた)

## 分かったこと(2026-10-25)
- 卓を空けた:③0台 種0・1・2 で 0・24・37 → 0・0・0。どの条件も0〜4に減った
- ③は、ロボットを入れると入店待ちが短くなる向きが試算と同じになった(種1:3.90 → 3.30 → 2.03分。試算 3.30 → 2.60 → 1.18分)
- ⑦は試算から大きく外れる(種1:6.16 → 8.22 → 4.06分。試算 2.76 → 1.81 → 1.97分)。客が帰ってから卓が使えるまで16分
- 詰まる場所が違う。ピーク後半の平均(③0台・種1):Unity はスタッフ4人がすべてふさがり、片付けを誰も取っていない卓が6.6卓。試算はスタッフ3.8人がふさがり、片付けが済んでいない卓は1.6卓、レジの列が6.8組
- 原因の見立て:仕事1件あたりのスタッフの時間が Unity の方が長い(1組あたり ③415秒・⑦459秒。試算は388秒。片付け ③134・⑦161秒/試算123秒、配膳1皿 37・41秒/31秒、案内 73・77秒/66秒)。試算はピークでスタッフの手が95%ふさがっているので、1割弱長いだけで手が足りなくなり、いちばん後回しの片付けがたまる。⑦は通路が狭く、長さの差が大きい
- 試算は、案内・片付けの歩く距離にも「受け渡し口から卓まで」の距離を使っている。実際は入口から卓、卓から洗い場で、Unity はその道を人をよけながら歩く

## 決めたこと(2026-10-25)
- 歩く距離の差は試算に入れず、ずれとして記録だけにする。この18回を「基準データ v2」とし、夜に回す改良(自動の判定・画面なしで回す・ケースの型・次の条件を選ぶ仕組み)の答え合わせはここと比べる(基準データ v1 は stageB_cs143)

ずれの一覧 unity_stageC_gaps_v1.md

11_unity_stageC_gaps_v1.md7,647字・97行・markdown
# 段階C:サイトの試算と Unity のずれの一覧 v1(2026-10-24)

元のデータ:段階Bの18回(cs:140・店の形 82023936eb99・試算 v2.7)。③⑦ × ロボット0・1・2台 × 種0・1・2。
分析のコードと結果:KIOXIA `r2r2r-data/service_sim/unity/stageC_v1/`(a1〜a4.py・rows.json・carry.json・*_output.txt)。
理由の分け方は設計書 0.5 章:1 数え方の違い/2 サイトにない動き(すれ違い・混雑)/3 Unity の作りの問題。
対処を実際に入れるのは次の版。この一覧では決めるところまで。

## 1. 合っていたもの
別のタブで開く保存

試算 v2.8 のコード

simulation/v2_8/README.md

# 配膳ロボットの試算 v2.8(2026-10-25 回す・公開)

会計をレジで1組ずつにした版。レジの数(1つ・2つ)を条件に足した。大型店は2つ、ほかの店は1つを最初に見せる。

## なぜ直したか

Unity の段階Bの18回(cs:143)で、入店待ちがロボットの台数で変わらなかった。調べると、Unity はレジの内側に店員が1人だけで会計が1組ずつ(1時間に約38組が上限)、試算は会計をスタッフのだれでも同時にできた。実店舗に近いのはレジ1つなので、試算の方にレジを入れた(エイイチと決めた。会計の時間は実感どおり90秒のまま)。記録は KIOXIA の service_sim/unity/stageB_cs143/b5_register_and_valve.py と、unity/unity_stageC_gaps_v1.md の「段階Cの2回目」。

## 決めたこと(2026-10-25)

- 食べ終えた組は卓に座ったままレジの順番を待ち、順番が来たらスタッフの手が空くのを待って会計する(pay_s 90秒。スタッフの手はこれまでどおりふさがる)
- レジ1つ・2つを選べる。対象は全部の店
- 結果に pay_wait_mean(食べ終えてから会計が始まるまで)を足した。平均10分以上なら「レジが詰まる」の印を出す。「店が回らない」の決め方(席は空いているのに10分以上待つ)は変えていない。LP の「店舗が回る人数」もこの決め方のまま
- Unity が読む serving-robot-params.json の6条件はレジ1つ

## 確かめたこと

- レジを無限(registers=999)にすると v2.7 と同じ数字になる(乱数の使い方は変えていない)
- クラウドと Mac で分けて回し、重なった37,581条件の数字はすべて一致した(simpy 4.1.1 と 4.1.2)
- 全37,800画面を描いて、欠け・undefined・NaN がない(test_page_v28.js)
- v2.7 との違いは compare_v27_v28.txt。レジ1つでは「店が回らない」が 2,956 → 1,937条件に減り、「レジが詰まる」が5,263条件。③⑦の普通(42組/時)は会計待ちが12分前後

## ファイル

- haizen_sim.py:試算 v2.8(layouts/ は p2m-site の public/sim/layouts/ の写し、店の形 8a5b2f6a016a)
- results.json:全32,850条件(40回の平均)
- run_shard.py・run_timed.py・run_timed_rev.py:計算機を分けて回すための道具(Mac 側は1回の呼び出しが3分までなので区切って回した)
- build_page_v28.py:公開中のページに結果とレジの切り替えを入れる → serving-robot.v28.html(p2m-site の public/sim/serving-robot.html)
- export_params.py → serving-robot-params.json(p2m-site の public/sim/serving-robot-params.json)
- test_page_v28.js:全組み合わせを描いて確かめる
- p2m-site の commit f26f27c で公開。LP の find にもレジの数を足した(src/lib/servingSim.ts)

## 作り方

```
python3 haizen_sim.py                                    # 全条件(2コアで約5時間。分けるなら run_shard.py)
python3 build_page_v28.py <公開中のページ> results.json serving-robot.v28.html
python3 export_params.py results.json serving-robot-params.json
node test_page_v28.js serving-robot.v28.html
```

- 公開済みの記事の数字は前の版のまま

simulation/v2_8/haizen_sim.py

12_simulation_v2_8_haizen_sim.py.txt32,373字・730行・python
"""
サービスロボット導入シミュレーション #1 配膳ロボット(試作 v2.4)

v2.4 で直したこと(2026-10-22。動画の台本を codex で検証したときに見つかった2点。直し方はエイイチと決めた)
  1. 入店待ちの終わりを「案内されて卓に着いたとき」にした。v2.3 までは、案内したスタッフが説明(guide_s)を
     終えて受け渡し口まで戻るときまで数えていた。説明の時間と戻りの時間は、スタッフの手がふさがる時間には
     これまでどおり入れ、お客さんの流れ(注文を待つ)もこれまでどおりスタッフが戻ってから進める。
  2. 「下げ膳にも使う」(bus)で、ロボットがスタッフより先に卓へ行き、皿を載せる前に帰っていたのを直した。
別のタブで開く保存

simulation/v2_8/run_shard.py

"""v2.8 の全条件を、番号で分けて回す(2026-10-25。計算機を2台に分けて速くするため)。
使い方: python3 run_shard.py <k> <n> <出力.json> [使い回す結果.json ...]
- 条件の並び(haizen_sim.conditions())の番号を n で割った余りが k のものだけ回す
- 使い回す結果に同じ条件(同じ前提)があれば回さない。100条件ごとに出力へ途中経過を書く"""
import json, os, sys
from multiprocessing import Pool
import haizen_sim as h

k, n, out = int(sys.argv[1]), int(sys.argv[2]), sys.argv[3]
common = json.loads(json.dumps(h.COMMON))
done = {}
for path in sys.argv[4:] + [out]:
    try:
        old = json.load(open(path, encoding="utf-8"))
        if old.get("common") == common and old.get("reps") == h.REPS:
            done.update({h.key(r): r for r in old["rows"]})
    except FileNotFoundError:
        pass
conds = [c for i, c in enumerate(h.conditions()) if i % n == k]
todo = [c for c in conds if h.key(c) not in done]
print(f"shard {k}/{n}: {len(conds)} conditions, {len(todo)} to run", flush=True)

def save():
    rows = [done[h.key(c)] for c in conds if h.key(c) in done]
    with open(out + ".tmp", "w", encoding="utf-8") as f:
        json.dump({"common": h.COMMON, "reps": h.REPS, "shard": [k, n], "rows": rows}, f, ensure_ascii=False)
    os.replace(out + ".tmp", out)

with Pool() as pool:
    for i, r in enumerate(pool.imap_unordered(h.run_condition, todo, chunksize=2), 1):
        done[h.key(r)] = r
        if i % 100 == 0:
            save(); print(f"  {i}/{len(todo)}", flush=True)
save(); print("done", flush=True)

simulation/v2_8/run_timed.py

"""run_shard.py と同じ分け方で、決めた秒数だけ回して途中経過を書く(Mac 側は1回の呼び出しが3分までなので、区切って回す)。
使い方: python3 run_timed.py <k> <n> <出力.json> <秒> [使い回す結果.json ...]"""
import json, os, sys, time
from multiprocessing import Pool
import haizen_sim as h
k, n, out, limit = int(sys.argv[1]), int(sys.argv[2]), sys.argv[3], float(sys.argv[4])
t0 = time.time()
common = json.loads(json.dumps(h.COMMON))
done = {}
for path in sys.argv[5:] + [out]:
    try:
        old = json.load(open(path, encoding="utf-8"))
        if old.get("common") == common and old.get("reps") == h.REPS:
            done.update({h.key(r): r for r in old["rows"]})
    except FileNotFoundError:
        pass
conds = [c for i, c in enumerate(h.conditions()) if i % n == k]
todo = [c for c in conds if h.key(c) not in done]
def save():
    rows = [done[h.key(c)] for c in conds if h.key(c) in done]
    with open(out + ".tmp", "w", encoding="utf-8") as f:
        json.dump({"common": h.COMMON, "reps": h.REPS, "shard": [k, n], "rows": rows}, f, ensure_ascii=False)
    os.replace(out + ".tmp", out)
got = 0
pool = Pool()
for r in pool.imap_unordered(h.run_condition, todo, chunksize=1):
    done[h.key(r)] = r; got += 1
    if time.time() - t0 > limit:
        break
pool.terminate()
save()
print(f"shard {k}/{n}: +{got} / left {len(todo) - got}", flush=True)

simulation/v2_8/run_timed_rev.py

"""run_shard.py と同じ分け方で、決めた秒数だけ回して途中経過を書く(Mac 側は1回の呼び出しが3分までなので、区切って回す)。
使い方: python3 run_timed.py <k> <n> <出力.json> <秒> [使い回す結果.json ...]"""
import json, os, sys, time
from multiprocessing import Pool
import haizen_sim as h
k, n, out, limit = int(sys.argv[1]), int(sys.argv[2]), sys.argv[3], float(sys.argv[4])
t0 = time.time()
common = json.loads(json.dumps(h.COMMON))
done = {}
for path in sys.argv[5:] + [out]:
    try:
        old = json.load(open(path, encoding="utf-8"))
        if old.get("common") == common and old.get("reps") == h.REPS:
            done.update({h.key(r): r for r in old["rows"]})
    except FileNotFoundError:
        pass
conds = [c for i, c in enumerate(h.conditions()) if i % n == k]
todo = [c for c in conds if h.key(c) not in done][::-1]   # 後ろから回す(クラウドは前から回している)
def save():
    rows = [done[h.key(c)] for c in conds if h.key(c) in done]
    with open(out + ".tmp", "w", encoding="utf-8") as f:
        json.dump({"common": h.COMMON, "reps": h.REPS, "shard": [k, n], "rows": rows}, f, ensure_ascii=False)
    os.replace(out + ".tmp", out)
got = 0
pool = Pool()
for r in pool.imap_unordered(h.run_condition, todo, chunksize=1):
    done[h.key(r)] = r; got += 1
    if time.time() - t0 > limit:
        break
pool.terminate()
save()
print(f"shard {k}/{n}: +{got} / left {len(todo) - got}", flush=True)

simulation/v2_8/build_page_v28.py

"""試算 v2.8(レジの数を条件に足した版)の結果を、公開中の試算のページに入れる(2026-10-25)。
使い方: python3 build_page_v28.py <公開中のページ> <v2.8 の results.json> <出力するページ>
- const D:条件に registers(レジの数)を足し、結果に pay_wait_mean(食べ終えてから会計まで)を足す。店の情報・平面図は公開中のまま、レジの数だけ足す
- 画面:条件に「レジ」を足す。結果に「食べ終えてから会計まで」を足す。前提に会計とレジの決まりを足す。見出しの版を v2.8 にする
- 費用の欄・LP 用の色・使われ方の記録は公開中のまま"""
import json, re, sys
import haizen_sim as h
別のタブで開く保存

simulation/v2_8/export_params.py

"""試算(haizen_sim.py)の条件と店の回し方の値を、Unity が読む JSON に書き出す(2026-10-22。v2.8 で2026-10-25 にレジの数を足した)。

「何について正か」(unity_design_v0.md の 0 章)で、条件と店の回し方の決まり・時間・数字の定義はサイトが正。
Unity はこの JSON を読み、値を手で写さない(LOG359 のずれ9「値の手写し」をなくすため)。

使い方: python3 export_params.py results.json <出力先>   (出力先は p2m-site の public/sim/serving-robot-params.json)
- common:店に共通の値。role が "unity_measures" の値は Unity で測ってサイトに戻すための仮の値なので、Unity は読まない
- stores:Unity で回す店(fam_p1・fam_p5)の値。registers はレジの数(v2.8)
別のタブで開く保存

simulation/v2_8/test_page_v28.js

// 作り直したページを、簡易の DOM で全組み合わせ描画して、データ欠けや例外がないか確かめる
// v2.8:レジの数(1つ・2つ)も切り替える
// 使い方: node test_page_v28.js <ページ>
const fs = require("fs");
const h = fs.readFileSync(process.argv[2], "utf8");
const scripts = h.split("<script>").slice(1).map(x => x.split("</script>")[0]);
let js = scripts.find(x => x.includes("const D ="));
js = js.replace(/buildStores\(\);\s*buildLayouts\(\);\s*buildControls\(\);\s*render\(\);\s*$/, "");
js += `;let n=0, ng=0, notes=0;
for (const st of Object.keys(STORES)) { state.store = st; const s = STORES[st];
  for (const sf of s.staff_all) for (const rb of ROBOTS_ALL) for (const md of ["serve","bus","patrol"]) for (const c of [0,1,2]) for (const tr of [1,3]) for (const fb of ["any","staff"]) for (const rg of s.registers_all) {
    Object.assign(state, {staff: sf, robots: rb, mode: md, crowd: c, trays: tr, fb: fb, reg: rg, cbase: s.staff_all[1 + (n % (s.staff_all.length - 1))], crob: 1 + (n % 6)});
    buildStores(); buildLayouts(); buildControls(); render(); n++;
    if (/レジが詰まる/.test(document.getElementById("kpis").innerHTML)) globalThis.rj=(globalThis.rj||0)+1;
    if (/店が回らない/.test(document.getElementById("kpis").innerHTML)) ng++;
    if (!document.getElementById("robot-note").hidden) notes++;
    if (/undefined|NaN/.test(document.getElementById("kpis").innerHTML + document.getElementById("cond-sub").textContent + document.getElementById("reg-note").textContent)) throw new Error("undefined/NaN " + JSON.stringify(state));
  } }
console.log("rendered", n, "/ レジが詰まるが出た画面", globalThis.rj, "/ 回らないが出た画面", ng, "/ 5台以上の注意", notes);`;
const els = {};
const mk = () => ({ innerHTML: "", textContent: "", onchange: null, onclick: null, hidden: false, style: {}, className: "",
  after() {}, querySelector() { return null; }, setAttribute() {}, addEventListener() {}, appendChild() {}, classList: { add() {}, remove() {}, toggle() {} } });
global.document = { getElementById: id => els[id] || (els[id] = mk()), querySelectorAll: () => [], querySelector: () => null,
  createElement: () => mk(), addEventListener() {} };
new Function(js)();

site_series.py(図解に使った試算の分ごとの平均)

# 図解用:試算 v2.8 の分ごとの平均(40回)を、Unity の timeline.csv と同じ時刻で出す(2026-10-25)
import json, sys, random, statistics as st, simpy
sys.path.insert(0, "/home/claude/sim28/v2_8")
import haizen_sim as h
def series(c):
    acc = {}
    for sd in range(40):
        store = h.STORES[c["store"]]; p = {**h.COMMON, **store, **c}
        env = simpy.Environment(); s = h.Store(env, random.Random(sd), p); env.process(s.arrivals())
        def samp():
            while True:
                k = int(env.now // 60)
                a = acc.setdefault(k, {"dirty": [], "pay": [], "wait": [], "busy": []})
                a["dirty"].append(s.dirty); a["pay"].append(len(s.register.queue) + s.register.count)
                a["wait"].append(len(s.tables.queue)); a["busy"].append(s.staff.count)
                yield env.timeout(60)
        env.process(samp()); env.run(until=200 * 60)
    return {k: [round(st.mean(acc[t][k]), 2) for t in sorted(acc)][:190] for k in ("dirty", "pay", "wait", "busy")}
out = {}
for store in ("fam_p1", "fam_p5"):
    for r, tr in ((0, 1), (2, 3)):
        out[f"{store}_r{r}"] = series({"store": store, "staff": 4, "robots": r, "arrivals_per_h": 42, "mode": "serve", "trays": tr, "fragile_by": "any", "registers": 1})
json.dump(out, open("/home/claude/log379/data/site_series.json", "w"))
print({k: max(v["dirty"]) for k, v in out.items()})

p2m-site の src/lib/servingSim.ts の差分(commit f26f27c)

commit f26f27cd05e0f8de36798110b2ccb39d0a8f714e
Author: Claude <noreply@anthropic.com>
Date:   Sun Oct 25 09:33:26 2026 +0900

    配膳ロボットの試算 v2.8:会計はレジで1組ずつ。レジの数(1つ・2つ)を選べるようにした
    
    - 全32,850条件を回し直した(レジを無限にすると v2.7 と同じ数字になることを確かめた)
    - 大型店はレジ2つ、ほかの店は1つを最初に見せる
    - 結果に「食べ終えてから会計が始まるまで」と、10分以上なら「レジが詰まる」の印を足した(「店が回らない」の決め方は変えていない)
    - Unity が読む条件(serving-robot-params.json)も v2.8 で書き出した。Unity の6条件はレジ1つ
    - LP の数字も試算から読み直すため、find にレジの数を足した
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_019NnEBnbQXFrCEXBxuWxmqB

diff --git a/src/lib/servingSim.ts b/src/lib/servingSim.ts
index c13a7d6..8e59738 100644
--- a/src/lib/servingSim.ts
+++ b/src/lib/servingSim.ts
@@ -25,12 +25,12 @@ const D = readJson(html, at);
 const K: string[] = D.k;
 const ROWS: Row[] = D.r.map((r: any[]) => Object.fromEntries(K.map((k, i) => [k, r[i]])));
 export const STORES: Record<string, Store> = D.stores;
-const IDX = new Map(ROWS.map((r) => [[r.store, r.staff, r.robots, r.arrivals_per_h, r.mode, r.trays, r.fragile_by].join('|'), r]));
+const IDX = new Map(ROWS.map((r) => [[r.store, r.staff, r.robots, r.arrivals_per_h, r.mode, r.trays, r.fragile_by, r.registers].join('|'), r]));
 
-// 試算のページの既定(1往復で3卓まで・壊れやすい皿は誰が運んでもよい)
+// 試算のページの既定(1往復で3卓まで・壊れやすい皿は誰が運んでもよい・レジは店の最初の数。v2.8 からレジの数も条件)
 const TRAYS = 3, FB = 'any';
-export function find(store: string, staff: number, robots: number, arr: number, mode = 'serve'): Row | undefined {
-  const k = robots === 0 ? [store, staff, 0, arr, 'serve', 1, 'any'] : [store, staff, robots, arr, mode, TRAYS, FB];
+export function find(store: string, staff: number, robots: number, arr: number, mode = 'serve', reg: number = STORES[store].registers): Row | undefined {
+  const k = robots === 0 ? [store, staff, 0, arr, 'serve', 1, 'any', reg] : [store, staff, robots, arr, mode, TRAYS, FB, reg];
   return IDX.get(k.join('|'));
 }
 export const unstable = (r: Row) => r.ng === 1;

Unity のコード(cs:144)の全文

v10・v10b で直したファイルです。

Assets/ServiceSim/Service/GroupManager.cs

using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 客を「1組ごとに、入ってから出るまで」で動かす(試算の group() と同じ流れ)。
    ///
別のタブで開く保存

Assets/ServiceSim/Service/ServiceOps.cs

using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>料理1皿</summary>
    public class DishOrder
    {
別のタブで開く保存

Assets/ServiceSim/Experiment/SimParams.cs

using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// サイトの試算の値(serving-robot-params.json)を読む .asset。
    ///
別のタブで開く保存

Assets/ServiceSim/Experiment/SimParams.asset

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: edb06b3fbd714f289d7487e59b3a49b3, type: 3}
  m_Name: SimParams
  m_EditorClassIdentifier: 
  paramsJson: {fileID: 4900000, guid: 7c1a9b5e4d2f4a6ca1b3e8d0f5c27a94, type: 3}
  fetchedAt: 2026-10-25 JST(試算 v2.8・店の形 8a5b2f6a016a)
  sourceUrl: unity/site_copy_v28/serving-robot-params.json(Mac から写した。ネットからは取っていない)
  expectModel: v2.8
  registersSupported: 1

Assets/ServiceSim/Experiment/ExperimentPlan.asset

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 9d7ca72301ae84dbd8937d681f3df43d, type: 3}
  m_Name: ExperimentPlan
  m_EditorClassIdentifier: 
  runName: family_v0
  aisleWidthIndices: 000000000100000002000000
  crowdLevels: 000000000100000002000000
  robotCounts: 0100000002000000
  tripsPerCondition: 100
  seeds: 0000000001000000020000000300000004000000
  timeScale: 1
  kioxiaVolumePath: /Volumes/KIOXIA
  kioxiaOutputRoot: /Volumes/KIOXIA/r2r2r-data/service_sim/unity
  trialAisleWidthIndex: 1
  trialCrowd: 1
  trialRobots: 2
  trialSeed: 1
  trialTrips: 20
  trialTimeScaleFast: 10
  trialCaptureDeltaTime: 0.02
  trialMaxWallSeconds: 2400
  trialOutputRoot: /Users/shimazueiichi/ServiceSim_tmp
  measureMinutes: 120
  warmupMinutes: 15
  measureCaptureDeltaTime: 0.05
  referenceCaptureDeltaTime: 0.02
  disableRenderingWhileRunning: 0
  showLabelsWhileRunning: 0
  showKitchenMonitor: 1
  stage3OutputRoot: /Users/shimazueiichi/ServiceSim_tmp/stage3
  stage3Resume: 1
  screenshotDir: "/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7/ADAPT
    DESIGN/\u3058\u3076\u3093\u306E/PixelstoMotors/\u30C7\u30B8\u30BF\u30EB\u30C4\u30A4\u30F3\u5B66\u7FD2R2R2R/\u30B5\u30FC\u30D3\u30B9\u30ED\u30DC\u30C3\u30C8\u5C0E\u5165\u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3/unity/screens"
  screenshotWidth: 1920
  screenshotHeight: 1080

Assets/ServiceSim/Experiment/TimelineLogger.cs

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ServiceSim
{
別のタブで開く保存

Assets/ServiceSim/Experiment/ExperimentRunner.cs

using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 条件を回して記録する。1回の Play の中で、条件を順に通せる(段階3の Run All)。
別のタブで開く保存

Assets/ServiceSim/Experiment/TripLogger.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ServiceSim
別のタブで開く保存

Assets/ServiceSim/Editor/ServiceSimLedger.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ServiceSim.EditorTools
別のタブで開く保存

Assets/ServiceSim/Editor/ServiceSimStageB.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
別のタブで開く保存