サービスロボット導入シミュレーション Log#20 | 38卓の店でも、詰まるのは片付けだった

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

38卓・158席のファミレスを Unity の3Dで再現したシミュレーションでも、配膳ロボットを足すと入店待ちは縮みましたが、詰まるのは会計のレジではなく、片付けと案内でした。ロボットは0台・1台・2台で走らせました。お客さんが店に来てから席に着くまでの待ちは、9.6分、6.0分、4.8分と縮みました。ロボットを足すと待ちが縮むという向きは、サイトに公開している試算と同じです。ところが、店が詰まる場所は今回も試算と違いました。試算ではレジの順番待ちで詰まり、3Dの店では「席は空いているのに、片付けと案内が追いつかずに待つ」ところで詰まりました。

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

今回の店(⑥ゆったり型・入口が左の壁)は、3Dの店に建てる前に図面を見て、2か所を直しました。1つはドリンクバーです。右側の卓へ行くロボットの道が、お客さんがドリンクを取るために立つ所を通っていたので、ドリンクバーを左へ0.8m寄せ、右の端に2.0mの抜け道を作りました。もう1つはレジです。レジの向きは縦のまま右へ0.8m、下の壁まで寄せ、入口の前に場所を空けました。直したあとの30回では、ロボット2台のときにロボットが30秒以上止まった122回のうち、ドリンクバーのまわりは5回、入口・レジのまわりは1回でした。

残った詰まりのうちいちばん多かったのは、ロボットの待機場所の中の60回で、ほぼ全部がロボットどうしです。待機場所が受け渡し口のすぐ横にある形は、これまでの5店すべてに共通していて、どの店でも同じ所で詰まっています。 店ごとに図面を直しても消えない詰まりなので、店の形の決め方の問題として記録しました。

やったことなぜ結果
⑥の図面にロボットの道を重ねて見た
⑤では建てたあとに図面の問題に気づき、2回建て直したため
気になる所を3つ挙げた
ドリンクバーを左へ0.8m寄せた
右側の卓への道が、ドリンクを取る人の立つ所を通っていた
右の端の抜け道 1.2m→2.0m
レジを縦のまま右へ0.8m・下の壁まで寄せた
入口の前に、案内を待つ人の場所を空けるため
レジの上と左の島の間 2.0m→2.7m
⑥の試算だけ計算し直した
卓までの距離が4卓で変わったため
数字の動きは乱数のぶれの大きさ
3Dの店に⑥を建てて1回ずつ回した
建てられるか、止まる所はどこかを見るため
建て直しなしで建った
⑥を種10個ずつ、計30回回した
1回ずつでは来る客の数がぶれるため
入れた組の数は試算とそろった

3Dの店で回した結果は次のとおりです(種10個の中央値。右は試算)。

ロボット0台1台2台
入店待ち(分)9.6 / 7.06.0 / 3.14.8 / 1.9
そのうち、席は空いているのに待った(分)
8.4 / 4.24.8 / 1.03.5 / 0.6
会計待ち(分)3.0 / 12.13.0 / 8.53.3 / 7.0
料理の待ち(分)1.1 / 0.82.1 / 2.01.3 / 0.8
ロボットが運んだ皿の割合−0.83 / 0.800.96 / 1.00

試算では、食べ終えたお客さんはレジの順番が来るまで卓に座ったまま待ちます。レジが1つだと会計待ちが7〜12分に延び、その分だけ卓が空かず、次のお客さんが待ちます。3Dの店では会計待ちは3分前後でレジは詰まらず、代わりに「席は空いているのに待った」時間が試算の2〜6倍になりました。3Dの店のスタッフは、片付け・案内・会計・料理運びを1人ずつ順番にこなし、店の中を歩きます。その分、片付けが後回しになっています。⑤(46卓)でも③⑦(42卓)でも同じ形のずれが出ていて、店の大きさが変わっても、試算=レジ、3Dの店=片付けと案内、という分かれ方は変わりませんでした。

この記事の後半は作業ログです。図面を見て気になる所を挙げたところから、ドリンクバーとレジを動かす案の比べ、試算の計算し直し、3Dの店での1回ずつの確かめと30回の結果までを、コード・依頼文・報告・出力とあわせて載せます。

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

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

作業ログ ― ⑥の図面に、受け渡し口から38卓へのロボットの道を重ねた

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

ファイルの一覧(9本)

01_draw_mark.py.txt ― 気になる所に印を付けたスクリプト draw_mark.py・3,096字

02_sec02.py.txt ― 並ぶ点が置けないときは、ドリンクバーの前の幅の中で左右へずらす / 場所の点を作る関数 unity_export.py の places(直した版)・5,415字

03_6_famires_v3.py.txt ― 6店の店の形を作り直した famires_v3.py の出力 / 確かめの仕組みを確かめた test_famires_v3.py の出力・5,617字

04_export_params.py.txt ― Unity が読む条件を書き出すスクリプト export_params.py(⑥を足した版)・4,814字

05_Unity_export_params.py.txt ― Unity が読む条件を書き出すスクリプト export_params.py(⑥を足した版) / ページの差し替え・条件の書き出し・ページの確かめの出力・5,156字

06_sec05.md ― 依頼文:⑥を建てて確かめる(20261028_21)・3,524字

07_sec05.txt ― 報告:20261028_1935_p4_build_check.txt・11,610字

08_runner_log.txt ― 夜の実行ログ runner_log.txt・8,544字

09_sec06.txt ― 報告:20261104_night_p4.txt・4,010字

⑤(46卓)では、3Dの店に建てて1回目の確かめを待っている間に図面の問題に気づき、店の形を直して2回建て直しました。⑥は、その前に図面を見ることにしました。

この記事で書く効果は、すべて3Dの店で測った値です。直す前の形では3Dの店を回していないので、前後の比べはありません。直したあとの値だけを書きます。

⑥は24m×17mの店で、卓38・席158です。受け渡し口(料理が出てくる所)はパントリーの下の端にあり、ロボットは受け渡し口の前の1点から38卓へ出ていきます。受け渡し口の右にロボット3台の待機場所があります。入口は左の壁で、入口の右にレジ、左下に待合があります。

直す前の⑥の図面と、受け渡し口から38卓へのロボットの道。赤い枠は、図面から気になった3か所(ドリンクバーのまわり、受け渡し口と待機場所、入口・レジのすぐ上)

図面から気になったのは次の3か所です。

場所図面で分かったこと
①ドリンクバーのまわり
ドリンクバーの上は、お客さんがドリンクを取るために立つ・並ぶ所。右側の卓(T30・T38)へ行く道がそこを通る。下の通路は1.6mで、T27〜T29 にロボットが止まると上下に0.55mずつしか残らない
②受け渡し口と待機場所
受け渡し口の前から待機場所まで1.2m。③⑦④と同じ形
③入口・レジのすぐ上
左の通路(T05〜T13)の入口が、入口とレジのすぐ上にある。38卓中7卓の道がここを通る(⑤は直したあとで46卓中10卓)

⑤のときのような、受け渡し口から抜けられない隙間はありませんでした。左の2本の通路(1.5m・両側に席)は、⑤と同じ造りです。

図面とロボットの道を描いたスクリプト draw.py

