Primary author
+Tom Arne Pedersen +Tom.Arne.Pedersen@dnv.com
+Claas Rostock +claas.rostock@dnv.com
+Minos Hemrich +Minos.Hemrich@dnv.com
+diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..69934b5 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 3ef4fb1aa496aded3da200383724e853 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/_downloads/3f0d4d1fea704d8ffdd7fa89c1edb3ed/ICMASS23_verfying_caga_systems.pdf b/_downloads/3f0d4d1fea704d8ffdd7fa89c1edb3ed/ICMASS23_verfying_caga_systems.pdf new file mode 100644 index 0000000..547c468 Binary files /dev/null and b/_downloads/3f0d4d1fea704d8ffdd7fa89c1edb3ed/ICMASS23_verfying_caga_systems.pdf differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000..fb5be46 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,118 @@ + + +
+ + +
+"""Module with helper functions to determine if a generated path is crossing land."""
+
+from global_land_mask import globe
+from maritime_schema.types.caga import Position
+
+from trafficgen.utils import calculate_position_at_certain_time, rad_2_deg
+
+
+
+[docs]
+def path_crosses_land(
+ position_1: Position,
+ speed: float,
+ course: float,
+ lat_lon0: Position,
+ time_interval: float = 300.0,
+) -> bool:
+ """
+ Find if path is crossing land.
+
+ Params:
+ position_1: Ship position in latitude/longitude [rad].
+ speed: Ship speed [m/s].
+ course: Ship course [rad].
+ lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad].
+ time_interval: The time interval the vessel should travel without crossing land [sec]
+
+ Returns
+ -------
+ is_on_land: True if parts of the path crosses land.
+ """
+
+ num_checks = 10
+ for i in range(int(time_interval / num_checks)):
+ position_2 = calculate_position_at_certain_time(
+ Position(latitude=position_1.latitude, longitude=position_1.longitude),
+ lat_lon0,
+ speed,
+ course,
+ i * time_interval / num_checks,
+ )
+
+ lat = rad_2_deg(position_2.latitude)
+ lon = rad_2_deg(position_2.longitude)
+ if globe.is_land(lat, lon): # type: ignore (The package is unfortunately not typed.)
+ return True
+ return False
+
+
+"""
+Functions to generate encounters consisting of one own ship and one to many target ships.
+The generated encounters may be of type head-on, overtaking give-way and stand-on and
+crossing give-way and stand-on.
+"""
+
+import random
+from typing import List, Optional, Tuple, Union
+from uuid import uuid4
+
+import numpy as np
+from maritime_schema.types.caga import (
+ AISNavStatus,
+ Initial,
+ OwnShip,
+ Position,
+ ShipStatic,
+ TargetShip,
+ Waypoint,
+)
+
+from trafficgen.check_land_crossing import path_crosses_land
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+from trafficgen.types import (
+ EncounterRelativeSpeed,
+ EncounterSettings,
+ EncounterType,
+ SituationInput,
+)
+from trafficgen.utils import (
+ calculate_bearing_between_waypoints,
+ calculate_position_along_track_using_waypoints,
+ calculate_position_at_certain_time,
+ convert_angle_0_to_2_pi_to_minus_pi_to_pi,
+ convert_angle_minus_pi_to_pi_to_0_to_2_pi,
+)
+
+
+
+[docs]
+def generate_encounter(
+ desired_encounter_type: EncounterType,
+ own_ship: OwnShip,
+ target_ships_static: List[ShipStatic],
+ encounter_number: int,
+ beta_default: Optional[Union[List[float], float]],
+ relative_sog_default: Optional[float],
+ vector_time_default: Optional[float],
+ settings: EncounterSettings,
+) -> Tuple[TargetShip, bool]:
+ """
+ Generate an encounter.
+
+ Params:
+ * desired_encounter_type: Desired encounter to be generated
+ * own_ship: Dict, information about own ship that will encounter a target ship
+ * target_ships_static: List of target ships including static information that
+ may be used in an encounter
+ * encounter_number: Integer, used to naming the target ships. target_ship_1,2 etc.
+ * beta_default: User defined beta. If not set, this is None.
+ * relative_sog_default: User defined relative sog between own ship and
+ target ship. If not set, this is None.
+ * vector_time_default: User defined vector time. If not set, this is None.
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * target_ship: target ship information, such as initial position, sog and cog
+ * encounter_found: True=encounter found, False=encounter not found
+ """
+ encounter_found: bool = False
+ outer_counter: int = 0
+
+ # Initiating some variables which later will be set if an encounter is found
+ assert own_ship.initial is not None
+ target_ship_initial_position: Position = own_ship.initial.position
+ target_ship_sog: float = 0
+ target_ship_cog: float = 0
+
+ # Initial posision of own ship used as reference point for lat_lon0
+ lat_lon0: Position = Position(
+ latitude=own_ship.initial.position.latitude,
+ longitude=own_ship.initial.position.longitude,
+ )
+
+ target_ship_static: ShipStatic = decide_target_ship(target_ships_static)
+ assert target_ship_static is not None
+
+ # Searching for encounter. Two loops used. Only vector time is locked in the
+ # first loop. In the second loop, beta and sog are assigned.
+ while not encounter_found and outer_counter < 5:
+ outer_counter += 1
+ inner_counter: int = 0
+
+ # resetting vector_time, beta and relative_sog to default values before
+ # new search for situation is done
+ vector_time: Union[float, None] = vector_time_default
+
+ if vector_time is None:
+ vector_time = random.uniform(settings.vector_range[0], settings.vector_range[1])
+ if beta_default is None:
+ beta: float = assign_beta(desired_encounter_type, settings)
+ elif isinstance(beta_default, List):
+ beta: float = assign_beta_from_list(beta_default)
+ else:
+ beta: float = beta_default
+
+ # Own ship
+ assert own_ship.initial is not None
+ assert own_ship.waypoints is not None
+ # Assuming ship is pointing in the direction of wp1
+ own_ship_cog = calculate_bearing_between_waypoints(
+ own_ship.waypoints[0].position, own_ship.waypoints[1].position
+ )
+ own_ship_position_future = calculate_position_along_track_using_waypoints(
+ own_ship.waypoints,
+ own_ship.initial.sog,
+ vector_time,
+ )
+
+ # Target ship
+ target_ship_position_future: Position = assign_future_position_to_target_ship(
+ own_ship_position_future, lat_lon0, settings.max_meeting_distance
+ )
+
+ while not encounter_found and inner_counter < 5:
+ inner_counter += 1
+ relative_sog = relative_sog_default
+ if relative_sog is None:
+ min_target_ship_sog = (
+ calculate_min_vector_length_target_ship(
+ own_ship.initial.position,
+ own_ship_cog,
+ target_ship_position_future,
+ beta,
+ lat_lon0,
+ )
+ / vector_time
+ )
+
+ target_ship_sog: float = assign_sog_to_target_ship(
+ desired_encounter_type,
+ own_ship.initial.sog,
+ min_target_ship_sog,
+ settings.relative_speed,
+ )
+ else:
+ target_ship_sog: float = relative_sog * own_ship.initial.sog
+
+ assert target_ship_static.speed_max is not None
+ target_ship_sog = round(np.minimum(target_ship_sog, target_ship_static.speed_max), 1)
+
+ target_ship_vector_length = target_ship_sog * vector_time
+ start_position_target_ship, position_found = find_start_position_target_ship(
+ own_ship.initial.position,
+ lat_lon0,
+ own_ship_cog,
+ target_ship_position_future,
+ target_ship_vector_length,
+ beta,
+ desired_encounter_type,
+ settings,
+ )
+
+ if position_found:
+ target_ship_initial_position: Position = start_position_target_ship
+ target_ship_cog: float = calculate_ship_cog(
+ target_ship_initial_position, target_ship_position_future, lat_lon0
+ )
+ encounter_ok: bool = check_encounter_evolvement(
+ own_ship,
+ own_ship_cog,
+ own_ship.initial.position,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ target_ship_position_future,
+ desired_encounter_type,
+ settings,
+ )
+
+ if settings.disable_land_check is False:
+ # Check if trajectory passes land
+ trajectory_on_land = path_crosses_land(
+ target_ship_initial_position,
+ target_ship_sog,
+ target_ship_cog,
+ lat_lon0,
+ settings.situation_length,
+ )
+ encounter_found = encounter_ok and not trajectory_on_land
+ else:
+ encounter_found = encounter_ok
+
+ if encounter_found:
+ target_ship_static.id = uuid4()
+ target_ship_static.name = f"target_ship_{encounter_number}"
+ target_ship_initial: Initial = Initial(
+ position=target_ship_initial_position,
+ sog=target_ship_sog,
+ cog=target_ship_cog,
+ heading=target_ship_cog,
+ nav_status=AISNavStatus.UNDER_WAY_USING_ENGINE,
+ )
+ target_ship_waypoint0 = Waypoint(
+ position=target_ship_initial_position.model_copy(deep=True), turn_radius=None, data=None
+ )
+
+ future_position_target_ship = calculate_position_at_certain_time(
+ target_ship_initial_position,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ settings.situation_length,
+ )
+
+ target_ship_waypoint1 = Waypoint(
+ position=future_position_target_ship, turn_radius=None, data=None
+ )
+ waypoints = [target_ship_waypoint0, target_ship_waypoint1]
+
+ target_ship = TargetShip(
+ static=target_ship_static, initial=target_ship_initial, waypoints=waypoints
+ )
+ else:
+ # Since encounter is not found, using initial values from own ship. Will not be taken into use.
+ target_ship = TargetShip(static=target_ship_static, initial=own_ship.initial, waypoints=None)
+ return target_ship, encounter_found
+
+
+
+
+[docs]
+def check_encounter_evolvement(
+ own_ship: OwnShip,
+ own_ship_cog: float,
+ own_ship_position_future: Position,
+ lat_lon0: Position,
+ target_ship_sog: float,
+ target_ship_cog: float,
+ target_ship_position_future: Position,
+ desired_encounter_type: EncounterType,
+ settings: EncounterSettings,
+) -> bool:
+ """
+ Check encounter evolvement. The generated encounter should be the same type of
+ encounter (head-on, crossing, give-way) also some time before the encounter is started.
+
+ Params:
+ * own_ship: Own ship information such as initial position, sog and cog
+ * target_ship: Target ship information such as initial position, sog and cog
+ * desired_encounter_type: Desired type of encounter to be generated
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * returns True if encounter ok, False if encounter not ok
+ """
+ theta13_criteria: float = settings.classification.theta13_criteria
+ theta14_criteria: float = settings.classification.theta14_criteria
+ theta15_criteria: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ assert own_ship.initial is not None
+
+ own_ship_sog: float = own_ship.initial.sog
+ evolve_time: float = settings.evolve_time
+
+ # Calculating position back in time to ensure that the encounter do not change from one type
+ # to another before the encounter is started
+ encounter_preposition_target_ship = calculate_position_at_certain_time(
+ target_ship_position_future,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ -evolve_time,
+ )
+ encounter_preposition_own_ship = calculate_position_at_certain_time(
+ own_ship_position_future,
+ lat_lon0,
+ own_ship_sog,
+ own_ship_cog,
+ -evolve_time,
+ )
+ pre_beta, pre_alpha = calculate_relative_bearing(
+ encounter_preposition_own_ship,
+ own_ship_cog,
+ encounter_preposition_target_ship,
+ target_ship_cog,
+ lat_lon0,
+ )
+
+ pre_colreg_state = determine_colreg(
+ pre_alpha, pre_beta, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ encounter_ok: bool = pre_colreg_state == desired_encounter_type
+
+ return encounter_ok
+
+
+
+
+[docs]
+def define_own_ship(
+ desired_traffic_situation: SituationInput,
+ own_ship_static: ShipStatic,
+ encounter_settings: EncounterSettings,
+ lat_lon0: Position,
+) -> OwnShip:
+ """
+ Define own ship based on information in desired traffic situation.
+
+ Params:
+ * desired_traffic_situation: Information about type of traffic situation to generate
+ * own_ship_static: Static information of own ship.
+ * encounter_settings: Necessary setting for the encounter
+ * lat_lon0: Reference position [deg]
+
+ Returns
+ -------
+ * own_ship: Own ship
+ """
+ own_ship_initial: Initial = desired_traffic_situation.own_ship.initial
+ if desired_traffic_situation.own_ship.waypoints is None:
+ # If waypoints are not given, let initial position be the first waypoint,
+ # then calculate second waypoint some time in the future
+ own_ship_waypoint0 = Waypoint(
+ position=own_ship_initial.position.model_copy(deep=True), turn_radius=None, data=None
+ )
+ ship_position_future = calculate_position_at_certain_time(
+ own_ship_initial.position,
+ lat_lon0,
+ own_ship_initial.sog,
+ own_ship_initial.cog,
+ encounter_settings.situation_length,
+ )
+ own_ship_waypoint1 = Waypoint(position=ship_position_future, turn_radius=None, data=None)
+ own_ship_waypoints: List[Waypoint] = [own_ship_waypoint0, own_ship_waypoint1]
+ elif len(desired_traffic_situation.own_ship.waypoints) == 1:
+ # If one waypoint is given, use initial position as first waypoint
+ own_ship_waypoint0 = Waypoint(
+ position=own_ship_initial.position.model_copy(deep=True), turn_radius=None, data=None
+ )
+ own_ship_waypoint1 = desired_traffic_situation.own_ship.waypoints[0]
+ own_ship_waypoints: List[Waypoint] = [own_ship_waypoint0, own_ship_waypoint1]
+ else:
+ own_ship_waypoints: List[Waypoint] = desired_traffic_situation.own_ship.waypoints
+
+ own_ship = OwnShip(
+ static=own_ship_static,
+ initial=own_ship_initial,
+ waypoints=own_ship_waypoints,
+ )
+
+ return own_ship
+
+
+
+
+[docs]
+def calculate_min_vector_length_target_ship(
+ own_ship_position: Position,
+ own_ship_cog: float,
+ target_ship_position_future: Position,
+ desired_beta: float,
+ lat_lon0: Position,
+) -> float:
+ """
+ Calculate minimum vector length (target ship sog x vector). This will
+ ensure that ship sog is high enough to find proper situation.
+
+ Params:
+ * own_ship_position: Own ship initial position, latitudinal [rad] and longitudinal [rad]
+ * own_ship_cog: Own ship initial cog
+ * target_ship_position_future: Target ship future position
+ * desired_beta: Desired relative bearing between
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * min_vector_length: Minimum vector length (target ship sog x vector)
+ """
+ psi: float = own_ship_cog + desired_beta
+
+ own_ship_position_north, own_ship_position_east, _ = llh2flat(
+ own_ship_position.latitude, own_ship_position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ target_ship_position_future_north, target_ship_position_future_east, _ = llh2flat(
+ target_ship_position_future.latitude,
+ target_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+
+ p_1 = np.array([own_ship_position_north, own_ship_position_east])
+ p_2 = np.array([own_ship_position_north + np.cos(psi), own_ship_position_east + np.sin(psi)])
+ p_3 = np.array([target_ship_position_future_north, target_ship_position_future_east])
+
+ min_vector_length: float = float(np.abs(np.cross(p_2 - p_1, p_3 - p_1) / np.linalg.norm(p_2 - p_1)))
+
+ return min_vector_length
+
+
+
+
+[docs]
+def find_start_position_target_ship(
+ own_ship_position: Position,
+ lat_lon0: Position,
+ own_ship_cog: float,
+ target_ship_position_future: Position,
+ target_ship_vector_length: float,
+ desired_beta: float,
+ desired_encounter_type: EncounterType,
+ settings: EncounterSettings,
+) -> Tuple[Position, bool]:
+ """
+ Find start position of target ship using desired beta and vector length.
+
+ Params:
+ * own_ship_position: Own ship initial position, sog and cog
+ * own_ship_cog: Own ship initial cog
+ * target_ship_position_future: Target ship future position
+ * target_ship_vector_length: vector length (target ship sog x vector)
+ * desired_beta: Desired bearing between own ship and target ship seen from own ship
+ * desired_encounter_type: Desired type of encounter to be generated
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * start_position_target_ship: Dict, initial position of target ship {north, east} [m]
+ * start_position_found: 0=position not found, 1=position found
+ """
+ theta13_criteria: float = settings.classification.theta13_criteria
+ theta14_criteria: float = settings.classification.theta14_criteria
+ theta15_criteria: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ n_1, e_1, _ = llh2flat(
+ own_ship_position.latitude, own_ship_position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ n_2, e_2, _ = llh2flat(
+ target_ship_position_future.latitude,
+ target_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ v_r: float = target_ship_vector_length
+ psi: float = own_ship_cog + desired_beta
+
+ n_4: float = n_1 + np.cos(psi)
+ e_4: float = e_1 + np.sin(psi)
+
+ b: float = (
+ -2 * e_2 * e_4
+ - 2 * n_2 * n_4
+ + 2 * e_1 * e_2
+ + 2 * n_1 * n_2
+ + 2 * e_1 * (e_4 - e_1)
+ + 2 * n_1 * (n_4 - n_1)
+ )
+ a: float = (e_4 - e_1) ** 2 + (n_4 - n_1) ** 2
+ c: float = e_2**2 + n_2**2 - 2 * e_1 * e_2 - 2 * n_1 * n_2 - v_r**2 + e_1**2 + n_1**2
+
+ # Assign conservative fallback values to return variables
+ start_position_found: bool = False
+ start_position_target_ship = target_ship_position_future.model_copy(deep=True)
+
+ if b**2 - 4 * a * c <= 0.0:
+ # Do not run calculation of target ship start position. Return fallback values.
+ return start_position_target_ship, start_position_found
+
+ # Calculation of target ship start position
+ s_1 = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a)
+ s_2 = (-b - np.sqrt(b**2 - 4 * a * c)) / (2 * a)
+
+ e_31 = round(e_1 + s_1 * (e_4 - e_1), 0)
+ n_31 = round(n_1 + s_1 * (n_4 - n_1), 0)
+ e_32 = round(e_1 + s_2 * (e_4 - e_1), 0)
+ n_32 = round(n_1 + s_2 * (n_4 - n_1), 0)
+
+ lat31, lon31, _ = flat2llh(n_31, e_31, lat_lon0.latitude, lat_lon0.longitude)
+ target_ship_cog_1: float = calculate_ship_cog(
+ pos_0=Position(latitude=lat31, longitude=lon31),
+ pos_1=target_ship_position_future,
+ lat_lon0=lat_lon0,
+ )
+ beta1, alpha1 = calculate_relative_bearing(
+ position_own_ship=own_ship_position,
+ heading_own_ship=own_ship_cog,
+ position_target_ship=Position(latitude=lat31, longitude=lon31),
+ heading_target_ship=target_ship_cog_1,
+ lat_lon0=lat_lon0,
+ )
+ colreg_state1: EncounterType = determine_colreg(
+ alpha1, beta1, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ lat32, lon32, _ = flat2llh(n_32, e_32, lat_lon0.latitude, lat_lon0.longitude)
+ target_ship_cog_2 = calculate_ship_cog(
+ pos_0=Position(latitude=lat32, longitude=lon32),
+ pos_1=target_ship_position_future,
+ lat_lon0=lat_lon0,
+ )
+ beta2, alpha2 = calculate_relative_bearing(
+ position_own_ship=own_ship_position,
+ heading_own_ship=own_ship_cog,
+ position_target_ship=Position(latitude=lat32, longitude=lon32),
+ heading_target_ship=target_ship_cog_2,
+ lat_lon0=lat_lon0,
+ )
+ colreg_state2: EncounterType = determine_colreg(
+ alpha2, beta2, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ if (
+ desired_encounter_type is colreg_state1
+ and np.abs(convert_angle_0_to_2_pi_to_minus_pi_to_pi(np.abs(beta1 - desired_beta))) < 0.01
+ ):
+ start_position_target_ship = Position(latitude=lat31, longitude=lon31)
+ start_position_found = True
+ elif (
+ desired_encounter_type is colreg_state2
+ and np.abs(convert_angle_0_to_2_pi_to_minus_pi_to_pi(np.abs(beta2 - desired_beta))) < 0.01
+ ):
+ start_position_target_ship = Position(latitude=lat32, longitude=lon32)
+ start_position_found = True
+
+ return start_position_target_ship, start_position_found
+
+
+
+
+[docs]
+def assign_future_position_to_target_ship(
+ own_ship_position_future: Position,
+ lat_lon0: Position,
+ max_meeting_distance: float,
+) -> Position:
+ """
+ Randomly assign future position of target ship. If drawing a circle with radius
+ max_meeting_distance around future position of own ship, future position of
+ target ship shall be somewhere inside this circle.
+
+ Params:
+ * own_ship_position_future: Dict, own ship position at a given time in the
+ future, {north, east}
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+ * max_meeting_distance: Maximum distance between own ship and target ship at
+ a given time in the future [m]
+
+ Returns
+ -------
+ future_position_target_ship: Future position of target ship {north, east} [m]
+ """
+ random_angle = random.uniform(0, 1) * 2 * np.pi
+ random_distance = random.uniform(0, 1) * max_meeting_distance
+
+ own_ship_position_future_north, own_ship_position_future_east, _ = llh2flat(
+ own_ship_position_future.latitude,
+ own_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ north: float = own_ship_position_future_north + random_distance * np.cos(random_angle)
+ east: float = own_ship_position_future_east + random_distance * np.sin(random_angle)
+ latitude, longitude, _ = flat2llh(north, east, lat_lon0.latitude, lat_lon0.longitude)
+ return Position(latitude=latitude, longitude=longitude)
+
+
+
+
+[docs]
+def determine_colreg(
+ alpha: float,
+ beta: float,
+ theta13_criteria: float,
+ theta14_criteria: float,
+ theta15_criteria: float,
+ theta15: List[float],
+) -> EncounterType:
+ """
+ Determine the colreg type based on alpha, relative bearing between target ship and own
+ ship seen from target ship, and beta, relative bearing between own ship and target ship
+ seen from own ship.
+
+ Params:
+ * alpha: relative bearing between target ship and own ship seen from target ship
+ * beta: relative bearing between own ship and target ship seen from own ship
+ * theta13_criteria: Tolerance for "coming up with" relative bearing
+ * theta14_criteria: Tolerance for "reciprocal or nearly reciprocal cogs",
+ "when in any doubt... assume... [head-on]"
+ * theta15_criteria: Crossing aspect limit, used for classifying a crossing encounter
+ * theta15: 22.5 deg aft of the beam, used for classifying a crossing and an overtaking
+ encounter
+
+ Returns
+ -------
+ * encounter classification
+ """
+ # Mapping
+ alpha_2_pi: float = alpha if alpha >= 0.0 else alpha + 2 * np.pi
+ beta_pi: float = beta if (beta >= 0.0) & (beta <= np.pi) else beta - 2 * np.pi
+
+ # Find appropriate rule set
+ if (beta > theta15[0]) & (beta < theta15[1]) & (abs(alpha) - theta13_criteria <= 0.001):
+ return EncounterType.OVERTAKING_STAND_ON
+ if (
+ (alpha_2_pi > theta15[0])
+ & (alpha_2_pi < theta15[1])
+ & (abs(beta_pi) - theta13_criteria <= 0.001)
+ ):
+ return EncounterType.OVERTAKING_GIVE_WAY
+ if (abs(beta_pi) - theta14_criteria <= 0.001) & (abs(alpha) - theta14_criteria <= 0.001):
+ return EncounterType.HEAD_ON
+ if (beta > 0) & (beta < theta15[0]) & (alpha > -theta15[0]) & (alpha - theta15_criteria <= 0.001):
+ return EncounterType.CROSSING_GIVE_WAY
+ if (
+ (alpha_2_pi > 0)
+ & (alpha_2_pi < theta15[0])
+ & (beta_pi > -theta15[0])
+ & (beta_pi - theta15_criteria <= 0.001)
+ ):
+ return EncounterType.CROSSING_STAND_ON
+ return EncounterType.NO_RISK_COLLISION
+
+
+
+
+[docs]
+def calculate_relative_bearing(
+ position_own_ship: Position,
+ heading_own_ship: float,
+ position_target_ship: Position,
+ heading_target_ship: float,
+ lat_lon0: Position,
+) -> Tuple[float, float]:
+ """
+ Calculate relative bearing between own ship and target ship, both seen from
+ own ship and seen from target ship.
+
+ Params:
+ * position_own_ship: Own ship position {latitude, longitude} [rad]
+ * heading_own_ship: Own ship heading [rad]
+ * position_target_ship: Target ship position {latitude, longitude} [rad]
+ * heading_target_ship: Target ship heading [rad]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * beta: relative bearing between own ship and target ship seen from own ship [rad]
+ * alpha: relative bearing between target ship and own ship seen from target ship [rad]
+ """
+ # POSE combination of relative bearing and contact angle
+ n_own_ship, e_own_ship, _ = llh2flat(
+ position_own_ship.latitude, position_own_ship.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ n_target_ship, e_target_ship, _ = llh2flat(
+ position_target_ship.latitude,
+ position_target_ship.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+
+ # Absolute bearing of target ship relative to own ship
+ bng_own_ship_target_ship: float = 0.0
+ if e_own_ship == e_target_ship:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 0.0
+ else:
+ bng_own_ship_target_ship = np.pi
+ else:
+ if e_own_ship < e_target_ship:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 1 / 2 * np.pi - np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ bng_own_ship_target_ship = 1 / 2 * np.pi + np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 3 / 2 * np.pi + np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ bng_own_ship_target_ship = 3 / 2 * np.pi - np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+
+ # Bearing of own ship from the perspective of the contact
+ bng_target_ship_own_ship: float = bng_own_ship_target_ship + np.pi
+
+ # Relative bearing of contact ship relative to own ship
+ beta: float = bng_own_ship_target_ship - heading_own_ship
+ while beta < 0:
+ beta += 2 * np.pi
+ while beta >= 2 * np.pi:
+ beta -= 2 * np.pi
+
+ # Relative bearing of own ship relative to target ship
+ alpha: float = bng_target_ship_own_ship - heading_target_ship
+ while alpha < -np.pi:
+ alpha += 2 * np.pi
+ while alpha >= np.pi:
+ alpha -= 2 * np.pi
+
+ return beta, alpha
+
+
+
+
+[docs]
+def calculate_ship_cog(pos_0: Position, pos_1: Position, lat_lon0: Position) -> float:
+ """
+ Calculate ship cog between two waypoints.
+
+ Params:
+ * waypoint_0: Dict, waypoint {latitude, longitude} [rad]
+ * waypoint_1: Dict, waypoint {latitude, longitude} [rad]
+
+ Returns
+ -------
+ * cog: Ship cog [rad]
+ """
+ n_0, e_0, _ = llh2flat(pos_0.latitude, pos_0.longitude, lat_lon0.latitude, lat_lon0.longitude)
+ n_1, e_1, _ = llh2flat(pos_1.latitude, pos_1.longitude, lat_lon0.latitude, lat_lon0.longitude)
+
+ cog: float = np.arctan2(e_1 - e_0, n_1 - n_0)
+ if cog < 0.0:
+ cog += 2 * np.pi
+ return round(cog, 3)
+
+
+
+
+[docs]
+def assign_vector_time(vector_time_range: List[float]):
+ """
+ Assign random (uniform) vector time.
+
+ Params:
+ * vector_range: Minimum and maximum value for vector time
+
+ Returns
+ -------
+ * vector_time: Vector time [min]
+ """
+ vector_time: float = vector_time_range[0] + random.uniform(0, 1) * (
+ vector_time_range[1] - vector_time_range[0]
+ )
+ return vector_time
+
+
+
+
+[docs]
+def assign_sog_to_target_ship(
+ encounter_type: EncounterType,
+ own_ship_sog: float,
+ min_target_ship_sog: float,
+ relative_sog_setting: EncounterRelativeSpeed,
+):
+ """
+ Assign random (uniform) sog to target ship depending on type of encounter.
+
+ Params:
+ * encounter_type: Type of encounter
+ * own_ship_sog: Own ship sog [m/s]
+ * min_target_ship_sog: Minimum target ship sog [m/s]
+ * relative_sog_setting: Relative sog setting dependent on encounter [-]
+
+ Returns
+ -------
+ * target_ship_sog: Target ship sog [m/s]
+ """
+ if encounter_type is EncounterType.OVERTAKING_STAND_ON:
+ relative_sog = relative_sog_setting.overtaking_stand_on
+ elif encounter_type is EncounterType.OVERTAKING_GIVE_WAY:
+ relative_sog = relative_sog_setting.overtaking_give_way
+ elif encounter_type is EncounterType.HEAD_ON:
+ relative_sog = relative_sog_setting.head_on
+ elif encounter_type is EncounterType.CROSSING_GIVE_WAY:
+ relative_sog = relative_sog_setting.crossing_give_way
+ elif encounter_type is EncounterType.CROSSING_STAND_ON:
+ relative_sog = relative_sog_setting.crossing_stand_on
+ else:
+ relative_sog = [0.0, 0.0]
+
+ # Check that minimum target ship sog is in the relative sog range
+ if (
+ min_target_ship_sog / own_ship_sog > relative_sog[0]
+ and min_target_ship_sog / own_ship_sog < relative_sog[1]
+ ):
+ relative_sog[0] = min_target_ship_sog / own_ship_sog
+
+ target_ship_sog: float = (
+ relative_sog[0] + random.uniform(0, 1) * (relative_sog[1] - relative_sog[0])
+ ) * own_ship_sog
+
+ return target_ship_sog
+
+
+
+
+[docs]
+def assign_beta_from_list(beta_limit: List[float]) -> float:
+ """
+ Assign random (uniform) relative bearing beta between own ship
+ and target ship depending between the limits given by beta_limit.
+
+ Params:
+ * beta_limit: Limits for beta
+
+ Returns
+ -------
+ * Relative bearing between own ship and target ship seen from own ship [rad]
+ """
+ assert len(beta_limit) == 2
+ beta: float = beta_limit[0] + random.uniform(0, 1) * (beta_limit[1] - beta_limit[0])
+ return beta
+
+
+
+
+[docs]
+def assign_beta(encounter_type: EncounterType, settings: EncounterSettings) -> float:
+ """
+ Assign random (uniform) relative bearing beta between own ship
+ and target ship depending on type of encounter.
+
+ Params:
+ * encounter_type: Type of encounter
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * Relative bearing between own ship and target ship seen from own ship [rad]
+ """
+ theta13_crit: float = settings.classification.theta13_criteria
+ theta14_crit: float = settings.classification.theta14_criteria
+ theta15_crit: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ if encounter_type is EncounterType.OVERTAKING_STAND_ON:
+ return theta15[0] + random.uniform(0, 1) * (theta15[1] - theta15[0])
+ if encounter_type is EncounterType.OVERTAKING_GIVE_WAY:
+ return -theta13_crit + random.uniform(0, 1) * (theta13_crit - (-theta13_crit))
+ if encounter_type is EncounterType.HEAD_ON:
+ return -theta14_crit + random.uniform(0, 1) * (theta14_crit - (-theta14_crit))
+ if encounter_type is EncounterType.CROSSING_GIVE_WAY:
+ return 0 + random.uniform(0, 1) * (theta15[0] - 0)
+ if encounter_type is EncounterType.CROSSING_STAND_ON:
+ return convert_angle_minus_pi_to_pi_to_0_to_2_pi(
+ -theta15[1] + random.uniform(0, 1) * (theta15[1] + theta15_crit)
+ )
+ return 0.0
+
+
+
+
+[docs]
+def decide_target_ship(target_ships_static: List[ShipStatic]) -> ShipStatic:
+ """
+ Randomly pick a target ship from a list of target ships.
+
+ Params:
+ * target_ships: list of target ships with static information
+
+ Returns
+ -------
+ * The target ship, info of type, size etc.
+ """
+ num_target_ships: int = len(target_ships_static)
+ target_ship_to_use: int = random.randint(1, num_target_ships)
+ target_ship_static: ShipStatic = target_ships_static[target_ship_to_use - 1]
+ return target_ship_static.model_copy(deep=True)
+
+
+"""
+The Marine Systems Simulator (MSS) is a Matlab and Simulink library for marine systems.
+
+It includes models for ships, underwater vehicles, unmanned surface vehicles, and floating structures.
+The library also contains guidance, navigation, and control (GNC) blocks for real-time simulation.
+The algorithms are described in:
+
+T. I. Fossen (2021). Handbook of Marine Craft Hydrodynamics and Motion Control. 2nd. Edition,
+Wiley. ISBN-13: 978-1119575054
+
+Parts of the library have been re-implemented in Python and are found below.
+"""
+
+from typing import Tuple
+
+import numpy as np
+
+
+
+[docs]
+def flat2llh(
+ x_n: float,
+ y_n: float,
+ lat_0: float,
+ lon_0: float,
+ z_n: float = 0.0,
+ height_ref: float = 0.0,
+) -> Tuple[float, float, float]:
+ """
+ Compute longitude lon (rad), latitude lat (rad) and height h (m) for the
+ NED coordinates (xn,yn,zn).
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ The method computes longitude lon (rad), latitude lat (rad) and height h (m) for the
+ NED coordinates (xn,yn,zn) using a flat Earth coordinate system defined by the WGS-84
+ ellipsoid. The flat Earth coordinate origin is located at (lon_0, lat_0) with reference
+ height h_ref in meters above the surface of the ellipsoid. Both height and h_ref
+ are positive upwards, while zn is positive downwards (NED).
+ Author: Thor I. Fossen
+ Date: 20 July 2018
+ Revisions: 2023-02-04 updates the formulas for latitude and longitude
+
+ Params:
+ * xn: Ship position, north [m]
+ * yn: Ship position, east [m]
+ * zn=0.0: Ship position, down [m]
+ * lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
+ * h_ref=0.0: Flat earth coordinate with reference h_ref in meters above the surface
+ of the ellipsoid
+
+ Returns
+ -------
+ * lat: Latitude [rad]
+ * lon: Longitude [rad]
+ * h: Height [m]
+
+ """
+ # WGS-84 parameters
+ a_radius = 6378137 # Semi-major axis
+ f_factor = 1 / 298.257223563 # Flattening
+ e_eccentricity = np.sqrt(2 * f_factor - f_factor**2) # Earth eccentricity
+
+ r_n = a_radius / np.sqrt(1 - e_eccentricity**2 * np.sin(lat_0) ** 2)
+ r_m = r_n * ((1 - e_eccentricity**2) / (1 - e_eccentricity**2 * np.sin(lat_0) ** 2))
+
+ d_lat = x_n / (r_m + height_ref) # delta latitude dmu = mu - mu0
+ d_lon = y_n / ((r_n + height_ref) * np.cos(lat_0)) # delta longitude dl = l - l0
+
+ lat = ssa(lat_0 + d_lat)
+ lon = ssa(lon_0 + d_lon)
+ height = height_ref - z_n
+
+ return lat, lon, height
+
+
+
+
+[docs]
+def llh2flat(
+ lat: float,
+ lon: float,
+ lat_0: float,
+ lon_0: float,
+ height: float = 0.0,
+ height_ref: float = 0.0,
+) -> Tuple[float, float, float]:
+ """
+ Compute (north, east) for a flat Earth coordinate system from longitude
+ lon (rad) and latitude lat (rad).
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ The method computes (north, east) for a flat Earth coordinate system from longitude
+ lon (rad) and latitude lat (rad) of the WGS-84 elipsoid. The flat Earth coordinate
+ origin is located at (lon_0, lat_0).
+ Author: Thor I. Fossen
+ Date: 20 July 2018
+ Revisions: 2023-02-04 updates the formulas for latitude and longitude
+
+ Params:
+ * lat: Ship position in latitude [rad]
+ * lon: Ship position in longitude [rad]
+ * h=0.0: Ship height in meters above the surface of the ellipsoid
+ * lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
+ * h_ref=0.0: Flat earth coordinate with reference h_ref in meters above
+ the surface of the ellipsoid
+
+ Returns
+ -------
+ * x_n: Ship position, north [m]
+ * y_n: Ship position, east [m]
+ * z_n: Ship position, down [m]
+ """
+
+ # WGS-84 parameters
+ a_radius = 6378137 # Semi-major axis (equitorial radius)
+ f_factor = 1 / 298.257223563 # Flattening
+ e_eccentricity = np.sqrt(2 * f_factor - f_factor**2) # Earth eccentricity
+
+ d_lon = lon - lon_0
+ d_lat = lat - lat_0
+
+ r_n = a_radius / np.sqrt(1 - e_eccentricity**2 * np.sin(lat_0) ** 2)
+ r_m = r_n * ((1 - e_eccentricity**2) / (1 - e_eccentricity**2 * np.sin(lat_0) ** 2))
+
+ x_n = d_lat * (r_m + height_ref)
+ y_n = d_lon * ((r_n + height_ref) * np.cos(lat_0))
+ z_n = height_ref - height
+
+ return x_n, y_n, z_n
+
+
+
+
+[docs]
+def ssa(angle: float) -> float:
+ """
+ Return the "smallest signed angle" (SSA) or the smallest difference between two angles.
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ Examples
+ --------
+ angle = ssa(angle) maps an angle in rad to the interval [-pi pi)
+
+ Author: Thor I. Fossen
+ Date: 2018-09-21
+
+ Param:
+ * angle: angle given in radius
+
+ Returns
+ -------
+ * smallest_angle: "smallest signed angle" or the smallest difference between two angles
+ """
+
+ return np.mod(angle + np.pi, 2 * np.pi) - np.pi
+
+
+# The matplotlib package is unfortunately not fully typed. Hence the following pyright exemption.
+# pyright: reportUnknownMemberType=false
+"""Functions to prepare and plot traffic situations."""
+import math
+from typing import List, Optional, Tuple, Union
+
+import matplotlib.pyplot as plt
+import numpy as np
+from folium import Map, Polygon
+from maritime_schema.types.caga import Position, Ship, TargetShip, TrafficSituation
+from matplotlib.axes import Axes as Axes
+from matplotlib.patches import Circle
+
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+from trafficgen.types import EncounterSettings
+from trafficgen.utils import m_2_nm, rad_2_deg
+
+
+
+[docs]
+def calculate_vector_arrow(
+ position: Position,
+ direction: float,
+ vector_length: float,
+ lat_lon0: Position,
+) -> List[Tuple[float, float]]:
+ """
+ Calculate the arrow with length vector pointing in the direction of ship course.
+
+ Params:
+ * position: {latitude}, {longitude} position of the ship [rad]
+ * direction: direction the arrow is pointing [rad]
+ * vector_length: length of vector [m]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * arrow_points: Polygon points to draw the arrow [deg]
+ """
+ north_start, east_start, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ side_length = vector_length / 10
+ sides_angle = 25
+
+ north_end = north_start + vector_length * np.cos(direction)
+ east_end = east_start + vector_length * np.sin(direction)
+
+ north_arrow_side_1 = north_end + side_length * np.cos(direction + np.pi - sides_angle)
+ east_arrow_side_1 = east_end + side_length * np.sin(direction + np.pi - sides_angle)
+ north_arrow_side_2 = north_end + side_length * np.cos(direction + np.pi + sides_angle)
+ east_arrow_side_2 = east_end + side_length * np.sin(direction + np.pi + sides_angle)
+
+ lat_start, lon_start, _ = flat2llh(north_start, east_start, lat_lon0.latitude, lat_lon0.longitude)
+ lat_end, lon_end, _ = flat2llh(north_end, east_end, lat_lon0.latitude, lat_lon0.longitude)
+ lat_arrow_side_1, lon_arrow_side_1, _ = flat2llh(
+ north_arrow_side_1, east_arrow_side_1, lat_lon0.latitude, lat_lon0.longitude
+ )
+ lat_arrow_side_2, lon_arrow_side_2, _ = flat2llh(
+ north_arrow_side_2, east_arrow_side_2, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ point_1 = (rad_2_deg(lat_start), rad_2_deg(lon_start))
+ point_2 = (rad_2_deg(lat_end), rad_2_deg(lon_end))
+ point_3 = (rad_2_deg(lat_arrow_side_1), rad_2_deg(lon_arrow_side_1))
+ point_4 = (rad_2_deg(lat_arrow_side_2), rad_2_deg(lon_arrow_side_2))
+
+ return [point_1, point_2, point_3, point_4, point_2]
+
+
+
+
+[docs]
+def calculate_ship_outline(
+ position: Position,
+ course: float,
+ lat_lon0: Position,
+ ship_length: float = 100.0,
+ ship_width: float = 15.0,
+) -> List[Tuple[float, float]]:
+ """
+ Calculate the outline of the ship pointing in the direction of ship course.
+
+ Params:
+ * position: {latitude}, {longitude} position of the ship [rad]
+ * course: course of the ship [rad]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+ * ship_length: Ship length. If not given, ship length is set to 100
+ * ship_width: Ship width. If not given, ship width is set to 15
+
+ Returns
+ -------
+ * ship_outline_points: Polygon points to draw the ship [deg]
+ """
+ north_start, east_start, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ # increase size for visualizing
+ ship_length *= 10
+ ship_width *= 10
+
+ north_pos1 = north_start + np.cos(course) * (-ship_length / 2) - np.sin(course) * ship_width / 2
+ east_pos1 = east_start + np.sin(course) * (-ship_length / 2) + np.cos(course) * ship_width / 2
+ lat_pos1, lon_pos1, _ = flat2llh(north_pos1, east_pos1, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos2 = (
+ north_start
+ + np.cos(course) * (ship_length / 2 - ship_length * 0.1)
+ - np.sin(course) * ship_width / 2
+ )
+ east_pos2 = (
+ east_start
+ + np.sin(course) * (ship_length / 2 - ship_length * 0.1)
+ + np.cos(course) * ship_width / 2
+ )
+ lat_pos2, lon_pos2, _ = flat2llh(north_pos2, east_pos2, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos3 = north_start + np.cos(course) * (ship_length / 2)
+ east_pos3 = east_start + np.sin(course) * (ship_length / 2)
+ lat_pos3, lon_pos3, _ = flat2llh(north_pos3, east_pos3, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos4 = (
+ north_start
+ + np.cos(course) * (ship_length / 2 - ship_length * 0.1)
+ - np.sin(course) * (-ship_width / 2)
+ )
+ east_pos4 = (
+ east_start
+ + np.sin(course) * (ship_length / 2 - ship_length * 0.1)
+ + np.cos(course) * (-ship_width / 2)
+ )
+ lat_pos4, lon_pos4, _ = flat2llh(north_pos4, east_pos4, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos5 = north_start + np.cos(course) * (-ship_length / 2) - np.sin(course) * (-ship_width / 2)
+ east_pos5 = east_start + np.sin(course) * (-ship_length / 2) + np.cos(course) * (-ship_width / 2)
+ lat_pos5, lon_pos5, _ = flat2llh(north_pos5, east_pos5, lat_lon0.latitude, lat_lon0.longitude)
+
+ point_1 = (rad_2_deg(lat_pos1), rad_2_deg(lon_pos1))
+ point_2 = (rad_2_deg(lat_pos2), rad_2_deg(lon_pos2))
+ point_3 = (rad_2_deg(lat_pos3), rad_2_deg(lon_pos3))
+ point_4 = (rad_2_deg(lat_pos4), rad_2_deg(lon_pos4))
+ point_5 = (rad_2_deg(lat_pos5), rad_2_deg(lon_pos5))
+
+ return [point_1, point_2, point_3, point_4, point_5, point_1]
+
+
+
+
+[docs]
+def plot_specific_traffic_situation(
+ traffic_situations: List[TrafficSituation],
+ situation_number: int,
+ encounter_settings: EncounterSettings,
+):
+ """
+ Plot a specific situation in map.
+
+ Params:
+ * traffic_situations: Generated traffic situations
+ * situation_number: The specific situation to be plotted
+ """
+
+ num_situations = len(traffic_situations)
+ if situation_number > num_situations:
+ print(
+ f"Situation_number specified higher than number of situations available, plotting last situation: {num_situations}"
+ )
+ situation_number = num_situations
+
+ situation: TrafficSituation = traffic_situations[situation_number - 1]
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ assert encounter_settings.common_vector is not None
+
+ lat_lon0 = situation.own_ship.initial.position
+
+ map_plot = Map(location=(rad_2_deg(lat_lon0.latitude), rad_2_deg(lat_lon0.longitude)), zoom_start=10)
+ map_plot = add_ship_to_map(
+ situation.own_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ map_plot,
+ "black",
+ )
+
+ target_ships: Union[List[TargetShip], None] = situation.target_ships
+ assert target_ships is not None
+ for target_ship in target_ships:
+ map_plot = add_ship_to_map(
+ target_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ map_plot,
+ "red",
+ )
+ map_plot.show_in_browser()
+
+
+
+
+[docs]
+def add_ship_to_map(
+ ship: Ship,
+ vector_time: float,
+ lat_lon0: Position,
+ map_plot: Optional[Map],
+ color: str = "black",
+) -> Map:
+ """
+ Add the ship to the map.
+
+ Params:
+ * ship: Ship information
+ * vector_time: Vector time [sec]
+ * lat_lon0=Reference point, latitudinal [rad] and longitudinal [rad]
+ * map_plot: Instance of Map. If not set, instance is set to None
+ * color: Color of the ship. If not set, color is 'black'
+
+ Returns
+ -------
+ * m: Updated instance of Map.
+ """
+ if map_plot is None:
+ map_plot = Map(
+ location=(rad_2_deg(lat_lon0.latitude), rad_2_deg(lat_lon0.longitude)), zoom_start=10
+ )
+
+ assert ship.initial is not None
+ vector_length = vector_time * ship.initial.sog
+ _ = map_plot.add_child(
+ Polygon(
+ calculate_vector_arrow(ship.initial.position, ship.initial.cog, vector_length, lat_lon0),
+ fill=True,
+ fill_opacity=1,
+ color=color,
+ )
+ )
+ _ = map_plot.add_child(
+ Polygon(
+ calculate_ship_outline(ship.initial.position, ship.initial.cog, lat_lon0),
+ fill=True,
+ fill_opacity=1,
+ color=color,
+ )
+ )
+ return map_plot
+
+
+
+
+[docs]
+def plot_traffic_situations(
+ traffic_situations: List[TrafficSituation],
+ col: int,
+ row: int,
+ encounter_settings: EncounterSettings,
+):
+ """
+ Plot the traffic situations in one more figures.
+
+ Params:
+ * traffic_situations: Traffic situations to be plotted
+ * col: Number of columns in each figure
+ * row: Number of rows in each figure
+ """
+ max_columns = col
+ max_rows = row
+ num_subplots_pr_plot = max_columns * max_rows
+ small_size = 6
+ bigger_size = 10
+
+ plt.rc("axes", titlesize=small_size) # fontsize of the axes title
+ plt.rc("axes", labelsize=small_size) # fontsize of the x and y labels
+ plt.rc("xtick", labelsize=small_size) # fontsize of the tick labels
+ plt.rc("ytick", labelsize=small_size) # fontsize of the tick labels
+ plt.rc("figure", titlesize=bigger_size) # fontsize of the figure title
+
+ # The axes should have the same x/y limits, thus find max value for
+ # north/east position to be used for plotting
+ max_value: float = 0.0
+ for situation in traffic_situations:
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ lat_lon0 = situation.own_ship.initial.position
+ max_value = find_max_value_for_plot(situation.own_ship, max_value, lat_lon0)
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ max_value = find_max_value_for_plot(target_ship, max_value, lat_lon0)
+
+ plot_number: int = 1
+ _ = plt.figure(plot_number)
+ for i, situation in enumerate(traffic_situations):
+ if math.floor(i / num_subplots_pr_plot) + 1 > plot_number:
+ plot_number += 1
+ _ = plt.figure(plot_number)
+
+ axes: Axes = plt.subplot(
+ max_rows,
+ max_columns,
+ int(1 + i - (plot_number - 1) * num_subplots_pr_plot),
+ xlabel="[nm]",
+ ylabel="[nm]",
+ )
+ _ = axes.set_title(situation.title)
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial
+ assert encounter_settings.common_vector is not None
+ lat_lon0 = situation.own_ship.initial.position
+ axes = add_ship_to_plot(
+ situation.own_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ axes,
+ "black",
+ )
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ axes = add_ship_to_plot(
+ target_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ axes,
+ "red",
+ )
+ axes.set_aspect("equal")
+
+ _ = plt.xlim(-max_value, max_value)
+ _ = plt.ylim(-max_value, max_value)
+ _ = plt.subplots_adjust(wspace=0.4, hspace=0.4)
+
+ plt.show()
+
+
+
+
+[docs]
+def find_max_value_for_plot(
+ ship: Ship,
+ max_value: float,
+ lat_lon0: Position,
+) -> float:
+ """
+ Find the maximum deviation from the Reference point in north and east direction.
+
+ Params:
+ * ship: Ship information
+ * max_value: maximum deviation in north, east direction
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * max_value: updated maximum deviation in north, east direction
+ """
+ assert ship.initial is not None
+
+ north, east, _ = llh2flat(
+ ship.initial.position.latitude,
+ ship.initial.position.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ max_value = np.max(
+ [
+ max_value,
+ np.abs(m_2_nm(north)),
+ np.abs(m_2_nm(east)),
+ ]
+ )
+ return max_value
+
+
+
+
+[docs]
+def add_ship_to_plot(
+ ship: Ship,
+ vector_time: float,
+ lat_lon0: Position,
+ axes: Optional[Axes],
+ color: str = "black",
+):
+ """
+ Add the ship to the plot.
+
+ Params:
+ * ship: Ship information
+ * vector_time: Vector time [sec]
+ * axes: Instance of figure axis. If not set, instance is set to None
+ * color: Color of the ship. If not set, color is 'black'
+ """
+ if axes is None:
+ axes = plt.gca()
+ assert isinstance(axes, Axes)
+
+ assert ship.initial is not None
+ pos_0_north, pos_0_east, _ = llh2flat(
+ ship.initial.position.latitude,
+ ship.initial.position.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ pos_0_north = m_2_nm(pos_0_north)
+ pos_0_east = m_2_nm(pos_0_east)
+ course = ship.initial.cog
+ speed = ship.initial.sog
+
+ vector_length = m_2_nm(vector_time * speed)
+
+ _ = axes.arrow(
+ pos_0_east,
+ pos_0_north,
+ vector_length * np.sin(course),
+ vector_length * np.cos(course),
+ edgecolor=color,
+ facecolor=color,
+ width=0.0001,
+ head_length=0.2,
+ head_width=0.2,
+ length_includes_head=True,
+ )
+ circle = Circle(
+ xy=(pos_0_east, pos_0_north),
+ radius=vector_time / 3000.0, # type: ignore
+ color=color,
+ )
+ _ = axes.add_patch(circle)
+
+ return axes
+
+
+"""Functions to read the files needed to build one or more traffic situations."""
+
+import json
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Union, cast
+from uuid import UUID, uuid4
+
+from maritime_schema.types.caga import (
+ ShipStatic,
+ TrafficSituation,
+)
+
+from trafficgen.types import EncounterSettings, SituationInput
+from trafficgen.utils import deg_2_rad, knot_2_m_pr_s, min_2_s, nm_2_m
+
+
+
+[docs]
+def read_situation_files(situation_folder: Path) -> List[SituationInput]:
+ """
+ Read traffic situation files.
+
+ Params:
+ * situation_folder: Path to the folder where situation files are found
+ * input_units: Specify if the inputs are given in si or maritime units
+
+ Returns
+ -------
+ * situations: List of desired traffic situations
+ """
+ situations: List[SituationInput] = []
+ for file_name in sorted([file for file in os.listdir(situation_folder) if file.endswith(".json")]):
+ file_path = os.path.join(situation_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+
+ data = convert_keys_to_snake_case(data)
+
+ if "num_situations" not in data:
+ data["num_situations"] = 1
+
+ situation: SituationInput = SituationInput(**data)
+ situation = convert_situation_data_from_maritime_to_si_units(situation)
+
+ situations.append(situation)
+ return situations
+
+
+
+
+[docs]
+def read_generated_situation_files(situation_folder: Path) -> List[TrafficSituation]:
+ """
+ Read the generated traffic situation files. Used for testing the trafficgen algorithm.
+
+ Params:
+ * situation_folder: Path to the folder where situation files are found
+
+ Returns
+ -------
+ * situations: List of desired traffic situations
+ """
+ situations: List[TrafficSituation] = []
+ for file_name in sorted([file for file in os.listdir(situation_folder) if file.endswith(".json")]):
+ file_path = os.path.join(situation_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ situation: TrafficSituation = TrafficSituation(**data)
+ situations.append(situation)
+ return situations
+
+
+
+
+[docs]
+def convert_situation_data_from_maritime_to_si_units(situation: SituationInput) -> SituationInput:
+ """
+ Convert situation data which is given in maritime units to SI units.
+
+ Params:
+ * own_ship_file: Path to the own_ship_file file
+
+ Returns
+ -------
+ * own_ship information
+ """
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ situation.own_ship.initial.position.longitude = deg_2_rad(
+ situation.own_ship.initial.position.longitude
+ )
+ situation.own_ship.initial.position.latitude = deg_2_rad(
+ situation.own_ship.initial.position.latitude
+ )
+ situation.own_ship.initial.cog = deg_2_rad(situation.own_ship.initial.cog)
+ situation.own_ship.initial.heading = deg_2_rad(situation.own_ship.initial.heading)
+ situation.own_ship.initial.sog = knot_2_m_pr_s(situation.own_ship.initial.sog)
+
+ if situation.own_ship.waypoints is not None:
+ for waypoint in situation.own_ship.waypoints:
+ waypoint.position.latitude = deg_2_rad(waypoint.position.latitude)
+ waypoint.position.longitude = deg_2_rad(waypoint.position.longitude)
+ if waypoint.data is not None:
+ assert waypoint.data.model_extra
+ if waypoint.data.model_extra.get("sog") is not None:
+ waypoint.data.model_extra["sog"]["value"] = knot_2_m_pr_s(waypoint.data.model_extra["sog"]["value"]) # type: ignore
+
+ assert situation.encounters is not None
+ for encounter in situation.encounters:
+ beta: Union[List[float], float, None] = encounter.beta
+ vector_time: Union[float, None] = encounter.vector_time
+ if beta is not None:
+ if isinstance(beta, List):
+ assert len(beta) == 2
+ for i in range(len(beta)):
+ beta[i] = deg_2_rad(beta[i])
+ encounter.beta = beta
+ else:
+ encounter.beta = deg_2_rad(beta)
+ if vector_time is not None:
+ encounter.vector_time = min_2_s(vector_time)
+ return situation
+
+
+
+
+[docs]
+def read_own_ship_static_file(own_ship_static_file: Path) -> ShipStatic:
+ """
+ Read own ship static data from file.
+
+ Params:
+ * own_ship_file: Path to the own_ship_static_file file
+
+ Returns
+ -------
+ * own_ship static information
+ """
+ with open(own_ship_static_file, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ if "id" not in data:
+ ship_id: UUID = uuid4()
+ data.update({"id": ship_id})
+
+ ship_static: ShipStatic = ShipStatic(**data)
+
+ return ship_static
+
+
+
+
+[docs]
+def read_target_ship_static_files(target_ship_folder: Path) -> List[ShipStatic]:
+ """
+ Read target ship static data files.
+
+ Params:
+ * target_ship_folder: Path to the folder where target ships are found
+
+ Returns
+ -------
+ * target_ships_static: List of different target ships with static information
+ """
+ target_ships_static: List[ShipStatic] = []
+ i = 0
+ for file_name in sorted([file for file in os.listdir(target_ship_folder) if file.endswith(".json")]):
+ i = i + 1
+ file_path = os.path.join(target_ship_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ if "id" not in data:
+ ship_id: UUID = uuid4()
+ data.update({"id": ship_id})
+
+ target_ship_static: ShipStatic = ShipStatic(**data)
+ target_ships_static.append(target_ship_static)
+ return target_ships_static
+
+
+
+
+[docs]
+def read_encounter_settings_file(settings_file: Path) -> EncounterSettings:
+ """
+ Read encounter settings file.
+
+ Params:
+ * settings_file: Path to the encounter setting file
+
+ Returns
+ -------
+ * encounter_settings: Settings for the encounter
+ """
+ with open(settings_file, encoding="utf-8") as f:
+ data = json.load(f)
+ data = check_input_units(data)
+ encounter_settings: EncounterSettings = EncounterSettings(**data)
+
+ encounter_settings = convert_settings_data_from_maritime_to_si_units(encounter_settings)
+
+ return encounter_settings
+
+
+
+
+[docs]
+def convert_settings_data_from_maritime_to_si_units(settings: EncounterSettings) -> EncounterSettings:
+ """
+ Convert situation data which is given in maritime units to SI units.
+
+ Params:
+ * own_ship_file: Path to the own_ship_file file
+
+ Returns
+ -------
+ * own_ship information
+ """
+ assert settings.classification is not None
+
+ settings.classification.theta13_criteria = deg_2_rad(settings.classification.theta13_criteria)
+ settings.classification.theta14_criteria = deg_2_rad(settings.classification.theta14_criteria)
+ settings.classification.theta15_criteria = deg_2_rad(settings.classification.theta15_criteria)
+ settings.classification.theta15[0] = deg_2_rad(settings.classification.theta15[0])
+ settings.classification.theta15[1] = deg_2_rad(settings.classification.theta15[1])
+
+ settings.vector_range[0] = min_2_s(settings.vector_range[0])
+ settings.vector_range[1] = min_2_s(settings.vector_range[1])
+
+ settings.situation_length = min_2_s(settings.situation_length)
+ settings.max_meeting_distance = nm_2_m(settings.max_meeting_distance)
+ settings.evolve_time = min_2_s(settings.evolve_time)
+ settings.common_vector = min_2_s(settings.common_vector)
+
+ return settings
+
+
+
+
+[docs]
+def check_input_units(data: Dict[str, Any]) -> Dict[str, Any]:
+ """Check if input unit is specified, if not specified it is set to SI."""
+
+ if "input_units" not in data:
+ data["input_units"] = "si"
+
+ return data
+
+
+
+
+[docs]
+def camel_to_snake(string: str) -> str:
+ """Convert a camel case string to snake case."""
+ return "".join([f"_{c.lower()}" if c.isupper() else c for c in string]).lstrip("_")
+
+
+
+
+[docs]
+def convert_keys_to_snake_case(data: Dict[str, Any]) -> Dict[str, Any]:
+ """Convert keys in a nested dictionary from camel case to snake case."""
+ return cast(Dict[str, Any], _convert_keys_to_snake_case(data))
+
+
+
+def _convert_keys_to_snake_case(
+ data: Union[Dict[str, Any], List[Any]],
+) -> Union[Dict[str, Any], List[Any]]:
+ """Convert keys in a nested dictionary from camel case to snake case."""
+
+ if isinstance(data, Dict): # Dict
+ converted_dict: Dict[str, Any] = {}
+ for key, value in data.items():
+ converted_key = camel_to_snake(key)
+ if isinstance(value, (Dict, List)):
+ converted_value = _convert_keys_to_snake_case(value)
+ else:
+ converted_value = value
+ converted_dict[converted_key] = converted_value
+ return converted_dict
+
+ # List
+ converted_list: List[Any] = []
+ for value in data:
+ if isinstance(value, (Dict, List)):
+ converted_value = _convert_keys_to_snake_case(value)
+ else:
+ converted_value = value
+ converted_list.append(value)
+ return converted_list
+
+"""Functions to generate traffic situations."""
+
+from pathlib import Path
+from typing import List, Union
+
+from maritime_schema.types.caga import (
+ OwnShip,
+ Position,
+ ShipStatic,
+ TargetShip,
+ TrafficSituation,
+)
+
+from trafficgen.encounter import (
+ define_own_ship,
+ generate_encounter,
+)
+from trafficgen.read_files import (
+ read_encounter_settings_file,
+ read_own_ship_static_file,
+ read_situation_files,
+ read_target_ship_static_files,
+)
+from trafficgen.types import EncounterSettings, EncounterType, SituationInput
+
+
+
+[docs]
+def generate_traffic_situations(
+ situation_folder: Path,
+ own_ship_file: Path,
+ target_ship_folder: Path,
+ settings_file: Path,
+) -> List[TrafficSituation]:
+ """
+ Generate a set of traffic situations using input files.
+ This is the main function for generating a set of traffic situations using input files
+ specifying number and type of encounter, type of target ships etc.
+
+ Params:
+ * situation_folder: Path to situation folder, files describing the desired situations
+ * own_ship_file: Path to where own ships is found
+ * target_ship_folder: Path to where different type of target ships is found
+ * settings_file: Path to settings file
+
+ Returns
+ -------
+ * traffic_situations: List of generated traffic situations.
+ * One situation may consist of one or more encounters.
+ """
+
+ own_ship_static: ShipStatic = read_own_ship_static_file(own_ship_file)
+ target_ships_static: List[ShipStatic] = read_target_ship_static_files(target_ship_folder)
+ encounter_settings: EncounterSettings = read_encounter_settings_file(settings_file)
+ desired_traffic_situations: List[SituationInput] = read_situation_files(situation_folder)
+ traffic_situations: List[TrafficSituation] = []
+
+ for desired_traffic_situation in desired_traffic_situations:
+ num_situations: int = desired_traffic_situation.num_situations
+ assert encounter_settings.common_vector is not None
+ assert desired_traffic_situation.own_ship is not None
+ assert desired_traffic_situation.encounters is not None
+
+ lat_lon0: Position = desired_traffic_situation.own_ship.initial.position
+
+ own_ship: OwnShip = define_own_ship(
+ desired_traffic_situation, own_ship_static, encounter_settings, lat_lon0
+ )
+ for _ in range(num_situations):
+ target_ships: List[TargetShip] = []
+ for i, encounter in enumerate(desired_traffic_situation.encounters):
+ desired_encounter_type = EncounterType(encounter.desired_encounter_type)
+ beta: Union[List[float], float, None] = encounter.beta
+ relative_speed: Union[float, None] = encounter.relative_speed
+ vector_time: Union[float, None] = encounter.vector_time
+
+ target_ship, encounter_found = generate_encounter(
+ desired_encounter_type,
+ own_ship.model_copy(deep=True),
+ target_ships_static,
+ i + 1,
+ beta,
+ relative_speed,
+ vector_time,
+ encounter_settings,
+ )
+ if encounter_found:
+ target_ships.append(target_ship.model_copy(deep=True))
+
+ traffic_situation: TrafficSituation = TrafficSituation(
+ title=desired_traffic_situation.title,
+ description=desired_traffic_situation.description,
+ own_ship=own_ship.model_copy(deep=True),
+ target_ships=target_ships,
+ start_time=None,
+ environment=None,
+ )
+ traffic_situations.append(traffic_situation)
+ return traffic_situations
+
+
+"""Domain specific data types used in trafficgen."""
+
+from enum import Enum
+from typing import List, Optional, Union
+
+from maritime_schema.types.caga import Initial, Waypoint
+from pydantic import BaseModel
+from pydantic.fields import Field
+
+
+
+[docs]
+def to_camel(string: str) -> str:
+ """Return a camel case formated string from snake case string."""
+
+ words = string.split("_")
+ return words[0] + "".join(word.capitalize() for word in words[1:])
+
+
+
+
+[docs]
+class EncounterType(Enum):
+ """Enumeration of encounter types."""
+
+ OVERTAKING_STAND_ON = "overtaking-stand-on"
+ OVERTAKING_GIVE_WAY = "overtaking-give-way"
+ HEAD_ON = "head-on"
+ CROSSING_GIVE_WAY = "crossing-give-way"
+ CROSSING_STAND_ON = "crossing-stand-on"
+ NO_RISK_COLLISION = "noRiskCollision"
+
+
+
+
+[docs]
+class Encounter(BaseModel):
+ """Data type for an encounter."""
+
+ desired_encounter_type: EncounterType
+ beta: Union[List[float], float, None] = None
+ relative_speed: Union[float, None] = None
+ vector_time: Union[float, None] = None
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterClassification(BaseModel):
+ """Data type for the encounter classification."""
+
+ theta13_criteria: float
+ theta14_criteria: float
+ theta15_criteria: float
+ theta15: List[float]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterRelativeSpeed(BaseModel):
+ """Data type for relative speed between two ships in an encounter."""
+
+ overtaking_stand_on: List[float]
+ overtaking_give_way: List[float]
+ head_on: List[float]
+ crossing_give_way: List[float]
+ crossing_stand_on: List[float]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterSettings(BaseModel):
+ """Data type for encounter settings."""
+
+ classification: EncounterClassification
+ relative_speed: EncounterRelativeSpeed
+ vector_range: List[float]
+ common_vector: float
+ situation_length: float
+ max_meeting_distance: float
+ evolve_time: float
+ disable_land_check: bool
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class OwnShipInitial(BaseModel):
+ """Data type for initial data for the own ship used for generating a situation."""
+
+ initial: Initial
+ waypoints: Optional[List[Waypoint]] = Field(None, description="An array of `Waypoint` objects.")
+
+
+
+
+[docs]
+class SituationInput(BaseModel):
+ """Data type for inputs needed for generating a situations."""
+
+ title: str
+ description: str
+ num_situations: int
+ own_ship: OwnShipInitial
+ encounters: List[Encounter]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+"""Utility functions that are used by several other functions."""
+
+from typing import List
+
+import numpy as np
+from maritime_schema.types.caga import Position, Waypoint
+
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+
+
+
+[docs]
+def knot_2_m_pr_s(speed_in_knot: float) -> float:
+ """
+ Convert ship speed in knots to meters pr second.
+
+ Params:
+ * speed_in_knot: Ship speed given in knots
+
+ Returns
+ -------
+ * speed_in_m_pr_s: Ship speed in meters pr second
+ """
+
+ knot_2_m_pr_sec: float = 0.5144
+ return speed_in_knot * knot_2_m_pr_sec
+
+
+
+
+[docs]
+def m_pr_s_2_knot(speed_in_m_pr_s: float) -> float:
+ """
+ Convert ship speed in knots to meters pr second.
+
+ Params:
+ * speed_in_m_pr_s: Ship speed given in meters pr second
+
+ Returns
+ -------
+ * speed_in_knot: Ship speed in knots
+ """
+
+ knot_2_m_pr_sec: float = 0.5144
+ return speed_in_m_pr_s / knot_2_m_pr_sec
+
+
+
+
+[docs]
+def min_2_s(time_in_min: float) -> float:
+ """
+ Convert time given in minutes to time given in seconds.
+
+ Params:
+ * time_in_min: Time given in minutes
+
+ Returns
+ -------
+ * time_in_s: Time in seconds
+ """
+
+ min_2_s_coeff: float = 60.0
+ return time_in_min * min_2_s_coeff
+
+
+
+
+[docs]
+def m_2_nm(length_in_m: float) -> float:
+ """
+ Convert length given in meters to length given in nautical miles.
+
+ Params:
+ * length_in_m: Length given in meters
+
+ Returns
+ -------
+ * length_in_nm: Length given in nautical miles
+ """
+
+ m_2_nm_coeff: float = 1.0 / 1852.0
+ return m_2_nm_coeff * length_in_m
+
+
+
+
+[docs]
+def nm_2_m(length_in_nm: float) -> float:
+ """
+ Convert length given in nautical miles to length given in meters.
+
+ Params:
+ * length_in_nm: Length given in nautical miles
+
+ Returns
+ -------
+ * length_in_m: Length given in meters
+ """
+
+ nm_2_m_factor: float = 1852.0
+ return length_in_nm * nm_2_m_factor
+
+
+
+
+[docs]
+def deg_2_rad(angle_in_degrees: float) -> float:
+ """
+ Convert angle given in degrees to angle give in radians.
+
+ Params:
+ * angle_in_degrees: Angle given in degrees
+
+ Returns
+ -------
+ * angle given in radians: Angle given in radians
+ """
+
+ return angle_in_degrees * np.pi / 180.0
+
+
+
+
+[docs]
+def rad_2_deg(angle_in_radians: float) -> float:
+ """
+ Convert angle given in radians to angle give in degrees.
+
+ Params:
+ * angle_in_degrees: Angle given in degrees
+
+ Returns
+ -------
+ * angle given in radians: Angle given in radians
+
+ """
+
+ return angle_in_radians * 180.0 / np.pi
+
+
+
+
+[docs]
+def convert_angle_minus_pi_to_pi_to_0_to_2_pi(angle_pi: float) -> float:
+ """
+ Convert an angle given in the region -pi to pi degrees to an
+ angle given in the region 0 to 2pi radians.
+
+ Params:
+ * angle_pi: Angle given in the region -pi to pi radians
+
+ Returns
+ -------
+ * angle_2_pi: Angle given in the region 0 to 2pi radians
+
+ """
+
+ return angle_pi if angle_pi >= 0.0 else angle_pi + 2 * np.pi
+
+
+
+
+[docs]
+def convert_angle_0_to_2_pi_to_minus_pi_to_pi(angle_2_pi: float) -> float:
+ """
+ Convert an angle given in the region 0 to 2*pi degrees to an
+ angle given in the region -pi to pi degrees.
+
+ Params:
+ * angle_2_pi: Angle given in the region 0 to 2pi radians
+
+ Returns
+ -------
+ * angle_pi: Angle given in the region -pi to pi radians
+
+ """
+
+ return angle_2_pi if (angle_2_pi >= 0.0) & (angle_2_pi <= np.pi) else angle_2_pi - 2 * np.pi
+
+
+
+
+[docs]
+def calculate_position_at_certain_time(
+ position: Position,
+ lat_lon0: Position,
+ speed: float,
+ course: float,
+ delta_time: float,
+) -> Position:
+ """
+ Calculate the position of the ship at a given time based on initial position
+ and delta time, and constant speed and course.
+
+ Params:
+ * position{latitude, longitude}: Initial ship position [rad]
+ * speed: Ship speed [m/s]
+ * course: Ship course [rad]
+ * delta_time: Delta time from now to the time new position is being calculated [minutes]
+
+ Returns
+ -------
+ * position{latitude, longitude}: Estimated ship position in delta time minutes [rad]
+ """
+
+ north, east, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ north = north + speed * delta_time * np.cos(course)
+ east = east + speed * delta_time * np.sin(course)
+
+ lat_future, lon_future, _ = flat2llh(north, east, lat_lon0.latitude, lat_lon0.longitude)
+
+ position_future: Position = Position(
+ latitude=lat_future,
+ longitude=lon_future,
+ )
+ return position_future
+
+
+
+
+[docs]
+def calculate_distance(position_prev: Position, position_next: Position) -> float:
+ """
+ Calculate the distance in meter between two waypoints.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * position_next{latitude, longitude}: Next waypoint [rad]
+
+ Returns
+ -------
+ * distance: Distance between waypoints [m]
+ """
+ # Using position of previous waypoint as reference point
+ north_next, east_next, _ = llh2flat(
+ position_next.latitude, position_next.longitude, position_prev.latitude, position_prev.longitude
+ )
+
+ distance: float = np.sqrt(north_next**2 + east_next**2)
+
+ return distance
+
+
+
+
+[docs]
+def calculate_position_along_track_using_waypoints(
+ waypoints: List[Waypoint],
+ inital_speed: float,
+ vector_time: float,
+) -> Position:
+ """
+ Calculate the position of the ship at a given time based on initial position
+ and delta time, and constant speed and course.
+
+ Params:
+ * position{latitude, longitude}: Initial ship position [rad]
+ * speed: Ship speed [m/s]
+ * course: Ship course [rad]
+ * delta_time: Delta time from now to the time new position is being calculated [sec]
+
+ Returns
+ -------
+ * position{latitude, longitude}: Estimated ship position in delta time minutes [rad]
+ """
+ time_in_transit: float = 0
+
+ for i in range(1, len(waypoints)):
+ ship_speed: float = inital_speed
+ if waypoints[i].data is not None and waypoints[i].data.model_extra["sog"] is not None: # type: ignore
+ ship_speed = waypoints[i].data.model_extra["sog"]["value"] # type: ignore
+
+ dist_between_waypoints = calculate_distance(waypoints[i - 1].position, waypoints[i].position)
+
+ # find distance ship will travel
+ dist_travel = ship_speed * (vector_time - time_in_transit)
+
+ if dist_travel > dist_between_waypoints:
+ time_in_transit = time_in_transit + dist_between_waypoints / ship_speed
+ else:
+ bearing = calculate_bearing_between_waypoints(
+ waypoints[i - 1].position, waypoints[i].position
+ )
+ position_along_track = calculate_destination_along_track(
+ waypoints[i - 1].position, dist_travel, bearing
+ )
+ return position_along_track
+
+ # if ship reach last waypoint in less time than vector_time, last waypoint is used
+ return waypoints[-1].position
+
+
+
+
+[docs]
+def calculate_bearing_between_waypoints(position_prev: Position, position_next: Position) -> float:
+ """
+ Calculate the bearing in rad between two waypoints.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * position_next{latitude, longitude}: Next waypoint [rad]
+
+ Returns
+ -------
+ * bearing: Bearing between waypoints [m]
+ """
+ # Using position of previous waypoint as reference point
+ north_next, east_next, _ = llh2flat(
+ position_next.latitude, position_next.longitude, position_prev.latitude, position_prev.longitude
+ )
+
+ bearing: float = convert_angle_minus_pi_to_pi_to_0_to_2_pi(np.arctan2(east_next, north_next))
+
+ return bearing
+
+
+
+
+[docs]
+def calculate_destination_along_track(
+ position_prev: Position, distance: float, bearing: float
+) -> Position:
+ """
+ Calculate the destination along the track between two waypoints when distance along the track is given.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * distance: Distance to travel [m]
+ * bearing: Bearing from previous waypoint to next waypoint [rad]
+
+ Returns
+ -------
+ * destination{latitude, longitude}: Destination along the track [rad]
+ """
+ north = distance * np.cos(bearing)
+ east = distance * np.sin(bearing)
+
+ lat, lon, _ = flat2llh(north, east, position_prev.latitude, position_prev.longitude)
+ destination = Position(latitude=lat, longitude=lon)
+
+ return destination
+
+
+"""Functions to clean traffic situations data before writing it to a json file."""
+
+from pathlib import Path
+from typing import List, TypeVar
+
+from maritime_schema.types.caga import OwnShip, Ship, TargetShip, TrafficSituation
+
+from trafficgen.utils import m_pr_s_2_knot, rad_2_deg
+
+T_ship = TypeVar("T_ship", Ship, OwnShip, TargetShip)
+
+
+
+[docs]
+def write_traffic_situations_to_json_file(situations: List[TrafficSituation], write_folder: Path):
+ """
+ Write traffic situations to json file.
+
+ Params:
+ * traffic_situations: Traffic situations to be written to file
+ * write_folder: Folder where the json files is to be written
+ """
+
+ Path(write_folder).mkdir(parents=True, exist_ok=True)
+ for i, situation in enumerate(situations):
+ file_number: int = i + 1
+ output_file_path: Path = write_folder / f"traffic_situation_{file_number:02d}.json"
+ situation = convert_situation_data_from_si_units_to__maritime(situation)
+ data: str = situation.model_dump_json(
+ by_alias=True, indent=4, exclude_unset=True, exclude_defaults=False, exclude_none=True
+ )
+ with open(output_file_path, "w", encoding="utf-8") as outfile:
+ _ = outfile.write(data)
+
+
+
+
+[docs]
+def convert_situation_data_from_si_units_to__maritime(situation: TrafficSituation) -> TrafficSituation:
+ """
+ Convert situation data which is given in SI units to maritime units.
+
+ Params:
+ * situation: Traffic situation data
+
+ Returns
+ -------
+ * situation: Converted traffic situation data
+ """
+ assert situation.own_ship is not None
+ situation.own_ship = convert_ship_data_from_si_units_to_maritime(situation.own_ship)
+
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ target_ship = convert_ship_data_from_si_units_to_maritime(target_ship)
+
+ return situation
+
+
+
+
+[docs]
+def convert_ship_data_from_si_units_to_maritime(ship: T_ship) -> T_ship:
+ """
+ Convert ship data which is given in SI units to maritime units.
+
+ Params:
+ * ship: Ship data
+
+ Returns
+ -------
+ * ship: Converted ship data
+ """
+ assert ship.initial is not None
+ ship.initial.position.longitude = round(rad_2_deg(ship.initial.position.longitude), 8)
+ ship.initial.position.latitude = round(rad_2_deg(ship.initial.position.latitude), 8)
+ ship.initial.cog = round(rad_2_deg(ship.initial.cog), 2)
+ ship.initial.sog = round(m_pr_s_2_knot(ship.initial.sog), 1)
+ ship.initial.heading = round(rad_2_deg(ship.initial.heading), 2)
+
+ if ship.waypoints is not None:
+ for waypoint in ship.waypoints:
+ waypoint.position.latitude = round(rad_2_deg(waypoint.position.latitude), 8)
+ waypoint.position.longitude = round(rad_2_deg(waypoint.position.longitude), 8)
+ if not waypoint.data:
+ continue
+ assert waypoint.data.model_extra
+ if waypoint.data.model_extra.get("sog") is not None:
+ waypoint.data.model_extra["sog"]["value"] = round(m_pr_s_2_knot(waypoint.data.model_extra["sog"]["value"]), 1) # type: ignore
+ if waypoint.data.model_extra.get("heading") is not None:
+ waypoint.data.model_extra["heading"]["value"] = round(m_pr_s_2_knot(waypoint.data.model_extra["heading"]["value"]), 2) # type: ignore
+
+ return ship
+
+
' + + '' + + _("Hide Search Matches") + + "
" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/api.html b/api.html new file mode 100644 index 0000000..7bd54bd --- /dev/null +++ b/api.html @@ -0,0 +1,220 @@ + + + + + + +assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
+"""Module with helper functions to determine if a generated path is crossing land."""
+
+from global_land_mask import globe
+from maritime_schema.types.caga import Position
+
+from trafficgen.utils import calculate_position_at_certain_time, rad_2_deg
+
+
+
+[docs]
+def path_crosses_land(
+ position_1: Position,
+ speed: float,
+ course: float,
+ lat_lon0: Position,
+ time_interval: float = 300.0,
+) -> bool:
+ """
+ Find if path is crossing land.
+
+ Params:
+ position_1: Ship position in latitude/longitude [rad].
+ speed: Ship speed [m/s].
+ course: Ship course [rad].
+ lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad].
+ time_interval: The time interval the vessel should travel without crossing land [sec]
+
+ Returns
+ -------
+ is_on_land: True if parts of the path crosses land.
+ """
+
+ num_checks = 10
+ for i in range(int(time_interval / num_checks)):
+ position_2 = calculate_position_at_certain_time(
+ Position(latitude=position_1.latitude, longitude=position_1.longitude),
+ lat_lon0,
+ speed,
+ course,
+ i * time_interval / num_checks,
+ )
+
+ lat = rad_2_deg(position_2.latitude)
+ lon = rad_2_deg(position_2.longitude)
+ if globe.is_land(lat, lon): # type: ignore (The package is unfortunately not typed.)
+ return True
+ return False
+
+
+"""
+Functions to generate encounters consisting of one own ship and one to many target ships.
+The generated encounters may be of type head-on, overtaking give-way and stand-on and
+crossing give-way and stand-on.
+"""
+
+import random
+from typing import List, Optional, Tuple, Union
+from uuid import uuid4
+
+import numpy as np
+from maritime_schema.types.caga import (
+ AISNavStatus,
+ Initial,
+ OwnShip,
+ Position,
+ ShipStatic,
+ TargetShip,
+ Waypoint,
+)
+
+from trafficgen.check_land_crossing import path_crosses_land
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+from trafficgen.types import (
+ EncounterRelativeSpeed,
+ EncounterSettings,
+ EncounterType,
+ SituationInput,
+)
+from trafficgen.utils import (
+ calculate_bearing_between_waypoints,
+ calculate_position_along_track_using_waypoints,
+ calculate_position_at_certain_time,
+ convert_angle_0_to_2_pi_to_minus_pi_to_pi,
+ convert_angle_minus_pi_to_pi_to_0_to_2_pi,
+)
+
+
+
+[docs]
+def generate_encounter(
+ desired_encounter_type: EncounterType,
+ own_ship: OwnShip,
+ target_ships_static: List[ShipStatic],
+ encounter_number: int,
+ beta_default: Optional[Union[List[float], float]],
+ relative_sog_default: Optional[float],
+ vector_time_default: Optional[float],
+ settings: EncounterSettings,
+) -> Tuple[TargetShip, bool]:
+ """
+ Generate an encounter.
+
+ Params:
+ * desired_encounter_type: Desired encounter to be generated
+ * own_ship: Dict, information about own ship that will encounter a target ship
+ * target_ships_static: List of target ships including static information that
+ may be used in an encounter
+ * encounter_number: Integer, used to naming the target ships. target_ship_1,2 etc.
+ * beta_default: User defined beta. If not set, this is None.
+ * relative_sog_default: User defined relative sog between own ship and
+ target ship. If not set, this is None.
+ * vector_time_default: User defined vector time. If not set, this is None.
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * target_ship: target ship information, such as initial position, sog and cog
+ * encounter_found: True=encounter found, False=encounter not found
+ """
+ encounter_found: bool = False
+ outer_counter: int = 0
+
+ # Initiating some variables which later will be set if an encounter is found
+ assert own_ship.initial is not None
+ target_ship_initial_position: Position = own_ship.initial.position
+ target_ship_sog: float = 0
+ target_ship_cog: float = 0
+
+ # Initial posision of own ship used as reference point for lat_lon0
+ lat_lon0: Position = Position(
+ latitude=own_ship.initial.position.latitude,
+ longitude=own_ship.initial.position.longitude,
+ )
+
+ target_ship_static: ShipStatic = decide_target_ship(target_ships_static)
+ assert target_ship_static is not None
+
+ # Searching for encounter. Two loops used. Only vector time is locked in the
+ # first loop. In the second loop, beta and sog are assigned.
+ while not encounter_found and outer_counter < 5:
+ outer_counter += 1
+ inner_counter: int = 0
+
+ # resetting vector_time, beta and relative_sog to default values before
+ # new search for situation is done
+ vector_time: Union[float, None] = vector_time_default
+
+ if vector_time is None:
+ vector_time = random.uniform(settings.vector_range[0], settings.vector_range[1])
+ if beta_default is None:
+ beta: float = assign_beta(desired_encounter_type, settings)
+ elif isinstance(beta_default, List):
+ beta: float = assign_beta_from_list(beta_default)
+ else:
+ beta: float = beta_default
+
+ # Own ship
+ assert own_ship.initial is not None
+ assert own_ship.waypoints is not None
+ # Assuming ship is pointing in the direction of wp1
+ own_ship_cog = calculate_bearing_between_waypoints(
+ own_ship.waypoints[0].position, own_ship.waypoints[1].position
+ )
+ own_ship_position_future = calculate_position_along_track_using_waypoints(
+ own_ship.waypoints,
+ own_ship.initial.sog,
+ vector_time,
+ )
+
+ # Target ship
+ target_ship_position_future: Position = assign_future_position_to_target_ship(
+ own_ship_position_future, lat_lon0, settings.max_meeting_distance
+ )
+
+ while not encounter_found and inner_counter < 5:
+ inner_counter += 1
+ relative_sog = relative_sog_default
+ if relative_sog is None:
+ min_target_ship_sog = (
+ calculate_min_vector_length_target_ship(
+ own_ship.initial.position,
+ own_ship_cog,
+ target_ship_position_future,
+ beta,
+ lat_lon0,
+ )
+ / vector_time
+ )
+
+ target_ship_sog: float = assign_sog_to_target_ship(
+ desired_encounter_type,
+ own_ship.initial.sog,
+ min_target_ship_sog,
+ settings.relative_speed,
+ )
+ else:
+ target_ship_sog: float = relative_sog * own_ship.initial.sog
+
+ assert target_ship_static.speed_max is not None
+ target_ship_sog = round(np.minimum(target_ship_sog, target_ship_static.speed_max), 1)
+
+ target_ship_vector_length = target_ship_sog * vector_time
+ start_position_target_ship, position_found = find_start_position_target_ship(
+ own_ship.initial.position,
+ lat_lon0,
+ own_ship_cog,
+ target_ship_position_future,
+ target_ship_vector_length,
+ beta,
+ desired_encounter_type,
+ settings,
+ )
+
+ if position_found:
+ target_ship_initial_position: Position = start_position_target_ship
+ target_ship_cog: float = calculate_ship_cog(
+ target_ship_initial_position, target_ship_position_future, lat_lon0
+ )
+ encounter_ok: bool = check_encounter_evolvement(
+ own_ship,
+ own_ship_cog,
+ own_ship.initial.position,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ target_ship_position_future,
+ desired_encounter_type,
+ settings,
+ )
+
+ if settings.disable_land_check is False:
+ # Check if trajectory passes land
+ trajectory_on_land = path_crosses_land(
+ target_ship_initial_position,
+ target_ship_sog,
+ target_ship_cog,
+ lat_lon0,
+ settings.situation_length,
+ )
+ encounter_found = encounter_ok and not trajectory_on_land
+ else:
+ encounter_found = encounter_ok
+
+ if encounter_found:
+ target_ship_static.id = uuid4()
+ target_ship_static.name = f"target_ship_{encounter_number}"
+ target_ship_initial: Initial = Initial(
+ position=target_ship_initial_position,
+ sog=target_ship_sog,
+ cog=target_ship_cog,
+ heading=target_ship_cog,
+ nav_status=AISNavStatus.UNDER_WAY_USING_ENGINE,
+ )
+ target_ship_waypoint0 = Waypoint(
+ position=target_ship_initial_position.model_copy(deep=True), turn_radius=None, data=None
+ )
+
+ future_position_target_ship = calculate_position_at_certain_time(
+ target_ship_initial_position,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ settings.situation_length,
+ )
+
+ target_ship_waypoint1 = Waypoint(
+ position=future_position_target_ship, turn_radius=None, data=None
+ )
+ waypoints = [target_ship_waypoint0, target_ship_waypoint1]
+
+ target_ship = TargetShip(
+ static=target_ship_static, initial=target_ship_initial, waypoints=waypoints
+ )
+ else:
+ # Since encounter is not found, using initial values from own ship. Will not be taken into use.
+ target_ship = TargetShip(static=target_ship_static, initial=own_ship.initial, waypoints=None)
+ return target_ship, encounter_found
+
+
+
+
+[docs]
+def check_encounter_evolvement(
+ own_ship: OwnShip,
+ own_ship_cog: float,
+ own_ship_position_future: Position,
+ lat_lon0: Position,
+ target_ship_sog: float,
+ target_ship_cog: float,
+ target_ship_position_future: Position,
+ desired_encounter_type: EncounterType,
+ settings: EncounterSettings,
+) -> bool:
+ """
+ Check encounter evolvement. The generated encounter should be the same type of
+ encounter (head-on, crossing, give-way) also some time before the encounter is started.
+
+ Params:
+ * own_ship: Own ship information such as initial position, sog and cog
+ * target_ship: Target ship information such as initial position, sog and cog
+ * desired_encounter_type: Desired type of encounter to be generated
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * returns True if encounter ok, False if encounter not ok
+ """
+ theta13_criteria: float = settings.classification.theta13_criteria
+ theta14_criteria: float = settings.classification.theta14_criteria
+ theta15_criteria: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ assert own_ship.initial is not None
+
+ own_ship_sog: float = own_ship.initial.sog
+ evolve_time: float = settings.evolve_time
+
+ # Calculating position back in time to ensure that the encounter do not change from one type
+ # to another before the encounter is started
+ encounter_preposition_target_ship = calculate_position_at_certain_time(
+ target_ship_position_future,
+ lat_lon0,
+ target_ship_sog,
+ target_ship_cog,
+ -evolve_time,
+ )
+ encounter_preposition_own_ship = calculate_position_at_certain_time(
+ own_ship_position_future,
+ lat_lon0,
+ own_ship_sog,
+ own_ship_cog,
+ -evolve_time,
+ )
+ pre_beta, pre_alpha = calculate_relative_bearing(
+ encounter_preposition_own_ship,
+ own_ship_cog,
+ encounter_preposition_target_ship,
+ target_ship_cog,
+ lat_lon0,
+ )
+
+ pre_colreg_state = determine_colreg(
+ pre_alpha, pre_beta, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ encounter_ok: bool = pre_colreg_state == desired_encounter_type
+
+ return encounter_ok
+
+
+
+
+[docs]
+def define_own_ship(
+ desired_traffic_situation: SituationInput,
+ own_ship_static: ShipStatic,
+ encounter_settings: EncounterSettings,
+ lat_lon0: Position,
+) -> OwnShip:
+ """
+ Define own ship based on information in desired traffic situation.
+
+ Params:
+ * desired_traffic_situation: Information about type of traffic situation to generate
+ * own_ship_static: Static information of own ship.
+ * encounter_settings: Necessary setting for the encounter
+ * lat_lon0: Reference position [deg]
+
+ Returns
+ -------
+ * own_ship: Own ship
+ """
+ own_ship_initial: Initial = desired_traffic_situation.own_ship.initial
+ if desired_traffic_situation.own_ship.waypoints is None:
+ # If waypoints are not given, let initial position be the first waypoint,
+ # then calculate second waypoint some time in the future
+ own_ship_waypoint0 = Waypoint(
+ position=own_ship_initial.position.model_copy(deep=True), turn_radius=None, data=None
+ )
+ ship_position_future = calculate_position_at_certain_time(
+ own_ship_initial.position,
+ lat_lon0,
+ own_ship_initial.sog,
+ own_ship_initial.cog,
+ encounter_settings.situation_length,
+ )
+ own_ship_waypoint1 = Waypoint(position=ship_position_future, turn_radius=None, data=None)
+ own_ship_waypoints: List[Waypoint] = [own_ship_waypoint0, own_ship_waypoint1]
+ elif len(desired_traffic_situation.own_ship.waypoints) == 1:
+ # If one waypoint is given, use initial position as first waypoint
+ own_ship_waypoint0 = Waypoint(
+ position=own_ship_initial.position.model_copy(deep=True), turn_radius=None, data=None
+ )
+ own_ship_waypoint1 = desired_traffic_situation.own_ship.waypoints[0]
+ own_ship_waypoints: List[Waypoint] = [own_ship_waypoint0, own_ship_waypoint1]
+ else:
+ own_ship_waypoints: List[Waypoint] = desired_traffic_situation.own_ship.waypoints
+
+ own_ship = OwnShip(
+ static=own_ship_static,
+ initial=own_ship_initial,
+ waypoints=own_ship_waypoints,
+ )
+
+ return own_ship
+
+
+
+
+[docs]
+def calculate_min_vector_length_target_ship(
+ own_ship_position: Position,
+ own_ship_cog: float,
+ target_ship_position_future: Position,
+ desired_beta: float,
+ lat_lon0: Position,
+) -> float:
+ """
+ Calculate minimum vector length (target ship sog x vector). This will
+ ensure that ship sog is high enough to find proper situation.
+
+ Params:
+ * own_ship_position: Own ship initial position, latitudinal [rad] and longitudinal [rad]
+ * own_ship_cog: Own ship initial cog
+ * target_ship_position_future: Target ship future position
+ * desired_beta: Desired relative bearing between
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * min_vector_length: Minimum vector length (target ship sog x vector)
+ """
+ psi: float = own_ship_cog + desired_beta
+
+ own_ship_position_north, own_ship_position_east, _ = llh2flat(
+ own_ship_position.latitude, own_ship_position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ target_ship_position_future_north, target_ship_position_future_east, _ = llh2flat(
+ target_ship_position_future.latitude,
+ target_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+
+ p_1 = np.array([own_ship_position_north, own_ship_position_east])
+ p_2 = np.array([own_ship_position_north + np.cos(psi), own_ship_position_east + np.sin(psi)])
+ p_3 = np.array([target_ship_position_future_north, target_ship_position_future_east])
+
+ min_vector_length: float = float(np.abs(np.cross(p_2 - p_1, p_3 - p_1) / np.linalg.norm(p_2 - p_1)))
+
+ return min_vector_length
+
+
+
+
+[docs]
+def find_start_position_target_ship(
+ own_ship_position: Position,
+ lat_lon0: Position,
+ own_ship_cog: float,
+ target_ship_position_future: Position,
+ target_ship_vector_length: float,
+ desired_beta: float,
+ desired_encounter_type: EncounterType,
+ settings: EncounterSettings,
+) -> Tuple[Position, bool]:
+ """
+ Find start position of target ship using desired beta and vector length.
+
+ Params:
+ * own_ship_position: Own ship initial position, sog and cog
+ * own_ship_cog: Own ship initial cog
+ * target_ship_position_future: Target ship future position
+ * target_ship_vector_length: vector length (target ship sog x vector)
+ * desired_beta: Desired bearing between own ship and target ship seen from own ship
+ * desired_encounter_type: Desired type of encounter to be generated
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * start_position_target_ship: Dict, initial position of target ship {north, east} [m]
+ * start_position_found: 0=position not found, 1=position found
+ """
+ theta13_criteria: float = settings.classification.theta13_criteria
+ theta14_criteria: float = settings.classification.theta14_criteria
+ theta15_criteria: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ n_1, e_1, _ = llh2flat(
+ own_ship_position.latitude, own_ship_position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ n_2, e_2, _ = llh2flat(
+ target_ship_position_future.latitude,
+ target_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ v_r: float = target_ship_vector_length
+ psi: float = own_ship_cog + desired_beta
+
+ n_4: float = n_1 + np.cos(psi)
+ e_4: float = e_1 + np.sin(psi)
+
+ b: float = (
+ -2 * e_2 * e_4
+ - 2 * n_2 * n_4
+ + 2 * e_1 * e_2
+ + 2 * n_1 * n_2
+ + 2 * e_1 * (e_4 - e_1)
+ + 2 * n_1 * (n_4 - n_1)
+ )
+ a: float = (e_4 - e_1) ** 2 + (n_4 - n_1) ** 2
+ c: float = e_2**2 + n_2**2 - 2 * e_1 * e_2 - 2 * n_1 * n_2 - v_r**2 + e_1**2 + n_1**2
+
+ # Assign conservative fallback values to return variables
+ start_position_found: bool = False
+ start_position_target_ship = target_ship_position_future.model_copy(deep=True)
+
+ if b**2 - 4 * a * c <= 0.0:
+ # Do not run calculation of target ship start position. Return fallback values.
+ return start_position_target_ship, start_position_found
+
+ # Calculation of target ship start position
+ s_1 = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a)
+ s_2 = (-b - np.sqrt(b**2 - 4 * a * c)) / (2 * a)
+
+ e_31 = round(e_1 + s_1 * (e_4 - e_1), 0)
+ n_31 = round(n_1 + s_1 * (n_4 - n_1), 0)
+ e_32 = round(e_1 + s_2 * (e_4 - e_1), 0)
+ n_32 = round(n_1 + s_2 * (n_4 - n_1), 0)
+
+ lat31, lon31, _ = flat2llh(n_31, e_31, lat_lon0.latitude, lat_lon0.longitude)
+ target_ship_cog_1: float = calculate_ship_cog(
+ pos_0=Position(latitude=lat31, longitude=lon31),
+ pos_1=target_ship_position_future,
+ lat_lon0=lat_lon0,
+ )
+ beta1, alpha1 = calculate_relative_bearing(
+ position_own_ship=own_ship_position,
+ heading_own_ship=own_ship_cog,
+ position_target_ship=Position(latitude=lat31, longitude=lon31),
+ heading_target_ship=target_ship_cog_1,
+ lat_lon0=lat_lon0,
+ )
+ colreg_state1: EncounterType = determine_colreg(
+ alpha1, beta1, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ lat32, lon32, _ = flat2llh(n_32, e_32, lat_lon0.latitude, lat_lon0.longitude)
+ target_ship_cog_2 = calculate_ship_cog(
+ pos_0=Position(latitude=lat32, longitude=lon32),
+ pos_1=target_ship_position_future,
+ lat_lon0=lat_lon0,
+ )
+ beta2, alpha2 = calculate_relative_bearing(
+ position_own_ship=own_ship_position,
+ heading_own_ship=own_ship_cog,
+ position_target_ship=Position(latitude=lat32, longitude=lon32),
+ heading_target_ship=target_ship_cog_2,
+ lat_lon0=lat_lon0,
+ )
+ colreg_state2: EncounterType = determine_colreg(
+ alpha2, beta2, theta13_criteria, theta14_criteria, theta15_criteria, theta15
+ )
+
+ if (
+ desired_encounter_type is colreg_state1
+ and np.abs(convert_angle_0_to_2_pi_to_minus_pi_to_pi(np.abs(beta1 - desired_beta))) < 0.01
+ ):
+ start_position_target_ship = Position(latitude=lat31, longitude=lon31)
+ start_position_found = True
+ elif (
+ desired_encounter_type is colreg_state2
+ and np.abs(convert_angle_0_to_2_pi_to_minus_pi_to_pi(np.abs(beta2 - desired_beta))) < 0.01
+ ):
+ start_position_target_ship = Position(latitude=lat32, longitude=lon32)
+ start_position_found = True
+
+ return start_position_target_ship, start_position_found
+
+
+
+
+[docs]
+def assign_future_position_to_target_ship(
+ own_ship_position_future: Position,
+ lat_lon0: Position,
+ max_meeting_distance: float,
+) -> Position:
+ """
+ Randomly assign future position of target ship. If drawing a circle with radius
+ max_meeting_distance around future position of own ship, future position of
+ target ship shall be somewhere inside this circle.
+
+ Params:
+ * own_ship_position_future: Dict, own ship position at a given time in the
+ future, {north, east}
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+ * max_meeting_distance: Maximum distance between own ship and target ship at
+ a given time in the future [m]
+
+ Returns
+ -------
+ future_position_target_ship: Future position of target ship {north, east} [m]
+ """
+ random_angle = random.uniform(0, 1) * 2 * np.pi
+ random_distance = random.uniform(0, 1) * max_meeting_distance
+
+ own_ship_position_future_north, own_ship_position_future_east, _ = llh2flat(
+ own_ship_position_future.latitude,
+ own_ship_position_future.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ north: float = own_ship_position_future_north + random_distance * np.cos(random_angle)
+ east: float = own_ship_position_future_east + random_distance * np.sin(random_angle)
+ latitude, longitude, _ = flat2llh(north, east, lat_lon0.latitude, lat_lon0.longitude)
+ return Position(latitude=latitude, longitude=longitude)
+
+
+
+
+[docs]
+def determine_colreg(
+ alpha: float,
+ beta: float,
+ theta13_criteria: float,
+ theta14_criteria: float,
+ theta15_criteria: float,
+ theta15: List[float],
+) -> EncounterType:
+ """
+ Determine the colreg type based on alpha, relative bearing between target ship and own
+ ship seen from target ship, and beta, relative bearing between own ship and target ship
+ seen from own ship.
+
+ Params:
+ * alpha: relative bearing between target ship and own ship seen from target ship
+ * beta: relative bearing between own ship and target ship seen from own ship
+ * theta13_criteria: Tolerance for "coming up with" relative bearing
+ * theta14_criteria: Tolerance for "reciprocal or nearly reciprocal cogs",
+ "when in any doubt... assume... [head-on]"
+ * theta15_criteria: Crossing aspect limit, used for classifying a crossing encounter
+ * theta15: 22.5 deg aft of the beam, used for classifying a crossing and an overtaking
+ encounter
+
+ Returns
+ -------
+ * encounter classification
+ """
+ # Mapping
+ alpha_2_pi: float = alpha if alpha >= 0.0 else alpha + 2 * np.pi
+ beta_pi: float = beta if (beta >= 0.0) & (beta <= np.pi) else beta - 2 * np.pi
+
+ # Find appropriate rule set
+ if (beta > theta15[0]) & (beta < theta15[1]) & (abs(alpha) - theta13_criteria <= 0.001):
+ return EncounterType.OVERTAKING_STAND_ON
+ if (
+ (alpha_2_pi > theta15[0])
+ & (alpha_2_pi < theta15[1])
+ & (abs(beta_pi) - theta13_criteria <= 0.001)
+ ):
+ return EncounterType.OVERTAKING_GIVE_WAY
+ if (abs(beta_pi) - theta14_criteria <= 0.001) & (abs(alpha) - theta14_criteria <= 0.001):
+ return EncounterType.HEAD_ON
+ if (beta > 0) & (beta < theta15[0]) & (alpha > -theta15[0]) & (alpha - theta15_criteria <= 0.001):
+ return EncounterType.CROSSING_GIVE_WAY
+ if (
+ (alpha_2_pi > 0)
+ & (alpha_2_pi < theta15[0])
+ & (beta_pi > -theta15[0])
+ & (beta_pi - theta15_criteria <= 0.001)
+ ):
+ return EncounterType.CROSSING_STAND_ON
+ return EncounterType.NO_RISK_COLLISION
+
+
+
+
+[docs]
+def calculate_relative_bearing(
+ position_own_ship: Position,
+ heading_own_ship: float,
+ position_target_ship: Position,
+ heading_target_ship: float,
+ lat_lon0: Position,
+) -> Tuple[float, float]:
+ """
+ Calculate relative bearing between own ship and target ship, both seen from
+ own ship and seen from target ship.
+
+ Params:
+ * position_own_ship: Own ship position {latitude, longitude} [rad]
+ * heading_own_ship: Own ship heading [rad]
+ * position_target_ship: Target ship position {latitude, longitude} [rad]
+ * heading_target_ship: Target ship heading [rad]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * beta: relative bearing between own ship and target ship seen from own ship [rad]
+ * alpha: relative bearing between target ship and own ship seen from target ship [rad]
+ """
+ # POSE combination of relative bearing and contact angle
+ n_own_ship, e_own_ship, _ = llh2flat(
+ position_own_ship.latitude, position_own_ship.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+ n_target_ship, e_target_ship, _ = llh2flat(
+ position_target_ship.latitude,
+ position_target_ship.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+
+ # Absolute bearing of target ship relative to own ship
+ bng_own_ship_target_ship: float = 0.0
+ if e_own_ship == e_target_ship:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 0.0
+ else:
+ bng_own_ship_target_ship = np.pi
+ else:
+ if e_own_ship < e_target_ship:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 1 / 2 * np.pi - np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ bng_own_ship_target_ship = 1 / 2 * np.pi + np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ if n_own_ship <= n_target_ship:
+ bng_own_ship_target_ship = 3 / 2 * np.pi + np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+ else:
+ bng_own_ship_target_ship = 3 / 2 * np.pi - np.arctan(
+ abs(n_target_ship - n_own_ship) / abs(e_target_ship - e_own_ship)
+ )
+
+ # Bearing of own ship from the perspective of the contact
+ bng_target_ship_own_ship: float = bng_own_ship_target_ship + np.pi
+
+ # Relative bearing of contact ship relative to own ship
+ beta: float = bng_own_ship_target_ship - heading_own_ship
+ while beta < 0:
+ beta += 2 * np.pi
+ while beta >= 2 * np.pi:
+ beta -= 2 * np.pi
+
+ # Relative bearing of own ship relative to target ship
+ alpha: float = bng_target_ship_own_ship - heading_target_ship
+ while alpha < -np.pi:
+ alpha += 2 * np.pi
+ while alpha >= np.pi:
+ alpha -= 2 * np.pi
+
+ return beta, alpha
+
+
+
+
+[docs]
+def calculate_ship_cog(pos_0: Position, pos_1: Position, lat_lon0: Position) -> float:
+ """
+ Calculate ship cog between two waypoints.
+
+ Params:
+ * waypoint_0: Dict, waypoint {latitude, longitude} [rad]
+ * waypoint_1: Dict, waypoint {latitude, longitude} [rad]
+
+ Returns
+ -------
+ * cog: Ship cog [rad]
+ """
+ n_0, e_0, _ = llh2flat(pos_0.latitude, pos_0.longitude, lat_lon0.latitude, lat_lon0.longitude)
+ n_1, e_1, _ = llh2flat(pos_1.latitude, pos_1.longitude, lat_lon0.latitude, lat_lon0.longitude)
+
+ cog: float = np.arctan2(e_1 - e_0, n_1 - n_0)
+ if cog < 0.0:
+ cog += 2 * np.pi
+ return round(cog, 3)
+
+
+
+
+[docs]
+def assign_vector_time(vector_time_range: List[float]):
+ """
+ Assign random (uniform) vector time.
+
+ Params:
+ * vector_range: Minimum and maximum value for vector time
+
+ Returns
+ -------
+ * vector_time: Vector time [min]
+ """
+ vector_time: float = vector_time_range[0] + random.uniform(0, 1) * (
+ vector_time_range[1] - vector_time_range[0]
+ )
+ return vector_time
+
+
+
+
+[docs]
+def assign_sog_to_target_ship(
+ encounter_type: EncounterType,
+ own_ship_sog: float,
+ min_target_ship_sog: float,
+ relative_sog_setting: EncounterRelativeSpeed,
+):
+ """
+ Assign random (uniform) sog to target ship depending on type of encounter.
+
+ Params:
+ * encounter_type: Type of encounter
+ * own_ship_sog: Own ship sog [m/s]
+ * min_target_ship_sog: Minimum target ship sog [m/s]
+ * relative_sog_setting: Relative sog setting dependent on encounter [-]
+
+ Returns
+ -------
+ * target_ship_sog: Target ship sog [m/s]
+ """
+ if encounter_type is EncounterType.OVERTAKING_STAND_ON:
+ relative_sog = relative_sog_setting.overtaking_stand_on
+ elif encounter_type is EncounterType.OVERTAKING_GIVE_WAY:
+ relative_sog = relative_sog_setting.overtaking_give_way
+ elif encounter_type is EncounterType.HEAD_ON:
+ relative_sog = relative_sog_setting.head_on
+ elif encounter_type is EncounterType.CROSSING_GIVE_WAY:
+ relative_sog = relative_sog_setting.crossing_give_way
+ elif encounter_type is EncounterType.CROSSING_STAND_ON:
+ relative_sog = relative_sog_setting.crossing_stand_on
+ else:
+ relative_sog = [0.0, 0.0]
+
+ # Check that minimum target ship sog is in the relative sog range
+ if (
+ min_target_ship_sog / own_ship_sog > relative_sog[0]
+ and min_target_ship_sog / own_ship_sog < relative_sog[1]
+ ):
+ relative_sog[0] = min_target_ship_sog / own_ship_sog
+
+ target_ship_sog: float = (
+ relative_sog[0] + random.uniform(0, 1) * (relative_sog[1] - relative_sog[0])
+ ) * own_ship_sog
+
+ return target_ship_sog
+
+
+
+
+[docs]
+def assign_beta_from_list(beta_limit: List[float]) -> float:
+ """
+ Assign random (uniform) relative bearing beta between own ship
+ and target ship depending between the limits given by beta_limit.
+
+ Params:
+ * beta_limit: Limits for beta
+
+ Returns
+ -------
+ * Relative bearing between own ship and target ship seen from own ship [rad]
+ """
+ assert len(beta_limit) == 2
+ beta: float = beta_limit[0] + random.uniform(0, 1) * (beta_limit[1] - beta_limit[0])
+ return beta
+
+
+
+
+[docs]
+def assign_beta(encounter_type: EncounterType, settings: EncounterSettings) -> float:
+ """
+ Assign random (uniform) relative bearing beta between own ship
+ and target ship depending on type of encounter.
+
+ Params:
+ * encounter_type: Type of encounter
+ * settings: Encounter settings
+
+ Returns
+ -------
+ * Relative bearing between own ship and target ship seen from own ship [rad]
+ """
+ theta13_crit: float = settings.classification.theta13_criteria
+ theta14_crit: float = settings.classification.theta14_criteria
+ theta15_crit: float = settings.classification.theta15_criteria
+ theta15: List[float] = settings.classification.theta15
+
+ if encounter_type is EncounterType.OVERTAKING_STAND_ON:
+ return theta15[0] + random.uniform(0, 1) * (theta15[1] - theta15[0])
+ if encounter_type is EncounterType.OVERTAKING_GIVE_WAY:
+ return -theta13_crit + random.uniform(0, 1) * (theta13_crit - (-theta13_crit))
+ if encounter_type is EncounterType.HEAD_ON:
+ return -theta14_crit + random.uniform(0, 1) * (theta14_crit - (-theta14_crit))
+ if encounter_type is EncounterType.CROSSING_GIVE_WAY:
+ return 0 + random.uniform(0, 1) * (theta15[0] - 0)
+ if encounter_type is EncounterType.CROSSING_STAND_ON:
+ return convert_angle_minus_pi_to_pi_to_0_to_2_pi(
+ -theta15[1] + random.uniform(0, 1) * (theta15[1] + theta15_crit)
+ )
+ return 0.0
+
+
+
+
+[docs]
+def decide_target_ship(target_ships_static: List[ShipStatic]) -> ShipStatic:
+ """
+ Randomly pick a target ship from a list of target ships.
+
+ Params:
+ * target_ships: list of target ships with static information
+
+ Returns
+ -------
+ * The target ship, info of type, size etc.
+ """
+ num_target_ships: int = len(target_ships_static)
+ target_ship_to_use: int = random.randint(1, num_target_ships)
+ target_ship_static: ShipStatic = target_ships_static[target_ship_to_use - 1]
+ return target_ship_static.model_copy(deep=True)
+
+
+"""
+The Marine Systems Simulator (MSS) is a Matlab and Simulink library for marine systems.
+
+It includes models for ships, underwater vehicles, unmanned surface vehicles, and floating structures.
+The library also contains guidance, navigation, and control (GNC) blocks for real-time simulation.
+The algorithms are described in:
+
+T. I. Fossen (2021). Handbook of Marine Craft Hydrodynamics and Motion Control. 2nd. Edition,
+Wiley. ISBN-13: 978-1119575054
+
+Parts of the library have been re-implemented in Python and are found below.
+"""
+
+from typing import Tuple
+
+import numpy as np
+
+
+
+[docs]
+def flat2llh(
+ x_n: float,
+ y_n: float,
+ lat_0: float,
+ lon_0: float,
+ z_n: float = 0.0,
+ height_ref: float = 0.0,
+) -> Tuple[float, float, float]:
+ """
+ Compute longitude lon (rad), latitude lat (rad) and height h (m) for the
+ NED coordinates (xn,yn,zn).
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ The method computes longitude lon (rad), latitude lat (rad) and height h (m) for the
+ NED coordinates (xn,yn,zn) using a flat Earth coordinate system defined by the WGS-84
+ ellipsoid. The flat Earth coordinate origin is located at (lon_0, lat_0) with reference
+ height h_ref in meters above the surface of the ellipsoid. Both height and h_ref
+ are positive upwards, while zn is positive downwards (NED).
+ Author: Thor I. Fossen
+ Date: 20 July 2018
+ Revisions: 2023-02-04 updates the formulas for latitude and longitude
+
+ Params:
+ * xn: Ship position, north [m]
+ * yn: Ship position, east [m]
+ * zn=0.0: Ship position, down [m]
+ * lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
+ * h_ref=0.0: Flat earth coordinate with reference h_ref in meters above the surface
+ of the ellipsoid
+
+ Returns
+ -------
+ * lat: Latitude [rad]
+ * lon: Longitude [rad]
+ * h: Height [m]
+
+ """
+ # WGS-84 parameters
+ a_radius = 6378137 # Semi-major axis
+ f_factor = 1 / 298.257223563 # Flattening
+ e_eccentricity = np.sqrt(2 * f_factor - f_factor**2) # Earth eccentricity
+
+ r_n = a_radius / np.sqrt(1 - e_eccentricity**2 * np.sin(lat_0) ** 2)
+ r_m = r_n * ((1 - e_eccentricity**2) / (1 - e_eccentricity**2 * np.sin(lat_0) ** 2))
+
+ d_lat = x_n / (r_m + height_ref) # delta latitude dmu = mu - mu0
+ d_lon = y_n / ((r_n + height_ref) * np.cos(lat_0)) # delta longitude dl = l - l0
+
+ lat = ssa(lat_0 + d_lat)
+ lon = ssa(lon_0 + d_lon)
+ height = height_ref - z_n
+
+ return lat, lon, height
+
+
+
+
+[docs]
+def llh2flat(
+ lat: float,
+ lon: float,
+ lat_0: float,
+ lon_0: float,
+ height: float = 0.0,
+ height_ref: float = 0.0,
+) -> Tuple[float, float, float]:
+ """
+ Compute (north, east) for a flat Earth coordinate system from longitude
+ lon (rad) and latitude lat (rad).
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ The method computes (north, east) for a flat Earth coordinate system from longitude
+ lon (rad) and latitude lat (rad) of the WGS-84 elipsoid. The flat Earth coordinate
+ origin is located at (lon_0, lat_0).
+ Author: Thor I. Fossen
+ Date: 20 July 2018
+ Revisions: 2023-02-04 updates the formulas for latitude and longitude
+
+ Params:
+ * lat: Ship position in latitude [rad]
+ * lon: Ship position in longitude [rad]
+ * h=0.0: Ship height in meters above the surface of the ellipsoid
+ * lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
+ * h_ref=0.0: Flat earth coordinate with reference h_ref in meters above
+ the surface of the ellipsoid
+
+ Returns
+ -------
+ * x_n: Ship position, north [m]
+ * y_n: Ship position, east [m]
+ * z_n: Ship position, down [m]
+ """
+
+ # WGS-84 parameters
+ a_radius = 6378137 # Semi-major axis (equitorial radius)
+ f_factor = 1 / 298.257223563 # Flattening
+ e_eccentricity = np.sqrt(2 * f_factor - f_factor**2) # Earth eccentricity
+
+ d_lon = lon - lon_0
+ d_lat = lat - lat_0
+
+ r_n = a_radius / np.sqrt(1 - e_eccentricity**2 * np.sin(lat_0) ** 2)
+ r_m = r_n * ((1 - e_eccentricity**2) / (1 - e_eccentricity**2 * np.sin(lat_0) ** 2))
+
+ x_n = d_lat * (r_m + height_ref)
+ y_n = d_lon * ((r_n + height_ref) * np.cos(lat_0))
+ z_n = height_ref - height
+
+ return x_n, y_n, z_n
+
+
+
+
+[docs]
+def ssa(angle: float) -> float:
+ """
+ Return the "smallest signed angle" (SSA) or the smallest difference between two angles.
+
+ Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink
+ library for marine systems.
+
+ Examples
+ --------
+ angle = ssa(angle) maps an angle in rad to the interval [-pi pi)
+
+ Author: Thor I. Fossen
+ Date: 2018-09-21
+
+ Param:
+ * angle: angle given in radius
+
+ Returns
+ -------
+ * smallest_angle: "smallest signed angle" or the smallest difference between two angles
+ """
+
+ return np.mod(angle + np.pi, 2 * np.pi) - np.pi
+
+
+# The matplotlib package is unfortunately not fully typed. Hence the following pyright exemption.
+# pyright: reportUnknownMemberType=false
+"""Functions to prepare and plot traffic situations."""
+import math
+from typing import List, Optional, Tuple, Union
+
+import matplotlib.pyplot as plt
+import numpy as np
+from folium import Map, Polygon
+from maritime_schema.types.caga import Position, Ship, TargetShip, TrafficSituation
+from matplotlib.axes import Axes as Axes
+from matplotlib.patches import Circle
+
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+from trafficgen.types import EncounterSettings
+from trafficgen.utils import m_2_nm, rad_2_deg
+
+
+
+[docs]
+def calculate_vector_arrow(
+ position: Position,
+ direction: float,
+ vector_length: float,
+ lat_lon0: Position,
+) -> List[Tuple[float, float]]:
+ """
+ Calculate the arrow with length vector pointing in the direction of ship course.
+
+ Params:
+ * position: {latitude}, {longitude} position of the ship [rad]
+ * direction: direction the arrow is pointing [rad]
+ * vector_length: length of vector [m]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * arrow_points: Polygon points to draw the arrow [deg]
+ """
+ north_start, east_start, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ side_length = vector_length / 10
+ sides_angle = 25
+
+ north_end = north_start + vector_length * np.cos(direction)
+ east_end = east_start + vector_length * np.sin(direction)
+
+ north_arrow_side_1 = north_end + side_length * np.cos(direction + np.pi - sides_angle)
+ east_arrow_side_1 = east_end + side_length * np.sin(direction + np.pi - sides_angle)
+ north_arrow_side_2 = north_end + side_length * np.cos(direction + np.pi + sides_angle)
+ east_arrow_side_2 = east_end + side_length * np.sin(direction + np.pi + sides_angle)
+
+ lat_start, lon_start, _ = flat2llh(north_start, east_start, lat_lon0.latitude, lat_lon0.longitude)
+ lat_end, lon_end, _ = flat2llh(north_end, east_end, lat_lon0.latitude, lat_lon0.longitude)
+ lat_arrow_side_1, lon_arrow_side_1, _ = flat2llh(
+ north_arrow_side_1, east_arrow_side_1, lat_lon0.latitude, lat_lon0.longitude
+ )
+ lat_arrow_side_2, lon_arrow_side_2, _ = flat2llh(
+ north_arrow_side_2, east_arrow_side_2, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ point_1 = (rad_2_deg(lat_start), rad_2_deg(lon_start))
+ point_2 = (rad_2_deg(lat_end), rad_2_deg(lon_end))
+ point_3 = (rad_2_deg(lat_arrow_side_1), rad_2_deg(lon_arrow_side_1))
+ point_4 = (rad_2_deg(lat_arrow_side_2), rad_2_deg(lon_arrow_side_2))
+
+ return [point_1, point_2, point_3, point_4, point_2]
+
+
+
+
+[docs]
+def calculate_ship_outline(
+ position: Position,
+ course: float,
+ lat_lon0: Position,
+ ship_length: float = 100.0,
+ ship_width: float = 15.0,
+) -> List[Tuple[float, float]]:
+ """
+ Calculate the outline of the ship pointing in the direction of ship course.
+
+ Params:
+ * position: {latitude}, {longitude} position of the ship [rad]
+ * course: course of the ship [rad]
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+ * ship_length: Ship length. If not given, ship length is set to 100
+ * ship_width: Ship width. If not given, ship width is set to 15
+
+ Returns
+ -------
+ * ship_outline_points: Polygon points to draw the ship [deg]
+ """
+ north_start, east_start, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ # increase size for visualizing
+ ship_length *= 10
+ ship_width *= 10
+
+ north_pos1 = north_start + np.cos(course) * (-ship_length / 2) - np.sin(course) * ship_width / 2
+ east_pos1 = east_start + np.sin(course) * (-ship_length / 2) + np.cos(course) * ship_width / 2
+ lat_pos1, lon_pos1, _ = flat2llh(north_pos1, east_pos1, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos2 = (
+ north_start
+ + np.cos(course) * (ship_length / 2 - ship_length * 0.1)
+ - np.sin(course) * ship_width / 2
+ )
+ east_pos2 = (
+ east_start
+ + np.sin(course) * (ship_length / 2 - ship_length * 0.1)
+ + np.cos(course) * ship_width / 2
+ )
+ lat_pos2, lon_pos2, _ = flat2llh(north_pos2, east_pos2, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos3 = north_start + np.cos(course) * (ship_length / 2)
+ east_pos3 = east_start + np.sin(course) * (ship_length / 2)
+ lat_pos3, lon_pos3, _ = flat2llh(north_pos3, east_pos3, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos4 = (
+ north_start
+ + np.cos(course) * (ship_length / 2 - ship_length * 0.1)
+ - np.sin(course) * (-ship_width / 2)
+ )
+ east_pos4 = (
+ east_start
+ + np.sin(course) * (ship_length / 2 - ship_length * 0.1)
+ + np.cos(course) * (-ship_width / 2)
+ )
+ lat_pos4, lon_pos4, _ = flat2llh(north_pos4, east_pos4, lat_lon0.latitude, lat_lon0.longitude)
+
+ north_pos5 = north_start + np.cos(course) * (-ship_length / 2) - np.sin(course) * (-ship_width / 2)
+ east_pos5 = east_start + np.sin(course) * (-ship_length / 2) + np.cos(course) * (-ship_width / 2)
+ lat_pos5, lon_pos5, _ = flat2llh(north_pos5, east_pos5, lat_lon0.latitude, lat_lon0.longitude)
+
+ point_1 = (rad_2_deg(lat_pos1), rad_2_deg(lon_pos1))
+ point_2 = (rad_2_deg(lat_pos2), rad_2_deg(lon_pos2))
+ point_3 = (rad_2_deg(lat_pos3), rad_2_deg(lon_pos3))
+ point_4 = (rad_2_deg(lat_pos4), rad_2_deg(lon_pos4))
+ point_5 = (rad_2_deg(lat_pos5), rad_2_deg(lon_pos5))
+
+ return [point_1, point_2, point_3, point_4, point_5, point_1]
+
+
+
+
+[docs]
+def plot_specific_traffic_situation(
+ traffic_situations: List[TrafficSituation],
+ situation_number: int,
+ encounter_settings: EncounterSettings,
+):
+ """
+ Plot a specific situation in map.
+
+ Params:
+ * traffic_situations: Generated traffic situations
+ * situation_number: The specific situation to be plotted
+ """
+
+ num_situations = len(traffic_situations)
+ if situation_number > num_situations:
+ print(
+ f"Situation_number specified higher than number of situations available, plotting last situation: {num_situations}"
+ )
+ situation_number = num_situations
+
+ situation: TrafficSituation = traffic_situations[situation_number - 1]
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ assert encounter_settings.common_vector is not None
+
+ lat_lon0 = situation.own_ship.initial.position
+
+ map_plot = Map(location=(rad_2_deg(lat_lon0.latitude), rad_2_deg(lat_lon0.longitude)), zoom_start=10)
+ map_plot = add_ship_to_map(
+ situation.own_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ map_plot,
+ "black",
+ )
+
+ target_ships: Union[List[TargetShip], None] = situation.target_ships
+ assert target_ships is not None
+ for target_ship in target_ships:
+ map_plot = add_ship_to_map(
+ target_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ map_plot,
+ "red",
+ )
+ map_plot.show_in_browser()
+
+
+
+
+[docs]
+def add_ship_to_map(
+ ship: Ship,
+ vector_time: float,
+ lat_lon0: Position,
+ map_plot: Optional[Map],
+ color: str = "black",
+) -> Map:
+ """
+ Add the ship to the map.
+
+ Params:
+ * ship: Ship information
+ * vector_time: Vector time [sec]
+ * lat_lon0=Reference point, latitudinal [rad] and longitudinal [rad]
+ * map_plot: Instance of Map. If not set, instance is set to None
+ * color: Color of the ship. If not set, color is 'black'
+
+ Returns
+ -------
+ * m: Updated instance of Map.
+ """
+ if map_plot is None:
+ map_plot = Map(
+ location=(rad_2_deg(lat_lon0.latitude), rad_2_deg(lat_lon0.longitude)), zoom_start=10
+ )
+
+ assert ship.initial is not None
+ vector_length = vector_time * ship.initial.sog
+ _ = map_plot.add_child(
+ Polygon(
+ calculate_vector_arrow(ship.initial.position, ship.initial.cog, vector_length, lat_lon0),
+ fill=True,
+ fill_opacity=1,
+ color=color,
+ )
+ )
+ _ = map_plot.add_child(
+ Polygon(
+ calculate_ship_outline(ship.initial.position, ship.initial.cog, lat_lon0),
+ fill=True,
+ fill_opacity=1,
+ color=color,
+ )
+ )
+ return map_plot
+
+
+
+
+[docs]
+def plot_traffic_situations(
+ traffic_situations: List[TrafficSituation],
+ col: int,
+ row: int,
+ encounter_settings: EncounterSettings,
+):
+ """
+ Plot the traffic situations in one more figures.
+
+ Params:
+ * traffic_situations: Traffic situations to be plotted
+ * col: Number of columns in each figure
+ * row: Number of rows in each figure
+ """
+ max_columns = col
+ max_rows = row
+ num_subplots_pr_plot = max_columns * max_rows
+ small_size = 6
+ bigger_size = 10
+
+ plt.rc("axes", titlesize=small_size) # fontsize of the axes title
+ plt.rc("axes", labelsize=small_size) # fontsize of the x and y labels
+ plt.rc("xtick", labelsize=small_size) # fontsize of the tick labels
+ plt.rc("ytick", labelsize=small_size) # fontsize of the tick labels
+ plt.rc("figure", titlesize=bigger_size) # fontsize of the figure title
+
+ # The axes should have the same x/y limits, thus find max value for
+ # north/east position to be used for plotting
+ max_value: float = 0.0
+ for situation in traffic_situations:
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ lat_lon0 = situation.own_ship.initial.position
+ max_value = find_max_value_for_plot(situation.own_ship, max_value, lat_lon0)
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ max_value = find_max_value_for_plot(target_ship, max_value, lat_lon0)
+
+ plot_number: int = 1
+ _ = plt.figure(plot_number)
+ for i, situation in enumerate(traffic_situations):
+ if math.floor(i / num_subplots_pr_plot) + 1 > plot_number:
+ plot_number += 1
+ _ = plt.figure(plot_number)
+
+ axes: Axes = plt.subplot(
+ max_rows,
+ max_columns,
+ int(1 + i - (plot_number - 1) * num_subplots_pr_plot),
+ xlabel="[nm]",
+ ylabel="[nm]",
+ )
+ _ = axes.set_title(situation.title)
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial
+ assert encounter_settings.common_vector is not None
+ lat_lon0 = situation.own_ship.initial.position
+ axes = add_ship_to_plot(
+ situation.own_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ axes,
+ "black",
+ )
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ axes = add_ship_to_plot(
+ target_ship,
+ encounter_settings.common_vector,
+ lat_lon0,
+ axes,
+ "red",
+ )
+ axes.set_aspect("equal")
+
+ _ = plt.xlim(-max_value, max_value)
+ _ = plt.ylim(-max_value, max_value)
+ _ = plt.subplots_adjust(wspace=0.4, hspace=0.4)
+
+ plt.show()
+
+
+
+
+[docs]
+def find_max_value_for_plot(
+ ship: Ship,
+ max_value: float,
+ lat_lon0: Position,
+) -> float:
+ """
+ Find the maximum deviation from the Reference point in north and east direction.
+
+ Params:
+ * ship: Ship information
+ * max_value: maximum deviation in north, east direction
+ * lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
+
+ Returns
+ -------
+ * max_value: updated maximum deviation in north, east direction
+ """
+ assert ship.initial is not None
+
+ north, east, _ = llh2flat(
+ ship.initial.position.latitude,
+ ship.initial.position.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ max_value = np.max(
+ [
+ max_value,
+ np.abs(m_2_nm(north)),
+ np.abs(m_2_nm(east)),
+ ]
+ )
+ return max_value
+
+
+
+
+[docs]
+def add_ship_to_plot(
+ ship: Ship,
+ vector_time: float,
+ lat_lon0: Position,
+ axes: Optional[Axes],
+ color: str = "black",
+):
+ """
+ Add the ship to the plot.
+
+ Params:
+ * ship: Ship information
+ * vector_time: Vector time [sec]
+ * axes: Instance of figure axis. If not set, instance is set to None
+ * color: Color of the ship. If not set, color is 'black'
+ """
+ if axes is None:
+ axes = plt.gca()
+ assert isinstance(axes, Axes)
+
+ assert ship.initial is not None
+ pos_0_north, pos_0_east, _ = llh2flat(
+ ship.initial.position.latitude,
+ ship.initial.position.longitude,
+ lat_lon0.latitude,
+ lat_lon0.longitude,
+ )
+ pos_0_north = m_2_nm(pos_0_north)
+ pos_0_east = m_2_nm(pos_0_east)
+ course = ship.initial.cog
+ speed = ship.initial.sog
+
+ vector_length = m_2_nm(vector_time * speed)
+
+ _ = axes.arrow(
+ pos_0_east,
+ pos_0_north,
+ vector_length * np.sin(course),
+ vector_length * np.cos(course),
+ edgecolor=color,
+ facecolor=color,
+ width=0.0001,
+ head_length=0.2,
+ head_width=0.2,
+ length_includes_head=True,
+ )
+ circle = Circle(
+ xy=(pos_0_east, pos_0_north),
+ radius=vector_time / 3000.0, # type: ignore
+ color=color,
+ )
+ _ = axes.add_patch(circle)
+
+ return axes
+
+
+"""Functions to read the files needed to build one or more traffic situations."""
+
+import json
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Union, cast
+from uuid import UUID, uuid4
+
+from maritime_schema.types.caga import (
+ ShipStatic,
+ TrafficSituation,
+)
+
+from trafficgen.types import EncounterSettings, SituationInput
+from trafficgen.utils import deg_2_rad, knot_2_m_pr_s, min_2_s, nm_2_m
+
+
+
+[docs]
+def read_situation_files(situation_folder: Path) -> List[SituationInput]:
+ """
+ Read traffic situation files.
+
+ Params:
+ * situation_folder: Path to the folder where situation files are found
+ * input_units: Specify if the inputs are given in si or maritime units
+
+ Returns
+ -------
+ * situations: List of desired traffic situations
+ """
+ situations: List[SituationInput] = []
+ for file_name in sorted([file for file in os.listdir(situation_folder) if file.endswith(".json")]):
+ file_path = os.path.join(situation_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+
+ data = convert_keys_to_snake_case(data)
+
+ if "num_situations" not in data:
+ data["num_situations"] = 1
+
+ situation: SituationInput = SituationInput(**data)
+ situation = convert_situation_data_from_maritime_to_si_units(situation)
+
+ situations.append(situation)
+ return situations
+
+
+
+
+[docs]
+def read_generated_situation_files(situation_folder: Path) -> List[TrafficSituation]:
+ """
+ Read the generated traffic situation files. Used for testing the trafficgen algorithm.
+
+ Params:
+ * situation_folder: Path to the folder where situation files are found
+
+ Returns
+ -------
+ * situations: List of desired traffic situations
+ """
+ situations: List[TrafficSituation] = []
+ for file_name in sorted([file for file in os.listdir(situation_folder) if file.endswith(".json")]):
+ file_path = os.path.join(situation_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ situation: TrafficSituation = TrafficSituation(**data)
+ situations.append(situation)
+ return situations
+
+
+
+
+[docs]
+def convert_situation_data_from_maritime_to_si_units(situation: SituationInput) -> SituationInput:
+ """
+ Convert situation data which is given in maritime units to SI units.
+
+ Params:
+ * own_ship_file: Path to the own_ship_file file
+
+ Returns
+ -------
+ * own_ship information
+ """
+ assert situation.own_ship is not None
+ assert situation.own_ship.initial is not None
+ situation.own_ship.initial.position.longitude = deg_2_rad(
+ situation.own_ship.initial.position.longitude
+ )
+ situation.own_ship.initial.position.latitude = deg_2_rad(
+ situation.own_ship.initial.position.latitude
+ )
+ situation.own_ship.initial.cog = deg_2_rad(situation.own_ship.initial.cog)
+ situation.own_ship.initial.heading = deg_2_rad(situation.own_ship.initial.heading)
+ situation.own_ship.initial.sog = knot_2_m_pr_s(situation.own_ship.initial.sog)
+
+ if situation.own_ship.waypoints is not None:
+ for waypoint in situation.own_ship.waypoints:
+ waypoint.position.latitude = deg_2_rad(waypoint.position.latitude)
+ waypoint.position.longitude = deg_2_rad(waypoint.position.longitude)
+ if waypoint.data is not None:
+ assert waypoint.data.model_extra
+ if waypoint.data.model_extra.get("sog") is not None:
+ waypoint.data.model_extra["sog"]["value"] = knot_2_m_pr_s(waypoint.data.model_extra["sog"]["value"]) # type: ignore
+
+ assert situation.encounters is not None
+ for encounter in situation.encounters:
+ beta: Union[List[float], float, None] = encounter.beta
+ vector_time: Union[float, None] = encounter.vector_time
+ if beta is not None:
+ if isinstance(beta, List):
+ assert len(beta) == 2
+ for i in range(len(beta)):
+ beta[i] = deg_2_rad(beta[i])
+ encounter.beta = beta
+ else:
+ encounter.beta = deg_2_rad(beta)
+ if vector_time is not None:
+ encounter.vector_time = min_2_s(vector_time)
+ return situation
+
+
+
+
+[docs]
+def read_own_ship_static_file(own_ship_static_file: Path) -> ShipStatic:
+ """
+ Read own ship static data from file.
+
+ Params:
+ * own_ship_file: Path to the own_ship_static_file file
+
+ Returns
+ -------
+ * own_ship static information
+ """
+ with open(own_ship_static_file, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ if "id" not in data:
+ ship_id: UUID = uuid4()
+ data.update({"id": ship_id})
+
+ ship_static: ShipStatic = ShipStatic(**data)
+
+ return ship_static
+
+
+
+
+[docs]
+def read_target_ship_static_files(target_ship_folder: Path) -> List[ShipStatic]:
+ """
+ Read target ship static data files.
+
+ Params:
+ * target_ship_folder: Path to the folder where target ships are found
+
+ Returns
+ -------
+ * target_ships_static: List of different target ships with static information
+ """
+ target_ships_static: List[ShipStatic] = []
+ i = 0
+ for file_name in sorted([file for file in os.listdir(target_ship_folder) if file.endswith(".json")]):
+ i = i + 1
+ file_path = os.path.join(target_ship_folder, file_name)
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ data = convert_keys_to_snake_case(data)
+
+ if "id" not in data:
+ ship_id: UUID = uuid4()
+ data.update({"id": ship_id})
+
+ target_ship_static: ShipStatic = ShipStatic(**data)
+ target_ships_static.append(target_ship_static)
+ return target_ships_static
+
+
+
+
+[docs]
+def read_encounter_settings_file(settings_file: Path) -> EncounterSettings:
+ """
+ Read encounter settings file.
+
+ Params:
+ * settings_file: Path to the encounter setting file
+
+ Returns
+ -------
+ * encounter_settings: Settings for the encounter
+ """
+ with open(settings_file, encoding="utf-8") as f:
+ data = json.load(f)
+ data = check_input_units(data)
+ encounter_settings: EncounterSettings = EncounterSettings(**data)
+
+ encounter_settings = convert_settings_data_from_maritime_to_si_units(encounter_settings)
+
+ return encounter_settings
+
+
+
+
+[docs]
+def convert_settings_data_from_maritime_to_si_units(settings: EncounterSettings) -> EncounterSettings:
+ """
+ Convert situation data which is given in maritime units to SI units.
+
+ Params:
+ * own_ship_file: Path to the own_ship_file file
+
+ Returns
+ -------
+ * own_ship information
+ """
+ assert settings.classification is not None
+
+ settings.classification.theta13_criteria = deg_2_rad(settings.classification.theta13_criteria)
+ settings.classification.theta14_criteria = deg_2_rad(settings.classification.theta14_criteria)
+ settings.classification.theta15_criteria = deg_2_rad(settings.classification.theta15_criteria)
+ settings.classification.theta15[0] = deg_2_rad(settings.classification.theta15[0])
+ settings.classification.theta15[1] = deg_2_rad(settings.classification.theta15[1])
+
+ settings.vector_range[0] = min_2_s(settings.vector_range[0])
+ settings.vector_range[1] = min_2_s(settings.vector_range[1])
+
+ settings.situation_length = min_2_s(settings.situation_length)
+ settings.max_meeting_distance = nm_2_m(settings.max_meeting_distance)
+ settings.evolve_time = min_2_s(settings.evolve_time)
+ settings.common_vector = min_2_s(settings.common_vector)
+
+ return settings
+
+
+
+
+[docs]
+def check_input_units(data: Dict[str, Any]) -> Dict[str, Any]:
+ """Check if input unit is specified, if not specified it is set to SI."""
+
+ if "input_units" not in data:
+ data["input_units"] = "si"
+
+ return data
+
+
+
+
+[docs]
+def camel_to_snake(string: str) -> str:
+ """Convert a camel case string to snake case."""
+ return "".join([f"_{c.lower()}" if c.isupper() else c for c in string]).lstrip("_")
+
+
+
+
+[docs]
+def convert_keys_to_snake_case(data: Dict[str, Any]) -> Dict[str, Any]:
+ """Convert keys in a nested dictionary from camel case to snake case."""
+ return cast(Dict[str, Any], _convert_keys_to_snake_case(data))
+
+
+
+def _convert_keys_to_snake_case(
+ data: Union[Dict[str, Any], List[Any]],
+) -> Union[Dict[str, Any], List[Any]]:
+ """Convert keys in a nested dictionary from camel case to snake case."""
+
+ if isinstance(data, Dict): # Dict
+ converted_dict: Dict[str, Any] = {}
+ for key, value in data.items():
+ converted_key = camel_to_snake(key)
+ if isinstance(value, (Dict, List)):
+ converted_value = _convert_keys_to_snake_case(value)
+ else:
+ converted_value = value
+ converted_dict[converted_key] = converted_value
+ return converted_dict
+
+ # List
+ converted_list: List[Any] = []
+ for value in data:
+ if isinstance(value, (Dict, List)):
+ converted_value = _convert_keys_to_snake_case(value)
+ else:
+ converted_value = value
+ converted_list.append(value)
+ return converted_list
+
+"""Functions to generate traffic situations."""
+
+from pathlib import Path
+from typing import List, Union
+
+from maritime_schema.types.caga import (
+ OwnShip,
+ Position,
+ ShipStatic,
+ TargetShip,
+ TrafficSituation,
+)
+
+from trafficgen.encounter import (
+ define_own_ship,
+ generate_encounter,
+)
+from trafficgen.read_files import (
+ read_encounter_settings_file,
+ read_own_ship_static_file,
+ read_situation_files,
+ read_target_ship_static_files,
+)
+from trafficgen.types import EncounterSettings, EncounterType, SituationInput
+
+
+
+[docs]
+def generate_traffic_situations(
+ situation_folder: Path,
+ own_ship_file: Path,
+ target_ship_folder: Path,
+ settings_file: Path,
+) -> List[TrafficSituation]:
+ """
+ Generate a set of traffic situations using input files.
+ This is the main function for generating a set of traffic situations using input files
+ specifying number and type of encounter, type of target ships etc.
+
+ Params:
+ * situation_folder: Path to situation folder, files describing the desired situations
+ * own_ship_file: Path to where own ships is found
+ * target_ship_folder: Path to where different type of target ships is found
+ * settings_file: Path to settings file
+
+ Returns
+ -------
+ * traffic_situations: List of generated traffic situations.
+ * One situation may consist of one or more encounters.
+ """
+
+ own_ship_static: ShipStatic = read_own_ship_static_file(own_ship_file)
+ target_ships_static: List[ShipStatic] = read_target_ship_static_files(target_ship_folder)
+ encounter_settings: EncounterSettings = read_encounter_settings_file(settings_file)
+ desired_traffic_situations: List[SituationInput] = read_situation_files(situation_folder)
+ traffic_situations: List[TrafficSituation] = []
+
+ for desired_traffic_situation in desired_traffic_situations:
+ num_situations: int = desired_traffic_situation.num_situations
+ assert encounter_settings.common_vector is not None
+ assert desired_traffic_situation.own_ship is not None
+ assert desired_traffic_situation.encounters is not None
+
+ lat_lon0: Position = desired_traffic_situation.own_ship.initial.position
+
+ own_ship: OwnShip = define_own_ship(
+ desired_traffic_situation, own_ship_static, encounter_settings, lat_lon0
+ )
+ for _ in range(num_situations):
+ target_ships: List[TargetShip] = []
+ for i, encounter in enumerate(desired_traffic_situation.encounters):
+ desired_encounter_type = EncounterType(encounter.desired_encounter_type)
+ beta: Union[List[float], float, None] = encounter.beta
+ relative_speed: Union[float, None] = encounter.relative_speed
+ vector_time: Union[float, None] = encounter.vector_time
+
+ target_ship, encounter_found = generate_encounter(
+ desired_encounter_type,
+ own_ship.model_copy(deep=True),
+ target_ships_static,
+ i + 1,
+ beta,
+ relative_speed,
+ vector_time,
+ encounter_settings,
+ )
+ if encounter_found:
+ target_ships.append(target_ship.model_copy(deep=True))
+
+ traffic_situation: TrafficSituation = TrafficSituation(
+ title=desired_traffic_situation.title,
+ description=desired_traffic_situation.description,
+ own_ship=own_ship.model_copy(deep=True),
+ target_ships=target_ships,
+ start_time=None,
+ environment=None,
+ )
+ traffic_situations.append(traffic_situation)
+ return traffic_situations
+
+
+"""Domain specific data types used in trafficgen."""
+
+from enum import Enum
+from typing import List, Optional, Union
+
+from maritime_schema.types.caga import Initial, Waypoint
+from pydantic import BaseModel
+from pydantic.fields import Field
+
+
+
+[docs]
+def to_camel(string: str) -> str:
+ """Return a camel case formated string from snake case string."""
+
+ words = string.split("_")
+ return words[0] + "".join(word.capitalize() for word in words[1:])
+
+
+
+
+[docs]
+class EncounterType(Enum):
+ """Enumeration of encounter types."""
+
+ OVERTAKING_STAND_ON = "overtaking-stand-on"
+ OVERTAKING_GIVE_WAY = "overtaking-give-way"
+ HEAD_ON = "head-on"
+ CROSSING_GIVE_WAY = "crossing-give-way"
+ CROSSING_STAND_ON = "crossing-stand-on"
+ NO_RISK_COLLISION = "noRiskCollision"
+
+
+
+
+[docs]
+class Encounter(BaseModel):
+ """Data type for an encounter."""
+
+ desired_encounter_type: EncounterType
+ beta: Union[List[float], float, None] = None
+ relative_speed: Union[float, None] = None
+ vector_time: Union[float, None] = None
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterClassification(BaseModel):
+ """Data type for the encounter classification."""
+
+ theta13_criteria: float
+ theta14_criteria: float
+ theta15_criteria: float
+ theta15: List[float]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterRelativeSpeed(BaseModel):
+ """Data type for relative speed between two ships in an encounter."""
+
+ overtaking_stand_on: List[float]
+ overtaking_give_way: List[float]
+ head_on: List[float]
+ crossing_give_way: List[float]
+ crossing_stand_on: List[float]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class EncounterSettings(BaseModel):
+ """Data type for encounter settings."""
+
+ classification: EncounterClassification
+ relative_speed: EncounterRelativeSpeed
+ vector_range: List[float]
+ common_vector: float
+ situation_length: float
+ max_meeting_distance: float
+ evolve_time: float
+ disable_land_check: bool
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+
+
+[docs]
+class OwnShipInitial(BaseModel):
+ """Data type for initial data for the own ship used for generating a situation."""
+
+ initial: Initial
+ waypoints: Optional[List[Waypoint]] = Field(None, description="An array of `Waypoint` objects.")
+
+
+
+
+[docs]
+class SituationInput(BaseModel):
+ """Data type for inputs needed for generating a situations."""
+
+ title: str
+ description: str
+ num_situations: int
+ own_ship: OwnShipInitial
+ encounters: List[Encounter]
+
+
+[docs]
+ class Config:
+ """For converting parameters written to file from snake to camel case."""
+
+ alias_generator = to_camel
+ populate_by_name = True
+
+
+
+"""Utility functions that are used by several other functions."""
+
+from typing import List
+
+import numpy as np
+from maritime_schema.types.caga import Position, Waypoint
+
+from trafficgen.marine_system_simulator import flat2llh, llh2flat
+
+
+
+[docs]
+def knot_2_m_pr_s(speed_in_knot: float) -> float:
+ """
+ Convert ship speed in knots to meters pr second.
+
+ Params:
+ * speed_in_knot: Ship speed given in knots
+
+ Returns
+ -------
+ * speed_in_m_pr_s: Ship speed in meters pr second
+ """
+
+ knot_2_m_pr_sec: float = 0.5144
+ return speed_in_knot * knot_2_m_pr_sec
+
+
+
+
+[docs]
+def m_pr_s_2_knot(speed_in_m_pr_s: float) -> float:
+ """
+ Convert ship speed in knots to meters pr second.
+
+ Params:
+ * speed_in_m_pr_s: Ship speed given in meters pr second
+
+ Returns
+ -------
+ * speed_in_knot: Ship speed in knots
+ """
+
+ knot_2_m_pr_sec: float = 0.5144
+ return speed_in_m_pr_s / knot_2_m_pr_sec
+
+
+
+
+[docs]
+def min_2_s(time_in_min: float) -> float:
+ """
+ Convert time given in minutes to time given in seconds.
+
+ Params:
+ * time_in_min: Time given in minutes
+
+ Returns
+ -------
+ * time_in_s: Time in seconds
+ """
+
+ min_2_s_coeff: float = 60.0
+ return time_in_min * min_2_s_coeff
+
+
+
+
+[docs]
+def m_2_nm(length_in_m: float) -> float:
+ """
+ Convert length given in meters to length given in nautical miles.
+
+ Params:
+ * length_in_m: Length given in meters
+
+ Returns
+ -------
+ * length_in_nm: Length given in nautical miles
+ """
+
+ m_2_nm_coeff: float = 1.0 / 1852.0
+ return m_2_nm_coeff * length_in_m
+
+
+
+
+[docs]
+def nm_2_m(length_in_nm: float) -> float:
+ """
+ Convert length given in nautical miles to length given in meters.
+
+ Params:
+ * length_in_nm: Length given in nautical miles
+
+ Returns
+ -------
+ * length_in_m: Length given in meters
+ """
+
+ nm_2_m_factor: float = 1852.0
+ return length_in_nm * nm_2_m_factor
+
+
+
+
+[docs]
+def deg_2_rad(angle_in_degrees: float) -> float:
+ """
+ Convert angle given in degrees to angle give in radians.
+
+ Params:
+ * angle_in_degrees: Angle given in degrees
+
+ Returns
+ -------
+ * angle given in radians: Angle given in radians
+ """
+
+ return angle_in_degrees * np.pi / 180.0
+
+
+
+
+[docs]
+def rad_2_deg(angle_in_radians: float) -> float:
+ """
+ Convert angle given in radians to angle give in degrees.
+
+ Params:
+ * angle_in_degrees: Angle given in degrees
+
+ Returns
+ -------
+ * angle given in radians: Angle given in radians
+
+ """
+
+ return angle_in_radians * 180.0 / np.pi
+
+
+
+
+[docs]
+def convert_angle_minus_pi_to_pi_to_0_to_2_pi(angle_pi: float) -> float:
+ """
+ Convert an angle given in the region -pi to pi degrees to an
+ angle given in the region 0 to 2pi radians.
+
+ Params:
+ * angle_pi: Angle given in the region -pi to pi radians
+
+ Returns
+ -------
+ * angle_2_pi: Angle given in the region 0 to 2pi radians
+
+ """
+
+ return angle_pi if angle_pi >= 0.0 else angle_pi + 2 * np.pi
+
+
+
+
+[docs]
+def convert_angle_0_to_2_pi_to_minus_pi_to_pi(angle_2_pi: float) -> float:
+ """
+ Convert an angle given in the region 0 to 2*pi degrees to an
+ angle given in the region -pi to pi degrees.
+
+ Params:
+ * angle_2_pi: Angle given in the region 0 to 2pi radians
+
+ Returns
+ -------
+ * angle_pi: Angle given in the region -pi to pi radians
+
+ """
+
+ return angle_2_pi if (angle_2_pi >= 0.0) & (angle_2_pi <= np.pi) else angle_2_pi - 2 * np.pi
+
+
+
+
+[docs]
+def calculate_position_at_certain_time(
+ position: Position,
+ lat_lon0: Position,
+ speed: float,
+ course: float,
+ delta_time: float,
+) -> Position:
+ """
+ Calculate the position of the ship at a given time based on initial position
+ and delta time, and constant speed and course.
+
+ Params:
+ * position{latitude, longitude}: Initial ship position [rad]
+ * speed: Ship speed [m/s]
+ * course: Ship course [rad]
+ * delta_time: Delta time from now to the time new position is being calculated [minutes]
+
+ Returns
+ -------
+ * position{latitude, longitude}: Estimated ship position in delta time minutes [rad]
+ """
+
+ north, east, _ = llh2flat(
+ position.latitude, position.longitude, lat_lon0.latitude, lat_lon0.longitude
+ )
+
+ north = north + speed * delta_time * np.cos(course)
+ east = east + speed * delta_time * np.sin(course)
+
+ lat_future, lon_future, _ = flat2llh(north, east, lat_lon0.latitude, lat_lon0.longitude)
+
+ position_future: Position = Position(
+ latitude=lat_future,
+ longitude=lon_future,
+ )
+ return position_future
+
+
+
+
+[docs]
+def calculate_distance(position_prev: Position, position_next: Position) -> float:
+ """
+ Calculate the distance in meter between two waypoints.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * position_next{latitude, longitude}: Next waypoint [rad]
+
+ Returns
+ -------
+ * distance: Distance between waypoints [m]
+ """
+ # Using position of previous waypoint as reference point
+ north_next, east_next, _ = llh2flat(
+ position_next.latitude, position_next.longitude, position_prev.latitude, position_prev.longitude
+ )
+
+ distance: float = np.sqrt(north_next**2 + east_next**2)
+
+ return distance
+
+
+
+
+[docs]
+def calculate_position_along_track_using_waypoints(
+ waypoints: List[Waypoint],
+ inital_speed: float,
+ vector_time: float,
+) -> Position:
+ """
+ Calculate the position of the ship at a given time based on initial position
+ and delta time, and constant speed and course.
+
+ Params:
+ * position{latitude, longitude}: Initial ship position [rad]
+ * speed: Ship speed [m/s]
+ * course: Ship course [rad]
+ * delta_time: Delta time from now to the time new position is being calculated [sec]
+
+ Returns
+ -------
+ * position{latitude, longitude}: Estimated ship position in delta time minutes [rad]
+ """
+ time_in_transit: float = 0
+
+ for i in range(1, len(waypoints)):
+ ship_speed: float = inital_speed
+ if waypoints[i].data is not None and waypoints[i].data.model_extra["sog"] is not None: # type: ignore
+ ship_speed = waypoints[i].data.model_extra["sog"]["value"] # type: ignore
+
+ dist_between_waypoints = calculate_distance(waypoints[i - 1].position, waypoints[i].position)
+
+ # find distance ship will travel
+ dist_travel = ship_speed * (vector_time - time_in_transit)
+
+ if dist_travel > dist_between_waypoints:
+ time_in_transit = time_in_transit + dist_between_waypoints / ship_speed
+ else:
+ bearing = calculate_bearing_between_waypoints(
+ waypoints[i - 1].position, waypoints[i].position
+ )
+ position_along_track = calculate_destination_along_track(
+ waypoints[i - 1].position, dist_travel, bearing
+ )
+ return position_along_track
+
+ # if ship reach last waypoint in less time than vector_time, last waypoint is used
+ return waypoints[-1].position
+
+
+
+
+[docs]
+def calculate_bearing_between_waypoints(position_prev: Position, position_next: Position) -> float:
+ """
+ Calculate the bearing in rad between two waypoints.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * position_next{latitude, longitude}: Next waypoint [rad]
+
+ Returns
+ -------
+ * bearing: Bearing between waypoints [m]
+ """
+ # Using position of previous waypoint as reference point
+ north_next, east_next, _ = llh2flat(
+ position_next.latitude, position_next.longitude, position_prev.latitude, position_prev.longitude
+ )
+
+ bearing: float = convert_angle_minus_pi_to_pi_to_0_to_2_pi(np.arctan2(east_next, north_next))
+
+ return bearing
+
+
+
+
+[docs]
+def calculate_destination_along_track(
+ position_prev: Position, distance: float, bearing: float
+) -> Position:
+ """
+ Calculate the destination along the track between two waypoints when distance along the track is given.
+
+ Params:
+ * position_prev{latitude, longitude}: Previous waypoint [rad]
+ * distance: Distance to travel [m]
+ * bearing: Bearing from previous waypoint to next waypoint [rad]
+
+ Returns
+ -------
+ * destination{latitude, longitude}: Destination along the track [rad]
+ """
+ north = distance * np.cos(bearing)
+ east = distance * np.sin(bearing)
+
+ lat, lon, _ = flat2llh(north, east, position_prev.latitude, position_prev.longitude)
+ destination = Position(latitude=lat, longitude=lon)
+
+ return destination
+
+
+"""Functions to clean traffic situations data before writing it to a json file."""
+
+from pathlib import Path
+from typing import List, TypeVar
+
+from maritime_schema.types.caga import OwnShip, Ship, TargetShip, TrafficSituation
+
+from trafficgen.utils import m_pr_s_2_knot, rad_2_deg
+
+T_ship = TypeVar("T_ship", Ship, OwnShip, TargetShip)
+
+
+
+[docs]
+def write_traffic_situations_to_json_file(situations: List[TrafficSituation], write_folder: Path):
+ """
+ Write traffic situations to json file.
+
+ Params:
+ * traffic_situations: Traffic situations to be written to file
+ * write_folder: Folder where the json files is to be written
+ """
+
+ Path(write_folder).mkdir(parents=True, exist_ok=True)
+ for i, situation in enumerate(situations):
+ file_number: int = i + 1
+ output_file_path: Path = write_folder / f"traffic_situation_{file_number:02d}.json"
+ situation = convert_situation_data_from_si_units_to__maritime(situation)
+ data: str = situation.model_dump_json(
+ by_alias=True, indent=4, exclude_unset=True, exclude_defaults=False, exclude_none=True
+ )
+ with open(output_file_path, "w", encoding="utf-8") as outfile:
+ _ = outfile.write(data)
+
+
+
+
+[docs]
+def convert_situation_data_from_si_units_to__maritime(situation: TrafficSituation) -> TrafficSituation:
+ """
+ Convert situation data which is given in SI units to maritime units.
+
+ Params:
+ * situation: Traffic situation data
+
+ Returns
+ -------
+ * situation: Converted traffic situation data
+ """
+ assert situation.own_ship is not None
+ situation.own_ship = convert_ship_data_from_si_units_to_maritime(situation.own_ship)
+
+ assert situation.target_ships is not None
+ for target_ship in situation.target_ships:
+ target_ship = convert_ship_data_from_si_units_to_maritime(target_ship)
+
+ return situation
+
+
+
+
+[docs]
+def convert_ship_data_from_si_units_to_maritime(ship: T_ship) -> T_ship:
+ """
+ Convert ship data which is given in SI units to maritime units.
+
+ Params:
+ * ship: Ship data
+
+ Returns
+ -------
+ * ship: Converted ship data
+ """
+ assert ship.initial is not None
+ ship.initial.position.longitude = round(rad_2_deg(ship.initial.position.longitude), 8)
+ ship.initial.position.latitude = round(rad_2_deg(ship.initial.position.latitude), 8)
+ ship.initial.cog = round(rad_2_deg(ship.initial.cog), 2)
+ ship.initial.sog = round(m_pr_s_2_knot(ship.initial.sog), 1)
+ ship.initial.heading = round(rad_2_deg(ship.initial.heading), 2)
+
+ if ship.waypoints is not None:
+ for waypoint in ship.waypoints:
+ waypoint.position.latitude = round(rad_2_deg(waypoint.position.latitude), 8)
+ waypoint.position.longitude = round(rad_2_deg(waypoint.position.longitude), 8)
+ if not waypoint.data:
+ continue
+ assert waypoint.data.model_extra
+ if waypoint.data.model_extra.get("sog") is not None:
+ waypoint.data.model_extra["sog"]["value"] = round(m_pr_s_2_knot(waypoint.data.model_extra["sog"]["value"]), 1) # type: ignore
+ if waypoint.data.model_extra.get("heading") is not None:
+ waypoint.data.model_extra["heading"]["value"] = round(m_pr_s_2_knot(waypoint.data.model_extra["heading"]["value"]), 2) # type: ignore
+
+ return ship
+
+
' + + '' + + _("Hide Search Matches") + + "
" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/branch/main/api.html b/branch/main/api.html new file mode 100644 index 0000000..7bd54bd --- /dev/null +++ b/branch/main/api.html @@ -0,0 +1,220 @@ + + + + + + +assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given.
+You can contribute in many ways:
+Report bugs at https://github.com/dnv-opensource/ship-traffic-generator/issues.
+If you are reporting a bug, please include:
+Your operating system name and version.
The version of Python (and Conda) that you are using.
Any additional details about your local setup that might be helpful in troubleshooting.
Detailed steps to reproduce the bug.
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help +wanted” is open to whoever wants to implement it.
+Look through the GitHub issues for features. Anything tagged with “enhancement” +and “help wanted” is open to whoever wants to implement it.
+Traffic Generator could always use more documentation, whether as part of the +official Traffic Generator docs, in docstrings, or even on the web in blog posts, +articles, and such.
+The best way to send feedback is to file an issue at https://github.com/dnv-opensource/ship-traffic-generator/issues.
+If you are proposing a feature:
+Explain in detail how it would work.
Keep the scope as narrow as possible, to make it easier to implement.
Remember that this is a volunteer-driven project, and that contributions +are welcome :)
Ready to contribute? Here’s how to set up trafficgen for local development.
+Clone the trafficgen repo on GitHub.
Install your local copy into a pyenv or conda environment.
Create a branch for local development:
+$ git checkout -b name-of-your-bugfix-or-feature
+
Now you can make your changes locally.
+When you’re done making changes, check that your changes pass flake8 and the +tests, including testing other Python versions with tox:
+$ flake8 --config tox.ini ./src/trafficgen ./tests
+$ pytest ./tests
+$ tox
+
If you installed the package with poetry
+++$ poetry install –with dev,docs
+
flake8, pytest and tox should already be installed in your Python environment. +Note that the tox config assumes Python 3.10 and Python 3.11, you would have +to have them both available to tox for all tests to run. +If you only have one of these available tox will skip the non supported +environment (and in most cases that is OK). +If you are managing your Python enhancements with pyenv you will have to +install the necessary versions and then run pyenv rehash to make them +available to tox (or set multiple local envs with pyenv local py310 py311). +If you are using conda you will have to create a new environment with +the necessary Python version, install virtualenv in the environments +and then run conda activate <env-name> to make it available to tox (to run all +the environments in one go do conda activate inside activated environments).
+You can also run the python tests from VSCode via the “Testing” view, +“Configure Python Tests”, with pytest` and select the folder tests.
+Commit your changes and push your branch to the source repo:
+$ git add .
+$ git commit -m "Your detailed description of your changes."
+$ git push origin name-of-your-bugfix-or-feature
+
Submit a pull request through https://github.com/dnv-opensource/ship-traffic-generator/pulls.
Before you submit a pull request, check that it meets these guidelines:
+The pull request should include tests.
If the pull request adds functionality, the docs should be updated. Put +your new functionality into a function with a docstring, and add the +feature to the list in README.md.
The pull request should work for Python 3.10.
To run a subset of tests:
+$ pytest tests.test_trafficgen
+
A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:
+$ bump2version patch # possible: major / minor / patch
+$ git push
+$ git push --tags
+
+ |
+ | + |
+ | + |
+ | + |
+ | + |
+ |
+ |
+ |
+ | + |
+ | + |
+ |
+ | + |
+ | + |
|
+
|
+
+ | + |
+ | + |
Changed
+Updated to download-artifact@v4 (from download-artifact@v3)
Changed
+removed specific names for target ships. Files generated with target ship 1, 2 etc.
changed tests. Still need to figure out why some tests “fail” using CLI.
Changed
+possible to have several aypoints for own ship
fixing pyright error
beta (relative bearing between osn ship and target ship seen from own ship) +is not just a number, but could also be a range
situation length is used when checking if target ship is passing land
Changed
+using types from maritime schema
lat/lon used instead of north/east
the generated output files are using “maritime” units: knots and degrees
Changed
+add-basic-code-quality-settings-black-ruff-pyright,
first-small-round-of-code-improvement
add-domain-specific-data-types-for-ship-situation-etc-using-pydantic-models,
activate-remaining-pyright-rules,
add-github-workflows-to-build-package-and-to-build-and-publish-documentation
sorting output from os.listdir
github workflow for release
removed cyclic import
length of encounter may be specified by user
First release on PyPI.
The tool generates a structured set of encounters for verifying automatic collision and +grounding avoidance systems. Based on input parameters such as desired situation, relative speed, +relative bearing etc, the tool will generate a set of traffic situations. The traffic situations +may be written to files and/or inspected using plots.
+A paper
is written describing the background for
+the tool and how it works.
Example 1: Complete specified situation:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ "beta": 2.0,
+ "relativeSpeed": 1.2,
+ "vectorTime": 15.0
+ }
+ ]
+}
+
The values may be given in either maritime units or SI units, and which unit is used shall be specified in the src/trafficgen/settings/encounter_settings.json file. +The common_vector is given in minutes (maritime) or seconds (SI). For radar plotting (plotting vessel positions and relative motions), +the common_vector and vector_time are used together with ship speed to display where the ship will be in e.g. 10 minutes +(Common vector is the common time vector used on a radar plot, e.g 10, 15, 20 minutes. The length of the arrow in the plot +will then be the speed times this time vector). +Speed and course of the own ship, which is the ship to be tested, are given in knots and degrees (maritime) or m/s and radians (SI), respectively. +The own ship position is given both in latitudinal and longitudinal (degree/radians) together with north/east in meters from the reference point. +The reference point is the initial position of own ship.
+An encounter may be fully described as shown above, but the user may also deside to input less data, +as demonstrated in Example 2. Desired encounter type is mandatory, +while the beta, relative_speed and vector_time parameters are optional:
++++
+- +
desired_encounter_type is either head-on, overtaking-give-way, overtaking-stand-on, crossing-give-way, and crossing-stand-on.
- +
beta is the relative bearing between the own ship and the target ship as seen from the own shop, given in degrees/radians.
- +
relative_speed is relative speed between the own ship and the target ship as seen from the own ship, such that a relative speed of 1.2 means that the target ship’s speed is 20% higher than the speed of the own ship.
An encounter is built using a maximum meeting distance [nm], see the paper linked in the introduction for more info. +At some time in the future, given by the vector_time, the target ship will be located somewhere inside a circle +with a radius given by max_meeting_distance and a center point given by the own ship position. This is not necessarily the +closest point of approach.
+The max_meeting_distance parameter is common for all encounters and is specified in src/trafficgen/settings/encounter_settings.json.
+Example 2: Minimum specified situation:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ }
+ ]
+}
+
You can also request the generation of several traffic situations of the same encounter type by specifying num_situations:
+Example 3: Generate multiple situations using numSituations:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "numSituations": 5
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ }
+ ]
+}
+
The next example show how it is possible to give a range for the relative bearing between own ship and target ship
+Example 4: Assign range for beta:
+{
+ "title": "CR_GW",
+ "common_vector": 10.0,
+ "own_ship": {
+ "speed": 7.0,
+ "course": 0.0,
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ }
+ },
+ "encounter": [
+ {
+ "desired_encounter_type": "crossing-give-way",
+ "beta": [45.0,120.0]
+ }
+ ]
+}
+
To install Traffic Generator, run this command in your terminal:
+$ pip install trafficgen
+
This is the preferred method to install Traffic Generator, as it will always install the most recent stable release.
+If you don’t have pip installed, this Python installation guide can guide +you through the process.
+The sources for Traffic Generator can be downloaded from the https://github.com/dnv-opensource/ship-traffic-generator.
+You can either clone the public repository:
+$ git clone https://github.com/dnv-opensource/ship-traffic-generator
+
Once you have a copy of the source, you can install it with:
+$ python setup.py install
+
assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
+ t | ||
+ |
+ trafficgen | + |
+ |
+ trafficgen.check_land_crossing | + |
+ |
+ trafficgen.cli | + |
+ |
+ trafficgen.encounter | + |
+ |
+ trafficgen.marine_system_simulator | + |
+ |
+ trafficgen.plot_traffic_situation | + |
+ |
+ trafficgen.read_files | + |
+ |
+ trafficgen.ship_traffic_generator | + |
+ |
+ trafficgen.types | + |
+ |
+ trafficgen.utils | + |
+ |
+ trafficgen.write_traffic_situation_to_file | + |
Module with helper functions to determine if a generated path is crossing land.
+Find if path is crossing land.
+position_1: Ship position in latitude/longitude [rad]. +speed: Ship speed [m/s]. +course: Ship course [rad]. +lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]. +time_interval: The time interval the vessel should travel without crossing land [sec]
+is_on_land
+True if parts of the path crosses land.
+CLI for trafficgen package.
+Functions to generate encounters consisting of one own ship and one to many target ships. +The generated encounters may be of type head-on, overtaking give-way and stand-on and +crossing give-way and stand-on.
+Assign random (uniform) relative bearing beta between own ship +and target ship depending on type of encounter.
+encounter_type: Type of encounter
settings: Encounter settings
* Relative bearing between own ship and target ship seen from own ship [rad]
+Assign random (uniform) relative bearing beta between own ship +and target ship depending between the limits given by beta_limit.
+beta_limit: Limits for beta
* Relative bearing between own ship and target ship seen from own ship [rad]
+Randomly assign future position of target ship. If drawing a circle with radius +max_meeting_distance around future position of own ship, future position of +target ship shall be somewhere inside this circle.
+future, {north, east}
+lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
a given time in the future [m]
+future_position_target_ship
+Future position of target ship {north, east} [m]
+Assign random (uniform) sog to target ship depending on type of encounter.
+encounter_type: Type of encounter
own_ship_sog: Own ship sog [m/s]
min_target_ship_sog: Minimum target ship sog [m/s]
relative_sog_setting: Relative sog setting dependent on encounter [-]
* target_ship_sog
+Target ship sog [m/s]
+Assign random (uniform) vector time.
+vector_range: Minimum and maximum value for vector time
* vector_time
+Vector time [min]
+Calculate minimum vector length (target ship sog x vector). This will +ensure that ship sog is high enough to find proper situation.
+own_ship_position: Own ship initial position, latitudinal [rad] and longitudinal [rad]
own_ship_cog: Own ship initial cog
target_ship_position_future: Target ship future position
desired_beta: Desired relative bearing between
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* min_vector_length
+Minimum vector length (target ship sog x vector)
+Calculate relative bearing between own ship and target ship, both seen from +own ship and seen from target ship.
+position_own_ship: Own ship position {latitude, longitude} [rad]
heading_own_ship: Own ship heading [rad]
position_target_ship: Target ship position {latitude, longitude} [rad]
heading_target_ship: Target ship heading [rad]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* beta (relative bearing between own ship and target ship seen from own ship [rad])
* alpha (relative bearing between target ship and own ship seen from target ship [rad])
Calculate ship cog between two waypoints.
+waypoint_0: Dict, waypoint {latitude, longitude} [rad]
waypoint_1: Dict, waypoint {latitude, longitude} [rad]
* cog
+Ship cog [rad]
+Check encounter evolvement. The generated encounter should be the same type of +encounter (head-on, crossing, give-way) also some time before the encounter is started.
+own_ship: Own ship information such as initial position, sog and cog
target_ship: Target ship information such as initial position, sog and cog
desired_encounter_type: Desired type of encounter to be generated
settings: Encounter settings
* returns True if encounter ok, False if encounter not ok
+Randomly pick a target ship from a list of target ships.
+target_ships: list of target ships with static information
* The target ship, info of type, size etc.
+Define own ship based on information in desired traffic situation.
+desired_traffic_situation: Information about type of traffic situation to generate
own_ship_static: Static information of own ship.
encounter_settings: Necessary setting for the encounter
lat_lon0: Reference position [deg]
* own_ship
+Own ship
+Determine the colreg type based on alpha, relative bearing between target ship and own +ship seen from target ship, and beta, relative bearing between own ship and target ship +seen from own ship.
+alpha: relative bearing between target ship and own ship seen from target ship
beta: relative bearing between own ship and target ship seen from own ship
theta13_criteria: Tolerance for “coming up with” relative bearing
theta14_criteria: Tolerance for “reciprocal or nearly reciprocal cogs”, +“when in any doubt… assume… [head-on]”
theta15_criteria: Crossing aspect limit, used for classifying a crossing encounter
encounter
+* encounter classification
+Find start position of target ship using desired beta and vector length.
+own_ship_position: Own ship initial position, sog and cog
own_ship_cog: Own ship initial cog
target_ship_position_future: Target ship future position
target_ship_vector_length: vector length (target ship sog x vector)
desired_beta: Desired bearing between own ship and target ship seen from own ship
desired_encounter_type: Desired type of encounter to be generated
settings: Encounter settings
* start_position_target_ship (Dict, initial position of target ship {north, east} [m])
* start_position_found (0=position not found, 1=position found)
Generate an encounter.
+desired_encounter_type: Desired encounter to be generated
own_ship: Dict, information about own ship that will encounter a target ship
target_ships_static: List of target ships including static information that +may be used in an encounter
encounter_number: Integer, used to naming the target ships. target_ship_1,2 etc.
beta_default: User defined beta. If not set, this is None.
target ship. If not set, this is None.
+vector_time_default: User defined vector time. If not set, this is None.
settings: Encounter settings
* target_ship (target ship information, such as initial position, sog and cog)
* encounter_found (True=encounter found, False=encounter not found)
Top-level package for Traffic Generator.
+Nothing exposed at the moment, but will be done if needed.
+assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
Encounter
+EncounterClassification
+EncounterRelativeSpeed
+EncounterSettings
EncounterSettings.Config
+EncounterSettings.classification
EncounterSettings.common_vector
EncounterSettings.disable_land_check
EncounterSettings.evolve_time
EncounterSettings.max_meeting_distance
EncounterSettings.model_config
EncounterSettings.relative_speed
EncounterSettings.situation_length
EncounterSettings.vector_range
EncounterType
+OwnShipInitial
+SituationInput
+to_camel()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
The Marine Systems Simulator (MSS) is a Matlab and Simulink library for marine systems.
+It includes models for ships, underwater vehicles, unmanned surface vehicles, and floating structures. +The library also contains guidance, navigation, and control (GNC) blocks for real-time simulation. +The algorithms are described in:
+T. I. Fossen (2021). Handbook of Marine Craft Hydrodynamics and Motion Control. 2nd. Edition, +Wiley. ISBN-13: 978-1119575054
+Parts of the library have been re-implemented in Python and are found below.
+Compute longitude lon (rad), latitude lat (rad) and height h (m) for the +NED coordinates (xn,yn,zn).
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+The method computes longitude lon (rad), latitude lat (rad) and height h (m) for the +NED coordinates (xn,yn,zn) using a flat Earth coordinate system defined by the WGS-84 +ellipsoid. The flat Earth coordinate origin is located at (lon_0, lat_0) with reference +height h_ref in meters above the surface of the ellipsoid. Both height and h_ref +are positive upwards, while zn is positive downwards (NED). +Author: Thor I. Fossen +Date: 20 July 2018 +Revisions: 2023-02-04 updates the formulas for latitude and longitude
+xn: Ship position, north [m]
yn: Ship position, east [m]
zn=0.0: Ship position, down [m]
lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
h_ref=0.0: Flat earth coordinate with reference h_ref in meters above the surface +of the ellipsoid
* lat (Latitude [rad])
* lon (Longitude [rad])
* h (Height [m])
Compute (north, east) for a flat Earth coordinate system from longitude +lon (rad) and latitude lat (rad).
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+The method computes (north, east) for a flat Earth coordinate system from longitude +lon (rad) and latitude lat (rad) of the WGS-84 elipsoid. The flat Earth coordinate +origin is located at (lon_0, lat_0). +Author: Thor I. Fossen +Date: 20 July 2018 +Revisions: 2023-02-04 updates the formulas for latitude and longitude
+lat: Ship position in latitude [rad]
lon: Ship position in longitude [rad]
h=0.0: Ship height in meters above the surface of the ellipsoid
lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
h_ref=0.0: Flat earth coordinate with reference h_ref in meters above +the surface of the ellipsoid
* x_n (Ship position, north [m])
* y_n (Ship position, east [m])
* z_n (Ship position, down [m])
Return the “smallest signed angle” (SSA) or the smallest difference between two angles.
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+Examples
+angle = ssa(angle) maps an angle in rad to the interval [-pi pi)
+Author: Thor I. Fossen +Date: 2018-09-21
+angle: angle given in radius
* smallest_angle
+“smallest signed angle” or the smallest difference between two angles
+Functions to prepare and plot traffic situations.
+Add the ship to the map.
+ship: Ship information
vector_time: Vector time [sec]
lat_lon0=Reference point, latitudinal [rad] and longitudinal [rad]
map_plot: Instance of Map. If not set, instance is set to None
color: Color of the ship. If not set, color is ‘black’
* m
+Updated instance of Map.
+Add the ship to the plot.
+ship: Ship information
vector_time: Vector time [sec]
axes: Instance of figure axis. If not set, instance is set to None
color: Color of the ship. If not set, color is ‘black’
Calculate the outline of the ship pointing in the direction of ship course.
+position: {latitude}, {longitude} position of the ship [rad]
course: course of the ship [rad]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
ship_length: Ship length. If not given, ship length is set to 100
ship_width: Ship width. If not given, ship width is set to 15
* ship_outline_points
+Polygon points to draw the ship [deg]
+Calculate the arrow with length vector pointing in the direction of ship course.
+position: {latitude}, {longitude} position of the ship [rad]
direction: direction the arrow is pointing [rad]
vector_length: length of vector [m]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* arrow_points
+Polygon points to draw the arrow [deg]
+Find the maximum deviation from the Reference point in north and east direction.
+ship: Ship information
max_value: maximum deviation in north, east direction
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* max_value
+updated maximum deviation in north, east direction
+Plot a specific situation in map.
+traffic_situations: Generated traffic situations
situation_number: The specific situation to be plotted
Plot the traffic situations in one more figures.
+traffic_situations: Traffic situations to be plotted
col: Number of columns in each figure
row: Number of rows in each figure
Functions to read the files needed to build one or more traffic situations.
+Convert a camel case string to snake case.
+Check if input unit is specified, if not specified it is set to SI.
+Convert keys in a nested dictionary from camel case to snake case.
+Convert situation data which is given in maritime units to SI units.
+own_ship_file: Path to the own_ship_file file
* own_ship information
+Convert situation data which is given in maritime units to SI units.
+own_ship_file: Path to the own_ship_file file
* own_ship information
+Read encounter settings file.
+settings_file: Path to the encounter setting file
* encounter_settings
+Settings for the encounter
+Read the generated traffic situation files. Used for testing the trafficgen algorithm.
+situation_folder: Path to the folder where situation files are found
* situations
+List of desired traffic situations
+Read own ship static data from file.
+own_ship_file: Path to the own_ship_static_file file
* own_ship static information
+Read traffic situation files.
+situation_folder: Path to the folder where situation files are found
input_units: Specify if the inputs are given in si or maritime units
* situations
+List of desired traffic situations
+Read target ship static data files.
+target_ship_folder: Path to the folder where target ships are found
* target_ships_static
+List of different target ships with static information
+Functions to generate traffic situations.
+Generate a set of traffic situations using input files. +This is the main function for generating a set of traffic situations using input files +specifying number and type of encounter, type of target ships etc.
+situation_folder: Path to situation folder, files describing the desired situations
own_ship_file: Path to where own ships is found
target_ship_folder: Path to where different type of target ships is found
settings_file: Path to settings file
* traffic_situations (List of generated traffic situations.)
* One situation may consist of one or more encounters.
Domain specific data types used in trafficgen.
+Bases: BaseModel
Data type for an encounter.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Bases: BaseModel
Data type for the encounter classification.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Bases: BaseModel
Data type for relative speed between two ships in an encounter.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Bases: BaseModel
Data type for encounter settings.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Bases: Enum
Enumeration of encounter types.
+Bases: BaseModel
Data type for initial data for the own ship used for generating a situation.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Bases: BaseModel
Data type for inputs needed for generating a situations.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Utility functions that are used by several other functions.
+Calculate the bearing in rad between two waypoints.
+position_prev{latitude, longitude}: Previous waypoint [rad]
position_next{latitude, longitude}: Next waypoint [rad]
* bearing
+Bearing between waypoints [m]
+Calculate the destination along the track between two waypoints when distance along the track is given.
+position_prev{latitude, longitude}: Previous waypoint [rad]
distance: Distance to travel [m]
bearing: Bearing from previous waypoint to next waypoint [rad]
* destination{latitude, longitude}
+Destination along the track [rad]
+Calculate the distance in meter between two waypoints.
+position_prev{latitude, longitude}: Previous waypoint [rad]
position_next{latitude, longitude}: Next waypoint [rad]
* distance
+Distance between waypoints [m]
+Calculate the position of the ship at a given time based on initial position +and delta time, and constant speed and course.
+position{latitude, longitude}: Initial ship position [rad]
speed: Ship speed [m/s]
course: Ship course [rad]
delta_time: Delta time from now to the time new position is being calculated [sec]
* position{latitude, longitude}
+Estimated ship position in delta time minutes [rad]
+Calculate the position of the ship at a given time based on initial position +and delta time, and constant speed and course.
+position{latitude, longitude}: Initial ship position [rad]
speed: Ship speed [m/s]
course: Ship course [rad]
delta_time: Delta time from now to the time new position is being calculated [minutes]
* position{latitude, longitude}
+Estimated ship position in delta time minutes [rad]
+Convert an angle given in the region 0 to 2*pi degrees to an +angle given in the region -pi to pi degrees.
+angle_2_pi: Angle given in the region 0 to 2pi radians
* angle_pi
+Angle given in the region -pi to pi radians
+Convert an angle given in the region -pi to pi degrees to an +angle given in the region 0 to 2pi radians.
+angle_pi: Angle given in the region -pi to pi radians
* angle_2_pi
+Angle given in the region 0 to 2pi radians
+Convert angle given in degrees to angle give in radians.
+angle_in_degrees: Angle given in degrees
* angle given in radians
+Angle given in radians
+Convert ship speed in knots to meters pr second.
+speed_in_knot: Ship speed given in knots
* speed_in_m_pr_s
+Ship speed in meters pr second
+Convert length given in meters to length given in nautical miles.
+length_in_m: Length given in meters
* length_in_nm
+Length given in nautical miles
+Convert ship speed in knots to meters pr second.
+speed_in_m_pr_s: Ship speed given in meters pr second
* speed_in_knot
+Ship speed in knots
+Convert time given in minutes to time given in seconds.
+time_in_min: Time given in minutes
* time_in_s
+Time in seconds
+Functions to clean traffic situations data before writing it to a json file.
+Convert ship data which is given in SI units to maritime units.
+ship: Ship data
* ship
+Converted ship data
+Convert situation data which is given in SI units to maritime units.
+situation: Traffic situation data
* situation
+Converted traffic situation data
+Write traffic situations to json file.
+traffic_situations: Traffic situations to be written to file
write_folder: Folder where the json files is to be written
To use Traffic Generator in a project:
+import trafficgen
+
To use Traffic Generator as a command line tool for generating traffic situations, write:
+trafficgen gen-situation
+
The command line tool takes different input options:
+-s, --situations PATH Folders with situations (default=./baseline_situations_input/)
+-t, --targets PATH Folder with target configurations (default=./target_ships/)
+-c, --settings PATH Path to settings file (default=./settings/encounter_settings.json)
+--visualize Plot visualization
+--col INTEGER Number of columns for plot, may be used with visualize (default=10)
+--row INTEGER Number of rows for plot, may be used with visualize (default=6)
+--visualize-situation INTEGER Plot individual traffic situation, specify INTEGER value
+-o, --output PATH Output folder (default=None)
+--help Show this message and exit.
+
Example:
+trafficgen gen-situation -s ./data/example_situations_input -o ./data/test_output_1
+
When generating situations without specifying where the desired situations (--situation
) are found, the
+default path, which is default=./baseline_situations_input/
, will be used.
The baseline situations are a set of generic traffic situations covering head-on, overtaking stand-on/give-way
+and crossing stand-on/give-way encounters. To cover the combination of encounters for 1, 2 and 3 target ships,
+there are in total 55 baseline situations. The input files for generating these situations are found in
+./baseline_situations_input/
All the generated situations are displayed if using --visualize
. This will pop up one or more plot windows,
+which show all the traffic situations. The number of colums and rows for the plots (per figure) can be specified by
+using --col
and --row
, respectively.
A specific encounter is visualized by using --visualize-situation INTEGER
, e.g.:
trafficgen gen-situation -s ./data/example_situations_input -o ./data/test_output_1 --visualize-situation 2
+
This will open a browser window/tab with an OpenStreetMap background and the traffic situation +radar plot as an overlay. +Note that the integer needs to be within the range of the number of generated situations, +for example 1 - 12 if you generated 12 situations.
+Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given.
+You can contribute in many ways:
+Report bugs at https://github.com/dnv-opensource/ship-traffic-generator/issues.
+If you are reporting a bug, please include:
+Your operating system name and version.
The version of Python (and Conda) that you are using.
Any additional details about your local setup that might be helpful in troubleshooting.
Detailed steps to reproduce the bug.
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help +wanted” is open to whoever wants to implement it.
+Look through the GitHub issues for features. Anything tagged with “enhancement” +and “help wanted” is open to whoever wants to implement it.
+Traffic Generator could always use more documentation, whether as part of the +official Traffic Generator docs, in docstrings, or even on the web in blog posts, +articles, and such.
+The best way to send feedback is to file an issue at https://github.com/dnv-opensource/ship-traffic-generator/issues.
+If you are proposing a feature:
+Explain in detail how it would work.
Keep the scope as narrow as possible, to make it easier to implement.
Remember that this is a volunteer-driven project, and that contributions +are welcome :)
Ready to contribute? Here’s how to set up trafficgen for local development.
+Clone the trafficgen repo on GitHub.
Install your local copy into a pyenv or conda environment.
Create a branch for local development:
+$ git checkout -b name-of-your-bugfix-or-feature
+
Now you can make your changes locally.
+When you’re done making changes, check that your changes pass flake8 and the +tests, including testing other Python versions with tox:
+$ flake8 --config tox.ini ./src/trafficgen ./tests
+$ pytest ./tests
+$ tox
+
If you installed the package with poetry
+++$ poetry install –with dev,docs
+
flake8, pytest and tox should already be installed in your Python environment. +Note that the tox config assumes Python 3.10 and Python 3.11, you would have +to have them both available to tox for all tests to run. +If you only have one of these available tox will skip the non supported +environment (and in most cases that is OK). +If you are managing your Python enhancements with pyenv you will have to +install the necessary versions and then run pyenv rehash to make them +available to tox (or set multiple local envs with pyenv local py310 py311). +If you are using conda you will have to create a new environment with +the necessary Python version, install virtualenv in the environments +and then run conda activate <env-name> to make it available to tox (to run all +the environments in one go do conda activate inside activated environments).
+You can also run the python tests from VSCode via the “Testing” view, +“Configure Python Tests”, with pytest` and select the folder tests.
+Commit your changes and push your branch to the source repo:
+$ git add .
+$ git commit -m "Your detailed description of your changes."
+$ git push origin name-of-your-bugfix-or-feature
+
Submit a pull request through https://github.com/dnv-opensource/ship-traffic-generator/pulls.
Before you submit a pull request, check that it meets these guidelines:
+The pull request should include tests.
If the pull request adds functionality, the docs should be updated. Put +your new functionality into a function with a docstring, and add the +feature to the list in README.md.
The pull request should work for Python 3.10.
To run a subset of tests:
+$ pytest tests.test_trafficgen
+
A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:
+$ bump2version patch # possible: major / minor / patch
+$ git push
+$ git push --tags
+
+ |
+ | + |
+ | + |
+ | + |
+ | + |
+ |
+ |
+ |
+ | + |
+ | + |
+ |
+ | + |
+ | + |
|
+
|
+
+ | + |
+ | + |
Changed
+Updated to download-artifact@v4 (from download-artifact@v3)
Changed
+removed specific names for target ships. Files generated with target ship 1, 2 etc.
changed tests. Still need to figure out why some tests “fail” using CLI.
Changed
+possible to have several aypoints for own ship
fixing pyright error
beta (relative bearing between osn ship and target ship seen from own ship) +is not just a number, but could also be a range
situation length is used when checking if target ship is passing land
Changed
+using types from maritime schema
lat/lon used instead of north/east
the generated output files are using “maritime” units: knots and degrees
Changed
+add-basic-code-quality-settings-black-ruff-pyright,
first-small-round-of-code-improvement
add-domain-specific-data-types-for-ship-situation-etc-using-pydantic-models,
activate-remaining-pyright-rules,
add-github-workflows-to-build-package-and-to-build-and-publish-documentation
sorting output from os.listdir
github workflow for release
removed cyclic import
length of encounter may be specified by user
First release on PyPI.
The tool generates a structured set of encounters for verifying automatic collision and +grounding avoidance systems. Based on input parameters such as desired situation, relative speed, +relative bearing etc, the tool will generate a set of traffic situations. The traffic situations +may be written to files and/or inspected using plots.
+A paper
is written describing the background for
+the tool and how it works.
Example 1: Complete specified situation:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ "beta": 2.0,
+ "relativeSpeed": 1.2,
+ "vectorTime": 15.0
+ }
+ ]
+}
+
The values may be given in either maritime units or SI units, and which unit is used shall be specified in the src/trafficgen/settings/encounter_settings.json file. +The common_vector is given in minutes (maritime) or seconds (SI). For radar plotting (plotting vessel positions and relative motions), +the common_vector and vector_time are used together with ship speed to display where the ship will be in e.g. 10 minutes +(Common vector is the common time vector used on a radar plot, e.g 10, 15, 20 minutes. The length of the arrow in the plot +will then be the speed times this time vector). +Speed and course of the own ship, which is the ship to be tested, are given in knots and degrees (maritime) or m/s and radians (SI), respectively. +The own ship position is given both in latitudinal and longitudinal (degree/radians) together with north/east in meters from the reference point. +The reference point is the initial position of own ship.
+An encounter may be fully described as shown above, but the user may also deside to input less data, +as demonstrated in Example 2. Desired encounter type is mandatory, +while the beta, relative_speed and vector_time parameters are optional:
++++
+- +
desired_encounter_type is either head-on, overtaking-give-way, overtaking-stand-on, crossing-give-way, and crossing-stand-on.
- +
beta is the relative bearing between the own ship and the target ship as seen from the own shop, given in degrees/radians.
- +
relative_speed is relative speed between the own ship and the target ship as seen from the own ship, such that a relative speed of 1.2 means that the target ship’s speed is 20% higher than the speed of the own ship.
An encounter is built using a maximum meeting distance [nm], see the paper linked in the introduction for more info. +At some time in the future, given by the vector_time, the target ship will be located somewhere inside a circle +with a radius given by max_meeting_distance and a center point given by the own ship position. This is not necessarily the +closest point of approach.
+The max_meeting_distance parameter is common for all encounters and is specified in src/trafficgen/settings/encounter_settings.json.
+Example 2: Minimum specified situation:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ }
+ ]
+}
+
You can also request the generation of several traffic situations of the same encounter type by specifying num_situations:
+Example 3: Generate multiple situations using numSituations:
+{
+ "title": "HO",
+ "description": "A head on situation with one target ship.",
+ "numSituations": 5
+ "ownShip": {
+ "initial": {
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ },
+ "sog": 10.0,
+ "cog": 0.0,
+ "heading": 0.0,
+ "navStatus": "Under way using engine"
+ }
+ },
+ "encounters": [
+ {
+ "desiredEncounterType": "head-on",
+ }
+ ]
+}
+
The next example show how it is possible to give a range for the relative bearing between own ship and target ship
+Example 4: Assign range for beta:
+{
+ "title": "CR_GW",
+ "common_vector": 10.0,
+ "own_ship": {
+ "speed": 7.0,
+ "course": 0.0,
+ "position": {
+ "latitude": 58.763449,
+ "longitude": 10.490654
+ }
+ },
+ "encounter": [
+ {
+ "desired_encounter_type": "crossing-give-way",
+ "beta": [45.0,120.0]
+ }
+ ]
+}
+
To install Traffic Generator, run this command in your terminal:
+$ pip install trafficgen
+
This is the preferred method to install Traffic Generator, as it will always install the most recent stable release.
+If you don’t have pip installed, this Python installation guide can guide +you through the process.
+The sources for Traffic Generator can be downloaded from the https://github.com/dnv-opensource/ship-traffic-generator.
+You can either clone the public repository:
+$ git clone https://github.com/dnv-opensource/ship-traffic-generator
+
Once you have a copy of the source, you can install it with:
+$ python setup.py install
+
assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
+ t | ||
+ |
+ trafficgen | + |
+ |
+ trafficgen.check_land_crossing | + |
+ |
+ trafficgen.cli | + |
+ |
+ trafficgen.encounter | + |
+ |
+ trafficgen.marine_system_simulator | + |
+ |
+ trafficgen.plot_traffic_situation | + |
+ |
+ trafficgen.read_files | + |
+ |
+ trafficgen.ship_traffic_generator | + |
+ |
+ trafficgen.types | + |
+ |
+ trafficgen.utils | + |
+ |
+ trafficgen.write_traffic_situation_to_file | + |
Module with helper functions to determine if a generated path is crossing land.
+Find if path is crossing land.
+position_1: Ship position in latitude/longitude [rad]. +speed: Ship speed [m/s]. +course: Ship course [rad]. +lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]. +time_interval: The time interval the vessel should travel without crossing land [sec]
+is_on_land
+True if parts of the path crosses land.
+CLI for trafficgen package.
+Functions to generate encounters consisting of one own ship and one to many target ships. +The generated encounters may be of type head-on, overtaking give-way and stand-on and +crossing give-way and stand-on.
+Assign random (uniform) relative bearing beta between own ship +and target ship depending on type of encounter.
+encounter_type: Type of encounter
settings: Encounter settings
* Relative bearing between own ship and target ship seen from own ship [rad]
+Assign random (uniform) relative bearing beta between own ship +and target ship depending between the limits given by beta_limit.
+beta_limit: Limits for beta
* Relative bearing between own ship and target ship seen from own ship [rad]
+Randomly assign future position of target ship. If drawing a circle with radius +max_meeting_distance around future position of own ship, future position of +target ship shall be somewhere inside this circle.
+future, {north, east}
+lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
a given time in the future [m]
+future_position_target_ship
+Future position of target ship {north, east} [m]
+Assign random (uniform) sog to target ship depending on type of encounter.
+encounter_type: Type of encounter
own_ship_sog: Own ship sog [m/s]
min_target_ship_sog: Minimum target ship sog [m/s]
relative_sog_setting: Relative sog setting dependent on encounter [-]
* target_ship_sog
+Target ship sog [m/s]
+Assign random (uniform) vector time.
+vector_range: Minimum and maximum value for vector time
* vector_time
+Vector time [min]
+Calculate minimum vector length (target ship sog x vector). This will +ensure that ship sog is high enough to find proper situation.
+own_ship_position: Own ship initial position, latitudinal [rad] and longitudinal [rad]
own_ship_cog: Own ship initial cog
target_ship_position_future: Target ship future position
desired_beta: Desired relative bearing between
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* min_vector_length
+Minimum vector length (target ship sog x vector)
+Calculate relative bearing between own ship and target ship, both seen from +own ship and seen from target ship.
+position_own_ship: Own ship position {latitude, longitude} [rad]
heading_own_ship: Own ship heading [rad]
position_target_ship: Target ship position {latitude, longitude} [rad]
heading_target_ship: Target ship heading [rad]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* beta (relative bearing between own ship and target ship seen from own ship [rad])
* alpha (relative bearing between target ship and own ship seen from target ship [rad])
Calculate ship cog between two waypoints.
+waypoint_0: Dict, waypoint {latitude, longitude} [rad]
waypoint_1: Dict, waypoint {latitude, longitude} [rad]
* cog
+Ship cog [rad]
+Check encounter evolvement. The generated encounter should be the same type of +encounter (head-on, crossing, give-way) also some time before the encounter is started.
+own_ship: Own ship information such as initial position, sog and cog
target_ship: Target ship information such as initial position, sog and cog
desired_encounter_type: Desired type of encounter to be generated
settings: Encounter settings
* returns True if encounter ok, False if encounter not ok
+Randomly pick a target ship from a list of target ships.
+target_ships: list of target ships with static information
* The target ship, info of type, size etc.
+Define own ship based on information in desired traffic situation.
+desired_traffic_situation: Information about type of traffic situation to generate
own_ship_static: Static information of own ship.
encounter_settings: Necessary setting for the encounter
lat_lon0: Reference position [deg]
* own_ship
+Own ship
+Determine the colreg type based on alpha, relative bearing between target ship and own +ship seen from target ship, and beta, relative bearing between own ship and target ship +seen from own ship.
+alpha: relative bearing between target ship and own ship seen from target ship
beta: relative bearing between own ship and target ship seen from own ship
theta13_criteria: Tolerance for “coming up with” relative bearing
theta14_criteria: Tolerance for “reciprocal or nearly reciprocal cogs”, +“when in any doubt… assume… [head-on]”
theta15_criteria: Crossing aspect limit, used for classifying a crossing encounter
encounter
+* encounter classification
+Find start position of target ship using desired beta and vector length.
+own_ship_position: Own ship initial position, sog and cog
own_ship_cog: Own ship initial cog
target_ship_position_future: Target ship future position
target_ship_vector_length: vector length (target ship sog x vector)
desired_beta: Desired bearing between own ship and target ship seen from own ship
desired_encounter_type: Desired type of encounter to be generated
settings: Encounter settings
* start_position_target_ship (Dict, initial position of target ship {north, east} [m])
* start_position_found (0=position not found, 1=position found)
Generate an encounter.
+desired_encounter_type: Desired encounter to be generated
own_ship: Dict, information about own ship that will encounter a target ship
target_ships_static: List of target ships including static information that +may be used in an encounter
encounter_number: Integer, used to naming the target ships. target_ship_1,2 etc.
beta_default: User defined beta. If not set, this is None.
target ship. If not set, this is None.
+vector_time_default: User defined vector time. If not set, this is None.
settings: Encounter settings
* target_ship (target ship information, such as initial position, sog and cog)
* encounter_found (True=encounter found, False=encounter not found)
Top-level package for Traffic Generator.
+Nothing exposed at the moment, but will be done if needed.
+assign_beta()
assign_beta_from_list()
assign_future_position_to_target_ship()
assign_sog_to_target_ship()
assign_vector_time()
calculate_min_vector_length_target_ship()
calculate_relative_bearing()
calculate_ship_cog()
check_encounter_evolvement()
decide_target_ship()
define_own_ship()
determine_colreg()
find_start_position_target_ship()
generate_encounter()
flat2llh()
llh2flat()
ssa()
camel_to_snake()
check_input_units()
convert_keys_to_snake_case()
convert_settings_data_from_maritime_to_si_units()
convert_situation_data_from_maritime_to_si_units()
read_encounter_settings_file()
read_generated_situation_files()
read_own_ship_static_file()
read_situation_files()
read_target_ship_static_files()
Encounter
+EncounterClassification
EncounterClassification.Config
+EncounterClassification.model_computed_fields
EncounterClassification.model_config
EncounterClassification.model_fields
EncounterClassification.theta13_criteria
EncounterClassification.theta14_criteria
EncounterClassification.theta15
EncounterClassification.theta15_criteria
EncounterRelativeSpeed
EncounterRelativeSpeed.Config
+EncounterRelativeSpeed.crossing_give_way
EncounterRelativeSpeed.crossing_stand_on
EncounterRelativeSpeed.head_on
EncounterRelativeSpeed.model_computed_fields
EncounterRelativeSpeed.model_config
EncounterRelativeSpeed.model_fields
EncounterRelativeSpeed.overtaking_give_way
EncounterRelativeSpeed.overtaking_stand_on
EncounterSettings
EncounterSettings.Config
+EncounterSettings.classification
EncounterSettings.common_vector
EncounterSettings.disable_land_check
EncounterSettings.evolve_time
EncounterSettings.max_meeting_distance
EncounterSettings.model_computed_fields
EncounterSettings.model_config
EncounterSettings.model_fields
EncounterSettings.relative_speed
EncounterSettings.situation_length
EncounterSettings.vector_range
EncounterType
+OwnShipInitial
+SituationInput
+to_camel()
calculate_bearing_between_waypoints()
calculate_destination_along_track()
calculate_distance()
calculate_position_along_track_using_waypoints()
calculate_position_at_certain_time()
convert_angle_0_to_2_pi_to_minus_pi_to_pi()
convert_angle_minus_pi_to_pi_to_0_to_2_pi()
deg_2_rad()
knot_2_m_pr_s()
m_2_nm()
m_pr_s_2_knot()
min_2_s()
nm_2_m()
rad_2_deg()
The Marine Systems Simulator (MSS) is a Matlab and Simulink library for marine systems.
+It includes models for ships, underwater vehicles, unmanned surface vehicles, and floating structures. +The library also contains guidance, navigation, and control (GNC) blocks for real-time simulation. +The algorithms are described in:
+T. I. Fossen (2021). Handbook of Marine Craft Hydrodynamics and Motion Control. 2nd. Edition, +Wiley. ISBN-13: 978-1119575054
+Parts of the library have been re-implemented in Python and are found below.
+Compute longitude lon (rad), latitude lat (rad) and height h (m) for the +NED coordinates (xn,yn,zn).
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+The method computes longitude lon (rad), latitude lat (rad) and height h (m) for the +NED coordinates (xn,yn,zn) using a flat Earth coordinate system defined by the WGS-84 +ellipsoid. The flat Earth coordinate origin is located at (lon_0, lat_0) with reference +height h_ref in meters above the surface of the ellipsoid. Both height and h_ref +are positive upwards, while zn is positive downwards (NED). +Author: Thor I. Fossen +Date: 20 July 2018 +Revisions: 2023-02-04 updates the formulas for latitude and longitude
+xn: Ship position, north [m]
yn: Ship position, east [m]
zn=0.0: Ship position, down [m]
lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
h_ref=0.0: Flat earth coordinate with reference h_ref in meters above the surface +of the ellipsoid
* lat (Latitude [rad])
* lon (Longitude [rad])
* h (Height [m])
Compute (north, east) for a flat Earth coordinate system from longitude +lon (rad) and latitude lat (rad).
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+The method computes (north, east) for a flat Earth coordinate system from longitude +lon (rad) and latitude lat (rad) of the WGS-84 elipsoid. The flat Earth coordinate +origin is located at (lon_0, lat_0). +Author: Thor I. Fossen +Date: 20 July 2018 +Revisions: 2023-02-04 updates the formulas for latitude and longitude
+lat: Ship position in latitude [rad]
lon: Ship position in longitude [rad]
h=0.0: Ship height in meters above the surface of the ellipsoid
lat_0, lon_0: Flat earth coordinate located at (lon_0, lat_0)
h_ref=0.0: Flat earth coordinate with reference h_ref in meters above +the surface of the ellipsoid
* x_n (Ship position, north [m])
* y_n (Ship position, east [m])
* z_n (Ship position, down [m])
Return the “smallest signed angle” (SSA) or the smallest difference between two angles.
+Method taken from the MSS (Marine System Simulator) toolbox which is a Matlab/Simulink +library for marine systems.
+Examples
+angle = ssa(angle) maps an angle in rad to the interval [-pi pi)
+Author: Thor I. Fossen +Date: 2018-09-21
+angle: angle given in radius
* smallest_angle
+“smallest signed angle” or the smallest difference between two angles
+Functions to prepare and plot traffic situations.
+Add the ship to the map.
+ship: Ship information
vector_time: Vector time [sec]
lat_lon0=Reference point, latitudinal [rad] and longitudinal [rad]
map_plot: Instance of Map. If not set, instance is set to None
color: Color of the ship. If not set, color is ‘black’
* m
+Updated instance of Map.
+Add the ship to the plot.
+ship: Ship information
vector_time: Vector time [sec]
axes: Instance of figure axis. If not set, instance is set to None
color: Color of the ship. If not set, color is ‘black’
Calculate the outline of the ship pointing in the direction of ship course.
+position: {latitude}, {longitude} position of the ship [rad]
course: course of the ship [rad]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
ship_length: Ship length. If not given, ship length is set to 100
ship_width: Ship width. If not given, ship width is set to 15
* ship_outline_points
+Polygon points to draw the ship [deg]
+Calculate the arrow with length vector pointing in the direction of ship course.
+position: {latitude}, {longitude} position of the ship [rad]
direction: direction the arrow is pointing [rad]
vector_length: length of vector [m]
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* arrow_points
+Polygon points to draw the arrow [deg]
+Find the maximum deviation from the Reference point in north and east direction.
+ship: Ship information
max_value: maximum deviation in north, east direction
lat_lon0: Reference point, latitudinal [rad] and longitudinal [rad]
* max_value
+updated maximum deviation in north, east direction
+Plot a specific situation in map.
+traffic_situations: Generated traffic situations
situation_number: The specific situation to be plotted
Plot the traffic situations in one more figures.
+traffic_situations: Traffic situations to be plotted
col: Number of columns in each figure
row: Number of rows in each figure
Functions to read the files needed to build one or more traffic situations.
+Convert a camel case string to snake case.
+Check if input unit is specified, if not specified it is set to SI.
+Convert keys in a nested dictionary from camel case to snake case.
+Convert situation data which is given in maritime units to SI units.
+own_ship_file: Path to the own_ship_file file
* own_ship information
+Convert situation data which is given in maritime units to SI units.
+own_ship_file: Path to the own_ship_file file
* own_ship information
+Read encounter settings file.
+settings_file: Path to the encounter setting file
* encounter_settings
+Settings for the encounter
+Read the generated traffic situation files. Used for testing the trafficgen algorithm.
+situation_folder: Path to the folder where situation files are found
* situations
+List of desired traffic situations
+Read own ship static data from file.
+own_ship_file: Path to the own_ship_static_file file
* own_ship static information
+Read traffic situation files.
+situation_folder: Path to the folder where situation files are found
input_units: Specify if the inputs are given in si or maritime units
* situations
+List of desired traffic situations
+Read target ship static data files.
+target_ship_folder: Path to the folder where target ships are found
* target_ships_static
+List of different target ships with static information
+Functions to generate traffic situations.
+Generate a set of traffic situations using input files. +This is the main function for generating a set of traffic situations using input files +specifying number and type of encounter, type of target ships etc.
+situation_folder: Path to situation folder, files describing the desired situations
own_ship_file: Path to where own ships is found
target_ship_folder: Path to where different type of target ships is found
settings_file: Path to settings file
* traffic_situations (List of generated traffic situations.)
* One situation may consist of one or more encounters.
Domain specific data types used in trafficgen.
+Bases: BaseModel
Data type for an encounter.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Bases: BaseModel
Data type for the encounter classification.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Bases: BaseModel
Data type for relative speed between two ships in an encounter.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Bases: BaseModel
Data type for encounter settings.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Bases: Enum
Enumeration of encounter types.
+Bases: BaseModel
Data type for initial data for the own ship used for generating a situation.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Bases: BaseModel
Data type for inputs needed for generating a situations.
+Bases: object
For converting parameters written to file from snake to camel case.
+Return a camel case formated string from snake case string.
+A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
+Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
+Metadata about the fields defined on the model, +mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
+This replaces Model.__fields__ from Pydantic V1.
+Utility functions that are used by several other functions.
+Calculate the bearing in rad between two waypoints.
+position_prev{latitude, longitude}: Previous waypoint [rad]
position_next{latitude, longitude}: Next waypoint [rad]
* bearing
+Bearing between waypoints [m]
+Calculate the destination along the track between two waypoints when distance along the track is given.
+position_prev{latitude, longitude}: Previous waypoint [rad]
distance: Distance to travel [m]
bearing: Bearing from previous waypoint to next waypoint [rad]
* destination{latitude, longitude}
+Destination along the track [rad]
+Calculate the distance in meter between two waypoints.
+position_prev{latitude, longitude}: Previous waypoint [rad]
position_next{latitude, longitude}: Next waypoint [rad]
* distance
+Distance between waypoints [m]
+Calculate the position of the ship at a given time based on initial position +and delta time, and constant speed and course.
+position{latitude, longitude}: Initial ship position [rad]
speed: Ship speed [m/s]
course: Ship course [rad]
delta_time: Delta time from now to the time new position is being calculated [sec]
* position{latitude, longitude}
+Estimated ship position in delta time minutes [rad]
+Calculate the position of the ship at a given time based on initial position +and delta time, and constant speed and course.
+position{latitude, longitude}: Initial ship position [rad]
speed: Ship speed [m/s]
course: Ship course [rad]
delta_time: Delta time from now to the time new position is being calculated [minutes]
* position{latitude, longitude}
+Estimated ship position in delta time minutes [rad]
+Convert an angle given in the region 0 to 2*pi degrees to an +angle given in the region -pi to pi degrees.
+angle_2_pi: Angle given in the region 0 to 2pi radians
* angle_pi
+Angle given in the region -pi to pi radians
+Convert an angle given in the region -pi to pi degrees to an +angle given in the region 0 to 2pi radians.
+angle_pi: Angle given in the region -pi to pi radians
* angle_2_pi
+Angle given in the region 0 to 2pi radians
+Convert angle given in degrees to angle give in radians.
+angle_in_degrees: Angle given in degrees
* angle given in radians
+Angle given in radians
+Convert ship speed in knots to meters pr second.
+speed_in_knot: Ship speed given in knots
* speed_in_m_pr_s
+Ship speed in meters pr second
+Convert length given in meters to length given in nautical miles.
+length_in_m: Length given in meters
* length_in_nm
+Length given in nautical miles
+Convert ship speed in knots to meters pr second.
+speed_in_m_pr_s: Ship speed given in meters pr second
* speed_in_knot
+Ship speed in knots
+Convert time given in minutes to time given in seconds.
+time_in_min: Time given in minutes
* time_in_s
+Time in seconds
+Functions to clean traffic situations data before writing it to a json file.
+Convert ship data which is given in SI units to maritime units.
+ship: Ship data
* ship
+Converted ship data
+Convert situation data which is given in SI units to maritime units.
+situation: Traffic situation data
* situation
+Converted traffic situation data
+Write traffic situations to json file.
+traffic_situations: Traffic situations to be written to file
write_folder: Folder where the json files is to be written
To use Traffic Generator in a project:
+import trafficgen
+
To use Traffic Generator as a command line tool for generating traffic situations, write:
+trafficgen gen-situation
+
The command line tool takes different input options:
+-s, --situations PATH Folders with situations (default=./baseline_situations_input/)
+-t, --targets PATH Folder with target configurations (default=./target_ships/)
+-c, --settings PATH Path to settings file (default=./settings/encounter_settings.json)
+--visualize Plot visualization
+--col INTEGER Number of columns for plot, may be used with visualize (default=10)
+--row INTEGER Number of rows for plot, may be used with visualize (default=6)
+--visualize-situation INTEGER Plot individual traffic situation, specify INTEGER value
+-o, --output PATH Output folder (default=None)
+--help Show this message and exit.
+
Example:
+trafficgen gen-situation -s ./data/example_situations_input -o ./data/test_output_1
+
When generating situations without specifying where the desired situations (--situation
) are found, the
+default path, which is default=./baseline_situations_input/
, will be used.
The baseline situations are a set of generic traffic situations covering head-on, overtaking stand-on/give-way
+and crossing stand-on/give-way encounters. To cover the combination of encounters for 1, 2 and 3 target ships,
+there are in total 55 baseline situations. The input files for generating these situations are found in
+./baseline_situations_input/
All the generated situations are displayed if using --visualize
. This will pop up one or more plot windows,
+which show all the traffic situations. The number of colums and rows for the plots (per figure) can be specified by
+using --col
and --row
, respectively.
A specific encounter is visualized by using --visualize-situation INTEGER
, e.g.:
trafficgen gen-situation -s ./data/example_situations_input -o ./data/test_output_1 --visualize-situation 2
+
This will open a browser window/tab with an OpenStreetMap background and the traffic situation +radar plot as an overlay. +Note that the integer needs to be within the range of the number of generated situations, +for example 1 - 12 if you generated 12 situations.
+