サービスロボット導入シミュレーション Log#22 | ファミレス6店で、配膳ロボット2台は同じ所で詰まった

この記事の素材(コード・ログ・データ 12ファイル・まとめてダウンロードあり)

Unity の3Dで再現した6つのファミレスで配膳ロボットを2台にすると、どの店でも、ロボットの待機場所と料理の受け渡し口のまわりに止まりが集まりました(30秒以上の止まりのうち25〜57%)。店ごとに図面を直しても同じ所で詰まるので、直す先は1店ずつの配置ではなく、待機場所をどこに置くかといった「店の形を作る決まり」のほうだと分かりました。

配膳ロボットを入れると店がどう変わるかを、2つのやり方で調べています。1つはサイトの試算で、卓までの距離・料理の数・スタッフの人数から待ち時間を計算します。もう1つは Unity(ゲームを作る道具)で作った3Dのファミレスで、お客さん・スタッフ・ロボットを1人ずつ動かすシミュレーションです。どちらも、コードで作った同じ「店の形」(卓・壁・通路の配置のデータ)を読みます。実在の店ではなく、仮の店です。

今回で、用意した6つの店の形をすべて3Dの店に建て終えました。最後の店は「② 定番の郊外型」です。昔働いていたファミレスの見取り図を記憶から起こし、決まりに合わせて直した19卓・88席の小さな店で、角テーブル・丸テーブル・長テーブルがあります。ほかの5店はボックス席だけだったので、②の14卓には椅子と座る位置のデータがなく、3Dの店では客が座れませんでした。まずこれを作りました。

建てて1回ずつ回すと、ロボット2台の回で、丸テーブル2つの間(1.1m)にロボットが止まり、もう1台が抜けられずに60秒動けなくなるのが8回出ました。記憶の店はかなり昔のものなので、回れない所は変えてよいと決め、右の丸テーブルを1.0m右へ寄せて間を2.1mにしました。建て直した確かめでは8回が0回になり、そのまま30回の夜に進みました。

やったことなぜ結果
角テーブル・丸テーブル・長テーブルに椅子と座る位置を作った
3Dの店で、②の14卓に客が座れなかった
椅子58・座る位置88。卓の数・距離は6店とも変わらない
②を3Dの店に建て、1回ずつ回した
建てられるか、どこで止まるかを見るため
建った。2台で、丸テーブルの間の60秒の止まりが8回
右の丸テーブル(T06)を右へ1.0m寄せた
間が1.1mで、止まったロボットの横に0.24mしか残らない
横に1.06m残る。確かめで8回→0回
②の試算だけ計算し直した卓までの距離が5卓で変わった
2台の入店待ち 1.60→1.17分
②を種10個ずつ、計30回回した
1回ずつでは来る客の数がぶれるため
1台は試算どおり。2台は1台より待ちが延びた
6店の夜を並べた
どの店にも出る詰まりを見分けるため
待機場所と受け渡し口のまわりが25〜57%

②の30回の結果は次のとおりです(種10個の中央値。右は試算)。

ロボット0台1台2台
入店待ち(分)1.96 / 2.651.69 / 1.692.10 / 1.17
料理の待ち(分)0.89 / 0.611.04 / 0.941.04 / 0.39
会計待ち(分)1.13 / 1.411.06 / 1.301.34 / 1.12
60秒動けなくなった(1回あたり)
0012.5

1台は、入店待ちが試算とぴったり同じ1.69分でした。2台にすると、試算では1.17分に縮むのに、3Dの店では2.10分に延びました。2台の回の30秒以上の止まり192回の内訳は、入口の右の待合のまわり56回、待機場所の中54回、受け渡し口のまわり32回です。待合のまわりは、お客さんが多く来た種だけで出ていて、案内を待つお客さんがロボットの通り道に立っていました。

6店の夜を並べると、待機場所の中と受け渡し口から2.5m以内の止まりは、③38%・⑦31%・④25%・⑤30%・⑥57%・②45%でした。6店とも、待機場所を受け渡し口のすぐ横(受け渡し口の点から待機場所の端まで0.95〜1.7m)に置くという同じ決め方で作っているので、店ごとにドリンクバーやレジや卓を動かしても、この詰まりは消えません。 次は、待機場所・待合・スタッフが待つ点を、店ごとではなく店の形を作る決まりとして考え直します。

この記事の後半は作業ログです。②の椅子の作り方、3Dの店での1回ずつの確かめ、丸テーブルを動かす案の比べ、試算の計算し直し、30回の結果、6店の並べ直しまでを、コード・依頼文・報告・出力とあわせて載せます。

配膳ロボットを自分の店に入れたらどうなるかは、このシリーズの試算をもとにした別のページで試せます。

店の立体図(席・キッチン・配膳ロボットの通り道)導入前シミュレーション配膳ロボットを、導入前に自分の店舗でシミュレーション店舗の図面と人数から、何人で回るか・どこで止まるか・月いくらかかるかを出します

作業ログ ― ②の14卓には椅子がなく、Unity で客が座れなかった

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

ファイルの一覧(12本)

01_mires_v3.py_dffc100b269c.txt ― 6店の店の形を作り直した famires_v3.py の出力(版 dffc100b269c) / 確かめの仕組みを確かめた test_famires_v3.py の出力(版 dffc100b269c)・5,782字

02_sec02.md ― 依頼文:②を建てて確かめる(20261029_23)・4,051字

03_sec02.txt ― 報告:20261029_1732_p0_build_check.txt・12,819字

04_sec02.md ― 依頼箱の結果:20261029_23_店2を建てて確かめる_20261029_1704.md・4,547字

05_round_compare_img.py.txt ― 2つの並びを描いたスクリプト round_compare_img.py・4,016字

06_famires_p0.py.txt ― 店の形の作り方 famires_p0.py(直した版・全文)・5,081字

07_mires_v3.py_08db8a0d067c.txt ― 6店の店の形を作り直した famires_v3.py の出力(版 08db8a0d067c) / 確かめの仕組みを確かめた test_famires_v3.py の出力(版 08db8a0d067c)・5,782字

08_export_params.py.txt ― Unity が読む条件を書き出す export_params.py(②を足した版・全文)・4,893字

09_sec05.md ― 依頼文:直した②を建てて確かめ、夜を回す(20261029_24)・4,425字

10_sec05.txt ― 報告:20261029_1847_p0fix_build_check.txt・9,784字

11_sec05.txt ― 報告:20261105_night_p0.txt・9,447字

12_draw_night_stops.py.txt ― 止まりを図面に重ねたスクリプト draw_night_stops.py・3,505字

④⑤⑥と進めてきた「サイトの店の形を Unity に建てる」作業の最後が、②(定番の郊外型・記憶の店)です。建てる前に、サイトの店の形から Unity 用に書き出している JSON(public/sim/layouts/unity/p0_memory.json)を見ると、19卓のうち14卓の parts(天板・ベンチ・椅子・座る位置)が null でした。

Unity は、ゲームを作る道具で、3Dの店を動かしている

Unity は、米国の Unity Technologies が作っているゲームエンジン(ゲームを作るための道具)です。3Dの空間に床や壁や人を置き、物理の計算をしながら動かせます。このシリーズでは、ファミレスの店を3Dで建て、お客さん・スタッフ・配膳ロボットを1人ずつ動かして、待ち時間や詰まりを測っています。

この記事で使う言葉は、次のとおりです。

言葉意味
店の形
卓・壁・部屋・通路・ロボットが止まる位置のデータ。サイトのコード(tools/sim_layouts/)で作り、Unity はそれを読んで店を建てる。Unity で形を作らない
NavMesh
人やロボットが歩ける床の面。Unity が店の形から作り、その上で道を引く
種(乱数の種)
お客さんが来る時刻・人数などを決めるくじの番号。同じ種なら同じお客さんが来る。種0〜9の10個で回すと、混み方の違う10通りが見られる
夜
条件と種を並べて、画面を出さずにまとめて回すこと(ロボット0・1・2台 × 種10個で30回)
台帳
3Dの店の回ごとの記録を1行ずつ足していく CSV(unity_ledger_v1.csv)。追記だけで、前の行は書き換えない
30秒以上の止まり・動けなくなった
ロボットが30秒以上止まった回数と、そのうち60秒止まって作りの逃がし弁で動かされた回数

このプロジェクトでの使い方は、サイトの試算を正とし、試算と同じ店の形・同じ条件で3Dの店を回して、通路で起きること(止まる回数・立ち往生)を測ることです。数字は完全には合わない前提で、向きと大きさで比べます。

②の角テーブル・丸テーブル・長テーブルは、部品の作り方がまだなかった

②は記憶の店から起こしたので、ほかの5店(ボックス席・2人席・L字ソファの6人席だけ)にはない卓があります。

卓の種類卓部品
4人の角テーブル(t4)
T03・T04・T07・T08・T11・T12・T14〜T17・T19 の11卓
なし(null)
4人の丸テーブル(r4)T05・T06 の2卓なし(null)
6人の長テーブル(long6)
T13 の1卓なし(null)
L字ソファの6人席(booth6L)
T01・T02・T09・T10・T18 の5卓
あり

部品がないと、Unity は卓の場所に箱を置くだけで、客が座る位置がありません。サイトの店の形の README にも「まだないもの」として書いてありました。部品の作り方はサイトの決まり(unity_export.py)で、Unity で手で作らないことにしているので、サイトの側で作ります。

記憶の店の4人席は、椅子2脚ずつの向かい合わせだった

角テーブルの椅子の並べ方は、記憶の店の実物に合わせることにしました。選んだのは「2脚ずつ向かい合わせ」です。そのうえで、椅子を置く2辺は、ロボットが止まる辺を含まない組にしました。ロボットは椅子のない天板の端に横付けし、両側の人が手を伸ばして料理を取る形です。

例外が1つあります。下の壁ぎわの4卓(T14・T16・T17・T19)は、壁との間が0.4mしかなく、壁側に座れません。ここは座れる2辺(左右)に椅子を置き、ロボットが止まる辺の横に椅子が来ます。丸テーブルも同じ置き方にし、天板は直径0.8mの丸、6人の長テーブルは図のとおり上下にスツール3つずつ(引かない)にしました。

  • ねらい:②の19卓すべてに客が座れるようにし、Unity でほかの店と同じ規則で建てられるようにする
  • 変わったこと:部品のない卓 14 → 0。椅子とスツール 58、座る位置 88(席数と同じ)。卓の数・止まる位置・距離は6店とも変わらない(測った値)
  • 見込める効果:ほかの店と同じ形の記録で②を比べられる。角テーブルの店(ボックス席のない店)を次に作るときも、同じ規則で椅子を置ける
②の19卓に椅子を置いた図。橙の丸が角テーブル・丸テーブル・長テーブルの椅子とスツール、赤い点はロボットが止まる位置。下の壁ぎわの4卓は、止まる辺の横に椅子が来る

卓の部品を作る関数 unity_export.py の free_parts・long6_parts・table_parts(足した版)

FREE_CHAIR_R = 0.2       # ②の角テーブル・丸テーブルの椅子の半径
FREE_TOP_GAP = 0.25      # 椅子の側の外形の辺から天板の縁まで(椅子は天板の下に0.15m入る)
STOOL_R = 0.22           # ②の6人の長テーブルのスツールの半径(図と同じ)
OUT = {"left": (-1, 0), "right": (1, 0), "bottom": (0, -1), "top": (0, 1)}


def free_parts(t, side, spot_sides):
    """②の4人の角テーブル(t4)・丸テーブル(r4):椅子は2脚ずつ向かい合わせ(2026-10-29 エイイチの記憶)。
    椅子を置く2辺は、ロボットが止まる辺を含まない組(ロボットは天板の端に横付け)。
    ただしその組の片方に席に着く点がない(壁との間が狭くて座れない)ときだけ、座れる方の組にする(止まる辺の横に椅子が来る)"""
    x, y, w, h = t["x"], t["y"], t["w"], t["h"]
    cx, cy = x + w / 2, y + h / 2
    pref = ("top", "bottom") if side in ("left", "right") else ("left", "right")
    other = ("left", "right") if pref == ("top", "bottom") else ("top", "bottom")
    pair = pref if all(s in spot_sides for s in pref) or not all(s in spot_sides for s in other) else other
    cr, g = FREE_CHAIR_R, FREE_TOP_GAP
    chairs = []
    for s in pair:
        nx, ny = OUT[s]
        for f in (0.25, 0.75):
            if nx:
                px, py = (x + cr if nx < 0 else x + w - cr), y + h * f
            else:
                px, py = x + w * f, (y + cr if ny < 0 else y + h - cr)
            chairs.append(dict(rect=rect(px - cr, py - cr, px + cr, py + cr), pullable=True, pull_dir=[nx, ny], center=[r2(px), r2(py)]))
    if t["type"] == "r4":
        d = 0.8   # 丸い天板の直径(top は外接する正方形。top_shape で丸と示す)
        top = rect(cx - d / 2, cy - d / 2, cx + d / 2, cy + d / 2)
    elif pair == ("left", "right"):
        top = rect(x + g, y + 0.05, x + w - g, y + h - 0.05)
    else:
        top = rect(x + 0.05, y + g, x + w - 0.05, y + h - g)
    seats = [c.pop("center") for c in chairs]
    out = dict(top=top, benches=[], chairs=chairs, seat_points=seats, chair_sides=list(pair))
    if t["type"] == "r4":
        out["top_shape"] = "circle"
    return out