import json, sys, matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt, matplotlib.patches as P
from matplotlib import font_manager as fm
fp=fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
B='/home/claude/p2m-site/public/sim/layouts/'
L=json.load(open(B+'p4_relaxed_side.json'))['layout']
U=json.load(open(B+'unity/p4_relaxed_side.json'))
W,D=L['outer']['w'],L['outer']['d']
fig,ax=plt.subplots(figsize=(14,10.5))
ax.add_patch(P.Rectangle((0,0),W,D,fill=False,lw=2))
e=L['entrance']; ax.plot([0,0],[e['a0'],e['a1']],color='w',lw=4); ax.text(-0.3,(e['a0']+e['a1'])/2,'入口',fontproperties=fp,ha='right',va='center',fontsize=10)
for r in L['rooms']:
    x0,y0,x1,y1=r['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc={'back':'#e4e4e0','guest':'#f1e6d6','drink':'#d8e8f4'}[r['kind']],ec='#999'))
    ax.text((x0+x1)/2,(y0+y1)/2,r['label'],ha='center',va='center',fontproperties=fp,fontsize=8)
for w in L['walls']:
    x0,y0,x1,y1=w['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='#555',ec='none'))
for k in U['robot_keepout']:
    x0,y0,x1,y1=k['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='none',ec='#c33',ls=':',lw=1.2))
for t in L['tables']:
    ax.add_patch(P.Rectangle((t['x'],t['y']),t['w'],t['h'],fc='w',ec='k',lw=.8))
    ax.text(t['x']+t['w']/2,t['y']+t['h']/2,t['id'][1:]+'\n'+str(t['seats'])+'人',ha='center',va='center',fontsize=6,fontproperties=fp)
d=L['dish_up'][0]['rect']; ax.add_patch(P.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fc='#333'))
s=L['robot_standby']['rect']; ax.add_patch(P.Rectangle((s[0],s[1]),s[2]-s[0],s[3]-s[1],fc='none',ec='#2a5cc8',ls='--',lw=1.5)); ax.text((s[0]+s[2])/2,s[1]-0.25,'ロボット待機(3台)',ha='center',fontproperties=fp,fontsize=8,color='#2a5cc8')
st=L['start']; ax.plot(*st,'o',color='#2a5cc8',ms=7); ax.text(st[0],st[1]-0.45,'受け渡し口の前',ha='center',fontproperties=fp,fontsize=8)
if len(sys.argv)>1:
    for k,v in U['routes']['from_pass'].items():
        xs,ys=zip(*v['points']); ax.plot(xs,ys,color='#2a5cc8',alpha=.35,lw=1.2)
for p in U['places']:
    if p['kind'] in ('StaffStandby',): ax.plot(*p['point'],'s',color='#2a7a2a',ms=4)
ax.set_xlim(-1.5,W+.5); ax.set_ylim(-.8,D+.5); ax.set_aspect('equal')
ax.set_xticks(range(0,int(W)+1,1)); ax.set_yticks(range(0,int(D)+1,1)); ax.grid(alpha=.15); ax.tick_params(labelsize=7)
ax.set_title(L['name']+('(青線:受け渡し口から各卓へのロボットの道)' if len(sys.argv)>1 else ''),fontproperties=fp,fontsize=12)
plt.tight_layout(); plt.savefig(sys.argv[1] if len(sys.argv)>1 else 'p4_plan.png',dpi=120)

気になる所に印を付けたスクリプト draw_mark.py

draw.py の中身を読み込み、図の保存の直前に赤い枠と説明を足しています。

01_draw_mark.py.txt3,096字・41行・python
import json, sys, matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt, matplotlib.patches as P
from matplotlib import font_manager as fm
fp=fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
B='/home/claude/p2m-site/public/sim/layouts/'
L=json.load(open(B+'p4_relaxed_side.json'))['layout']
U=json.load(open(B+'unity/p4_relaxed_side.json'))
W,D=L['outer']['w'],L['outer']['d']
別のタブで開く保存

気になる所を数えたスクリプト check.py

道が入口・レジのまわりとドリンクバーの上下を通るかを、道の線分を100に分けて調べました。

import json
B='/home/claude/p2m-site/public/sim/layouts/'
U=json.load(open(B+'unity/p4_relaxed_side.json'))
fp=U['routes']['from_pass']
def through(pts,box):
    x0,y0,x1,y1=box
    for p,q in zip(pts,pts[1:]):
        for i in range(101):
            x=p[0]+(q[0]-p[0])*i/100; y=p[1]+(q[1]-p[1])*i/100
            if x0<=x<=x1 and y0<=y<=y1: return True
    return False
zones={'入口・レジのまわり(x<4.6,y<5.0)':(0,0,4.6,5.0),
 'ドリンクバーの上(客が立つ・並ぶ帯 x15.6-20.4,y4.1-5.65)':(15.6,4.1,20.4,5.65),
 'ドリンクバーの下(x15.6-20.4,y1.8-3.4)':(15.6,1.8,20.4,3.4)}
for n,b in zones.items():
    h=[k for k,v in fp.items() if through(v['points'],b)]
    print(n,len(h),h)
# 細い通路に止まったときの残り幅
T={t['id']:t for t in U['tables']}
for a in U['aisles']:
    x0,y0,x1,y1=a['rect']; ts=[k for k,t in T.items() if t.get('aisle')==a['id']]
    seats=sum(T[k]['seats'] for k in ts)
    xs=[T[k]['stop'][0] for k in ts]
    rem=[max(x0-0, (x1-(x+0.25)), ((x-0.25)-x0)) for x in xs]
    print(a['id'],'幅',a['width'],'卓',len(ts),'席',seats,'止まる位置x',sorted(set(xs)),'止まったときの横の残り(最大)',round(max(rem),2),'最小',round(min(rem),2))
# ドリンクバーの下で止まったとき
for k in ('T27','T28','T29'):
    y=T[k]['stop'][1]; print(k,'止まると上の残り',round(3.4-(y+0.25),2),'下の残り',round((y-0.25)-1.8,2))
print('出発点から待機場所の左端', 15.5-14.3)
tot=sum(t['seats'] for t in U['tables']); print('卓',len(T),'席',tot)

check.py の出力

入口・レジのまわり(x<4.6,y<5.0) 7 ['T05', 'T06', 'T07', 'T08', 'T10', 'T11', 'T12']
ドリンクバーの上(客が立つ・並ぶ帯 x15.6-20.4,y4.1-5.65) 2 ['T30', 'T38']
ドリンクバーの下(x15.6-20.4,y1.8-3.4) 3 ['T27', 'T28', 'T29']
A01 幅 1.5 卓 10 席 42 止まる位置x [1.988, 2.212] 止まったときの横の残り(最大) 1.2 最小 1.2
A02 幅 1.5 卓 8 席 32 止まる位置x [6.038, 6.262] 止まったときの横の残り(最大) 5.25 最小 5.25
T27 止まると上の残り 0.56 下の残り 0.54
T28 止まると上の残り 0.56 下の残り 0.54
T29 止まると上の残り 0.56 下の残り 0.54
出発点から待機場所の左端 1.1999999999999993
卓 38 席 158

細い通路に止まったときの横の残りは、計算の式が間違っていました(max(x0-0, ...) で、通路の左の端の座標そのものを残りとして数えていた)。左右それぞれの残りを出し直したのが次のスクリプトです。

細い通路の横の残りを出し直したスクリプト aisle_rem.py

import json
U=json.load(open('/home/claude/p2m-site/public/sim/layouts/unity/p4_relaxed_side.json'))
T={t['id']:t for t in U['tables']}
for a in U['aisles']:
    x0,y0,x1,y1=a['rect']
    for x in sorted(set(T[k]['stop'][0] for k in T if T[k].get('aisle')==a['id'])):
        print(a['id'],x,'左の残り',round(x-0.25-x0,2),'右の残り',round(x1-x-0.25,2))
# ⑤ の同じ数字(比べ)
U5=json.load(open('/home/claude/p2m-site/public/sim/layouts/unity/p3_relaxed_center.json'))
T5={t['id']:t for t in U5['tables']}
for a in U5['aisles']:
    ts=[k for k in T5 if T5[k].get('aisle')==a['id']]; print('⑤',a['id'],a['width'],len(ts),'卓', a['rect'])
for a in json.load(open('/home/claude/p2m-site/public/sim/layouts/unity/p1_efficient_corner.json'))['aisles']: print('③',a['id'],a['width'],a['rect'])

aisle_rem.py の出力

A01 1.988 左の残り 0.54 右の残り 0.46
A01 2.212 左の残り 0.76 右の残り 0.24
A02 6.038 左の残り 0.54 右の残り 0.46
A02 6.262 左の残り 0.76 右の残り 0.24
⑤ A01 1.5 11 卓 [1.2, 1.4, 2.7, 12.65]
⑤ A02 1.5 7 卓 [5.25, 3.4, 6.75, 12.65]
⑤ A03 1.5 13 卓 [23.3, 1.4, 24.8, 16.2]
③ A01 1.2 [1.2, 4.6, 2.4, 12.2]
③ A02 1.2 [4.9, 4.8, 6.1, 14.05]
③ A03 1.2 [8.6, 4.8, 9.8, 14.05]

左の2本の通路(A01・A02)は、ロボットが1台止まると、横の残りは0.24〜0.76mで、ほかのロボットは抜けられません。ただ、⑤の3本の通路も同じ1.5mで、⑤は夜の30回を最後まで回せています。⑥だけの問題ではないので、ここは直していません。

作業ログ ― ドリンクバーを左へ0.8m寄せ、右の端に抜け道を作った

図面を見て、まずドリンクバーを左へ寄せる直しから始めることにしました。寄せる幅を決めるために、店の形の作り方のコード(サイトの famires_v3.py)をそのまま使い、ドリンクバーだけを動かした形で、決まりの確かめと卓までの距離の計算を回しました。

寄せる幅を変えて、決まりと道を確かめたスクリプト try_dx.py

"""⑥のドリンクバーを左へ dx m 寄せた形を、サイトの決まりと解析で確かめる(書き出しは作業用フォルダだけ)"""
import sys, json, copy
sys.path.insert(0, '/home/claude/p2m-site/tools/sim_layouts')
from pathlib import Path
import famires_v3 as F, famires_gen2 as G
def through(pts, box):
    x0,y0,x1,y1=box
    for p,q in zip(pts,pts[1:]):
        for i in range(101):
            x=p[0]+(q[0]-p[0])*i/100; y=p[1]+(q[1]-p[1])*i/100
            if x0<=x<=x1 and y0<=y<=y1: return True
    return False
for dx in [float(a) for a in sys.argv[1:]]:
    L = G.p4_relaxed_side()
    for r in L['rooms']:
        if r['kind']=='drink': r['rect']=[round(r['rect'][0]-dx,3),r['rect'][1],round(r['rect'][2]-dx,3),r['rect'][3]]; db=r['rect']
    L = F.prepare(L)
    v = F.check_rules(L)
    if v: print('dx',dx,'決まり違反',v); continue
    ctx={}; ra,ea=F.analyze(L,F.SETTINGS['res'],ctx); rb,eb=F.analyze(L,F.SETTINGS['check_res'])
    if ea or eb: print('dx',dx,'解析の違反',ea,eb); continue
    summ=F.summarize(L,ra)
    out=Path(f'out_dx{dx}'); out.mkdir(exist_ok=True)
    U=F.export_unity(L,ra,ctx,'p4',out,'try')
    vr=F.check_register_clear(U)
    fp=U['routes']['from_pass']
    x0,y0,x1,y1=db
    up=(x0-0.4,y1,x1+0.4,5.65); low=(x0-0.4,1.8,x1+0.4,y0)
    q=[p['point'] for p in U['places'] if p['kind']=='DrinkBarQueue']
    print(f"dx={dx} ドリンクバー {db} 卓までの距離 平均{summ['robot_dist_mean']} 最長{summ['robot_dist_max']} 分類 robot{summ['robot_tables']} narrow{summ['narrow_tables']} human{summ['human_tables']} レジ{vr}")
    print('   上の帯を通る', [k for k,r in fp.items() if through(r['points'],up)])
    print('   下を通る', [k for k,r in fp.items() if through(r['points'],low)])
    print('   右端と T38 の間', round(21.2-x1,2), ' 並ぶ点', q, ' 出発点(14.3,5.95)との距離', [round(((a-14.3)**2+(b-5.95)**2)**.5,2) for a,b in q])
    json.dump(dict(L=L), open(out/'layout.json','w'), ensure_ascii=False)

try_dx.py の出力

$ python3 try_dx.py 0 0.8 1.2 1.6 2.0
dx=0.0 ドリンクバー [16.0, 3.4, 20.0, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T30', 'T38']
   下を通る ['T27', 'T28', 'T29']
   右端と T38 の間 1.2  並ぶ点 [[18.0, 5.4]]  出発点(14.3,5.95)との距離 [3.74]
dx=0.8 ドリンクバー [15.2, 3.4, 19.2, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T29', 'T30', 'T38']
   下を通る ['T26', 'T27', 'T28']
   右端と T38 の間 2.0  並ぶ点 []  出発点(14.3,5.95)との距離 []
dx=1.2 ドリンクバー [14.8, 3.4, 18.8, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T29', 'T30', 'T38']
   下を通る ['T26', 'T27', 'T28']
   右端と T38 の間 2.4  並ぶ点 []  出発点(14.3,5.95)との距離 []
dx=1.6 ドリンクバー [14.4, 3.4, 18.4, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T26', 'T27', 'T28', 'T29', 'T30', 'T35', 'T36', 'T37', 'T38']
   下を通る ['T26', 'T27']
   右端と T38 の間 2.8  並ぶ点 []  出発点(14.3,5.95)との距離 []
dx=2.0 ドリンクバー [14.0, 3.4, 18.0, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T26', 'T27', 'T28', 'T29', 'T30', 'T31', 'T32', 'T33', 'T34', 'T35', 'T36', 'T37', 'T38']
   下を通る ['T26', 'T27']
   右端と T38 の間 3.2  並ぶ点 []  出発点(14.3,5.95)との距離 []

$ python3 try_dx.py 0.4
dx=0.4 ドリンクバー [15.6, 3.4, 19.6, 4.1] 卓までの距離 平均10.7 最長18.38 分類 robot38 narrow0 human0 レジ[]
   上の帯を通る ['T29', 'T30', 'T38']
   下を通る ['T27', 'T28']
   右端と T38 の間 1.6  並ぶ点 [[20.9, 3.75]]  出発点(14.3,5.95)との距離 [6.96]

どの幅でも決まり違反はなく、38卓すべてにロボットが届き、卓までの距離の平均と最長は変わりませんでした。分かったことは3つです。

  • 寄せると右の端と T38 の間が広がり、T29・T30・T38 へ行くロボットはその広くなった所から下りるようになる
  • ドリンクバーの下の通路の狭さは消えない。下の6人席の列にドリンクバーが重なっている限り、そこに止まったロボットの上下は0.55mずつ。ふさぐ卓が T27〜T29 から T26〜T28 へずれるだけ
  • 1.6m以上寄せると、ドリンクバーの前に立つお客さんが受け渡し口の前の真下に来て、下の卓へ行く9〜13本の道とぶつかる

0.4mのときは、ドリンクバーの前(立つ側)が上ではなく右の端に選ばれ、立つ点が2つ同じ所に重なりました。前を選ぶ規則が、上の帯の狭さで右を選んだためです。0.4mは候補から外しました。

ドリンクバーの前に立つ点と卓までの距離を並べたスクリプト stand_points.py

import json
for dx in ('0.0','0.4','0.8','1.2','1.6'):
    try: U=json.load(open(f'out_dx{dx}/unity/p4.json'))
    except: continue
    st=[p['point'] for p in U['places'] if p['kind'].startswith('DrinkBar')]
    d={t['id']:t['dist'] for t in U['tables']}
    print(dx, st, {k:d[k] for k in ('T26','T27','T28','T29','T30','T38')})

stand_points.py の出力

0.0 [[18.0, 4.5], [16.5, 4.5], [17.5, 4.5], [18.5, 4.5], [19.5, 4.5], [18.0, 5.4]] {'T26': 3.62, 'T27': 4.79, 'T28': 6.64, 'T29': 8.49, 'T30': 10.22, 'T38': 6.43}
0.4 [[20.0, 3.75], [20.0, 3.75], [20.9, 3.75]] {'T26': 3.62, 'T27': 5.02, 'T28': 6.87, 'T29': 8.35, 'T30': 9.99, 'T38': 6.43}
0.8 [[17.2, 4.5], [15.7, 4.5], [16.7, 4.5], [17.7, 4.5], [18.7, 4.5]] {'T26': 3.62, 'T27': 5.25, 'T28': 7.1, 'T29': 8.12, 'T30': 9.75, 'T38': 6.43}
1.2 [[16.8, 4.5], [15.3, 4.5], [16.3, 4.5], [17.3, 4.5], [18.3, 4.5]] {'T26': 3.64, 'T27': 5.49, 'T28': 7.34, 'T29': 7.88, 'T30': 9.52, 'T38': 6.43}
1.6 [[16.4, 4.5], [14.9, 4.5], [15.9, 4.5], [16.9, 4.5], [17.9, 4.5]] {'T26': 4.2, 'T27': 6.05, 'T28': 7.0, 'T29': 7.65, 'T30': 9.43, 'T38': 6.43}

0.8m・1.2m では、並ぶ点(ドリンクバーの真ん中の前から外へ並べる点)が消えました。ドリンクバーの真ん中が待機場所の真下に来て、障害物から0.25m以上離れた所に1つ目の点が置けなくなったためです。

今・0.8m・1.2m の3つを描いて比べた

ドリンクバーを今のまま・左へ0.8m・左へ1.2m寄せた3つの形。濃い青線は右側の6卓(T26〜T30・T38)へのロボットの道、橙の丸はドリンクバーの前に立つ点、橙の三角は並ぶ点

3つの形を描いたスクリプト draw3.py

import json, matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt, matplotlib.patches as P
from matplotlib import font_manager as fm
fp=fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
def panel(ax,dx,title):
    L=json.load(open(f'out_dx{dx}/layout.json'))['L']; U=json.load(open(f'out_dx{dx}/unity/p4.json'))
    ax.add_patch(P.Rectangle((0,0),24,17,fill=False,lw=1.5))
    for r in L['rooms']:
        x0,y0,x1,y1=r['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc={'back':'#e4e4e0','guest':'#f1e6d6','drink':'#9cc7ea'}[r['kind']],ec='#999'))
        if r['kind']=='drink': ax.text((x0+x1)/2,(y0+y1)/2,'ドリンクバー',ha='center',va='center',fontproperties=fp,fontsize=8)
    for w in L['walls']:
        x0,y0,x1,y1=w['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='#555',ec='none'))
    for t in L['tables']:
        ax.add_patch(P.Rectangle((t['x'],t['y']),t['w'],t['h'],fc='w',ec='k',lw=.6))
        ax.text(t['x']+t['w']/2,t['y']+t['h']/2,t['id'][1:],ha='center',va='center',fontsize=6,clip_on=True)
    s=L['robot_standby']['rect']; ax.add_patch(P.Rectangle((s[0],s[1]),s[2]-s[0],s[3]-s[1],fc='none',ec='#2a5cc8',ls='--',lw=1.2))
    d=L['dish_up'][0]['rect']; ax.add_patch(P.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fc='#333'))
    for k,v in U['routes']['from_pass'].items():
        xs,ys=zip(*v['points']); ax.plot(xs,ys,color='#2a5cc8',alpha=.45 if k in ('T26','T27','T28','T29','T30','T38') else .12,lw=1.3)
    for p in U['places']:
        if p['kind']=='DrinkBarStand': ax.plot(*p['point'],'o',color='#e8590c',ms=5)
        if p['kind']=='DrinkBarQueue': ax.plot(*p['point'],'^',color='#e8590c',ms=6)
    ax.plot(*L['start'],'o',color='#2a5cc8',ms=6)
    ax.set_xlim(9.5,24.3); ax.set_ylim(-.3,8.0); ax.set_aspect('equal')
    ax.set_xticks(range(10,25,1)); ax.set_yticks(range(0,11,1)); ax.grid(alpha=.15); ax.tick_params(labelsize=6)
    ax.set_title(title,fontproperties=fp,fontsize=10)
fig,axs=plt.subplots(1,3,figsize=(19,5.6))
panel(axs[0],'0.0','今(16.0〜20.0m)\n右端とT38の間 1.2m')
panel(axs[1],'0.8','案1:左へ0.8m(15.2〜19.2m)\n右端とT38の間 2.0m')
panel(axs[2],'1.2','案2:左へ1.2m(14.8〜18.8m)\n右端とT38の間 2.4m')
fig.text(0.5,0.01,'濃い青線:右側の6卓(T26〜T30・T38)へのロボットの道 橙の丸:ドリンクバーの前に立つ点 橙の三角:並ぶ点(案1・案2では待機場所に近すぎて置けない)',ha='center',fontproperties=fp,fontsize=9)
plt.tight_layout(rect=(0,0.05,1,0.95)); plt.savefig('store6_drinkbar_left_options.png',dpi=120)

案1(0.8m)にしました。右の端は2.0mあれば、止まっているロボットの横を別のロボットが抜けられます。1.2mだと、ドリンクバーの左の端が、受け渡し口の前から下りる道のすぐ横に来ます。並ぶ点は、今と同じ所(待機場所のすぐ右)に残すことにしました。

  • ねらい:右側の卓(T29・T30・T38)へ行くロボットが、ドリンクを取る人の立つ所を通らずに、右の端から回れるようにする
  • 変わったこと:右の端と T38 の間 1.2m→2.0m。卓までの距離は T29 8.49→8.12m・T30 10.22→9.75m、T27 4.79→5.25m・T28 6.64→7.10m(平均10.7m・最長18.4mは同じ)
  • 見込める効果:直したあとの30回で、ロボット2台のときのドリンクバーのまわり(x 14.8〜19.6m・y 1.8〜5.65m)の30秒以上の止まりは、種10個の合計で122回中5回だった。直す前の形では回していないので、前後の比べはない

並ぶ点が置けないときは、ドリンクバーの前の幅の中で左右へずらす

並ぶ点を「今と同じ所」に残すため、店の形の作り方(unity_export.py)に決まりを1つ足しました。真ん中の前の1つ目の点が置けないときだけ、ドリンクバーの前の幅の中で0.05mずつ左右へずらし、最初に置ける所から並べます。⑥は、前と同じ (18.0, 5.4) に来ました。ほかの5店は真ん中の前に置けているので、変わりません。

場所の点を作る関数 unity_export.py の places(直した版)

02_sec02.py.txt5,415字・96行・python
def places(L, grid, standby_slots, aisles):
    W, D = L["outer"]["w"], L["outer"]["d"]

    def free(x, y):   # 障害物までの余裕(m)。店の外は −1
        cy, cx = grid.cell(x, y)
        if not (0 <= cy < grid.ny and 0 <= cx < grid.nx):
            return -1.0
        return float(grid.clear_person[cy, cx])   # 場所の点は人が立つ所なので、人の余裕で見る
別のタブで開く保存

作業ログ ― レジを縦のまま右へ0.8m、下の壁まで寄せた

ドリンクバーの案を見ていた間に、レジについて別の案が出ました。

レジを右に寄せて、下の壁際によせたら?そうしたら入り口近くにスペースできるんじゃない?

レジはこれまで、入口から少し離れた所(x 2.8〜4.4m・y 0.7〜3.2m)に縦向きで置き、お客さんは入口の側に立って、入口へ向かって並ぶ形でした。この形は保ったまま、右へ0.4m寄せる案と0.8m寄せる案を、ドリンクバーを0.8m寄せた形の上で回しました。

レジの位置を変えて確かめたスクリプト try_reg2.py

import sys, json
sys.path.insert(0, '/home/claude/p2m-site/tools/sim_layouts')
from pathlib import Path
import importlib, famires_v3 as F, famires_gen2 as G
def through(pts, box):
    x0,y0,x1,y1=box
    for p,q in zip(pts,pts[1:]):
        for i in range(101):
            x=p[0]+(q[0]-p[0])*i/100; y=p[1]+(q[1]-p[1])*i/100
            if x0<=x<=x1 and y0<=y<=y1: return True
    return False
V={'now':None,'R1':3.2,'R2':3.6}
for name in sys.argv[1:]:
    L=G.p4_relaxed_side()
    if V[name]:
        a=V[name]
        for r in L['rooms']:
            if r['label']=='レジ': r['rect']=[a,0.0,round(a+0.7,2),2.5]
            if r['label']=='レジ内': r['rect']=[round(a+0.7,2),0.0,round(a+1.6,2),2.5]
    L=F.prepare(L); v=F.check_rules(L)
    if v: print(name,'違反',v); continue
    ctx={}; ra,ea=F.analyze(L,F.SETTINGS['res'],ctx); rb,eb=F.analyze(L,F.SETTINGS['check_res'])
    if ea or eb: print(name,'解析違反',ea,eb); continue
    s=F.summarize(L,ra); out=Path('reg_'+name); out.mkdir(exist_ok=True)
    U=F.export_unity(L,ra,ctx,'p4',out,'try'); vr=F.check_register_clear(U)
    fp=U['routes']['from_pass']
    pl={p['kind']:[] for p in U['places']}
    for p in U['places']: pl[p['kind']].append(p['point'])
    print(name,'距離',s['robot_dist_mean'],s['robot_dist_max'],'分類',s['robot_tables'],'レジ違反',vr)
    print('  入口まわり(x<4.6,y<5)を通る道',[k for k,r in fp.items() if through(r['points'],(0,0,4.6,5.0))])
    print('  レジ',pl.get('Register'),'並ぶ',pl.get('RegisterQueue'),'keepout',U['robot_keepout'],'待合',pl.get('Waiting'),'ドリンク並ぶ',pl.get('DrinkBarQueue'))
    d={t['id']:t['dist'] for t in U['tables']}; print('  ',{k:d[k] for k in ('T05','T10','T14','T31','T32')})
    json.dump(dict(L=L),open(out/'layout.json','w'),ensure_ascii=False)

try_reg2.py の出力

$ python3 try_reg2.py R1 R2
R1 距離 10.7 18.38 分類 38 レジ違反 []
  入口まわり(x<4.6,y<5)を通る道 ['T05', 'T06', 'T07', 'T08', 'T10', 'T11', 'T12']
  レジ [[2.8, 1.25]] 並ぶ [[2.3, 1.25], [1.8, 1.25], [1.3, 1.25]] keepout [{'label': '会計の列(ロボットは通らない)', 'rect': [1.05, 0.75, 3.2, 1.75]}] 待合 [[1.8, 0.9]] ドリンク並ぶ [[18.0, 5.4]]
   {'T05': 12.95, 'T10': 13.12, 'T14': 9.16, 'T31': 8.64, 'T32': 7.54}
R2 距離 10.7 18.38 分類 38 レジ違反 []
  入口まわり(x<4.6,y<5)を通る道 ['T05', 'T06', 'T07', 'T08', 'T10', 'T11', 'T12']
  レジ [[3.2, 1.25]] 並ぶ [[2.7, 1.25], [2.2, 1.25], [1.7, 1.25]] keepout [{'label': '会計の列(ロボットは通らない)', 'rect': [1.45, 0.75, 3.6, 1.75]}] 待合 [[1.8, 0.9]] ドリンク並ぶ [[18.0, 5.4]]
   {'T05': 12.95, 'T10': 13.12, 'T14': 9.16, 'T31': 8.64, 'T32': 7.54}

どちらも決まり違反はなく、ロボットの道も卓までの距離も変わりませんでした。左の通路へ行く道は、もともとレジの上の y 4.9m あたりを通っているためです。変わるのは人の動きです。

レジの今の位置と、縦のまま右へ0.4m・0.8m寄せて下の壁につけた2案。橙の丸は会計に立つ点、三角は並ぶ点、紫の四角は案内を待つ人が立つ点、赤の点線はロボットが通らない会計の列

3つの位置を描いたスクリプト draw_reg2.py

import json, matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt, matplotlib.patches as P
from matplotlib import font_manager as fm
fp=fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
def panel(ax,d,title):
    L=json.load(open(f'{d}/layout.json'))['L']; U=json.load(open(f'{d}/unity/p4.json'))
    ax.add_patch(P.Rectangle((0,0),24,17,fill=False,lw=1.5))
    e=L['entrance']; ax.plot([0,0],[e['a0'],e['a1']],color='w',lw=5); ax.text(-0.2,2.2,'入口',fontproperties=fp,ha='right',va='center',fontsize=9)
    for r in L['rooms']:
        x0,y0,x1,y1=r['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc={'back':'#e4e4e0','guest':'#f1e6d6','drink':'#9cc7ea'}[r['kind']],ec='#999'))
        if r['label'] in ('レジ','レジ内','待合'): ax.text((x0+x1)/2,(y0+y1)/2,r['label'],ha='center',va='center',fontproperties=fp,fontsize=8,clip_on=True)
    for w in L['walls']:
        x0,y0,x1,y1=w['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='#555',ec='none'))
    for t in L['tables']:
        ax.add_patch(P.Rectangle((t['x'],t['y']),t['w'],t['h'],fc='w',ec='k',lw=.6))
        ax.text(t['x']+t['w']/2,t['y']+t['h']/2,t['id'][1:],ha='center',va='center',fontsize=6,clip_on=True)
    for k in U['robot_keepout']:
        x0,y0,x1,y1=k['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='none',ec='#c33',ls=':',lw=1.3))
    for k,v in U['routes']['from_pass'].items():
        xs,ys=zip(*v['points']); ax.plot(xs,ys,color='#2a5cc8',alpha=.35,lw=1.2)
    for p in U['places']:
        if p['kind']=='Register': ax.plot(*p['point'],'o',color='#e8590c',ms=6)
        if p['kind']=='RegisterQueue': ax.plot(*p['point'],'^',color='#e8590c',ms=6)
        if p['kind']=='Waiting': ax.plot(*p['point'],'s',color='#7a4b9c',ms=6)
    ax.set_xlim(-1.2,10.5); ax.set_ylim(-.3,8.0); ax.set_aspect('equal')
    ax.set_xticks(range(0,11)); ax.set_yticks(range(0,9)); ax.grid(alpha=.15); ax.tick_params(labelsize=7)
    ax.set_title(title,fontproperties=fp,fontsize=10)
fig,axs=plt.subplots(1,3,figsize=(19,5.4))
panel(axs[0],'reg_now','今:レジ x 2.8〜4.4・y 0.7〜3.2\nレジの上と島の下の間 2.0m')
panel(axs[1],'reg_R1','案1:右へ0.4m・下の壁まで(x 3.2〜4.8・y 0〜2.5)\nレジの上と島の下の間 2.7m')
panel(axs[2],'reg_R2','案2:右へ0.8m・下の壁まで(x 3.6〜5.2・y 0〜2.5)\nレジの上と島の下の間 2.7m')
fig.text(0.5,0.02,'橙の丸:会計に立つ点 橙の三角:並ぶ点 紫の四角:待合の前(案内を待つ客が立つ点) 赤の点線:会計の列(ロボットは通らない) 青線:ロボットの道',ha='center',fontproperties=fp,fontsize=9)
plt.tight_layout(rect=(0,0.05,1,0.93)); plt.savefig('store6_register_option2.png',dpi=120)

案2(右へ0.8m)にしました。会計の列が入口から少し離れます。

  • ねらい:入口の前に、入ってきたお客さんと案内するスタッフが立てる場所を空ける
  • 変わったこと:レジの上と左の島の下の間 2.0m→2.7m。入口の上のほう(y 2.5〜3.4m)とレジの上が空いた。ロボットの道と卓までの距離は変わらない
  • 見込める効果:直したあとの30回で、入口・レジのまわり(x 5.2mより左・y 5.0mより下)のロボットの30秒以上の止まりは、2台の種10個の合計で1回。人どうしの詰まりは0回。直す前の形では回していないので、前後の比べはない

1つ残したことがあります。会計の列(y 1.25m)が、待合の前の点(1.8, 0.9)の0.35m上に来ます。案内を待つお客さんと会計に並ぶお客さんが重なり得ます。待合は動かさず、3Dの店で見ることにしました。

店の形の作り方 famires_gen2.py の⑥の部分(直した版)

def p4_relaxed_side():
    W, D = 24.0, 17.0
    rooms, walls, dish, sb, start = boh_right(W, D, 13.0, 6.5, 14.5)
    toilet, twalls, tdoor = toilet_block([9.4, 14.8, 12.4, 17.0], "bottom", W, D)   # 上の壁の真ん中。入口は下向き
    rooms += [toilet]
    walls += twalls
    rooms += [dict(label="待合", rect=[0.6, 0.0, 3.0, 0.5], kind="guest"),
              # 縦のカウンター。客は入口の側(左)に立ち、入口へ向かって並ぶ。下の通路をふさがない(2026-10-23 エイイチと決めた)
              # 2026-10-28:縦のまま右へ0.8m・下の壁まで(前は x 2.8〜4.4・y 0.7〜3.2)。レジの上と左の島の下の間が2.0m→2.7mになり、入口の前が空く
              *register([3.6, 0.0, 4.3, 2.5], [4.3, 0.0, 5.2, 2.5]),
              # ドリンクバーは左へ0.8m(2026-10-28 エイイチと決めた。前は 16.0〜20.0m で、右端と右の L字席 T38 の間が1.2mしかなく、
              # 右側の卓へ行くロボットが、客が立つ・並ぶドリンクバーの上の帯を通っていた。右端の間を2.0mにした。1.6m以上寄せると
              # 前に立つ客が受け渡し口の前の真下に来るので0.8m。下の6人席の列の前の通路の狭さ(止まると上下0.55m)は残る)
              dict(label="ドリンクバー", rect=[15.2, 3.4, 19.2, 4.1], kind="drink")]
    t = strip(0.3, 15.2, "h", 4, seats=6, walls=walls)    # 家族用の6人ボックス(上の壁ぞい)
    t += strip(0.0, 5.2, "v", 5, walls=walls)              # 左の壁ぞい
    t += island(2.7, 5.6, "v", 4, walls=walls)
    t += island(6.75, 5.6, "v", 4, walls=walls)
    t += strip(24.0 - 5 * UNIT, 0.0, "h", 5, seats=6, walls=walls)  # 家族用の6人ボックス(右下・右の壁まで)
    t += two_seat_row(6.6, 0.0, "bottom", 7)
    t += [lsofa(21.2, 3.0, 2.8, 3.5, flip=True, mirror=True)]   # 下のボックスとの間を1.2mあける
    return dict(name="⑥ ゆったり型・入口が左の壁", style="ゆったり", outer=dict(w=W, d=D),
                entrance=dict(side="left", a0=1.0, a1=3.4), rooms=rooms, walls=walls, toilet_doors=[tdoor],
                dish_up=dish, robot_standby=sb, start=start, tables=t)

店の形の説明 README.md に足した決まり

- ドリンクバーに並ぶ点は、ふつうは真ん中の前から外へ並べる。真ん中の前の1つ目の点が置けないとき(障害物から0.25m未満・細い通路)だけ、ドリンクバーの前の幅の中で0.05mずつ左右へずらし、最初に置ける所から並べる(2026-10-28。⑥のドリンクバーを左へ寄せたら、真ん中の前が待機場所に近すぎて並ぶ点が消えたため。⑥は待機場所の右=前と同じ所 x18.0・y5.4 に来る。ほかの店は前と同じ)

## 決まり(2026-10-28 エイイチと決めたこと:⑥のドリンクバーとレジ)

- ⑥のドリンクバーを左へ0.8m(16.0〜20.0m → 15.2〜19.2m)。右端と右の L字席(T38)の間が1.2mしかなく、右側の卓(T29・T30・T38)へ行くロボットが、客が立つ・並ぶドリンクバーの上の帯を通っていた。右端の間を2.0mにした。1.6m以上寄せると、前に立つ客が受け渡し口の前の真下に来て、下の卓へ行く道とぶつかる。下の6人席の列の前の通路の狭さ(ロボットが止まると上下0.55m)は残る
- ⑥のレジは縦向きのまま右へ0.8m・下の壁まで(x 2.8〜4.4・y 0.7〜3.2 → x 3.6〜5.2・y 0〜2.5)。レジの上と左の島の下の間が2.0m→2.7mになり、入口の前が空く。客は今までどおり入口の側に立ち、入口へ向かって並ぶ
- 記録だけ:会計の列(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は同じ)。⑥以外は版の番号だけ

6店の店の形を作り直した famires_v3.py の出力

[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 の出力

03_6_famires_v3.py.txt5,617字・145行・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

店の形の版の番号は、作り方のコードから決まります。コードを変えたので6店とも番号は変わりますが、中身は⑥だけ変わっているはずです。前の版のファイルの番号だけを新しい番号に置き換えて、今のファイルと同じかを比べました。

import json,subprocess
new=json.load(open('public/sim/layouts/summary.json'))[0]['generation']
print('新しい版',new)
fs=subprocess.check_output(['git','diff','--name-only','public/sim/layouts']).decode().split()
for f in fs:
    old=subprocess.check_output(['git','show','HEAD:'+f]).decode()
    cur=open(f).read()
    same = old.replace('d7bb919590d4',new)==cur
    print(('版だけ' if same else '中身も変わった'),f)

check_gen_only.py の出力

新しい版 2e9fc773050a
版だけ public/sim/layouts/p0_memory.json
版だけ public/sim/layouts/p0_memory_analysis.json
版だけ public/sim/layouts/p1_efficient_corner.json
版だけ public/sim/layouts/p1_efficient_corner_analysis.json
版だけ public/sim/layouts/p2_efficient_narrow.json
版だけ public/sim/layouts/p2_efficient_narrow_analysis.json
版だけ public/sim/layouts/p3_relaxed_center.json
版だけ public/sim/layouts/p3_relaxed_center_analysis.json
中身も変わった public/sim/layouts/p4_relaxed_side.json
中身も変わった public/sim/layouts/p4_relaxed_side_analysis.json
版だけ public/sim/layouts/p5_crammed.json
版だけ public/sim/layouts/p5_crammed_analysis.json
版だけ public/sim/layouts/summary.json
版だけ public/sim/layouts/unity/p0_memory.json
版だけ public/sim/layouts/unity/p0_memory_map.json
版だけ public/sim/layouts/unity/p1_efficient_corner.json
版だけ public/sim/layouts/unity/p1_efficient_corner_map.json
版だけ public/sim/layouts/unity/p2_efficient_narrow.json
版だけ public/sim/layouts/unity/p2_efficient_narrow_map.json
版だけ public/sim/layouts/unity/p3_relaxed_center.json
版だけ public/sim/layouts/unity/p3_relaxed_center_map.json
中身も変わった public/sim/layouts/unity/p4_relaxed_side.json
中身も変わった public/sim/layouts/unity/p4_relaxed_side_map.json
版だけ public/sim/layouts/unity/p5_crammed.json
版だけ public/sim/layouts/unity/p5_crammed_map.json

⑥の卓までの距離とレジ・並ぶ点を確かめたスクリプト dist_change.py

import json,subprocess
f='public/sim/layouts/p4_relaxed_side_analysis.json'
o=json.loads(subprocess.check_output(['git','show','HEAD:'+f])); n=json.load(open(f))
for a,b in zip(o['tables'],n['tables']):
    if a.get('dist')!=b.get('dist') or a['category']!=b['category']: print(a['id'],a.get('dist'),'→',b.get('dist'),b['category'])
print(o['summary']['robot_dist_mean'],n['summary']['robot_dist_mean'])
U=json.load(open('public/sim/layouts/unity/p4_relaxed_side.json'))
print([ (p['kind'],p['point']) for p in U['places'] if p['kind'] in ('Register','RegisterQueue','Waiting','DrinkBarQueue')], U['robot_keepout'])

dist_change.py の出力

T27 4.79 → 5.25 robot
T28 6.64 → 7.1 robot
T29 8.49 → 8.12 robot
T30 10.22 → 9.75 robot
10.7 10.7
[('Waiting', [1.8, 0.9]), ('Register', [3.2, 1.25]), ('RegisterQueue', [2.7, 1.25]), ('RegisterQueue', [2.2, 1.25]), ('RegisterQueue', [1.7, 1.25]), ('DrinkBarQueue', [18.0, 5.4])] [{'label': '会計の列(ロボットは通らない)', 'rect': [1.45, 0.75, 3.6, 1.75]}]

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

試算は、店の種類・スタッフの人数・ロボットの台数・来客のペースなどの組み合わせ32,850条件を、1条件40回ずつ回した平均です。前提が変わった⑥の3,066条件だけを計算し直し、ほかは前の結果を使い回しました。Claude のクラウドの作業環境(2コア・simpy 4.1.1)で約22分でした。

haizen_sim.py の実行ログ

32850 conditions, 29784 reused, 3066 to run
  100/3066
  200/3066
  300/3066
  400/3066
  500/3066
  600/3066
  700/3066
  800/3066
  900/3066
  1000/3066
  1100/3066
  1200/3066
  1300/3066
  1400/3066
  1500/3066
  1600/3066
  1700/3066
  1800/3066
  1900/3066
  2000/3066
  2100/3066
  2200/3066
  2300/3066
  2400/3066
  2500/3066
  2600/3066
  2700/3066
  2800/3066
  2900/3066
  3000/3066
32850 conditions written

直す前と後の試算を比べたスクリプト compare_p4_site.py

"""⑥の試算を、店の形を直す前(results_before.json)と後(results.json)で比べる(2026-10-28)
使い方: python3 compare_p4_site.py   (haizen_sim.py と同じフォルダで)"""
import json, haizen_sim as h
A = {h.key(r): r for r in json.load(open('results_before.json'))['rows']}
B = {h.key(r): r for r in json.load(open('results.json'))['rows']}
ch = {}
for k in B:
    if A[k] != B[k]:
        ch[k[0]] = ch.get(k[0], 0) + 1
print(ch)
s = h.STORES['fam_p4']
for st in s['staff']:
    for rb in (0, 1, 2):
        c = {'store': 'fam_p4', 'staff': st, 'robots': rb, 'arrivals_per_h': 38, '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', 'robot_share')))

compare_p4_site.py の出力

{'fam_p4': 3066}
2 0 entry_wait_mean:26.45→26.66 staff_wait_mean:20.41→20.38 dish_wait_mean:1.38→1.38 pay_wait_mean:12.49→12.35 robot_share:0.00→0.00
2 1 entry_wait_mean:12.94→12.76 staff_wait_mean:8.50→8.38 dish_wait_mean:1.81→1.80 pay_wait_mean:15.86→15.93 robot_share:0.89→0.89
2 2 entry_wait_mean:10.94→11.02 staff_wait_mean:7.20→6.02 dish_wait_mean:0.72→0.72 pay_wait_mean:16.51→18.13 robot_share:1.00→1.00
3 0 entry_wait_mean:6.31→6.95 staff_wait_mean:3.77→4.20 dish_wait_mean:0.73→0.75 pay_wait_mean:11.73→12.05 robot_share:0.00→0.00
3 1 entry_wait_mean:3.15→3.12 staff_wait_mean:0.89→1.00 dish_wait_mean:1.93→1.97 pay_wait_mean:8.24→8.47 robot_share:0.80→0.80
3 2 entry_wait_mean:2.34→1.89 staff_wait_mean:0.70→0.63 dish_wait_mean:0.85→0.82 pay_wait_mean:7.70→6.99 robot_share:1.00→1.00
4 0 entry_wait_mean:1.92→1.71 staff_wait_mean:0.61→0.64 dish_wait_mean:0.38→0.39 pay_wait_mean:6.35→6.51 robot_share:0.00→0.00
4 1 entry_wait_mean:2.36→1.44 staff_wait_mean:0.53→0.41 dish_wait_mean:1.86→1.81 pay_wait_mean:5.02→4.59 robot_share:0.76→0.77
4 2 entry_wait_mean:1.38→1.12 staff_wait_mean:0.36→0.39 dish_wait_mean:0.84→0.86 pay_wait_mean:4.30→4.22 robot_share:1.00→1.00

3Dの店で回す条件(スタッフ3人・1時間38組)では、入店待ちは 0台 6.3→7.0分・1台 3.2→3.1分・2台 2.3→1.9分でした。卓までの距離は4卓で0.4〜0.5m変わっただけなので、この差は40回の平均の乱数のぶれの大きさです。

ページの⑥だけを差し替えるスクリプト update_page_p4.py

公開中の試算のページは、全条件の結果と店の平面図を1つのデータとして持っています。⑥の行だけが変わったことを確かめてから結果を入れ替え、⑥の平面図を作り直しました。

"""⑥の店の形を直したとき(2026-10-28)、公開中の試算のページの⑥だけを差し替える。
使い方: python3 update_page_p4.py <公開中のページ> <新しい results.json> <前の p4 の店の形> <新しい p4 の店の形> <新しい summary.json> <出力するページ>
- const D の r(結果):⑥(fam_p4)の行だけが変わっていることを確かめてから、全部を新しい結果に入れ替える
- const D の stores.fam_p4:卓までの距離(dist_m・dist_mean)を新しい値に
- const D の plans.fam_p4:⑥はドリンクバーとレジの位置が変わったので、平面図をまるごと作り直す
  (作り方は 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_p4"}, changed
D["r"] = rows

st = h.STORES["fam_p4"]
D["stores"]["fam_p4"]["dist_m"] = list(st["dist_m"])
D["stores"]["fam_p4"]["dist_mean"] = round(sum(st["dist_list"]) / len(st["dist_list"]), 1)
assert D["stores"]["fam_p4"]["tables"] == st["tables"] == 38
print("⑥の距離:", D["stores"]["fam_p4"]["dist_m"], D["stores"]["fam_p4"]["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_p4"], "前の店の形から作った平面図が公開中のページと違う"
D["plans"]["fam_p4"] = plan(new_lay)
print("⑥の平面図を作り直した(卓", len(D["plans"]["fam_p4"]["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"])

Unity が読む条件を書き出すスクリプト export_params.py(⑥を足した版)

04_export_params.py.txt4,814字・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)の値。registers はレジの数(v2.8)
別のタブで開く保存

ページの差し替え・条件の書き出し・ページの確かめの出力

05_Unity_export_params.py.txt5,156字・35行・text
$ python3 update_page_p4.py ../../p2m-site/public/sim/serving-robot.html results.json p4_before.json p4_after.json ../../p2m-site/public/sim/layouts/summary.json serving-robot.v28e.html
結果が変わった行: {'fam_p4': 3066}
⑥の距離: [3.6, 18.4] 10.7
⑥の平面図を作り直した(卓 38 )
書いた: serving-robot.v28e.html 版 2e9fc773050a

$ python3 export_params.py results.json serving-robot-params.new.json
/tmp/claude-0/-home-claude/0c16993b-2ddd-5d32-af2a-73934f1ff276/scratchpad/sim/p_check.json 15 conditions / generation 2e9fc773050a
別のタブで開く保存

ページの全37,800画面を描き、欠け・undefined・NaN がないことを確かめてから、店の形・ページ・条件を1つの commit(p2m-site 536bd8e)で公開しました。

作業ログ ― 3Dの店に⑥を建て、1回ずつ回した

3Dの店の作業は、Mac の「依頼箱」(依頼の文を置くと、Mac で Claude Code が自動で実行して報告を書き戻す仕組み)に依頼を置いて進めました。台帳(3Dの店の回の記録の CSV)は、依頼の前に Claude が KIOXIA の archive に写して退避しています。

依頼文:⑥を建てて確かめる(20261028_21)

06_sec05.md3,524字・63行・markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼:⑥ ゆったり・入口が左の壁の店(fam_p4)を Unity に建て、1回ずつ回して確かめる(2026-10-28)

エイイチの決定(2026-10-28):⑤はここで区切り(LOG397)、次は⑥。④⑤のときと同じく、今回は⑥だけ。
別のタブで開く保存

報告:20261028_1935_p4_build_check.txt

07_sec05.txt11,610字・229行・text
====================================================================
⑥ ゆったり・入口が左の壁の店(fam_p4)を建てて、1回ずつ回して確かめた
依頼:20261028_21_店6を建てて確かめる.md
2026-10-28 19:35 check-in:cs:37・cs:38 build_id cs38_20261028_1923(保留 0)
====================================================================

結論
別のタブで開く保存

⑥は建て直しなしで建ち、2台の止まりは受け渡し口と待機場所の間に集まった

⑥は③⑦④⑤と同じコードで建ち、⑥のための手直しは要りませんでした。⑤では、建てる前の確かめで通路の端の点が外壁の上にあることが分かり、サイトの作り方を直して2回建て直しています。

1回ずつの3回では、入れた組の数が3回とも59組で、試算(74〜76組)より少なくなりました。⑤でも1回目は71組と少なく、夜の種10個で見ると種0だけのことでした。2台の回の30秒以上の止まり23回のうち12回は受け渡し口の点から2.5m以内で、理由はすべて「もう1台のロボット」でした。そのうち9回は待機場所の中です。

建てたときの確かめで、3つの表示が出ました。レジを下の壁まで寄せたので入るのが上からだけになったレジの内側、人が歩く NavMesh(歩ける床の面)で細い通路のいちばん狭いところが0.98mになったこと、ロボット用の NavMesh で受け渡し口とつながっていない面が大きく出たことです。どれも回は最後まで回り、ロボットは全卓に料理を運べています。⑤と同じく、記録だけにしました。

作業ログ ― ⑥を種10個ずつ、計30回回した

確かめの3回を見て、⑥の夜(ロボット0・1・2台 × 種0〜9 の30回)を回すことにしました。受け渡し口と待機場所の間の詰まりは、待機場所と受け渡し口の距離が③⑦④と同じ形なので、⑥だけ直さずに夜の数で見ます。

依頼文:⑥の夜を回す(20261028_22)

---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 180
---
# 依頼:⑥の夜(30回)を回す(2026-10-28)

エイイチが「⑥の夜を回す」と決めた(2026-10-28 19:52)。⑥(fam_p4・店の形 2e9fc773050a)× ロボット0・1・2台 × 種0〜9、今の標準(すれ違い入・待機場所へ戻る 入・案B 切)。
依頼 21 の報告(20261028_1935_p4_build_check.txt)の「決めてほしいこと」への答え:
- 1:回す(この依頼)
- 2:受け渡し口と待機場所1の間の詰まりは、今は店の形を直さず、この夜の種10個で数を見る(待機場所と受け渡し口の距離は③⑦④も同じ形のため)
- 3:入れた組の数が59組と少ない件は、先に調べず、この夜の種10個で見る
- 4:建てたときの3つの表示(レジの内側の「西のふち 0 m」、細い通路の人用の幅 0.98 m、ロボット用 NavMesh の「つながっていない面」118.63 m2)は、今は直さず記録だけ

台帳の退避は Claude が済ませた。
- 退避先:/Volumes/KIOXIA/r2r2r-data/service_sim/unity/archive/unity_ledger_v1_prenight_20261104_p4/unity_ledger_v1.csv
- 601 行・md5 bd712cbae6008066b75da7d91670a360

## 始める前に確かめること(1つでも違えば回さずに報告して終わる)

1. Unity エディタが起動していない(起動していたら閉じずに報告して終わる)
2. /Volumes/KIOXIA がマウントされている。鍵ファイル unity_ledger_v1.lock がない
3. 本物の台帳の md5 が上の退避と同じ
4. ServiceSim の保留が 0 で、実行ファイルの build_info が cs38_20261028_1923・保留なし

## 回す

作業フォルダで、次の2つを順に実行する。night_runner.py は終わるまで待つ。

```
python3 tools/make_queue.py --stores fam_p4 --out queue_p4.json
python3 tools/night_runner.py --queue queue_p4.json --parallel 4 --night 20261104 --record-name 20261104_p4
```

- 夜の名前は 20261104。記録は night/20261104_p4/ に置く。オプションの名前が違えば、同じ意味になるように合わせる
- 並ぶのは 30 回(⑥ × 0・1・2台 × 種0〜9・すれ違い入・待機場所へ戻る 入・案B 切)であることを、回す前に queue_p4.json で確かめる。30 回でなければ回さずに報告
- 回している間は、ほかの作業をしない。Assets・tools・設定ファイルを開かない・変えない。Unity を開かない
- Ctrl+C で止めない。途中で止まっても、同じ夜を回し直さない(そのまま報告する)
- 終わったら queue_p4.json を作業フォルダに残さない(night/20261104_p4/ に写しがあればそれでよい)

## 報告

依頼箱の結果に加えて、claude_code_reports に 20261104_night_p4.txt として書く(場所:~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/claude_code_reports/)。
- 始める前の確かめ4つの結果
- runner_log のまとめ(completed の回数・失敗・止まった回・最長の実時間)
- 台帳の行数(601 → 631 になるはず)と、先頭 601 行の md5 が退避と同じか
- 全員が帰らなかった回があれば、その回の名前(原因の調べはしない)
- 台数ごと(0・1・2台)の種10個の中央値と最小・最大:入店待ち・そのうち席は空いているのに待った時間・料理の待ち・会計待ち・入れた組の数・ロボットが運んだ皿の割合・往復の数・30秒以上の止まり(1往復あたり)・動けなくなった(60秒)・瞬間移動・打ち切り・卓を空けた・冷めた皿の割合。試算の値(params の site_result)も並べる
- 種ごとの入れた組の数(種0〜9。台数で変わらなければ1行でよい)と、試算の組の数(約74〜76組)との比
- 2台の30秒以上の止まりを、理由ごと・場所ごとに、種10個の合計で。場所は次の分け方(重なる所は上の方に数える)
  - 待機場所の四角の中(x 15.5〜17.9・y 5.65〜6.45)
  - 受け渡し口の点 (14.3, 5.95) から 2.5 m 以内(待機場所の中を除く)
  - ドリンクバーのまわり(x 14.8〜19.6・y 1.8〜5.65)
  - 入口・レジのまわり(x が 5.2 m より左で、y が 5.0 m より下)
  - 左の2本の通路(x 1.2〜2.7 と 5.25〜6.75、y 5.2〜14.5)
  - スタッフが待つ点の近く 1.0 m 以内
  - そのほか(多い所があれば 2 m 四方のマスで上位3つ)
- 入口・レジのまわり(x が 5.2 m より左で、y が 2.5 m より下)で、スタッフがロボットに道をゆずって待った回数・人どうしの詰まり・あきらめた/移した回数(種10個の合計。台数ごと)
- ロボットを1台増やしたときの向き(入店待ち・料理の待ち)が、種の過半数で試算と同じか逆か(judge と同じ数え方)
- judge.md ができたか(⑥は基準がないので「基準なし」でよい。中身の判断はしない)

夜の実行ログ runner_log.txt

08_runner_log.txt8,544字・75行・text
2026-10-28 19:54:48  === 夜の実行係を始めます(night 20261104・同時 4 本・上限 90.0 分)===
2026-10-28 19:54:48  並べた回: queue_p4.json(30 回)
2026-10-28 19:54:48  台帳: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv(59 列)
2026-10-28 19:54:48  版: ServiceSim/cs:38(Builds/headless/build_info.json)→ 出力フォルダの字 cs38
2026-10-28 19:54:48  並べた回の試算の版: v2.8 / 店の形の版: 2e9fc773050a
2026-10-28 19:54:48  スリープを止めました(caffeinate pid 98277)
2026-10-28 19:54:48  並べた回を写しました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261104_p4/queue.json
2026-10-28 19:54:48  並べた回 30 / もう completed の回を飛ばす 0 / 回す 30
別のタブで開く保存

判定 judge.md

# 夜の結果の振り分け(20261104_night_*)

基準データ: 20261102_night_cs34_*(60回)/振り分けた回: 30回

切り替えの入切(条件の一部。同じ入切の基準の回とだけ比べる): 今回 すれ違い入・待機場所へ戻る入・案B切/基準 すれ違い入・待機場所へ戻る入・案B切

| 振り分け | 回数 |
|---|---|
| 正常 | 0 |
| 試算を見直す候補 | 0 |
| Unity の作り | 0 |
| 回の失敗 | 0 |
| 基準なし | 30 |

## 回の失敗

- なし

## 基準なし(基準データに同じ条件・同じ入切の回がないので判定していない)

- fam_p4 スタッフ3 ロボット0台・すれ違い入・待機場所へ戻る入・案B切
- fam_p4 スタッフ3 ロボット1台・すれ違い入・待機場所へ戻る入・案B切
- fam_p4 スタッフ3 ロボット2台・すれ違い入・待機場所へ戻る入・案B切

## Unity の作り(種の中央値が、基準の同じ条件の最大を超えた)

- なし

## 試算を見直す候補

### ロボットを増やしたときの向きが、種の過半数で試算と逆

- なし

### Unity÷試算 の大きさが、基準の同じ条件の範囲から外れた

- なし

## 記録(印にしない):試算との差の大きさ(条件ごとの種の中央値・Unity÷試算)

| 条件 | 種の数 | 入店待ち | 料理の待ち | 席は空いているのに待った |
|---|---|---|---|---|
| fam_p4 スタッフ3 ロボット0台・すれ違い入・待機場所へ戻る入・案B切 | 10 | 1.38 | 1.49 | 2.00 |
| fam_p4 スタッフ3 ロボット1台・すれ違い入・待機場所へ戻る入・案B切 | 10 | 1.94 | 1.06 | 4.82 |
| fam_p4 スタッフ3 ロボット2台・すれ違い入・待機場所へ戻る入・案B切 | 10 | 2.55 | 1.56 | 5.53 |

## すれ違い 入 と 切 の比べ(同じ夜・同じ条件・種の中央値。印にしない)

| 条件 | 入店待ち | 料理の待ち | 席は空いているのに待った | 動けなくなった | あきらめた | ロボットが運んだ割合 |
|---|---|---|---|---|---|---|
| fam_p4 スタッフ3 ロボット0台・すれ違い切・待機場所へ戻る入・案B切(0回) | - | - | - | - | - | - |
| fam_p4 スタッフ3 ロボット0台・すれ違い入・待機場所へ戻る入・案B切(10回) | 9.60 | 1.12 | 8.42 | 0.00 | 7.00 | 0.00 |
| fam_p4 スタッフ3 ロボット1台・すれ違い切・待機場所へ戻る入・案B切(0回) | - | - | - | - | - | - |
| fam_p4 スタッフ3 ロボット1台・すれ違い入・待機場所へ戻る入・案B切(10回) | 6.04 | 2.09 | 4.84 | 1.00 | 19.00 | 0.83 |
| fam_p4 スタッフ3 ロボット2台・すれ違い切・待機場所へ戻る入・案B切(0回) | - | - | - | - | - | - |
| fam_p4 スタッフ3 ロボット2台・すれ違い入・待機場所へ戻る入・案B切(10回) | 4.83 | 1.27 | 3.49 | 8.00 | 28.50 | 0.96 |

線: 立ち往生 30秒以上の止まり/向きは 5%・0.2分より小さい変化を数えない/外れは基準の同じ条件の最小〜最大

報告:20261104_night_p4.txt

09_sec06.txt4,010字・78行・text
⑥の夜(20261104_p4・30回)の報告 — Claude Code(2026-10-28 20:35)
依頼:20261028_22_店6の夜を回す.md
版:cs38_20261028_1923・店の形 2e9fc773050a・試算 v2.8・すれ違い入・待機場所へ戻る 入・案B 切・スタッフ3人・38組/時・レジ1つ
記録:/Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261104_p4/(runner_log.txt・runs.csv・queue.json・judge.md・judge.csv・logs)

■ 始める前の確かめ(4つとも合格)
1 Unity エディタ:起動していない(Unity Hub だけが起動していた。エディタ本体のプロセスはなし)
2 KIOXIA:マウントあり。unity_ledger_v1.lock なし
別のタブで開く保存

来る客の数は試算とそろい、59組は種0だけだった

種10個の入れた組の数は59〜88組、中央値73.5組で、試算の74〜76組とほぼ同じでした。確かめの3回で59組だったのは種0のことで、⑤のときと同じです。

ロボットを足すと待ちが縮む向きは、試算と同じだった

入店待ちは、0台から1台にすると種10個のうち8個で縮み(逆0、変化が小さく数えない2)、1台から2台にすると5個で縮みました(逆3、数えない2)。料理の待ちは、0台→1台で延び、1台→2台で縮む向きが、10個すべてで試算と同じでした。判定の決まりは「種の過半数で試算と逆」のときに印を付けるもので、逆は最大3個なので印は付いていません。

試算はレジで詰まり、3Dの店は片付けと案内で詰まる

試算の会計待ちは7〜12分、3Dの店は3分前後です。代わりに、席は空いているのに待った時間が、3Dの店では 0台 8.4分(試算4.2分)・1台 4.8分(1.0分)・2台 3.5分(0.6分)でした。judge.md の記録の表でも、Unity÷試算 は入店待ちで1.4〜2.6倍、席は空いているのに待った時間で2.0〜5.5倍です。

⑤・③⑦と同じ形のずれです。試算ではスタッフの手は「仕事の数×時間」でふさがるだけですが、3Dの店のスタッフは店の中を歩き、片付けと案内を後回しにしています。このずれは、③⑦のときに決めたとおり、試算に入れず記録だけにしています。

2台のときに残った詰まりは、待機場所の中と左の島の下の入口

直した⑥の図面に、ロボット2台の夜(種10個)の30秒以上の止まり122回を場所ごとに重ねた図。待機場所の中が60回、左の島の下の入口(x 6〜8m・y 4〜6m)が27回

止まりの数を図に重ねたスクリプト draw_after.py

"""直した⑥の図面に、ロボットの道と、2台の夜(種10個)の30秒以上の止まりの数を場所ごとに重ねる(2026-10-28)
止まりの数は 20261104_night_p4.txt(Claude Code の報告)の「2台の30秒以上の止まり」から写した"""
import json, matplotlib; matplotlib.use('Agg')
import matplotlib.pyplot as plt, matplotlib.patches as P
from matplotlib import font_manager as fm
fp=fm.FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc')
B='/home/claude/p2m-site/public/sim/layouts/'
L=json.load(open(B+'p4_relaxed_side.json'))['layout']
U=json.load(open(B+'unity/p4_relaxed_side.json'))
W,D=L['outer']['w'],L['outer']['d']
fig,ax=plt.subplots(figsize=(14,10.5))
ax.add_patch(P.Rectangle((0,0),W,D,fill=False,lw=2))
e=L['entrance']; ax.plot([0,0],[e['a0'],e['a1']],color='w',lw=4); ax.text(-0.3,(e['a0']+e['a1'])/2,'入口',fontproperties=fp,ha='right',va='center',fontsize=10)
for r in L['rooms']:
    x0,y0,x1,y1=r['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc={'back':'#e4e4e0','guest':'#f1e6d6','drink':'#d8e8f4'}[r['kind']],ec='#999'))
    ax.text((x0+x1)/2,(y0+y1)/2,r['label'],ha='center',va='center',fontproperties=fp,fontsize=8)
for w in L['walls']:
    x0,y0,x1,y1=w['rect']; ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='#555',ec='none'))
for t in L['tables']:
    ax.add_patch(P.Rectangle((t['x'],t['y']),t['w'],t['h'],fc='w',ec='k',lw=.8))
    ax.text(t['x']+t['w']/2,t['y']+t['h']/2,t['id'][1:],ha='center',va='center',fontsize=6)
d=L['dish_up'][0]['rect']; ax.add_patch(P.Rectangle((d[0],d[1]),d[2]-d[0],d[3]-d[1],fc='#333'))
s=L['robot_standby']['rect']; ax.add_patch(P.Rectangle((s[0],s[1]),s[2]-s[0],s[3]-s[1],fc='none',ec='#2a5cc8',ls='--',lw=1.5))
for k,v in U['routes']['from_pass'].items():
    xs,ys=zip(*v['points']); ax.plot(xs,ys,color='#2a5cc8',alpha=.3,lw=1.1)
Z=[('待機場所の中',(15.5,5.65,17.9,6.45),60,(18.1,6.9)),
   ('受け渡し口から2.5m以内',(11.8,3.45,16.8,8.45),9,(9.9,3.1)),
   ('ドリンクバーのまわり',(14.8,1.8,19.6,5.65),5,(17.4,1.95)),
   ('入口・レジのまわり',(0,0,5.2,5.0),1,(0.15,4.55)),
   ('左の2本の通路',(1.2,5.2,2.7,14.5),7,(2.9,14.75)),
   ('島の下の入口(x6〜8・y4〜6)',(6,4,8,6),27,(6.1,3.55))]
for name,(x0,y0,x1,y1),n,(tx,ty) in Z:
    ax.add_patch(P.Rectangle((x0,y0),x1-x0,y1-y0,fc='#d33',alpha=min(.45,.05+n/140),ec='#d33',lw=1.4))
    ax.text(tx,ty,f'{name} {n}回',fontproperties=fp,fontsize=9.5,color='#b00',bbox=dict(fc='w',ec='#d33',lw=.8,pad=2))
ax.add_patch(P.Rectangle((1.2,5.2),1.5,9.3,fc='#d33',alpha=.12,ec='#d33',lw=1.4))
ax.add_patch(P.Rectangle((5.25,5.2),1.5,9.3,fc='#d33',alpha=.12,ec='#d33',lw=1.4))
ax.set_xlim(-1.5,W+.5); ax.set_ylim(-.8,D+.5); ax.set_aspect('equal')
ax.set_xticks(range(0,int(W)+1,1)); ax.set_yticks(range(0,int(D)+1,1)); ax.grid(alpha=.15); ax.tick_params(labelsize=7)
ax.set_title('直した⑥と、ロボット2台の夜(種10個)の30秒以上の止まり 計122回のうちの場所ごとの数',fontproperties=fp,fontsize=12)
plt.tight_layout(); plt.savefig('store6_04_night_stops.png',dpi=120)
場所30秒以上の止まりおもな理由
待機場所の中60回もう1台のロボット 59
左の島の下の入口(x 6〜8m・y 4〜6m)
27回
人が立ち止まっていた 9・スタッフ 7・お客さん 6・もう1台 4
受け渡し口から2.5m以内(待機場所の中を除く)
9回スタッフ 7
左の2本の通路7回お客さん 5
ドリンクバーのまわり5回もう1台 4
入口・レジのまわり1回お客さん 1

待機場所の中の60回は、ほぼ全部がロボットどうしです。往復を終えて待機場所へ戻るロボットと、待機場所から受け渡し口へ出るロボットが、1.2mの間で出会っています。③⑦④⑤でも、受け渡し口と待機場所のまわりは止まりの多い所でした。店ごとに図面を直しても、待機場所が受け渡し口のすぐ横にあるという決め方が同じなので、同じ詰まりが出ます。

左の島の下の入口は、受け渡し口から左側の卓へ行くロボットの道と、入口から左の席へ向かうお客さん・案内するスタッフの道が交わる所です。ロボットの道は y 4.9m あたりを1本で走っていて、ここだけ人の動きと重なります。

入口・レジのまわりでは、スタッフがロボットに道をゆずって待ったのが、1台で44回、2台で37回ありました。人どうしの詰まりは0回で、待合と会計の列の重なりは、今の記録の上では止まりとして出ていません。

作業ログ ― ⑥はここで区切る

④⑤と同じく、⑥もここで区切りにしました。残った詰まりは直し続けず、記録して調べる対象にします。直すなら、店の形(サイト)を先に直します。

決めたこと・残したこと中身
⑥の店の形
ドリンクバーを左へ0.8m、レジを縦のまま右へ0.8m・下の壁まで。版 2e9fc773050a
ドリンクバーに並ぶ点
真ん中の前に置けないときは、前の幅の中で左右へずらす(サイトの作り方の決まり)
⑥の試算
⑥の3,066条件だけ計算し直した(v2.8 のまま)
Unity で回す店
③⑦④⑤⑥の15条件(serving-robot-params.json)
記録だけにしたもの
試算=レジ・3Dの店=片付けと案内、という詰まる所のずれ。待機場所の中と左の島の下の入口の詰まり。待合と会計の列の重なり。建てたときの3つの表示
5店に共通の詰まり
待機場所が受け渡し口のすぐ横(1.2〜1.5m)にあり、戻るロボットと出るロボットが出会う。店ごとの直しではなく、待機場所の決め方の問題として記録した