A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
leo-mobility-test-suite.cc
Go to the documentation of this file.
1/*
2 * Copyright 2014 University of Washington, Benjamin Cizdziel (tolerance-based ECEF test approach)
3 *
4 * SPDX-License-Identifier: GPL-2.0-only and NIST-Software
5 */
6
7/*
8 * This test suite validates the LeoCircularOrbitMobilityModel, including
9 * initial position computation, Earth rotation direction in the ECEF frame,
10 * orbital offset, and on-demand orbital progress.
11 */
12
13#include "ns3/double.h"
14#include "ns3/geographic-positions.h"
15#include "ns3/leo-circular-orbit-mobility-model.h"
16#include "ns3/leo-circular-orbit-position-allocator.h"
17#include "ns3/leo-orbital-shell.h"
18#include "ns3/log.h"
19#include "ns3/node.h"
20#include "ns3/simulator.h"
21#include "ns3/test.h"
22#include "ns3/uinteger.h"
23
24#include <cmath>
25#include <numbers>
26#include <sstream>
27
28NS_LOG_COMPONENT_DEFINE("LeoMobilityTestSuite");
29
30using namespace ns3;
31
32/// Tolerance in meters for ECEF position comparisons
33static constexpr double TOLERANCE = 10.0;
34
35/// Earth sphere radius in km (must match the model)
37
38/// Orbit radius in meters for altitude = 1000 km
39static constexpr double ORBIT_RADIUS_M = (EARTH_RADIUS_KM + 1000.0) * 1000.0;
40
41/// Geocentric gravitational constant in km^3/s^2 (must match the model)
42static constexpr double GM_KM3_S2 = 398600.7;
43
44/// Orbit height in km (geocentric radius) for altitude = 1000 km
45static constexpr double ORBIT_HEIGHT_KM = EARTH_RADIUS_KM + 1000.0;
46
47/**
48 * @ingroup mobility-test
49 * @ingroup leo
50 *
51 * @brief Test that a satellite at time 0 is placed at the analytically expected
52 * ECEF position for a given altitude, inclination, and longitude.
53 *
54 * With altitude = 1000 km, inclination = 45 degrees, longitude = 0 degrees, and
55 * orbital offset = 0 degrees, the expected ECEF position at time 0 is:
56 * x = r * cos(45 deg) = r * sqrt(2)/2
57 * y = 0
58 * z = r * sin(45 deg) = r * sqrt(2)/2
59 * where r = (R_earth + 1000 km) in meters.
60 */
62{
63 public:
65
66 private:
67 void DoRun() override;
68};
69
71 : TestCase("LEO initial ECEF position at time zero")
72{
73}
74
75void
77{
78 auto node = CreateObject<Node>();
80 mob->SetAttribute("Altitude", DoubleValue(1000.0)); // km
81 mob->SetAttribute("Inclination", DoubleValue(45.0)); // degrees
82 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
83 node->AggregateObject(mob);
84
85 // Set longitude = 0 degrees, orbital offset = 0 degrees
86 mob->SetPosition(Vector(0.0, 0.0, 0.0));
87
88 Vector pos = mob->GetPosition();
89
90 double expectedX = ORBIT_RADIUS_M * std::cos(std::numbers::pi / 4.0);
91 double expectedY = 0.0;
92 double expectedZ = ORBIT_RADIUS_M * std::sin(std::numbers::pi / 4.0);
93
94 NS_TEST_ASSERT_MSG_EQ_TOL(pos.x, expectedX, TOLERANCE, "x coordinate at time 0 is incorrect");
95 NS_TEST_ASSERT_MSG_EQ_TOL(pos.y, expectedY, TOLERANCE, "y coordinate at time 0 should be zero");
96 NS_TEST_ASSERT_MSG_EQ_TOL(pos.z, expectedZ, TOLERANCE, "z coordinate at time 0 is incorrect");
97
99}
100
101/**
102 * @ingroup mobility-test
103 * @ingroup leo
104 *
105 * @brief Test that Earth rotation causes the orbital plane to drift westward
106 * in the ECEF frame.
107 *
108 * After exactly one orbital period T, the satellite returns to its starting
109 * position on the orbit (orbital offset = 0 again), but the Earth has rotated
110 * eastward by angle delta = (T / 24h) * 2 * pi. In the ECEF frame, the
111 * ascending node longitude has drifted westward by delta.
112 *
113 * Expected ECEF position at time T (with longitude = 0 degrees at time zero):
114 * lon_T = -delta
115 * x = r * cos(45 deg) * cos(lon_T)
116 * y = r * cos(45 deg) * sin(lon_T) [negative = westward drift]
117 * z = r * sin(45 deg)
118 */
120{
121 public:
123
124 private:
125 void DoRun() override;
126
127 /**
128 * @brief Check satellite position at the scheduled time
129 * @param mob the mobility model to query
130 * @param orbitalPeriod the orbital period used for expected value computation
131 */
133};
134
136 : TestCase("LEO orbital plane drifts westward due to Earth rotation")
137{
138}
139
140void
142{
143 Vector pos = mob->GetPosition();
144
145 // Earth rotation angle during one orbital period
146 double earthRotAngle =
147 (orbitalPeriod.GetDouble() / Hours(24).GetDouble()) * 2.0 * std::numbers::pi;
148 double lonAtT = -earthRotAngle; // westward drift
149
150 double expectedX = ORBIT_RADIUS_M * std::cos(std::numbers::pi / 4.0) * std::cos(lonAtT);
151 double expectedY = ORBIT_RADIUS_M * std::cos(std::numbers::pi / 4.0) * std::sin(lonAtT);
152 double expectedZ = ORBIT_RADIUS_M * std::sin(std::numbers::pi / 4.0);
153
155 expectedX,
156 TOLERANCE,
157 "x coordinate at one orbital period is incorrect");
159 expectedY,
160 TOLERANCE,
161 "y coordinate should be negative (westward drift)");
163 expectedZ,
164 TOLERANCE,
165 "z coordinate at one orbital period is incorrect");
166
167 // Verify westward drift: y must be negative
168 NS_TEST_ASSERT_MSG_LT(pos.y, 0.0, "y coordinate must be negative for westward drift");
169}
170
171void
173{
174 auto node = CreateObject<Node>();
176 mob->SetAttribute("Altitude", DoubleValue(1000.0));
177 mob->SetAttribute("Inclination", DoubleValue(45.0));
178 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
179 node->AggregateObject(mob);
180
181 mob->SetPosition(Vector(0.0, 0.0, 0.0));
182
183 // Compute the orbital period: T = 2*pi * sqrt(r^3 / GM)
185 Time orbitalPeriod = Seconds(2.0 * std::numbers::pi * std::sqrt(r3 / GM_KM3_S2));
186
187 // Check position after exactly one orbital period
188 Simulator::Schedule(orbitalPeriod,
190 this,
191 mob,
192 orbitalPeriod);
193
196}
197
198/**
199 * @ingroup mobility-test
200 * @ingroup leo
201 *
202 * @brief Test that a non-zero initial longitude offsets the orbital plane
203 * correctly at time 0.
204 *
205 * With longitude = 90 degrees east, inclination = 45 degrees, and
206 * orbital offset = 0 degrees, the expected position at time 0 is:
207 * x = r * cos(45 deg) * cos(90 deg) = 0
208 * y = r * cos(45 deg) * sin(90 deg) = r * sqrt(2)/2
209 * z = r * sin(45 deg) = r * sqrt(2)/2
210 */
212{
213 public:
215
216 private:
217 void DoRun() override;
218};
219
221 : TestCase("LEO non-zero initial longitude offset")
222{
223}
224
225void
227{
228 auto node = CreateObject<Node>();
230 mob->SetAttribute("Altitude", DoubleValue(1000.0));
231 mob->SetAttribute("Inclination", DoubleValue(45.0));
232 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
233 node->AggregateObject(mob);
234
235 // Set longitude = 90 degrees east
236 mob->SetPosition(Vector(90.0, 0.0, 0.0));
237
238 Vector pos = mob->GetPosition();
239
240 double expectedX = 0.0;
241 double expectedY = ORBIT_RADIUS_M * std::cos(std::numbers::pi / 4.0);
242 double expectedZ = ORBIT_RADIUS_M * std::sin(std::numbers::pi / 4.0);
243
245 expectedX,
246 TOLERANCE,
247 "x coordinate should be near zero for longitude = 90 deg");
249 expectedY,
250 TOLERANCE,
251 "y coordinate is incorrect for longitude = 90 deg");
253 expectedZ,
254 TOLERANCE,
255 "z coordinate is incorrect for longitude = 90 deg");
256
258}
259
260/**
261 * @ingroup mobility-test
262 * @ingroup leo
263 *
264 * @brief Test that a non-zero orbital offset correctly rotates the satellite
265 * along its orbital plane via the Rodrigues rotation in RotatePlane().
266 *
267 * An offset of 0 places the satellite at the ascending node; a non-zero
268 * offset rotates it around the orbital plane normal. The first three test
269 * cases all use offset = 0 degrees, meaning RotatePlane() acts as an
270 * identity and is not exercised. This test uses offset = 90 degrees to
271 * verify the Rodrigues rotation.
272 *
273 * RotatePlane() implements the Rodrigues rotation formula
274 * (see https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula ):
275 * v' = (n . v) * n + cos(a) * ((n x v) x n) + sin(a) * (n x v)
276 *
277 * With altitude = 1000 km, inclination = 45 degrees, longitude = 0 degrees,
278 * and orbital offset = 90 degrees, the Rodrigues rotation yields:
279 *
280 * The orbital plane normal is n = (-sqrt(2)/2, 0, sqrt(2)/2).
281 * The ascending-node position is x = r * (sqrt(2)/2, 0, sqrt(2)/2).
282 * Since n . x = 0, the formula simplifies:
283 * result = cos(90 deg) * ((n x x) x n) + sin(90 deg) * (n x x)
284 * = 0 + (0, r, 0)
285 * = (0, r, 0)
286 *
287 * Expected ECEF position: (0, r, 0) where r = (R_earth + 1000 km) in meters.
288 */
290{
291 public:
293
294 private:
295 void DoRun() override;
296};
297
299 : TestCase("LEO non-zero orbital offset exercises RotatePlane")
300{
301}
302
303void
305{
306 auto node = CreateObject<Node>();
308 mob->SetAttribute("Altitude", DoubleValue(1000.0));
309 mob->SetAttribute("Inclination", DoubleValue(45.0));
310 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
311 node->AggregateObject(mob);
312
313 // Set longitude = 0, orbital offset = 90 degrees
314 mob->SetPosition(Vector(0.0, 90.0, 0.0));
315
316 Vector pos = mob->GetPosition();
317
318 double expectedX = 0.0;
319 double expectedY = ORBIT_RADIUS_M;
320 double expectedZ = 0.0;
321
323 expectedX,
324 TOLERANCE,
325 "x coordinate should be near zero for orbital offset = 90 deg");
327 expectedY,
328 TOLERANCE,
329 "y coordinate should equal orbit radius for orbital offset = 90 deg");
331 expectedZ,
332 TOLERANCE,
333 "z coordinate should be near zero for orbital offset = 90 deg");
334
336}
337
338/**
339 * @ingroup mobility-test
340 * @ingroup leo
341 *
342 * @brief Test that on-demand orbital progress produces the expected position
343 * after a quarter orbital period.
344 *
345 * Starting at offset = 0 (ascending node), after T/4 the satellite should
346 * have advanced 90 degrees along its orbit. This is equivalent to the
347 * LeoOrbitalOffsetTestCase result (offset = 90 degrees at t=0), but
348 * verified through time evolution rather than initial offset.
349 *
350 * At T/4, Earth rotation causes a small longitude drift that must be
351 * accounted for in the expected position.
352 */
354{
355 public:
357
358 private:
359 void DoRun() override;
360
361 /**
362 * @brief Check satellite position at T/4
363 * @param mob the mobility model to query
364 * @param quarterPeriod T/4 for expected value computation
365 */
367};
368
370 : TestCase("LEO on-demand orbital progress after quarter period")
371{
372}
373
374void
376 Time quarterPeriod)
377{
378 Vector pos = mob->GetPosition();
379
380 // After T/4, the satellite has advanced 90 degrees along its orbit.
381 // Earth rotation causes a small longitude drift.
382 double earthRotAngle =
383 (quarterPeriod.GetDouble() / Hours(24).GetDouble()) * 2.0 * std::numbers::pi;
384 double lonAtT = -earthRotAngle;
385
386 // At offset = 0 degrees, the ascending-node position before Rodrigues rotation is:
387 // x0 = r * cos(inc) * cos(lon), y0 = r * cos(inc) * sin(lon), z0 = r * sin(inc)
388 // After 90 degrees of Rodrigues rotation around the plane normal n:
389 // Since n . x0 = 0 (ascending node is perpendicular to normal),
390 // result = sin(pi/2) * (n x x0) = n x x0
391 // The plane normal at time t is:
392 // n = (-sin(inc) * cos(lon), -sin(inc) * sin(lon), cos(inc))
393 double inc = std::numbers::pi / 4.0;
394 double r = ORBIT_RADIUS_M;
395
396 // Compute n x x0 analytically:
397 // n = (-sin(inc) * cos(lon), -sin(inc) * sin(lon), cos(inc))
398 // x0 = (r * cos(inc) * cos(lon), r * cos(inc) * sin(lon), r * sin(inc))
399 // n . x0 = 0 (verified: -sin * cos^2 * cos^2(lon) - sin * cos^2 * sin^2(lon) + cos * sin = 0
400 // when sin * cos = sin * cos, which holds for any inc)
401 // n x x0 = r * (cos(inc) * sin(lon) * cos(inc) - sin(inc) * (-sin(inc) * sin(lon)),
402 // sin(inc) * (-sin(inc) * cos(lon)) - (-sin(inc) * cos(lon)) * cos(inc) ... )
403 // This simplifies to (0, r, 0) only at lon = 0 degrees. At lon != 0, it is more complex.
404 // Use a direct numerical computation instead.
405 double nx = -std::sin(inc) * std::cos(lonAtT);
406 double ny = -std::sin(inc) * std::sin(lonAtT);
407 double nz = std::cos(inc);
408
409 double x0 = r * std::cos(inc) * std::cos(lonAtT);
410 double y0 = r * std::cos(inc) * std::sin(lonAtT);
411 double z0 = r * std::sin(inc);
412
413 // n x x0
414 double cx = ny * z0 - nz * y0;
415 double cy = nz * x0 - nx * z0;
416 double cz = nx * y0 - ny * x0;
417
418 // After 90 degree rotation: result = sin(pi/2) * (n x x0) = (cx, cy, cz)
419 double expectedX = cx;
420 double expectedY = cy;
421 double expectedZ = cz;
422
424 expectedX,
425 TOLERANCE,
426 "x coordinate after quarter orbit is incorrect");
428 expectedY,
429 TOLERANCE,
430 "y coordinate after quarter orbit is incorrect");
432 expectedZ,
433 TOLERANCE,
434 "z coordinate after quarter orbit is incorrect");
435
436 // Verify orbit radius is preserved
437 double radius = std::sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
440 TOLERANCE,
441 "orbit radius should be preserved after quarter period");
442}
443
444void
446{
447 auto node = CreateObject<Node>();
449 mob->SetAttribute("Altitude", DoubleValue(1000.0));
450 mob->SetAttribute("Inclination", DoubleValue(45.0));
451 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
452 node->AggregateObject(mob);
453
454 mob->SetPosition(Vector(0.0, 0.0, 0.0));
455
456 // Compute the quarter orbital period: T/4 = (pi/2) * sqrt(r^3 / GM)
458 Time quarterPeriod = Seconds(0.5 * std::numbers::pi * std::sqrt(r3 / GM_KM3_S2));
459
460 Simulator::Schedule(quarterPeriod,
462 this,
463 mob,
464 quarterPeriod);
465
468}
469
470/**
471 * @ingroup mobility-test
472 * @ingroup leo
473 *
474 * @brief Test that the Walker Delta phasing factor correctly staggers
475 * satellites in adjacent orbital planes.
476 *
477 * With NumOrbits = 2, NumSatellites = 2, and PhasingFactor = 1 (Walker
478 * 4/2/1), the first satellite in plane 1 should have an orbital offset
479 * that is 1 * 1 * 360 / 4 = 90 degrees greater than the first satellite
480 * in plane 0.
481 *
482 * The allocator returns positions in order: plane 0 sat 0, plane 0 sat 1,
483 * plane 1 sat 0, plane 1 sat 1. We verify that the y-component (orbital
484 * offset in degrees) of the third call includes the 90-degree phasing
485 * offset.
486 */
488{
489 public:
491
492 private:
493 void DoRun() override;
494};
495
497 : TestCase("LEO Walker Delta phasing factor staggers adjacent planes")
498{
499}
500
501void
503{
505 allocator->SetAttribute("NumOrbits", UintegerValue(2));
506 allocator->SetAttribute("NumSatellites", UintegerValue(2));
507 allocator->SetAttribute("PhasingFactor", UintegerValue(1));
508
509 // Plane 0, satellite 0: longitude = 0, offset = 0
510 Vector pos0 = allocator->GetNext();
511 NS_TEST_ASSERT_MSG_EQ_TOL(pos0.x, 0.0, 0.01, "Plane 0 longitude should be 0");
512 NS_TEST_ASSERT_MSG_EQ_TOL(pos0.y, 0.0, 0.01, "Plane 0 sat 0 offset should be 0");
513
514 // Plane 0, satellite 1: longitude = 0, offset = 180
515 Vector pos1 = allocator->GetNext();
516 NS_TEST_ASSERT_MSG_EQ_TOL(pos1.x, 0.0, 0.01, "Plane 0 longitude should be 0");
517 NS_TEST_ASSERT_MSG_EQ_TOL(pos1.y, 180.0, 0.01, "Plane 0 sat 1 offset should be 180");
518
519 // Plane 1, satellite 0: longitude = 180, offset = 0 + 90 (phasing) = 90
520 Vector pos2 = allocator->GetNext();
521 NS_TEST_ASSERT_MSG_EQ_TOL(pos2.x, 180.0, 0.01, "Plane 1 longitude should be 180");
522 NS_TEST_ASSERT_MSG_EQ_TOL(pos2.y, 90.0, 0.01, "Plane 1 sat 0 offset should be 90 (phasing)");
523
524 // Plane 1, satellite 1: longitude = 180, offset = 180 + 90 (phasing) = 270
525 Vector pos3 = allocator->GetNext();
526 NS_TEST_ASSERT_MSG_EQ_TOL(pos3.x, 180.0, 0.01, "Plane 1 longitude should be 180");
528 270.0,
529 0.01,
530 "Plane 1 sat 1 offset should be 270 (180 + 90 phasing)");
531
533}
534
535/**
536 * @ingroup mobility-test
537 * @ingroup leo
538 *
539 * @brief Test that LeoOrbitalShell constructor correctly handles Delta and Star parameters.
540 */
542{
543 public:
545
546 private:
547 void DoRun() override;
548};
549
551 : TestCase("LEO Orbit constructor handles parameters")
552{
553}
554
555void
557{
558 // Walker Delta
559 LeoOrbitalShell delta(1000.0, 50.0, 2, 12, 1.0);
560 NS_TEST_ASSERT_MSG_EQ_TOL(delta.alt, 1000.0, 0.01, "Altitude mismatch");
561 NS_TEST_ASSERT_MSG_EQ_TOL(delta.inc, 50.0, 0.01, "Inclination mismatch");
562 NS_TEST_ASSERT_MSG_EQ(delta.planes, 2, "Planes mismatch");
563 NS_TEST_ASSERT_MSG_EQ(delta.sats, 12, "Sats mismatch");
564 NS_TEST_ASSERT_MSG_EQ_TOL(delta.phasing, 1.0, 0.01, "Phasing mismatch");
565 NS_TEST_ASSERT_MSG_EQ_TOL(delta.raanSpanDeg, 360.0, 0.01, "RAAN span mismatch");
566
567 // Walker Star
568 LeoOrbitalShell star(1000.0, 50.0, 2, 12, 1.0, 180.0);
569 NS_TEST_ASSERT_MSG_EQ_TOL(star.alt, 1000.0, 0.01, "Altitude mismatch");
570 NS_TEST_ASSERT_MSG_EQ_TOL(star.inc, 50.0, 0.01, "Inclination mismatch");
571 NS_TEST_ASSERT_MSG_EQ(star.planes, 2, "Planes mismatch");
572 NS_TEST_ASSERT_MSG_EQ(star.sats, 12, "Sats mismatch");
573 NS_TEST_ASSERT_MSG_EQ_TOL(star.phasing, 1.0, 0.01, "Phasing mismatch");
574 NS_TEST_ASSERT_MSG_EQ_TOL(star.raanSpanDeg, 180.0, 0.01, "RAAN span mismatch");
575
577}
578
579/**
580 * @ingroup mobility-test
581 * @ingroup leo
582 *
583 * @brief Test that Walker Star constellations (RaanSpanDeg = 180) space
584 * orbital planes over 180 degrees instead of 360 degrees.
585 *
586 * With NumOrbits = 2, NumSatellites = 1, and RaanSpanDeg = 180, the two
587 * planes should be separated by 90 degrees (180 / 2) rather than
588 * 180 degrees (360 / 2) as in Walker Delta.
589 */
591{
592 public:
594
595 private:
596 void DoRun() override;
597};
598
600 : TestCase("LEO Walker Star spaces planes over 180 degrees")
601{
602}
603
604void
606{
608 allocator->SetAttribute("NumOrbits", UintegerValue(2));
609 allocator->SetAttribute("NumSatellites", UintegerValue(1));
610 allocator->SetAttribute("RaanSpanDeg", DoubleValue(180.0));
611
612 // Plane 0: RAAN = 0
613 Vector pos0 = allocator->GetNext();
614 NS_TEST_ASSERT_MSG_EQ_TOL(pos0.x, 0.0, 0.01, "Plane 0 RAAN should be 0");
615
616 // Plane 1: RAAN = 90 (180 / 2)
617 Vector pos1 = allocator->GetNext();
619 90.0,
620 0.01,
621 "Plane 1 RAAN should be 90 (180 / 2) for Walker Star");
622
624}
625
626/**
627 * @ingroup mobility-test
628 * @ingroup leo
629 *
630 * @brief Test that LeoCircularOrbitAllocator::GetNextOrbitPosition() returns
631 * the correct structured values matching GetNext() and DoSetPosition().
632 */
634{
635 public:
637
638 private:
639 void DoRun() override;
640};
641
643 : TestCase("LEO allocator GetNextOrbitPosition returns correct struct")
644{
645}
646
647void
649{
651 allocator->SetAttribute("NumOrbits", UintegerValue(2));
652 allocator->SetAttribute("NumSatellites", UintegerValue(2));
653 allocator->SetAttribute("PhasingFactor", UintegerValue(1));
654 allocator->SetAttribute("RaanSpanDeg", DoubleValue(360.0));
655
656 // Plane 0, satellite 0: longitude = 0, argumentOfLatitude = 0
657 auto params0 = allocator->GetNextOrbitPosition();
658 NS_TEST_ASSERT_MSG_EQ_TOL(params0.longitude, 0.0, 0.01, "Plane 0 longitude should be 0");
659 NS_TEST_ASSERT_MSG_EQ_TOL(params0.argumentOfLatitude,
660 0.0,
661 0.01,
662 "Plane 0 sat 0 argLat should be 0");
663 NS_TEST_ASSERT_MSG_EQ_TOL(params0.satelliteIndex, 0.0, 0.01, "Plane 0 sat 0 index should be 0");
664
665 // Plane 0, satellite 1: longitude = 0, argumentOfLatitude = 180
666 auto params1 = allocator->GetNextOrbitPosition();
667 NS_TEST_ASSERT_MSG_EQ_TOL(params1.longitude, 0.0, 0.01, "Plane 0 longitude should be 0");
668 NS_TEST_ASSERT_MSG_EQ_TOL(params1.argumentOfLatitude,
669 180.0,
670 0.01,
671 "Plane 0 sat 1 argLat should be 180");
672
673 // Plane 1, satellite 0: longitude = 180, argumentOfLatitude = 0 + 90 (phasing) = 90
674 auto params2 = allocator->GetNextOrbitPosition();
675 NS_TEST_ASSERT_MSG_EQ_TOL(params2.longitude, 180.0, 0.01, "Plane 1 longitude should be 180");
676 NS_TEST_ASSERT_MSG_EQ_TOL(params2.argumentOfLatitude,
677 90.0,
678 0.01,
679 "Plane 1 sat 0 argLat should be 90");
680
681 // Plane 1, satellite 1: longitude = 180, argumentOfLatitude = 180 + 90 = 270
682 auto params3 = allocator->GetNextOrbitPosition();
683 NS_TEST_ASSERT_MSG_EQ_TOL(params3.longitude, 180.0, 0.01, "Plane 1 longitude should be 180");
684 NS_TEST_ASSERT_MSG_EQ_TOL(params3.argumentOfLatitude,
685 270.0,
686 0.01,
687 "Plane 1 sat 1 argLat should be 270");
688
689 // Verify backward compat: GetNext() produces same fields as GetNextOrbitPosition
690 auto allocCompat = CreateObject<LeoCircularOrbitAllocator>();
691 allocCompat->SetAttribute("NumOrbits", UintegerValue(2));
692 allocCompat->SetAttribute("NumSatellites", UintegerValue(2));
693 allocCompat->SetAttribute("PhasingFactor", UintegerValue(1));
694 allocCompat->SetAttribute("RaanSpanDeg", DoubleValue(360.0));
695
696 auto vecFirst = allocCompat->GetNext();
697 NS_TEST_ASSERT_MSG_EQ_TOL(vecFirst.x, 0.0, 0.01, "GetNext x (longitude) should be 0");
698 NS_TEST_ASSERT_MSG_EQ_TOL(vecFirst.y, 0.0, 0.01, "GetNext y (argLat) should be 0");
699 NS_TEST_ASSERT_MSG_EQ_TOL(vecFirst.z, 0.0, 0.01, "GetNext z (satIndex) should be 0");
700
701 // Test DoSetPosition structured bindings: create a mobility model
702 // and verify it correctly extracts longitude/argumentOfLatitude
703 auto node = CreateObject<Node>();
705 mob->SetAttribute("Altitude", DoubleValue(1000.0));
706 mob->SetAttribute("Inclination", DoubleValue(45.0));
707 mob->SetAttribute("Resolution", TimeValue(Seconds(0)));
708 node->AggregateObject(mob);
709
710 // Use GetNextOrbitPosition to get the first position, then SetPosition with it
711 auto paramsFirst = allocator->GetNextOrbitPosition();
712 Vector setPositionVec{paramsFirst.longitude, paramsFirst.argumentOfLatitude, 0.0};
713 mob->SetPosition(setPositionVec);
714
715 // Verify internal state by checking GetPosition
716 Vector pos = mob->GetPosition();
717
718 // The satellite should be at the ascending node with the correct inclination
719 double expectedX = ORBIT_RADIUS_M * std::cos(std::numbers::pi / 4.0);
720 double expectedZ = ORBIT_RADIUS_M * std::sin(std::numbers::pi / 4.0);
721
723 expectedX,
724 TOLERANCE,
725 "x coordinate after SetPosition is incorrect");
727 expectedZ,
728 TOLERANCE,
729 "z coordinate after SetPosition is incorrect");
730
732}
733
734/**
735 * @ingroup mobility-test
736 * @ingroup leo
737 *
738 * @brief Test LeoOrbitalShell serialization and deserialization round-trip.
739 *
740 * Verifies that a LeoOrbitalShell serialized to CSV text can be deserialized back
741 * and that all six fields (altitude, inclination, planes, sats, phasing,
742 * RAAN span) are preserved exactly.
743 */
745{
746 public:
748
749 private:
750 void DoRun() override;
751};
752
754 : TestCase("LEO Orbit serialization round-trip preserves all fields")
755{
756}
757
758void
760{
761 // Walker Delta orbit with all six fields set
762 LeoOrbitalShell original(700.0, 98.0, 2, 12, 3.0, 360.0);
763
764 // Serialize to text
765 std::ostringstream oss;
766 oss << original;
767 std::string csvText = oss.str();
768
769 // Expected format: "700,98,2,12,3,360"
770 NS_TEST_ASSERT_MSG_EQ(csvText, "700,98,2,12,3,360", "Serialized CSV text mismatch");
771
772 // Deserialize back
773 std::istringstream iss(csvText);
774 LeoOrbitalShell restored;
775 iss >> restored;
776
777 // Verify all fields match
779 original.alt,
780 0.01,
781 "Altitude mismatch after round-trip");
783 original.inc,
784 0.01,
785 "Inclination mismatch after round-trip");
786 NS_TEST_ASSERT_MSG_EQ(restored.planes, original.planes, "Planes mismatch after round-trip");
787 NS_TEST_ASSERT_MSG_EQ(restored.sats, original.sats, "Sats mismatch after round-trip");
789 original.phasing,
790 0.01,
791 "Phasing mismatch after round-trip");
793 original.raanSpanDeg,
794 0.01,
795 "RAAN span mismatch after round-trip");
796
797 // Test Walker Star orbit (raanSpanDeg = 180)
798 LeoOrbitalShell starOriginal(550.0, 53.0, 4, 8, 2.0, 180.0);
799 std::ostringstream oss2;
800 oss2 << starOriginal;
801 std::string csvText2 = oss2.str();
802
803 std::istringstream iss2(csvText2);
804 LeoOrbitalShell starRestored;
805 iss2 >> starRestored;
806
807 NS_TEST_ASSERT_MSG_EQ_TOL(starRestored.alt, starOriginal.alt, 0.01, "Star altitude mismatch");
808 NS_TEST_ASSERT_MSG_EQ_TOL(starRestored.inc,
809 starOriginal.inc,
810 0.01,
811 "Star inclination mismatch");
812 NS_TEST_ASSERT_MSG_EQ(starRestored.planes, starOriginal.planes, "Star planes mismatch");
813 NS_TEST_ASSERT_MSG_EQ(starRestored.sats, starOriginal.sats, "Star sats mismatch");
815 starOriginal.phasing,
816 0.01,
817 "Star phasing mismatch");
819 starOriginal.raanSpanDeg,
820 0.01,
821 "Star RAAN span mismatch");
822
823 // Test default values (only 4 columns)
824 LeoOrbitalShell defaultOriginal(800.0, 45.0, 1, 6); // phasing=0, raanSpanDeg=360 by default
825 std::ostringstream oss3;
826 oss3 << defaultOriginal;
827 std::string csvText3 = oss3.str();
828
829 std::istringstream iss3(csvText3);
830 LeoOrbitalShell defaultRestored;
831 iss3 >> defaultRestored;
832
833 NS_TEST_ASSERT_MSG_EQ_TOL(defaultRestored.alt,
834 defaultOriginal.alt,
835 0.01,
836 "Default altitude mismatch");
837 NS_TEST_ASSERT_MSG_EQ_TOL(defaultRestored.inc,
838 defaultOriginal.inc,
839 0.01,
840 "Default inclination mismatch");
841 NS_TEST_ASSERT_MSG_EQ(defaultRestored.planes,
842 defaultOriginal.planes,
843 "Default planes mismatch");
844 NS_TEST_ASSERT_MSG_EQ(defaultRestored.sats, defaultOriginal.sats, "Default sats mismatch");
845 NS_TEST_ASSERT_MSG_EQ_TOL(defaultRestored.phasing,
846 defaultOriginal.phasing,
847 0.01,
848 "Default phasing mismatch");
850 defaultOriginal.raanSpanDeg,
851 0.01,
852 "Default RAAN span mismatch");
853
855}
856
857/**
858 * @ingroup mobility-test
859 * @ingroup leo
860 *
861 * @brief LEO mobility test suite
862 */
864{
865 public:
867};
868
883
884/// Static variable for test initialization
uint32_t r
Test that Earth rotation causes the orbital plane to drift westward in the ECEF frame.
void DoRun() override
Implementation to actually run this TestCase.
void CheckPosition(Ptr< LeoCircularOrbitMobilityModel > mob, Time orbitalPeriod)
Check satellite position at the scheduled time.
Test that a satellite at time 0 is placed at the analytically expected ECEF position for a given alti...
void DoRun() override
Implementation to actually run this TestCase.
Test that a non-zero initial longitude offsets the orbital plane correctly at time 0.
void DoRun() override
Implementation to actually run this TestCase.
LEO mobility test suite.
Test that LeoOrbitalShell constructor correctly handles Delta and Star parameters.
void DoRun() override
Implementation to actually run this TestCase.
Test that LeoCircularOrbitAllocator::GetNextOrbitPosition() returns the correct structured values mat...
void DoRun() override
Implementation to actually run this TestCase.
Test LeoOrbitalShell serialization and deserialization round-trip.
void DoRun() override
Implementation to actually run this TestCase.
Test that a non-zero orbital offset correctly rotates the satellite along its orbital plane via the R...
void DoRun() override
Implementation to actually run this TestCase.
Test that on-demand orbital progress produces the expected position after a quarter orbital period.
void DoRun() override
Implementation to actually run this TestCase.
void CheckPosition(Ptr< LeoCircularOrbitMobilityModel > mob, Time quarterPeriod)
Check satellite position at T/4.
Test that the Walker Delta phasing factor correctly staggers satellites in adjacent orbital planes.
void DoRun() override
Implementation to actually run this TestCase.
Test that Walker Star constellations (RaanSpanDeg = 180) space orbital planes over 180 degrees instea...
void DoRun() override
Implementation to actually run this TestCase.
This class can be used to hold variables of floating point type such as 'double' or 'float'.
Definition double.h:31
static constexpr double EARTH_SPHERE_RADIUS
Earth's radius in meters if modeled as a perfect sphere.
Orbit definition.
double inc
Inclination of orbit (degrees).
std::size_t sats
Number of satellites in those planes.
double raanSpanDeg
RAAN span in degrees (360 for Walker Delta, 180 for Walker Star).
std::size_t planes
Number of planes with that altitude and inclination.
double alt
Altitude of orbit (km, from earth surface).
double phasing
Walker Delta phasing factor F (0 means no inter-plane stagger).
Smart pointer class similar to boost::intrusive_ptr.
Definition ptr.h:70
static EventId Schedule(const Time &delay, FUNC f, Ts &&... args)
Schedule an event to expire after delay.
Definition simulator.h:580
static void Destroy()
Execute the events scheduled with ScheduleDestroy().
Definition simulator.cc:125
static void Run()
Run the simulation.
Definition simulator.cc:161
void AddTestCase(TestCase *testCase, Duration duration=Duration::QUICK)
Add an individual child TestCase to this test suite.
Definition test.cc:296
@ QUICK
Fast test.
Definition test.h:1057
TestCase(const TestCase &)=delete
Caller graph was not generated because of its size.
Type
Type of test.
Definition test.h:1271
TestSuite(std::string name, Type type=Type::UNIT)
Construct a new test suite.
Definition test.cc:494
Simulation virtual time values and global simulation resolution.
Definition nstime.h:95
double GetDouble() const
Get the raw time value, in the current resolution unit.
Definition nstime.h:445
Hold an unsigned integer type.
Definition uinteger.h:34
#define NS_LOG_COMPONENT_DEFINE(name)
Define a Log component with a specific name.
Definition log.h:194
Ptr< T > CreateObject(Args &&... args)
Create an object by type, with varying number of constructor parameters.
Definition object.h:627
#define NS_TEST_ASSERT_MSG_LT(actual, limit, msg)
Test that an actual value is less than a limit and report and abort if not.
Definition test.h:698
#define NS_TEST_ASSERT_MSG_EQ(actual, limit, msg)
Test that an actual and expected (limit) value are equal and report and abort if not.
Definition test.h:133
#define NS_TEST_ASSERT_MSG_EQ_TOL(actual, limit, tol, msg)
Test that actual and expected (limit) values are equal to plus or minus some tolerance and report and...
Definition test.h:326
Time Seconds(double value)
Construct a Time in the indicated unit.
Definition nstime.h:1273
Time Hours(double value)
Construct a Time in the indicated unit.
Definition nstime.h:1244
static constexpr double ORBIT_HEIGHT_KM
Orbit height in km (geocentric radius) for altitude = 1000 km.
static constexpr double ORBIT_RADIUS_M
Orbit radius in meters for altitude = 1000 km.
static constexpr double GM_KM3_S2
Geocentric gravitational constant in km^3/s^2 (must match the model).
static LeoMobilityTestSuite g_leoMobilityTestSuite
Static variable for test initialization.
static constexpr double EARTH_RADIUS_KM
Earth sphere radius in km (must match the model).
Every class exported by the ns3 library is enclosed in the ns3 namespace.
static const double TOLERANCE
Tolerance used to check reciprocal of two numbers.
#define TOLERANCE