def long6_parts(t):
    """②の6人の長テーブル:天板3.2m×0.7m、上下にスツール3つずつ(図と同じ位置)。スツールは引かない"""
    cx, cy = t["x"] + t["w"] / 2, t["y"] + t["h"] / 2
    top = rect(cx - 1.6, cy - 0.35, cx + 1.6, cy + 0.35)
    chairs, seats = [], []
    for dy in (-0.75, 0.75):
        for dx in (-1.0, 0.0, 1.0):
            px, py = cx + dx, cy + dy
            chairs.append(dict(rect=rect(px - STOOL_R, py - STOOL_R, px + STOOL_R, py + STOOL_R), pullable=False, pull_dir=None, stool=True))
            seats.append([r2(px), r2(py)])
    return dict(top=top, benches=[], chairs=chairs, seat_points=seats, chair_sides=["bottom", "top"])


def table_parts(t, FT, side=None, spot_sides=()):
    if t["type"] == "booth":
        return booth_parts(t, FT)
    if t["type"] == "t2":
        return t2_parts(t, FT)
    if t["type"] == "booth6L":
        return lsofa_parts(t)
    if t["type"] in ("t4", "r4"):
        return free_parts(t, side, spot_sides)
    if t["type"] == "long6":
        return long6_parts(t)
    return None

部品を作る前に席に着く点を出すようにした unity_export.py の build(直した版)

椅子を置く辺を決めるのに「席に着く点がある辺」を使うので、seat_spots を先に呼ぶ順番に変えました。

def build(L, analysis_tables, grid, dist, dist_pass, sides, standby_slots, FT, meta):
    aisles = find_aisles(L)
    aisle_entrances(aisles, grid, dist, dist_pass)
    tables = []
    for t, a, sd in zip(L["tables"], analysis_tables, sides):
        spots = seat_spots(sd, grid)
        parts = table_parts(t, FT, a.get("side"), {s["side"] for s in spots})
        stop = a.get("stop")
        ai = None
        if stop:
            for x in aisles:
                r = x["rect"]
                if r[0] - 0.05 <= stop[0] <= r[2] + 0.05 and r[1] - 0.05 <= stop[1] <= r[3] + 0.05:
                    ai = x["id"]
                    break
        tables.append(dict(id=t["id"], type=t["type"], seats=t["seats"], footprint=rect(t["x"], t["y"], t["x"] + t["w"], t["y"] + t["h"]),
                           category=a["category"], stop=stop, side=a.get("side"), dist=a.get("dist"), aisle=ai,
                           parts=parts, spots=spots))
    walls = [dict(label=w["label"], rect=w["rect"], height=HEIGHTS["partition"] if w.get("joint") or w["label"] in ("仕切り", "目隠し") else
                  (HEIGHTS["toilet_wall"] if w["label"] == "トイレの壁" else HEIGHTS["wall"])) for w in L["walls"]]
    st = (meta or {}).get("settings") or {}
    model = (meta or {}).get("robot_model") or {}
    routes = build_routes(L, analysis_tables, grid, st.get("normal_width", 1.0), model.get("pass_width", 0.8)) if st else None
    return dict(meta=dict(meta, heights=HEIGHTS, rules=__doc__.strip()), routes=routes,
                name=L["name"], outer=L["outer"], outline=L.get("outline"), entrance=L["entrance"],
                floor=floor_rects(L), outer_walls=outer_walls(L), walls=walls,
                rooms=[dict(label=r["label"], kind=r["kind"], rect=r["rect"], **WALKABLE.get(r["label"], {"walkable": False}))
                       for r in L["rooms"] if r["kind"] != "outside"],
                doors=L.get("doors", []), robot_keepout=[dict(label="会計の列(ロボットは通らない)", rect=list(r)) for r in L.get("robot_keepout", [])], aisles=aisles, tables=tables, places=places(L, grid, standby_slots, aisles))

家具の寸法 famires_v3.py の FURNITURE_TYPES(足した版)

FURNITURE_TYPES = {
    "booth": dict(desc="ボックス席。向かい合うベンチ2つと間のテーブル。座る側(開いた側)は長い2辺。短い2辺はベンチの背",
                  bench_depth=0.55, table_depth=0.75, length=1.85, bench_length={"4": 1.2, "6": 1.8},
                  axis={"v": "ベンチが上下(座る側は左右)", "h": "ベンチが左右(座る側は上下)"}),
    "t2": dict(desc="2人席。壁側がベンチ、通路側が椅子。止まってよいのは椅子の側と、テーブルの横(ベンチの横は使わない)",
               footprint=[0.8, 1.4], table=[0.7, 0.6], bench_depth=0.45, chair_radius=0.18),
    "booth6L": dict(desc="L字ソファの6人席。ソファの内側の角にテーブル(約0.75m×1.2m)。止まってよいのは、内側の角の開いた2辺と、"
                         "そこからソファの端に沿って lsofa_end_reach(端の席の人が受け取れる所)まで",
                    sofa_depth_ratio={"band": 0.45, "column": 0.55}),
    "t4": dict(desc="②の4人の角テーブル(外形1.2m四方)。椅子は2脚ずつ向かい合わせ。止まってよいのは4辺(椅子はロボットが止まる辺を含まない組に置く)",
               footprint=[1.2, 1.2], chair_radius=0.2),
    "r4": dict(desc="②の4人の丸テーブル(外形1.1m四方・天板の直径0.8m)。椅子は t4 と同じ置き方。止まってよいのは4辺",
               footprint=[1.1, 1.1], top_diameter=0.8, chair_radius=0.2),
    "long6": dict(desc="②の6人の長テーブル(天板3.2m×0.7m)。上下にスツール3つずつ。止まってよいのは4辺",
                  footprint=[3.2, 1.94], table=[3.2, 0.7], stool_radius=0.22),
    "walls": dict(height_m_仮={"壁": 2.7, "仕切り": 1.2, "トイレの壁": 2.7}),
}

確かめの仕組みに足した確かめ test_famires_v3.py(②の部品)

# ---- ②の角テーブル・丸テーブル・長テーブルの椅子(2026-10-29)
L = v.prepare(famires_p0.p0_memory())
ctx = {}
res, _ = v.analyze(L, v.SETTINGS["res"], ctx)
U = ux.build(L, res, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], v.standby_slots(L["robot_standby"]), v.FURNITURE_TYPES, {})
free = [t for t in U["tables"] if t["type"] in ("t4", "r4", "long6")]
expect("②:全部の卓に部品があり、座る位置の数が席数と同じ",
       all(t["parts"] and len(t["parts"]["seat_points"]) == t["seats"] for t in U["tables"]))
expect("②:角テーブル・丸テーブルの椅子は2脚ずつ向かい合わせ(向かい合う2辺に2脚ずつ)",
       all(len(t["parts"]["chairs"]) == 4 and set(t["parts"]["chair_sides"]) in ({"left", "right"}, {"top", "bottom"})
           for t in free if t["type"] != "long6"))
expect("②:椅子とスツールは卓の外形の内側",
       all(t["footprint"][0] - 1e-9 <= c["rect"][0] and c["rect"][2] <= t["footprint"][2] + 1e-9 and
           t["footprint"][1] - 1e-9 <= c["rect"][1] and c["rect"][3] <= t["footprint"][3] + 1e-9 for t in free for c in t["parts"]["chairs"]))
spot_sides = {t["id"]: {s["side"] for s in t["spots"]} for t in free}
expect("②:椅子を置く辺には、どれも席に着く点がある", all(set(t["parts"]["chair_sides"]) <= spot_sides[t["id"]] for t in free))
expect("②:ロボットが止まる辺に椅子があるのは、その向きの組では座れない卓だけ",
       all(t["side"] not in t["parts"]["chair_sides"] or not ({"top", "bottom"} if t["side"] in ("left", "right") else {"left", "right"}) <= spot_sides[t["id"]]
           for t in free if t["type"] != "long6"))

卓ごとの椅子の置き方を出したスクリプト check_chairs.py

"""②の角テーブル・丸テーブル・長テーブルに椅子を置いた結果を、卓ごとに出して図にする(2026-10-29)。
使い方: python3 check_chairs.py <tools/sim_layouts のフォルダ> <出力.png>"""
import sys, json
sys.path.insert(0, sys.argv[1])
import famires_v3 as v, famires_p0, unity_export as ux
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle
from matplotlib import font_manager as fm
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
fm.fontManager.addfont(FONT); plt.rcParams["font.family"] = fm.FontProperties(fname=FONT).get_name()
L = v.prepare(famires_p0.p0_memory()); ctx = {}
res, err = v.analyze(L, v.SETTINGS["res"], ctx)
U = ux.build(L, res, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], v.standby_slots(L["robot_standby"]), v.FURNITURE_TYPES, {})
for t in U["tables"]:
    if t["type"] in ("t4", "r4", "long6"):
        p = t["parts"]; sp = {s["side"] for s in t["spots"]}
        print(t["id"], t["type"], "stop", t["side"], "spots", sorted(sp), "chairs", p["chair_sides"], "seats", len(p["seat_points"]),
              "stop-side-has-chair", t["side"] in p["chair_sides"])
fig, ax = plt.subplots(figsize=(13, 10.5))
for r in U["rooms"]:
    x0, y0, x1, y1 = r["rect"]; ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, fc="#eceef2", ec="#999", lw=0.6))
    ax.text((x0 + x1) / 2, (y0 + y1) / 2, r["label"], ha="center", va="center", fontsize=7, color="#555")
for w in U["walls"] + [dict(rect=r) for r in U["outer_walls"]]:
    x0, y0, x1, y1 = w["rect"]; ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, fc="#7d8590", ec="none"))
for t in U["tables"]:
    p = t["parts"]; x0, y0, x1, y1 = t["footprint"]
    ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, fc="none", ec="#ccc", lw=0.5, ls="--"))
    for b in p["benches"]: ax.add_patch(Rectangle((b[0], b[1]), b[2] - b[0], b[3] - b[1], fc="#d6d9df", ec="#555", lw=0.6))
    tp = p["top"]
    if p.get("top_shape") == "circle": ax.add_patch(Circle(((tp[0] + tp[2]) / 2, (tp[1] + tp[3]) / 2), (tp[2] - tp[0]) / 2, fc="white", ec="#222", lw=1))
    else: ax.add_patch(Rectangle((tp[0], tp[1]), tp[2] - tp[0], tp[3] - tp[1], fc="white", ec="#222", lw=1))
    for c in p["chairs"]:
        r = c["rect"]; col = "#f08c00" if t["type"] in ("t4", "r4", "long6") else "#888"
        ax.add_patch(Circle(((r[0] + r[2]) / 2, (r[1] + r[3]) / 2), (r[2] - r[0]) / 2, fc=col, ec="#333", lw=0.5, alpha=0.85))
    if t["stop"]: ax.add_patch(Circle(tuple(t["stop"]), 0.13, fc="#e8590c", ec="white", lw=0.8, zorder=5))
    ax.text(x0, y1 + 0.08, t["id"], fontsize=7, color="#333")
ax.set_xlim(-0.5, 24.5); ax.set_ylim(-0.5, 19.6); ax.set_aspect("equal"); ax.axis("off")
ax.set_title("② 定番の郊外型:椅子の置き方(橙の丸=椅子・スツール、赤い点=ロボットが止まる位置)", fontsize=11)
plt.tight_layout(); plt.savefig(sys.argv[2], dpi=110)

check_chairs.py の出力

