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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions docs/endpoints/about-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ The endpoints module allows you to query Vortexa's data.
The VortexaSDK currently contains the following endpoints:

1. Cargo Movements
1. Voyages
1. Charterers
1. Geographies
1. Products
1. Vessels
1. Cargo Time Series
1. EIA Forecasts
1. Tonne-miles
1. Vessel Availability
1. Crude Onshore Inventories
1. Freight Pricing
1. Vessel Summary
1. Vessel Positions
1. Canal transit
1. Canal transit Time Series
1. Refineries
2. Voyages
3. Charterers
4. Geographies
5. Products
6. Vessels
7. Cargo Time Series
8. EIA Forecasts
9. Tonne-miles
10. Vessel Availability
11. Crude Onshore Inventories
12. Freight Pricing
13. Vessel Summary
14. Vessel Positions
15. Canal transit
16. Canal transit Time Series
17. Refineries
18. Voyage Calculator

Each endpoint offers either one, or both, of two different functionalities:

Expand Down
132 changes: 132 additions & 0 deletions tests/endpoints/test_voyage_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from tests.testcases import TestCaseUsingRealAPI
from vortexasdk import VoyageCalculator

ras_tanura = "539db1548407fd97024391d01a6a2be239b1100f070a137d54a79907d03db6c8"
rotterdam = "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e"


class TestVoyageCalculator(TestCaseUsingRealAPI):
def test_calculate_eta(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
)

result_list = result.to_list()
assert len(result_list) == 1
assert "ETA" in result_list[0]
assert "ETD" in result_list[0]
assert "speed" in result_list[0]
assert "duration" in result_list[0]

def test_calculate_etd(self):
result = VoyageCalculator().search(
type="ETD",
vessel_status="vessel_status_ballast",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETA="2024-04-01T00:00:00.000Z",
speed=12,
)

result_list = result.to_list()
assert len(result_list) == 1
assert "ETD" in result_list[0]

def test_calculate_speed(self):
result = VoyageCalculator().search(
type="speed",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
ETA="2024-04-01T00:00:00.000Z",
)

result_list = result.to_list()
assert len(result_list) == 1
assert "speed" in result_list[0]
assert result_list[0]["speed"] > 0

def test_calculate_with_latlong_origin(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin={"lat": 26.6, "long": 50.1},
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
)

result_list = result.to_list()
assert len(result_list) == 1

def test_calculate_with_avoid_zone(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
avoid_zone=["Suez Canal"],
)

result_list = result.to_list()
assert len(result_list) == 1

def test_to_df(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
)

df = result.to_df()
assert len(df) == 1
assert "ETA" in df.columns
assert "ETD" in df.columns
assert "speed" in df.columns
assert "duration" in df.columns

def test_to_df_with_columns(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
)

df = result.to_df(columns=["ETA", "speed"])
assert len(df.columns) == 2

def test_calculate_with_delay_factor(self):
result = VoyageCalculator().search(
type="ETA",
vessel_status="vessel_status_laden_known",
origin=ras_tanura,
destination=rotterdam,
vessel_class="oil_vlcc",
ETD="2024-03-01T00:00:00.000Z",
speed=12,
voyage_delay_factor=0.2,
)

result_list = result.to_list()
assert len(result_list) == 1
8 changes: 8 additions & 0 deletions vortexasdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
VoyagesCongestionBreakdown,
VoyagesTopHits,
VoyagesSearchEnriched,
VoyageCalculator,
VoyageCalculatorType,
VoyageCalculatorVesselStatus,
VoyageCalculatorAvoidZone,
VesselSummary,
VesselPositions,
Refineries,
Expand Down Expand Up @@ -102,6 +106,10 @@
"VoyagesCongestionBreakdown",
"VoyagesTopHits",
"VoyagesSearchEnriched",
"VoyageCalculator",
"VoyageCalculatorType",
"VoyageCalculatorVesselStatus",
"VoyageCalculatorAvoidZone",
"VesselSummary",
"VesselPositions",
"Refineries",
Expand Down
11 changes: 11 additions & 0 deletions vortexasdk/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
)
from vortexasdk.endpoints.voyages_top_hits import VoyagesTopHits
from vortexasdk.endpoints.voyages_search_enriched import VoyagesSearchEnriched
from vortexasdk.endpoints.voyage_calculator import (
VoyageCalculator,
VoyageCalculatorType,
VoyageCalculatorVesselStatus,
VoyageCalculatorAvoidZone,
)
from vortexasdk.endpoints.fixtures import Fixtures
from vortexasdk.endpoints.vessel_summary import VesselSummary
from vortexasdk.endpoints.vessel_positions import VesselPositions
Expand Down Expand Up @@ -154,6 +160,11 @@
"AnywhereFreightPricingTopPortsOrigin",
"AnywhereFreightPricingVesselClassesDetails",
"AnywhereFreightPricingForecastExplanation",
"VoyageCalculator",
# Voyage Calculator types
"VoyageCalculatorType",
"VoyageCalculatorVesselStatus",
"VoyageCalculatorAvoidZone",
# AFP types for user type annotations
"AfpAvoidZone",
"AfpExplanationFrequency",
Expand Down
2 changes: 2 additions & 0 deletions vortexasdk/endpoints/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@

FIXTURES = "/v5/search/fixtures"

VOYAGE_CALCULATOR = "/v5/voyages/voyage-calculator"

