""" サービスロボット導入シミュレーション #1 配膳ロボット(試作 v2.2) 飲食店のピーク時間を SimPy で再現する。 入店待ちの内訳と「店が回らない」(v2.2) 入店待ちは、店に来てから案内されて席に着くまで(entry_wait_mean)。 そのうち、満席で待った時間(full_wait_mean)と、席は空いているのに片付け(下げ膳・卓拭き)や 案内が終わらず待った時間(staff_wait_mean)に分ける。staff_wait_mean が NG_MIN 分以上なら「店が回らない」。 片付け待ちは、客が帰ってから下げ膳が終わるまでの卓の数だけ、入口の列の前から数えた組が 「席はあるのに待っている」とみなして数える。乱数の使い方は変えていないので、ほかの数字は v2 と同じ。 時間の区切り方(v2.1) ページに出す数字(ピークの回し方):空の店に peak_min 分だけ選んだペースで来客し、全員が帰るまで回す。 ランチ・ディナーのように、混む時間は限られている前提。v2 と同じで、数字も v2 と同じになる。 回るかの確かめ(--sustain。v2.2 のページでは使わない):空の店から始め、来客は選んだペースのまま止めない。 最初の warmup_min 分は数えず、そのあとの peak_min 分に来た組を、卓が空くまで追いかける。 その時間のスタッフの手がふさがっている割合(staff_util)を出し、STAFF_FULL 以上なら「店が回らない」とする。 入店待ちの延び方(seat_wait_trend)は、卓の数を超える客が来ると人手に関係なく延びるので、判定には使わない (混雑は多くの店で卓の数を超えている。そのときの行列は、ピーク2時間の入店待ちの数字に出ている)。 店のタイプ(5種)、ロボットの使い方(3種)、1往復で回る卓の数(段の数)、 倒れやすい物(デザート・ドリンク)を誰が運ぶか、を切り替えて比べられる。 数字はすべて仮置き。公開事例・メーカー仕様・観察で順に置き換える。 SimPy が扱うのは「店の図面がなくても答えられる問い」(時間・順番・数)だけ。 通路での鉢合わせ・接触・揺れ・こぼれは Unity で扱い、その結果を 「一時停止」「立ち往生」の数字や、ロボットに載せる皿の決まりとしてここに入れる。 ロボットの使い方 serve : 上げ膳だけ。下げ膳は人がやる(ガスト型) bus : 下げ膳にも使う。皿はスタッフがロボットに載せる(サイゼリヤ・ブロンコビリー型) patrol : 食事中に席を回り、客が空いた皿を載せる(焼肉の和民・かみむら牧場型) 使い方: pip install simpy python haizen_sim.py # 総当たりを回して results.json を書く python haizen_sim.py --main # よく使う人数・台数(0〜3台)だけ先に回す 途中経過は results_partial.json に保存され、止まっても続きから回る python haizen_sim.py --one # 1条件だけ回して結果を表示 python haizen_sim.py --sustain # 回るかの確かめを総当たりで回して results_sustain.json を書く 依存: Python 3.10 以降 / simpy 4.1 """ import json import random import statistics import sys from itertools import product from multiprocessing import Pool import simpy # --------------------------------------------------------------- # 共通の前提(すべて仮) # --------------------------------------------------------------- COMMON = { "staff_speed": 1.0, # スタッフが歩く速さ(m/秒) "robot_speed": 0.5, # ロボットが店内を走る速さ(m/秒) "guide_s": 45, # 案内(歩く時間は別) "staff_place_s": 10, # スタッフが皿を置く "robot_handoff_s": 25, # 客がロボットから皿を取る "pay_s": 90, # 会計 "call_s": 30, # 呼び出し対応(歩く時間は別) "clear_s": 40, # テーブルの皿をまとめる "wipe_s": 40, # 卓を拭いてセットする "load_s": 15, # まとめた皿をロボットに載せる "guest_load_s": 20, # 客が空いた皿をロボットに載せる(巡回回収) "pause_rate": 0.3, # 一時停止:1往復で、人が前を横切って止まる確率(自然に解ける) "pause_s": (3, 10), # 一時停止の長さ(秒) "stuck_rate": 0.01, # 立ち往生:1往復で、スタッフが動かしに行かないと解けない確率 "rescue_s": 15, # スタッフがロボットを動かす作業 "hop_m": 3, # 1往復で複数卓を回るとき、卓から次の卓への寄り道(m) "fragile_cook": (2, 4, 7), # デザート・ドリンクができるまで(分) "cold_min": 3, # これを超えて届いた皿を「待たせた皿」とする "dish_extra_m": 0, # 洗い場までの追加距離(店のタイプで上書き) "sustain": False, # True:回るかの確かめ(来客を止めない回し方) "warmup_min": 60, # 回るかの確かめで、数えない慣らしの時間(分) "util_sample_s": 10, # 手がふさがっている人数・台数を数える間隔(秒) "max_extra_min": 600, # 数える時間のあと、追いかけるのを打ち切る上限(分)。店が回らない条件の保険 } # --------------------------------------------------------------- # 店のタイプ(すべて仮) # dist_m : 厨房(洗い場)から卓までの距離の幅 # first_dishes : 最初の注文の皿。料理ができるまで(最短, 最頻, 最長 分) # extra_orders : 食事中の追加注文の回数(食べ放題) # bus_trips : 下げ膳を人だけでやるときの往復回数(皿の量) # dish_extra_m : 洗い場が厨房の受け渡し口よりさらに奥にあるときの追加距離(ホテル小倉の事例) # fragile : 1組あたりの倒れやすい物(デザート・パフェ・ドリンク)の数の平均 # staff : 最初に見せる人数 # staff_all : 「その他」で選べる人数(総当たりはこちら) # arrivals : 来客ペース(空いている・普通・混雑) # --------------------------------------------------------------- STORES = { "small": { "label": "小型店", "tables": 18, "dist_m": (4, 12), "peak_min": 120, "first_dishes": [(4, 7, 11), (6, 9, 14)], "extra_orders": 0, "extra_cook": (3, 5, 8), "eat_min": (15, 25, 40), "call_rate": 0.4, "bus_trips": 1, "fragile": 0.3, "staff": [1, 2, 3], "staff_all": [1, 2, 3, 4, 5], "arrivals": [15, 22, 30], }, "family": { "label": "標準ファミレス", "tables": 35, "dist_m": (6, 22), "peak_min": 120, "first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 0, "extra_cook": (3, 5, 8), "eat_min": (20, 30, 45), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.5, "staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [25, 35, 45], }, "large": { "label": "大型店", "tables": 60, "dist_m": (8, 35), "peak_min": 120, "first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 0, "extra_cook": (3, 5, 8), "eat_min": (20, 30, 45), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.5, "staff": [4, 5, 6], "staff_all": [2, 3, 4, 5, 6, 7, 8, 9, 10], "arrivals": [45, 60, 75], }, "hotel": { "label": "洗い場が遠い店", "tables": 30, "dist_m": (6, 20), "dish_extra_m": 30, "peak_min": 120, "first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 2, "extra_cook": (4, 6, 10), "eat_min": (30, 45, 60), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.6, "staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [16, 22, 28], }, "buffet": { "label": "焼肉食べ放題", "tables": 35, "dist_m": (6, 22), "peak_min": 150, "first_dishes": [(2, 4, 8), (3, 5, 9), (4, 7, 12)], "extra_orders": 5, "extra_cook": (2, 4, 7), "eat_min": (60, 80, 90), "call_rate": 0.8, "bus_trips": 3, "fragile": 1.5, "staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [14, 20, 26], }, } ROBOTS = [0, 1, 2, 3] # 最初に見せる台数 ROBOTS_ALL = list(range(7)) # 「その他」で選べる台数(総当たりはこちら) MODES = ["serve", "bus", "patrol"] TRAYS = [1, 3] # 1往復で回る卓の数(1卓ずつ/最大3卓) FRAGILE_BY = ["any", "staff"] # 倒れやすい物を ロボットも運ぶ/人だけが運ぶ REPS = 40 def tri(rng, lmh): lo, mode, hi = lmh return rng.triangular(lo, hi, mode) class Store: def __init__(self, env, rng, p): self.env, self.rng, self.p = env, rng, p self.tables = simpy.Resource(env, capacity=p["tables"]) self.staff = simpy.PriorityResource(env, capacity=p["staff"]) self.robots = simpy.PriorityResource(env, capacity=p["robots"]) if p["robots"] else None self.dish_waits, self.seat_waits, self.turns = [], [], [] self.staff_walk_m = self.staff_busy_s = self.robot_busy_s = 0.0 self.robot_trips = self.staff_trips = self.rescues = self.collects = self.done = 0 self.robot_bus = self.staff_bus = 0 self.fragile_robot = self.pauses = 0 self.pass_q = [] # 受け渡し口でロボットを待っている料理 # 回るかの確かめ(sustain)で数える時間。ピークの回し方では全部の組を数える self.t0 = p["warmup_min"] * 60 if p["sustain"] else 0 self.t1 = self.t0 + p["peak_min"] * 60 self.counted = self.counted_done = 0 self.window_closed = False self.finished = env.event() self.capped = False self.seat_wait_by_arrival = [] # (来た時刻, 入店待ち 分)。待ちが伸び続けるかを見る self.staff_busy_n = [] # 数える時間のあいだ、一定間隔で数えた手がふさがっている人数 self.robot_busy_n = [] self.snap0 = self.snap1 = None # 入店待ちの内訳(v2.2) # 片付けが終わらず使えない卓(客は帰ったが下げ膳・卓拭きが済んでいない)の数 # 入口の列の前から数えて、その数までの組は「席はあるのに待っている」とみなす self.dirty = 0 self._acc_t = 0.0 self.blocked = {} # 卓の順番待ちの request → 片付け待ちで待った秒数 self.blocked_waits = [] # 組ごと:席はあるのに片付けが終わらず待った時間(分) self.guide_waits = [] # 組ごと:卓が決まってから、案内のスタッフの手が空くまで(分) self.entry_waits = [] # 組ごと:店に来てから、案内されて席に着くまで(分) def _acc(self): """前回からの時間を、列の前から片付け待ちの卓の数までの組に足す(列や卓の数が変わる直前に呼ぶ)""" now = self.env.now dt = now - self._acc_t if dt > 0 and self.dirty > 0: for req in self.tables.queue[:self.dirty]: self.blocked[req] = self.blocked.get(req, 0.0) + dt self._acc_t = now SNAP = ("staff_walk_m", "robot_trips", "staff_trips", "rescues", "collects", "robot_bus", "staff_bus", "fragile_robot", "pauses") def _snap(self): return {k: getattr(self, k) for k in self.SNAP} def window(self): """数える時間の始めと終わりに数を控え、そのあいだは手がふさがっている人数・台数を数える""" env, p = self.env, self.p yield env.timeout(self.t0) self.snap0 = self._snap() while env.now < self.t1: self.staff_busy_n.append(self.staff.count) if self.robots is not None: self.robot_busy_n.append(self.robots.count) yield env.timeout(p["util_sample_s"]) self.snap1 = self._snap() self.window_closed = True self._check_finished() yield env.timeout(p["max_extra_min"] * 60) if not self.finished.triggered: # 店が回らず、数える組が出きらない self.capped = True self.finished.succeed() def _check_finished(self): if self.window_closed and self.counted_done >= self.counted and not self.finished.triggered: self.finished.succeed() # ---- スタッフの仕事(priority 0=ロボット救出, 1=配膳・案内・会計, 2=下げ膳) def staff_job(self, priority, walk_m, work_s, waited=None): t0 = self.env.now with self.staff.request(priority=priority) as req: yield req if waited is not None: waited.append(self.env.now - t0) # 手が空くのを待った時間(案内待ち) t = walk_m / self.p["staff_speed"] + work_s self.staff_walk_m += walk_m self.staff_busy_s += t yield self.env.timeout(t) # ---- ロボットが途中で止まる(1往復に1回判定) def maybe_stop(self, d, helper_waiting=False): p, rng = self.p, self.rng if rng.random() < p["pause_rate"]: # 一時停止:数秒で自然に解ける self.pauses += 1 yield self.env.timeout(rng.uniform(*p["pause_s"])) if rng.random() < p["stuck_rate"]: # 立ち往生:スタッフが動かしに行く self.rescues += 1 if helper_waiting: # 卓でロボットを待っているスタッフがそのまま動かす yield self.env.timeout(d / p["staff_speed"] + p["rescue_s"]) else: yield self.env.process(self.staff_job(0, d, p["rescue_s"])) # ---- 配膳 # 料理ができたら、ロボットとスタッフのうち先に空いた方が運ぶ(同時ならロボット)。 # ロボットが出るときは、受け渡し口で待っている料理を段の数までまとめて載せ、近い卓から回る。 def deliver(self, d, fragile=False, counted=True): env, p = self.env, self.p rec = {"d": d, "ready": env.now, "fragile": fragile, "counted": counted, "taken": env.event(), "arrived": env.event()} robot_ok = self.robots is not None and not (fragile and p["fragile_by"] == "staff") sreq = self.staff.request(priority=1) if not robot_ok: yield sreq yield env.process(self._staff_carry(rec, sreq)) return rreq = self.robots.request(priority=0) rec["sreq"], rec["rreq"] = sreq, rreq self.pass_q.append(rec) yield rreq | sreq | rec["taken"] if rec["taken"].triggered: # ほかの料理と一緒にロボットに載った yield rec["arrived"] return self.pass_q.remove(rec) if rreq.triggered: # ロボットが運ぶ self._drop(sreq, self.staff) batch = [rec] for o in list(self.pass_q): if len(batch) >= p["trays"]: break self.pass_q.remove(o) self._drop(o["rreq"], self.robots) self._drop(o["sreq"], self.staff) o["taken"].succeed() batch.append(o) env.process(self._robot_run(batch, rreq)) yield rec["arrived"] return rreq.cancel() # スタッフが運ぶ yield env.process(self._staff_carry(rec, sreq)) @staticmethod def _drop(req, res): if req.triggered: res.release(req) else: req.cancel() def _robot_run(self, batch, rreq): env, p = self.env, self.p start = env.now batch.sort(key=lambda r: r["d"]) pos = 0.0 for i, rec in enumerate(batch): leg = rec["d"] - pos + (p["hop_m"] if i else 0) yield env.timeout(max(leg, 0) / p["robot_speed"]) if i == 0: yield env.process(self.maybe_stop(rec["d"])) if rec["counted"]: self.dish_waits.append((env.now - rec["ready"]) / 60) self.robot_trips += 1 self.fragile_robot += rec["fragile"] rec["arrived"].succeed() yield env.timeout(p["robot_handoff_s"]) pos = rec["d"] yield env.timeout(pos / p["robot_speed"]) self.robot_busy_s += env.now - start self.robots.release(rreq) def _staff_carry(self, rec, sreq): env, p = self.env, self.p self.staff_trips += 1 go = rec["d"] / p["staff_speed"] yield env.timeout(go) if rec["counted"]: self.dish_waits.append((env.now - rec["ready"]) / 60) rec["arrived"].succeed() yield env.timeout(p["staff_place_s"] + go) self.staff_walk_m += 2 * rec["d"] self.staff_busy_s += 2 * go + p["staff_place_s"] self.staff.release(sreq) # ---- 巡回回収(食事中にロボットが来て、客が空いた皿を載せる) def collect_visit(self, d, deadline, box): env, p = self.env, self.p req = self.robots.request(priority=2) timer = env.timeout(max(deadline - env.now, 0)) res = yield req | timer if req not in res: req.cancel() return start = env.now t = d / p["robot_speed"] back = (d + p["dish_extra_m"]) / p["robot_speed"] yield env.timeout(t) yield env.process(self.maybe_stop(d)) yield env.timeout(p["guest_load_s"] + back) self.robot_busy_s += env.now - start self.robots.release(req) self.collects += 1 box[0] += 1 # ---- 下げ膳(客が帰ってから卓が使えるようになるまで) def bus(self, d, collected, counted=True): env, p = self.env, self.p start = env.now trips = max(p["bus_trips"] - collected, 0) if p["mode"] == "bus" and self.robots is not None and trips > 0: rreq = self.robots.request(priority=1) if rreq.triggered: # 空いているロボットがいれば呼ぶ self.robot_bus += 1 arrive = env.event() env.process(self._robot_bus_trip(d, rreq, arrive)) # スタッフは卓まで行き、皿をまとめ、ロボットが来たら載せ、卓を拭いて戻る with self.staff.request(priority=2) as sreq: yield sreq s0 = env.now yield env.timeout(d / p["staff_speed"] + p["clear_s"]) yield arrive yield env.timeout(p["load_s"] + p["wipe_s"]) loaded_at = env.now yield env.timeout(d / p["staff_speed"]) self.staff_walk_m += 2 * d self.staff_busy_s += env.now - s0 arrive.loaded = loaded_at if counted: self.turns.append((loaded_at - start) / 60) return rreq.cancel() # 人だけで下げる(皿の量に応じて往復)。皿がもうなければ拭きに行くだけ self.staff_bus += 1 walk = 2 * (d + p["dish_extra_m"]) * trips if trips > 0 else 2 * d work = (p["clear_s"] if trips > 0 else 0) + p["wipe_s"] yield env.process(self.staff_job(2, walk, work)) if counted: self.turns.append((env.now - start) / 60) def _robot_bus_trip(self, d, rreq, arrive): env, p = self.env, self.p start = env.now t = d / p["robot_speed"] yield env.timeout(t) yield env.process(self.maybe_stop(d, helper_waiting=True)) arrive.succeed() back = (d + p["dish_extra_m"]) / p["robot_speed"] yield env.timeout(p["load_s"] + back + 15) # 洗い場で降ろすのは厨房側 self.robot_busy_s += env.now - start self.robots.release(rreq) # ---- 1組の流れ def group(self): env, p, rng = self.env, self.p, self.rng arrive = env.now counted = (self.t0 <= arrive < self.t1) if p["sustain"] else True # 数える時間に来た組だけを記録する if counted: self.counted += 1 d = rng.uniform(*p["dist_m"]) self._acc() table = self.tables.request() yield table blocked = self.blocked.pop(table, 0.0) if counted: w = (env.now - arrive) / 60 self.seat_waits.append(w) self.seat_wait_by_arrival.append((arrive, w)) gw = [] yield env.process(self.staff_job(1, 2 * d, p["guide_s"], gw)) if counted: self.blocked_waits.append(blocked / 60) self.guide_waits.append(gw[0] / 60) self.entry_waits.append((env.now - arrive) / 60) firsts = [env.process(self._cook_then_deliver(c, d, False, counted)) for c in p["first_dishes"]] yield simpy.AllOf(env, firsts) eat = tri(rng, p["eat_min"]) * 60 end = env.now + eat # 食事中の追加注文・呼び出し・巡回回収 extras = [] for _ in range(p["extra_orders"]): extras.append(env.process(self._order_later(rng.uniform(0, eat * 0.7), d, p["extra_cook"], False, counted))) n_fragile = int(p["fragile"]) + (rng.random() < p["fragile"] - int(p["fragile"])) for _ in range(n_fragile): extras.append(env.process(self._order_later(rng.uniform(0.3, 0.85) * eat, d, p["fragile_cook"], True, counted))) calls = sum(rng.random() < p["call_rate"] / 2 for _ in range(2)) for _ in range(calls): env.process(self._call_later(rng.uniform(0, eat), d)) collected = [0] if p["mode"] == "patrol" and self.robots is not None: for k in range(p["bus_trips"]): at = eat * (k + 1) / (p["bus_trips"] + 1) env.process(self._collect_later(at, d, end, collected)) yield env.timeout(eat) if extras: yield simpy.AllOf(env, extras) yield env.process(self.staff_job(1, 0, p["pay_s"])) self._acc() self.dirty += 1 # 客は帰った。片付けが済むまで卓は使えない yield env.process(self.bus(d, collected[0], counted)) self._acc() self.dirty -= 1 self.tables.release(table) self.done += 1 if counted: self.counted_done += 1 self._check_finished() def _cook_then_deliver(self, cook, d, fragile=False, counted=True): yield self.env.timeout(tri(self.rng, cook) * 60) yield self.env.process(self.deliver(d, fragile, counted)) def _order_later(self, delay, d, cook, fragile, counted=True): yield self.env.timeout(delay) yield self.env.process(self._cook_then_deliver(cook, d, fragile, counted)) def _call_later(self, delay, d): yield self.env.timeout(delay) yield self.env.process(self.staff_job(1, 2 * d, self.p["call_s"])) def _collect_later(self, delay, d, deadline, box): yield self.env.timeout(delay) yield self.env.process(self.collect_visit(d, deadline, box)) def arrivals(self): env, p, rng = self.env, self.p, self.rng rate = p["arrivals_per_h"] / 3600 while not self.finished.triggered: yield env.timeout(rng.expovariate(rate)) if not p["sustain"] and env.now > p["peak_min"] * 60: return # ピークの回し方:peak_min 分で来客が止まる env.process(self.group()) # 回るかの確かめ:来客は止めない(数える組が出きるまで) UNSTABLE_MIN = 5 # 参考:数える時間のうちに入店待ちがこれ以上延びていく条件(判定には使わない。卓が足りない店の見分けに使う) NG_MIN = 10 # 席は空いているのに待った時間(staff_wait_mean)がこれ以上なら「店が回らない」とする(分) STAFF_FULL = 0.95 # 回るかの確かめで、スタッフの手がこれ以上ふさがっていたら「店が回らない」とする def run_once(params, seed): store = STORES[params["store"]] p = {**COMMON, **store, **params} env = simpy.Environment() s = Store(env, random.Random(seed), p) env.process(s.arrivals()) if p["sustain"]: return _sustain_result(env, s, p) env.run() end = env.now w = s.dish_waits return { "dish_wait_mean": statistics.mean(w), "dish_wait_p90": sorted(w)[int(0.9 * (len(w) - 1))], "cold_share": sum(x > p["cold_min"] for x in w) / len(w), "seat_wait_mean": statistics.mean(s.seat_waits), "turn_mean": statistics.mean(s.turns) if s.turns else 0.0, "walk_km_per_staff": s.staff_walk_m / p["staff"] / 1000, "staff_util": s.staff_busy_s / (p["staff"] * end), "robot_util": (s.robot_busy_s / (p["robots"] * end)) if p["robots"] else 0.0, "robot_share": s.robot_trips / max(s.robot_trips + s.staff_trips, 1), "robot_bus_share": s.robot_bus / max(s.robot_bus + s.staff_bus, 1), "collects": s.collects, "rescues": s.rescues, "pauses": s.pauses, "fragile_robot": s.fragile_robot, "groups": s.done, # 入店待ちの内訳(v2.2) "entry_wait_mean": statistics.mean(s.entry_waits), # 来てから案内されて席に着くまで "blocked_wait_mean": statistics.mean(s.blocked_waits), # 席はあるのに片付けが終わらず待った "guide_wait_mean": statistics.mean(s.guide_waits), # 卓は決まったが案内の手が空かず待った "staff_wait_mean": statistics.mean(b + g for b, g in zip(s.blocked_waits, s.guide_waits)), # 席はあるのに待った(合計) "full_wait_mean": statistics.mean(w - b for w, b in zip(s.seat_waits, s.blocked_waits)), # 席が埋まっていて待った } def _sustain_result(env, s, p): """回るかの確かめ:来客を止めずに回し、数える時間の結果を出す""" env.process(s.window()) env.run(until=s.finished) win_s = p["peak_min"] * 60 q = win_s / 4 first = [x for t, x in s.seat_wait_by_arrival if t < s.t0 + q] last = [x for t, x in s.seat_wait_by_arrival if t >= s.t1 - q] trend = (statistics.mean(last) - statistics.mean(first)) if first and last else 0.0 return { "seat_wait_mean": statistics.mean(s.seat_waits), "seat_wait_trend": trend, "staff_util": statistics.mean(s.staff_busy_n) / p["staff"], "capped": 1.0 if s.capped else 0.0, } def conditions(main_only=False): for key, st in STORES.items(): staffs = st["staff"] if main_only else st["staff_all"] robots_list = ROBOTS if main_only else ROBOTS_ALL for staff, robots, arr, mode, trays, fb in product(staffs, robots_list, st["arrivals"], MODES, TRAYS, FRAGILE_BY): if robots == 0 and (mode != "serve" or trays != 1 or fb != "any"): continue # ロボットなしなら、使い方・段の数・使い分けは関係ない yield {"store": key, "staff": staff, "robots": robots, "arrivals_per_h": arr, "mode": mode, "trays": trays, "fragile_by": fb} def run_condition(params): runs = [run_once(params, seed) for seed in range(REPS)] agg = {k: round(statistics.mean(r[k] for r in runs), 4) for k in runs[0]} return {**params, **agg} def key(c): return (c["store"], c["staff"], c["robots"], c["arrivals_per_h"], c["mode"], c["trays"], c["fragile_by"]) def model_sig(common, stores): """結果に効く前提だけを取り出す(人数・台数の選択肢や名前は含めない)""" skip = {"label", "staff", "staff_all", "arrivals"} return json.loads(json.dumps({"common": common, "reps": REPS, "stores": {k: {a: b for a, b in v.items() if a not in skip} for k, v in stores.items()}})) if __name__ == "__main__": if "--one" in sys.argv: for mode in MODES: for trays, fb in product(TRAYS, FRAGILE_BY): r = run_condition({"store": "family", "staff": 3, "robots": 2, "arrivals_per_h": 35, "mode": mode, "trays": trays, "fragile_by": fb}) print(mode, trays, fb, {k: round(v, 2) for k, v in r.items() if isinstance(v, float)}) elif "--sustain" in sys.argv: # 回るかの確かめ:全条件を、来客を止めない回し方で回す conds = [{**c, "sustain": True} for c in conditions()] done = {} try: with open("results_sustain_partial.json", encoding="utf-8") as f: old = json.load(f) if old.get("common") == COMMON and old.get("reps") == REPS and old.get("unstable_min") == UNSTABLE_MIN: done = {key(r): r for r in old["rows"]} except (FileNotFoundError, KeyError, TypeError, json.JSONDecodeError): pass def save_s(path, rows): out = {"common": COMMON, "reps": REPS, "unstable_min": UNSTABLE_MIN, "rows": rows} with open(path + ".tmp", "w", encoding="utf-8") as f: json.dump(out, f, ensure_ascii=False) import os os.replace(path + ".tmp", path) todo = [c for c in conds if key(c) not in done] print(f"{len(conds)} conditions, {len(conds) - len(todo)} reused, {len(todo)} to run", flush=True) with Pool() as pool: for i, r in enumerate(pool.imap_unordered(run_condition, todo, chunksize=2), 1): done[key(r)] = r if i % 100 == 0: save_s("results_sustain_partial.json", list(done.values())) print(f" {i}/{len(todo)}", flush=True) save_s("results_sustain_partial.json", list(done.values())) save_s("results_sustain.json", [done[key(c)] for c in conds]) print(len(conds), "conditions written", flush=True) else: conds = list(conditions(main_only="--main" in sys.argv)) sig = model_sig(COMMON, STORES) # 前提が同じなら、前回の結果と途中経過を使い回して足りない条件だけ回す cache = {} for path in ("results.json", "results_partial.json"): try: with open(path, encoding="utf-8") as f: old = json.load(f) if model_sig(old["common"], old["stores"]) == sig and old.get("reps") == REPS: cache.update({key(r): r for r in old["rows"]}) except (FileNotFoundError, KeyError, TypeError, json.JSONDecodeError): pass def save(path, rows): out = {"common": COMMON, "stores": STORES, "robots": ROBOTS, "robots_all": ROBOTS_ALL, "modes": MODES, "trays": TRAYS, "fragile_by": FRAGILE_BY, "reps": REPS, "rows": rows} with open(path + ".tmp", "w", encoding="utf-8") as f: json.dump(out, f, ensure_ascii=False) import os os.replace(path + ".tmp", path) todo = [c for c in conds if key(c) not in cache] print(f"{len(conds)} conditions, {len(conds) - len(todo)} reused, {len(todo)} to run", flush=True) with Pool() as pool: for i, r in enumerate(pool.imap_unordered(run_condition, todo, chunksize=2), 1): cache[key(r)] = r if i % 100 == 0: save("results_partial.json", list(cache.values())) print(f" {i}/{len(todo)}", flush=True) save("results_partial.json", list(cache.values())) save("results.json", [cache[key(c)] for c in conds]) print(len(conds), "conditions written", flush=True)