Isochrone

Wind-aware reachability boundaries. Given a starting state, a time budget, and (optionally) a recovery destination + on-station observation requirement, compute the polygon around the start where the aircraft can operate within the time budget.

Modes

  • one_way — single-leg reach. “Where can I be after budget?”

  • round_trip — out and back from the same place. Default; the recovery destination defaults to start.

  • return_safe — out, observe, recover at a different airport within budget. Required for missions where the target is far from base.

Compute

compute_isochrone(aircraft, start, budget, *, cruise_altitude=None, on_station_altitude=None, start_time=None, wind_source=None, return_destination=None, mode='round_trip', on_station_time=<Quantity(0, 'minute')>, reserve=<Quantity(0, 'minute')>, azimuth_resolution_deg=5.0, distance_tolerance_nmi=0.5, wind_sampling='cruise_midpoint', wind_sample_spacing=<Quantity(100, 'nautical_mile')>, max_wind_samples_per_leg=20, ray_strategy='uniform', adaptive_spacing_nmi=None, max_adaptive_rays=180)[source]

Compute a wind-aware isochrone around start.

Parameters:
  • aircraft (Aircraft) – The aircraft model. Climb / cruise / descent profiles and speed schedules are read from this object.

  • start (Union[Airport, Waypoint]) – Starting state. Accepts either an Airport (resolved to a Waypoint at runway elevation, heading 0°) or a Waypoint directly. When a Waypoint is provided, altitude_msl is required. Pass an Airport for pre-flight planning; pass a Waypoint with a non-runway altitude for in-flight re-tasking.

  • budget (Quantity) – Total time available (e.g. endurance remaining).

  • cruise_altitude (Optional[Quantity]) – Transit / observation altitude. Defaults to start.altitude_msl. v1 treats this as the single altitude for both the outbound transit and on-station phase.

  • on_station_altitude (Optional[Quantity]) – Reserved for future multi-altitude support. v1 requires this to be None or equal to cruise_altitude; a distinct value raises HyPlanValueError.

  • start_time (Optional[datetime]) – UTC datetime when the sortie begins. Used to query time-varying wind providers. Defaults to datetime.now(UTC).

  • wind_source (Optional[WindField]) – A WindField provider. Defaults to StillAirField.

  • return_destination (Union[Airport, Waypoint, None]) – Where the aircraft must reach by budget reserve. Required for "return_safe"; defaults to start for "round_trip"; ignored (with a warning) for "one_way". Accepts an Airport (resolved to its runway-elevation Waypoint) or a Waypoint.

  • mode (str) – "one_way" | "round_trip" | "return_safe".

  • on_station_time (Quantity) – Required dwell at the target.

  • reserve (Quantity) – Mandatory unflown time buffer (fuel/reserve fuel).

  • azimuth_resolution_deg (float) – Step in degrees between sweep rays. Default 5.0 → 72 rays.

  • distance_tolerance_nmi (float) – Binary-search stop criterion (boundary distance precision in nmi).

  • wind_sampling (str) – How wind is sampled per leg — "cruise_midpoint" (default; one sample at cruise midpoint, climb/descent still-air), "phase_midpoint" (adds one wind sample each at the climb-segment-mid and descent-segment-mid altitudes, redistributing ground distance between phases), or "segmented_cruise" (splits cruise into multiple subsegments, ~``wind_sample_spacing`` apart). See the module docstring’s “Wind sampling” section.

  • wind_sample_spacing (Quantity) – Cruise-segment spacing for wind_sampling="segmented_cruise" (ignored otherwise). Default 100 nmi.

  • max_wind_samples_per_leg (int) – Hard cap on cruise subsegments to keep _leg_time cost bounded for very long legs. Default 20.

  • ray_strategy (str) – How sweep azimuths are chosen. "uniform" preserves the original fixed-angle sweep. "auto" uses ellipse-aware seed rays when start and recovery differ, otherwise uniform rays. "adaptive" adds midpoint rays where solved boundary chords exceed adaptive_spacing_nmi; "ellipse" and "ellipse_adaptive" force the two-focus seed distribution when possible.

  • adaptive_spacing_nmi (Optional[float]) – Target maximum boundary chord length for adaptive refinement. Defaults to 100 nmi when an adaptive strategy is selected.

  • max_adaptive_rays (int) – Hard cap on adaptive ray count.

Return type:

GeoDataFrame

Returns:

