Skip to content
Merged
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
6 changes: 3 additions & 3 deletions AGENTS.md

Large diffs are not rendered by default.

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

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

Expand All @@ -515,7 +515,7 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int!

### 所要時間のベンチマーク (`travel_times/`)

`trainRoute` の `Estimated` の所要時間を、実際の列車の所要時間と比べる基準を
到着時間推定 (`estimateArrivalTimes` と `trainRoute` の `Estimated`) の所要時間を、実際の列車の所要時間と比べる基準を
`travel_times/cases.csv` に置いています。速度の較正テーブルや一般則は、1 つの路線に合わせて変えると、同じ
規則を使うほかの路線の推定も変わります。変更の前後で全体の誤差を測るための仕組み
です。
Expand Down Expand Up @@ -767,7 +767,7 @@ repository の実装がないメソッドは、空の結果ではなく `DomainE
│ │ ├── entity/ # Station / Line / TrainType / Company ...
│ │ ├── repository/ # 抽象インターフェース
│ │ ├── arrival_estimation.rs
│ │ ├── legacy_speed_table.rs # 元の較正 (Estimated 以外のすべてが使う)
│ │ ├── legacy_speed_table.rs # 元の較正 (connectedRoutes と trainRoute の Legacy が使う)
│ │ ├── route_search.rs # 乗換経路探索 (RAPTOR)
│ │ ├── route_topology.rs # 所要時間を持たない系統網 (stationsByName の到達判定)
│ │ ├── segment_speed_table.rs
Expand Down
6 changes: 3 additions & 3 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ OSM データは [Open Database License (ODbL)](https://www.openstreetmap.org/co
## compute_speed_table.py

公開 GTFS 時刻表から、到着時間推定(`arrival_estimation.rs`)の速度較正テーブル
2 種類を再計算します。このテーブルを使うのは `trainRoute` の `Estimated` だけです
(`estimateArrivalTimes` と `connectedRoutes` は `legacy_speed_table.rs` の元の較正を
使います)。運動学モデルを Python で再現し、実ダイヤの所要時間を
2 種類を再計算します。このテーブルを使うのは `estimateArrivalTimes` と `trainRoute` の
`Estimated` です (`connectedRoutes` と `trainRoute` の `Legacy` は `legacy_speed_table.rs`
の元の較正を使います)。運動学モデルを Python で再現し、実ダイヤの所要時間を
再現する実効最高速度を二分探索でフィッティングします。

駅間の距離は、推定と同じく線路の長さ(`generated/connections.csv`)を使い、無い
Expand Down
4 changes: 2 additions & 2 deletions scripts/travel_time_report.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""実際の所要時間 (travel_times/cases.csv) に対する trainRoute の Estimated (MobileApp の
GPX の生成が使う推定) の誤差を、動いている Worker に問い合わせて Markdown で出す。
"""実際の所要時間 (travel_times/cases.csv) に対する 到着時間推定 (trainRoute の
Estimated。estimateArrivalTimes と同じ値) の誤差を、動いている Worker に問い合わせて Markdown で出す。

CI の回帰テスト (src/travel_times.rs) は data/*.csv だけで動くので、生成データにしか
無い種別グループを飛ばし、線路の長さも持たない。本番と同じ生成データでの精度は、
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ pub enum ConnectedRouteSort {
// おすすめ順 (評価値 = 最初の列車の待ち時間を含む所要時間の見込み、に乗換 1 回あたり
// 5 分を足した値の小さい順)。既定
Recommended,
// 到着の早い順 (estimateArrivalTimes の見込みと同じ所要時間)。同じなら乗換の少ない順
// 到着の早い順 (探索の所要時間。元の較正で見積もるので estimateArrivalTimes の
// 見込みとは一致しない)。同じなら乗換の少ない順
ArrivalTime,
// 乗換の少ない順。同じなら到着の早い順
TransferCount,
Expand Down
56 changes: 25 additions & 31 deletions src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,16 +1322,11 @@ mod tests {
.flat_map(|leg| leg.station_cds.iter().copied())
.collect();
assert_eq!(eta_ids, journey_ids);
// 累積は減らず、最後は探索の所要時間と (ほぼ) 一致する
// 累積は減らない。見込みは求め直した較正と線路の長さで出すので、元の較正で
// 見積もる探索の所要時間 (journey.total_seconds) とは一致しない
assert!(eta.windows(2).all(|pair| {
pair[1].cumulative_minutes >= pair[0].departure_cumulative_minutes - 1e-9
}));
let last = eta.last().unwrap().cumulative_minutes;
let expected = f64::from(journey.total_seconds) / 60.0;
assert!(
(last - expected).abs() < 1.0,
"eta {last} vs search {expected}"
);
// 乗換では徒歩の後に乗換先の列車を待ち、走行区間は距離 0 から始まる。
// 乗車駅の行は、前の区間の降車駅の行のすぐ後
let mut board = 0;
Expand Down Expand Up @@ -1666,11 +1661,9 @@ mod tests {
assert_eq!(route_ids, ids);
}

/// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ駅を
/// 同じ組み立て方 (乗換の徒歩と待ち時間) でつないだ見込みを返し、停車・加減速も
/// 推定のモデルにそろう。値は Estimated だけが求め直した較正と線路の長さを使う
/// ので、estimateArrivalTimes とは一致しない。Legacy は見込みを返さず、区間の値も
/// 変わらない。
/// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ
/// 見込みを返し、停車・加減速も推定のモデルにそろう。Legacy は見込みを返さず、
/// 区間の値も変わらない。
#[test]
fn estimated_connected_train_route_carries_the_estimate_arrival_times() {
use stationapi::use_case::traits::query::QueryUseCase;
Expand All @@ -1690,17 +1683,17 @@ mod tests {
.unwrap();

assert_eq!(estimated.len(), eta.len());
let mut previous_departure = 0.0;
for (i, (segment, stop)) in estimated.iter().zip(&eta).enumerate() {
assert_eq!(
segment.station.as_ref().map(|station| station.id as i32),
Some(stop.station_cd)
segment.arrival_cumulative_minutes,
Some(stop.cumulative_minutes)
);
assert_eq!(
segment.departure_cumulative_minutes,
Some(stop.departure_cumulative_minutes)
);
let arrival = segment.arrival_cumulative_minutes.unwrap();
let departure = segment.departure_cumulative_minutes.unwrap();
assert!(arrival >= previous_departure - 1e-9 && departure >= arrival);
previous_departure = departure;
assert_eq!(segment.stops, stop.stops_here);
assert_eq!(segment.max_speed, stop.max_speed_kmh / 3.6);
assert_eq!(segment.max_acceleration, params.accel);
assert_eq!(segment.max_deceleration, params.decel);
// 駅と距離は Legacy と同じ
Expand All @@ -1719,7 +1712,7 @@ mod tests {
}

/// lineGroupId で呼んだ Estimated の trainRoute は、その区間を 1 つの leg にした
/// Estimated の trainRoute と同じ見込みになる (環状線でない系統では同じ駅列を
/// estimateArrivalTimes と同じ見込みになる (環状線でない系統では同じ駅列を
/// 同じ較正母数で推定するため)。#1709 の 2 経路で確かめる。
#[test]
fn estimated_train_route_matches_a_single_leg_estimate() {
Expand All @@ -1734,26 +1727,27 @@ mod tests {
TrainRouteModel::Estimated,
))
.unwrap();
let single_leg = block_on(interactor.get_connected_train_route(
&[model::RouteLegRequest {
let eta = block_on(interactor.estimate_connected_route_arrival_times(&[
model::RouteLegRequest {
line_group_id,
from_station_id: from,
to_station_id: to,
}],
TrainRouteModel::Estimated,
))
},
]))
.unwrap();
assert_eq!(segments.len(), single_leg.len());
for (segment, leg_segment) in segments.iter().zip(&single_leg) {
assert_eq!(segment.station, leg_segment.station);
assert!(segment.arrival_cumulative_minutes.is_some());
assert_eq!(segments.len(), eta.len());
for (segment, stop) in segments.iter().zip(&eta) {
assert_eq!(
segment.station.as_ref().map(|station| station.id as i32),
Some(stop.station_cd)
);
assert_eq!(
segment.arrival_cumulative_minutes,
leg_segment.arrival_cumulative_minutes
Some(stop.cumulative_minutes)
);
assert_eq!(
segment.departure_cumulative_minutes,
leg_segment.departure_cumulative_minutes
Some(stop.departure_cumulative_minutes)
);
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/travel_times.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! 実際の所要時間 (`travel_times/cases.csv`) に対する `trainRoute` の `Estimated`
//! (MobileApp の GPX の生成が使う推定) の回帰の見張り。`estimateArrivalTimes` と
//! `connectedRoutes` は元の較正のままなので、ここでは測らない。
//! 実際の所要時間 (`travel_times/cases.csv`) に対する到着時間推定 (`trainRoute` の
//! `Estimated`。`estimateArrivalTimes` と同じ値) の回帰の見張り。`connectedRoutes` の
//! 所要時間は元の較正のままなので、ここでは測らない。
//!
//! 基準ごとに推定の所要時間を出し、実際の典型的な所要時間 (平日日中の中央値) から
//! のずれを求める。記録した推定 (`travel_times/baseline.csv`) より悪くなった基準が
Expand Down Expand Up @@ -156,8 +156,8 @@ fn estimate(case: &Case) -> Option<f64> {
from_station_id: case.from_station_id,
to_station_id: case.slice_end_station_id,
}];
// 見張るのは trainRoute の Estimated (GPX の生成が使う推定)。estimateArrivalTimes と
// connectedRoutes は元の較正のままで、推定の規則や較正を変えても動かない
// 見張るのは trainRoute の Estimated (estimateArrivalTimes と同じ値)。connectedRoutes の
// 所要時間は元の較正のままで、推定の規則や較正を変えても動かない
let segments =
block_on(crate::interactor().get_connected_train_route(&legs, TrainRouteModel::Estimated))
.unwrap_or_else(|e| panic!("{}: 推定できない: {e}", case.label));
Expand Down
8 changes: 4 additions & 4 deletions stationapi/src/domain/arrival_estimation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ pub struct EstimatedStop {
/// 速度の較正にどの表を使うか。
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpeedCalibration {
/// 元の較正 (`legacy_speed_table`。#1712 で求め直す前の表)。`estimateArrivalTimes`、
/// `connectedRoutes` など、`trainRoute` の `Estimated` 以外はすべてこれを使う。
/// 元の較正 (`legacy_speed_table`。#1712 で求め直す前の表)。`connectedRoutes` の
/// 所要時間が使う。
#[default]
Original,
/// 駅間の距離に線路の長さを使う前提で求め直した較正 (`speed_table` /
/// `segment_speed_table`)。`trainRoute` の `Estimated` (MobileApp の GPX の生成)
/// だけが使う。
/// `segment_speed_table`)。到着見込み (`estimateArrivalTimes` と `trainRoute` の
/// `Estimated`) が使う。
Recalibrated,
}

Expand Down
11 changes: 5 additions & 6 deletions stationapi/src/domain/legacy_speed_table.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
//! 元の速度の較正テーブル。`trainRoute` の `Estimated` 以外のすべて
//! (`estimateArrivalTimes`、`connectedRoutes`、`trainRoute` の `Legacy` = MobileApp の
//! オートモード) が使う。
//! 元の速度の較正テーブル。`connectedRoutes` の所要時間と `trainRoute` の `Legacy`
//! が使う。
//!
//! `speed_table` / `segment_speed_table` は、駅間の距離に線路の長さを使う前提で
//! 求め直した (#1712)。求め直した較正は `trainRoute` の `Estimated` (MobileApp の GPX の
//! 生成) だけが使い、ほかの推定の値が変わらないよう、求め直す前の表
//! 求め直した (#1712)。求め直した較正は到着見込み (`estimateArrivalTimes` と
//! `trainRoute` の `Estimated`) が使う。経路検索の結果が変わらないよう、求め直す前の表
//! (TrainLCD/StationAPI の `dev`、#1711 の時点) をここに凍結して持つ。
//! `scripts/compute_speed_table.py` はこのファイルを書き換えない。値を変えると
//! ETA・経路検索・オートモードが変わるので、変えないこと。
//! 経路検索の結果が変わるので、変えないこと。

use crate::domain::segment_speed_table::segment_override_applies_to_kind;
use crate::model::TrainTypeKind;
Expand Down
5 changes: 3 additions & 2 deletions stationapi/src/domain/route_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ pub enum JourneySort {
/// [`RouteNetwork::search`] が返す順そのもの。
#[default]
Recommended,
/// 到着の早い順。[`Journey::total_seconds`] (`estimateArrivalTimes` の見込みと
/// 同じく、最初の列車の待ち時間を含まない) の小さい順で、同じなら乗換の少ない順。
/// 到着の早い順。[`Journey::total_seconds`] (最初の列車の待ち時間を含まない) の
/// 小さい順で、同じなら乗換の少ない順。所要時間は元の較正で見積もるので、
/// `estimateArrivalTimes` の見込みとは一致しない。
ArrivalTime,
/// 乗換の少ない順。同じなら到着の早い順。
TransferCount,
Expand Down
5 changes: 3 additions & 2 deletions stationapi/src/domain/segment_speed_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
//! この残差を原理的に解消できないため、公開 GTFS 時刻表から隣接駅ペア単位で
//! フィットした実効速度をここで上書きする。
//!
//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが
//! 参照する。ほかの推定は、求め直す前の表 (`legacy_speed_table`) を使う。
//! `estimateArrivalTimes` と `trainRoute` の `Estimated` の推定
//! (`SpeedCalibration::Recalibrated`) が参照する。`connectedRoutes` は、求め直す前の表
//! (`legacy_speed_table`) を使う。
//!
//! 生成は `scripts/compute_speed_table.py --apply`(路線単位テーブルと同時生成)。
//! 駅間の純走行時間(次駅の到着時刻が別記録されていれば「出発 → 次駅到着」、
Expand Down
10 changes: 5 additions & 5 deletions stationapi/src/domain/speed_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
//!
//! 値の意味は「その路線・種別での実効巡航速度(km/h)」。理論上の車両性能では
//! なく、時刻表所要時間を運動学モデルで再現する値として較正している。
//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが
//! 参照する。`estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、
//! 求め直す前の表 (`legacy_speed_table`) を使う。
//! `estimateArrivalTimes` と `trainRoute` の `Estimated` の推定
//! (`SpeedCalibration::Recalibrated`) が参照する。`connectedRoutes`・`trainRoute` の
//! `Legacy` は、求め直す前の表 (`legacy_speed_table`) を使う。
//!
//! エントリ追加の指針:
//! - 公表運転速度(例: 京急快特 120km/h、スカイライナー 160km/h)を起点にし、
Expand All @@ -33,8 +33,8 @@ use crate::model::TrainTypeKind;
/// 距離が直線 × 迂回係数だった頃の値は、距離の水増しを速度で打ち消していたので、
/// 線路の長さへ替えたときに求め直した。小田急線 (快速急行) は一般則で典型値に
/// 近づいたので外した。つくばエクスプレスと都営大江戸線は GTFS の自動較正に任せる。
/// `estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、求め直す前の
/// 値を `legacy_speed_table` で使い続ける。
/// `connectedRoutes`・`trainRoute` の `Legacy` は、求め直す前の値を
/// `legacy_speed_table` で使い続ける。
const LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[
// 総武快速線: 最高 130km/h の別線を走る。StationAPI の路線には快速の停車駅しか
// 無く、通過駅が無いので推定は各停 (Default) として扱う。一般則の 80km/h では
Expand Down
Loading
Loading