Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core, editoast, python: stop train on next signal instead of OP #10200

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class PathfindingBlockRequest(
val rollingStockSupportedSignalingSystems: List<String>,
@Json(name = "rolling_stock_maximum_speed") val rollingStockMaximumSpeed: Double,
@Json(name = "rolling_stock_length") val rollingStockLength: Double,
@Json(name = "stop_at_next_signal") val stopAtNextSignal: Boolean,
val timeout: Double?,
val infra: String,
@Json(name = "expected_version") val expectedVersion: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,25 @@ fun runPathfinding(
): PathfindingBlockResponse {
// Parse the waypoints
val waypoints = ArrayList<Collection<PathfindingEdgeLocationId<Block>>>()
for (step in request.pathItems) {
request.pathItems.forEachIndexed { stepIndex, step ->
val allStarts = HashSet<PathfindingEdgeLocationId<Block>>()
for (direction in Direction.entries) {
for (waypoint in step) allStarts.addAll(findWaypointBlocks(infra, waypoint, direction))
for (waypoint in step) {
val waypointBlocks = findWaypointBlocks(infra, waypoint, direction)
if (request.stopAtNextSignal && stepIndex != 0) {
allStarts.addAll(
waypointBlocks.map {
findNextSignalBlockOnWaypointBlock(
it,
infra,
request.rollingStockLength
)
}
)
} else {
allStarts.addAll(waypointBlocks)
}
}
}
waypoints.add(allStarts)
}
Expand Down Expand Up @@ -369,3 +384,37 @@ private fun getBlockOffset(
String.format("getBlockOffset: Track chunk %s not in block %s", trackChunkId, blockId)
)
}

fun findNextSignalBlockOnWaypointBlock(
waypointBlock: PathfindingEdgeLocationId<Block>,
infra: FullInfra,
rollingStockLength: Double
): PathfindingEdgeLocationId<Block> {
val nextSignalOffset =
getNextSignalOffset(waypointBlock.edge, waypointBlock.offset, infra, rollingStockLength)
return PathfindingEdgeLocationId(waypointBlock.edge, nextSignalOffset)
}

private fun getNextSignalOffset(
blockId: BlockId,
blockOffset: Offset<Block>,
infra: FullInfra,
rollingStockLength: Double
): Offset<Block> {
val signalsPositions = infra.blockInfra.getSignalsPositions(blockId)
val blockLength = infra.blockInfra.getBlockLength(blockId).distance
val nextSignalPosition = signalsPositions.firstOrNull { it.distance >= blockOffset.distance }

// some blocks are < 1m long (even 0m), we can't get further in the block
val maxHeadOffset =
if (blockOffset.distance < 1.meters) {
blockOffset.distance
} else {
(nextSignalPosition?.distance ?: blockLength) - 1.meters
}

val minTailOffset = blockOffset.distance + rollingStockLength.meters
val finalOffset = if (minTailOffset <= maxHeadOffset) minTailOffset else maxHeadOffset

return Offset(finalOffset)
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,5 @@ class SimulationPowerRestrictionItem(
)

class TrainScheduleOptions(
@Json(name = "use_electrical_profiles") val useElectricalProfiles: Boolean
@Json(name = "use_electrical_profiles") val useElectricalProfiles: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fr.sncf.osrd.api.ExceptionHandler
import fr.sncf.osrd.api.FullInfra
import fr.sncf.osrd.api.InfraManager
import fr.sncf.osrd.api.api_v2.*
import fr.sncf.osrd.api.api_v2.pathfinding.findNextSignalBlockOnWaypointBlock
import fr.sncf.osrd.api.api_v2.pathfinding.findWaypointBlocks
import fr.sncf.osrd.api.api_v2.pathfinding.runPathfindingBlockPostProcessing
import fr.sncf.osrd.api.api_v2.standalone_sim.*
Expand Down Expand Up @@ -98,7 +99,13 @@ class STDCMEndpointV2(private val infraManager: InfraManager) : Take {
convertWorkScheduleCollection(infra.rawInfra, request.workSchedules)
trainsRequirements.add(convertedWorkSchedules)
val spacingRequirements = trainsRequirements.flatMap { it.spacingRequirements }
val steps = parseSteps(infra, request.pathItems, request.startTime)
val steps =
parseSteps(
infra,
request.pathItems,
request.startTime,
request.rollingStock.length.distance.meters
)

// Run the STDCM pathfinding
val path =
Expand Down Expand Up @@ -282,7 +289,8 @@ fun buildTemporarySpeedLimitManager(
private fun parseSteps(
infra: FullInfra,
pathItems: List<STDCMPathItem>,
startTime: ZonedDateTime
startTime: ZonedDateTime,
rollingStockLength: Double
): List<STDCMStep> {
if (pathItems.last().stopDuration == null) {
throw OSRDError(ErrorType.MissingLastSTDCMStop)
Expand All @@ -297,9 +305,15 @@ private fun parseSteps(
pathItems.first().stopDuration = null

return pathItems
.map {
.mapIndexed { index, it ->
STDCMStep(
findWaypointBlocks(infra, it.locations),
if (index != 0) {
findWaypointBlocks(infra, it.locations).map { waypointBlock ->
findNextSignalBlockOnWaypointBlock(waypointBlock, infra, rollingStockLength)
}
} else {
findWaypointBlocks(infra, it.locations)
},
it.stopDuration?.seconds,
it.stopDuration != null,
if (it.stepTimingData != null)
Expand Down Expand Up @@ -362,7 +376,7 @@ private fun checkForConflicts(

private fun findWaypointBlocks(
infra: FullInfra,
waypoints: Collection<TrackLocation>
waypoints: Collection<TrackLocation>,
): Set<PathfindingEdgeLocationId<Block>> {
val waypointBlocks = HashSet<PathfindingEdgeLocationId<Block>>()
for (waypoint in waypoints) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class STDCMRequestV2(
@Json(name = "rolling_stock_supported_signaling_systems")
val rollingStockSupportedSignalingSystems: List<String>,
@Json(name = "trains_requirements") val trainsRequirements: Map<Long, TrainRequirementsRequest>,
@Json(name = "stop_at_next_signal") val stopAtNextSignal: Boolean,

// Simulation inputs
val comfort: Comfort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@ pub struct TrainScheduleOptions {
#[derivative(Default(value = "true"))]
#[serde(default = "default_use_electrical_profiles")]
use_electrical_profiles: bool,

#[derivative(Default(value = "true"))]
#[serde(default = "default_stop_at_next_signal")]
stop_at_next_signal: bool,
}

fn default_use_electrical_profiles() -> bool {
true
}

fn default_stop_at_next_signal() -> bool {
true
}

impl TrainScheduleOptions {
pub fn stops_at_next_signal(&self) -> bool {
self.stop_at_next_signal
}
}
8 changes: 8 additions & 0 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8129,6 +8129,7 @@ components:
- path_items
- rolling_stock_maximum_speed
- rolling_stock_length
- stop_at_next_signal
properties:
path_items:
type: array
Expand Down Expand Up @@ -8160,6 +8161,9 @@ components:
items:
type: string
description: List of supported signaling systems
stop_at_next_signal:
type: boolean
description: Stops the train at next signal instead of on path item
PathfindingInputError:
oneOf:
- type: object
Expand Down Expand Up @@ -11346,6 +11350,8 @@ components:
options:
type: object
properties:
stop_at_next_signal:
type: boolean
use_electrical_profiles:
type: boolean
additionalProperties: false
Expand Down Expand Up @@ -11441,6 +11447,8 @@ components:
TrainScheduleOptions:
type: object
properties:
stop_at_next_signal:
type: boolean
use_electrical_profiles:
type: boolean
additionalProperties: false
Expand Down
2 changes: 2 additions & 0 deletions editoast/src/core/pathfinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub struct PathfindingRequest {
pub rolling_stock_maximum_speed: f64,
/// Rolling stock length in meters:
pub rolling_stock_length: f64,
/// If the train should stop on the next signal instead of on the operational point
pub stop_at_next_signal: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
Expand Down
4 changes: 4 additions & 0 deletions editoast/src/views/path/pathfinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ struct PathfindingInput {
/// Rolling stock length
#[schema(value_type = f64)]
rolling_stock_length: OrderedFloat<f64>,
/// Stops the train at next signal instead of on path item
stop_at_next_signal: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)]
Expand Down Expand Up @@ -325,6 +327,7 @@ fn build_pathfinding_request(
.clone(),
rolling_stock_maximum_speed: pathfinding_input.rolling_stock_maximum_speed.0,
rolling_stock_length: pathfinding_input.rolling_stock_length.0,
stop_at_next_signal: pathfinding_input.stop_at_next_signal,
})
}

Expand Down Expand Up @@ -398,6 +401,7 @@ pub async fn pathfinding_from_train_batch(
.into_iter()
.map(|item| item.location)
.collect(),
stop_at_next_signal: train_schedule.options.stops_at_next_signal(),
};
to_compute.push(path_input);
to_compute_index.push(index);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const useSetupItineraryForTrainUpdate = (trainIdToEdit: number) => {
rolling_stock_supported_signaling_systems: rollingStock.supported_signaling_systems,
rolling_stock_maximum_speed: rollingStock.max_speed,
rolling_stock_length: rollingStock.length,
stop_at_next_signal: true, // TODO: change to false and set to true elsewhere if needed
},
};
const pathfindingResult = await postPathfindingBlocks(params).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions front/src/common/api/generatedEditoastApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2737,6 +2737,8 @@ export type PathfindingInput = {
rolling_stock_supported_electrifications: string[];
/** List of supported signaling systems */
rolling_stock_supported_signaling_systems: string[];
/** Stops the train at next signal instead of on path item */
stop_at_next_signal: boolean;
};
export type RoutePath = {
switches_directions: (string & string)[][];
Expand Down Expand Up @@ -3193,6 +3195,7 @@ export type Margins = {
values: string[];
};
export type TrainScheduleOptions = {
stop_at_next_signal?: boolean;
use_electrical_profiles?: boolean;
};
export type PathItem = PathItemLocation & {
Expand Down Expand Up @@ -3438,6 +3441,7 @@ export type TrainScheduleBase = {
values: string[];
};
options?: {
stop_at_next_signal?: boolean;
use_electrical_profiles?: boolean;
};
path: (PathItemLocation & {
Expand Down
1 change: 1 addition & 0 deletions front/src/modules/pathfinding/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const getPathfindingQuery = ({
rolling_stock_supported_signaling_systems: rollingStock.supported_signaling_systems,
rolling_stock_maximum_speed: rollingStock.max_speed,
rolling_stock_length: rollingStock.length,
stop_at_next_signal: true, // TODO: change to false and set to true elsewhere if needed
},
};
}
Expand Down
9 changes: 8 additions & 1 deletion python/osrd_schemas/osrd_schemas/train_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,19 @@ class PowerRestrictionRanges(RootModel):


class TrainScheduleOptions(BaseModel):
"""Optional arguments for the standalone simulation."""
"""Optional arguments :
- `ignore_electrical_profiles` : for the standalone simulation
- `stop_at_next_signal` : for dealing with stopped trains that overflow on switches during imports
"""

ignore_electrical_profiles: bool = Field(
default=False,
description="If true, the electrical profiles are ignored in the standalone simulation",
)
stop_at_next_signal: bool = Field(
default=False,
description="If true, the train will stop at the next signal instead of at the operational point",
)


if __name__ == "__main__":
Expand Down
Loading