A geopandas.GeoDataFrame in EPSG:4326, one row per ray, with Point geometries on the isochrone boundary. See the module docstring for the column schema. Invocation context is stashed in gdf.attrs.

Raises:
  • HyPlanValueError – For invalid arguments (see input validation in the plan / tests).

  • HyPlanRuntimeError – If the expanding bracket exceeds 20000 nmi (pathological wind field or aircraft).

compute_concentric_isochrones(aircraft, start, budgets, *, cruise_altitude=None, on_station_altitude=None, start_time=None, wind_source=None, return_destination=None, mode='round_trip', on_station_time=<Quantity(0, 'minute')>, reserve=<Quantity(0, 'minute')>, azimuth_resolution_deg=5.0, distance_tolerance_nmi=0.5, wind_sampling='cruise_midpoint', wind_sample_spacing=<Quantity(100, 'nautical_mile')>, max_wind_samples_per_leg=20, ray_strategy='uniform', adaptive_spacing_nmi=None, max_adaptive_rays=180)[source]

Compute multiple isochrone contours in one call (e.g., 1/2/3 hr).

Sweeps budgets in ascending order and seeds each budget’s bracket search from the previous (smaller) budget’s converged distances per ray, exploiting the fact that feasibility is monotone in budget. Total cost is roughly O(M + N) rather than O(M·N) for M budgets and N rays.

Parameters:
Return type:

GeoDataFrame

:param : :type start_time: Optional[datetime] :param start_time: :type wind_source: Optional[WindField] :param wind_source: :type return_destination: Union[Airport, Waypoint, None] :param return_destination: :type mode: str :param mode: :param : :type on_station_time: Quantity :param on_station_time: :type reserve: Quantity :param reserve: :type azimuth_resolution_deg: float :param azimuth_resolution_deg: :param : :type distance_tolerance_nmi: float :param distance_tolerance_nmi: :type wind_sampling: str :param wind_sampling: :type wind_sample_spacing: Quantity :param wind_sample_spacing: :param : :type max_wind_samples_per_leg: int :param max_wind_samples_per_leg: :type ray_strategy: str :param ray_strategy: :param : :type adaptive_spacing_nmi: Optional[float] :param adaptive_spacing_nmi: same semantics as

Parameters:
Returns:

4326, one row per (budget, azimuth) combination. Columns match compute_isochrone() plus budget_min and budget_hr tagging which contour each row belongs to. gdf.attrs["budgets_hr"] lists budgets in computed order (ascending).

Return type:

A geopandas.GeoDataFrame in EPSG

Refuel-aware concentric reach is intentionally out of scope — sweeping (sortie, day) tuples is a different UI problem.

compute_refuel_isochrone(aircraft, start, sortie_budget, *, flight_day_budget, cruise_altitude=None, refuel_airports, refuel_time=<Quantity(60, 'minute')>, return_destination=None, mode='return_safe', on_station_altitude=None, on_station_time=<Quantity(0, 'minute')>, reserve=<Quantity(0, 'minute')>, max_refuel_stops=1, start_time=None, wind_source=None, azimuth_resolution_deg=5.0, distance_tolerance_nmi=0.5, wind_sampling='cruise_midpoint', wind_sample_spacing=<Quantity(100, 'nautical_mile')>, max_wind_samples_per_leg=20)[source]

Wind-aware isochrone with a single optional refuel stop.

Two clocks are tracked: sortie_budget per fuel cycle (resets after each refuel) and flight_day_budget total wall-clock (does not reset, includes refuel time). reserve applies per fuel cycle, not to the day budget.

For every azimuth, three itinerary templates are evaluated and the one reaching the largest distance wins:

  • directstart target recovery

  • outbound_refuel(R)start R target recovery

  • return_refuel(R)start target R recovery

Each refuel airport in refuel_airports is tested in both placements (where eligible). v1 limits max_refuel_stops to 1 — a single sortie touches at most two tanks.