CANAL_TRANSIT = "/v5/canal-transit"
CANAL_TRANSIT_SEARCH = "/v5/canal-transit/search"
CANAL_TRANSIT_TIME_SERIES = "/v5/canal-transit/time-series"
Expand Down
143 changes: 143 additions & 0 deletions vortexasdk/endpoints/voyage_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from typing import Any, Dict, List, Optional, Union

from vortexasdk.endpoints.endpoints import VOYAGE_CALCULATOR
from vortexasdk.endpoints.voyage_calculator_result import (
VoyageCalculatorResult,
)
from vortexasdk.operations import Search

from typing_extensions import Literal

VoyageCalculatorType = Literal["speed", "ETA", "ETD"]
VoyageCalculatorVesselStatus = Literal[
"vessel_status_ballast",
"vessel_status_laden_known",
"vessel_status_laden_unknown",
]
VoyageCalculatorAvoidZone = Literal["Panama Canal", "Suez Canal"]

LatLong = Dict[str, float]


class VoyageCalculator(Search):
"""
Voyage Calculator endpoint.

Calculates voyage routes, ETAs, ETDs, or speeds between an origin and destination.
The calculator accounts for vessel class, laden/ballast status, canal avoidance
zones, and optional waypoints.
"""

def __init__(self) -> None:
Search.__init__(self, VOYAGE_CALCULATOR)

def search(
self,
type: VoyageCalculatorType,
vessel_status: VoyageCalculatorVesselStatus,
origin: Union[str, LatLong],
destination: Union[str, LatLong],
vessel_id: Optional[str] = None,
vessel_class: Optional[str] = None,
waypoints: Optional[List[str]] = None,
ETA: Optional[str] = None,
ETD: Optional[str] = None,
speed: Optional[float] = None,
avoid_zone: Optional[List[VoyageCalculatorAvoidZone]] = None,
voyage_delay_factor: Optional[float] = None,
) -> "VoyageCalculatorResult":
"""
Calculate a voyage route between an origin and destination.

# Arguments
type: The type of calculation to perform. One of:
- `'speed'`: Calculate the speed required given an ETD and ETA.
- `'ETA'`: Calculate the ETA given an ETD and speed.
- `'ETD'`: Calculate the ETD given an ETA and speed.

vessel_status: Whether the vessel is laden or ballast. One of:
`'vessel_status_ballast'`, `'vessel_status_laden_known'`, `'vessel_status_laden_unknown'`.

origin: The origin of the voyage. Can be either:
- A string ID (vessel ID for current position, or geography ID for centroid).
- A dict with `lat` and `long` keys, e.g. `{"lat": 51.9, "long": 4.5}`.

destination: The destination of the voyage. Can be either:
- A geography ID string.
- A dict with `lat` and `long` keys, e.g. `{"lat": 29.9, "long": 32.5}`.

vessel_id: A vessel identifier (IMO, MMSI, vessel name, or Vortexa ID).
Used to determine the vessel's current position (when origin is a vessel ID)
and deadweight tonnage for routing.

vessel_class: Vessel class used to determine DWT when `vessel_id` is not provided.
Examples: `'oil_vlcc'`, `'oil_suezmax_lr3'`, `'oil_aframax_lr2'`, `'lng_conventional_lng'`.

waypoints: A list of geography IDs representing intermediate waypoints.

ETA: Estimated time of arrival as an ISO 8601 date string (e.g. `'2024-03-15T00:00:00.000Z'`).
Required when `type` is `'speed'` or `'ETD'`.

ETD: Estimated time of departure as an ISO 8601 date string (e.g. `'2024-03-01T00:00:00.000Z'`).
Required when `type` is `'speed'` or `'ETA'`.

speed: Speed in knots. Required when `type` is `'ETA'` or `'ETD'`.

avoid_zone: A list of zones to avoid in routing. Options: `'Panama Canal'`, `'Suez Canal'`.

voyage_delay_factor: A factor between 0 and 1 to simulate increased voyage duration.
For example, 0.2 means 120% of the original duration.

# Returns
`VoyageCalculatorResult`

# Example

_Calculate ETA for a VLCC travelling from Ras Tanura to Rotterdam at 12 knots._

```python
>>> from vortexasdk import VoyageCalculator
>>> ras_tanura = "539db1548407fd97024391d01a6a2be239b1100f070a137d54a79907d03db6c8"
>>> rotterdam = "68faf65af1345067f11dc6723b8da32f00e304a6f33c000118fccd81947deb4e"
>>> result = VoyageCalculator().search(
... type="ETA",
... vessel_status="vessel_status_laden_known",
... origin=ras_tanura,
... destination=rotterdam,
... vessel_class="oil_vlcc",
... ETD="2024-03-01T00:00:00.000Z",
... speed=12,
... )
>>> df = result.to_df()

```

Returns a DataFrame with columns:

| | ETA | ETD | duration | speed |
|---:|:-------------------------|:-------------------------|-----------:|--------:|
| 0 | 2024-03-25T14:30:00.000Z | 2024-03-01T00:00:00.000Z | 590.5 | 12 |

"""
api_params: Dict[str, Any] = {
"type": type,
"vessel_status": vessel_status,
"origin": origin,
"destination": destination,
"vessel_id": vessel_id,
"vessel_class": vessel_class,
"waypoints": waypoints,
"ETA": ETA,
"ETD": ETD,
"speed": speed,
"avoid_zone": avoid_zone,
"voyage_delay_factor": voyage_delay_factor,
}

response = super().search_with_client(
response_type="breakdown", **api_params
)

return VoyageCalculatorResult(
records=response["data"], reference=response.get("reference", {})
)
Loading
Loading