Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ This project includes a comprehensive dataset of Japanese railway information in
`preprocessor/src/gtfs/feed.rs` and `preprocessor/src/gtfs/odpt.rs`.
- Some of the reference travel times in `travel_times/cases.csv` are derived from
the Toei Subway GTFS (Bureau of Transportation, Tokyo Metropolitan Government,
CC BY 4.0) and from the Tokyo Metro, Metropolitan Intercity Railway (Tsukuba
Express), Tokyo Waterfront Area Rapid Transit (Rinkai Line), and Tama Toshi
Monorail GTFS feeds published by the Public Transportation Open Data Center
under the Public Transportation Open Data Basic License.
CC BY 4.0) and from the Tokyo Metro and Metropolitan Intercity Railway
(Tsukuba Express) GTFS feeds published by the Public Transportation Open Data
Center under the Public Transportation Open Data Basic License.

## Contributors ✨

Expand Down
11 changes: 3 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,20 +481,16 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int!
- `Legacy` (省略時): 追加した時点 (#1568) のモデルです。最高速度と加減速は
`dto::simulation::resolve_speed_profile` が決め、到着・出発の見込み
(`arrivalCumulativeMinutes` / `departureCumulativeMinutes`) は `null` です。
MobileApp のオートモードがこの値で走るので、値を変えません。速度の較正は、
到着時間推定の較正を求め直す前の表を `domain/legacy_speed_table.rs` に凍結して
使います。
MobileApp のオートモードがこの値で走るので、値を変えません。
- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルです。速度の較正
テーブルは、このモデルに当てはめて求めています。返す駅列に推定を掛け、停車・
通過、最高速度、加減速を推定が使った値に置き換えて、到着・出発の見込みを入れ
ます。`legs` を渡したときの見込みは、同じ `legs` を渡した
`estimateArrivalTimes` と同じ値です。バスの駅を含む経路は推定のモデルの対象外
なので、`Legacy` と同じ値を返します。

2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブル、駅間の距離が違い
ます。到着時間推定は、線路の長さ (`connections`) がある駅間ではそれを走行距離に
使い、無い駅間だけ直線距離 × 迂回係数で見積もります。乗換経路探索
(`connectedRoutes`) の所要時間も同じ距離で求めます。`Legacy` の値で台形の速度プロファイルを作って走らせると、
2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブルに無い路線・種別の
速度が違います。`Legacy` の値で台形の速度プロファイルを作って走らせると、
`estimateArrivalTimes` より短い時間で走り切ります (#1709)。所要時間を推定に
合わせたいクライアントは、`Estimated` の見込みを使います。

Expand Down Expand Up @@ -764,7 +760,6 @@ repository の実装がないメソッドは、空の結果ではなく `DomainE
│ │ ├── entity/ # Station / Line / TrainType / Company ...
│ │ ├── repository/ # 抽象インターフェース
│ │ ├── arrival_estimation.rs
│ │ ├── legacy_speed_table.rs # trainRoute の Legacy (オートモード) が使う凍結した較正
│ │ ├── route_search.rs # 乗換経路探索 (RAPTOR)
│ │ ├── route_topology.rs # 所要時間を持たない系統網 (stationsByName の到達判定)
│ │ ├── segment_speed_table.rs
Expand Down
5 changes: 0 additions & 5 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ OSM データは [Open Database License (ODbL)](https://www.openstreetmap.org/co
2 種類を再計算します。運動学モデルを Python で再現し、実ダイヤの所要時間を
再現する実効最高速度を二分探索でフィッティングします。

駅間の距離は、推定と同じく線路の長さ(`generated/connections.csv`)を使い、無い
駅間だけ直線距離 × 迂回係数で見積もります。先に `make data` で `generated/` を
作ってから実行してください。較正を変えたら、`make travel-time-report` で全体の
誤差が悪くならないことを確かめます(`travel_times/README.md`)。

1. **路線 × 列車種別**(`stationapi/src/domain/speed_table.rs`):
列車全体の所要時間へのフィット。一般則(路線種別の基本速度 × 種別倍率)から
±10% 以上乖離した路線だけを出力します。
Expand Down
50 changes: 9 additions & 41 deletions scripts/compute_speed_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@
TYPES_CSV = os.path.join(ROOT, "data", "4!types.csv")
SST_CSV = os.path.join(ROOT, "data", "5!station_station_types.csv")
SPEED_TABLE_RS = os.path.join(ROOT, "stationapi", "src", "domain", "speed_table.rs")
# 隣り合う駅の線路の長さ。preprocessor (make data) が N02 から求め、Worker に埋め込む
# ものと同じ。到着時間推定はこれがある駅間では迂回係数ではなく線路の長さを使うので、
# 較正も同じ距離で行う。
CONNECTIONS_CSV = os.path.join(ROOT, "generated", "connections.csv")
CACHE_DIR = os.path.join(HERE, ".gtfs_cache")

HTTP_TIMEOUT = 120
Expand All @@ -75,7 +71,7 @@ class Feed:
Feed(
key="hakodate_tram",
name="函館市電",
url="https://api-public.odpt.org/api/v4/files/odpt/HakodateCity/Alllines.zip?date=20260815",
url="https://api-public.odpt.org/api/v4/files/odpt/HakodateCity/Alllines.zip?date=20260615",
needs_token=False,
license="公共交通オープンデータセンター(認証なし公開)",
),
Expand Down Expand Up @@ -313,32 +309,6 @@ def load_repo_data():
return lines, by_line, kinds, groups


def load_track_distances() -> dict[tuple[int, int], float]:
"""隣り合う 2 駅の線路の長さ(m)。キーは (小さい station_cd, 大きい station_cd)。"""
if not os.path.isfile(CONNECTIONS_CSV):
raise SystemExit(
f"{CONNECTIONS_CSV} がありません。make data で generated/ を作ってから実行してください"
"(到着時間推定は線路の長さを使うので、較正も同じ距離で行う)"
)
track: dict[tuple[int, int], float] = {}
with open(CONNECTIONS_CSV, newline="", encoding="utf-8") as f:
for r in csv.DictReader(f):
a, b = int(r["station_cd1"]), int(r["station_cd2"])
d = float(r["distance"])
if d > 0:
track[(min(a, b), max(a, b))] = d
return track


def pair_distance(track: dict, a: dict, b: dict, alpha: float) -> float:
"""駅 a→b の走行距離(m)。estimate_arrival_minutes_with_track と同じく、線路の長さが
あればそれを、無ければ直線距離 × 迂回係数を使う。"""
d = track.get((min(a["cd"], b["cd"]), max(a["cd"], b["cd"])))
if d:
return d
return haversine(a["lat"], a["lon"], b["lat"], b["lon"]) * alpha


def line_detour(lines: dict, by_line: dict, line_cd: int) -> float:
"""路線全体で較正した迂回係数 α(estimate_arrival_minutes_calibrated と同等)。"""
li = lines[line_cd]
Expand Down Expand Up @@ -710,7 +680,7 @@ def segment_sanity_upper_kmh(base: float, line_type: int | None) -> float:


def calibrate_feed(
feed: Feed, zf: zipfile.ZipFile, lines, by_line, kinds, groups, track
feed: Feed, zf: zipfile.ZipFile, lines, by_line, kinds, groups
) -> tuple[list[Calibration], dict[tuple[int, int, int], list[tuple[float, str]]]]:
stops_by_id = {
s["stop_id"]: (norm_name(s["stop_name"]), float(s["stop_lat"]), float(s["stop_lon"]))
Expand Down Expand Up @@ -790,7 +760,8 @@ def calibrate_feed(
alpha = line_detour(lines, by_line, line_cd)
seq: list[tuple[float, bool]] = [(0.0, True)]
for a, b in zip(span, span[1:]):
seq.append((pair_distance(track, a, b, alpha), b["cd"] in served))
d = haversine(a["lat"], a["lon"], b["lat"], b["lon"]) * alpha
seq.append((d, b["cd"] in served))
obs = arr_last - dep0

dedup_key = (tuple(s["cd"] for s in span), tuple(sorted(served)), round(obs, 1))
Expand Down Expand Up @@ -1018,7 +989,6 @@ def main() -> int:

token = os.environ.get("ODPT_ACCESS_TOKEN")
lines, by_line, kinds, groups = load_repo_data()
track = load_track_distances()

all_results: list[Calibration] = []
all_seg_samples: dict[tuple[int, int, int], list[tuple[float, str]]] = {}
Expand All @@ -1035,7 +1005,7 @@ def main() -> int:
if zf is None:
continue
with zf:
results, seg_samples = calibrate_feed(feed, zf, lines, by_line, kinds, groups, track)
results, seg_samples = calibrate_feed(feed, zf, lines, by_line, kinds, groups)
all_results.extend(results)
for key, ts in seg_samples.items():
all_seg_samples.setdefault(key, []).extend(ts)
Expand Down Expand Up @@ -1073,10 +1043,9 @@ def line_baseline(line_cd: int) -> float:
return general_rule_speed(lines[line_cd]["line_type"], 0)

name_of = {s["cd"]: s["name"] for sts in by_line.values() for s in sts}
station_of = {s["cd"]: s for sts in by_line.values() for s in sts}
coord_of = {s["cd"]: (s["lat"], s["lon"]) for sts in by_line.values() for s in sts}

# ペアごとの平均ターゲットとみなし距離。距離は線路の長さ (無ければ路線較正済みの
# 迂回係数で見積もった直線距離) を使う。
# ペアごとの平均ターゲットとみなし距離。距離には路線較正済みの迂回係数を使う。
mean_targets: dict[tuple[int, int, int], float] = {}
pair_dist_m: dict[tuple[int, int, int], float] = {}
# 出発間隔ベースでしか較正できなかったペア(繰り越し補正の対象)。
Expand All @@ -1087,6 +1056,7 @@ def line_baseline(line_cd: int) -> float:
continue
if line_cd not in detour_cache:
detour_cache[line_cd] = line_detour(lines, by_line, line_cd)
(lat1, lon1), (lat2, lon2) = coord_of[cd_lo], coord_of[cd_hi]
key = (line_cd, cd_lo, cd_hi)
# 到着時刻ベース(純走行時間)が十分あればそれだけを使う。無いフィード
# (東京メトロは全駅 arr==dep)では出発間隔ベースの上側トリム平均で
Expand All @@ -1100,9 +1070,7 @@ def line_baseline(line_cd: int) -> float:
used = dep_ts[:keep]
dep_only_keys.add(key)
mean_targets[key] = sum(used) / len(used)
pair_dist_m[key] = pair_distance(
track, station_of[cd_lo], station_of[cd_hi], detour_cache[line_cd]
)
pair_dist_m[key] = haversine(lat1, lon1, lat2, lon2) * detour_cache[line_cd]

raw_targets = dict(mean_targets)
rebalance_line_targets(mean_targets, pair_dist_m, by_line, lines, dep_only_keys)
Expand Down
3 changes: 1 addition & 2 deletions src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,9 @@ fn route_network() -> &'static Arc<RouteNetwork> {
}

fn build_route_network() -> RouteNetwork {
RouteNetwork::build_with_track(
RouteNetwork::build(
rail_line_group_cds().map(|group| stations_of_line_groups(&[group as u32])),
&EstimationParams::default(),
index::track_distance,
)
}

Expand Down
1 change: 0 additions & 1 deletion stationapi/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ pub mod arrival_estimation;
pub mod entity;
pub mod error;
pub mod ipa;
pub mod legacy_speed_table;
pub mod normalize;
pub mod repository;
pub mod romaji;
Expand Down
119 changes: 41 additions & 78 deletions stationapi/src/domain/arrival_estimation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,14 +538,6 @@ pub fn estimate_arrival_minutes(
estimate_arrival_minutes_calibrated(stops, stops, params)
}

/// 隣り合う 2 駅の線路の長さ (m)。キーは `(小さい station_cd, 大きい station_cd)`。
pub type TrackDistances = HashMap<(i32, i32), f64>;

/// [`TrackDistances`] のキー。向きを問わずに引けるよう小さい駅を先にする。
pub fn track_distance_key(a: i32, b: i32) -> (i32, i32) {
(a.min(b), a.max(b))
}

/// [`estimate_arrival_minutes`] の較正母数指定版。
///
/// `calibration_stops` には経路全体(部分区間へ切り出す前)の駅列を渡す。
Expand All @@ -557,19 +549,6 @@ pub fn estimate_arrival_minutes_calibrated(
stops: &[&Station],
calibration_stops: &[&Station],
params: &EstimationParams,
) -> Vec<EstimatedStop> {
estimate_arrival_minutes_with_track(stops, calibration_stops, params, None)
}

/// [`estimate_arrival_minutes_calibrated`] に、駅間の線路の長さを渡せる版。
///
/// `track` に隣り合う 2 駅の線路の長さがあれば、その駅間の走行距離は直線距離 ×
/// 迂回係数ではなく線路の長さを使う。無い駅間は従来どおり迂回係数で見積もる。
pub fn estimate_arrival_minutes_with_track(
stops: &[&Station],
calibration_stops: &[&Station],
params: &EstimationParams,
track: Option<&TrackDistances>,
) -> Vec<EstimatedStop> {
let n = stops.len();
if n == 0 {
Expand Down Expand Up @@ -697,16 +676,7 @@ pub fn estimate_arrival_minutes_with_track(
let mut seg: Vec<(f64, f64, usize, bool)> = Vec::new();

for i in 1..n {
let track_m = track
.and_then(|t| {
t.get(&track_distance_key(
stops[i - 1].station_cd,
stops[i].station_cd,
))
})
.copied()
.filter(|d| *d > 0.0)
.unwrap_or_else(|| straight_km[i] * detour_of(stops[i]) * 1000.0);
let track_m = straight_km[i] * detour_of(stops[i]) * 1000.0;
let v_kmh = speed_kmh_at(i);

let idx = result.len();
Expand Down Expand Up @@ -1169,6 +1139,38 @@ mod tests {
}
}

#[test]
fn ginza_line_shibuya_to_shimbashi_matches_real_travel_time() {
let p = EstimationParams::default();
// 東京メトロ銀座線 渋谷→新橋。実座標・実データ値
// (average_distance = 780.46154m、地下鉄 line_type=3)。
// 実乗車時間は東京メトロ/乗換案内系の標準所要時間で約13分。
let data = [
(2800119, 35.659066, 139.701000), // 渋谷
(2800118, 35.665247, 139.712314), // 表参道
(2800117, 35.670527, 139.717857), // 外苑前
(2800116, 35.672765, 139.724159), // 青山一丁目
(2800115, 35.677021, 139.737047), // 赤坂見附
(2800114, 35.673621, 139.741419), // 溜池山王
(2800113, 35.670236, 139.749832), // 虎ノ門
(2800112, 35.667434, 139.758432), // 新橋
];
let stations: Vec<Station> = data
.iter()
.map(|&(cd, lat, lon)| {
let mut s = station(cd, 28001, lat, lon, Some(780.46154));
s.line_type = Some(LINE_TYPE_SUBWAY);
s.line_group_cd = Some(28001);
s
})
.collect();
let refs: Vec<&Station> = stations.iter().collect();
let est = estimate_arrival_minutes(&refs, &p);

let total = est.last().unwrap().cumulative_minutes;
assert!((12.0..14.0).contains(&total), "got {total}");
}

/// GTFS インポート後のバス停を再現する(line_type=3 は GTFS route_type のバス、
/// kind=7 は TrainTypeKind::BusRoute、average_distance は無し)。
fn bus_station(station_cd: i32, line_cd: i32, lat: f64, lon: f64) -> Station {
Expand Down Expand Up @@ -1454,8 +1456,8 @@ mod tests {

#[test]
fn speed_table_override_beats_general_rule() {
// 阪急神戸本線(34001)の特急(LimitedExpress)は較正テーブルの 110km/h が
// 適用され、一般則(80×1.2=96km/h)の路線より同一区間を速く走る。
// 京急本線(27001)の快特(Express)は較正テーブルの 120km/h が適用され、
// 一般則(80×1.15=92km/h)の路線より同一区間を速く走る。
let p = EstimationParams::default();
let time_on_line = |line_cd: i32| -> f64 {
// 8km 区間(巡航支配)で比較する。0.072 度 ≈ 8km。
Expand All @@ -1470,21 +1472,21 @@ mod tests {
];
stations[2].pass = Some(1);
for s in stations.iter_mut() {
s.kind = Some(TrainTypeKind::LimitedExpress as i32);
s.kind = Some(TrainTypeKind::Express as i32);
}
let refs: Vec<&Station> = stations.iter().collect();
estimate_arrival_minutes(&refs, &p)[1].cumulative_minutes
};
let hankyu = time_on_line(34001);
let keikyu = time_on_line(27001);
let generic = time_on_line(100);
assert!(
hankyu < generic,
"hankyu {hankyu} should be < generic {generic}"
keikyu < generic,
"keikyu {keikyu} should be < generic {generic}"
);
// テーブル値 110km/h での運動学モデルと厳密に一致する。
// テーブル値 120km/h での運動学モデルと厳密に一致する。
let straight_m = haversine_distance(35.000, 139.0, 35.072, 139.0);
// average_distance 無し → 在来線フォールバック α=1.30。
approx(hankyu, segment_run_minutes(straight_m * 1.30, 110.0, &p));
approx(keikyu, segment_run_minutes(straight_m * 1.30, 120.0, &p));
}

#[test]
Expand Down Expand Up @@ -1671,43 +1673,4 @@ mod tests {
segment_run_minutes(track_m, est[1].max_speed_kmh, &p),
);
}

#[test]
fn track_distance_replaces_the_detour_estimate_where_known() {
let p = EstimationParams::default();
let stations = three_collinear_stations();
let refs: Vec<&Station> = stations.iter().collect();
let without = estimate_arrival_minutes(&refs, &p);

// 1→2 だけ線路の長さ (直線の 2 倍) を渡す。2→3 は迂回係数のまま
let straight_m = haversine_distance(35.000, 139.0, 35.016, 139.0);
let mut track = TrackDistances::new();
track.insert(track_distance_key(2, 1), straight_m * 2.0);
let with = estimate_arrival_minutes_with_track(&refs, &refs, &p, Some(&track));

approx(
with[1].cumulative_minutes,
segment_run_minutes(straight_m * 2.0, with[1].max_speed_kmh, &p),
);
// 線路の長さが無い駅間の走行時間は変わらない
approx(
with[2].cumulative_minutes - with[1].departure_cumulative_minutes,
without[2].cumulative_minutes - without[1].departure_cumulative_minutes,
);
// 表を渡さない (None) のは従来どおり
let none = estimate_arrival_minutes_with_track(&refs, &refs, &p, None);
approx(none[2].cumulative_minutes, without[2].cumulative_minutes);
}

#[test]
fn non_positive_track_distance_falls_back_to_the_detour_estimate() {
let p = EstimationParams::default();
let stations = three_collinear_stations();
let refs: Vec<&Station> = stations.iter().collect();
let without = estimate_arrival_minutes(&refs, &p);
let mut track = TrackDistances::new();
track.insert(track_distance_key(1, 2), 0.0);
let with = estimate_arrival_minutes_with_track(&refs, &refs, &p, Some(&track));
approx(with[1].cumulative_minutes, without[1].cumulative_minutes);
}
}
Loading
Loading