Parameters:
  • aircraft (Aircraft) – Aircraft model.

  • start (Union[Airport, Waypoint]) – Departure point (Airport or Waypoint).

  • sortie_budget (Quantity) – Per-fuel-cycle endurance. Positional to mirror compute_isochrone(..., budget=...).

  • flight_day_budget (Quantity) – Total wall-clock budget (kw-only — the new required clock).

  • refuel_airports (Sequence[Union[Airport, Waypoint]]) – Pre-cleared refuel candidates. Empty raises; use compute_isochrone if no refuel option applies.

  • refuel_time (Quantity) – Per-refuel wall-clock cost. Default 60 min.

  • return_destination (Union[Airport, Waypoint, None]) – Recovery field. Required for "return_safe"; defaults to start for "round_trip".

  • mode (str) – "return_safe" (default) or "round_trip". "one_way" is rejected — multi-hop one-way reach is deferred.

  • max_refuel_stops (int) – v1 must be 1. Reserved for future chained refuels.

  • compute_isochrone. (Other kwargs match)

  • cruise_altitude (Quantity | None)

  • on_station_altitude (Quantity | None)

  • on_station_time (Quantity)

  • reserve (Quantity)

  • start_time (datetime | None)

  • wind_source (WindField | None)

  • azimuth_resolution_deg (float)

  • distance_tolerance_nmi (float)

  • wind_sampling (str)

  • wind_sample_spacing (Quantity)

  • max_wind_samples_per_leg (int)

Return type:

GeoDataFrame

Returns:

A GeoDataFrame with one row per ray. Per-leg time columns (start_to_target_time_min, start_to_refuel_time_min, refuel_to_target_time_min, target_to_refuel_time_min, refuel_to_return_time_min, target_to_return_time_min) are NaN unless the itinerary uses that leg. itinerary, refuel_airport, refuel_count, day_total_time_min, sortie_cycle_1_min, sortie_cycle_2_min, sortie_margin_min, day_margin_min, limiting_leg describe the chosen path. limiting_leg values for refuel results are "sortie" | "flight_day" | "both" | "slack" | "unflyable" (a different value vocabulary than compute_isochrone’s, but the column name is shared so consumers holding both gdfs use the same accessor).

gdf.attrs includes refuel_airports_evaluated, refuel_airports_unreachable, and refuel_airports_used.

evaluate_target_reachability(aircraft, start, target, *, sortie_budget, flight_day_budget=None, cruise_altitude=None, refuel_airports=(), refuel_time=<Quantity(60, 'minute')>, return_destination=None, mode='return_safe', on_station_altitude=None, on_station_time=<Quantity(0, 'minute')>, reserve=<Quantity(0, 'minute')>, start_time=None, wind_source=None, wind_sampling='cruise_midpoint', wind_sample_spacing=<Quantity(100, 'nautical_mile')>, max_wind_samples_per_leg=20)[source]

Evaluate reachability of a single target via direct + refuel paths.

The complement of compute_refuel_isochrone(): rather than sweeping azimuths to find the boundary, this asks “given this target, which itineraries reach it within budget?” and reports every feasible option.

Parameters:
Return type:

dict

:param : :type refuel_airports: Sequence[Union[Airport, Waypoint]] :param refuel_airports: :type refuel_time: Quantity :param refuel_time: :type return_destination: Union[Airport, Waypoint, None] :param return_destination: :type mode: str :param mode: :param : :type on_station_altitude: Optional[Quantity] :param on_station_altitude: :type on_station_time: Quantity :param on_station_time: :type reserve: Quantity :param reserve: :type start_time: Optional[datetime] :param start_time: :param : :type wind_source: Optional[WindField] :param wind_source: same semantics as

compute_refuel_isochrone(). flight_day_budget defaults to sortie_budget (the day clock then never binds). refuel_airports may be empty — only the direct itinerary is then evaluated.

Parameters:
Returns:

reachable: bool. best: itinerary diagnostic dict for the chosen route,

or None when reachable is False.

alternatives: list of dicts for the other feasible

itineraries, sorted by ascending day_total_time_min.

unreachable_reason: short human-readable string when

reachable is False, else None.

target_lat, target_lon: coordinates of the target.

Return type:

Dict with keys

Concentric reach

compute_concentric_isochrones sweeps multiple budgets in one call and amortizes the bracket search across them — each budget seeds the next larger budget’s lower bound, so total cost is roughly O(M+N) rather than O(M·N) for M budgets and N rays. Returns one stacked GeoDataFrame tagged with budget_min / budget_hr columns.

Refuel-aware reach

compute_refuel_isochrone extends the standard isochrone with a single optional refuel stop drawn from a list of pre-cleared candidates. Two clocks are tracked: a per-fuel-cycle sortie_budget (resets after each refuel) and a total wall-clock flight_day_budget (does not reset and absorbs refuel_time). reserve applies per fuel cycle only.

For every azimuth, three itineraries are evaluated and the most-extending one wins:

  • directstart target recovery

  • outbound_refuel(R)start R target recovery

  • return_refuel(R)start target R recovery

