using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;
namespace ServiceSim
{
///
/// trips.csv・conditions.json・summary.json を書く。
/// この script は実験だけを扱う。店・ロボット・人は動かさない。
/// 1往復ぶんの値は ExperimentRunner が Row に詰めて渡す。
///
public class TripLogger
{
/// trips.csv の1行
public struct Row
{
public string conditionId;
public float aisleWidthM;
public string crowd;
public int robots;
public int seed;
public int robotId;
public int tripId;
public int tableId;
public float distanceM;
public float totalS;
public float stoppedS;
public int nStops;
public float[] stopDurationsS;
public bool stuck;
public bool timeout;
public float waitAisleS;
public float maxDecelMps2;
public float minClearanceM;
public bool metRobot;
// v2:1行=ロボットの1走り
public int[] tableIds; // 回った卓
public int dishes; // 載せた料理の数
public float[] dishReadyS; // 各料理ができた時刻
public float[] dishArrivedS; // 各料理が届いた時刻(-1 は届かなかった)
public int undelivered; // 届けられず受け渡し口に戻した数
// ---- 往復ごとの測り(v5 3章・設計書2章の4つ)----
/// その往復で実際に走れた速さ(m/秒)= distance_m /(total_s − stopped_s)
public float speedMps;
/// 動けなくなってスタッフを呼んだ回数と、スタッフを待った秒数
public int stuckCount;
public float stuckWaitS;
/// その往復でサイトの道順が 0.8 m の道を使ったか(routes の use が narrow)
public bool viaNarrow;
/// 止まりの回数と秒数を、狭い通路の中と外に分けたもの(⑦の読み分け用)
public int stopsInNarrow, stopsOutNarrow;
public float stoppedInNarrowS, stoppedOutNarrowS;
}
/// groups.csv の1行(1組ぶん)
public struct GroupRow
{
public string conditionId;
public int seed;
public int groupId;
public int size;
public int tableId;
/// 座った卓の大きさ(2 なら2人席、4 なら4人席。0 は座れなかった)
public int tableSeats;
public float arriveS, guideStartS, seatedS, eatEndS, payS, leftS;
/// 会計を始めた時刻(食べ終わって会計待ちに入った時刻, 秒)
public float payStartS;
public float waitedMin;
public bool warmup;
public string state;
}
/// stops.csv の1行(止まり1回ぶん)
public struct StopRow
{
public string conditionId;
public int seed;
public int robotId;
public int tripId;
public float tS; // 店の中の時刻
public float x, z;
public string aisle; // 通路の名前
///
/// サイトの決まりで「狭い通路」(幅が meta.settings.normal_width 未満)の中で
/// 止まったか(v5 3章。⑦の読み分け。③は 1.2 m なのでいつも 0)
///
public bool inNarrow;
public string cause;
public string causeDetail;
public bool pathBlocked; // 行き先への道が引けていなかったか
public float durationS;
public string resolved;
public float clearanceLeftCm;
public float clearanceRightCm;
public float minGapCm;
// 止まっている間、何で動けなかったかの内訳(秒)。B7 を調べるため
public float blockedS;
public float noPathS;
public float yieldS;
public float clearWaitS;
public int replans;
// 道を引いたときの中身(調べるためだけ。動きは変えない)
/// 寄せたあとの出発点・行き先("x;z")
public string solveFrom, solveTo;
/// CalculatePath の結果と、引けた角の数
public string solveStatus;
public int solveCorners;
/// ロボットの位置から、いちばん近い焼いた面までの距離(m)。-1 は見つからない
public float offMeshM;
}
///
/// 止まった原因の名前。観察シート(serving-robot-observation.html)の
/// 「止まった原因」の選択肢にそろえる。Unity にしかない原因は足した。
///
public static string CauseLabel(ServingRobotController.StopCause cause)
{
switch (cause)
{
case ServingRobotController.StopCause.Guest: return "お客さんとすれ違った";
case ServingRobotController.StopCause.Staff: return "スタッフとすれ違った";
case ServingRobotController.StopCause.PersonStanding: return "人が立ち止まっていた";
case ServingRobotController.StopCause.Chair: return "椅子・荷物・ベビーカー";
case ServingRobotController.StopCause.NoPath: return "通路の幅が足りない";
case ServingRobotController.StopCause.OtherRobot: return "もう1台のロボット";
case ServingRobotController.StopCause.AisleQueue: return "通路の順番待ち";
default: return "分からない";
}
}
/// どう動き出したかの名前
public static string ResolveLabel(ServingRobotController.StopResolve r)
{
switch (r)
{
case ServingRobotController.StopResolve.SelfResume: return "自分で動き出した";
case ServingRobotController.StopResolve.Replanned: return "経路を引き直した";
case ServingRobotController.StopResolve.Stuck: return "動けなくなった";
default: return "入口で待った";
}
}
readonly List _rows = new List();
readonly List _stops = new List();
public int StopCount { get { return _stops.Count; } }
///
/// 一時停止の回数の合計(pauses)。
/// 往復ごとの n_stops(自分で動き出した止まり)を足す。
/// 通路の入口で待った分と、動けなくなった分は入らない
///
public int TotalStopCount()
{
int n = 0;
for (int i = 0; i < _rows.Count; i++) n += _rows[i].nStops;
return n;
}
public IReadOnlyList Stops { get { return _stops; } }
public void AddStop(StopRow row) { _stops.Add(row); }
public int RowCount { get { return _rows.Count; } }
public IReadOnlyList Rows { get { return _rows; } }
public void Add(Row row)
{
_rows.Add(row);
}
public void Clear()
{
_rows.Clear();
_stops.Clear();
_groups.Clear();
}
// ---------------- trips.csv ----------------
public const string Header =
"condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id," +
"distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s," +
"max_decel_mps2,min_clearance_m,met_robot," +
"table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered," +
// v5 3章:往復ごとの測り(設計書2章の4つ)。⑦は狭い通路の中と外を分ける
"speed_mps,stuck_count,stuck_wait_s,via_narrow," +
"stops_in_narrow,stops_out_narrow,stopped_in_narrow_s,stopped_out_narrow_s";
public void WriteTripsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(Header).Append('\n');
for (int i = 0; i < _rows.Count; i++) AppendRow(sb, _rows[i]);
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static void AppendRow(StringBuilder sb, Row r)
{
sb.Append(r.conditionId).Append(',')
.Append(F(r.aisleWidthM)).Append(',')
.Append(r.crowd).Append(',')
.Append(r.robots).Append(',')
.Append(r.seed).Append(',')
.Append(r.robotId).Append(',')
.Append(r.tripId).Append(',')
.Append(r.tableId).Append(',')
.Append(F(r.distanceM)).Append(',')
.Append(F(r.totalS)).Append(',')
.Append(F(r.stoppedS)).Append(',')
.Append(r.nStops).Append(',')
.Append(Join(r.stopDurationsS)).Append(',')
.Append(r.stuck ? 1 : 0).Append(',')
.Append(r.timeout ? 1 : 0).Append(',')
.Append(F(r.waitAisleS)).Append(',')
.Append(F(r.maxDecelMps2)).Append(',')
.Append(F(r.minClearanceM)).Append(',')
.Append(r.metRobot ? 1 : 0).Append(',')
.Append(JoinInt(r.tableIds)).Append(',')
.Append(r.dishes).Append(',')
.Append(Join(r.dishReadyS)).Append(',')
.Append(Join(r.dishArrivedS)).Append(',')
.Append(r.undelivered).Append(',')
.Append(F(r.speedMps)).Append(',')
.Append(r.stuckCount).Append(',')
.Append(F(r.stuckWaitS)).Append(',')
.Append(r.viaNarrow ? 1 : 0).Append(',')
.Append(r.stopsInNarrow).Append(',')
.Append(r.stopsOutNarrow).Append(',')
.Append(F(r.stoppedInNarrowS)).Append(',')
.Append(F(r.stoppedOutNarrowS))
.Append('\n');
}
static string Join(float[] values)
{
if (values == null || values.Length == 0) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(';');
sb.Append(F(values[i]));
}
return sb.ToString();
}
static string JoinInt(int[] values)
{
if (values == null || values.Length == 0) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(';');
sb.Append(values[i]);
}
return sb.ToString();
}
static string F(float v)
{
return v.ToString("0.####", CultureInfo.InvariantCulture);
}
// ---------------- groups.csv ----------------
readonly List _groups = new List();
public int GroupRowCount { get { return _groups.Count; } }
public void AddGroup(GroupRow r) { _groups.Add(r); }
/// table_seats は座った卓の大きさ(2=2人席・4=4人席・0=座れなかった)
public const string GroupHeader =
"condition_id,seed,group_id,size,table_id,table_seats,arrive_s,guide_start_s,seated_s," +
"eat_end_s,pay_start_s,pay_s,left_s,waited_min,warmup,state";
public void WriteGroupsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(GroupHeader).Append('\n');
for (int i = 0; i < _groups.Count; i++)
{
GroupRow r = _groups[i];
sb.Append(r.conditionId).Append(',').Append(r.seed).Append(',')
.Append(r.groupId).Append(',').Append(r.size).Append(',').Append(r.tableId).Append(',')
.Append(r.tableSeats).Append(',')
.Append(F(r.arriveS)).Append(',').Append(F(r.guideStartS)).Append(',')
.Append(F(r.seatedS)).Append(',').Append(F(r.eatEndS)).Append(',')
.Append(F(r.payStartS)).Append(',')
.Append(F(r.payS)).Append(',').Append(F(r.leftS)).Append(',')
.Append(F(r.waitedMin)).Append(',').Append(r.warmup ? 1 : 0).Append(',')
.Append(r.state).Append('\n');
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- people_jams.csv ----------------
public const string JamHeader =
"condition_id,seed,t_s,x,z,aisle,people,duration_s,resolved,robot_blocked,robot_id";
public static void WriteJamsCsv(string path, string conditionId, int seed,
IReadOnlyList rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(JamHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
JamWatcher.JamRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(r.people).Append(',')
.Append(F(r.durationS)).Append(',').Append(r.resolved).Append(',')
.Append(r.robotBlocked ? 1 : 0).Append(',').Append(r.robotId).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- give_ups.csv(B1)----------------
public const string GiveUpHeader =
"condition_id,seed,t_s,who,job,table_id,to_x,to_z,x,z,aisle,waited_s,action";
/// あきらめた・移した・卓を空けたことを1回1行で書く
public static void WriteGiveUpsCsv(string path, string conditionId, int seed,
IReadOnlyList rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(GiveUpHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
GiveUpRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(r.who).Append(',').Append(r.job).Append(',')
.Append(r.tableId).Append(',')
.Append(F(r.toX)).Append(',').Append(F(r.toZ)).Append(',')
.Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(F(r.waitedS)).Append(',')
.Append(r.action).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- contacts.csv(人がロボットに近づきすぎた)----------------
public const string ContactHeader =
"condition_id,seed,t_s,x,z,aisle,place,who,careless,robot_id,gap_cm";
/// 人がロボットの外周の近くまで来た1回を1行で書く
public static void WriteContactsCsv(string path, string conditionId, int seed,
IReadOnlyList rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(ContactHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
ContactRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(r.place).Append(',')
.Append(r.who).Append(',')
.Append(r.careless ? 1 : 0).Append(',').Append(r.robotId).Append(',')
.Append(F(r.gapM * 100f)).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- dishes.csv(B8)----------------
public const string RequeueHeader =
"condition_id,seed,t_s,dish_id,table_id,robot_id,fragile,ready_s,waited_s,requeued_so_far,why";
/// 届けられずに受け渡し口へ戻した1回を1行で書く
public static void WriteRequeuesCsv(string path, string conditionId, int seed,
IReadOnlyList rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(RequeueHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
RequeueRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(r.dishId).Append(',').Append(r.tableId).Append(',')
.Append(r.robotId).Append(',').Append(r.fragile ? 1 : 0).Append(',')
.Append(F(r.readyS)).Append(',').Append(F(r.tS - r.readyS)).Append(',')
.Append(r.requeuedSoFar).Append(',').Append(r.why).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- stops.csv ----------------
public const string StopHeader =
"condition_id,seed,robot_id,trip_id,t_s,x,z,aisle,in_narrow,cause,cause_detail,path_blocked," +
"duration_s,resolved,clearance_left_cm,clearance_right_cm,min_gap_cm," +
"blocked_s,no_path_s,yield_s,clear_wait_s,replans," +
// 道が引けない原因を追うための列(記録だけ。動きは変えない)
"solve_from,solve_to,solve_status,solve_corners,off_mesh_m";
public void WriteStopsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(StopHeader).Append('\n');
for (int i = 0; i < _stops.Count; i++)
{
StopRow r = _stops[i];
sb.Append(r.conditionId).Append(',')
.Append(r.seed).Append(',')
.Append(r.robotId).Append(',')
.Append(r.tripId).Append(',')
.Append(F(r.tS)).Append(',')
.Append(F(r.x)).Append(',')
.Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',')
.Append(r.inNarrow ? 1 : 0).Append(',')
.Append(r.cause).Append(',')
.Append(r.causeDetail).Append(',')
.Append(r.pathBlocked ? 1 : 0).Append(',')
.Append(F(r.durationS)).Append(',')
.Append(r.resolved).Append(',')
.Append(F(r.clearanceLeftCm)).Append(',')
.Append(F(r.clearanceRightCm)).Append(',')
.Append(F(r.minGapCm)).Append(',')
.Append(F(r.blockedS)).Append(',')
.Append(F(r.noPathS)).Append(',')
.Append(F(r.yieldS)).Append(',')
.Append(F(r.clearWaitS)).Append(',')
.Append(r.replans).Append(',')
.Append(r.solveFrom).Append(',')
.Append(r.solveTo).Append(',')
.Append(r.solveStatus).Append(',')
.Append(r.solveCorners).Append(',')
.Append(F(r.offMeshM))
.Append('\n');
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- run_info.json(1回ぶんの回し方とコマの粗さ)----------------
/// 1回ぶんの回し方と、コマの粗さの記録
public class RunInfo
{
public string runLabel = "";
public string conditionId = "";
public string timeMode = "";
public string storeKind = "";
public string crowd = "";
public int tripsRequested;
public int tripsDone;
public int robots;
public int seed;
public float aisleWidthM;
/// 卓の停止点を通路の中心からずらす距離(m)。.asset に入れた値(C1)
public float deliveryOffsetM;
/// そのうち、実際に使った分(通路が狭いときは NavMesh に乗る範囲に抑える)
public float deliveryOffsetUsedM;
public float simSeconds;
public float wallSeconds;
public float timeScale;
public float captureDeltaTime;
public bool stoppedEarly;
/// 1コマの時間(秒)。Time.deltaTime をそのまま
public readonly List frameDeltas = new List();
/// 1コマでロボットが進んだ距離(m)。台のうちいちばん大きいもの
public readonly List robotSteps = new List();
///
/// 1コマで人が進んだ距離(m)。通路を歩く人のうちいちばん大きいもの(B10)。
/// スタッフは 1.0 m/秒 なので、1コマ 0.05 秒だと 0.05 m でちょうど境目になる。
///
public readonly List peopleSteps = new List();
/// 椅子を引いた回数(B6)
public int chairPulls;
/// 椅子を引いたきっかけごとの回数(B6)
public readonly List> chairPullsByReason =
new List>();
}
/// コマの粗さを1行にまとめる(Console 用)
public static string FrameStatsText(RunInfo info)
{
if (info == null || info.frameDeltas.Count == 0) return "コマの記録なし";
int over = 0;
for (int i = 0; i < info.robotSteps.Count; i++) if (info.robotSteps[i] > 0.05f) over++;
return "コマ " + info.frameDeltas.Count +
" / 1コマの時間 中央 " + F(Percentile(info.frameDeltas, 0.5f)) +
"s・90% " + F(Percentile(info.frameDeltas, 0.9f)) +
"s・最大 " + F(Percentile(info.frameDeltas, 1f)) + "s" +
" / 1コマで進む距離 最大 " + F(Percentile(info.robotSteps, 1f)) +
"m・90% " + F(Percentile(info.robotSteps, 0.9f)) + "m" +
" / 0.05 m を超えたコマ ロボット " + over + "(" +
(info.robotSteps.Count > 0 ? (100f * over / info.robotSteps.Count).ToString("0.0") : "0") +
"%)・人 " + CountOver(info.peopleSteps, 0.05f) + "(" +
(info.peopleSteps.Count > 0
? (100f * CountOver(info.peopleSteps, 0.05f) / info.peopleSteps.Count).ToString("0.0")
: "0") + "%)";
}
public static void WriteRunInfoJson(string path, RunInfo info)
{
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_label\": ").Append(Str(info.runLabel)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"condition_id\": ").Append(Str(info.conditionId)).Append(",\n");
sb.Append(" \"store_kind\": ").Append(Str(info.storeKind)).Append(",\n");
sb.Append(" \"time_mode\": ").Append(Str(info.timeMode)).Append(",\n");
sb.Append(" \"time_scale\": ").Append(F(info.timeScale)).Append(",\n");
sb.Append(" \"capture_delta_time\": ").Append(F(info.captureDeltaTime)).Append(",\n");
sb.Append(" \"aisle_width_m\": ").Append(F(info.aisleWidthM)).Append(",\n");
sb.Append(" \"delivery_offset_m\": ").Append(F(info.deliveryOffsetM)).Append(",\n");
sb.Append(" \"delivery_offset_used_m\": ").Append(F(info.deliveryOffsetUsedM)).Append(",\n");
sb.Append(" \"crowd\": ").Append(Str(info.crowd)).Append(",\n");
sb.Append(" \"robots\": ").Append(info.robots).Append(",\n");
sb.Append(" \"seed\": ").Append(info.seed).Append(",\n");
sb.Append(" \"trips_requested\": ").Append(info.tripsRequested).Append(",\n");
sb.Append(" \"trips_done\": ").Append(info.tripsDone).Append(",\n");
sb.Append(" \"stopped_early\": ").Append(info.stoppedEarly ? "true" : "false").Append(",\n");
sb.Append(" \"sim_seconds\": ").Append(F(info.simSeconds)).Append(",\n");
sb.Append(" \"wall_seconds\": ").Append(F(info.wallSeconds)).Append(",\n");
sb.Append(" \"speedup\": ").Append(F(info.wallSeconds > 0.01f ? info.simSeconds / info.wallSeconds : 0f)).Append(",\n");
sb.Append(" \"frame_delta_s\": ");
AppendStats(sb, info.frameDeltas);
sb.Append(",\n");
sb.Append(" \"robot_step_m\": ");
AppendStats(sb, info.robotSteps);
sb.Append(",\n");
sb.Append(" \"people_step_m\": ");
AppendStats(sb, info.peopleSteps);
sb.Append(",\n");
// すり抜けの確かめ(検査3)。ロボットと人を分けて出す
int over = CountOver(info.robotSteps, 0.05f);
int overPeople = CountOver(info.peopleSteps, 0.05f);
sb.Append(" \"frames_over_005m\": ").Append(over).Append(",\n");
sb.Append(" \"frames_over_005m_ratio\": ")
.Append(F(info.robotSteps.Count > 0 ? (float)over / info.robotSteps.Count : 0f)).Append(",\n");
sb.Append(" \"people_frames_over_005m\": ").Append(overPeople).Append(",\n");
sb.Append(" \"people_frames_over_005m_ratio\": ")
.Append(F(info.peopleSteps.Count > 0 ? (float)overPeople / info.peopleSteps.Count : 0f)).Append(",\n");
// 椅子を引いた回数(B6)
sb.Append(" \"chair_pulls\": ").Append(info.chairPulls).Append(",\n");
sb.Append(" \"chair_pulls_by_reason\": {");
for (int i = 0; i < info.chairPullsByReason.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(info.chairPullsByReason[i].Key)).Append(": ")
.Append(info.chairPullsByReason[i].Value);
}
sb.Append("}\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
/// しきい値を超えたコマの数
static int CountOver(List values, float limit)
{
if (values == null) return 0;
int n = 0;
for (int i = 0; i < values.Count; i++) if (values[i] > limit) n++;
return n;
}
static void AppendStats(StringBuilder sb, List values)
{
if (values == null || values.Count == 0) { sb.Append("null"); return; }
float sum = 0f;
for (int i = 0; i < values.Count; i++) sum += values[i];
sb.Append("{\"count\": ").Append(values.Count);
sb.Append(", \"mean\": ").Append(F(sum / values.Count));
sb.Append(", \"min\": ").Append(F(Percentile(values, 0f)));
sb.Append(", \"p50\": ").Append(F(Percentile(values, 0.5f)));
sb.Append(", \"p90\": ").Append(F(Percentile(values, 0.9f)));
sb.Append(", \"p99\": ").Append(F(Percentile(values, 0.99f)));
sb.Append(", \"max\": ").Append(F(Percentile(values, 1f)));
sb.Append("}");
}
// ---------------- conditions.json ----------------
///
/// 18条件の中身と、設定ファイル(.asset)の値すべてを書く。
/// 値は実行したときのものを JsonUtility でそのまま出す。
///
public static void WriteConditionsJson(string path, string runName,
StoreLayout layout, RobotSpec robotSpec,
PeopleSpec peopleSpec, ExperimentPlan plan,
List conditions)
{
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_name\": ").Append(Str(runName)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"unity_version\": ").Append(Str(Application.unityVersion)).Append(",\n");
sb.Append(" \"trips_csv_columns\": ").Append(Str(Header)).Append(",\n");
sb.Append(" \"settings\": {\n");
sb.Append(" \"store_layout\": ").Append(Dump(layout)).Append(",\n");
sb.Append(" \"robot_spec\": ").Append(Dump(robotSpec)).Append(",\n");
sb.Append(" \"people_spec\": ").Append(Dump(peopleSpec)).Append(",\n");
sb.Append(" \"experiment_plan\": ").Append(Dump(plan)).Append("\n");
sb.Append(" },\n");
// 組み立てた形から出る値(conditions.json だけ見れば店の大きさが分かるように)
sb.Append(" \"derived\": {\n");
if (layout != null)
{
StorePlan sp = layout.NewPlan();
sb.Append(" \"store_kind\": ").Append(Str(layout.StoreKindName)).Append(",\n");
sb.Append(" \"aisle_width_m\": ").Append(F(layout.AisleWidth)).Append(",\n");
if (sp != null)
{
sb.Append(" \"floor_min_x\": ").Append(F(sp.floor.xMin)).Append(",\n");
sb.Append(" \"floor_max_x\": ").Append(F(sp.floor.xMax)).Append(",\n");
sb.Append(" \"floor_min_z\": ").Append(F(sp.floor.zMin)).Append(",\n");
sb.Append(" \"floor_max_z\": ").Append(F(sp.floor.zMax)).Append(",\n");
sb.Append(" \"outer_size_x_m\": ").Append(F(sp.floor.SizeX)).Append(",\n");
sb.Append(" \"outer_size_z_m\": ").Append(F(sp.floor.SizeZ)).Append(",\n");
sb.Append(" \"table_count\": ").Append(sp.seats.Count).Append(",\n");
sb.Append(" \"aisle_count\": ").Append(sp.aisles.Count).Append(",\n");
sb.Append(" \"narrow_aisle_count\": ").Append(sp.NarrowAisleCount).Append("\n");
}
else
{
sb.Append(" \"plan\": null\n");
}
}
sb.Append(" },\n");
sb.Append(" \"conditions\": [\n");
if (conditions != null)
{
for (int i = 0; i < conditions.Count; i++)
{
ExperimentPlan.Condition c = conditions[i];
sb.Append(" {");
sb.Append("\"condition_id\": ").Append(Str(c.id)).Append(", ");
sb.Append("\"aisle_width_m\": ").Append(F(c.aisleWidthM)).Append(", ");
sb.Append("\"aisle_width_index\": ").Append(c.aisleWidthIndex).Append(", ");
sb.Append("\"crowd\": ").Append(Str(ExperimentPlan.CrowdName(c.crowd))).Append(", ");
sb.Append("\"walkers\": ").Append(peopleSpec != null ? peopleSpec.WalkerCount(c.crowd) : 0).Append(", ");
sb.Append("\"robots\": ").Append(c.robots).Append(", ");
sb.Append("\"trips\": ").Append(plan != null ? plan.tripsPerCondition : 0).Append(", ");
sb.Append("\"seeds\": ").Append(IntArray(plan != null ? plan.seeds : null)).Append(", ");
sb.Append("\"aisle_yielding\": ").Append(robotSpec != null && robotSpec.aisleYielding ? "true" : "false");
sb.Append('}');
if (i < conditions.Count - 1) sb.Append(',');
sb.Append('\n');
}
}
sb.Append(" ]\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static string Dump(ScriptableObject asset)
{
if (asset == null) return "null";
return JsonUtility.ToJson(asset, false);
}
static string IntArray(int[] values)
{
if (values == null || values.Length == 0) return "[]";
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(values[i]);
}
return sb.Append(']').ToString();
}
static string Str(string s)
{
if (s == null) return "null";
StringBuilder sb = new StringBuilder("\"");
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (c == '"' || c == '\\') sb.Append('\\').Append(c);
else if (c == '\n') sb.Append("\\n");
else if (c == '\r') sb.Append("\\r");
else if (c == '\t') sb.Append("\\t");
else sb.Append(c);
}
return sb.Append('"').ToString();
}
// ---------------- summary.json ----------------
/// 条件ごとにまとめる
/// 試算(haizen_sim.py)と同じ意味の数字。名前も同じにする
public class SimLike
{
public List dishWaitMin = new List();
public List seatWaitMin = new List();
public List turnMin = new List();
public float coldMin = 3f;
public int robotDishes, staffDishes, requeuedDishes, groups;
/// あきらめた・移した・卓を空けた回数(B1)
public int gaveUp, warped, tableFreed;
/// 人どうしの詰まりのうち、近くでロボットが止まっていたもの(B9)
public int robotBlockedJams;
/// 人がロボットに近づきすぎた回数・待った回数と秒数・回り道した回数
public int robotContacts, robotYieldWaits, robotYieldDetours;
public float robotYieldWaitSeconds;
/// レジの前にいた人数(いちばん多いとき・平均)
public int registerCrowdMax;
public float registerCrowdMean;
/// 会計の回数と、そのうちスタッフが店員側にいなかった回数(0 になるはず)
public int registerPayCount, registerPayNotAtBackCount;
/// 立つ点から離れて会計した回数(0 になるはず)と、その距離
public int registerPayOffSpotCount;
public float registerPayMeanOffsetM, registerPayMaxOffsetM;
/// 会計でないスタッフが店員側にいた回数と秒数(どちらも 0 になるはず)
public int registerBackIntruderCount;
public float registerBackIntruderSeconds;
/// スタッフの時間の使い方(仕事の名前 → 秒)
public readonly List> staffTime =
new List>();
/// 仕事が板に出てから取られるまでの秒数の平均(仕事の名前 → 秒)
public readonly List> jobWaitMean =
new List>();
/// 届いた料理の、届くまでの内訳(ロボット・スタッフ別に分ける)
public readonly List dishParts = new List();
/// 椅子を引いた回数(B6)
public int chairPulls;
/// ドリンクバー:並んだ回数・あきらめた回数・並んで待った分(B4)
public int drinkBarQueued, drinkBarGaveUp;
public readonly List drinkBarWaitMin = new List();
public int jamCount; public float jamTotalS;
public float walkersMean;
public float measuredMinutes;
// ---- サイトと同じ名前で出す値(v5 3章・meta.metrics)----
/// 入店待ちの内訳のもと(分)。分け方はサイトの試算と同じ
public readonly List entryWaitMin = new List();
public readonly List fullWaitMin = new List();
public readonly List blockedWaitMin = new List();
public readonly List guideWaitMin = new List();
public readonly List staffWaitMin = new List();
/// walk_km_per_staff・staff_util・robot_util
public float walkKmPerStaff, staffUtil, robotUtil;
/// rescues:立ち往生でスタッフを呼んだ回数(着いて動かした回数も残す)
public int rescues, rescuesDone;
public float rescueWaitS;
/// pauses:一時停止の回数(自分で動き出した止まりの合計)
public int pauses;
/// サイトの条件で回したか(v5 3章)と、その条件の名前
public bool siteRun;
public string siteConditionId = "";
///
/// サイトの試算の答え(unity_conditions の site_result)。
/// 同じ summary.json に入れておけば、並べた表をあとから手で写さずに作れる
///
public readonly List> siteResult =
new List>();
/// 来客を受け付けた分と、全員が帰るまでの分(打ち切りなら everyoneLeft が false)
public float arrivalsMinutes, totalMinutes;
public bool everyoneLeft;
}
SimLike _sim;
public void SetSimLike(SimLike s) { _sim = s; }
public void WriteSummaryJson(string path, string runName)
{
// 条件ごとに行を分ける(出てきた順を保つ)
List order = new List();
Dictionary> byCondition = new Dictionary>();
for (int i = 0; i < _rows.Count; i++)
{
string id = _rows[i].conditionId;
if (!byCondition.ContainsKey(id)) { byCondition[id] = new List(); order.Add(id); }
byCondition[id].Add(_rows[i]);
}
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_name\": ").Append(Str(runName)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"note\": ").Append(Str(
"speed_mps = distance_m / (total_s - stopped_s)。" +
"stopped_s には、動けなくなった60秒と、スタッフを待った秒数(stuck_wait_s)、" +
"通路の入口で待った秒数も入る。" +
"pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。" +
"site_metrics はサイトの meta.metrics と同じ名前・同じ定義。" +
"入店待ちの分け方もサイトの試算と同じで、" +
"blocked_wait は片付け待ちの卓の数だけ待っている列の前から順に足した分、" +
"full_wait は seat_wait − blocked_wait、" +
"staff_wait は blocked_wait + guide_wait。" +
"pauses と rescues は、サイト側が unity_measures の仮の値から出した数なので出どころが違う。"))
.Append(",\n");
if (_sim != null)
{
sb.Append(" \"sim_like\": {");
sb.Append("\"dish_wait_mean\": ").Append(F(Mean(_sim.dishWaitMin))).Append(", ");
sb.Append("\"dish_wait_p90\": ").Append(F(Percentile(_sim.dishWaitMin, 0.9f))).Append(", ");
sb.Append("\"cold_share\": ").Append(F(Share(_sim.dishWaitMin, _sim.coldMin))).Append(", ");
sb.Append("\"seat_wait_mean\": ").Append(F(Mean(_sim.seatWaitMin))).Append(", ");
sb.Append("\"turn_mean\": ").Append(F(Mean(_sim.turnMin))).Append(", ");
sb.Append("\"robot_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
? (float)_sim.robotDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
sb.Append("\"robot_dishes\": ").Append(_sim.robotDishes).Append(", ");
sb.Append("\"staff_dishes\": ").Append(_sim.staffDishes).Append(", ");
sb.Append("\"requeued_dishes\": ").Append(_sim.requeuedDishes).Append(", ");
sb.Append("\"groups\": ").Append(_sim.groups).Append(", ");
sb.Append("\"people_jams\": ").Append(_sim.jamCount).Append(", ");
sb.Append("\"robot_blocked_jams\": ").Append(_sim.robotBlockedJams).Append(", ");
sb.Append("\"robot_blocked_jam_share\": ").Append(F(_sim.jamCount > 0
? (float)_sim.robotBlockedJams / _sim.jamCount : 0f)).Append(", ");
sb.Append("\"chair_pulls\": ").Append(_sim.chairPulls).Append(", ");
sb.Append("\"robot_contacts\": ").Append(_sim.robotContacts).Append(", ");
sb.Append("\"robot_yield_waits\": ").Append(_sim.robotYieldWaits).Append(", ");
sb.Append("\"robot_yield_wait_s\": ").Append(F(_sim.robotYieldWaitSeconds)).Append(", ");
sb.Append("\"robot_yield_detours\": ").Append(_sim.robotYieldDetours).Append(", ");
sb.Append("\"register_crowd_max\": ").Append(_sim.registerCrowdMax).Append(", ");
sb.Append("\"register_crowd_mean\": ").Append(F(_sim.registerCrowdMean)).Append(", ");
sb.Append("\"register_pay_count\": ").Append(_sim.registerPayCount).Append(", ");
sb.Append("\"register_pay_not_at_back\": ").Append(_sim.registerPayNotAtBackCount).Append(", ");
sb.Append("\"register_pay_off_spot\": ").Append(_sim.registerPayOffSpotCount).Append(", ");
sb.Append("\"register_pay_offset_mean_m\": ").Append(F(_sim.registerPayMeanOffsetM)).Append(", ");
sb.Append("\"register_pay_offset_max_m\": ").Append(F(_sim.registerPayMaxOffsetM)).Append(", ");
sb.Append("\"register_back_intruders\": ").Append(_sim.registerBackIntruderCount).Append(", ");
sb.Append("\"register_back_intruder_s\": ").Append(F(_sim.registerBackIntruderSeconds)).Append(", ");
sb.Append("\"drink_bar_queued\": ").Append(_sim.drinkBarQueued).Append(", ");
sb.Append("\"drink_bar_gave_up\": ").Append(_sim.drinkBarGaveUp).Append(", ");
sb.Append("\"drink_bar_wait_mean\": ").Append(F(Mean(_sim.drinkBarWaitMin))).Append(", ");
sb.Append("\"drink_bar_wait_p90\": ").Append(F(Percentile(_sim.drinkBarWaitMin, 0.9f))).Append(", ");
sb.Append("\"gave_up\": ").Append(_sim.gaveUp).Append(", ");
sb.Append("\"warped\": ").Append(_sim.warped).Append(", ");
sb.Append("\"table_freed\": ").Append(_sim.tableFreed).Append(", ");
sb.Append("\"people_jam_total_s\": ").Append(F(_sim.jamTotalS)).Append(", ");
sb.Append("\"walkers_mean\": ").Append(F(_sim.walkersMean)).Append(", ");
sb.Append("\"measured_minutes\": ").Append(F(_sim.measuredMinutes));
sb.Append("},\n");
AppendSiteMetrics(sb);
// スタッフの時間の使い方(割合も出す)
float staffTotal = 0f;
for (int i = 0; i < _sim.staffTime.Count; i++) staffTotal += _sim.staffTime[i].Value;
sb.Append(" \"staff_time_s\": {");
for (int i = 0; i < _sim.staffTime.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.staffTime[i].Key)).Append(": ").Append(F(_sim.staffTime[i].Value));
}
sb.Append("},\n");
// 料理ができてから届くまでの内訳(受け渡し口で待った/運んでいた/戻されて失った)
sb.Append(" \"dish_wait_parts\": {");
AppendDishParts(sb, "robot", true);
sb.Append(", ");
AppendDishParts(sb, "staff", false);
sb.Append(", ");
AppendDishParts(sb, "all", null);
sb.Append("},\n");
sb.Append(" \"job_wait_mean_s\": {");
for (int i = 0; i < _sim.jobWaitMean.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.jobWaitMean[i].Key)).Append(": ").Append(F(_sim.jobWaitMean[i].Value));
}
sb.Append("},\n");
sb.Append(" \"staff_time_share\": {");
for (int i = 0; i < _sim.staffTime.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.staffTime[i].Key)).Append(": ")
.Append(F(staffTotal > 0.01f ? _sim.staffTime[i].Value / staffTotal : 0f));
}
sb.Append("},\n");
}
// 止まり全体のまとめ(B7 の前後を並べるため)
sb.Append(" \"stops\": {");
sb.Append("\"count\": ").Append(_stops.Count).Append(", ");
sb.Append("\"stuck_count\": ").Append(StuckStopCount()).Append(", ");
sb.Append("\"partial_path_count\": ").Append(PartialPathStopCount());
sb.Append("},\n");
sb.Append(" \"stop_causes\": [\n");
AppendCauseSummary(sb);
sb.Append(" ],\n");
sb.Append(" \"conditions\": [\n");
for (int i = 0; i < order.Count; i++)
{
List rows = byCondition[order[i]];
AppendConditionSummary(sb, order[i], rows);
if (i < order.Count - 1) sb.Append(',');
sb.Append('\n');
}
sb.Append(" ]\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
///
/// サイトと同じ名前・同じ定義で出す欄(v5 3章・meta.metrics)。
/// site_result と並べるときは、この欄だけを見れば足りるようにする。
///
/// 入店待ちの分け方は、サイトの試算(haizen_sim.py)と同じ:
/// seat_wait 店に来てから案内が始まるまで
/// blocked_wait そのうち、卓は空いたのに片付けが終わらず待った分
/// (片付け待ちの卓の数だけ、待っている列の前から順に足す。サイトの _acc と同じ)
/// full_wait seat_wait − blocked_wait
/// guide_wait 案内の仕事が板に出てから、スタッフが取るまで
/// staff_wait blocked_wait + guide_wait
/// entry_wait 店に来てから卓に着くまで(案内の歩きと guide_s を含む)
///
void AppendSiteMetrics(StringBuilder sb)
{
sb.Append(" \"site_metrics\": {");
sb.Append("\"entry_wait_mean\": ").Append(F(Mean(_sim.entryWaitMin))).Append(", ");
sb.Append("\"full_wait_mean\": ").Append(F(Mean(_sim.fullWaitMin))).Append(", ");
sb.Append("\"staff_wait_mean\": ").Append(F(Mean(_sim.staffWaitMin))).Append(", ");
sb.Append("\"blocked_wait_mean\": ").Append(F(Mean(_sim.blockedWaitMin))).Append(", ");
sb.Append("\"guide_wait_mean\": ").Append(F(Mean(_sim.guideWaitMin))).Append(", ");
sb.Append("\"seat_wait_mean\": ").Append(F(Mean(_sim.seatWaitMin))).Append(", ");
sb.Append("\"dish_wait_mean\": ").Append(F(Mean(_sim.dishWaitMin))).Append(", ");
sb.Append("\"dish_wait_p90\": ").Append(F(Percentile(_sim.dishWaitMin, 0.9f))).Append(", ");
sb.Append("\"cold_share\": ").Append(F(Share(_sim.dishWaitMin, _sim.coldMin))).Append(", ");
sb.Append("\"turn_mean\": ").Append(F(Mean(_sim.turnMin))).Append(", ");
sb.Append("\"walk_km_per_staff\": ").Append(F(_sim.walkKmPerStaff)).Append(", ");
sb.Append("\"staff_util\": ").Append(F(_sim.staffUtil)).Append(", ");
sb.Append("\"robot_util\": ").Append(F(_sim.robotUtil)).Append(", ");
sb.Append("\"robot_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
? (float)_sim.robotDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
sb.Append("\"rescues\": ").Append(_sim.rescues).Append(", ");
sb.Append("\"pauses\": ").Append(_sim.pauses).Append(", ");
sb.Append("\"groups\": ").Append(_sim.groups);
sb.Append("},\n");
sb.Append(" \"site_run\": {");
sb.Append("\"site_run\": ").Append(_sim.siteRun ? "true" : "false").Append(", ");
sb.Append("\"condition_id\": ").Append(Str(_sim.siteConditionId)).Append(", ");
sb.Append("\"arrivals_minutes\": ").Append(F(_sim.arrivalsMinutes)).Append(", ");
sb.Append("\"total_minutes\": ").Append(F(_sim.totalMinutes)).Append(", ");
sb.Append("\"everyone_left\": ").Append(_sim.everyoneLeft ? "true" : "false").Append(", ");
sb.Append("\"rescues_done\": ").Append(_sim.rescuesDone).Append(", ");
sb.Append("\"rescue_wait_s\": ").Append(F(_sim.rescueWaitS)).Append(", ");
// 逃がし弁(0 回に近いことを確かめる。仕組みは消さない)
sb.Append("\"gave_up\": ").Append(_sim.gaveUp).Append(", ");
sb.Append("\"warped\": ").Append(_sim.warped).Append(", ");
sb.Append("\"table_freed\": ").Append(_sim.tableFreed);
sb.Append("},\n");
// サイトの試算の答え(並べる表のもと)
sb.Append(" \"site_result\": {");
for (int i = 0; i < _sim.siteResult.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.siteResult[i].Key)).Append(": ").Append(F(_sim.siteResult[i].Value));
}
sb.Append("},\n");
}
///
/// 原因ごとの回数と秒数の合計。
/// stuck_count は「動けなくなった」で終わった止まり(止まりの上限に達した回数)。
/// partial_path_count は「途中までの経路」に縛られて引き直して出た止まり(B7)。
/// この2つを、停止点を変えた2回で並べれば「椅子が原因で上限に達した回数」の差が出る。
///
void AppendCauseSummary(StringBuilder sb)
{
string stuckLabel = ResolveLabel(ServingRobotController.StopResolve.Stuck);
List order = new List();
Dictionary count = new Dictionary();
Dictionary secs = new Dictionary();
Dictionary longest = new Dictionary();
Dictionary blocked = new Dictionary();
Dictionary stuck = new Dictionary();
Dictionary partial = new Dictionary();
Dictionary noPath = new Dictionary();
for (int i = 0; i < _stops.Count; i++)
{
string c = _stops[i].cause;
if (!count.ContainsKey(c))
{
order.Add(c); count[c] = 0; secs[c] = 0f; longest[c] = 0f; blocked[c] = 0;
stuck[c] = 0; partial[c] = 0; noPath[c] = 0f;
}
count[c]++;
secs[c] += _stops[i].durationS;
if (_stops[i].durationS > longest[c]) longest[c] = _stops[i].durationS;
if (_stops[i].pathBlocked) blocked[c]++;
if (_stops[i].resolved == stuckLabel) stuck[c]++;
if (HasPartialPathMark(_stops[i].causeDetail)) partial[c]++;
noPath[c] += _stops[i].noPathS;
}
for (int i = 0; i < order.Count; i++)
{
string c = order[i];
sb.Append(" {\"cause\": ").Append(Str(c))
.Append(", \"count\": ").Append(count[c])
.Append(", \"total_s\": ").Append(F(secs[c]))
.Append(", \"mean_s\": ").Append(F(count[c] > 0 ? secs[c] / count[c] : 0f))
.Append(", \"longest_s\": ").Append(F(longest[c]))
.Append(", \"path_blocked_count\": ").Append(blocked[c])
.Append(", \"stuck_count\": ").Append(stuck[c])
.Append(", \"partial_path_count\": ").Append(partial[c])
.Append(", \"no_path_total_s\": ").Append(F(noPath[c]))
.Append('}');
if (i < order.Count - 1) sb.Append(',');
sb.Append('\n');
}
}
///
/// 届くまでの内訳を1つぶん書く。byRobot が null なら全部。
/// 秒は「分」に直して出す(dish_wait_mean と同じ単位にそろえる)。
///
void AppendDishParts(StringBuilder sb, string name, bool? byRobot)
{
int n = 0, requeued = 0;
float pass = 0f, carry = 0f, requeue = 0f;
if (_sim != null)
{
for (int i = 0; i < _sim.dishParts.Count; i++)
{
ServiceOps.DishParts p = _sim.dishParts[i];
if (byRobot.HasValue && p.byRobot != byRobot.Value) continue;
n++;
pass += p.passWaitS;
carry += p.carryS;
requeue += p.requeueS;
requeued += p.requeued;
}
}
float d = n > 0 ? n : 1f;
sb.Append(Str(name)).Append(": {");
sb.Append("\"dishes\": ").Append(n).Append(", ");
sb.Append("\"pass_wait_mean_min\": ").Append(F(pass / d / 60f)).Append(", ");
sb.Append("\"carry_mean_min\": ").Append(F(carry / d / 60f)).Append(", ");
sb.Append("\"requeue_lost_mean_min\": ").Append(F(requeue / d / 60f)).Append(", ");
sb.Append("\"total_mean_min\": ").Append(F((pass + carry + requeue) / d / 60f)).Append(", ");
sb.Append("\"requeued_per_dish\": ").Append(F(requeued / d)).Append(", ");
sb.Append("\"requeued_total\": ").Append(requeued);
sb.Append("}");
}
/// この止まりが「途中までの経路」に縛られたものか(B7)
static bool HasPartialPathMark(string causeDetail)
{
return !string.IsNullOrEmpty(causeDetail) &&
causeDetail.Contains(ServingRobotController.PartialPathMark);
}
/// 「動けなくなった」で終わった止まりの数(止まりの上限に達した回数)
int StuckStopCount()
{
string label = ResolveLabel(ServingRobotController.StopResolve.Stuck);
int n = 0;
for (int i = 0; i < _stops.Count; i++) if (_stops[i].resolved == label) n++;
return n;
}
/// 「途中までの経路」から引き直して出た止まりの数(B7)
int PartialPathStopCount()
{
int n = 0;
for (int i = 0; i < _stops.Count; i++) if (HasPartialPathMark(_stops[i].causeDetail)) n++;
return n;
}
static void AppendConditionSummary(StringBuilder sb, string id, List rows)
{
List speeds = new List();
List stopped = new List();
List pauses = new List();
List waits = new List();
int stuckCount = 0, timeoutCount = 0, pausedCount = 0, metRobotCount = 0;
float aisleWidth = 0f; int robots = 0; string crowd = "";
for (int i = 0; i < rows.Count; i++)
{
Row r = rows[i];
aisleWidth = r.aisleWidthM; robots = r.robots; crowd = r.crowd;
float moving = r.totalS - r.stoppedS;
if (moving > 0.01f) speeds.Add(r.distanceM / moving);
stopped.Add(r.stoppedS);
waits.Add(r.waitAisleS);
if (r.stopDurationsS != null)
for (int k = 0; k < r.stopDurationsS.Length; k++) pauses.Add(r.stopDurationsS[k]);
if (r.stuck) stuckCount++;
if (r.timeout) timeoutCount++;
if (r.nStops >= 1) pausedCount++;
if (r.metRobot) metRobotCount++;
}
int n = rows.Count;
sb.Append(" {");
sb.Append("\"condition_id\": ").Append(Str(id)).Append(", ");
sb.Append("\"aisle_width_m\": ").Append(F(aisleWidth)).Append(", ");
sb.Append("\"crowd\": ").Append(Str(crowd)).Append(", ");
sb.Append("\"robots\": ").Append(robots).Append(", ");
sb.Append("\"trips\": ").Append(n).Append(", ");
sb.Append("\"robot_speed_mps_median\": ").Append(F(Percentile(speeds, 0.5f))).Append(", ");
sb.Append("\"stopped_s_median\": ").Append(F(Percentile(stopped, 0.5f))).Append(", ");
sb.Append("\"stopped_s_p90\": ").Append(F(Percentile(stopped, 0.9f))).Append(", ");
sb.Append("\"pause_s_median\": ").Append(F(Percentile(pauses, 0.5f))).Append(", ");
sb.Append("\"pause_s_p90\": ").Append(F(Percentile(pauses, 0.9f))).Append(", ");
sb.Append("\"pause_count\": ").Append(pauses.Count).Append(", ");
sb.Append("\"wait_aisle_s_median\": ").Append(F(Percentile(waits, 0.5f))).Append(", ");
sb.Append("\"stuck_rate\": ").Append(F(n > 0 ? stuckCount / (float)n : 0f)).Append(", ");
sb.Append("\"stuck_count\": ").Append(stuckCount).Append(", ");
sb.Append("\"timeout_rate\": ").Append(F(n > 0 ? timeoutCount / (float)n : 0f)).Append(", ");
sb.Append("\"timeout_count\": ").Append(timeoutCount).Append(", ");
sb.Append("\"pause_rate\": ").Append(F(n > 0 ? pausedCount / (float)n : 0f)).Append(", ");
sb.Append("\"met_robot_rate\": ").Append(F(n > 0 ? metRobotCount / (float)n : 0f));
sb.Append('}');
}
static float Mean(List v)
{
if (v == null || v.Count == 0) return -1f;
float s = 0f;
for (int i = 0; i < v.Count; i++) s += v[i];
return s / v.Count;
}
static float Share(List v, float over)
{
if (v == null || v.Count == 0) return -1f;
int n = 0;
for (int i = 0; i < v.Count; i++) if (v[i] > over) n++;
return (float)n / v.Count;
}
/// 小さい順に並べて p の位置の値を返す(0〜1)。空なら -1
public static float Percentile(List values, float p)
{
if (values == null || values.Count == 0) return -1f;
List v = new List(values);
v.Sort();
if (v.Count == 1) return v[0];
float pos = Mathf.Clamp01(p) * (v.Count - 1);
int lo = Mathf.FloorToInt(pos);
int hi = Mathf.Min(lo + 1, v.Count - 1);
float t = pos - lo;
return Mathf.Lerp(v[lo], v[hi], t);
}
}
}