import argparse import xml.etree.ElementTree as ET from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parent.parent DEFAULT_SPEED_CSV = PROJECT_ROOT / "speed.csv" DEFAULT_ROUTE_DIR = PROJECT_ROOT / "sumo_resource" DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "results" / "fundamental_diagram" DEFAULT_EDGE_ID = "G1523_AM7.1" PAPER_FIGSIZE = (6.6, 4.8) PAPER_DPI = 300 def parse_args(): parser = argparse.ArgumentParser( description="Plot q-k, v-k, and q-v fundamental diagrams from SUMO route files and speed.csv." ) parser.add_argument( "--speed-csv", default=str(DEFAULT_SPEED_CSV), help=f"Path to speed CSV. Default: {DEFAULT_SPEED_CSV}", ) parser.add_argument( "--route-dir", default=str(DEFAULT_ROUTE_DIR), help=f"Directory containing routes_YYYYMMDD.xml. Default: {DEFAULT_ROUTE_DIR}", ) parser.add_argument( "--edge-id", default=DEFAULT_EDGE_ID, help=f"Representative section edge id. Default: {DEFAULT_EDGE_ID}", ) parser.add_argument( "--date", default="all", help="Use one day in YYYY-MM-DD format or all overlapping full dates. Default: all", ) parser.add_argument( "--output-dir", default=str(DEFAULT_OUTPUT_DIR), help=f"Output directory. Default: {DEFAULT_OUTPUT_DIR}", ) return parser.parse_args() def load_speed_profile(speed_csv: Path) -> pd.DataFrame: df = pd.read_csv(speed_csv, encoding="utf-8-sig") time_col = df.columns[0] speed_col = df.columns[1] speed = df[[time_col, speed_col]].copy() speed.columns = ["datetime", "v_kmh"] speed["datetime"] = pd.to_datetime(speed["datetime"]) speed["v_kmh"] = pd.to_numeric(speed["v_kmh"], errors="coerce") speed = speed.dropna() speed["date"] = speed["datetime"].dt.strftime("%Y-%m-%d") speed["clock_time"] = speed["datetime"].dt.strftime("%H:%M") return speed[["datetime", "date", "clock_time", "v_kmh"]] def extract_edge_flow_from_route(route_path: Path, edge_id: str) -> pd.DataFrame: root = ET.parse(route_path).getroot() route_edges = {elem.attrib["id"]: elem.attrib.get("edges", "").split() for elem in root.findall("route")} bucket_counts: dict[int, int] = {} # Use departure interval of vehicles whose routes traverse the target edge as section equivalent flow. for vehicle in root.findall("vehicle"): route_id = vehicle.attrib.get("route") edges = route_edges.get(route_id) if not edges or edge_id not in edges: continue depart = float(vehicle.attrib["depart"]) % 86400.0 bucket = int(depart // 1800.0) if 0 <= bucket < 48: bucket_counts[bucket] = bucket_counts.get(bucket, 0) + 1 flow = pd.DataFrame({"bucket": range(48)}) flow["veh_30min"] = flow["bucket"].map(lambda bucket: bucket_counts.get(bucket, 0)) flow["q_vph"] = flow["veh_30min"] * 2.0 flow["clock_time"] = pd.to_datetime(flow["bucket"] * 1800, unit="s").dt.strftime("%H:%M") return flow[["clock_time", "veh_30min", "q_vph"]] def available_route_dates(route_dir: Path) -> set[str]: dates = set() for path in route_dir.glob("routes_*.xml"): stem = path.stem parts = stem.split("_") if len(parts) != 2 or len(parts[1]) != 8 or not parts[1].isdigit(): continue date_str = f"{parts[1][:4]}-{parts[1][4:6]}-{parts[1][6:8]}" dates.add(date_str) return dates def select_dates(speed: pd.DataFrame, route_dir: Path, date_arg: str) -> list[str]: speed_counts = speed.groupby("date").size() route_dates = available_route_dates(route_dir) if date_arg != "all": if date_arg not in route_dates: raise FileNotFoundError(f"Route file for {date_arg} not found under {route_dir}") if int(speed_counts.get(date_arg, 0)) == 0: raise ValueError(f"No speed observations found for {date_arg}") return [date_arg] dates = [date for date, count in speed_counts.items() if count >= 48 and date in route_dates] if not dates: raise ValueError("No overlapping full dates between speed.csv and route files.") return sorted(dates) def build_flow_dataset(route_dir: Path, edge_id: str, dates: list[str]) -> pd.DataFrame: frames = [] for date_str in dates: route_path = route_dir / f"routes_{date_str.replace('-', '')}.xml" flow = extract_edge_flow_from_route(route_path, edge_id) flow["date"] = date_str flow["datetime"] = pd.to_datetime(flow["date"] + " " + flow["clock_time"]) frames.append(flow[["datetime", "date", "clock_time", "veh_30min", "q_vph"]]) return pd.concat(frames, ignore_index=True).sort_values("datetime").reset_index(drop=True) def build_merged_dataset(flow: pd.DataFrame, speed: pd.DataFrame, dates: list[str]) -> pd.DataFrame: speed_use = speed[speed["date"].isin(dates)].copy() merged = flow.merge(speed_use[["datetime", "date", "clock_time", "v_kmh"]], on=["datetime", "date", "clock_time"], how="inner") if merged.empty: raise ValueError("No aligned flow-speed observations after merging route flow and speed data.") merged = merged[(merged["q_vph"] > 0) & (merged["v_kmh"] > 0)].copy() merged["k_vpkm"] = merged["q_vph"] / merged["v_kmh"] return merged[["datetime", "date", "clock_time", "veh_30min", "q_vph", "v_kmh", "k_vpkm"]] def fit_greenshields(merged: pd.DataFrame) -> dict[str, float]: coef = np.polyfit(merged["k_vpkm"], merged["v_kmh"], 1) slope, intercept = coef vf = float(intercept) kj = float(-intercept / slope) kc = kj / 2.0 capacity = vf * kj / 4.0 pred_speed = np.polyval(coef, merged["k_vpkm"]) ss_res = float(np.sum((merged["v_kmh"] - pred_speed) ** 2)) ss_tot = float(np.sum((merged["v_kmh"] - merged["v_kmh"].mean()) ** 2)) r2 = 1.0 - ss_res / ss_tot if ss_tot else float("nan") return { "slope": float(slope), "intercept": float(intercept), "vf": vf, "kj": kj, "kc": kc, "capacity": capacity, "r2": r2, "observed_max_flow_veh_h": float(merged["q_vph"].max()), "sample_count": int(len(merged)), } def configure_paper_style(): plt.rcParams.update( { "font.family": "serif", "font.serif": ["SimSun", "STSong", "Times New Roman", "DejaVu Serif"], "font.sans-serif": ["SimHei", "Microsoft YaHei", "DejaVu Sans"], "axes.unicode_minus": False, "font.size": 11, "axes.labelsize": 11, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 9, "figure.titlesize": 12, } ) def style_axis(ax): ax.grid(True, color="#d9d9d9", linewidth=0.6, linestyle="-", alpha=0.9) ax.tick_params(direction="in", length=4, width=0.8) for spine in ax.spines.values(): spine.set_linewidth(0.9) spine.set_color("black") def build_curve(fit: dict[str, float]) -> tuple[np.ndarray, np.ndarray, np.ndarray]: density_grid = np.linspace(0.0, fit["kj"], 300) speed_grid = fit["vf"] + fit["slope"] * density_grid flow_grid = density_grid * speed_grid return density_grid, speed_grid, flow_grid def save_flow_density_plot( merged: pd.DataFrame, fit: dict[str, float], edge_id: str, date_label: str, output_png: Path, ): density_grid, _, flow_grid = build_curve(fit) fig, ax = plt.subplots(figsize=PAPER_FIGSIZE) ax.scatter( merged["k_vpkm"], merged["q_vph"], s=18, facecolors="white", edgecolors="#1f4e79", linewidths=0.8, alpha=0.95, label="观测值", ) ax.plot(density_grid, flow_grid, color="#111111", linewidth=1.6, label="拟合曲线") ax.axvline(fit["kc"], color="#666666", linestyle="--", linewidth=1.0) ax.axhline(fit["capacity"], color="#666666", linestyle="--", linewidth=1.0) ax.scatter([fit["kc"]], [fit["capacity"]], color="#aa3a38", s=36, zorder=5) ax.annotate( f"$C$={fit['capacity']:.1f} veh/h\n$k_c$={fit['kc']:.1f} veh/km", xy=(fit["kc"], fit["capacity"]), xytext=(10, -18), textcoords="offset points", fontsize=8.8, bbox={"boxstyle": "square,pad=0.18", "facecolor": "white", "edgecolor": "#999999", "linewidth": 0.6}, ) ax.set_xlabel("交通密度 k(veh/km)") ax.set_ylabel("交通流率 q(veh/h)") style_axis(ax) ax.legend( loc="upper left", frameon=True, fancybox=False, edgecolor="#666666", borderpad=0.25, handlelength=1.8, handletextpad=0.5, labelspacing=0.3, ) fig.tight_layout() fig.savefig(output_png, dpi=PAPER_DPI, bbox_inches="tight") plt.close(fig) def save_speed_density_plot( merged: pd.DataFrame, fit: dict[str, float], output_png: Path, ): density_grid, speed_grid, _ = build_curve(fit) fig, ax = plt.subplots(figsize=PAPER_FIGSIZE) ax.scatter( merged["k_vpkm"], merged["v_kmh"], s=18, facecolors="white", edgecolors="#2d6a4f", linewidths=0.8, alpha=0.95, label="观测值", ) ax.plot(density_grid, speed_grid, color="#111111", linewidth=1.6, label="拟合曲线") ax.axvline(fit["kc"], color="#666666", linestyle="--", linewidth=1.0) ax.axhline(fit["vf"], color="#666666", linestyle=":", linewidth=1.0) ax.set_xlabel("交通密度 k(veh/km)") ax.set_ylabel("平均速度 v(km/h)") style_axis(ax) ax.legend( loc="upper right", frameon=True, fancybox=False, edgecolor="#666666", borderpad=0.25, handlelength=1.8, handletextpad=0.5, labelspacing=0.3, ) fig.tight_layout() fig.savefig(output_png, dpi=PAPER_DPI, bbox_inches="tight") plt.close(fig) def save_flow_speed_plot( merged: pd.DataFrame, fit: dict[str, float], output_png: Path, ): _, speed_grid, flow_grid = build_curve(fit) order = np.argsort(speed_grid) fig, ax = plt.subplots(figsize=PAPER_FIGSIZE) ax.scatter( merged["v_kmh"], merged["q_vph"], s=18, facecolors="white", edgecolors="#b26a00", linewidths=0.8, alpha=0.95, label="观测值", ) ax.plot(speed_grid[order], flow_grid[order], color="#111111", linewidth=1.6, label="拟合曲线") ax.set_xlabel("平均速度 v(km/h)") ax.set_ylabel("交通流率 q(veh/h)") style_axis(ax) ax.legend( loc="upper left", frameon=True, fancybox=False, edgecolor="#666666", borderpad=0.25, handlelength=1.8, handletextpad=0.5, labelspacing=0.3, ) fig.tight_layout() fig.savefig(output_png, dpi=PAPER_DPI, bbox_inches="tight") plt.close(fig) def build_tag(dates: list[str]) -> str: if len(dates) == 1: return dates[0] return f"{dates[0]}_to_{dates[-1]}" def main(): args = parse_args() speed_csv = Path(args.speed_csv) route_dir = Path(args.route_dir) output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) speed = load_speed_profile(speed_csv) dates = select_dates(speed, route_dir, args.date) flow = build_flow_dataset(route_dir, args.edge_id, dates) merged = build_merged_dataset(flow, speed, dates) fit = fit_greenshields(merged) configure_paper_style() tag = build_tag(dates) merged_csv = output_dir / f"fundamental_diagram_data_{args.edge_id}_{tag}.csv" metrics_txt = output_dir / f"fundamental_diagram_metrics_{args.edge_id}_{tag}.txt" qk_png = output_dir / f"fundamental_diagram_qk_{args.edge_id}_{tag}.png" vk_png = output_dir / f"fundamental_diagram_vk_{args.edge_id}_{tag}.png" qv_png = output_dir / f"fundamental_diagram_qv_{args.edge_id}_{tag}.png" merged.to_csv(merged_csv, index=False, encoding="utf-8-sig") save_flow_density_plot(merged, fit, args.edge_id, tag, qk_png) save_speed_density_plot(merged, fit, vk_png) save_flow_speed_plot(merged, fit, qv_png) metrics_lines = [ f"edge_id={args.edge_id}", f"period={tag}", f"samples={fit['sample_count']}", f"vf_kmh={fit['vf']:.6f}", f"kj_veh_per_km={fit['kj']:.6f}", f"kc_veh_per_km={fit['kc']:.6f}", f"capacity_veh_per_h={fit['capacity']:.6f}", f"observed_max_flow_veh_per_h={fit['observed_max_flow_veh_h']:.6f}", f"vk_r2={fit['r2']:.6f}", ] metrics_txt.write_text("\n".join(metrics_lines), encoding="utf-8") print(f"Using edge: {args.edge_id}") print(f"Using dates: {', '.join(dates)}") print(f"Saved merged data: {merged_csv}") print(f"Saved metrics: {metrics_txt}") print(f"Saved q-k plot: {qk_png}") print(f"Saved v-k plot: {vk_png}") print(f"Saved q-v plot: {qv_png}") if __name__ == "__main__": main()