mahbubchula commited on
Commit
eb67502
Β·
verified Β·
1 Parent(s): 4a67235

Upload 11 files

Browse files
pages/10_🧠_AI_Link_Flow_Emulator.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/10_🧠_AI_Link_Flow_Emulator.py
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import numpy as np
6
+ import os
7
+
8
+ from modules.ai_link_flow_emulator import (
9
+ train_link_flow_emulator,
10
+ predict_link_flows,
11
+ )
12
+ from modules.route_assignment import generate_synthetic_network
13
+
14
+ st.set_page_config(layout="wide")
15
+ st.title("🧠 AI Link Flow Emulator")
16
+
17
+ # -----------------------------------------------------
18
+ # CHECK REQUIRED STATE
19
+ # -----------------------------------------------------
20
+ if "mode_choice" not in st.session_state:
21
+ st.error("Run Mode Choice first (Page 4).")
22
+ st.stop()
23
+
24
+ if "city" not in st.session_state:
25
+ st.error("Generate synthetic city first (Page 1).")
26
+ st.stop()
27
+
28
+ city = st.session_state["city"]
29
+ taz = city.taz
30
+ mode_choice = st.session_state["mode_choice"]
31
+
32
+ # Use car OD as baseline demand
33
+ base_car_od = mode_choice.volumes["car"]
34
+ base_car_od_np = base_car_od.to_numpy()
35
+
36
+ # -----------------------------------------------------
37
+ # NETWORK
38
+ # -----------------------------------------------------
39
+ if "network" not in st.session_state:
40
+ network = generate_synthetic_network(taz)
41
+ st.session_state["network"] = network
42
+ else:
43
+ network = st.session_state["network"]
44
+
45
+ st.markdown("### Network Summary")
46
+ st.write(f"Number of links: **{len(network)}**")
47
+
48
+ # -----------------------------------------------------
49
+ # TRAINING SCENARIOS
50
+ # -----------------------------------------------------
51
+ n_scenarios = st.slider(
52
+ "Number of training scenarios to generate",
53
+ min_value=5, max_value=100, value=20, step=1,
54
+ help="More scenarios β†’ better emulator accuracy, slower training."
55
+ )
56
+
57
+ if st.button("Train Emulator"):
58
+ st.info("Training emulator… please wait.")
59
+
60
+ emulator, training_history = train_link_flow_emulator(
61
+ base_car_od_np,
62
+ network,
63
+ n_scenarios=n_scenarios
64
+ )
65
+
66
+ st.session_state["link_flow_emulator"] = emulator
67
+ st.session_state["emulator_training_history"] = training_history
68
+
69
+ st.success("AI Link Flow Emulator trained successfully!")
70
+
71
+ # -----------------------------------------------------
72
+ # PREDICTION MODULE
73
+ # -----------------------------------------------------
74
+ if "link_flow_emulator" in st.session_state:
75
+ emulator = st.session_state["link_flow_emulator"]
76
+
77
+ st.header("πŸ“‘ Predict New Link Flows with AI")
78
+ scale = st.slider(
79
+ "Demand scaling factor",
80
+ min_value=0.5, max_value=1.5, value=1.0, step=0.05,
81
+ help="Scale baseline OD (e.g., 1.2 = +20% demand)"
82
+ )
83
+
84
+ if st.button("Predict Link Flows"):
85
+ pred_df = predict_link_flows(emulator, scale, network)
86
+
87
+ st.subheader("AI Predicted Link Flows (sample)")
88
+ st.dataframe(pred_df.head(12))
89
+
90
+ # Save output
91
+ os.makedirs("data", exist_ok=True)
92
+ pred_df.to_csv("data/emulator_predicted_link_flows.csv", index=False)
93
+ st.success("Predicted flows saved to /data/emulator_predicted_link_flows.csv")
94
+
95
+ # Download button
96
+ csv_bytes = pred_df.to_csv(index=False).encode("utf-8")
97
+ st.download_button(
98
+ label="⬇ Download Predicted Link Flows (CSV)",
99
+ data=csv_bytes,
100
+ file_name="predicted_link_flows_ai.csv",
101
+ mime="text/csv"
102
+ )
103
+
104
+ else:
105
+ st.info("Train the emulator to enable AI predictions.")
pages/11_🎯_Scenario_Optimization.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/11_🎯_Scenario_Optimization.py
2
+
3
+ import streamlit as st
4
+ import numpy as np
5
+ import pandas as pd
6
+ import os
7
+
8
+ from typing import Dict
9
+
10
+ from modules.route_assignment import generate_synthetic_network, frank_wolfe_ue
11
+ from modules.ai_link_flow_emulator import predict_link_flows
12
+
13
+ st.set_page_config(layout="wide")
14
+ st.title("🎯 Scenario Optimization Engine")
15
+
16
+ # ------------------------------------------------------
17
+ # CHECK REQUIRED STATE
18
+ # ------------------------------------------------------
19
+ required_keys = ["city", "productions", "attractions", "od", "mode_choice"]
20
+ missing = [k for k in required_keys if k not in st.session_state]
21
+ if missing:
22
+ st.error(f"Please complete earlier steps first. Missing: {', '.join(missing)}")
23
+ st.stop()
24
+
25
+ city = st.session_state["city"]
26
+ taz = city.taz
27
+ od_base_dict: Dict[str, pd.DataFrame] = st.session_state["od"]
28
+ mode_choice_base = st.session_state["mode_choice"]
29
+ tt_car_base = city.travel_time_matrix
30
+
31
+ # ------------------------------------------------------
32
+ # NETWORK
33
+ # ------------------------------------------------------
34
+ if "network" not in st.session_state:
35
+ network = generate_synthetic_network(taz)
36
+ st.session_state["network"] = network
37
+ else:
38
+ network = st.session_state["network"]
39
+
40
+ od_total_car = mode_choice_base.volumes["car"]
41
+
42
+
43
+ # ------------------------------------------------------
44
+ # REUSE POLICY MODE CHOICE LOGIC (copied from Page 8)
45
+ # ------------------------------------------------------
46
+ def build_policy_time_cost_matrices(
47
+ tt_car_base: pd.DataFrame,
48
+ metro_time_reduction_pct: float,
49
+ metro_fare_change_pct: float,
50
+ congestion_charge: float,
51
+ cbd_zones: list
52
+ ):
53
+ tt_car = tt_car_base.copy()
54
+ tt_metro = tt_car * 0.8 * (1 - metro_time_reduction_pct / 100.0)
55
+ tt_bus = tt_car * 1.3
56
+
57
+ dist_proxy = tt_car / 60 * 30
58
+ cost_car = 2 + 0.12 * dist_proxy
59
+ cost_metro = 15 * (1 + metro_fare_change_pct / 100.0)
60
+ cost_bus = 8 + 0.03 * dist_proxy
61
+
62
+ # Vectorized congestion charge
63
+ cost_car.loc[:, cbd_zones] += congestion_charge
64
+
65
+ return (
66
+ {"car": tt_car, "metro": tt_metro, "bus": tt_bus},
67
+ {"car": cost_car, "metro": cost_metro, "bus": cost_bus},
68
+ )
69
+
70
+
71
+ def policy_mode_choice(
72
+ od_mats: dict,
73
+ taz: pd.DataFrame,
74
+ tt_car_base: pd.DataFrame,
75
+ metro_time_reduction_pct: float,
76
+ metro_fare_change_pct: float,
77
+ congestion_charge: float,
78
+ cbd_zones: list,
79
+ beta_time: float = -0.06,
80
+ beta_cost: float = -0.03,
81
+ beta_car_own: float = 0.5
82
+ ):
83
+ zones = tt_car_base.index
84
+ total_od = sum(od_mats.values()).loc[zones, zones]
85
+
86
+ time_mats, cost_mats = build_policy_time_cost_matrices(
87
+ tt_car_base,
88
+ metro_time_reduction_pct,
89
+ metro_fare_change_pct,
90
+ congestion_charge,
91
+ cbd_zones,
92
+ )
93
+
94
+ car_own = taz["car_ownership_rate"].reindex(zones).to_numpy()
95
+ car_own_mat = np.repeat(car_own[:, None], len(zones), axis=1)
96
+
97
+ modes = ["car", "metro", "bus"]
98
+ utilities = {}
99
+
100
+ for mode in modes:
101
+ tt = time_mats[mode].to_numpy()
102
+ cc = cost_mats[mode].to_numpy()
103
+
104
+ if mode == "car":
105
+ U = beta_time * tt + beta_cost * cc + beta_car_own * car_own_mat
106
+ else:
107
+ U = beta_time * tt + beta_cost * cc
108
+
109
+ utilities[mode] = U
110
+
111
+ exp_sum = sum(np.exp(U) for U in utilities.values())
112
+ probabilities = {
113
+ m: pd.DataFrame(np.exp(U) / np.maximum(exp_sum, 1e-12), index=zones, columns=zones)
114
+ for m, U in utilities.items()
115
+ }
116
+
117
+ volumes = {
118
+ m: pd.DataFrame(
119
+ total_od.to_numpy() * probabilities[m].to_numpy(),
120
+ index=zones, columns=zones
121
+ )
122
+ for m in modes
123
+ }
124
+
125
+ return probabilities, volumes, total_od
126
+
127
+
128
+ # ------------------------------------------------------
129
+ # UI OPTIONS
130
+ # ------------------------------------------------------
131
+ use_emulator = st.checkbox(
132
+ "Use AI Link Flow Emulator (if trained)",
133
+ value=False
134
+ )
135
+
136
+ st.sidebar.header("Search Space")
137
+
138
+ mt_min = st.sidebar.slider("Metro time reduction min (%)", 0, 50, 0)
139
+ mt_max = st.sidebar.slider("Metro time reduction max (%)", 0, 50, 30)
140
+ mt_step = st.sidebar.slider("Metro step (%)", 5, 20, 10)
141
+
142
+ fare_min = st.sidebar.slider("Metro fare change min (%)", -50, 50, -30)
143
+ fare_max = st.sidebar.slider("Metro fare change max (%)", -50, 50, 10)
144
+ fare_step = st.sidebar.slider("Metro fare step (%)", 10, 30, 20)
145
+
146
+ cc_min = st.sidebar.slider("Congestion charge min", 0, 100, 0)
147
+ cc_max = st.sidebar.slider("Congestion charge max", 0, 100, 50)
148
+ cc_step = st.sidebar.slider("Charge step", 10, 50, 20)
149
+
150
+ default_cbd = list(taz.index[:5])
151
+ cbd_zones = st.sidebar.multiselect(
152
+ "CBD zones",
153
+ options=list(taz.index),
154
+ default=default_cbd,
155
+ )
156
+
157
+ objective_choice = st.selectbox(
158
+ "Optimization Objective",
159
+ ["Minimize total car trips", "Minimize total car link flow"]
160
+ )
161
+
162
+
163
+ # ------------------------------------------------------
164
+ # OPTIMIZATION ENGINE
165
+ # ------------------------------------------------------
166
+ if st.button("Run Optimization Search"):
167
+ st.info("Running optimization… may take some time.")
168
+
169
+ metro_range = np.arange(mt_min, mt_max + 1e-6, mt_step)
170
+ fare_range = np.arange(fare_min, fare_max + 1e-6, fare_step)
171
+ cc_range = np.arange(cc_min, cc_max + 1e-6, cc_step)
172
+
173
+ emulator = st.session_state.get("link_flow_emulator", None)
174
+ results = []
175
+
176
+ for mt_red in metro_range:
177
+ for fare_ch in fare_range:
178
+ for cc in cc_range:
179
+
180
+ probs, vols, total_od = policy_mode_choice(
181
+ od_base_dict, taz, tt_car_base,
182
+ mt_red, fare_ch, cc, cbd_zones
183
+ )
184
+
185
+ car_od = vols["car"]
186
+ total_car_trips = car_od.values.sum()
187
+
188
+ if use_emulator and emulator is not None:
189
+ base = od_total_car.values.sum()
190
+ demand_scale = float(total_car_trips / max(base, 1e-9))
191
+ df_flows = predict_link_flows(emulator, demand_scale, network)
192
+
193
+ col = "flow_vehph_emulated" if "flow_vehph_emulated" in df_flows.columns else df_flows.columns[-1]
194
+ total_car_flow = df_flows[col].sum()
195
+ else:
196
+ df_flows = frank_wolfe_ue(car_od, network, max_iter=30)
197
+ col = "flow_vehph" if "flow_vehph" in df_flows.columns else df_flows.columns[-1]
198
+ total_car_flow = df_flows[col].sum()
199
+
200
+ objective_value = total_car_trips if objective_choice.startswith("Minimize total car trips") else total_car_flow
201
+
202
+ results.append({
203
+ "metro_time_reduction_pct": mt_red,
204
+ "metro_fare_change_pct": fare_ch,
205
+ "congestion_charge": cc,
206
+ "total_car_trips": total_car_trips,
207
+ "total_car_flow": total_car_flow,
208
+ "objective": objective_value
209
+ })
210
+
211
+ res_df = pd.DataFrame(results)
212
+ res_sorted = res_df.sort_values("objective", ascending=True).reset_index(drop=True)
213
+
214
+ st.subheader("Top 10 Best Scenarios")
215
+ st.dataframe(res_sorted.head(10))
216
+
217
+ # Save results
218
+ os.makedirs("data", exist_ok=True)
219
+ res_sorted.to_csv("data/optimization_results.csv", index=False)
220
+
221
+ st.session_state["opt_results"] = res_sorted
222
+
223
+ best = res_sorted.iloc[0]
224
+ st.success(
225
+ f"Best scenario: Metro time ↓{best['metro_time_reduction_pct']}%, "
226
+ f"Metro fare {best['metro_fare_change_pct']}%, "
227
+ f"Charge={best['congestion_charge']} β†’ Objective={best['objective']:.2f}"
228
+ )
pages/1_πŸ“Š_Generate_Synthetic_City.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/synthetic_city.py
2
+ # TripAI – Synthetic City Generator (20 TAZ by default)
3
+
4
+ from __future__ import annotations
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ # Global defaults
13
+ RANDOM_SEED = 42
14
+ NUM_ZONES_DEFAULT = 20
15
+
16
+
17
+ @dataclass
18
+ class SyntheticCity:
19
+ """
20
+ Container for synthetic city data.
21
+ """
22
+ taz: pd.DataFrame # TAZ-level attributes
23
+ distance_matrix: pd.DataFrame # inter-TAZ distances (km)
24
+ travel_time_matrix: pd.DataFrame # inter-TAZ travel times (minutes)
25
+
26
+
27
+ def generate_synthetic_city(
28
+ num_zones: int = NUM_ZONES_DEFAULT,
29
+ seed: Optional[int] = RANDOM_SEED
30
+ ) -> SyntheticCity:
31
+ """
32
+ Generate a synthetic metropolitan region with a specified number of
33
+ Traffic Analysis Zones (TAZs).
34
+
35
+ Outputs:
36
+ - taz: DataFrame indexed by TAZ id with socio-economic attributes
37
+ - distance_matrix: symmetric TAZ-to-TAZ distances (km)
38
+ - travel_time_matrix: car travel time (minutes)
39
+
40
+ Parameters
41
+ ----------
42
+ num_zones : int
43
+ Number of zones (TAZ) to generate. Default = 20.
44
+ seed : int, optional
45
+ Random seed for reproducibility.
46
+
47
+ Returns
48
+ -------
49
+ SyntheticCity
50
+ """
51
+ rng = np.random.default_rng(seed)
52
+
53
+ # ---------------------------------------------------------
54
+ # 1. Generate basic spatial layout (coordinates)
55
+ # ---------------------------------------------------------
56
+ # City spread over roughly 10 x 10 km area
57
+ x = rng.uniform(0, 10, size=num_zones)
58
+ y = rng.uniform(0, 10, size=num_zones)
59
+
60
+ # ---------------------------------------------------------
61
+ # 2. Socio-economic attributes at TAZ level
62
+ # ---------------------------------------------------------
63
+
64
+ # Population distribution (clip to avoid negative / too small)
65
+ population = rng.normal(loc=25000, scale=5000, size=num_zones)
66
+ population = np.clip(population, 8000, None).astype(int)
67
+
68
+ # Average household size ~3.2 with small variation
69
+ hh_size = rng.normal(loc=3.2, scale=0.3, size=num_zones)
70
+ households = (population / np.maximum(hh_size, 1.5)).astype(int)
71
+
72
+ # Workers and students as shares of population
73
+ workers = (population * rng.uniform(0.35, 0.45, size=num_zones)).astype(int)
74
+ students = (population * rng.uniform(0.20, 0.30, size=num_zones)).astype(int)
75
+
76
+ # Monthly income (arbitrary units), lognormal
77
+ income = rng.lognormal(mean=10.0, sigma=0.4, size=num_zones)
78
+
79
+ # Car ownership rate as a sigmoid of income
80
+ def sigmoid(z):
81
+ return 1.0 / (1.0 + np.exp(-z))
82
+
83
+ car_ownership_rate = sigmoid(0.00003 * income - 3.0)
84
+ cars = (
85
+ car_ownership_rate
86
+ * households
87
+ * rng.uniform(0.8, 1.2, size=num_zones)
88
+ ).astype(int)
89
+
90
+ # Land-use mix index (0–1)
91
+ land_use_mix = rng.uniform(0.2, 0.9, size=num_zones)
92
+
93
+ # Jobs and floor area
94
+ service_jobs = (workers * rng.uniform(0.8, 1.4, size=num_zones)).astype(int)
95
+ industrial_jobs = (workers * rng.uniform(0.3, 0.8, size=num_zones)).astype(int)
96
+ retail_jobs = (workers * rng.uniform(0.3, 0.7, size=num_zones)).astype(int)
97
+
98
+ school_capacity = (students * rng.uniform(1.1, 1.5, size=num_zones)).astype(int)
99
+ retail_floor_area = retail_jobs * rng.uniform(20, 40, size=num_zones) # arbitrary units
100
+
101
+ # Build TAZ DataFrame
102
+ taz_df = pd.DataFrame({
103
+ "TAZ": np.arange(1, num_zones + 1),
104
+ "x_km": x,
105
+ "y_km": y,
106
+ "population": population,
107
+ "households": households,
108
+ "workers": workers,
109
+ "students": students,
110
+ "income": income,
111
+ "car_ownership_rate": car_ownership_rate,
112
+ "cars": cars,
113
+ "land_use_mix": land_use_mix,
114
+ "service_jobs": service_jobs,
115
+ "industrial_jobs": industrial_jobs,
116
+ "retail_jobs": retail_jobs,
117
+ "school_capacity": school_capacity,
118
+ "retail_floor_area": retail_floor_area,
119
+ }).set_index("TAZ")
120
+
121
+ # ---------------------------------------------------------
122
+ # 3. Distance & travel time matrices
123
+ # ---------------------------------------------------------
124
+ coords = taz_df[["x_km", "y_km"]].to_numpy()
125
+ dx = coords[:, 0][:, None] - coords[:, 0][None, :]
126
+ dy = coords[:, 1][:, None] - coords[:, 1][None, :]
127
+ dist_km = np.sqrt(dx**2 + dy**2)
128
+
129
+ # Average car speed (km/h) and base travel times (minutes)
130
+ avg_speed_kmh = rng.uniform(25, 35)
131
+ tt_base = (dist_km / np.maximum(avg_speed_kmh, 1e-3)) * 60.0 # minutes
132
+
133
+ # Add random terminal / intersection delays (3–8 minutes)
134
+ tt_matrix = tt_base + rng.uniform(3, 8, size=(num_zones, num_zones))
135
+
136
+ # Intra-zonal adjustment (short distances and times)
137
+ np.fill_diagonal(dist_km, rng.uniform(0.2, 0.5, size=num_zones))
138
+ np.fill_diagonal(tt_matrix, rng.uniform(3, 5, size=num_zones))
139
+
140
+ distance_df = pd.DataFrame(
141
+ dist_km,
142
+ index=taz_df.index,
143
+ columns=taz_df.index,
144
+ )
145
+ travel_time_df = pd.DataFrame(
146
+ tt_matrix,
147
+ index=taz_df.index,
148
+ columns=taz_df.index,
149
+ )
150
+
151
+ # ---------------------------------------------------------
152
+ # 4. Return SyntheticCity object
153
+ # ---------------------------------------------------------
154
+ return SyntheticCity(
155
+ taz=taz_df,
156
+ distance_matrix=distance_df,
157
+ travel_time_matrix=travel_time_df,
158
+ )
pages/2_🚢_Trip_Generation.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from modules.trip_generation import trip_generation
3
+ import pandas as pd
4
+ import os
5
+
6
+ st.title("🚢 Trip Generation")
7
+
8
+ if "city" not in st.session_state:
9
+ st.error("Please generate the synthetic city first.")
10
+ st.stop()
11
+
12
+ city = st.session_state["city"]
13
+
14
+ if st.button("Run Trip Generation"):
15
+ P, A = trip_generation(city.taz)
16
+ st.session_state["productions"] = P
17
+ st.session_state["attractions"] = A
18
+ st.success("Trip generation completed!")
19
+
20
+ if "productions" in st.session_state:
21
+ P = st.session_state["productions"]
22
+ A = st.session_state["attractions"]
23
+
24
+ st.subheader("Productions")
25
+ st.dataframe(P)
26
+
27
+ st.subheader("Attractions (Balanced)")
28
+ st.dataframe(A)
29
+
30
+ os.makedirs("data", exist_ok=True)
31
+ P.to_csv("data/productions.csv")
32
+ A.to_csv("data/attractions.csv")
33
+ st.info("Saved to /data/")
pages/3_🌍_Trip_Distribution.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from modules.gravity_model import build_all_od_matrices
3
+ import pandas as pd
4
+ import os
5
+
6
+ st.title("🌍 Trip Distribution – Gravity Model")
7
+
8
+ # --------------------------------------------------
9
+ # CHECK PREVIOUS STEPS
10
+ # --------------------------------------------------
11
+ if "productions" not in st.session_state:
12
+ st.error("Please complete Trip Generation first.")
13
+ st.stop()
14
+
15
+ # --------------------------------------------------
16
+ # RUN GRAVITY MODEL
17
+ # --------------------------------------------------
18
+ if st.button("Run Gravity Model"):
19
+ P = st.session_state["productions"]
20
+ A = st.session_state["attractions"]
21
+ TT = st.session_state["city"].travel_time_matrix
22
+
23
+ od_mats = build_all_od_matrices(P, A, TT)
24
+
25
+ st.session_state["od"] = od_mats
26
+ st.success("Trip distribution completed!")
27
+
28
+ # --------------------------------------------------
29
+ # DISPLAY RESULTS
30
+ # --------------------------------------------------
31
+ if "od" in st.session_state:
32
+ od = st.session_state["od"]
33
+
34
+ os.makedirs("data", exist_ok=True)
35
+
36
+ for purpose, mat in od.items():
37
+ st.subheader(f"OD Matrix – {purpose}")
38
+ st.dataframe(mat)
39
+
40
+ mat.to_csv(f"data/od_{purpose}.csv")
41
+
42
+ st.info("OD matrices saved to /data/")
pages/4_🚈_Mode_Choice.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from modules.mode_choice import mode_choice
3
+ import os
4
+
5
+ st.title("🚈 Mode Choice – Multinomial Logit")
6
+
7
+ # -----------------------------------------
8
+ # CHECK PREVIOUS STEPS
9
+ # -----------------------------------------
10
+ if "od" not in st.session_state:
11
+ st.error("Please run Trip Distribution first.")
12
+ st.stop()
13
+
14
+ # -----------------------------------------
15
+ # RUN MODE CHOICE
16
+ # -----------------------------------------
17
+ if st.button("Run Mode Choice"):
18
+ result = mode_choice(
19
+ st.session_state["od"],
20
+ st.session_state["city"].taz,
21
+ st.session_state["city"].travel_time_matrix
22
+ )
23
+ st.session_state["mode_choice"] = result
24
+ st.success("Mode choice completed!")
25
+
26
+ # -----------------------------------------
27
+ # DISPLAY RESULTS
28
+ # -----------------------------------------
29
+ if "mode_choice" in st.session_state:
30
+
31
+ result = st.session_state["mode_choice"]
32
+
33
+ st.subheader("Total OD Matrix (all purposes)")
34
+ st.dataframe(result.total_od)
35
+
36
+ # ensure save folder exists
37
+ os.makedirs("data", exist_ok=True)
38
+
39
+ # save & display mode-specific OD volumes
40
+ for m in result.volumes:
41
+ st.subheader(f"Mode: {m}")
42
+ st.dataframe(result.volumes[m])
43
+
44
+ # Save to CSV
45
+ result.volumes[m].to_csv(f"data/od_mode_{m}.csv")
46
+
47
+ st.info("Mode-choice outputs saved to /data/")
pages/5_πŸ›£οΈ_Route_Assignment.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/5_πŸ›£οΈ_Route_Assignment.py
2
+
3
+ import streamlit as st
4
+ import os
5
+
6
+ from modules.route_assignment import (
7
+ generate_synthetic_network,
8
+ aon_assignment,
9
+ frank_wolfe_ue,
10
+ )
11
+
12
+ st.title("πŸ›£οΈ Route Assignment – AON & UE")
13
+
14
+ # ------------------------------------------------
15
+ # CHECK MODE CHOICE
16
+ # ------------------------------------------------
17
+ if "mode_choice" not in st.session_state:
18
+ st.error("Run Mode Choice first (Page 4).")
19
+ st.stop()
20
+
21
+ mode_choice = st.session_state["mode_choice"]
22
+ city = st.session_state["city"]
23
+ taz = city.taz
24
+
25
+ # ------------------------------------------------
26
+ # CHOOSE METHOD
27
+ # ------------------------------------------------
28
+ assignment_type = st.selectbox(
29
+ "Select assignment method",
30
+ ["All-or-Nothing (AON)", "User Equilibrium (UE – Frank–Wolfe)"]
31
+ )
32
+
33
+ # ------------------------------------------------
34
+ # RUN ASSIGNMENT
35
+ # ------------------------------------------------
36
+ if st.button("Generate Network & Run Assignment"):
37
+
38
+ # Generate or load network
39
+ if "network" in st.session_state:
40
+ network = st.session_state["network"]
41
+ else:
42
+ network = generate_synthetic_network(taz)
43
+ st.session_state["network"] = network
44
+
45
+ # Use car OD matrix only
46
+ car_od = mode_choice.volumes["car"]
47
+
48
+ if assignment_type.startswith("All"):
49
+ link_flows = aon_assignment(car_od, network)
50
+ st.session_state["link_flows"] = link_flows
51
+ st.success("All-or-Nothing assignment completed.")
52
+ else:
53
+ link_flows_ue = frank_wolfe_ue(car_od, network)
54
+ st.session_state["link_flows"] = link_flows_ue
55
+ st.success("User Equilibrium (Frank–Wolfe) assignment completed.")
56
+
57
+ # ------------------------------------------------
58
+ # DISPLAY RESULTS
59
+ # ------------------------------------------------
60
+ if "link_flows" in st.session_state:
61
+ st.subheader("Assigned Link Flows (sample)")
62
+ st.dataframe(st.session_state["link_flows"].head(12))
63
+
64
+ # Save to /data/
65
+ os.makedirs("data", exist_ok=True)
66
+ st.session_state["link_flows"].to_csv("data/link_flows.csv")
67
+ st.info("Link flows saved to /data/")
pages/6_πŸ€–_AI_Enhanced_Models.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import shap
5
+ import matplotlib.pyplot as plt
6
+ import os
7
+
8
+ from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
9
+ from sklearn.model_selection import train_test_split
10
+ from sklearn.metrics import mean_absolute_error, r2_score, accuracy_score
11
+
12
+ st.set_page_config(layout="wide")
13
+ st.title("πŸ€– AI-Enhanced Four-Step Model")
14
+
15
+ st.markdown("""
16
+ This module introduces **Machine Learning + Explainable AI (XAI)** to improve:
17
+ - **Trip Generation (Regression Model)**
18
+ - **Mode Choice (Classification Model)**
19
+ - **Behavioral Interpretation using SHAP**
20
+
21
+ Use this page *after* completing Steps 1–5.
22
+ """)
23
+
24
+ # -------------------------------------------------------
25
+ # CHECK DATA
26
+ # -------------------------------------------------------
27
+ if "city" not in st.session_state:
28
+ st.error("Please generate the synthetic city first (Page 1).")
29
+ st.stop()
30
+
31
+ if "productions" not in st.session_state:
32
+ st.error("Please complete Trip Generation (Page 2).")
33
+ st.stop()
34
+
35
+ if "mode_choice" not in st.session_state:
36
+ st.error("Please complete Mode Choice (Page 4).")
37
+ st.stop()
38
+
39
+ # Load needed data
40
+ taz = st.session_state["city"].taz
41
+ productions = st.session_state["productions"]
42
+ mode_choice_result = st.session_state["mode_choice"]
43
+
44
+ # -------------------------------------------------------
45
+ # SECTION 1 β€” AI Trip Generation (Regression)
46
+ # -------------------------------------------------------
47
+ st.header("🚢 AI-based Trip Generation (Regression)")
48
+
49
+ purpose = st.selectbox(
50
+ "Select Trip Purpose to Model",
51
+ ["HBW", "HBE", "HBS"]
52
+ )
53
+
54
+ X = taz[[
55
+ "population", "households", "workers", "students",
56
+ "income", "car_ownership_rate", "land_use_mix",
57
+ "service_jobs", "industrial_jobs", "retail_jobs"
58
+ ]]
59
+
60
+ y = productions[purpose]
61
+
62
+ if st.button("Train AI Trip Generation Model"):
63
+ X_train, X_test, y_train, y_test = train_test_split(
64
+ X, y, test_size=0.25, random_state=42
65
+ )
66
+
67
+ model = RandomForestRegressor(
68
+ n_estimators=300,
69
+ max_depth=10,
70
+ random_state=42
71
+ )
72
+ model.fit(X_train, y_train)
73
+
74
+ y_pred = model.predict(X_test)
75
+
76
+ mae = mean_absolute_error(y_test, y_pred)
77
+ r2 = r2_score(y_test, y_pred)
78
+
79
+ st.success("AI Regression Model Trained!")
80
+ st.write(f"**MAE:** {mae:.2f}")
81
+ st.write(f"**RΒ²:** {r2:.3f}")
82
+
83
+ st.session_state["ai_tripgen_model"] = model
84
+
85
+ st.subheader("πŸ” SHAP Explanation of Trip Generation Model")
86
+ explainer = shap.Explainer(model, X_train)
87
+ shap_values = explainer(X_train)
88
+
89
+ shap.plots.bar(shap_values, max_display=10, show=False)
90
+ fig = plt.gcf()
91
+ st.pyplot(fig)
92
+
93
+ # -------------------------------------------------------
94
+ # SECTION 2 β€” AI Mode Choice (Classification)
95
+ # -------------------------------------------------------
96
+ st.header("🚈 AI-based Mode Choice (Classification)")
97
+
98
+ vol = mode_choice_result.volumes
99
+ P = mode_choice_result.probabilities
100
+
101
+ rows = []
102
+ zones = list(taz.index)
103
+ TT = st.session_state["city"].travel_time_matrix
104
+
105
+ for i in zones:
106
+ for j in zones:
107
+ if i == j:
108
+ continue
109
+
110
+ probs = [
111
+ P["car"].loc[i, j],
112
+ P["metro"].loc[i, j],
113
+ P["bus"].loc[i, j]
114
+ ]
115
+
116
+ # Normalize probabilities (avoid zero-sum)
117
+ s = sum(probs)
118
+ if s == 0:
119
+ continue
120
+ probs = [p / s for p in probs]
121
+
122
+ label = np.random.choice(["car", "metro", "bus"], p=probs)
123
+
124
+ rows.append({
125
+ "origin": i,
126
+ "destination": j,
127
+ "travel_time": TT.loc[i, j],
128
+ "car_ownership": float(taz.loc[i, "car_ownership_rate"]),
129
+ "cost_car": 2 + 0.1 * TT.loc[i, j],
130
+ "cost_metro": 15,
131
+ "cost_bus": 8,
132
+ "label": label
133
+ })
134
+
135
+ df_mc = pd.DataFrame(rows)
136
+
137
+ feature_cols = ["travel_time", "car_ownership", "cost_car", "cost_metro", "cost_bus"]
138
+ X_mc = df_mc[feature_cols]
139
+ y_mc = df_mc["label"]
140
+
141
+ if st.button("Train AI Mode Choice Classifier"):
142
+ X_train, X_test, y_train, y_test = train_test_split(
143
+ X_mc, y_mc, test_size=0.25, random_state=42, stratify=y_mc
144
+ )
145
+
146
+ clf = RandomForestClassifier(
147
+ n_estimators=300,
148
+ max_depth=12,
149
+ class_weight="balanced",
150
+ random_state=42
151
+ )
152
+ clf.fit(X_train, y_train)
153
+
154
+ y_pred = clf.predict(X_test)
155
+ acc = accuracy_score(y_test, y_pred)
156
+
157
+ st.success("AI Mode Choice Classifier Trained!")
158
+ st.write(f"**Accuracy:** {acc:.3f}")
159
+
160
+ st.session_state["ai_modechoice_model"] = clf
161
+
162
+ st.subheader("πŸ” SHAP Explanation for Mode Choice")
163
+
164
+ explainer = shap.Explainer(clf, X_train)
165
+ shap_values = explainer(X_train)
166
+
167
+ shap.plots.bar(shap_values, max_display=10, show=False)
168
+ fig2 = plt.gcf()
169
+ st.pyplot(fig2)
170
+
171
+ # -------------------------------------------------------
172
+ # SECTION 3 β€” Summary
173
+ # -------------------------------------------------------
174
+ st.header("πŸ“˜ Interpretation Summary")
175
+
176
+ st.markdown("""
177
+ ### βœ” Completed:
178
+ - **AI Regression for Trip Generation**
179
+ - **AI Classification for Mode Choice**
180
+ - **SHAP-based Explainability**
181
+
182
+ ### βœ” Enables:
183
+ - Hybrid classical–AI modelling
184
+ - Behavioral insights
185
+ - Scenario sensitivity
186
+ - Publishable Q1-grade figures
187
+ """)
pages/7_πŸ“¦_Export_Results.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ import zipfile
5
+ from io import BytesIO
6
+
7
+ st.set_page_config(layout="wide")
8
+ st.title("πŸ“¦ Export Results")
9
+
10
+ st.markdown("""
11
+ This module allows you to **download full outputs** from all steps of the Four-Step Model:
12
+
13
+ - Synthetic City (TAZ data)
14
+ - Trip Generation
15
+ - Trip Distribution (OD matrices)
16
+ - Mode Choice (volumes + probabilities)
17
+ - Route Assignment (link flows)
18
+ - AI Model Outputs (regression, classification)
19
+ """)
20
+
21
+ # Ensure data folder exists
22
+ os.makedirs("data", exist_ok=True)
23
+
24
+ # ------------------------------------------------------
25
+ # Helper: Save CSV to buffer
26
+ # ------------------------------------------------------
27
+ def make_csv_download(df: pd.DataFrame, filename: str):
28
+ csv = df.to_csv().encode("utf-8")
29
+ st.download_button(
30
+ label=f"⬇ Download {filename}",
31
+ data=csv,
32
+ file_name=filename,
33
+ mime="text/csv"
34
+ )
35
+
36
+ # ------------------------------------------------------
37
+ # Helper: Create ZIP file dynamically
38
+ # ------------------------------------------------------
39
+ def build_zip_file():
40
+ buffer = BytesIO()
41
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as z:
42
+
43
+ if "city" in st.session_state:
44
+ city = st.session_state["city"]
45
+ z.writestr("taz_attributes.csv", city.taz.to_csv())
46
+ z.writestr("distance_matrix.csv", city.distance_matrix.to_csv())
47
+ z.writestr("travel_time_matrix.csv", city.travel_time_matrix.to_csv())
48
+
49
+ if "productions" in st.session_state:
50
+ z.writestr("productions.csv", st.session_state["productions"].to_csv())
51
+ if "attractions" in st.session_state:
52
+ z.writestr("attractions.csv", st.session_state["attractions"].to_csv())
53
+
54
+ if "od" in st.session_state:
55
+ for p, df in st.session_state["od"].items():
56
+ z.writestr(f"od_{p}.csv", df.to_csv())
57
+
58
+ if "mode_choice" in st.session_state:
59
+ mc = st.session_state["mode_choice"]
60
+ z.writestr("total_od.csv", mc.total_od.to_csv())
61
+ for m, df in mc.volumes.items():
62
+ z.writestr(f"od_mode_{m}.csv", df.to_csv())
63
+ for m, df in mc.probabilities.items():
64
+ z.writestr(f"mode_prob_{m}.csv", df.to_csv())
65
+
66
+ if "link_flows" in st.session_state:
67
+ link_flows = st.session_state["link_flows"]
68
+ z.writestr("link_flows.csv", link_flows.to_csv())
69
+
70
+ # AI models not serializable β†’ export predictions & metadata only
71
+ if "ai_tripgen_model" in st.session_state:
72
+ model = st.session_state["ai_tripgen_model"]
73
+ city = st.session_state["city"]
74
+ preds = model.predict(city.taz[[
75
+ "population","households","workers","students",
76
+ "income","car_ownership_rate","land_use_mix",
77
+ "service_jobs","industrial_jobs","retail_jobs"
78
+ ]])
79
+ pred_df = pd.DataFrame(preds, index=city.taz.index,
80
+ columns=["AI_TripGen_Pred"])
81
+ z.writestr("ai_trip_generation_predictions.csv", pred_df.to_csv())
82
+
83
+ if "ai_modechoice_model" in st.session_state:
84
+ clf = st.session_state["ai_modechoice_model"]
85
+ z.writestr("ai_modechoice_classes.txt",
86
+ "\n".join(list(clf.classes_)))
87
+
88
+ buffer.seek(0)
89
+ return buffer
90
+
91
+ # ------------------------------------------------------
92
+ # DISPLAY DOWNLOAD SECTION
93
+ # ------------------------------------------------------
94
+
95
+ st.header("πŸ“ Download Individual Outputs")
96
+
97
+ # Synthetic City
98
+ if "city" in st.session_state:
99
+ st.subheader("πŸ™οΈ Synthetic City")
100
+ make_csv_download(st.session_state["city"].taz, "taz_attributes.csv")
101
+ make_csv_download(st.session_state["city"].distance_matrix, "distance_matrix.csv")
102
+ make_csv_download(st.session_state["city"].travel_time_matrix, "travel_time_matrix.csv")
103
+ else:
104
+ st.info("Synthetic city not generated yet.")
105
+
106
+ # Trip Generation
107
+ if "productions" in st.session_state:
108
+ st.subheader("🚢 Trip Generation")
109
+ make_csv_download(st.session_state["productions"], "productions.csv")
110
+ make_csv_download(st.session_state["attractions"], "attractions.csv")
111
+
112
+ # Trip Distribution
113
+ if "od" in st.session_state:
114
+ st.subheader("🌍 Trip Distribution – OD Matrices")
115
+ for purpose, df in st.session_state["od"].items():
116
+ make_csv_download(df, f"od_{purpose}.csv")
117
+
118
+ # Mode Choice
119
+ if "mode_choice" in st.session_state:
120
+ st.subheader("🚈 Mode Choice – Volumes & Probabilities")
121
+ mc = st.session_state["mode_choice"]
122
+ make_csv_download(mc.total_od, "total_od.csv")
123
+ for m, df in mc.volumes.items():
124
+ make_csv_download(df, f"od_mode_{m}.csv")
125
+ for m, df in mc.probabilities.items():
126
+ make_csv_download(df, f"mode_prob_{m}.csv")
127
+
128
+ # Route Assignment
129
+ if "link_flows" in st.session_state:
130
+ st.subheader("πŸ›£οΈ Route Assignment – Link Flows")
131
+ make_csv_download(st.session_state["link_flows"], "link_flows.csv")
132
+
133
+ # AI Models
134
+ if "ai_tripgen_model" in st.session_state or "ai_modechoice_model" in st.session_state:
135
+ st.subheader("πŸ€– AI Model Outputs")
136
+
137
+ if "ai_tripgen_model" in st.session_state:
138
+ st.write("β€’ AI Trip Generation model predictions available")
139
+
140
+ if "ai_modechoice_model" in st.session_state:
141
+ st.write("β€’ AI Mode Choice classifier classes available")
142
+
143
+ # ------------------------------------------------------
144
+ # ZIP EXPORT
145
+ # ------------------------------------------------------
146
+
147
+ st.header("πŸ“¦ Download EVERYTHING (ZIP)")
148
+
149
+ if st.button("Create ZIP Package"):
150
+ zip_buffer = build_zip_file()
151
+ st.download_button(
152
+ label="⬇ Download Zip File",
153
+ data=zip_buffer,
154
+ file_name="TripAI_outputs.zip",
155
+ mime="application/zip"
156
+ )
157
+ st.success("ZIP file prepared!")
pages/8_βš™οΈ_Policy_Scenarios.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import pandas as pd
4
+ import os
5
+
6
+ from modules.gravity_model import build_all_od_matrices
7
+ from modules.route_assignment import generate_synthetic_network, aon_assignment
8
+
9
+ st.set_page_config(layout="wide")
10
+ st.title("βš™οΈ Policy Scenario Engine")
11
+
12
+ st.markdown("""
13
+ Use this page to test **policy scenarios** on top of the synthetic four-step model:
14
+
15
+ - 🚈 **Metro Improvement**: faster travel time, lower fare
16
+ - πŸš— **Congestion Charge**: extra cost for car trips into CBD zones
17
+ - πŸ™οΈ **Transit-Oriented Development (TOD)**: increase attractions in selected TAZs
18
+
19
+ The engine compares **Baseline vs Scenario** in terms of:
20
+ - Mode shares (Car / Metro / Bus)
21
+ - Car link flows (AON assignment)
22
+ """)
23
+
24
+ # ------------------------------------------------------
25
+ # CHECK REQUIRED STATE
26
+ # ------------------------------------------------------
27
+ required_keys = ["city", "productions", "attractions", "od", "mode_choice"]
28
+ missing = [k for k in required_keys if k not in st.session_state]
29
+
30
+ if missing:
31
+ st.error(f"Please complete previous steps first. Missing: {', '.join(missing)}")
32
+ st.stop()
33
+
34
+ city = st.session_state["city"]
35
+ taz = city.taz
36
+ productions = st.session_state["productions"]
37
+ attractions_base = st.session_state["attractions"]
38
+ od_base = st.session_state["od"]
39
+ mode_choice_base = st.session_state["mode_choice"]
40
+ tt_car_base = city.travel_time_matrix.copy()
41
+
42
+ zones = list(taz.index)
43
+
44
+ # ------------------------------------------------------
45
+ # HELPER: MODE SHARE CALCULATION
46
+ # ------------------------------------------------------
47
+ def compute_mode_shares(mode_volumes: dict, total_od: pd.DataFrame) -> pd.DataFrame:
48
+ total_trips = total_od.values.sum()
49
+ rows = []
50
+ for m, mat in mode_volumes.items():
51
+ trips = mat.values.sum()
52
+ share = trips / total_trips if total_trips > 0 else 0
53
+ rows.append({"mode": m, "trips": trips, "share": share})
54
+ return pd.DataFrame(rows)
55
+
56
+ # ------------------------------------------------------
57
+ # SIDEBAR – POLICY CONTROLS
58
+ # ------------------------------------------------------
59
+ st.sidebar.header("Policy Controls")
60
+
61
+ st.sidebar.subheader("🚈 Metro Improvement")
62
+ metro_time_reduction_pct = st.sidebar.slider(
63
+ "Metro travel time reduction (%)", 0, 50, 20
64
+ )
65
+ metro_fare_change_pct = st.sidebar.slider(
66
+ "Metro fare change (%)", -50, 50, -20
67
+ )
68
+
69
+ st.sidebar.subheader("πŸš— Congestion Charge (Car)")
70
+ congestion_charge = st.sidebar.slider(
71
+ "Extra generalized cost for car entering CBD", 0.0, 50.0, 20.0, step=1.0
72
+ )
73
+
74
+ default_cbd = zones[:5] if len(zones) >= 5 else zones
75
+ cbd_zones = st.sidebar.multiselect(
76
+ "CBD zones (destinations)", options=zones, default=default_cbd
77
+ )
78
+
79
+ st.sidebar.subheader("πŸ™οΈ TOD – Modify Attractions")
80
+ apply_tod = st.sidebar.checkbox("Apply TOD", value=False)
81
+ tod_increase_pct = st.sidebar.slider(
82
+ "Attraction increase (%)", 0, 100, 30
83
+ )
84
+ tod_zones = st.sidebar.multiselect(
85
+ "TOD zones", options=zones, default=zones[:3] if len(zones) >= 3 else zones
86
+ )
87
+
88
+ st.sidebar.markdown("---")
89
+ run_button = st.sidebar.button("β–Ά Run Scenario")
90
+
91
+ # ------------------------------------------------------
92
+ # BASELINE SUMMARY
93
+ # ------------------------------------------------------
94
+ st.header("πŸ“Š Baseline Summary")
95
+
96
+ baseline_shares = compute_mode_shares(
97
+ mode_choice_base.volumes, mode_choice_base.total_od
98
+ )
99
+
100
+ col1, col2 = st.columns(2)
101
+
102
+ with col1:
103
+ st.subheader("Baseline Mode Shares")
104
+ st.dataframe(baseline_shares.style.format({"trips": "{:.1f}", "share": "{:.3f}"}))
105
+
106
+ with col2:
107
+ if "link_flows" in st.session_state:
108
+ st.subheader("Baseline Car Link Flows (sample)")
109
+ st.dataframe(st.session_state["link_flows"].head(10))
110
+ else:
111
+ st.info("Baseline link flows not stored. Run Route Assignment page.")
112
+
113
+ # ------------------------------------------------------
114
+ # BUILD POLICY TIME/COST MATRICES
115
+ # ------------------------------------------------------
116
+ def build_policy_time_cost_matrices(
117
+ tt_car_base: pd.DataFrame,
118
+ metro_time_reduction_pct: float,
119
+ metro_fare_change_pct: float,
120
+ congestion_charge: float,
121
+ cbd_zones: list
122
+ ):
123
+ """
124
+ Build modified travel time and cost matrices under policy scenario.
125
+ """
126
+ tt_car = tt_car_base.copy()
127
+
128
+ # Base functions: metro faster, bus slower
129
+ tt_metro = tt_car * 0.8 * (1 - metro_time_reduction_pct / 100.0)
130
+ tt_bus = tt_car * 1.3
131
+
132
+ # Distance proxy
133
+ dist_proxy = tt_car / 60 * 30
134
+
135
+ cost_car = 2 + 0.12 * dist_proxy
136
+ cost_metro = 15 * (1 + metro_fare_change_pct / 100.0)
137
+ cost_bus = 8 + 0.03 * dist_proxy
138
+
139
+ # FIX: apply congestion charge vectorized
140
+ cost_car.loc[:, cbd_zones] += congestion_charge
141
+
142
+ return (
143
+ {"car": tt_car, "metro": tt_metro, "bus": tt_bus},
144
+ {"car": cost_car, "metro": cost_metro, "bus": cost_bus}
145
+ )
146
+
147
+ # ------------------------------------------------------
148
+ # POLICY MODE CHOICE (Multinomial Logit)
149
+ # ------------------------------------------------------
150
+ def policy_mode_choice(
151
+ od_mats: dict,
152
+ taz: pd.DataFrame,
153
+ tt_car_base: pd.DataFrame,
154
+ metro_time_reduction_pct: float,
155
+ metro_fare_change_pct: float,
156
+ congestion_charge: float,
157
+ cbd_zones: list,
158
+ beta_time: float = -0.06,
159
+ beta_cost: float = -0.03,
160
+ beta_car_own: float = 0.5
161
+ ):
162
+ zones = tt_car_base.index
163
+
164
+ # Total OD (sum over purposes)
165
+ total_od = sum(od_mats.values())
166
+ total_od = total_od.loc[zones, zones]
167
+
168
+ # Updated time/cost
169
+ time_mats, cost_mats = build_policy_time_cost_matrices(
170
+ tt_car_base,
171
+ metro_time_reduction_pct,
172
+ metro_fare_change_pct,
173
+ congestion_charge,
174
+ cbd_zones
175
+ )
176
+
177
+ # Car ownership
178
+ car_own = taz["car_ownership_rate"].reindex(zones).to_numpy()
179
+ car_own_matrix = np.repeat(car_own[:, None], len(zones), axis=1)
180
+
181
+ modes = ["car", "metro", "bus"]
182
+ utilities = {}
183
+
184
+ for mode in modes:
185
+ tt = time_mats[mode].to_numpy()
186
+ cc = cost_mats[mode].to_numpy()
187
+
188
+ if mode == "car":
189
+ U = beta_time * tt + beta_cost * cc + beta_car_own * car_own_matrix
190
+ else:
191
+ U = beta_time * tt + beta_cost * cc
192
+
193
+ utilities[mode] = U
194
+
195
+ # Probabilities
196
+ exp_sum = sum(np.exp(U) for U in utilities.values())
197
+ probabilities = {
198
+ mode: pd.DataFrame(np.exp(U) / np.maximum(exp_sum, 1e-12), index=zones, columns=zones)
199
+ for mode, U in utilities.items()
200
+ }
201
+
202
+ # Mode flows
203
+ volumes = {
204
+ mode: pd.DataFrame(
205
+ total_od.to_numpy() * probabilities[mode].to_numpy(),
206
+ index=zones, columns=zones
207
+ )
208
+ for mode in modes
209
+ }
210
+
211
+ return probabilities, volumes, total_od
212
+
213
+ # ------------------------------------------------------
214
+ # RUN SCENARIO
215
+ # ------------------------------------------------------
216
+ if run_button:
217
+ st.header("πŸ§ͺ Scenario Results")
218
+
219
+ # 1) TOD β†’ Modify attractions
220
+ if apply_tod:
221
+ st.subheader("πŸ™οΈ TOD Applied β€” Recomputing OD")
222
+
223
+ A_scenario = attractions_base.copy(deep=True)
224
+ factor = 1 + tod_increase_pct / 100.0
225
+
226
+ for z in tod_zones:
227
+ if z in A_scenario.index:
228
+ A_scenario.loc[z, ["HBW", "HBS"]] *= factor
229
+
230
+ # FIX: consistent call
231
+ od_scenario = build_all_od_matrices(productions, A_scenario, tt_car_base)
232
+ else:
233
+ st.subheader("πŸ™οΈ TOD NOT applied β€” using baseline OD")
234
+ od_scenario = od_base
235
+
236
+ # 2) Policy Mode Choice
237
+ st.subheader("🚈 Mode Choice under Policy Scenario")
238
+ probs_scen, vols_scen, total_od_scen = policy_mode_choice(
239
+ od_scenario,
240
+ taz,
241
+ tt_car_base,
242
+ metro_time_reduction_pct,
243
+ metro_fare_change_pct,
244
+ congestion_charge,
245
+ cbd_zones
246
+ )
247
+
248
+ # 3) Mode share comparison
249
+ scenario_shares = compute_mode_shares(vols_scen, total_od_scen)
250
+
251
+ colA, colB = st.columns(2)
252
+ with colA:
253
+ st.markdown("#### Baseline Mode Shares")
254
+ st.dataframe(baseline_shares.style.format({"trips": "{:.1f}", "share": "{:.3f}"}))
255
+ with colB:
256
+ st.markdown("#### Scenario Mode Shares")
257
+ st.dataframe(scenario_shares.style.format({"trips": "{:.1f}", "share": "{:.3f}"}))
258
+
259
+ # 4) AON Car Assignment
260
+ st.subheader("πŸ›£οΈ Scenario Car Assignment (AON)")
261
+
262
+ if "network" in st.session_state:
263
+ network = st.session_state["network"]
264
+ else:
265
+ network = generate_synthetic_network(taz)
266
+
267
+ car_od_scen = vols_scen["car"]
268
+ link_flows_scen = aon_assignment(car_od_scen, network)
269
+ st.session_state["link_flows_scenario"] = link_flows_scen
270
+
271
+ # FIX: save to data folder
272
+ os.makedirs("data", exist_ok=True)
273
+ link_flows_scen.to_csv("data/link_flows_scenario.csv")
274
+
275
+ st.markdown("**Scenario Car Link Flows (sample)**")
276
+ st.dataframe(link_flows_scen.head(10))
277
+
278
+ # 5) Summary Numbers
279
+ st.subheader("πŸ“‰ Key Comparison")
280
+
281
+ baseline_car = mode_choice_base.volumes["car"].values.sum()
282
+ scenario_car = vols_scen["car"].values.sum()
283
+
284
+ st.write(f"**Baseline car trips:** {baseline_car:,.1f}")
285
+ st.write(f"**Scenario car trips:** {scenario_car:,.1f}")
286
+
287
+ if baseline_car > 0:
288
+ pct = 100 * (scenario_car - baseline_car) / baseline_car
289
+ st.write(f"**Change in car trips:** {pct:+.2f}%")
290
+
291
+ st.success("Scenario evaluation completed. Use Export page to download results.")
292
+
293
+ else:
294
+ st.info("Adjust policy parameters on the left, then click **Run Scenario**.")
pages/9_πŸ“ˆ_Visualization_Dashboard.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ import os
7
+
8
+ st.set_page_config(layout="wide")
9
+ st.title("πŸ“ˆ Visualization Dashboard")
10
+
11
+ st.markdown("""
12
+ This dashboard provides **research-grade visualizations** for:
13
+ - Mode share comparison (Baseline vs Scenario)
14
+ - OD heatmaps
15
+ - Car flow changes on network links
16
+ - TAZ-level spatial indicators
17
+
18
+ All figures are exportable in 600 DPI for Q1-grade publications.
19
+ """)
20
+
21
+ # ------------------------------------------------------
22
+ # CHECK REQUIRED STATE
23
+ # ------------------------------------------------------
24
+ if "city" not in st.session_state:
25
+ st.error("Generate synthetic city first.")
26
+ st.stop()
27
+
28
+ if "mode_choice" not in st.session_state:
29
+ st.error("Complete Mode Choice first.")
30
+ st.stop()
31
+
32
+ city = st.session_state["city"]
33
+ taz = city.taz
34
+
35
+ # Baseline
36
+ mode_base = st.session_state["mode_choice"]
37
+ car_flow_base = st.session_state.get("link_flows", None)
38
+
39
+ # Scenario check
40
+ scenario_exists = (
41
+ "vols_scen" in st.session_state and
42
+ "total_od_scen" in st.session_state and
43
+ "link_flows_scenario" in st.session_state
44
+ )
45
+
46
+ # ------------------------------------------------------
47
+ # Helper: Export figure in 600 DPI
48
+ # ------------------------------------------------------
49
+ def export_fig(fig, filename):
50
+ fig.savefig(filename, dpi=600, bbox_inches="tight")
51
+ with open(filename, "rb") as f:
52
+ st.download_button(
53
+ label="⬇ Download Figure",
54
+ data=f,
55
+ file_name=filename,
56
+ mime="image/png"
57
+ )
58
+ st.success("Figure exported in 600 DPI!")
59
+
60
+ # ======================================================
61
+ # SECTION 1: MODE SHARE COMPARISON
62
+ # ======================================================
63
+ st.header("🚈 Mode Share Comparison (Baseline vs Scenario)")
64
+
65
+ def compute_mode_shares(mode_volumes, total_od):
66
+ total_trips = total_od.values.sum()
67
+ rows = []
68
+ for m, mat in mode_volumes.items():
69
+ trips = mat.values.sum()
70
+ share = trips / total_trips if total_trips > 0 else 0
71
+ rows.append([m, trips, share])
72
+ return pd.DataFrame(rows, columns=["Mode", "Trips", "Share"])
73
+
74
+ baseline_shares = compute_mode_shares(
75
+ mode_base.volumes, mode_base.total_od
76
+ )
77
+
78
+ if scenario_exists:
79
+ vols_scen = st.session_state["vols_scen"]
80
+ total_od_scen = st.session_state["total_od_scen"]
81
+ scenario_shares = compute_mode_shares(vols_scen, total_od_scen)
82
+
83
+ colA, colB = st.columns(2)
84
+
85
+ with colA:
86
+ st.subheader("Baseline Mode Shares")
87
+ st.dataframe(baseline_shares.style.format({"Trips": "{:,.1f}", "Share": "{:.3f}"}))
88
+
89
+ with colB:
90
+ if scenario_exists:
91
+ st.subheader("Scenario Mode Shares")
92
+ st.dataframe(scenario_shares.style.format({"Trips": "{:,.1f}", "Share": "{:.3f}"}))
93
+ else:
94
+ st.info("Run a policy scenario to enable comparison.")
95
+
96
+ # Bar chart
97
+ fig, ax = plt.subplots(figsize=(8, 5))
98
+ ax.bar(baseline_shares["Mode"], baseline_shares["Share"], label="Baseline")
99
+
100
+ if scenario_exists:
101
+ ax.bar(
102
+ np.arange(len(scenario_shares)) + 0.3,
103
+ scenario_shares["Share"],
104
+ width=0.3,
105
+ label="Scenario"
106
+ )
107
+
108
+ ax.set_ylabel("Mode Share")
109
+ ax.set_title("Baseline vs Scenario Mode Shares")
110
+ ax.legend()
111
+ st.pyplot(fig)
112
+
113
+ export_fig(fig, "mode_share_comparison.png")
114
+
115
+ # ======================================================
116
+ # SECTION 2: OD HEATMAPS
117
+ # ======================================================
118
+ st.header("🌍 OD Heatmaps (Baseline & Scenario)")
119
+
120
+ od_base = st.session_state["od"]
121
+ purpose = st.selectbox("Select Trip Purpose", list(od_base.keys()))
122
+
123
+ # Baseline heatmap
124
+ st.subheader(f"Baseline OD – {purpose}")
125
+
126
+ fig2, ax2 = plt.subplots(figsize=(6, 5))
127
+ sns.heatmap(od_base[purpose], cmap="viridis", ax=ax2)
128
+ ax2.set_title(f"Baseline OD – {purpose}")
129
+ st.pyplot(fig2)
130
+ export_fig(fig2, f"baseline_od_{purpose}.png")
131
+
132
+ # Scenario heatmap
133
+ if scenario_exists:
134
+ od_scenario = st.session_state.get("od_scenario", None)
135
+
136
+ if isinstance(od_scenario, dict) and purpose in od_scenario:
137
+ od_scen_matrix = od_scenario[purpose]
138
+ else:
139
+ od_scen_matrix = od_base[purpose]
140
+
141
+ st.subheader(f"Scenario OD – {purpose}")
142
+
143
+ fig3, ax3 = plt.subplots(figsize=(6, 5))
144
+ sns.heatmap(od_scen_matrix, cmap="viridis", ax=ax3)
145
+ ax3.set_title(f"Scenario OD – {purpose}")
146
+ st.pyplot(fig3)
147
+ export_fig(fig3, f"scenario_od_{purpose}.png")
148
+
149
+ # ======================================================
150
+ # SECTION 3: CAR FLOW COMPARISON
151
+ # ======================================================
152
+ st.header("πŸš— Car Link Flows (Baseline vs Scenario)")
153
+
154
+ if car_flow_base is None:
155
+ st.info("Baseline link flows unavailable. Run Route Assignment first.")
156
+ else:
157
+ st.subheader("Baseline Link Flows")
158
+ st.dataframe(car_flow_base.head(10))
159
+
160
+ if scenario_exists:
161
+ car_flow_scen = st.session_state["link_flows_scenario"]
162
+
163
+ st.subheader("Scenario Link Flows")
164
+ st.dataframe(car_flow_scen.head(10))
165
+
166
+ # Safe merged comparison
167
+ merged = car_flow_base.copy()
168
+ merged["scenario"] = car_flow_scen.iloc[:, -1]
169
+ merged["change"] = merged["scenario"] - merged.iloc[:, -1]
170
+
171
+ fig4, ax4 = plt.subplots(figsize=(10, 5))
172
+ ax4.bar(
173
+ merged.index,
174
+ merged["change"],
175
+ color=["red" if x > 0 else "green" for x in merged["change"]]
176
+ )
177
+ ax4.set_title("Change in Car Link Flows")
178
+ ax4.set_ylabel("Ξ” Flow (veh/h)")
179
+ st.pyplot(fig4)
180
+
181
+ export_fig(fig4, "car_link_flow_change.png")
182
+
183
+ # ======================================================
184
+ # SECTION 4: TAZ SPATIAL MAPS
185
+ # ======================================================
186
+ st.header("πŸ—ΊοΈ TAZ-Level Spatial Indicators")
187
+
188
+ indicator = st.selectbox(
189
+ "Select variable to map",
190
+ ["population", "workers", "students", "land_use_mix", "cars"]
191
+ )
192
+
193
+ fig5, ax5 = plt.subplots(figsize=(6, 6))
194
+ scatter = ax5.scatter(
195
+ taz["x_km"], taz["y_km"],
196
+ c=taz[indicator],
197
+ s=220,
198
+ cmap="plasma",
199
+ edgecolors="black"
200
+ )
201
+ plt.colorbar(scatter, ax=ax5, label=indicator)
202
+ ax5.set_title(f"TAZ Map – {indicator.capitalize()}")
203
+ st.pyplot(fig5)
204
+
205
+ export_fig(fig5, f"taz_map_{indicator}.png")
206
+
207
+ st.success("Visualization dashboard ready.")