v1 limits max_refuel_stops to 1 (a single sortie touches at most two tanks). Chained refuels are deferred.

Single-target reachability

evaluate_target_reachability is the complement of compute_refuel_isochrone: rather than sweeping azimuths to find the boundary, it asks “given this target, which itineraries reach it?” and returns a structured dict with the best route plus all feasible alternatives.

Helpers

isochrone_polygon(gdf)[source]

Connect the isochrone boundary points into a closed polygon.

Boundary points are taken in order of azimuth_deg. Result is returned in EPSG:4326 (matches the input GeoDataFrame’s CRS).

Return type:

Polygon

Parameters:

gdf (GeoDataFrame)

plot_isochrone(gdf, base_map=None, *, color='steelblue', fill_opacity=0.2, tiles='OpenStreetMap', zoom_start=6)[source]

Render the isochrone on a Folium map.

Parameters:
  • gdf (GeoDataFrame) – A GeoDataFrame returned by compute_isochrone() or compute_refuel_isochrone(). Refuel results are recognized via gdf.attrs["refuel_airports_evaluated"] and trigger refuel-airport markers + per-itinerary dot coloring (see color below).

  • base_map (Optional[Map]) – Optional existing Folium map to add to. If None, a new one is created centered on start with the tiles basemap loaded.

  • color (str) – Polygon stroke + fill color. For refuel-isochrone results, this color is also used for "direct" ray dots, but "outbound_refuel" and "return_refuel" dots use a fixed palette (steel blue / red) so the itinerary structure is legible regardless of the polygon color.

  • fill_opacity (float) – Polygon fill opacity (0–1).

  • tiles (str) – Folium basemap identifier when constructing a new map. Built-in choices include "OpenStreetMap" (default), "CartoDB positron" (clean light gray, good for science figures), "CartoDB dark_matter", and "Esri.WorldImagery" (satellite). Ignored when base_map is supplied.

  • zoom_start (int) – Initial zoom level (Folium scale 1–18). Ignored when base_map is supplied.

Return type:

Map

Returns:

The Folium map.

plot_isochrone_static(layers, *, points=(), title='', figsize=(9, 9), wind_field=None, wind_altitude=None, wind_time=None, wind_caption=None, basemap_scale='50m', show_refuel_markers=True)[source]

Render one or more isochrones on a static Cartopy basemap.

Companion to hyplan.plot_isochrone (the Folium / interactive plotter). This produces publication-quality figures with Natural Earth vector basemaps and is the right choice for static output (papers, briefings, reports).

Parameters:
  • layers – Either a single gpd.GeoDataFrame from compute_isochrone(), compute_refuel_isochrone(), or compute_concentric_isochrones(); or an iterable of (gdf, color, label) tuples for layered comparison (e.g., still-air vs. windy). When a single concentric GDF is passed without explicit splitting, the function auto-groups rows by budget_hr and renders each contour with a viridis-indexed color.

  • points – Iterable of {"lat", "lon", "label", "color", "marker", "markersize"} dicts for airports / waypoints of interest.

  • title (str) – Figure title.

  • figsize (Tuple[float, float]) – Matplotlib figsize.

  • wind_field – Optional WindField provider — when supplied, samples wind on a grid covering the plot extent and overlays as aviation-convention wind barbs (half-barb 5 kt, full barb 10 kt, flag 50 kt).

  • wind_altitude – Altitude (Quantity) for wind queries; required when wind_field is supplied.

  • wind_time – UTC datetime to query the wind at; defaults to now.

  • wind_caption (Optional[str]) – Optional caption drawn in the corner.

  • basemap_scale (str) – Natural Earth resolution: "110m" (lowest, ships with cartopy), "50m" (default), or "10m" (highest; downloads on first use).

  • show_refuel_markers (bool) – When True (default) and the input GDF has attrs["refuel_airports_evaluated"], draw refuel airport markers (used = darkblue, evaluated-not-used = gray, unreachable = light gray).

Returns:

(fig, ax) so the caller can add overlays after.

plot_isochrone is the interactive Folium plotter. plot_isochrone_static (in hyplan/plotting.py) is the publication-quality Cartopy plotter — accepts either a single GDF or a list of (gdf, color, label) tuples for layered comparison, and auto-handles concentric and refuel results.

See also

  • flight_plan — once you’ve selected a target site inside the isochrone, hand that target to compute_flight_plan to produce the actual sortie.

  • The isochrone tutorial notebook walks through all three modes plus a multi-aircraft fleet comparison.