···11+ISC License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission to use, copy, modify, and/or distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1010+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1111+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1212+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1313+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1515+PERFORMANCE OF THIS SOFTWARE.
+116
README.md
···11+# cgr
22+33+Contact Graph Routing for time-varying satellite networks.
44+55+## Overview
66+77+CGR computes routes through scheduled communication contacts in DTN
88+(Delay-Tolerant Networking) environments. Unlike traditional routing where
99+links are persistent, CGR handles networks where connectivity is intermittent
1010+but predictable - such as satellite constellations, deep space networks, and
1111+scheduled terrestrial links.
1212+1313+The algorithm implements CCSDS Schedule-Aware Bundle Routing (SABR) using
1414+Dijkstra's shortest-path algorithm adapted for time-varying graphs where edges
1515+(contacts) have temporal validity windows.
1616+1717+## Features
1818+1919+- **Contact Plans**: Define scheduled communication windows between nodes
2020+- **Time-aware routing**: Routes respect contact start/end times
2121+- **Propagation delay**: Supports one-way light time (OWLT) for deep space
2222+- **Multiple routes**: Find alternative paths for redundancy
2323+- **Bottleneck capacity**: Track minimum capacity across route hops
2424+2525+## Installation
2626+2727+```
2828+opam install cgr
2929+```
3030+3131+## Usage
3232+3333+```ocaml
3434+open Cgr
3535+3636+(* Define nodes *)
3737+let earth = Node.v "EARTH"
3838+let mars = Node.v "MARS"
3939+let relay = Node.v "RELAY"
4040+4141+(* Define contacts (scheduled communication windows) *)
4242+let contacts = [
4343+ Contact.v ~from:earth ~to_:relay ~start:0. ~stop:100. ~rate:1_000_000. ();
4444+ Contact.v ~from:relay ~to_:mars ~start:50. ~stop:150. ~rate:500_000.
4545+ ~owlt:600. (); (* 10 min light time *)
4646+]
4747+4848+(* Create contact plan and find route *)
4949+let plan = Contact_plan.of_list contacts
5050+5151+let () =
5252+ match find_route plan ~src:earth ~dst:mars ~time:0. with
5353+ | None -> print_endline "No route available"
5454+ | Some route ->
5555+ Format.printf "Route found: %a@." Route.pp route;
5656+ Format.printf "Arrival time: %.0f seconds@." (Route.arrival_time route)
5757+```
5858+5959+## API
6060+6161+### Nodes
6262+6363+- `Node.v name` - Create a node identifier
6464+- `Node.name node` - Get the node's name
6565+6666+### Contacts
6767+6868+- `Contact.v ~from ~to_ ~start ~stop ~rate ?owlt ()` - Create a contact
6969+- `Contact.is_active contact ~time` - Check if contact is active
7070+- `Contact.capacity contact` - Maximum bytes transmittable
7171+7272+### Contact Plans
7373+7474+- `Contact_plan.of_list contacts` - Create a plan from contacts
7575+- `Contact_plan.contacts_from plan node` - Get outgoing contacts
7676+- `Contact_plan.active_at plan ~time` - Get contacts active at time
7777+7878+### Routing
7979+8080+- `find_route plan ~src ~dst ~time` - Find best route (earliest arrival)
8181+- `find_routes plan ~src ~dst ~time ~max` - Find multiple alternative routes
8282+8383+### Routes
8484+8585+- `Route.hops route` - List of contacts forming the path
8686+- `Route.arrival_time route` - Earliest delivery time
8787+- `Route.capacity route` - Bottleneck capacity (minimum across hops)
8888+8989+## Algorithm
9090+9191+CGR uses Dijkstra's algorithm with arrival time as the distance metric:
9292+9393+1. Initialize arrival time at source to query time, infinity elsewhere
9494+2. Select unvisited node with minimum arrival time
9595+3. For each outgoing contact from current node:
9696+ - Skip if contact ends before we arrive
9797+ - Compute arrival at neighbor: max(arrival, contact_start) + owlt
9898+ - Update if this improves the neighbor's arrival time
9999+4. Mark current node visited, repeat until destination reached
100100+101101+## Related Work
102102+103103+- [ION](https://sourceforge.net/projects/ion-dtn/) - NASA/JPL's DTN implementation (C)
104104+- [CGR Tutorial](https://hal.science/hal-03494106/file/2020-JNCA-CGR-Tutorial.pdf) - Fraire et al., 2020
105105+- [CCSDS SABR](https://public.ccsds.org/Pubs/734x2b1.pdf) - Schedule-Aware Bundle Routing standard
106106+- [DtnSim](https://bitbucket.org/lcd-unc-ar/dtnsim/) - DTN network simulator
107107+108108+## References
109109+110110+- IETF Draft: [Contact Graph Routing](https://datatracker.ietf.org/doc/html/draft-burleigh-dtnrg-cgr)
111111+- CCSDS 734.2-B-1: Schedule-Aware Bundle Routing
112112+- Fraire et al., "Routing in the Space Internet: A contact graph routing tutorial", JNCA 2020
113113+114114+## License
115115+116116+ISC License. See [LICENSE.md](LICENSE.md) for details.
+25
dune-project
···11+(lang dune 3.0)
22+33+(name cgr)
44+55+(generate_opam_files true)
66+77+(license ISC)
88+(authors "Thomas Gazagnaire <thomas@gazagnaire.org>")
99+(maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>")
1010+(homepage "https://tangled.org/samoht.me/ocaml-cgr")
1111+(bug_reports "https://tangled.org/samoht.me/ocaml-cgr/issues")
1212+1313+(package
1414+ (name cgr)
1515+ (synopsis "Contact Graph Routing for time-varying satellite networks")
1616+ (description
1717+ "CGR computes routes through scheduled communication contacts in DTN
1818+ (Delay-Tolerant Networking) environments. It implements the CCSDS
1919+ Schedule-Aware Bundle Routing (SABR) algorithm using Dijkstra over
2020+ time-varying graphs where edges (contacts) have temporal validity windows.")
2121+ (depends
2222+ (ocaml (>= 5.1))
2323+ (fmt (>= 0.9))
2424+ (alcotest :with-test)
2525+ (crowbar :with-test)))
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+open Crowbar
77+open Cgr
88+99+(* Generators *)
1010+1111+let node_names = [| "A"; "B"; "C"; "D"; "E" |]
1212+1313+let gen_node =
1414+ map [ range (Array.length node_names) ] (fun i -> Node.v node_names.(i))
1515+1616+let gen_time = map [ range 1000 ] float_of_int
1717+let gen_rate = map [ range 1000 ] (fun r -> float_of_int (r + 1))
1818+1919+let gen_contact =
2020+ map [ gen_node; gen_node; gen_time; range 100; gen_rate ] (fun from to_ start duration rate ->
2121+ let stop = start +. float_of_int (duration + 1) in
2222+ Contact.v ~from ~to_ ~start ~stop ~rate ())
2323+2424+let gen_contact_list = list gen_contact
2525+2626+(* Properties *)
2727+2828+(* Property: If a route is found, arrival time is finite *)
2929+let test_route_arrival_finite contacts src dst time =
3030+ let plan = Contact_plan.of_list contacts in
3131+ match find_route plan ~src ~dst ~time with
3232+ | None -> ()
3333+ | Some route ->
3434+ let arrival = Route.arrival_time route in
3535+ check (Float.is_finite arrival)
3636+3737+(* Property: Route endpoints match query *)
3838+let test_route_endpoints contacts src dst time =
3939+ let plan = Contact_plan.of_list contacts in
4040+ match find_route plan ~src ~dst ~time with
4141+ | None -> ()
4242+ | Some route ->
4343+ check (Node.equal (Route.src route) src);
4444+ check (Node.equal (Route.dst route) dst)
4545+4646+(* Property: Route arrival time >= query time *)
4747+let test_route_arrival_after_start contacts src dst time =
4848+ let plan = Contact_plan.of_list contacts in
4949+ match find_route plan ~src ~dst ~time with
5050+ | None -> ()
5151+ | Some route ->
5252+ let arrival = Route.arrival_time route in
5353+ check (arrival >= time)
5454+5555+(* Property: Route hops form a valid path *)
5656+let test_route_path_valid contacts src dst time =
5757+ let plan = Contact_plan.of_list contacts in
5858+ match find_route plan ~src ~dst ~time with
5959+ | None -> ()
6060+ | Some route -> (
6161+ match Route.hops route with
6262+ | [] ->
6363+ (* Empty path only valid if src = dst *)
6464+ check (Node.equal src dst)
6565+ | first :: rest ->
6666+ (* First hop starts from src *)
6767+ check (Node.equal (Contact.from first) src);
6868+ (* Each hop connects to next *)
6969+ let rec check_chain prev = function
7070+ | [] ->
7171+ (* Last hop goes to dst *)
7272+ check (Node.equal (Contact.to_ prev) dst)
7373+ | next :: rest ->
7474+ check (Node.equal (Contact.to_ prev) (Contact.from next));
7575+ check_chain next rest
7676+ in
7777+ check_chain first rest)
7878+7979+(* Property: Multiple routes have different first hops *)
8080+let test_routes_different_first_hops contacts src dst time =
8181+ let plan = Contact_plan.of_list contacts in
8282+ let routes = find_routes plan ~src ~dst ~time ~max:5 in
8383+ let first_hops =
8484+ List.filter_map
8585+ (fun r -> match Route.hops r with c :: _ -> Some c | [] -> None)
8686+ routes
8787+ in
8888+ (* Check all first hops are distinct (by physical identity) *)
8989+ let rec all_distinct = function
9090+ | [] -> true
9191+ | x :: xs -> not (List.exists (fun y -> x == y) xs) && all_distinct xs
9292+ in
9393+ check (all_distinct first_hops)
9494+9595+(* Property: Routes are ordered by arrival time *)
9696+let test_routes_ordered contacts src dst time =
9797+ let plan = Contact_plan.of_list contacts in
9898+ let routes = find_routes plan ~src ~dst ~time ~max:5 in
9999+ let arrivals = List.map Route.arrival_time routes in
100100+ let rec is_sorted = function
101101+ | [] | [ _ ] -> true
102102+ | a :: (b :: _ as rest) -> a <= b && is_sorted rest
103103+ in
104104+ check (is_sorted arrivals)
105105+106106+(* Register tests *)
107107+let () =
108108+ add_test ~name:"cgr: route arrival finite"
109109+ [ gen_contact_list; gen_node; gen_node; gen_time ]
110110+ test_route_arrival_finite;
111111+ add_test ~name:"cgr: route endpoints match"
112112+ [ gen_contact_list; gen_node; gen_node; gen_time ]
113113+ test_route_endpoints;
114114+ add_test ~name:"cgr: route arrival >= start"
115115+ [ gen_contact_list; gen_node; gen_node; gen_time ]
116116+ test_route_arrival_after_start;
117117+ add_test ~name:"cgr: route path valid"
118118+ [ gen_contact_list; gen_node; gen_node; gen_time ]
119119+ test_route_path_valid;
120120+ add_test ~name:"cgr: routes have different first hops"
121121+ [ gen_contact_list; gen_node; gen_node; gen_time ]
122122+ test_routes_different_first_hops;
123123+ add_test ~name:"cgr: routes ordered by arrival"
124124+ [ gen_contact_list; gen_node; gen_node; gen_time ]
125125+ test_routes_ordered
+290
lib/cgr.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* Nodes *)
77+88+module Node = struct
99+ type t = string
1010+1111+ let v name = name
1212+ let name t = t
1313+ let equal = String.equal
1414+ let compare = String.compare
1515+ let pp = Fmt.string
1616+end
1717+1818+module Node_set = Set.Make (Node)
1919+module Node_map = Map.Make (Node)
2020+2121+(* Contacts *)
2222+2323+module Contact = struct
2424+ type t = {
2525+ from : Node.t;
2626+ to_ : Node.t;
2727+ start : float;
2828+ stop : float;
2929+ rate : float;
3030+ owlt : float;
3131+ }
3232+3333+ let v ~from ~to_ ~start ~stop ~rate ?(owlt = 0.) () =
3434+ { from; to_; start; stop; rate; owlt }
3535+3636+ let from t = t.from
3737+ let to_ t = t.to_
3838+ let start t = t.start
3939+ let stop t = t.stop
4040+ let rate t = t.rate
4141+ let owlt t = t.owlt
4242+ let duration t = t.stop -. t.start
4343+ let capacity t = duration t *. t.rate
4444+ let is_active t ~time = t.start <= time && time < t.stop
4545+4646+ let pp ppf t =
4747+ Fmt.pf ppf "%a->%a [%.0f-%.0f] @%.0f B/s" Node.pp t.from Node.pp t.to_
4848+ t.start t.stop t.rate
4949+end
5050+5151+(* Contact Plans *)
5252+5353+module Contact_plan = struct
5454+ type t = {
5555+ contacts : Contact.t list;
5656+ by_from : Contact.t list Node_map.t;
5757+ by_to : Contact.t list Node_map.t;
5858+ }
5959+6060+ let empty = { contacts = []; by_from = Node_map.empty; by_to = Node_map.empty }
6161+6262+ let add contact t =
6363+ let add_to_map key contact map =
6464+ let existing = Option.value ~default:[] (Node_map.find_opt key map) in
6565+ Node_map.add key (contact :: existing) map
6666+ in
6767+ {
6868+ contacts = contact :: t.contacts;
6969+ by_from = add_to_map (Contact.from contact) contact t.by_from;
7070+ by_to = add_to_map (Contact.to_ contact) contact t.by_to;
7171+ }
7272+7373+ let of_list contacts = List.fold_left (fun t c -> add c t) empty contacts
7474+ let contacts t = t.contacts
7575+7676+ let contacts_from t node =
7777+ Option.value ~default:[] (Node_map.find_opt node t.by_from)
7878+7979+ let contacts_to t node =
8080+ Option.value ~default:[] (Node_map.find_opt node t.by_to)
8181+8282+ let contacts_between t a b =
8383+ contacts_from t a |> List.filter (fun c -> Node.equal (Contact.to_ c) b)
8484+8585+ let nodes t =
8686+ let add_node node set = Node_set.add node set in
8787+ let set =
8888+ List.fold_left
8989+ (fun set c ->
9090+ set |> add_node (Contact.from c) |> add_node (Contact.to_ c))
9191+ Node_set.empty t.contacts
9292+ in
9393+ Node_set.elements set
9494+9595+ let active_at t ~time = List.filter (fun c -> Contact.is_active c ~time) t.contacts
9696+9797+ let pp ppf t =
9898+ Fmt.pf ppf "@[<v>%a@]" (Fmt.list ~sep:Fmt.cut Contact.pp) t.contacts
9999+end
100100+101101+(* Routes *)
102102+103103+module Route = struct
104104+ type t = { hops : Contact.t list; src : Node.t; dst : Node.t }
105105+106106+ let hops t = t.hops
107107+ let src t = t.src
108108+ let dst t = t.dst
109109+110110+ let departure_time t =
111111+ match t.hops with [] -> 0. | c :: _ -> Contact.start c
112112+113113+ let arrival_time t =
114114+ let rec last_arrival time = function
115115+ | [] -> time
116116+ | c :: rest ->
117117+ (* We can start transmitting when we arrive or when contact starts *)
118118+ let tx_start = Float.max time (Contact.start c) in
119119+ (* Arrival = tx_start + propagation delay *)
120120+ let arrival = tx_start +. Contact.owlt c in
121121+ last_arrival arrival rest
122122+ in
123123+ last_arrival (departure_time t) t.hops
124124+125125+ let capacity t =
126126+ match t.hops with
127127+ | [] -> infinity
128128+ | hops -> List.fold_left (fun acc c -> Float.min acc (Contact.capacity c)) infinity hops
129129+130130+ let latency t = arrival_time t -. departure_time t
131131+132132+ let pp ppf t =
133133+ Fmt.pf ppf "@[<v>%a -> %a (arrives %.2f):@,%a@]" Node.pp t.src Node.pp t.dst
134134+ (arrival_time t)
135135+ (Fmt.list ~sep:(Fmt.any " ->@ ") Contact.pp)
136136+ t.hops
137137+end
138138+139139+(* Dijkstra *)
140140+141141+module Dijkstra = struct
142142+ type node_state = {
143143+ arrival_time : float;
144144+ predecessor : Contact.t option;
145145+ visited : bool;
146146+ }
147147+148148+ type state = {
149149+ plan : Contact_plan.t;
150150+ src : Node.t;
151151+ start_time : float;
152152+ nodes : node_state Node_map.t;
153153+ }
154154+155155+ let infinity = Float.infinity
156156+157157+ let init plan ~src ~time =
158158+ let nodes =
159159+ Contact_plan.nodes plan
160160+ |> List.fold_left
161161+ (fun map node ->
162162+ let arrival_time = if Node.equal node src then time else infinity in
163163+ let state = { arrival_time; predecessor = None; visited = false } in
164164+ Node_map.add node state map)
165165+ Node_map.empty
166166+ in
167167+ { plan; src; start_time = time; nodes }
168168+169169+ let get_node_state state node =
170170+ match Node_map.find_opt node state.nodes with
171171+ | Some s -> s
172172+ | None -> { arrival_time = infinity; predecessor = None; visited = false }
173173+174174+ let arrival_time state node =
175175+ let ns = get_node_state state node in
176176+ if Float.is_finite ns.arrival_time then Some ns.arrival_time else None
177177+178178+ let predecessor state node = (get_node_state state node).predecessor
179179+180180+ (* Find unvisited node with minimum arrival time *)
181181+ let find_min_unvisited state =
182182+ Node_map.fold
183183+ (fun node ns acc ->
184184+ if ns.visited then acc
185185+ else
186186+ match acc with
187187+ | None -> Some (node, ns.arrival_time)
188188+ | Some (_, best_time) when ns.arrival_time < best_time ->
189189+ Some (node, ns.arrival_time)
190190+ | Some _ -> acc)
191191+ state.nodes None
192192+ |> Option.map fst
193193+194194+ let step state =
195195+ match find_min_unvisited state with
196196+ | None -> None
197197+ | Some current when not (Float.is_finite (get_node_state state current).arrival_time) ->
198198+ (* All remaining nodes are unreachable *)
199199+ None
200200+ | Some current ->
201201+ let current_state = get_node_state state current in
202202+ let current_arrival = current_state.arrival_time in
203203+204204+ (* Mark current as visited *)
205205+ let nodes =
206206+ Node_map.add current { current_state with visited = true } state.nodes
207207+ in
208208+209209+ (* Relax edges: check all contacts from current node *)
210210+ let nodes =
211211+ Contact_plan.contacts_from state.plan current
212212+ |> List.fold_left
213213+ (fun nodes contact ->
214214+ let neighbor = Contact.to_ contact in
215215+ let neighbor_state = get_node_state { state with nodes } neighbor in
216216+217217+ if neighbor_state.visited then nodes
218218+ else
219219+ (* Can we use this contact? *)
220220+ (* We need to arrive before the contact ends *)
221221+ let contact_usable = current_arrival < Contact.stop contact in
222222+ if not contact_usable then nodes
223223+ else
224224+ (* Compute arrival time at neighbor via this contact *)
225225+ (* We can start transmitting when we arrive or when contact starts *)
226226+ let tx_start =
227227+ Float.max current_arrival (Contact.start contact)
228228+ in
229229+ (* Arrival at neighbor = tx_start + propagation delay *)
230230+ let new_arrival = tx_start +. Contact.owlt contact in
231231+232232+ if new_arrival < neighbor_state.arrival_time then
233233+ Node_map.add neighbor
234234+ { neighbor_state with
235235+ arrival_time = new_arrival;
236236+ predecessor = Some contact;
237237+ }
238238+ nodes
239239+ else nodes)
240240+ nodes
241241+ in
242242+ Some { state with nodes }
243243+244244+ let rec run state =
245245+ match step state with None -> state | Some state' -> run state'
246246+247247+ let extract_route state ~dst =
248248+ let ns = get_node_state state dst in
249249+ if not (Float.is_finite ns.arrival_time) then None
250250+ else
251251+ (* Backtrack through predecessors to build route *)
252252+ let rec build_path node acc =
253253+ if Node.equal node state.src then acc
254254+ else
255255+ match (get_node_state state node).predecessor with
256256+ | None -> acc (* Should not happen if arrival_time is finite *)
257257+ | Some contact -> build_path (Contact.from contact) (contact :: acc)
258258+ in
259259+ let hops = build_path dst [] in
260260+ Some Route.{ hops; src = state.src; dst }
261261+end
262262+263263+(* High-level routing *)
264264+265265+let find_route plan ~src ~dst ~time =
266266+ let state = Dijkstra.init plan ~src ~time in
267267+ let final = Dijkstra.run state in
268268+ Dijkstra.extract_route final ~dst
269269+270270+let find_routes plan ~src ~dst ~time ~max =
271271+ (* Find multiple routes by suppressing initial contacts of found routes *)
272272+ let rec loop plan routes_found =
273273+ if List.length routes_found >= max then List.rev routes_found
274274+ else
275275+ match find_route plan ~src ~dst ~time with
276276+ | None -> List.rev routes_found
277277+ | Some route -> (
278278+ match Route.hops route with
279279+ | [] -> List.rev (route :: routes_found)
280280+ | first_hop :: _ ->
281281+ (* Remove the first contact to find alternative routes *)
282282+ let plan' =
283283+ Contact_plan.of_list
284284+ (List.filter
285285+ (fun c -> c != first_hop)
286286+ (Contact_plan.contacts plan))
287287+ in
288288+ loop plan' (route :: routes_found))
289289+ in
290290+ loop plan []
+247
lib/cgr.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Contact Graph Routing for time-varying networks.
77+88+ CGR computes routes through scheduled communication contacts in DTN
99+ (Delay-Tolerant Networking) environments. Unlike traditional routing
1010+ where links are persistent, CGR handles networks where connectivity
1111+ is intermittent but predictable - such as satellite constellations,
1212+ deep space networks, and scheduled terrestrial links.
1313+1414+ {2 Overview}
1515+1616+ A {e contact} is a scheduled window during which one node can transmit
1717+ to another. The {e contact plan} is the complete schedule of all contacts.
1818+ CGR uses Dijkstra's algorithm over this time-varying graph to find routes
1919+ that minimize delivery time while respecting contact windows.
2020+2121+ {2 Example}
2222+2323+ {[
2424+ open Cgr
2525+2626+ (* Define nodes *)
2727+ let earth = Node.v "EARTH"
2828+ let mars = Node.v "MARS"
2929+ let relay = Node.v "RELAY"
3030+3131+ (* Define contacts (start_time, end_time, rate in bytes/sec) *)
3232+ let contacts = [
3333+ Contact.v ~from:earth ~to_:relay ~start:0. ~stop:100. ~rate:1_000_000.;
3434+ Contact.v ~from:relay ~to_:mars ~start:50. ~stop:150. ~rate:500_000.;
3535+ ]
3636+3737+ (* Create contact plan and find route *)
3838+ let plan = Contact_plan.of_list contacts
3939+ let route = find_route plan ~src:earth ~dst:mars ~time:0.
4040+ ]}
4141+4242+ {2 References}
4343+4444+ - {{:https://datatracker.ietf.org/doc/html/draft-burleigh-dtnrg-cgr}
4545+ IETF Contact Graph Routing draft}
4646+ - {{:https://public.ccsds.org/Pubs/734x2b1.pdf}
4747+ CCSDS Schedule-Aware Bundle Routing (SABR)}
4848+ - {{:https://hal.science/hal-03494106/file/2020-JNCA-CGR-Tutorial.pdf}
4949+ CGR Tutorial (Fraire et al., 2020)} *)
5050+5151+(** {1 Nodes} *)
5252+5353+module Node : sig
5454+ type t
5555+ (** A network node identifier. *)
5656+5757+ val v : string -> t
5858+ (** [v name] creates a node with the given name. *)
5959+6060+ val name : t -> string
6161+ (** [name node] returns the node's name. *)
6262+6363+ val equal : t -> t -> bool
6464+ (** [equal a b] is [true] if nodes are identical. *)
6565+6666+ val compare : t -> t -> int
6767+ (** Total ordering on nodes. *)
6868+6969+ val pp : t Fmt.t
7070+ (** Pretty-printer for nodes. *)
7171+end
7272+7373+(** {1 Contacts} *)
7474+7575+module Contact : sig
7676+ type t
7777+ (** A scheduled communication window between two nodes.
7878+7979+ A contact represents a period during which the transmitting node
8080+ can send data to the receiving node at a specified rate. *)
8181+8282+ val v :
8383+ from:Node.t ->
8484+ to_:Node.t ->
8585+ start:float ->
8686+ stop:float ->
8787+ rate:float ->
8888+ ?owlt:float ->
8989+ unit ->
9090+ t
9191+ (** [v ~from ~to_ ~start ~stop ~rate ?owlt ()] creates a contact.
9292+9393+ @param from The transmitting node
9494+ @param to_ The receiving node
9595+ @param start Contact start time (seconds since epoch)
9696+ @param stop Contact end time (seconds since epoch)
9797+ @param rate Transmission rate in bytes per second
9898+ @param owlt One-way light time (propagation delay) in seconds.
9999+ Defaults to [0.] for terrestrial links. *)
100100+101101+ val from : t -> Node.t
102102+ (** [from c] is the transmitting node. *)
103103+104104+ val to_ : t -> Node.t
105105+ (** [to_ c] is the receiving node. *)
106106+107107+ val start : t -> float
108108+ (** [start c] is the contact start time. *)
109109+110110+ val stop : t -> float
111111+ (** [stop c] is the contact end time. *)
112112+113113+ val rate : t -> float
114114+ (** [rate c] is the transmission rate in bytes/second. *)
115115+116116+ val owlt : t -> float
117117+ (** [owlt c] is the one-way light time (propagation delay). *)
118118+119119+ val duration : t -> float
120120+ (** [duration c] is [stop c -. start c]. *)
121121+122122+ val capacity : t -> float
123123+ (** [capacity c] is [duration c *. rate c], the maximum bytes transmittable. *)
124124+125125+ val is_active : t -> time:float -> bool
126126+ (** [is_active c ~time] is [true] if [start c <= time < stop c]. *)
127127+128128+ val pp : t Fmt.t
129129+ (** Pretty-printer for contacts. *)
130130+end
131131+132132+(** {1 Contact Plans} *)
133133+134134+module Contact_plan : sig
135135+ type t
136136+ (** A collection of scheduled contacts forming the network topology. *)
137137+138138+ val empty : t
139139+ (** Empty contact plan. *)
140140+141141+ val add : Contact.t -> t -> t
142142+ (** [add contact plan] adds a contact to the plan. *)
143143+144144+ val of_list : Contact.t list -> t
145145+ (** [of_list contacts] creates a plan from a list of contacts. *)
146146+147147+ val contacts : t -> Contact.t list
148148+ (** [contacts plan] returns all contacts. *)
149149+150150+ val contacts_from : t -> Node.t -> Contact.t list
151151+ (** [contacts_from plan node] returns contacts where [node] is transmitting. *)
152152+153153+ val contacts_to : t -> Node.t -> Contact.t list
154154+ (** [contacts_to plan node] returns contacts where [node] is receiving. *)
155155+156156+ val contacts_between : t -> Node.t -> Node.t -> Contact.t list
157157+ (** [contacts_between plan a b] returns contacts from [a] to [b]. *)
158158+159159+ val nodes : t -> Node.t list
160160+ (** [nodes plan] returns all nodes mentioned in any contact. *)
161161+162162+ val active_at : t -> time:float -> Contact.t list
163163+ (** [active_at plan ~time] returns contacts active at the given time. *)
164164+165165+ val pp : t Fmt.t
166166+ (** Pretty-printer for contact plans. *)
167167+end
168168+169169+(** {1 Routes} *)
170170+171171+module Route : sig
172172+ type t
173173+ (** A computed path through the contact graph. *)
174174+175175+ val hops : t -> Contact.t list
176176+ (** [hops route] returns the sequence of contacts forming the route. *)
177177+178178+ val src : t -> Node.t
179179+ (** [src route] is the source node. *)
180180+181181+ val dst : t -> Node.t
182182+ (** [dst route] is the destination node. *)
183183+184184+ val departure_time : t -> float
185185+ (** [departure_time route] is when transmission should begin. *)
186186+187187+ val arrival_time : t -> float
188188+ (** [arrival_time route] is the earliest possible delivery time. *)
189189+190190+ val capacity : t -> float
191191+ (** [capacity route] is the minimum capacity across all hops (bottleneck). *)
192192+193193+ val latency : t -> float
194194+ (** [latency route] is [arrival_time route -. departure_time route]. *)
195195+196196+ val pp : t Fmt.t
197197+ (** Pretty-printer for routes. *)
198198+end
199199+200200+(** {1 Routing} *)
201201+202202+val find_route :
203203+ Contact_plan.t -> src:Node.t -> dst:Node.t -> time:float -> Route.t option
204204+(** [find_route plan ~src ~dst ~time] finds the best route from [src] to [dst]
205205+ starting no earlier than [time].
206206+207207+ Returns [None] if no route exists. The "best" route minimizes arrival time
208208+ at the destination (earliest delivery). *)
209209+210210+val find_routes :
211211+ Contact_plan.t ->
212212+ src:Node.t ->
213213+ dst:Node.t ->
214214+ time:float ->
215215+ max:int ->
216216+ Route.t list
217217+(** [find_routes plan ~src ~dst ~time ~max] finds up to [max] routes.
218218+219219+ Routes are returned in order of preference (earliest arrival first).
220220+ Subsequent routes use different initial contacts to provide redundancy. *)
221221+222222+(** {1 Route Computation Details} *)
223223+224224+module Dijkstra : sig
225225+ (** Low-level access to the Dijkstra computation.
226226+227227+ Most users should use {!find_route} instead. This module exposes
228228+ internals for debugging, visualization, or custom routing strategies. *)
229229+230230+ type state
231231+ (** Internal state of a Dijkstra computation. *)
232232+233233+ val init : Contact_plan.t -> src:Node.t -> time:float -> state
234234+ (** [init plan ~src ~time] initializes a computation from [src] at [time]. *)
235235+236236+ val step : state -> state option
237237+ (** [step state] performs one iteration. Returns [None] when complete. *)
238238+239239+ val arrival_time : state -> Node.t -> float option
240240+ (** [arrival_time state node] returns the best known arrival time at [node]. *)
241241+242242+ val predecessor : state -> Node.t -> Contact.t option
243243+ (** [predecessor state node] returns the contact used to reach [node]. *)
244244+245245+ val extract_route : state -> dst:Node.t -> Route.t option
246246+ (** [extract_route state ~dst] extracts the route to [dst] if reachable. *)
247247+end