# -*- coding: utf-8 -*- """仮の病院のフロアの形(同じ階・案 h0)を作る。 サービスロボット導入シミュレーション 病院の搬送ロボット(検体・薬剤)用。 実在の病院ではなく、外来・検査部・薬剤部・外来化学療法室・救急室の 典型的な並びをコードで一から起こしたもの(2026-11-09 エイイチと決めた)。 決まり - 廊下の幅は医療法施行規則 第16条1項11号の一般の廊下に合わせる:中廊下 2.1m 以上 - 外来の大廊下(待合に面する)は 4.0m(仮置き。人が多い所) - ロボットが通れる最小の幅は W3 の公表値 0.7m。全部の廊下がこれを超える - 距離は廊下の中心線の上で測る(扉の点 → 廊下 → 扉の点)。試算はこの距離を使う 使い方:python3 make_hospital.py 出力先 [h0|h0b] 出力:hospital_{h0,h0b}.json(形・3ルートの道と距離・点と点の距離 distances)・hospital_h0.png(平面図) """ import heapq import json import sys from pathlib import Path W, D = 120.0, 60.0 # 外寸 m(x 右、y 奥) HALL_Y0, HALL_Y1 = 27.0, 31.0 # 外来の大廊下 4.0m STAFF_Y0, STAFF_Y1 = 13.0, 15.1 # 職員の中廊下 2.1m HALL_C = (HALL_Y0 + HALL_Y1) / 2 STAFF_C = (STAFF_Y0 + STAFF_Y1) / 2 CROWD_X0, CROWD_X1 = 0.0, 70.0 # 外来の大廊下のうち受付・会計・待合の前(人の多い区間) CONNECT_X = {"西の縦廊下": 24.0, "玄関の通り": 48.0, "東の縦廊下": 99.0} CONNECT_W = {"西の縦廊下": 2.1, "玄関の通り": 8.0, "東の縦廊下": 2.1} ROOMS = [ # 名前, x0, y0, x1, y1, 種類 ("内科外来", 0, 31, 42, 60, "外来"), ("ほかの外来", 42, 31, 96, 60, "外来"), ("外来化学療法室", 96, 31, 120, 60, "届け先"), ("薬剤部", 0, 0, 23, 13, "出発"), ("お薬の窓口", 0, 15.1, 23, 27, "その他"), ("総合受付・会計・待合", 25.1, 15.1, 70, 27, "混む所"), ("倉庫", 25.1, 0, 44, 13, "その他"), ("正面玄関", 44, 0, 52, 13, "混む所"), ("医事・事務", 52, 0, 70, 13, "その他"), ("中央検査室", 70, 0, 98, 13, "届け先"), ("生理検査", 70, 15.1, 98, 27, "その他"), ("救急室", 100.1, 0, 120, 27, "出発"), ] POINTS = { # 扉の前の点(ロボットが積む・受け取る所)。名前: (x, y, つながる廊下の中心の点) "内科外来の処置室": (12.0, HALL_Y1, (12.0, HALL_C)), "外来化学療法室の受付": (104.0, HALL_Y1, (104.0, HALL_C)), "薬剤部の払い出し口": (13.0, STAFF_Y0, (13.0, STAFF_C)), "検査部の検体受付": (80.0, STAFF_Y0, (80.0, STAFF_C)), "救急室の処置室": (108.0, STAFF_Y0 + 0.0, (108.0, STAFF_C)), } # 救急室は x100.1〜 にあり、中廊下の南ではなく東の端。扉は中廊下の東の突き当たりに出す POINTS["救急室の処置室"] = (100.1, STAFF_C, (99.0, STAFF_C)) ROUTES = { "green": ("救急室の処置室", "検査部の検体受付", "検体(救急・病棟)"), "blue": ("内科外来の処置室", "検査部の検体受付", "採血管・PCR/抗原・尿"), "red": ("薬剤部の払い出し口", "外来化学療法室の受付", "抗がん剤・前投薬"), } # 案 h0b(2026-11-09 エイイチと決めた「大廊下を通らない道」): # 外来の奥(北の壁ぞい)に職員通路 2.1m を足し、内科外来の処置室の裏口から積む。 # 北の職員通路 → 東の縦廊下(x99)で大廊下の人の少ない東の端を横切る → 職員の中廊下 → 検査部。 # h0 の部屋・廊下・点はそのまま(内科外来・ほかの外来は奥行きが2.1m 縮み、外来化学療法室は縦廊下の分だけ右へ縮む)。 NORTH_Y0, NORTH_Y1 = D - 2.1, D NORTH_C = (NORTH_Y0 + NORTH_Y1) / 2 NORTH_X1 = 100.05 NORTH_CONNECT_X = 99.0 VARIANTS = { "h0": {"name": "仮の病院 h0(外来の階)", "north": False}, "h0b": {"name": "仮の病院 h0b(外来の階・外来の奥に職員通路)", "north": True}, } def apply_variant(v): """h0b のときだけ、部屋・点・ルートを差し替える(h0 は何もしない)""" global ROOMS, POINTS, ROUTES if not VARIANTS[v]["north"]: return rooms = [] for n, x0, y0, x1, y1, k in ROOMS: if n in ("内科外来", "ほかの外来"): y1 = NORTH_Y0 if n == "ほかの外来": x1 = NORTH_CONNECT_X - 1.05 if n == "外来化学療法室": x0 = NORTH_CONNECT_X + 1.05 rooms.append((n, x0, y0, x1, y1, k)) ROOMS = rooms POINTS = dict(POINTS) POINTS["内科外来の処置室(裏口)"] = (12.0, NORTH_Y0, (12.0, NORTH_C)) ROUTES = dict(ROUTES) ROUTES["blue"] = ("内科外来の処置室(裏口)", "検査部の検体受付", "採血管・PCR/抗原・尿") def build_graph(variant="h0"): nodes = set() edges = {} def add(a, b): d = abs(a[0] - b[0]) + abs(a[1] - b[1]) # 廊下は縦横だけ if abs(a[1] - HALL_C) < 1e-6 and abs(b[1] - HALL_C) < 1e-6: d += 0.01 * crowded_len(a[0], b[0]) # 同じ長さなら人の多い所を通らない方を選ぶ(距離には入れない) edges.setdefault(a, []).append((b, d)) edges.setdefault(b, []).append((a, d)) nodes.update([a, b]) north = VARIANTS[variant]["north"] hall_xs = {0.0, W} | set(CONNECT_X.values()) staff_xs = {0.0, 100.1} | {v for v in CONNECT_X.values() if v <= 100.1} north_xs = {0.0, NORTH_CONNECT_X} for name, (x, y, c) in POINTS.items(): if abs(c[1] - HALL_C) < 1e-6: hall_xs.add(c[0]) elif abs(c[1] - NORTH_C) < 1e-6: north_xs.add(c[0]) else: staff_xs.add(c[0]) add((x, y), c) if north: hall_xs.add(NORTH_CONNECT_X) xs = sorted(north_xs) for a, b in zip(xs, xs[1:]): add((a, NORTH_C), (b, NORTH_C)) add((NORTH_CONNECT_X, NORTH_C), (NORTH_CONNECT_X, HALL_C)) xs = sorted(hall_xs) for a, b in zip(xs, xs[1:]): add((a, HALL_C), (b, HALL_C)) xs = sorted(staff_xs) for a, b in zip(xs, xs[1:]): add((a, STAFF_C), (b, STAFF_C)) for x in CONNECT_X.values(): add((x, STAFF_C), (x, HALL_C)) return edges def crowded_len(xa, xb): lo, hi = min(xa, xb), max(xa, xb) return max(0.0, min(hi, CROWD_X1) - max(lo, CROWD_X0)) def true_len(path): return sum(abs(p[0] - q[0]) + abs(p[1] - q[1]) for p, q in zip(path, path[1:])) def shortest(edges, s, t): dist = {s: 0.0} prev = {} q = [(0.0, s)] while q: d, u = heapq.heappop(q) if u == t: break if d > dist[u]: continue for v, w in edges[u]: nd = d + w if nd < dist.get(v, 1e18) - 1e-9: dist[v] = nd prev[v] = u heapq.heappush(q, (nd, v)) path = [t] while path[-1] != s: path.append(prev[path[-1]]) return dist[t], path[::-1] def on_hall(p, q): return abs(p[1] - HALL_C) < 1e-6 and abs(q[1] - HALL_C) < 1e-6 def main(out_dir, variant="h0"): apply_variant(variant) out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) edges = build_graph(variant) routes = {} for key, (a, b, carries) in ROUTES.items(): pa, pb = POINTS[a][:2], POINTS[b][:2] d, path = shortest(edges, pa, pb) d = true_len(path) hall_m = sum(crowded_len(p[0], q[0]) for p, q in zip(path, path[1:]) if on_hall(p, q)) routes[key] = {"from": a, "to": b, "carries": carries, "distance_m": round(d, 2), "crowded_hall_m": round(hall_m, 2), "path": [list(map(lambda v: round(v, 2), p)) for p in path]} names = list(POINTS) dist = {} for a in names: for b in names: if a != b: dist[f"{a}|{b}"] = round(true_len(shortest(edges, POINTS[a][:2], POINTS[b][:2])[1]), 2) data = { "meta": { "schema_version": "hospital-layout/0.1", "name": VARIANTS[variant]["name"], "units": "m", "coords": "原点は建物の外壁の内側の左下の角。x は右、y は奥(上)。配膳の店の形と同じ向き", "rules": { "staff_corridor_w": STAFF_Y1 - STAFF_Y0, "staff_corridor_w_source": "医療法施行規則 第16条1項11号(一般の中廊下 内法2.1m以上)", "hall_w": HALL_Y1 - HALL_Y0, "hall_w_basis": "仮置き", "robot_min_passage_m": 0.7, "distance": "廊下の中心線の上で、扉の点から扉の点まで(縦横の移動だけ)", "crowded": "外来の大廊下の x70 より西(受付・会計・待合の前)を、人の多い区間として数える", }, }, "layout": { "outer": {"w": W, "d": D}, "rooms": [{"name": n, "rect": [x0, y0, x1, y1], "kind": k} for n, x0, y0, x1, y1, k in ROOMS], "corridors": [ {"name": "外来の大廊下", "rect": [0, HALL_Y0, W, HALL_Y1], "w": HALL_Y1 - HALL_Y0}, {"name": "職員の中廊下", "rect": [0, STAFF_Y0, 100.1, STAFF_Y1], "w": STAFF_Y1 - STAFF_Y0}, ] + [{"name": n, "rect": [x - CONNECT_W[n] / 2, STAFF_Y1, x + CONNECT_W[n] / 2, HALL_Y0], "w": CONNECT_W[n]} for n, x in CONNECT_X.items()] + ([{"name": "外来の奥の職員通路", "rect": [0, NORTH_Y0, NORTH_X1, NORTH_Y1], "w": 2.1}, {"name": "東の縦廊下(北)", "rect": [NORTH_CONNECT_X - 1.05, HALL_Y1, NORTH_CONNECT_X + 1.05, NORTH_Y0], "w": 2.1}] if VARIANTS[variant]["north"] else []), "points": {n: [x, y] for n, (x, y, _) in POINTS.items()}, }, "routes": routes, "distances": dist, } if variant != "h0": # h0 の JSON は中身を変えない(Unity の依頼が md5 で確かめているため) data["meta"]["variant"] = variant (out / f"hospital_{variant}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8") draw(data, out / f"hospital_{variant}.png") for k, r in routes.items(): print(k, r["from"], "→", r["to"], r["distance_m"], "m(人の多い区間", r["crowded_hall_m"], "m)") def draw(data, path): import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib import font_manager from matplotlib.patches import Rectangle for f in font_manager.findSystemFonts(): if "NotoSansCJK" in f or "NotoSansJP" in f: font_manager.fontManager.addfont(f) plt.rcParams["font.family"] = font_manager.FontProperties(fname=f).get_name() break col = {"外来": "#eef1f5", "届け先": "#dce9fb", "出発": "#e3f3e6", "混む所": "#fff1dc", "その他": "#f4f4f4"} rc = {"green": "#2e9d4f", "blue": "#2f6fd6", "red": "#d24b3e"} fig, ax = plt.subplots(figsize=(16, 8.6), dpi=150) for r in data["layout"]["rooms"]: x0, y0, x1, y1 = r["rect"] ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, fc=col[r["kind"]], ec="#9aa3ad", lw=1)) ax.text((x0 + x1) / 2, (y0 + y1) / 2, r["name"], ha="center", va="center", fontsize=11, color="#333") for c in data["layout"]["corridors"]: x0, y0, x1, y1 = c["rect"] ax.add_patch(Rectangle((x0, y0), x1 - x0, y1 - y0, fc="#ffffff", ec="none")) ax.add_patch(Rectangle((CROWD_X0, HALL_Y0), CROWD_X1 - CROWD_X0, HALL_Y1 - HALL_Y0, fc="#fff1dc", ec="none")) ax.text(45, HALL_C, "外来の大廊下 4.0m(受付・待合の前は人が多い)", va="center", ha="center", fontsize=9, color="#8a5a00") ax.text(0.6, STAFF_C, "職員の中廊下 2.1m", va="center", fontsize=8, color="#777") offs = {"green": 0.45, "blue": 0.0, "red": -0.45} for k, r in data["routes"].items(): xs = [p[0] for p in r["path"]] ys = [p[1] + offs[k] for p in r["path"]] ax.plot(xs, ys, color=rc[k], lw=2.6, solid_capstyle="round") ax.plot(xs[0], ys[0], "o", color=rc[k], ms=9) ax.plot(xs[-1], ys[-1], "s", color=rc[k], ms=9) for n, (x, y) in data["layout"]["points"].items(): dy = -1.6 if y > D - 3 else (1.6 if y > 20 else -1.6) ax.text(x, y + dy, n, ha="center", va="center", fontsize=9, color="#222", bbox=dict(fc="white", ec="#bbb", lw=0.6, pad=1.5)) legend = [f"{ {'green':'緑','blue':'青','red':'赤'}[k]} {r['from']} → {r['to']} {r['distance_m']:.0f}m" for k, r in data["routes"].items()] for i, (k, t) in enumerate(zip(data["routes"], legend)): ax.text(1, -3 - i * 2.2, "●", color=rc[k], fontsize=12, va="center") ax.text(3, -3 - i * 2.2, t, fontsize=11, va="center") ax.text(60, -3, "● 出発(積む所) ■ 届け先 オレンジ=人の多い所", fontsize=10, color="#555", va="center") ax.set_xlim(-1, W + 1) ax.set_ylim(-9.5, D + 1) ax.set_aspect("equal") ax.axis("off") ax.add_patch(Rectangle((0, 0), W, D, fc="none", ec="#555", lw=1.6)) ax.set_title(f"{data['meta']['name']}(120m×60m)と搬送の3ルート", fontsize=15, loc="left") fig.savefig(path, bbox_inches="tight", facecolor="white") if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else ".", sys.argv[2] if len(sys.argv) > 2 else "h0")