···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "Contact Graph Routing for time-varying satellite networks"
44+description: """
55+CGR computes routes through scheduled communication contacts in DTN
66+ (Delay-Tolerant Networking) environments. It implements the CCSDS
77+ Schedule-Aware Bundle Routing (SABR) algorithm using Dijkstra over
88+ time-varying graphs where edges (contacts) have temporal validity windows."""
99+maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"]
1010+authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"]
1111+license: "ISC"
1212+homepage: "https://tangled.org/samoht.me/ocaml-cgr"
1313+bug-reports: "https://tangled.org/samoht.me/ocaml-cgr/issues"
1414+depends: [
1515+ "dune" {>= "3.0"}
1616+ "ocaml" {>= "5.1"}
1717+ "fmt" {>= "0.9"}
1818+ "alcotest" {with-test}
1919+ "crowbar" {with-test}
2020+ "odoc" {with-doc}
2121+]
2222+build: [
2323+ ["dune" "subst"] {dev}
2424+ [
2525+ "dune"
2626+ "build"
2727+ "-p"
2828+ name
2929+ "-j"
3030+ jobs
3131+ "@install"
3232+ "@runtest" {with-test}
3333+ "@doc" {with-doc}
3434+ ]
3535+]
+6-4
fuzz/fuzz_cgr.ml
···1717let gen_rate = map [ range 1000 ] (fun r -> float_of_int (r + 1))
18181919let 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 ())
2020+ map
2121+ [ gen_node; gen_node; gen_time; range 100; gen_rate ]
2222+ (fun from to_ start duration rate ->
2323+ let stop = start +. float_of_int (duration + 1) in
2424+ Contact.v ~from ~to_ ~start ~stop ~rate ())
23252426let gen_contact_list = list gen_contact
2527···8890 (* Check all first hops are distinct (by physical identity) *)
8991 let rec all_distinct = function
9092 | [] -> true
9191- | x :: xs -> not (List.exists (fun y -> x == y) xs) && all_distinct xs
9393+ | x :: xs -> (not (List.exists (fun y -> x == y) xs)) && all_distinct xs
9294 in
9395 check (all_distinct first_hops)
9496
+26-11
lib/cgr.ml
···5757 by_to : Contact.t list Node_map.t;
5858 }
59596060- let empty = { contacts = []; by_from = Node_map.empty; by_to = Node_map.empty }
6060+ let empty =
6161+ { contacts = []; by_from = Node_map.empty; by_to = Node_map.empty }
61626263 let add contact t =
6364 let add_to_map key contact map =
···9293 in
9394 Node_set.elements set
94959595- let active_at t ~time = List.filter (fun c -> Contact.is_active c ~time) t.contacts
9696+ let active_at t ~time =
9797+ List.filter (fun c -> Contact.is_active c ~time) t.contacts
96989799 let pp ppf t =
98100 Fmt.pf ppf "@[<v>%a@]" (Fmt.list ~sep:Fmt.cut Contact.pp) t.contacts
···125127 let capacity t =
126128 match t.hops with
127129 | [] -> infinity
128128- | hops -> List.fold_left (fun acc c -> Float.min acc (Contact.capacity c)) infinity hops
130130+ | hops ->
131131+ List.fold_left
132132+ (fun acc c -> Float.min acc (Contact.capacity c))
133133+ infinity hops
129134130135 let latency t = arrival_time t -. departure_time t
131136···159164 Contact_plan.nodes plan
160165 |> List.fold_left
161166 (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
167167+ let arrival_time =
168168+ if Node.equal node src then time else infinity
169169+ in
170170+ let state =
171171+ { arrival_time; predecessor = None; visited = false }
172172+ in
164173 Node_map.add node state map)
165174 Node_map.empty
166175 in
···194203 let step state =
195204 match find_min_unvisited state with
196205 | None -> None
197197- | Some current when not (Float.is_finite (get_node_state state current).arrival_time) ->
206206+ | Some current
207207+ when not (Float.is_finite (get_node_state state current).arrival_time) ->
198208 (* All remaining nodes are unreachable *)
199209 None
200210 | Some current ->
···212222 |> List.fold_left
213223 (fun nodes contact ->
214224 let neighbor = Contact.to_ contact in
215215- let neighbor_state = get_node_state { state with nodes } neighbor in
225225+ let neighbor_state =
226226+ get_node_state { state with nodes } neighbor
227227+ in
216228217229 if neighbor_state.visited then nodes
218230 else
219231 (* Can we use this contact? *)
220232 (* We need to arrive before the contact ends *)
221221- let contact_usable = current_arrival < Contact.stop contact in
233233+ let contact_usable =
234234+ current_arrival < Contact.stop contact
235235+ in
222236 if not contact_usable then nodes
223237 else
224238 (* Compute arrival time at neighbor via this contact *)
···231245232246 if new_arrival < neighbor_state.arrival_time then
233247 Node_map.add neighbor
234234- { neighbor_state with
248248+ {
249249+ neighbor_state with
235250 arrival_time = new_arrival;
236251 predecessor = Some contact;
237252 }
···256271 | None -> acc (* Should not happen if arrival_time is finite *)
257272 | Some contact -> build_path (Contact.from contact) (contact :: acc)
258273 in
259259- let hops = build_path dst [] in
260260- Some Route.{ hops; src = state.src; dst }
274274+ let path = build_path dst [] in
275275+ Some { Route.hops = path; src = state.src; dst }
261276end
262277263278(* High-level routing *)
+32-27
lib/cgr.mli
···66(** Contact Graph Routing for time-varying networks.
7788 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.
99+ (Delay-Tolerant Networking) environments. Unlike traditional routing where
1010+ links are persistent, CGR handles networks where connectivity is
1111+ intermittent but predictable - such as satellite constellations, deep space
1212+ networks, and scheduled terrestrial links.
13131414 {2 Overview}
15151616- 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.
1616+ A {e contact} is a scheduled window during which one node can transmit to
1717+ another. The {e contact plan} is the complete schedule of all contacts. CGR
1818+ uses Dijkstra's algorithm over this time-varying graph to find routes that
1919+ minimize delivery time while respecting contact windows.
20202121 {2 Example}
2222···2929 let relay = Node.v "RELAY"
30303131 (* 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- ]
3232+ let contacts =
3333+ [
3434+ Contact.v ~from:earth ~to_:relay ~start:0. ~stop:100. ~rate:1_000_000.;
3535+ Contact.v ~from:relay ~to_:mars ~start:50. ~stop:150. ~rate:500_000.;
3636+ ]
36373738 (* Create contact plan and find route *)
3839 let plan = Contact_plan.of_list contacts
···41424243 {2 References}
43444444- - {{: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)} *)
4545+ - {{:https://datatracker.ietf.org/doc/html/draft-burleigh-dtnrg-cgr} IETF
4646+ Contact Graph Routing draft}
4747+ - {{:https://public.ccsds.org/Pubs/734x2b1.pdf} CCSDS Schedule-Aware Bundle
4848+ Routing (SABR)}
4949+ - {{:https://hal.science/hal-03494106/file/2020-JNCA-CGR-Tutorial.pdf} CGR
5050+ Tutorial (Fraire et al., 2020)} *)
50515152(** {1 Nodes} *)
5253···7677 type t
7778 (** A scheduled communication window between two nodes.
78797979- A contact represents a period during which the transmitting node
8080- can send data to the receiving node at a specified rate. *)
8080+ A contact represents a period during which the transmitting node can send
8181+ data to the receiving node at a specified rate. *)
81828283 val v :
8384 from:Node.t ->
···9596 @param start Contact start time (seconds since epoch)
9697 @param stop Contact end time (seconds since epoch)
9798 @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. *)
9999+ @param owlt
100100+ One-way light time (propagation delay) in seconds. Defaults to [0.] for
101101+ terrestrial links. *)
100102101103 val from : t -> Node.t
102104 (** [from c] is the transmitting node. *)
···120122 (** [duration c] is [stop c -. start c]. *)
121123122124 val capacity : t -> float
123123- (** [capacity c] is [duration c *. rate c], the maximum bytes transmittable. *)
125125+ (** [capacity c] is [duration c *. rate c], the maximum bytes transmittable.
126126+ *)
124127125128 val is_active : t -> time:float -> bool
126129 (** [is_active c ~time] is [true] if [start c <= time < stop c]. *)
···148151 (** [contacts plan] returns all contacts. *)
149152150153 val contacts_from : t -> Node.t -> Contact.t list
151151- (** [contacts_from plan node] returns contacts where [node] is transmitting. *)
154154+ (** [contacts_from plan node] returns contacts where [node] is transmitting.
155155+ *)
152156153157 val contacts_to : t -> Node.t -> Contact.t list
154158 (** [contacts_to plan node] returns contacts where [node] is receiving. *)
···224228module Dijkstra : sig
225229 (** Low-level access to the Dijkstra computation.
226230227227- Most users should use {!find_route} instead. This module exposes
228228- internals for debugging, visualization, or custom routing strategies. *)
231231+ Most users should use {!find_route} instead. This module exposes internals
232232+ for debugging, visualization, or custom routing strategies. *)
229233230234 type state
231235 (** Internal state of a Dijkstra computation. *)
···237241 (** [step state] performs one iteration. Returns [None] when complete. *)
238242239243 val arrival_time : state -> Node.t -> float option
240240- (** [arrival_time state node] returns the best known arrival time at [node]. *)
244244+ (** [arrival_time state node] returns the best known arrival time at [node].
245245+ *)
241246242247 val predecessor : state -> Node.t -> Contact.t option
243248 (** [predecessor state node] returns the contact used to reach [node]. *)
+45-30
test/test_cgr.ml
···1616 Float.abs (Route.arrival_time a -. Route.arrival_time b) < 0.001)
17171818let some_route = Alcotest.option route_arrival
1919-2019let float_eq ?(eps = 0.001) a b = Float.abs (a -. b) < eps
21202221(* Test nodes *)
···3938 Alcotest.(check node) "src" earth (Route.src route);
4039 Alcotest.(check node) "dst" mars (Route.dst route);
4140 Alcotest.(check int) "hops" 1 (List.length (Route.hops route));
4242- Alcotest.(check bool) "arrival at 0" true (float_eq 0. (Route.arrival_time route))
4141+ Alcotest.(check bool)
4242+ "arrival at 0" true
4343+ (float_eq 0. (Route.arrival_time route))
43444445(* Test: Two-hop route *)
4546···5657 let route = Option.get route in
5758 Alcotest.(check int) "hops" 2 (List.length (Route.hops route));
5859 (* Arrive at relay at t=0, wait until contact c2 starts at t=50 *)
5959- Alcotest.(check bool) "arrival at 50" true (float_eq 50. (Route.arrival_time route))
6060+ Alcotest.(check bool)
6161+ "arrival at 50" true
6262+ (float_eq 50. (Route.arrival_time route))
60636164(* Test: No route available *)
6265···7679 Contact.v ~from:earth ~to_:mars ~start:100. ~stop:200. ~rate:1000. ()
7780 in
7881 (* Fast path via relay *)
7979- let c1 = Contact.v ~from:earth ~to_:relay ~start:0. ~stop:50. ~rate:1000. () in
8282+ let c1 =
8383+ Contact.v ~from:earth ~to_:relay ~start:0. ~stop:50. ~rate:1000. ()
8484+ in
8085 let c2 =
8186 Contact.v ~from:relay ~to_:mars ~start:10. ~stop:60. ~rate:1000. ()
8287 in
···8590 Alcotest.(check bool) "route exists" true (Option.is_some route);
8691 let route = Option.get route in
8792 (* Fast path arrives at t=10 (via relay), slow path at t=100 *)
8888- Alcotest.(check bool) "chose fast path" true (float_eq 10. (Route.arrival_time route));
9393+ Alcotest.(check bool)
9494+ "chose fast path" true
9595+ (float_eq 10. (Route.arrival_time route));
8996 Alcotest.(check int) "via relay (2 hops)" 2 (List.length (Route.hops route))
90979198(* Test: Must wait for contact window *)
···100107 Alcotest.(check bool) "route exists" true (Option.is_some route);
101108 let route = Option.get route in
102109 (* Must wait until t=50 to transmit *)
103103- Alcotest.(check bool) "arrival at 50" true (float_eq 50. (Route.arrival_time route))
110110+ Alcotest.(check bool)
111111+ "arrival at 50" true
112112+ (float_eq 50. (Route.arrival_time route))
104113105114(* Test: Propagation delay (one-way light time) *)
106115···115124 Alcotest.(check bool) "route exists" true (Option.is_some route);
116125 let route = Option.get route in
117126 (* Arrival = transmit time + OWLT = 0 + 600 *)
118118- Alcotest.(check bool) "arrival includes owlt" true
127127+ Alcotest.(check bool)
128128+ "arrival includes owlt" true
119129 (float_eq owlt (Route.arrival_time route))
120130121131(* Test: Contact expires before we arrive *)
···126136 Contact.v ~from:earth ~to_:relay ~start:0. ~stop:100. ~rate:1000. ()
127137 in
128138 (* But the mars contact ends before we could use it *)
129129- let c2 =
130130- Contact.v ~from:relay ~to_:mars ~start:0. ~stop:1. ~rate:1000. ()
131131- in
139139+ let c2 = Contact.v ~from:relay ~to_:mars ~start:0. ~stop:1. ~rate:1000. () in
132140 let plan = Contact_plan.of_list [ c1; c2 ] in
133141 let route = find_route plan ~src:earth ~dst:mars ~time:0. in
134142 (* We arrive at relay at t=0, but c2 ends at t=1 - should still work
···156164 Alcotest.(check int) "found 2 routes" 2 (List.length routes);
157165 (* First route should be fastest (via relay, arrives at 10) *)
158166 let first = List.hd routes in
159159- Alcotest.(check bool) "first is fastest" true
167167+ Alcotest.(check bool)
168168+ "first is fastest" true
160169 (float_eq 10. (Route.arrival_time first))
161170162171(* Test: Contact plan operations *)
163172164173let test_contact_plan () =
165165- let c1 = Contact.v ~from:earth ~to_:mars ~start:0. ~stop:100. ~rate:1000. () in
166166- let c2 = Contact.v ~from:earth ~to_:relay ~start:50. ~stop:150. ~rate:500. () in
167167- let c3 = Contact.v ~from:relay ~to_:mars ~start:100. ~stop:200. ~rate:800. () in
174174+ let c1 =
175175+ Contact.v ~from:earth ~to_:mars ~start:0. ~stop:100. ~rate:1000. ()
176176+ in
177177+ let c2 =
178178+ Contact.v ~from:earth ~to_:relay ~start:50. ~stop:150. ~rate:500. ()
179179+ in
180180+ let c3 =
181181+ Contact.v ~from:relay ~to_:mars ~start:100. ~stop:200. ~rate:800. ()
182182+ in
168183 let plan = Contact_plan.of_list [ c1; c2; c3 ] in
169169- Alcotest.(check int) "all contacts" 3 (List.length (Contact_plan.contacts plan));
170170- Alcotest.(check int) "contacts from earth" 2
184184+ Alcotest.(check int)
185185+ "all contacts" 3
186186+ (List.length (Contact_plan.contacts plan));
187187+ Alcotest.(check int)
188188+ "contacts from earth" 2
171189 (List.length (Contact_plan.contacts_from plan earth));
172172- Alcotest.(check int) "contacts to mars" 2
190190+ Alcotest.(check int)
191191+ "contacts to mars" 2
173192 (List.length (Contact_plan.contacts_to plan mars));
174174- Alcotest.(check int) "contacts earth->mars" 1
193193+ Alcotest.(check int)
194194+ "contacts earth->mars" 1
175195 (List.length (Contact_plan.contacts_between plan earth mars));
176196 Alcotest.(check int) "nodes" 3 (List.length (Contact_plan.nodes plan));
177177- Alcotest.(check int) "active at 75" 2
197197+ Alcotest.(check int)
198198+ "active at 75" 2
178199 (List.length (Contact_plan.active_at plan ~time:75.))
179200180201(* Test: Route capacity is bottleneck *)
···183204 let c1 =
184205 Contact.v ~from:earth ~to_:relay ~start:0. ~stop:100. ~rate:1000. ()
185206 in
186186- let c2 =
187187- Contact.v ~from:relay ~to_:mars ~start:0. ~stop:50. ~rate:500. ()
188188- in
207207+ let c2 = Contact.v ~from:relay ~to_:mars ~start:0. ~stop:50. ~rate:500. () in
189208 let plan = Contact_plan.of_list [ c1; c2 ] in
190209 let route = find_route plan ~src:earth ~dst:mars ~time:0. in
191210 let route = Option.get route in
192211 (* Capacity is min of: c1 (100*1000=100000) and c2 (50*500=25000) *)
193193- Alcotest.(check bool) "capacity is bottleneck" true
212212+ Alcotest.(check bool)
213213+ "capacity is bottleneck" true
194214 (float_eq 25000. (Route.capacity route))
195215196216(* Suite *)
···209229 Alcotest.test_case "find routes" `Quick test_find_routes;
210230 ] );
211231 ( "contact plan",
212212- [
213213- Alcotest.test_case "operations" `Quick test_contact_plan;
214214- ] );
215215- ( "route",
216216- [
217217- Alcotest.test_case "capacity" `Quick test_route_capacity;
218218- ] );
232232+ [ Alcotest.test_case "operations" `Quick test_contact_plan ] );
233233+ ("route", [ Alcotest.test_case "capacity" `Quick test_route_capacity ]);
219234 ]