# sidereon: GNSS and astrodynamics in Elixir

```elixir
Mix.install([
  {:sidereon, "~> 0.9"}
])
```

## About

[sidereon](https://hex.pm/packages/sidereon) is the Elixir interface to a GNSS
and astrodynamics engine written in Rust. The numerics run in that engine and
ship as a precompiled NIF, so there is nothing to compile: this notebook runs as
written. Every cell below is self-contained and uses only data embedded here.

## Propagate a satellite and take a look angle

Parse a two-line element set (these are real elements for the ISS), run SGP4 to
get the sub-satellite point, and compute the azimuth, elevation, and range from a
ground station.

```elixir
line1 = "1 25544U 98067A   24001.50000000  .00016717  00000-0  10270-3 0  9009"
line2 = "2 25544  51.6400 208.8657 0002644 250.3037 109.7782 15.49560812999990"

{:ok, iss} = Sidereon.parse_tle(line1, line2)

epoch = ~U[2024-01-01 12:00:00Z]
{:ok, geo} = Sidereon.geodetic(iss, epoch)

%{
  catalog_number: iss.catalog_number,
  sub_lat_deg: Float.round(geo.latitude, 3),
  sub_lon_deg: Float.round(geo.longitude, 3),
  altitude_km: Float.round(geo.altitude_km, 1)
}
```

```elixir
# London ground station: latitude and longitude in degrees, altitude in meters.
station = %{latitude: 51.5, longitude: -0.1, altitude_m: 10.0}

{:ok, look} = Sidereon.look_angle(iss, epoch, station)

%{
  azimuth_deg: Float.round(look.azimuth, 2),
  elevation_deg: Float.round(look.elevation, 2),
  range_km: Float.round(look.range_km, 1)
}
```

## Predict passes over a station

`Sidereon.predict_passes/5` returns every pass that rises above a minimum
elevation in the window, each with rise and set times, peak elevation, and
duration.

```elixir
window_start = ~U[2024-01-01 00:00:00Z]
window_end = DateTime.add(window_start, 1, :day)

{:ok, passes} =
  Sidereon.predict_passes(iss, station, window_start, window_end, min_elevation: 10.0)

for p <- passes do
  %{
    rise: Calendar.strftime(p.rise, "%H:%M:%S"),
    set: Calendar.strftime(p.set, "%H:%M:%S"),
    peak_elevation_deg: Float.round(p.max_elevation, 1),
    duration_min: Float.round(p.duration_seconds / 60, 1)
  }
end
```

## Solve a position from pseudoranges

Single-point positioning. Feed `Sidereon.GNSS.Positioning.solve/4` a set of
pseudoranges and a precise SP3 ephemeris and it returns a least-squares fix:
ECEF and geodetic position, the receiver clock bias, and geometry diagnostics.
The SP3 product below is embedded so the cell runs offline; in practice you would
load one with `Sidereon.GNSS.SP3.load/1`.

```elixir
# GPS L1 pseudoranges (meters) for the satellites in view at the epoch.
observations = [
  {"G08", 23_825_519.8},
  {"G10", 22_717_690.1},
  {"G16", 20_478_653.4},
  {"G18", 21_768_335.2},
  {"G20", 21_248_327.7},
  {"G21", 20_808_709.8}
]

sp3_data = """
#cP2020  6 24  9 45  0.00000000      19 TRACK IGb14 FIT GRGS
## 2111 259200.00000000   900.00000000 59024 0.0000000000000
+    6   G08G10G16G18G20G21  0  0  0  0  0  0  0  0  0  0  0
+          0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
+          0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
+          0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
+          0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
++         4  4  4  4  4  4  0  0  0  0  0  0  0  0  0  0  0
++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
%c M  cc GPS ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc
%f  0.0000000  0.000000000  0.00000000000  0.000000000000000
%f  0.0000000  0.000000000  0.00000000000  0.000000000000000
%i    0    0    0    0      0      0      0      0         0
%i    0    0    0    0      0      0      0      0         0
/* CNES/CLS/GRGS - TOULOUSE,FRANCE - Contact : igs-ac@cls.fr
/* PCV:IGS14_2108 OL/AL:FES2012  NONE     NN ORB:CoN CLK:CoN
/* CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
/* CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
*  2020  6 24  9 45  0.00000000
PG08   4643.904912 -24162.558516  -9650.049610    -38.635183
PG10  17886.671789   3039.006353 -19391.393793   -380.478330
PG16   3673.608258 -18933.969815  17839.814705   -174.352223
PG18  23700.127530   6507.753313  10093.688366    228.808891
PG20  21015.602926  12381.822675 -10856.524893    527.448122
PG21  26299.269767  -2460.995007    716.844563     15.508264
*  2020  6 24 10  0  0.00000000
PG08   5254.320976 -24937.345008  -6980.386897    -38.636691
PG10  19010.269319   4866.851618 -17921.084337   -380.488198
PG16   4860.112779 -17100.303698  19364.987730   -174.356395
PG18  22418.061092   6785.447511  12538.206018    228.818132
PG20  21540.808052  13373.630254  -8364.466576    527.447968
PG21  26177.802655  -2146.941378   3560.974533     15.512752
*  2020  6 24 10 15  0.00000000
PG08   5710.564170 -25453.105280  -4188.721104    -38.638443
PG10  20115.189466   6485.476927 -16142.894217   -380.498119
PG16   6226.321564 -15204.115225  20546.504507   -174.360500
PG18  20889.343380   7160.242478  14767.111628    228.827365
PG20  21908.611100  14137.388789  -5730.669927    527.447926
PG21  25760.514626  -1769.431822   6343.514786     15.516981
*  2020  6 24 10 30  0.00000000
PG08   6042.284075 -25684.525952  -1323.814227    -38.640412
PG10  21159.408012   7879.460815 -14087.964262   -380.507982
PG16   7759.098529 -13294.312785  21364.232984   -174.364502
PG18  19148.071571   7657.452624  16742.023312    228.836711
PG20  22085.133601  14686.270178  -2999.737823    527.447449
PG21  25067.475736  -1296.107146   9017.301047     15.521531
*  2020  6 24 10 45  0.00000000
PG08   6283.596097 -25614.361768   1564.242656    -38.641756
PG10  22100.268611   9041.937175 -11792.020438   -380.517890
PG16   9436.967444 -11417.672716  21804.746560   -174.368582
PG18  17234.002467   8295.952772  18428.910848    228.845896
PG20  22041.002117  15040.256156   -217.934016    527.447614
PG21  24126.033756   -698.817607  11537.888708     15.525474
*  2020  6 24 11  0  0.00000000
PG08   6471.529324 -25234.155183   4424.947554    -38.642177
PG10  22896.131923   9974.623387  -9294.735744   -380.527755
PG16  11230.846890  -9617.105625  21861.506015   -174.372582
PG18  15191.136796   9087.282030  19798.684014    228.855179
PG20  21752.562074  15225.214120   2567.564373    527.447459
PG21  22969.651026     45.186775  13864.197409     15.529722
*  2020  6 24 11 15  0.00000000
PG08   6644.360327 -24544.616731   7208.313715    -38.643219
PG10  23507.988002  10687.523653  -6639.037631   -380.537732
PG16  13105.095965  -7930.108701  21534.895433   -174.377072
PG18  13066.149231  10035.038521  20827.696867    228.864441
PG20  21202.891348  15271.756854   5309.432624    527.447520
PG21  21636.563260    952.305394  15959.053118     15.534059
*  2020  6 24 11 30  0.00000000
PG08   6839.902243 -23555.650675   9865.783600    -38.644720
PG10  23900.971736  11198.322998  -3870.373323   -380.547626
PG16  15018.822256  -6387.464812  20832.117593   -174.381097
PG18  10906.724082  11134.589308  21498.159124    228.873724
PG20  20382.574992  15213.927788   7960.972253    527.447536
PG21  20168.317061   2031.899012  17789.631146     15.538108
*  2020  6 24 11 45  0.00000000
PG08   7093.820244 -22286.025531  12351.111011    -38.645281
PG10  24045.725638  11531.498375  -1035.945281   -380.557547
PG16  16927.393599  -5012.236439  19766.958053   -174.385107
PG18   8759.860313  12373.107746  21798.447374    228.883076
PG20  19290.210666  15087.759974  10476.882192    527.447233
PG21  18608.242309   3285.876753  19327.806335     15.542220
*  2020  6 24 12  0  0.00000000
PG08   7438.042916 -20762.704119  14621.192800    -38.647002
PG10  23919.560682  11717.183156   1816.071269   -380.567404
PG16  18784.088401  -3819.088286  18359.430195   -174.389524
PG18   6670.210753  13729.937789  21723.310519    228.892428
PG20  17932.623680  14929.762015  12814.016152    527.447354
PG21  16999.913180   4708.560403  20550.418405     15.546299
*  2020  6 24 12 15  0.00000000
PG08   7899.334503 -19019.862023  16636.833344    -38.647763
PG10  23507.373783  11789.829543   4637.317275   -380.577286
PG16  20541.815806  -2813.957336  16635.315598   -174.393761
PG18   4678.519090  15177.272097  21273.965621    228.901739
PG20  16324.780780  14775.387488  14932.114622    527.446948
PG21  15385.647811   6286.819595  21439.461249     15.550671
*  2020  6 24 12 30  0.00000000
PG08   8498.085463 -17097.635989  18363.427105    -38.649562
PG10  22802.289648  11786.720063   7380.034276   -380.587160
PG16  22154.836702  -1994.074300  14625.615492   -174.397842
PG18   2820.214554  16681.118608  20458.082166    228.911022
PG20  14489.402694  14657.545765  16794.499670    527.447051
PG21  13805.092069   8000.463976  21982.204956     15.554459
*  2020  6 24 12 45  0.00000000
PG08   9247.369015 -15040.654467  19771.547156    -38.650484
PG10  21806.004803  11746.384064   9997.833858   -380.596890
PG16  23580.419142  -1348.326156  12365.929519   -174.402246
PG18   1124.218120  18202.518914  19289.654667    228.920337
PG20  12456.286563  14605.211454  18368.720568    527.447006
PG21  12293.927824   9822.872282  22171.258698     15.558791
*  2020  6 24 13  0  0.00000000
PG08  10152.299669 -12896.410588  20837.430270    -38.651742
PG10  20528.822194  11706.977946  12446.442268   -380.606741
PG16  24780.366877   -857.936737   9895.777935   -174.406704
PG18   -387.994051  19698.971935  17788.765433    228.929616
PG20  10261.360393  14642.186916  19627.138333    527.447150
PG21  10882.740798  11721.833118  22004.581485     15.563408
*  2020  6 24 13 15  0.00000000
PG08  11209.716830 -10713.543221  21543.351971    -38.652897
PG10  18989.375629  11704.688739  14684.410767   -380.616599
PG16  25722.366833   -497.431201   7257.882649   -174.411115
PG18  -1703.034135  21126.008215  15981.241263    228.938925
PG20   7945.502154  14786.067388  20547.437547    527.446927
PG21   9596.077666  13660.568119  21485.446350     15.567441
*  2020  6 24 13 30  0.00000000
PG08  12408.203090  -8540.094231  21877.887705    -38.653740
PG10  17214.054103  11772.219589  16673.782838   -380.626446
PG16  26381.110092   -235.841125   4497.421430   -174.415665
PG18  -2815.928687  22438.854161  13898.209721    228.948171
PG20   5553.165699  15047.451338  21113.054383    527.446839
PG21   8451.716682  15598.904800  20622.361836     15.572148
*  2020  6 24 13 45  0.00000000
PG08  13728.433306  -6421.809657  21836.058871    -38.653666
PG10  15236.146433  11937.412720  18380.709769   -380.636302
PG16  26739.150669    -38.100050   1661.268176   -174.419882
PG18  -3730.115104  23594.121838  11575.562442    228.957546
PG20   3130.863904  15429.429881  21313.510668    527.447015
PG21   7460.170730  17494.563735  19428.952908     15.577088
*  2020  6 24 14  0  0.00000000
PG08  15143.837640  -4400.549449  21419.364822    -38.654444
PG10  13094.736367  12222.060725  19776.006518   -380.646060
PG16  26787.476793    133.425531  -1202.769329   -174.424084
PG18  -4457.109220  24551.458779   9053.334699    228.966797
PG20    725.565911  15927.378707  21144.644989    527.446993
PG21   6624.436243  19304.522499  17923.801627     15.580392
*  2020  6 24 14 15  0.00000000
PG08  16621.549904  -2512.864913  20635.703989    -38.655549
PG10  10833.387063  12640.950613  20835.640171   -380.655943
PG16  26525.779980    317.327470  -4046.704331   -174.428345
PG18  -5015.857646  25275.093632   6375.012002    228.975990
PG20  -1616.930248  16529.064324  20608.733434    527.446900
PG21   5939.995958  20986.417129  16130.246267     15.584692
EOF
"""

{:ok, sp3} = Sidereon.GNSS.SP3.parse(sp3_data)

{:ok, solution} =
  Sidereon.GNSS.Positioning.solve(sp3, observations, ~N[2020-06-24 12:00:00],
    initial_guess: [4_500_000.0, 500_000.0, 4_500_000.0, 0.0]
  )

%{
  position_ecef_m: solution.position,
  rx_clock_s: solution.rx_clock_s,
  pdop: Float.round(solution.dop.pdop, 2),
  used_sats: solution.used_sats
}
```

## Screen a conjunction and compute collision probability

Parse a CCSDS Conjunction Data Message (CDM), convert it to collision
parameters, and compute the probability of collision (Pc) with the Foster method.

```elixir
cdm_kvn = """
CCSDS_CDM_VERS                = 1.0
CREATION_DATE                 = 2010-03-12T22:31:12.000
ORIGINATOR                    = JSPOC
MESSAGE_ID                    = 201113719185
TCA                           = 2010-03-13T22:37:52.618
MISS_DISTANCE                 = 715 [m]
RELATIVE_SPEED                = 14762 [m/s]
OBJECT                        = OBJECT1
OBJECT_DESIGNATOR             = 12345
OBJECT_NAME                   = SATELLITE A
REF_FRAME                     = EME2000
X                             = 2570.097065 [km]
Y                             = 2244.654904 [km]
Z                             = 6281.497978 [km]
X_DOT                         = 4.418769571 [km/s]
Y_DOT                         = 4.833547743 [km/s]
Z_DOT                         = -3.526774282 [km/s]
CR_R                          = 4.142E+01 [m**2]
CT_R                          = -8.579E+00 [m**2]
CT_T                          = 2.533E+03 [m**2]
CN_R                          = -2.313E+01 [m**2]
CN_T                          = 1.336E+01 [m**2]
CN_N                          = 7.098E+01 [m**2]
OBJECT                        = OBJECT2
OBJECT_DESIGNATOR             = 30337
OBJECT_NAME                   = FENGYUN 1C DEB
REF_FRAME                     = EME2000
X                             = 2569.540800 [km]
Y                             = 2245.093614 [km]
Z                             = 6281.599946 [km]
X_DOT                         = -2.888612500 [km/s]
Y_DOT                         = -6.007247516 [km/s]
Z_DOT                         = 3.328770172 [km/s]
CR_R                          = 1.337E+03 [m**2]
CT_R                          = -4.806E+04 [m**2]
CT_T                          = 2.492E+06 [m**2]
CN_R                          = -3.298E+01 [m**2]
CN_T                          = -7.5888E+02 [m**2]
CN_N                          = 7.105E+01 [m**2]
"""

{:ok, cdm} = Sidereon.CCSDS.CDM.parse(cdm_kvn)

# Combined hard-body radius of 20 meters.
cdm = %{cdm | hard_body_radius_m: 20.0}
params = Sidereon.CCSDS.CDM.to_collision_params(cdm)

{:ok, equal_area} = Sidereon.Collision.probability(params, method: :equal_area)
{:ok, numerical} = Sidereon.Collision.probability(params, method: :numerical)

%{
  miss_km: Float.round(equal_area.miss_km, 4),
  pc_equal_area: equal_area.pc,
  pc_numerical: numerical.pc
}
```

## RF link budget

Size a UHF amateur-radio link to the satellite at the look angle computed above:
free-space path loss, EIRP, and the resulting link margin.

```elixir
fspl_db = Sidereon.RF.fspl(look.range_km, 437.0)
eirp_dbw = Sidereon.RF.eirp(30.0, 10.0)

margin_db =
  Sidereon.RF.link_margin(%{
    eirp_dbw: eirp_dbw,
    fspl_db: fspl_db,
    receiver_gt_dbk: -20.0,
    other_losses_db: 3.0,
    required_cn0_dbhz: 30.0
  })

%{
  fspl_db: Float.round(fspl_db, 1),
  eirp_dbw: Float.round(eirp_dbw, 1),
  link_margin_db: Float.round(margin_db, 1)
}
```

## Where to next

Full signatures live on [HexDocs](https://hexdocs.pm/sidereon). More notebooks,
including a ground-track map and a GNSS positioning walkthrough, live under
[`examples/`](https://github.com/neilberkman/sidereon-ex/tree/main/examples).