T03 t4 stop bottom spots ['bottom', 'left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair False
T04 t4 stop bottom spots ['bottom', 'left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair False
T05 r4 stop right spots ['bottom', 'left', 'right', 'top'] chairs ['top', 'bottom'] seats 4 stop-side-has-chair False
T06 r4 stop bottom spots ['bottom', 'left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair False
T07 t4 stop right spots ['bottom', 'left', 'right', 'top'] chairs ['top', 'bottom'] seats 4 stop-side-has-chair False
T08 t4 stop right spots ['bottom', 'left', 'right', 'top'] chairs ['top', 'bottom'] seats 4 stop-side-has-chair False
T11 t4 stop bottom spots ['bottom', 'left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair False
T12 t4 stop right spots ['bottom', 'left', 'right', 'top'] chairs ['top', 'bottom'] seats 4 stop-side-has-chair False
T13 long6 stop right spots ['bottom', 'left', 'right', 'top'] chairs ['bottom', 'top'] seats 6 stop-side-has-chair False
T14 t4 stop right spots ['left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair True
T15 t4 stop top spots ['left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair False
T16 t4 stop left spots ['left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair True
T17 t4 stop left spots ['left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair True
T19 t4 stop left spots ['left', 'right', 'top'] chairs ['left', 'right'] seats 4 stop-side-has-chair True

6店の店の形を作り直した famires_v3.py の出力(版 dffc100b269c)

[OK] p0_memory: {'name': '② 定番の郊外型', 'seats': 88, 'tables': 19, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 19, 'robot_seats': 88, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 8.6, 'robot_dist_max': 13.78, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p1_efficient_corner: {'name': '③ 効率重視型・入口が左下の角', 'seats': 160, 'tables': 42, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 42, 'robot_seats': 160, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.6, 'robot_dist_max': 20.14, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p2_efficient_narrow: {'name': '④ 効率重視型・狭い店・入口が右の壁', 'seats': 88, 'tables': 28, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 28, 'robot_seats': 88, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 8.5, 'robot_dist_max': 13.92, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p3_relaxed_center: {'name': '⑤ ゆったり型・入口が正面の中央', 'seats': 200, 'tables': 46, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 46, 'robot_seats': 200, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 12.5, 'robot_dist_max': 18.8, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p4_relaxed_side: {'name': '⑥ ゆったり型・入口が左の壁', 'seats': 158, 'tables': 38, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 38, 'robot_seats': 158, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.7, 'robot_dist_max': 18.38, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p5_crammed: {'name': '⑦ ③の列の間の通路を95cmに(卓と席数は③と同じ)', 'seats': 160, 'tables': 42, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 18, 'robot_seats': 64, 'narrow_tables': 24, 'narrow_seats': 96, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.8, 'robot_dist_max': 20.17, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []

確かめの仕組みを確かめた test_famires_v3.py の出力(版 dffc100b269c)

01_mires_v3.py_dffc100b269c.txt5,782字・150行・text
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.0)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.0)
OK  75cmの通路はServiで通れる(マス目0.025・ずらし0.0)
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.013)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.013)
OK  75cmの通路はServiで通れる(マス目0.025・ずらし0.013)
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.031)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.031)
別のタブで開く保存

前の版と比べ、版の番号と説明文のほかは変わっていないことを確かめたスクリプト check_gen_only.py

"""前の版と新しい版の店の形(public/sim/layouts)を比べ、版の番号(generation)を除いて何が変わったかを出す(2026-10-29)。
使い方: python3 check_gen_only.py <前の layouts のフォルダ> <新しい layouts のフォルダ>"""
import json, glob, os, sys
B, A = sys.argv[1], sys.argv[2]
def strip(o):
    if isinstance(o, dict): return {k: strip(v) for k, v in o.items() if k != "generation"}
    if isinstance(o, list): return [strip(v) for v in o]
    return o
print("generation", json.load(open(B + "/summary.json"))[0]["generation"], "->", json.load(open(A + "/summary.json"))[0]["generation"])
for f in sorted(glob.glob(B + "/**/*.json", recursive=True)):
    rel = os.path.relpath(f, B); a = strip(json.load(open(f))); b = strip(json.load(open(os.path.join(A, rel))))
    if a == b:
        print("同じ(版の番号だけ)", rel); continue
    if isinstance(a, dict):
        ks = [k for k in a if a.get(k) != b.get(k)]
        line = f"違う {rel}: {ks}"
        if "meta" in ks: line += f"  meta の中: {[k for k in a['meta'] if a['meta'][k] != b['meta'].get(k)]}"
        if "tables" in ks and isinstance(a["tables"], list):
            ta = {t.get("id", i): t for i, t in enumerate(a["tables"])}; tb = {t.get("id", i): t for i, t in enumerate(b["tables"])}
            line += f"  卓: {[i for i in ta if ta[i] != tb.get(i)]}"
        print(line)
    else:
        print("違う", rel, "(list)", [i for i, (x, y) in enumerate(zip(a, b)) if x != y])

check_gen_only.py の出力(2e9fc773050a → dffc100b269c)

generation 2e9fc773050a -> dffc100b269c
違う p0_memory.json: ['meta']  meta の中: ['furniture_types']
違う p0_memory_analysis.json: ['meta']  meta の中: ['furniture_types']
違う p1_efficient_corner.json: ['meta']  meta の中: ['furniture_types']
違う p1_efficient_corner_analysis.json: ['meta']  meta の中: ['furniture_types']
違う p2_efficient_narrow.json: ['meta']  meta の中: ['furniture_types']
違う p2_efficient_narrow_analysis.json: ['meta']  meta の中: ['furniture_types']
違う p3_relaxed_center.json: ['meta']  meta の中: ['furniture_types']
違う p3_relaxed_center_analysis.json: ['meta']  meta の中: ['furniture_types']
違う p4_relaxed_side.json: ['meta']  meta の中: ['furniture_types']
違う p4_relaxed_side_analysis.json: ['meta']  meta の中: ['furniture_types']
違う p5_crammed.json: ['meta']  meta の中: ['furniture_types']
違う p5_crammed_analysis.json: ['meta']  meta の中: ['furniture_types']
同じ(版の番号だけ) summary.json
違う unity/p0_memory.json: ['meta', 'tables']  meta の中: ['rules']  卓: ['T03', 'T04', 'T05', 'T06', 'T07', 'T08', 'T11', 'T12', 'T13', 'T14', 'T15', 'T16', 'T17', 'T19']
同じ(版の番号だけ) unity/p0_memory_map.json
違う unity/p1_efficient_corner.json: ['meta']  meta の中: ['rules']
同じ(版の番号だけ) unity/p1_efficient_corner_map.json
違う unity/p2_efficient_narrow.json: ['meta']  meta の中: ['rules']
同じ(版の番号だけ) unity/p2_efficient_narrow_map.json
違う unity/p3_relaxed_center.json: ['meta']  meta の中: ['rules']
同じ(版の番号だけ) unity/p3_relaxed_center_map.json
違う unity/p4_relaxed_side.json: ['meta']  meta の中: ['rules']
同じ(版の番号だけ) unity/p4_relaxed_side_map.json
違う unity/p5_crammed.json: ['meta']  meta の中: ['rules']
同じ(版の番号だけ) unity/p5_crammed_map.json

店の形の README.md に足した行(椅子の決まり)

diff --git a/tools/sim_layouts/README.md b/tools/sim_layouts/README.md
index ecb0db7..81ddaea 100644
--- a/tools/sim_layouts/README.md
+++ b/tools/sim_layouts/README.md
@@ -34,7 +34,10 @@
 - ロボットの道順(`routes`):受け渡し口の前→各卓の止まる位置(`from_pass`)と、卓→卓(`between`。"T01|T02" は T01 から T02、逆向きは点を逆にたどる)。ふだん走らせる幅の道で行けるならその道、行けなければ通れる幅の道(試算と同じ規則。`use` が normal か narrow。試算の卓の分け方と一致することをテストで確かめる)。マス目の最短の道を、通れる範囲でまっすぐにつないだ曲がり角の点の並び。2026-10-22 から Unity のロボットはこの点を順にたどっていたが、2026-10-24 から下の地図の上で自分で道を引く形に変える(線に縛ると、行きと帰りが同じ線になり、ロボットどうしが正面から出会って回り込めなかったため)。道順は、回り込みで延びた距離を測る物差しと、前の動き方に戻す切り替えのために残す
 - ロボットの地図(`unity/{名前}_map.json`。2026-10-24):試算が卓までの距離を測るのと同じ2.5cmのマス目で、マスごとに「ふだんの道(1.0m)が通れる=2/通れる幅(0.8m)の道だけ通れる=1/通れない=0」(`free_class`)と、いちばん近い障害物までの距離(`clear_mm`)を入れたもの。分け方は試算と同じ式(`Grid.free`)。会計の列は通れない。待機場所は床として入れる(止まっているロボットは Unity で動くものとして扱う。待機場所の真ん中は壁から0.4m前後なので1のマス)。Unity のロボットはこの上で自分で道を引く(行き先までふだんの道で行けるならその道だけ、行けなければ狭い道も使う。試算と同じ規則)。地図だけで試算の卓の分け方(robot・narrow)が再現できることをテストで確かめる。中身は gzip で縮めた base64(読み方は `unity_export.unpack_map`)
 - 会計の列(`robot_keepout`):ロボットの道にしない矩形。人は通れるので、Unity では部屋や壁として建てない(道順はこれをよけて作ってある)
-- まだないもの:② の4人席・丸テーブル・6人の長テーブルの椅子と座る位置(parts が null)、人がロボットをよける逃げ場(refuges)
+- ②の4人の角テーブル・丸テーブル(2026-10-29 エイイチの記憶):椅子は2脚ずつ向かい合わせ。椅子を置く2辺は、ロボットが止まる辺を含まない組(ロボットは天板の端に横付け)。その組の片方に席に着く点がない(壁ぎわで座れない)ときだけ、もう一方の組にする(下の壁ぎわの T14・T16・T17・T19 は、止まる辺の横に椅子が来る)。椅子は半径0.2m・外形の内側で、外へ引ける。丸テーブルの天板は直径0.8m(`top` は外接する正方形、`top_shape: "circle"`)
+- ②の6人の長テーブル:天板3.2m×0.7m、上下にスツール3つずつ(半径0.22m・図と同じ位置)。スツールは引かない(`pullable: false`・`stool: true`)
+- 卓の数・止まる位置・距離は変わらない(椅子は外形の内側に置くだけ)。版 2e9fc773050a → dffc100b269c(②の部品と説明が増えただけで、6店とも数字は同じ。サイトの試算は計算し直していない)
+- まだないもの:人がロボットをよける逃げ場(refuges)
 
 ## 動かし方
 

②を Unity で回す条件:スタッフ2人・1時間に19組・ロボット0・1・2台

Unity で回す条件は、ほかの店と同じ決め方にしました。スタッフは選べる人数(1・2・3人)の真ん中の2人、混み具合は普通の1時間19組、ロボット0・1・2台、上げ膳だけ、1往復で3卓、レジ1つです。Unity が読む条件のファイル(serving-robot-params.json)の export_params.py に②を足し、条件は15件から18件になりました。前の15件と共通の値は変わっていません。試算での②の入店待ちは、0台2.68分・1台2.44分・2台1.60分で、小さい店で余裕があり、ロボットの効きは小さい店です。

サイトへの反映は p2m-site の commit b0f1b0c(31ファイル)です。公開後、https://pixelstomotors.com/sim/serving-robot-params.json の版の番号が dffc100b269c・条件18件になったことを確かめました。

作業ログ ― Unity に②を建て、1回ずつ回した(依頼23)

Claude Code と依頼箱で、Mac の Unity を動かしている

Claude Code は、Anthropic の AI(Claude)が、パソコンの中でコードを読み書きし、コマンドを動かす道具です。このプロジェクトでは、Unity の店のコードを書いて回すのは Mac の Claude Code の役目で、設計とサイトの試算はこの会話の Claude が受け持っています。

その間の受け渡しが「依頼箱」です。依頼の文(Markdown)を iCloud の 依頼箱/新規/ に置くと、Mac の見張り係が Claude Code を起動して依頼を実行し、依頼箱/結果/ に報告を書き戻します。終わると Mac で音が鳴ります。依頼の文には、始める前に確かめること(Unity が閉じているか・外付けの SSD がつながっているか・台帳が退避と同じか)と、違ったら手を止めて報告することを毎回書きます。台帳(回の記録の CSV)は、依頼の前に Claude が KIOXIA の archive に写して退避しています(631行・md5 41c8ceff63036168a9d2940fb1e8c4ba)。

依頼文:②を建てて確かめる(20261029_23)

02_sec02.md4,051字・65行・markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼:② 定番の郊外型(fam_p0・記憶の店)を Unity に建て、1回ずつ回して確かめる(2026-10-29)

エイイチの決定:⑥はここで区切り(LOG398)。④⑤⑥ → ② の順の最後、②へ進む。今回は②だけ。
別のタブで開く保存

Unity 側で足したのは、丸い天板とスツールを読むための2つの項目だった

作業中、Unity の変更を見る画面(Unity Version Control の差分)が開きました。Claude Code が StorePlan.cs に足したのは、椅子が背のないスツールかどうか(stool)と、天板が丸いかどうか(tableRound)の2つの項目です。依頼で「parts の読み方を店に関係なく使える形に広げてよい」とした範囲で、ほかの店の作りや回し方には触れていません。

Unity Version Control の差分の画面。StorePlan.cs に、背のないスツールかを表す stool と、天板が丸いかを表す tableRound の2つの項目が足されている

報告:20261029_1732_p0_build_check.txt

03_sec02.txt12,819字・253行・text
====================================================================
② 定番の郊外型(fam_p0・記憶の店)を建てて、1回ずつ回して確かめた
依頼:20261029_23_店2を建てて確かめる.md
2026-10-29 17:32 check-in:cs:39 build_id cs39_20261029_1717(保留 0)
====================================================================

結論
別のタブで開く保存

依頼箱の結果:20261029_23_店2を建てて確かめる_20261029_1704.md

04_sec02.md4,547字・84行・markdown
# 結果:20261029_23_店2を建てて確かめる

- 状態:完了
- 始めた時刻:2026-10-29 17:04
- 終わった時刻:2026-10-29 17:33(30分)
- 作業フォルダ:/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき:作業フォルダで `claude --resume 5e0d11db-6205-4c89-814d-f8a518aa5999`
- 止められた操作:1件(いちばん下に一覧)
別のタブで開く保存

1台はきれいに回り、2台は丸テーブルの間で動けなくなった

②は Unity で手直しなしに建ちました(卓19・席88・止まる位置19か所すべて NavMesh の上)。3回とも最後まで回り、入店待ちは0台1.33分・1台1.21分・2台1.45分です。1台の回は30秒以上の止まりが1回だけでした。

2台の回は、30秒以上の止まりが17回、60秒動けなくなったのが12回で、1台の回より悪くなりました。12回のうち8回が、丸テーブル T05 と T06 の間です。理由は椅子4回・もう1台のロボット4回でした。

建てる前の図面の点検で、奥の8卓(36席)へ行く道がすべて通る「仕切り壁の右端とパントリーの間の1.5m」を、詰まりやすい所として挙げていました。2台のロボットがすれ違うには1.6m要るためです。確かめの1回では、ここの30秒以上の止まりは0回で、人がよけて数秒待つだけで抜けていました(あとの30回の夜では16回出ています)。60秒動けなくなったのは、図面の点検で気にしていなかった丸テーブルの間でした。

作業ログ ― 丸テーブル T06 を右へ1.0m寄せた

直し方を決める前に、Unity の結果を受けて方針を決めました。

先にサイトで丸テーブルの並びを見直してから夜を回す(記憶の店といってもかなり昔なので無理そうなレイアウトなら変えていいよ)

T05 に止まったロボットの横には、0.24mしか残っていなかった

上の区画の卓は、左右2列 × 3段に並んでいます。左の列と右の列の間は、どの段も1.0〜1.1mです。T05(左の丸テーブル)に料理を置くロボットは、T05 の右の辺、つまり丸テーブル2つの間に止まります。止まったロボットの体(幅0.5m)の横に残るのは0.24mだけで、上の段(T03・T04)へこの間を通って行くロボットは抜けられません。さらに T06 の左の椅子はこの間の側にあり、客が座るときに引くと、もっと狭くなります。

寄せ方の案を、決まり・止まる位置・距離で比べたスクリプト round_options.py

案は2つ比べました。A は T06 だけを右へ0.7m、B は T06 と下の段の T08 を右へ0.7m です。

"""②の丸テーブルまわりの直し方の案(今・A:T06を右へ0.7m・B:T06とT08を右へ0.7m)を、決まり・止まる位置・距離・道順で比べる(2026-10-29)。
使い方: python3 round_options.py <tools/sim_layouts のフォルダ> <出力フォルダ/>"""
import sys, json, copy
sys.path.insert(0, sys.argv[1])
import famires_v3 as v, famires_p0, unity_export as ux
OUT = sys.argv[2]
opts = {"now": {}, "A": {"T06": 0.7}, "B": {"T06": 0.7, "T08": 0.7}}
res_all = {}
for name, mv in opts.items():
    L = v.prepare(famires_p0.p0_memory())
    for t in L["tables"]:
        if t["id"] in mv: t["x"] = round(t["x"] + mv[t["id"]], 3)
    viol = v.check_rules(L)
    if viol: print(name, "VIOL", viol); continue
    ctx = {}
    res, err = v.analyze(L, v.SETTINGS["res"], ctx)
    resb, errb = v.analyze(L, v.SETTINGS["check_res"])
    conv = [a["id"] for a, b in zip(res, resb) if a["category"] != b["category"]]
    U = ux.build(L, res, ctx["grid"], ctx["d_norm"], ctx["d_pass"], ctx["sides"], v.standby_slots(L["robot_standby"]), v.FURNITURE_TYPES, {})
    R = ux.build_routes(L, res, ctx["grid"], v.SETTINGS["normal_width"], v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"])
    U["routes"] = R
    vr = v.check_register_clear(U)
    s = v.summarize(L, res)
    print(name, "err", err, "conv", conv, "reg", vr, "mean", s["robot_dist_mean"], "max", s["robot_dist_max"], "cats", s["robot_tables"], s["narrow_tables"], s["human_tables"])
    for t in U["tables"]:
        if t["id"] in ("T05", "T06", "T07", "T08", "T03", "T04"):
            print("  ", t["id"], t["footprint"], "stop", t["stop"], t["side"], "dist", t["dist"], "chairs", t["parts"]["chair_sides"])
    json.dump(U, open(OUT + f"U_{name}.json", "w"), ensure_ascii=False)
    res_all[name] = [r.get("dist") for r in res]
json.dump(res_all, open(OUT + "dists.json", "w"))

round_options.py の出力

now err [] conv [] reg [] mean 8.6 max 13.78 cats 19 0 0
   T03 [4.0, 15.0, 5.2, 16.2] stop [np.float64(5.038), np.float64(14.212)] bottom dist 12.56 chairs ['left', 'right']
   T04 [6.0, 15.0, 7.2, 16.2] stop [np.float64(7.038), np.float64(14.238)] bottom dist 11.41 chairs ['left', 'right']
   T05 [3.65, 12.65, 4.75, 13.75] stop [np.float64(5.363), np.float64(12.812)] right dist 11.03 chairs ['top', 'bottom']
   T06 [5.85, 12.65, 6.95, 13.75] stop [np.float64(6.788), np.float64(11.862)] bottom dist 9.05 chairs ['left', 'right']
   T07 [3.6, 9.6, 4.8, 10.8] stop [np.float64(5.312), np.float64(10.638)] right dist 10.71 chairs ['top', 'bottom']
   T08 [5.8, 9.6, 7.0, 10.8] stop [np.float64(7.788), np.float64(9.763)] right dist 6.63 chairs ['top', 'bottom']
A err [] conv [] reg [] mean 8.5 max 13.78 cats 19 0 0
   T03 [4.0, 15.0, 5.2, 16.2] stop [np.float64(5.038), np.float64(14.212)] bottom dist 12.15 chairs ['left', 'right']
   T04 [6.0, 15.0, 7.2, 16.2] stop [np.float64(6.163), np.float64(14.212)] bottom dist 11.77 chairs ['left', 'right']
   T05 [3.65, 12.65, 4.75, 13.75] stop [np.float64(5.538), np.float64(12.812)] right dist 10.62 chairs ['top', 'bottom']
   T06 [6.55, 12.65, 7.65, 13.75] stop [np.float64(7.488), np.float64(11.862)] bottom dist 8.76 chairs ['left', 'right']
   T07 [3.6, 9.6, 4.8, 10.8] stop [np.float64(5.312), np.float64(10.638)] right dist 10.71 chairs ['top', 'bottom']
   T08 [5.8, 9.6, 7.0, 10.8] stop [np.float64(7.788), np.float64(9.763)] right dist 6.63 chairs ['top', 'bottom']
B err [] conv [] reg [] mean 8.6 max 14.19 cats 19 0 0
   T03 [4.0, 15.0, 5.2, 16.2] stop [np.float64(5.038), np.float64(14.212)] bottom dist 12.56 chairs ['left', 'right']
   T04 [6.0, 15.0, 7.2, 16.2] stop [np.float64(6.163), np.float64(14.212)] bottom dist 12.18 chairs ['left', 'right']
   T05 [3.65, 12.65, 4.75, 13.75] stop [np.float64(5.538), np.float64(12.812)] right dist 11.03 chairs ['top', 'bottom']
   T06 [6.55, 12.65, 7.65, 13.75] stop [np.float64(7.488), np.float64(11.862)] bottom dist 8.76 chairs ['left', 'right']
   T07 [3.6, 9.6, 4.8, 10.8] stop [np.float64(5.588), np.float64(10.638)] right dist 10.61 chairs ['top', 'bottom']
   T08 [6.5, 9.6, 7.7, 10.8] stop [np.float64(8.488), np.float64(9.763)] right dist 6.24 chairs ['top', 'bottom']

B は、T08 に料理を置くロボットが、T08 の右(パントリーの壁ぞい)に止まります。奥の区画へ行く道はすべてこの壁ぞいを通るので、止まったロボットの横に0.76mしか残らず、ロボットの通れる幅(0.8m)を下回ります。奥の8卓への一本道をふさぐことになるので、B はやめました。

丸テーブルの間を通る道を数えたスクリプト gap_routes.py

"""今の並びと案A(T06を右へ0.7m)で、受け渡し口から T01〜T06 への道と、卓から卓への道が、丸テーブル2つの間を通るかを数える(2026-10-29)。
使い方: python3 gap_routes.py <round_options.py の出力フォルダ/>"""
import json, sys
D = sys.argv[1]
for n, gap in (("now", (4.75, 5.85)), ("A", (4.75, 6.55))):
    U = json.load(open(D + f"U_{n}.json"))
    fp = U["routes"]["from_pass"]
    for tid in ("T01", "T02", "T03", "T04", "T05", "T06"):
        pts = fp[tid]["points"]
        thru = any(gap[0] < p[0] < gap[1] and 12.65 < p[1] < 13.75 for p in pts)
        print(n, tid, "len", fp[tid]["length"], "thru_gap", thru, [(round(p[0], 1), round(p[1], 1)) for p in pts][-5:])
    c = sum(1 for k, r in U["routes"]["between"].items() if any(gap[0] < p[0] < gap[1] and 12.65 < p[1] < 13.75 for p in r["points"]))
    print(n, "between routes through gap", c)

gap_routes.py の出力

now T01 len 13.191 thru_gap False [(7.1, 11.3), (3.4, 12.2), (3.2, 12.4), (3.2, 12.6), (3.2, 13.4)]
now T02 len 11.831 thru_gap False [(9.0, 5.9), (8.3, 10.2), (7.2, 11.3), (7.1, 11.3), (3.4, 11.3)]
now T03 len 12.049 thru_gap False [(5.5, 12.3), (5.4, 12.4), (5.4, 12.6), (5.2, 14.0), (5.0, 14.2)]
now T04 len 10.918 thru_gap False [(9.0, 5.8), (9.0, 5.9), (7.7, 13.7), (7.2, 14.2), (7.0, 14.2)]
now T05 len 10.569 thru_gap True [(7.2, 11.3), (5.5, 12.3), (5.4, 12.4), (5.4, 12.6), (5.4, 12.8)]
now T06 len 8.749 thru_gap False [(9.3, 5.5), (9.0, 5.8), (9.0, 5.9), (8.3, 10.3), (6.8, 11.9)]
now between routes through gap 18
A T01 len 13.191 thru_gap False [(7.1, 11.3), (3.4, 12.2), (3.2, 12.4), (3.2, 12.6), (3.2, 13.4)]
A T02 len 11.831 thru_gap False [(9.0, 5.9), (8.3, 10.2), (7.2, 11.3), (7.1, 11.3), (3.4, 11.3)]
A T03 len 11.694 thru_gap False [(7.2, 11.3), (6.1, 12.3), (6.1, 12.4), (5.1, 14.1), (5.0, 14.2)]
A T04 len 11.448 thru_gap False [(6.1, 12.3), (6.1, 12.4), (6.1, 12.5), (6.1, 14.1), (6.2, 14.2)]
A T05 len 10.279 thru_gap True [(9.0, 5.8), (9.0, 5.9), (8.3, 10.2), (7.2, 11.3), (5.5, 12.8)]
A T06 len 8.411 thru_gap False [(9.3, 5.5), (9.0, 5.8), (9.0, 5.9), (8.1, 11.2), (7.5, 11.9)]
A between routes through gap 31

A にすると、上の段の T03・T04 へ行く道がどちらも丸テーブルの間を通るようになり、卓から卓への道も18本から31本に増えます。間が広がる分、ここが上の段への主な道になります。それなら、T05 にロボットが止まっていても、横をふだんの道(1.0m)で抜けられる幅が要ります。

T06 を寄せる幅を0.8〜1.2mで比べたスクリプト sweep_t06.py

"""T06 を右へ寄せる幅(0.8・0.9・1.0・1.2m)ごとに、T05 にロボットが止まったときの横の残り・T06 の右の通路・止まる位置・距離を出す(2026-10-29)。
使い方: python3 sweep_t06.py <tools/sim_layouts のフォルダ>"""
import sys, json
sys.path.insert(0, sys.argv[1])
import famires_v3 as v, famires_p0, unity_export as ux
for sh in (0.8, 0.9, 1.0, 1.2):
    L = v.prepare(famires_p0.p0_memory())
    for t in L["tables"]:
        if t["id"] == "T06": t["x"] = round(t["x"] + sh, 3)
    viol = v.check_rules(L)
    if viol: print(sh, "VIOL", viol); continue
    ctx = {}
    res, err = v.analyze(L, v.SETTINGS["res"], ctx)
    R = ux.build_routes(L, res, ctx["grid"], v.SETTINGS["normal_width"], v.ROBOT_MODELS[v.SETTINGS["model"]]["pass_width"])
    d = {r["id"]: r for r in res}; T = {t["id"]: t for t in L["tables"]}
    s5 = d["T05"]["stop"]; t6 = T["T06"]
    free = t6["x"] - (s5[0] + 0.25)
    gap = (4.75, t6["x"])
    thru = [k for k in ("T03", "T04") if any(gap[0] < p[0] < gap[1] for p in R["from_pass"][k]["points"])]
    print(sh, "T06", t6["x"], t6["x"]+t6["w"], "corr", round(9.5-(t6["x"]+t6["w"]),2), "T05stop", [round(x,2) for x in s5], d["T05"]["side"],
          "free_beside", round(free,2), "T06stop", [round(x,2) for x in d["T06"]["stop"]], d["T06"]["side"], "top via gap", thru,
          "mean", v.summarize(L,res)["robot_dist_mean"], "dists", {k: d[k]["dist"] for k in ("T03","T04","T05","T06")})

sweep_t06.py の出力

0.8 T06 6.65 7.75 corr 1.75 T05stop [np.float64(5.54), np.float64(12.81)] right free_beside 0.86 T06stop [np.float64(7.59), np.float64(11.86)] bottom top via gap ['T03', 'T04'] mean 8.5 dists {'T03': 12.12, 'T04': 11.65, 'T05': 10.62, 'T06': 8.71}
0.9 T06 6.75 7.85 corr 1.65 T05stop [np.float64(5.54), np.float64(12.81)] right free_beside 0.96 T06stop [np.float64(7.69), np.float64(11.86)] bottom top via gap ['T03', 'T04'] mean 8.5 dists {'T03': 12.12, 'T04': 11.61, 'T05': 10.62, 'T06': 8.67}
1.0 T06 6.85 7.949999999999999 corr 1.55 T05stop [np.float64(5.54), np.float64(12.81)] right free_beside 1.06 T06stop [np.float64(7.79), np.float64(11.86)] bottom top via gap ['T03', 'T04'] mean 8.5 dists {'T03': 12.12, 'T04': 11.57, 'T05': 10.62, 'T06': 8.63}
1.2 T06 7.05 8.15 corr 1.35 T05stop [np.float64(5.54), np.float64(12.81)] right free_beside 1.26 T06stop [np.float64(7.99), np.float64(11.86)] bottom top via gap ['T03', 'T04'] mean 8.5 dists {'T03': 12.12, 'T04': 11.49, 'T05': 10.62, 'T06': 8.55}
T06 を右へT05 に止まったロボットの横
T06 の右とパントリーの壁の間
0.8m0.86m1.75m
0.9m0.96m1.65m
1.0m1.06m1.55m
1.2m1.26m1.35m

1.0m で、止まったロボットの横がふだんの道の1.0mを超えます。1.2m にすると、右の壁ぞい(奥の区画への一本道)が1.35m まで狭まり、T06 の右の椅子を引くとさらに減るので、1.0m にしました。

  • ねらい:T05 に料理を置くロボットが止まっていても、上の段へ行くロボットが横を抜けられるようにする
  • 変わったこと:丸テーブル2つの間 1.1m → 2.1m、止まったロボットの横 0.24m → 1.06m。19卓・88席のまま。卓までの距離が変わったのは5卓(T01 13.78→13.77・T03 12.56→12.12・T04 11.41→11.57・T05 11.03→10.62・T06 9.05→8.63m)
  • 見込める効果:確かめの2台の回で、丸テーブルのまわりの60秒の止まり 8回 → 0回、料理の待ち 0.94 → 0.54分(測った値。下の依頼24)。30回の夜でも、ここの60秒の止まりは2回だけだった
上の区画の、いまの並び(左)と T06 を右へ1.0m 寄せた案(右)。赤い丸は T05 に料理を置くロボット、赤い数字はその横を別のロボットが抜けられる幅(0.24m → 1.06m)

2つの並びを描いたスクリプト round_compare_img.py

05_round_compare_img.py.txt4,016字・56行・python
"""いまの並びと、T06 を右へ1.0m 寄せた案を、上の区画だけ並べて描く(2026-10-29)。
使い方: python3 round_compare_img.py <tools/sim_layouts のフォルダ> <出力フォルダ/>"""
import sys, json
sys.path.insert(0, sys.argv[1])
import famires_v3 as v, famires_p0, unity_export as ux
import matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle
別のタブで開く保存

店の形の作り方 famires_p0.py(直した版・全文)

06_famires_p0.py.txt5,081字・99行・python
"""記憶の店(昔働いていたファミレスを記憶から起こした店・layout_v11)を、5パターンと同じ決まりに合わせた版。

layout_v11.json(記憶のまま)は変えずに残し、ここで直した版を作る。
直したこと(2026-10-21 エイイチと決めたこと)
- 建物の形:客席の奥の壁はそのまま。トイレ・パントリーの出入り口・洗い場・物置の並びが、その外へ約2.2m出っ張る
- 空いた所は厨房が広がる(キッチンと冷蔵庫を奥へ延ばし、出っ張りとの間に裏の通路)
- トイレへは、パントリーの壁ぞいの通路を奥へ進んで入る。元のトイレの場所は通路。トイレの手前に短い仕切り壁(約2.6m)
- トイレの客席側(通路側)に厚い壁(30cm)、入口は下向き(通路の突き当たり)
別のタブで開く保存

店の形の README.md と famires_p0.py の差分

diff --git a/tools/sim_layouts/README.md b/tools/sim_layouts/README.md
index 81ddaea..5be98f2 100644
--- a/tools/sim_layouts/README.md
+++ b/tools/sim_layouts/README.md
@@ -132,6 +132,13 @@ python3 test_famires_v3.py         # 確かめの仕組みそのものを確か
 - 記録だけ:会計の列(y 1.25)が待合の前の点(1.8, 0.9)の0.35m上に来て、案内を待つ客と会計に並ぶ客が重なる。待合は動かしていない(Unity で見る)
 - ⑥の卓までの距離:T27 4.79→5.25・T28 6.64→7.10・T29 8.49→8.12・T30 10.22→9.75m(平均10.7m・最長18.4mは同じ)。⑥以外は版の番号だけ
 
+## 決まり(2026-10-29 エイイチと決めたこと:②の丸テーブル)
+
+- ②の右の丸テーブル(T06)を右へ1.0m(x 5.85〜6.95 → 6.85〜7.95)。丸テーブル2つの間が1.1mしかなく、T05 に料理を置くロボットが止まると横に0.24mしか残らず、上の段(T03・T04)へ行くロボットが抜けられなかった(Unity の確かめの2台の回で、ここで60秒の止まり8回)。間を2.1mにし、T05 にロボットが止まっていても横に1.06m(ふだんの道1.0m)残る。T06 の右とパントリーの壁の間は2.55m → 1.55m
+- 記憶の店は昔の記憶なので、ロボットが回れない所は変えてよい(エイイチ)
+- ②の卓までの距離:T01 13.78→13.77・T03 12.56→12.12・T04 11.41→11.57・T05 11.03→10.62・T06 9.05→8.63m(平均8.6→8.5m)。②以外は版の番号だけ
+- 下の段の T08 も右へ寄せる案はやめた(T08 に料理を置くロボットが、パントリーの壁ぞいの奥への一本道を0.76mまでふさぐため)
+
 ## まだ入っていないこと
 
 - 厨房の中の人の動き(扉・作業通路・下げ膳の返却口)
diff --git a/tools/sim_layouts/famires_p0.py b/tools/sim_layouts/famires_p0.py
index 5f7c5ec..eefd99d 100644
--- a/tools/sim_layouts/famires_p0.py
+++ b/tools/sim_layouts/famires_p0.py
@@ -80,6 +80,12 @@ def p0_memory():
     for t in tables:
         if t["type"] == "t4" and abs(t["y"] - 10.0) < 1e-6:
             t["y"] = 9.6
+    # 右の丸テーブル(T06)を右へ1.0m(2026-10-29 エイイチと決めた)。丸テーブル2つの間が1.1mしかなく、T05 に料理を置くロボットが
+    # 止まると横に0.24mしか残らず、上の段(T03・T04)へ行くロボットが抜けられなかった(Unity の確かめの2台の回で60秒の止まり8回)。
+    # 間を2.1mにし、T05 にロボットが止まっていても横に1.06m(ふだんの道1.0m)残す。T06 の右とパントリーの壁の間は2.55m → 1.55m
+    for t in tables:
+        if t["type"] == "r4" and abs(t["x"] - 5.85) < 1e-6:
+            t["x"] = 6.85
 
     # 待機場所:記憶のまま(2.2×0.7m)だと3台分(1台0.8×0.8m)に足りないので、右に0.2m・手前に0.1m広げる(2026-10-22 エイイチと決めた)
     sb0 = B["robot_standby"]

6店の店の形を作り直した famires_v3.py の出力(版 08db8a0d067c)

[OK] p0_memory: {'name': '② 定番の郊外型', 'seats': 88, 'tables': 19, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 19, 'robot_seats': 88, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 8.5, 'robot_dist_max': 13.77, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p1_efficient_corner: {'name': '③ 効率重視型・入口が左下の角', 'seats': 160, 'tables': 42, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 42, 'robot_seats': 160, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.6, 'robot_dist_max': 20.14, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p2_efficient_narrow: {'name': '④ 効率重視型・狭い店・入口が右の壁', 'seats': 88, 'tables': 28, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 28, 'robot_seats': 88, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 8.5, 'robot_dist_max': 13.92, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p3_relaxed_center: {'name': '⑤ ゆったり型・入口が正面の中央', 'seats': 200, 'tables': 46, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 46, 'robot_seats': 200, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 12.5, 'robot_dist_max': 18.8, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p4_relaxed_side: {'name': '⑥ ゆったり型・入口が左の壁', 'seats': 158, 'tables': 38, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 38, 'robot_seats': 158, 'narrow_tables': 0, 'narrow_seats': 0, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.7, 'robot_dist_max': 18.38, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []
[OK] p5_crammed: {'name': '⑦ ③の列の間の通路を95cmに(卓と席数は③と同じ)', 'seats': 160, 'tables': 42, 'model': 'BellaBot', 'pass_width': 0.8, 'normal_width': 1.0, 'robot_tables': 18, 'robot_seats': 64, 'narrow_tables': 24, 'narrow_seats': 96, 'human_tables': 0, 'human_seats': 0, 'robot_dist_mean': 10.8, 'robot_dist_max': 20.17, 'human_walk_total': 0.0, 'human_walk_max': None}  マス目の差: []

確かめの仕組みを確かめた test_famires_v3.py の出力(版 08db8a0d067c)

07_mires_v3.py_08db8a0d067c.txt5,782字・150行・text
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.0)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.0)
OK  75cmの通路はServiで通れる(マス目0.025・ずらし0.0)
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.013)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.013)
OK  75cmの通路はServiで通れる(マス目0.025・ずらし0.013)
OK  80cmの通路はBellaBotで通れる(マス目0.025・ずらし0.031)
OK  77cmの通路はBellaBotで通れない(マス目0.025・ずらし0.031)
別のタブで開く保存

check_gen_only.py の出力(dffc100b269c → 08db8a0d067c)

generation dffc100b269c -> 08db8a0d067c
違う p0_memory.json: ['layout']
違う p0_memory_analysis.json: ['summary', 'tables']  卓: ['T01', 'T03', 'T04', 'T05', 'T06']
同じ(版の番号だけ) p1_efficient_corner.json
同じ(版の番号だけ) p1_efficient_corner_analysis.json
同じ(版の番号だけ) p2_efficient_narrow.json
同じ(版の番号だけ) p2_efficient_narrow_analysis.json
同じ(版の番号だけ) p3_relaxed_center.json
同じ(版の番号だけ) p3_relaxed_center_analysis.json
同じ(版の番号だけ) p4_relaxed_side.json
同じ(版の番号だけ) p4_relaxed_side_analysis.json
同じ(版の番号だけ) p5_crammed.json
同じ(版の番号だけ) p5_crammed_analysis.json
違う summary.json (list) [0]
違う unity/p0_memory.json: ['routes', 'tables']  卓: ['T01', 'T03', 'T04', 'T05', 'T06']
違う unity/p0_memory_map.json: ['counts', 'free_class', 'clear_mm']
同じ(版の番号だけ) unity/p1_efficient_corner.json
同じ(版の番号だけ) unity/p1_efficient_corner_map.json
同じ(版の番号だけ) unity/p2_efficient_narrow.json
同じ(版の番号だけ) unity/p2_efficient_narrow_map.json
同じ(版の番号だけ) unity/p3_relaxed_center.json
同じ(版の番号だけ) unity/p3_relaxed_center_map.json
同じ(版の番号だけ) unity/p4_relaxed_side.json
同じ(版の番号だけ) unity/p4_relaxed_side_map.json
同じ(版の番号だけ) unity/p5_crammed.json
同じ(版の番号だけ) unity/p5_crammed_map.json

②以外の5店は、版の番号のほかは何も変わっていません。

作業ログ ― 卓までの距離が5卓で変わったので、②の試算だけ計算し直した

サイトの試算は、SimPy で店の時間を進めて数えている

サイトの試算(haizen_sim.py)は、Python の SimPy(出来事を時刻の順に並べて進める、待ち行列のシミュレーションの道具)で書いています。お客さんが来る・席に案内される・料理ができる・ロボットかスタッフが運ぶ・食べ終えて会計する、という出来事を、ピークの2時間ぶん時刻の順に進め、待ち時間を数えます。卓までの距離は、店の形から測った卓ごとの距離の一覧(dist_list)から選びます。条件(店・スタッフ・ロボットの台数・来客のペースなど)の組み合わせは32,850あり、1つの条件を40回回した平均を、サイトのページに出しています。

使い回す結果から②を外した drop_p0.py

試算は、前の結果に同じ条件があれば回し直さない作りです。ところが条件の名前(h.key)には卓までの距離が入らないので、そのままでは②も回し直されません。②の行だけを外してから回しました。

"""前の results.json から②(fam_p0)の行を外して、使い回す結果 results_reuse.json を作る(2026-10-29)。
②は卓までの距離(dist_list)が変わったが、条件の名前(h.key)には距離が入らないので、外さないと計算し直されない"""
import json
R = json.load(open('results.json'))
R2 = dict(R); R2['rows'] = [r for r in R['rows'] if r['store'] != 'fam_p0']
print(len(R['rows']), len(R2['rows']), set(R.keys()))
json.dump(R2, open('results_reuse.json', 'w'), ensure_ascii=False)

drop_p0.py の出力

32850 30222 {'robots', 'trays', 'modes', 'rows', 'reps', 'fragile_by', 'common', 'robots_all', 'stores'}

条件を番号で分けて回す 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)

run_shard.py の実行ログ(Claude のクラウド・2コア・約20分)

python3 run_shard.py 0 1 results_new.json results_reuse.json
shard 0/1: 32850 conditions, 2628 to run
  100/2628
  200/2628
  300/2628
  400/2628
  500/2628
  600/2628
  700/2628
  800/2628
  900/2628
  1000/2628
  1100/2628
  1200/2628
  1300/2628
  1400/2628
  1500/2628
  1600/2628
  1700/2628
  1800/2628
  1900/2628
  2000/2628
  2100/2628
  2200/2628
  2300/2628
  2400/2628
  2500/2628
  2600/2628
done

結果をまとめる merge_p0.py

"""run_shard.py の出力(results_new.json)を、前の results.json と同じ形(条件の並び・stores など)にまとめる(2026-10-29)。
使い方: python3 merge_p0.py   (haizen_sim.py と同じフォルダで)→ results_p0fix.json"""
import json, haizen_sim as h
R = json.load(open('results.json')); N = json.load(open('results_new.json'))
conds = list(h.conditions()); nk = {h.key(r): r for r in N['rows']}
assert len(nk) == len(conds) == len(R['rows'])
out = dict(R); out['rows'] = [nk[h.key(c)] for c in conds]
out['stores'] = json.loads(json.dumps(h.STORES))
assert out['common'] == N['common'] and out['reps'] == N['reps']
old = {h.key(r): r for r in R['rows']}
ch = {}
for r in out['rows']:
    if old[h.key(r)] != r:
        ch[r['store']] = ch.get(r['store'], 0) + 1
print('changed', ch)
json.dump(out, open('results_p0fix.json', 'w'), ensure_ascii=False)

merge_p0.py の出力

changed {'fam_p0': 2628}

直す前と後の②の試算を比べた compare_p0_site.py

"""②の試算を、T06 を動かす前(results.json)と後(results_p0fix.json)で比べる(2026-10-29)
使い方: python3 compare_p0_site.py   (haizen_sim.py と同じフォルダで)"""
import json, haizen_sim as h
A = {h.key(r): r for r in json.load(open('results.json'))['rows']}
B = {h.key(r): r for r in json.load(open('results_p0fix.json'))['rows']}
ch = {}
for k in B:
    if A[k] != B[k]:
        ch[k[0]] = ch.get(k[0], 0) + 1
print(ch)
for st in (1, 2, 3):
    for rb in (0, 1, 2):
        c = {'store': 'fam_p0', 'staff': st, 'robots': rb, 'arrivals_per_h': 19, 'mode': 'serve', 'trays': 3 if rb else 1, 'fragile_by': 'any', 'registers': 1}
        a = A[h.key(c)]; b = B[h.key(c)]
        print(st, rb, ' '.join(f"{m}:{a[m]:.2f}→{b[m]:.2f}" for m in ('entry_wait_mean', 'staff_wait_mean', 'dish_wait_mean', 'pay_wait_mean', 'groups')))

compare_p0_site.py の出力

{'fam_p0': 2628}
1 0 entry_wait_mean:22.64→22.00 staff_wait_mean:19.71→19.26 dish_wait_mean:1.81→1.78 pay_wait_mean:3.93→3.79 groups:36.62→36.25
1 1 entry_wait_mean:15.37→14.97 staff_wait_mean:12.51→12.43 dish_wait_mean:0.76→0.77 pay_wait_mean:3.99→3.85 groups:38.42→37.88
1 2 entry_wait_mean:13.94→12.60 staff_wait_mean:11.96→11.13 dish_wait_mean:0.38→0.37 pay_wait_mean:4.16→4.27 groups:37.50→36.83
2 0 entry_wait_mean:2.68→2.65 staff_wait_mean:1.53→1.59 dish_wait_mean:0.60→0.61 pay_wait_mean:1.31→1.41 groups:37.42→38.10
2 1 entry_wait_mean:2.44→1.69 staff_wait_mean:0.83→0.71 dish_wait_mean:0.94→0.94 pay_wait_mean:1.30→1.30 groups:38.73→38.25
2 2 entry_wait_mean:1.60→1.17 staff_wait_mean:0.62→0.55 dish_wait_mean:0.40→0.39 pay_wait_mean:1.25→1.12 groups:37.48→36.40
3 0 entry_wait_mean:1.03→0.73 staff_wait_mean:0.28→0.25 dish_wait_mean:0.22→0.22 pay_wait_mean:0.54→0.53 groups:36.20→36.40
3 1 entry_wait_mean:1.38→1.24 staff_wait_mean:0.33→0.30 dish_wait_mean:0.94→0.92 pay_wait_mean:0.65→0.69 groups:39.27→38.12
3 2 entry_wait_mean:1.32→1.31 staff_wait_mean:0.25→0.28 dish_wait_mean:0.41→0.41 pay_wait_mean:0.66→0.66 groups:38.10→38.35

スタッフ2人・19組/時(Unity で回す条件)の入店待ちは、0台2.68→2.65分・1台2.44→1.69分・2台1.60→1.17分になりました。丸テーブルを寄せて上の段への道が短くなった分、ロボットがいる条件の入店待ちが縮んでいます。

ページの②だけを差し替える update_page_p0.py

"""②の店の形を直したとき(2026-10-29)、公開中の試算のページの②だけを差し替える。
使い方: python3 update_page_p0.py <公開中のページ> <新しい results.json> <前の p0 の店の形> <新しい p0 の店の形> <新しい summary.json> <出力するページ>
- const D の r(結果):②(fam_p0)の行だけが変わっていることを確かめてから、全部を新しい結果に入れ替える
- const D の stores.fam_p0:卓までの距離(dist_m・dist_mean)を新しい値に
- const D の plans.fam_p0:②は右の丸テーブル(T06)の位置が変わったので、平面図をまるごと作り直す
  (作り方は build_page_v23.py と同じ。前の店の形から作った平面図が、公開中のページと同じになることを先に確かめる)
- const D の layout_generation:新しい版の番号に
ほかの店・費用の欄・画面の文は変えない"""
import json, sys
import haizen_sim as h

live, res, old_lay, new_lay, summ, out = sys.argv[1:7]
s = open(live, encoding="utf-8").read()
N = json.load(open(res, encoding="utf-8"))
assert N["common"] == json.loads(json.dumps(h.COMMON))
lines = [l for l in s.split("\n") if l.startswith("const D = ")]
assert len(lines) == 1
D = json.loads(lines[0][len("const D = "):].rstrip(";"))
cond = D["k"][:8]
mets = D["k"][8:-1]
conds = list(h.conditions())
assert len(N["rows"]) == len(conds) == len(D["r"])
rows, changed = [], {}
for c, r, o in zip(conds, N["rows"], D["r"]):
    assert all(r[k] == c[k] for k in cond)
    row = [r[k] for k in cond] + [round(r[k], 3) for k in mets] + [1 if r["staff_wait_mean"] >= h.NG_MIN else 0]
    assert o[:8] == row[:8]
    if o != row:
        changed[c["store"]] = changed.get(c["store"], 0) + 1
    rows.append(row)
print("結果が変わった行:", changed)
assert set(changed) <= {"fam_p0"}, changed
D["r"] = rows

st = h.STORES["fam_p0"]
D["stores"]["fam_p0"]["dist_m"] = list(st["dist_m"])
D["stores"]["fam_p0"]["dist_mean"] = round(sum(st["dist_list"]) / len(st["dist_list"]), 1)
assert D["stores"]["fam_p0"]["tables"] == st["tables"] == 19
print("②の距離:", D["stores"]["fam_p0"]["dist_m"], D["stores"]["fam_p0"]["dist_mean"])

q = lambda a: [round(v, 2) for v in a]
def plan(path):
    L = json.load(open(path, encoding="utf-8"))["layout"]
    W, Dd = L["outer"]["w"], L["outer"]["d"]
    return json.loads(json.dumps({
        "w": W, "d": Dd,
        "outline": L.get("outline") or [[0, 0], [W, 0], [W, Dd], [0, Dd]],
        "rooms": [q(r["rect"]) + [r["kind"], r["label"]] for r in L["rooms"] if r["kind"] != "outside"],
        "walls": [q(w["rect"]) for w in L["walls"]],
        "tables": [q([t["x"], t["y"], t["x"] + t["w"], t["y"] + t["h"]]) + [t["seats"]] for t in L["tables"]],
        "dish": [q(d["rect"]) for d in L["dish_up"]],
        "standby": q(L["robot_standby"]["rect"]),
        "ent": [L["entrance"]["side"], L["entrance"]["a0"], L["entrance"]["a1"]],
    }))
assert plan(old_lay) == D["plans"]["fam_p0"], "前の店の形から作った平面図が公開中のページと違う"
D["plans"]["fam_p0"] = plan(new_lay)
print("②の平面図を作り直した(卓", len(D["plans"]["fam_p0"]["tables"]), ")")

gens = {g["generation"] for g in json.load(open(summ, encoding="utf-8"))}
assert len(gens) == 1
D["layout_generation"] = gens.pop()
s = s.replace(lines[0], "const D = " + json.dumps(D, ensure_ascii=False, separators=(",", ":")) + ";")
open(out, "w", encoding="utf-8").write(s)
print("書いた:", out, "版", D["layout_generation"])

ページの差し替えと、全画面の確かめの出力

python3 update_page_p0.py page_before.html results_p0fix.json p0_memory.json(直す前) p0_memory.json(直した後) summary.json page_check.html
node test_page_v28.js page_check.html
結果が変わった行: {'fam_p0': 2628}
②の距離: [2.5, 13.8] 8.5
②の平面図を作り直した(卓 19 )
書いた: page_check.html 版 08db8a0d067c
rendered 37800 / レジが詰まるが出た画面 7083 / 回らないが出た画面 5913 / 5台以上の注意 10800
公開中のページと同じ

Unity が読む条件を書き出す export_params.py(②を足した版・全文)

08_export_params.py.txt4,893字・93行・python
"""試算(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・fam_p2・fam_p3・fam_p4・fam_p0)の値。registers はレジの数(v2.8)
別のタブで開く保存

書き出した条件を、公開中のものと比べた compare_params.py

"""書き出し直した serving-robot-params.json(params_new.json)を、公開中のもの(引数)と比べる(2026-10-29)"""
import json, sys
a = json.load(open(sys.argv[1])); b = json.load(open('params_new.json'))
print('meta diff', [k for k in a['meta'] if a['meta'][k] != b['meta'][k]], b['meta']['layout_generation'])
print('common', a['common'] == b['common'], 'rules', a['unity_rules'] == b['unity_rules'])
print('stores diff', [k for k in b['stores'] if a['stores'].get(k) != b['stores'][k]])
print('conds diff idx', [i for i, (x, y) in enumerate(zip(a['unity_conditions'], b['unity_conditions'])) if x != y], len(b['unity_conditions']))
for c in b['unity_conditions'][15:]:
    r = c['site_result']; print(c['condition']['robots'], {m: round(r[m], 2) for m in ('entry_wait_mean', 'staff_wait_mean', 'dish_wait_mean', 'pay_wait_mean')})

compare_params.py の出力

meta diff ['layout_generation'] 08db8a0d067c
common True rules True
stores diff ['fam_p0']
conds diff idx [15, 16, 17] 18
0 {'entry_wait_mean': 2.65, 'staff_wait_mean': 1.59, 'dish_wait_mean': 0.61, 'pay_wait_mean': 1.41}
1 {'entry_wait_mean': 1.69, 'staff_wait_mean': 0.71, 'dish_wait_mean': 0.94, 'pay_wait_mean': 1.3}
2 {'entry_wait_mean': 1.17, 'staff_wait_mean': 0.55, 'dish_wait_mean': 0.39, 'pay_wait_mean': 1.12}

サイトへの反映は p2m-site の commit 07e09c7(29ファイル)です。公開後、params の版の番号が 08db8a0d067c・条件18件、ページの版の番号も 08db8a0d067c、Unity 用の JSON の T06 が x 6.85〜7.95 になったことを確かめました。

作業ログ ― 直した②を建て直し、合格したので続けて夜の30回を回した(依頼24)

1つの依頼の中で、建て直し・確かめの3回・夜の30回を続けて回すようにしました。確かめで丸テーブルのまわりの60秒の止まりが0でなければ、夜は回さずに止まって報告します。台帳は依頼の前に退避しました(634行・md5 738553fac6a9bd2732cf6ad19818f16b)。

依頼文:直した②を建てて確かめ、夜を回す(20261029_24)

09_sec05.md4,425字・93行・markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼:直した②(fam_p0)を建て直して確かめの3回を回し、合格なら続けて②の夜(30回)を回す(2026-10-29)

依頼 23 の報告(20261029_1732_p0_build_check.txt)の「決めてほしいこと」への答え(エイイチと決めた):
- 2:丸テーブル T05・T06 のあいだ → サイトで直した(下)。記憶の店は昔の記憶なので、ロボットが回れない所は変えてよい
別のタブで開く保存

報告:20261029_1847_p0fix_build_check.txt

10_sec05.txt9,784字・185行・text
====================================================================
直した②(fam_p0・T06 を右へ 1.0 m)を建て直して、1回ずつ回して確かめた
依頼:20261029_24_直した店2を建てて確かめ夜を回す.md
2026-10-29 18:47 check-in:cs:40 build_id cs40_20261029_1816(保留 0)
====================================================================

結論
別のタブで開く保存

報告:20261105_night_p0.txt

11_sec05.txt9,447字・177行・text
====================================================================
②(fam_p0・直した版 08db8a0d067c)の夜 30 回
依頼:20261029_24_直した店2を建てて確かめ夜を回す.md
夜の名前 20261105 回した日時 2026-10-29 18:20〜18:47 build_id cs40_20261029_1816(ServiceSim/cs:40・保留 0)
====================================================================

結論
別のタブで開く保存

依頼箱の結果:20261029_24_直した店2を建てて確かめ夜を回す_20261029_1811.md

# 結果:20261029_24_直した店2を建てて確かめ夜を回す

- 状態:完了
- 始めた時刻:2026-10-29 18:11
- 終わった時刻:2026-10-29 18:49(38分)
- 作業フォルダ:/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき:作業フォルダで `claude --resume 084522e2-491f-4016-8e85-eeafbed32da3`

## Claude Code の報告

やったこと
- 始める前の確かめを5つ行い、すべて合格でした。Unity エディタは起動していない(Hub だけ)、KIOXIA はマウント済みで鍵ファイルなし、台帳は 634 行で md5 は退避と同じ、保留 0(cs:39 head)、公開中の params は版 08db8a0d067c・条件 18 件でした。
- サイトから params と6店の形を curl で取り、cp でプロジェクトに写しました。版はすべて 08db8a0d067c でそろっています。③〜⑦は版の番号以外に違いがなく、params で違うのは stores.fam_p0 と②の3条件の試算の値だけでした。
- ②を cs:39 と同じコードで建てました。「② を建てて確かめる」と「② 確かめ A」は、どちらも画面なしのエディタで実行しました。コードは変えていません。そのあと check-in して cs:40 とし、画面なしの実行ファイル(build_id cs40_20261029_1816)を作り直しました。保留は 0 です。
- 確かめの3回(ラベル p0fixcheck)を回し、合格したので、続けて②の夜 30 回(夜の名前 20261105、記録は night/20261105_p0/)を回しました。作業フォルダの queue_p0.json は消してあります。

結果
- 建てた②:卓 19・席 88 が JSON・summary.json と一致しました。止まる位置 19 か所はすべて NavMesh の上に乗り、確かめ A は全卓と待機場所3つとも道が引けました。T06 を動かした分、地図の「ふだんの道」のマスが 342 増えました。
- 確かめの3回:3回とも completed・全員帰りました。2台の回で丸テーブルのまわりの「動けなくなった」は 0(前は 8)なので合格です。2台の回全体では、30 秒以上の止まりが 17 → 5、動けなくなったが 12 → 2、料理の待ちが 0.94 → 0.54 分(試算 0.39)になりました。
- 台帳:確かめで 634 → 637 行、夜で 637 → 667 行になりました。どちらも追記だけで、先頭の md5 はそれぞれ記録と同じです。
- 夜 30 回:すべて completed・全員帰りました。失敗・止まった回・やり直しは 0 で、最長の実時間は 3.8 分です。
- 夜の入店待ち(種10個の中央値):

| 台数 | Unity | 試算 |
|---|---|---|
| 0台 | 1.96 分 | 2.65 分 |
| 1台 | 1.69 分 | 1.69 分 |
| 2台 | 2.10 分 | 1.17 分 |

- 入れた組の数の中央値は 36.5 組で、試算の 0.95〜1.00 倍でした。確かめで見た 31 組は種0 だけの値です。種1〜4 は台数によらず、入店待ちが 5〜10 分と長くなりました。
- 2台の 30 秒以上の止まりは、種10個の合計で 192 回、うち 60 秒の「動けなくなった」が 146 回でした。場所ごとの内訳です。

| 場所 | 30 秒以上の止まり |
|---|---|
| 入口・レジのまわり | 56 |
| 待機場所の中 | 54 |
| 受け渡し口のまわり | 32 |
| そのほか(ほぼ全部が x 8〜10・y 6〜8 のマス) | 29 |
| 仕切りの右端のすき間 | 16 |
| 丸テーブルのまわり | 2 |

- ロボットを1台増やしたときの向きが、種の過半数で試算と逆になったものはありませんでした。ただし 1→2台では、入店待ち・料理の待ちとも逆が 4/10 でした。
- judge.md はできています。30 回とも「基準なし」です。
- 依頼 23 で書き出した screens の、時刻の入らない名前のファイル(20261029_p0_memory_json_check.txt など)は、今回の書き出しで上書きされました。依頼 23 の数字は、依頼 23 の報告に残っています。

変えたファイル
- cs:40 で check-in(コード・.asset・シーンは変えていません):
  - Assets/ServiceSim/Experiment/serving-robot-params.json
  - Assets/ServiceSim/Store/Layouts/ の summary.json と6店の JSON・地図(12 ファイル)
- 建てる確かめで変更ありになった NavMeshAreas.asset・Mat_RobotKeepout.mat・StoreLayout_p0_memory.asset は、cm undo で戻しました。
- 足したもの:
  - unity/site_copy_08db8a0d067c/(サイトの控え)
  - claude_code_reports/20261029_1847_p0fix_build_check.txt
  - claude_code_reports/20261105_night_p0.txt

決めてほしいこと
1. 2台の「動けなくなった」146 回のうち、どこを先に見るかを決めてください。多いのは次の場所です。調べるか、サイトで店の形を直すか、このまま記録にするかも合わせて決めてください。
   - 入口・レジのまわり 48(種1〜4 で、人が立ち止まっていた)
   - 待機場所の中 37
   - 受け渡し口のまわり 24
   - 受け渡し口の左 x 8〜10・y 6〜8 が 23
2. 種1〜4 は台数によらず入店待ちが 5〜10 分と長くなります。これを調べるかどうかを決めてください。
3. 1→2台で、試算と同じ向きの種が過半数に届いていません(逆 4/10)。印にはならない数ですが、扱いをどうするか決めてください。
4. StoreLayout_p0_memory.asset の「写した日時」の字が、前の版(dffc100b269c)のままです。中身の JSON は新しい版です。今後、写し直すたびにこの字も直すかどうかを決めてください。

1台は試算とぴったり同じで、2台は1台より待ちが延びた

確かめの2台の回では、丸テーブルのまわりの60秒の止まりが8回から0回になり、30秒以上の止まりも17回から5回に減りました。合格の条件を満たしたので、Claude Code はそのまま夜の30回に進みました。18:20に始まり18:47に終わっています。

夜の入店待ちの中央値は、0台1.96分・1台1.69分・2台2.10分です。試算は2.65分・1.69分・1.17分で、1台は小数第2位まで同じでした。2台は、試算では1台より縮むのに、3Dの店では延びています。種ごとに見ると、1台から2台で入店待ちが試算と同じ向き(縮む)になった種は10個のうち2個、逆(延びる)が4個、変わらないが4個でした。

来る客の数は種ごとにばらつき(26〜48組)、中央値は36.5組で試算(36〜38組)とそろいました。確かめで31組と少なく見えたのは、種0だけのことです。

2台の止まりは、待合のまわり・待機場所の中・受け渡し口のまわりに集まった

直した②の図面に、ロボット2台の夜(種10個)の30秒以上の止まり192回を場所ごとに色を分けて重ねた図。入口の右の待合のまわり56回、待機場所の中54回、受け渡し口から2.5m以内32回、パントリーの壁ぞいの仕切りの右端16回
場所30秒以上の止まりそのうち60秒動けなくなったおもな理由
入口・レジのまわり(待合の前)
5648人が立ち止まっていた 35
待機場所の中5437もう1台のロボット 54
受け渡し口から2.5m以内3224もう1台のロボット 19
そのほか(受け渡し口の左、パントリーの壁ぞい)
2923もう1台のロボット 22
仕切りの右端のすき間1610もう1台のロボット 11
丸テーブルのまわり22もう1台のロボット 2
ドリンクバーのまわり・奥の区画のそのほか
32もう1台のロボット 3

いちばん多かった入口・レジのまわりは、x 13〜14m・y 1.1〜3.0m の、レジの右から右下の卓(T16)へ向かう通路です。待合(x 12.7〜13.9m・y 0〜0.5m)のすぐ上にあたり、案内を待つお客さんが立ち止まる所と、ロボットが右下の卓へ向かう道が重なっています。お客さんが多かった種1〜4だけで出ていて、同じ点で60秒を何度も続けていました。

受け渡し口の左(x 8.3〜8.9m・y 6.3〜8.0m)の29回は、パントリーの壁ぞいで、スタッフが待つ点(x 9.25m・y 6.3〜9.4m)の並びのすぐ横です。奥の区画へ行くロボットの道もここを通ります。

止まりを書き出したスクリプト p0_night_stops_dump.py(KIOXIA の r2r2r-data/service_sim/unity で実行)

"""②の夜(20261105_p0)のロボット2台・種0〜9の回から、30秒以上の止まりを [x, y, 理由, どう抜けたか, 秒, 種] で書き出す(図にするため)。
使い方: KIOXIA の r2r2r-data/service_sim/unity で python3 p0_night_stops_dump.py <出力.json>"""
import csv, glob, json, sys
pts = []
for d in sorted(glob.glob("20261105_night_cs40_fam_p0_r2_s*_passon_sbret")):
    for r in csv.DictReader(open(d + "/stops.csv")):
        if float(r["duration_s"]) >= 30:
            pts.append([round(float(r["x"]), 2), round(float(r["z"]), 2), r["cause"], r["resolved"], round(float(r["duration_s"]), 1), d.split("_s")[-1].split("_")[0]])
json.dump(pts, open(sys.argv[1], "w"), ensure_ascii=False)
print(len(pts))

p0_night_stops_dump.py の出力

192

止まりを図面に重ねたスクリプト draw_night_stops.py

12_draw_night_stops.py.txt3,505字・56行・python
"""②の夜(20261105_p0)のロボット2台・種10個の30秒以上の止まり192回を、直した②の図面(版 08db8a0d067c)に重ねる。
使い方: python3 draw_night_stops.py <unity/p0_memory.json> <p0_night_r2_stops.json> <出力.png>"""
import json, sys, math
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle
from matplotlib import font_manager as fm
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
別のタブで開く保存

作業ログ ― 6店の夜を並べると、待機場所のまわりはどの店でも詰まっていた

②で、用意した6店(③⑦④⑤⑥②)をすべて Unity で回し終えました。どの店も、ロボット0・1・2台 × 種0〜9 を今の標準(すれ違いあり・往復を終えたら待機場所へ戻る)で回しています。店ごとの報告を見比べるのではなく、6店の回の記録(stops.csv・summary.json)から同じ数え方で並べ直しました。④だけは、待機場所へ戻る作りを試した夜(20261031)の回を使っています。

6店の回をまとめたスクリプト six_store_stops.py(KIOXIA の r2r2r-data/service_sim/unity で実行)

"""Unity で建てた6店の夜(今の標準:すれ違い入・待機場所へ戻る 入・案B 切)の、ロボット0・1・2台の回を店ごとにまとめる(2026-10-29)。
30秒以上の止まりの数・そのうち待機場所の四角の中・受け渡し口の点から2.5m以内(待機場所を除く)・60秒の「動けなくなった」と、
入店待ちの種10個の中央値と試算(params の site_result)を並べる。待機場所と受け渡し口の点は、公開中の店の形(unity/*.json)の値。
使い方: KIOXIA の r2r2r-data/service_sim/unity で python3 six_store_stops.py <出力.json>"""
import csv, glob, json, math, statistics, sys

G = {"fam_p0": {"sb": [12.2, 5.1, 14.6, 5.9], "fp": [10.9, 5.45]}, "fam_p1": {"sb": [16.5, 5.65, 18.9, 6.45], "fp": [15.3, 5.95]},
     "fam_p2": {"sb": [12.0, 4.15, 13.6, 4.95], "fp": [11.05, 4.45]}, "fam_p3": {"sb": [14.2, 8.55, 16.6, 9.35], "fp": [12.5, 8.85]},
     "fam_p4": {"sb": [15.5, 5.65, 17.9, 6.45], "fp": [14.3, 5.95]}, "fam_p5": {"sb": [16.5, 5.65, 18.9, 6.45], "fp": [15.3, 5.95]}}
# 店ごとに、今の標準で種10個を回した夜(④は待機場所へ戻るを試した夜 20261031 の B)
NIGHTS = {"fam_p1": "20261102_night_cs34_fam_p1", "fam_p5": "20261102_night_cs34_fam_p5", "fam_p2": "20261031_p2_cs33_fam_p2",
          "fam_p3": "20261103_night_cs36_fam_p3", "fam_p4": "20261104_night_cs38_fam_p4", "fam_p0": "20261105_night_cs40_fam_p0"}
res = {}
for st, pre in NIGHTS.items():
    sb, fp = G[st]["sb"], G[st]["fp"]
    for rb in (0, 1, 2):
        dirs = sorted(d for d in glob.glob(f"{pre}_r{rb}_s*_passon_sbret") if "ruleb" not in d)
        if not dirs:
            continue
        tot = insb = nearfp = stuck = 0
        ew = []
        for d in dirs:
            with open(d + "/stops.csv") as f:
                for r in csv.DictReader(f):
                    if float(r["duration_s"]) < 30:
                        continue
                    x, z = float(r["x"]), float(r["z"])
                    tot += 1
                    if r["resolved"] == "動けなくなった":
                        stuck += 1
                    if sb[0] <= x <= sb[2] and sb[1] <= z <= sb[3]:
                        insb += 1
                    elif math.hypot(x - fp[0], z - fp[1]) <= 2.5:
                        nearfp += 1
            s = json.load(open(d + "/summary.json"))
            for key in ("sim_like", "site_metrics"):
                if isinstance(s.get(key), dict) and "entry_wait_mean" in s[key]:
                    ew.append(s[key]["entry_wait_mean"])
                    break
        site = json.load(open(dirs[0] + "/summary.json")).get("site_result", {}).get("entry_wait_mean")
        res[f"{st}_r{rb}"] = dict(n=len(dirs), stops=tot, standby=insb, nearpass=nearfp, stuck=stuck,
                                  entry_med=round(statistics.median(ew), 2) if ew else None, site=site)
        print(st, rb, res[f"{st}_r{rb}"])
json.dump(res, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)

six_store_stops.py の出力

fam_p1 0 {'n': 10, 'stops': 0, 'standby': 0, 'nearpass': 0, 'stuck': 0, 'entry_med': 4.09, 'site': 3.301}
fam_p1 1 {'n': 10, 'stops': 88, 'standby': 0, 'nearpass': 2, 'stuck': 54, 'entry_med': 5.0, 'site': 2.5992}
fam_p1 2 {'n': 10, 'stops': 230, 'standby': 41, 'nearpass': 46, 'stuck': 156, 'entry_med': 3.99, 'site': 1.1788}
fam_p5 0 {'n': 10, 'stops': 0, 'standby': 0, 'nearpass': 0, 'stuck': 0, 'entry_med': 4.49, 'site': 2.7562}
fam_p5 1 {'n': 10, 'stops': 86, 'standby': 0, 'nearpass': 3, 'stuck': 53, 'entry_med': 3.22, 'site': 1.815}
fam_p5 2 {'n': 10, 'stops': 247, 'standby': 31, 'nearpass': 45, 'stuck': 157, 'entry_med': 3.56, 'site': 1.9669}
fam_p2 1 {'n': 10, 'stops': 242, 'standby': 2, 'nearpass': 18, 'stuck': 175, 'entry_med': 14.4, 'site': 2.9349}
fam_p2 2 {'n': 10, 'stops': 759, 'standby': 107, 'nearpass': 82, 'stuck': 610, 'entry_med': 24.79, 'site': 3.4597}
fam_p3 0 {'n': 10, 'stops': 0, 'standby': 0, 'nearpass': 0, 'stuck': 0, 'entry_med': 8.14, 'site': 6.2179}
fam_p3 1 {'n': 10, 'stops': 42, 'standby': 5, 'nearpass': 2, 'stuck': 19, 'entry_med': 6.6, 'site': 3.4227}
fam_p3 2 {'n': 10, 'stops': 184, 'standby': 51, 'nearpass': 5, 'stuck': 129, 'entry_med': 5.46, 'site': 3.6696}
fam_p4 0 {'n': 10, 'stops': 0, 'standby': 0, 'nearpass': 0, 'stuck': 0, 'entry_med': 9.6, 'site': 6.9454}
fam_p4 1 {'n': 10, 'stops': 33, 'standby': 0, 'nearpass': 2, 'stuck': 18, 'entry_med': 6.04, 'site': 3.116}
fam_p4 2 {'n': 10, 'stops': 122, 'standby': 60, 'nearpass': 9, 'stuck': 76, 'entry_med': 4.83, 'site': 1.8939}
fam_p0 0 {'n': 10, 'stops': 0, 'standby': 0, 'nearpass': 0, 'stuck': 0, 'entry_med': 1.96, 'site': 2.6471}
fam_p0 1 {'n': 10, 'stops': 46, 'standby': 0, 'nearpass': 7, 'stuck': 36, 'entry_med': 1.69, 'site': 1.6909}
fam_p0 2 {'n': 10, 'stops': 192, 'standby': 54, 'nearpass': 32, 'stuck': 146, 'entry_med': 2.1, 'site': 1.169}

ロボット2台の30秒以上の止まりは、6店とも待機場所と受け渡し口のまわりに25〜57%

店卓
30秒以上の止まり(2台・種10個)
待機場所の中受け渡し口から2.5m以内合わせた割合
入店待ち 1台→2台(Unity)
同(試算)
③42230414638%5.00→3.992.60→1.18
⑦42247314531%3.22→3.561.82→1.97
④287591078225%14.40→24.792.93→3.46
⑤4618451530%6.60→5.463.42→3.67
⑥3812260957%6.04→4.833.12→1.89
②19192543245%1.69→2.101.69→1.17

6店とも、待機場所は受け渡し口のすぐ横(受け渡し口の点から待機場所の端まで0.95〜1.7m)にあり、往復を終えて戻るロボットと、待機場所から受け渡し口へ出るロボットが、ここで出会います。④⑤⑥では、店ごとにドリンクバー・レジ・卓の位置を直しました。直した所の止まりは減りましたが、待機場所のまわりの止まりは、どの店にも残っています。

1台から2台にしたときの入店待ちの向きは、③⑦④⑥では試算と同じでした。逆になったのは⑤(Unity は縮み、試算は延びる)と②(Unity は延び、試算は縮む)です。小さい店の④(28卓)と②(19卓)は、Unity では2台目で入店待ちが延びています。

作業ログ ― ②で区切り、直す先を店の形の決まりへ移す

②もここで区切りにしました。残った詰まりは店ごとに直さず、記録して、店の形を作る決まりのほうで考えます。直すなら、これまでどおりサイトの店の形を先に直し、Unity はそれに合わせます。

決めたこと・残したこと中身
②の椅子
角テーブル・丸テーブルは2脚ずつ向かい合わせ。椅子はロボットが止まる辺を含まない2辺に置く(壁ぎわで座れない卓だけ例外)。長テーブルは上下にスツール3つずつ
②の店の形
右の丸テーブル T06 を右へ1.0m(間 1.1m → 2.1m)。版 08db8a0d067c
②の試算
②の2,628条件だけ計算し直した(v2.8 のまま)
Unity で回す店
③⑦④⑤⑥②の18条件(serving-robot-params.json)
Unity の作り
丸い天板とスツールを読めるように、部品の読み方を店に関係なく広げた(cs:39)。裏の部屋は名前を知らなくても通れない箱として建てる
記録だけにしたもの
待合のまわりの止まり(お客さんが多い種だけ)、受け渡し口の左のパントリーの壁ぞいの止まり、種1〜4で入店待ちが5〜10分と長いこと、1台→2台で試算と逆の向きの種が4個あること、T19 の右の席に着く点、レジの内側の幅 0.52m
6店に共通の詰まり
待機場所が受け渡し口のすぐ横にあり、戻るロボットと出るロボットが出会う。店ごとの直しではなく、待機場所・待合・スタッフが待つ点の置き方を、店の形を作る決まりとして考え直す