diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 00000000..c730e0f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,43 @@ +--- +name: Documentation +about: Recommend an update to the documentation +title: '' +labels: 'Documentation' +assignees: '' + +--- + +## Description + +Please describe the issue or enhancement you want to report. Provide as much detail as possible to help us understand and address it. + +## Documentation type + +Tell us which of the four Diátaxis documentation types this issue or enhancement relates to: + +- Tutorial +- How-to guide +- Technical reference / Math Spec +- Explanation + +## Affected area(s) + +For the selected documentation type, tell us what specific area(s) this issue relates to. Provide titles, page numbers, URLs or other locators. + +## Expected or desired behavior + +Tell us what you expected to see in the documentation, or how you think it could be improved. For enhancements, describe the improvement you think could be made. + +## Additional context + +Provide any other context or screenshots that would help us understand the issue or enhancement. + +## Possible solutions (optional) + +If you have any suggestions for how to address the issue or implement the enhancement, provide them here. We appreciate any insights you have! + +## Who should review this? + +Tag any individuals, teams, or roles that would likely need to review or address this issue for the specified documentation type. + +We will do our best to direct this to the appropriate people. Thank you for your feedback! diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c85e2304..0b2833a3 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -63,6 +63,11 @@ jobs: set -e pip3 install nyx_space --find-links dist --force-reinstall pytest + - name: Upload python tests HTMLs + uses: actions/upload-artifact@v3 + with: + name: od-plots + path: output_data/*.html windows: runs-on: windows-latest diff --git a/Cargo.toml b/Cargo.toml index 267584ec..13720924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nyx-space" build = "build.rs" -version = "2.0.0-alpha.3" +version = "2.0.0-alpha" edition = "2021" authors = ["Christopher Rabotin "] description = "A high-fidelity space mission toolkit, with orbit propagation, estimation and some systems engineering" @@ -35,11 +35,7 @@ rand_distr = "0.4" meval = "0.2" rust-embed = "6" toml = "0.7" -config = {version = "0.13.3", features = ["yaml"]} regex = "1.5" -pretty_env_logger = "0.5" -dialoguer = "0.10" -glob = "0.3" rayon = "1.6" lazy_static = "1.4.0" approx = "0.5" @@ -50,19 +46,22 @@ numpy = {version = "0.17", optional = true} indicatif = {version = "0.17", features = ["rayon"]} rstats = "1.2.50" thiserror = "1.0" -parquet = {version = "39.0.0", default-features = false, features = ["arrow"]} -arrow = "39.0.0" -shadow-rs = {version = "0.21.0", default-features = false} +parquet = {version = "40.0.0", default-features = false, features = ["arrow"]} +arrow = "40.0.0" serde_yaml = "0.9.21" whoami = "1.3.0" either = {version = "1.8.1", features = ["serde"]} num = "0.4.0" enum-iterator = "1.4.0" -rstest = "0.17.0" getrandom = {version = "0.2", features = ["js"]} +[dev-dependencies] +polars = {version = "0.29.0", features = ["parquet"]} +rstest = "0.17.0" +pretty_env_logger = "0.5" + [build-dependencies] -shadow-rs = "0.21.0" +shadow-rs = {version = "0.21.0", default-features = false} [features] default = [] diff --git a/README.md b/README.md index 10e05c73..8ca95bdd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ and portability to all platforms supported by [Rust](https://forge.rust-lang.org [![nyx-space on crates.io][cratesio-image]][cratesio] [![nyx-space on docs.rs][docsrs-image]][docsrs] -[![LoC](https://tokei.rs/b1/gitlab/nyx-space/nyx?category=lines)](https://github.com/nyx-space/nyx). +[![LoC](https://tokei.rs/b1/github/nyx-space/nyx?category=lines)](https://github.com/nyx-space/nyx). [cratesio-image]: https://img.shields.io/crates/v/nyx-space.svg [cratesio]: https://crates.io/crates/nyx-space diff --git a/data/tests/ccsds/oem/GEO_20s.oem b/data/tests/ccsds/oem/GEO_20s.oem new file mode 100644 index 00000000..cce92bd0 --- /dev/null +++ b/data/tests/ccsds/oem/GEO_20s.oem @@ -0,0 +1,204 @@ +CCSDS_OEM_VERS = 2.0 + +COMMENT Orbit data are consistent with planetary ephemeris DE-430 + +CREATION_DATE = 2020-06-01T00:51:59 +ORIGINATOR = Test + +META_START +OBJECT_NAME = TEST_OBJ +OBJECT_ID = 0000-000A +CENTER_NAME = Earth +REF_FRAME = ICRF +TIME_SYSTEM = UTC +START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_STOP_TIME = 2020-06-01T13:00:00.000000 +STOP_TIME = 2020-06-01T13:00:00.000000 +INTERPOLATION = Lagrange +INTERPOLATION_DEGREE = 5 +META_STOP + +COMMENT Vehicle's position at any requested time was actually computed using an algorithm, not an interpolation of a table of ephemeris. + +2020-06-01T12:00:00.000000 4.200243572628212e+04 -3.700484048865550e+03 -5.884882406018843e+01 2.695321451082662e-01 3.062817188910680e+00 -3.828496759806085e-04 +2020-06-01T12:00:20.000000 4.200778163657439e+04 -3.639224748339197e+03 -5.885666667931729e+01 2.650651218152016e-01 3.063207457945818e+00 -3.765769895598969e-04 +2020-06-01T12:00:40.000000 4.201303820212367e+04 -3.577957707666860e+03 -5.886438358580432e+01 2.605975328286982e-01 3.063591212129920e+00 -3.703034445849152e-04 +2020-06-01T12:01:00.000000 4.201820541171129e+04 -3.516683057150813e+03 -5.887197476200347e+01 2.561293876496311e-01 3.063968450641196e+00 -3.640290544474632e-04 +2020-06-01T12:01:20.000000 4.202328325430835e+04 -3.455400927112373e+03 -5.887944019053694e+01 2.516606957802736e-01 3.064339172671692e+00 -3.577538325413305e-04 +2020-06-01T12:01:40.000000 4.202827171907642e+04 -3.394111447885419e+03 -5.888677985429661e+01 2.471914667238274e-01 3.064703377427332e+00 -3.514777922610580e-04 +2020-06-01T12:02:00.000000 4.203317079536672e+04 -3.332814749822908e+03 -5.889399373644027e+01 2.427217099848949e-01 3.065061064127878e+00 -3.452009470051031e-04 +2020-06-01T12:02:20.000000 4.203798047272090e+04 -3.271510963289943e+03 -5.890108182039616e+01 2.382514350689766e-01 3.065412232006973e+00 -3.389233101721088e-04 +2020-06-01T12:02:40.000000 4.204270074087044e+04 -3.210200218670044e+03 -5.890804408985979e+01 2.337806514829256e-01 3.065756880312095e+00 -3.326448951633756e-04 +2020-06-01T12:03:00.000000 4.204733158973732e+04 -3.148882646358444e+03 -5.891488052879578e+01 2.293093687344608e-01 3.066095008304613e+00 -3.263657153817230e-04 +2020-06-01T12:03:20.000000 4.205187300943338e+04 -3.087558376767387e+03 -5.892159112143719e+01 2.248375963325513e-01 3.066426615259742e+00 -3.200857842314024e-04 +2020-06-01T12:03:40.000000 4.205632499026076e+04 -3.026227540322871e+03 -5.892817585228519e+01 2.203653437871814e-01 3.066751700466565e+00 -3.138051151190211e-04 +2020-06-01T12:04:00.000000 4.206068752271169e+04 -2.964890267465681e+03 -5.893463470610901e+01 2.158926206094235e-01 3.067070263228033e+00 -3.075237214535250e-04 +2020-06-01T12:04:20.000000 4.206496059746888e+04 -2.903546688647549e+03 -5.894096766794808e+01 2.114194363111598e-01 3.067382302860973e+00 -3.012416166437907e-04 +2020-06-01T12:04:40.000000 4.206914420540504e+04 -2.842196934337759e+03 -5.894717472310893e+01 2.069458004055634e-01 3.067687818696072e+00 -2.949588141019373e-04 +2020-06-01T12:05:00.000000 4.207323833758340e+04 -2.780841135014743e+03 -5.895325585716829e+01 2.024717224064844e-01 3.067986810077908e+00 -2.886753272401490e-04 +2020-06-01T12:05:20.000000 4.207724298525709e+04 -2.719479421174101e+03 -5.895921105596939e+01 1.979972118290383e-01 3.068279276364904e+00 -2.823911694746675e-04 +2020-06-01T12:05:40.000000 4.208115813986999e+04 -2.658111923319942e+03 -5.896504030562674e+01 1.935222781889679e-01 3.068565216929384e+00 -2.761063542203438e-04 +2020-06-01T12:06:00.000000 4.208498379305597e+04 -2.596738771972777e+03 -5.897074359252166e+01 1.890469310032268e-01 3.068844631157534e+00 -2.698208948958586e-04 +2020-06-01T12:06:20.000000 4.208871993663943e+04 -2.535360097661340e+03 -5.897632090330504e+01 1.845711797893775e-01 3.069117518449422e+00 -2.635348049205161e-04 +2020-06-01T12:06:40.000000 4.209236656263504e+04 -2.473976030929400e+03 -5.898177222489659e+01 1.800950340660886e-01 3.069383878218996e+00 -2.572480977152137e-04 +2020-06-01T12:07:00.000000 4.209592366324781e+04 -2.412586702332662e+03 -5.898709754448480e+01 1.756185033529133e-01 3.069643709894074e+00 -2.509607867023992e-04 +2020-06-01T12:07:20.000000 4.209939123087336e+04 -2.351192242432813e+03 -5.899229684952716e+01 1.711415971698496e-01 3.069897012916379e+00 -2.446728853056965e-04 +2020-06-01T12:07:40.000000 4.210276925809748e+04 -2.289792781808605e+03 -5.899737012774986e+01 1.666643250381509e-01 3.070143786741495e+00 -2.383844069506332e-04 +2020-06-01T12:08:00.000000 4.210605773769644e+04 -2.228388451047830e+03 -5.900231736714850e+01 1.621866964797412e-01 3.070384030838893e+00 -2.320953650632356e-04 +2020-06-01T12:08:20.000000 4.210925666263705e+04 -2.166979380746505e+03 -5.900713855598682e+01 1.577087210171564e-01 3.070617744691940e+00 -2.258057730722445e-04 +2020-06-01T12:08:40.000000 4.211236602607647e+04 -2.105565701514220e+03 -5.901183368279860e+01 1.532304081739304e-01 3.070844927797876e+00 -2.195156444063368e-04 +2020-06-01T12:09:00.000000 4.211538582136239e+04 -2.044147543967191e+03 -5.901640273638587e+01 1.487517674740950e-01 3.071065579667845e+00 -2.132249924962047e-04 +2020-06-01T12:09:20.000000 4.211831604203300e+04 -1.982725038734548e+03 -5.902084570582006e+01 1.442728084426310e-01 3.071279699826859e+00 -2.069338307739426e-04 +2020-06-01T12:09:40.000000 4.212115668181692e+04 -1.921298316451313e+03 -5.902516258044151e+01 1.397935406049620e-01 3.071487287813836e+00 -2.006421726725555e-04 +2020-06-01T12:10:00.000000 4.212390773463338e+04 -1.859867507765117e+03 -5.902935334985975e+01 1.353139734874419e-01 3.071688343181577e+00 -1.943500316265909e-04 +2020-06-01T12:10:20.000000 4.212656919459209e+04 -1.798432743328842e+03 -5.903341800395381e+01 1.308341166168182e-01 3.071882865496777e+00 -1.880574210709300e-04 +2020-06-01T12:10:40.000000 4.212914105599330e+04 -1.736994153806792e+03 -5.903735653287142e+01 1.263539795206825e-01 3.072070854340027e+00 -1.817643544425247e-04 +2020-06-01T12:11:00.000000 4.213162331332771e+04 -1.675551869871253e+03 -5.904116892702955e+01 1.218735717272187e-01 3.072252309305800e+00 -1.754708451790809e-04 +2020-06-01T12:11:20.000000 4.213401596127684e+04 -1.614106022199842e+03 -5.904485517711457e+01 1.173929027650114e-01 3.072427230002475e+00 -1.691769067195338e-04 +2020-06-01T12:11:40.000000 4.213631899471255e+04 -1.552656741480632e+03 -5.904841527408229e+01 1.129119821634190e-01 3.072595616052324e+00 -1.628825525032471e-04 +2020-06-01T12:12:00.000000 4.213853240869735e+04 -1.491204158408954e+03 -5.905184920915656e+01 1.084308194523393e-01 3.072757467091508e+00 -1.565877959728363e-04 +2020-06-01T12:12:20.000000 4.214065619848446e+04 -1.429748403684949e+03 -5.905515697383248e+01 1.039494241620317e-01 3.072912782770095e+00 -1.502926505683261e-04 +2020-06-01T12:12:40.000000 4.214269035951753e+04 -1.368289608018920e+03 -5.905833855987299e+01 9.946780582350967e-02 3.073061562752046e+00 -1.439971297334314e-04 +2020-06-01T12:13:00.000000 4.214463488743104e+04 -1.306827902123940e+03 -5.906139395931061e+01 9.498597396799685e-02 3.073203806715219e+00 -1.377012469124457e-04 +2020-06-01T12:13:20.000000 4.214648977804983e+04 -1.245363416723394e+03 -5.906432316444754e+01 9.050393812748239e-02 3.073339514351374e+00 -1.314050155496180e-04 +2020-06-01T12:13:40.000000 4.214825502738965e+04 -1.183896282544111e+03 -5.906712616785517e+01 8.602170783421603e-02 3.073468685366171e+00 -1.251084490910745e-04 +2020-06-01T12:14:00.000000 4.214993063165675e+04 -1.122426630318927e+03 -5.906980296237429e+01 8.153929262089780e-02 3.073591319479167e+00 -1.188115609831855e-04 +2020-06-01T12:14:20.000000 4.215151658724806e+04 -1.060954590785714e+03 -5.907235354111505e+01 7.705670202060520e-02 3.073707416423828e+00 -1.125143646730835e-04 +2020-06-01T12:14:40.000000 4.215301289075115e+04 -9.994802946893744e+02 -5.907477789745693e+01 7.257394556693987e-02 3.073816975947514e+00 -1.062168736096578e-04 +2020-06-01T12:15:00.000000 4.215441953894432e+04 -9.380038727785654e+02 -5.907707602504919e+01 6.809103279378799e-02 3.073919997811490e+00 -9.991910124121469e-05 +2020-06-01T12:15:20.000000 4.215573652879650e+04 -8.765254558077334e+02 -5.907924791781016e+01 6.360797323547020e-02 3.074016481790925e+00 -9.362106101783776e-05 +2020-06-01T12:15:40.000000 4.215696385746735e+04 -8.150451745328616e+02 -5.908129356992774e+01 5.912477642642808e-02 3.074106427674890e+00 -8.732276639025359e-05 +2020-06-01T12:16:00.000000 4.215810152230714e+04 -7.535631597174978e+02 -5.908321297585937e+01 5.464145190166780e-02 3.074189835266365e+00 -8.102423080955475e-05 +2020-06-01T12:16:20.000000 4.215914952085696e+04 -6.920795421253930e+02 -5.908500613033208e+01 5.015800919622123e-02 3.074266704382226e+00 -7.472546772713712e-05 +2020-06-01T12:16:40.000000 4.216010785084847e+04 -6.305944525280194e+02 -5.908667302834220e+01 4.567445784569409e-02 3.074337034853261e+00 -6.842649059596707e-05 +2020-06-01T12:17:00.000000 4.216097651020415e+04 -5.691080216953442e+02 -5.908821366515576e+01 4.119080738559258e-02 3.074400826524162e+00 -6.212731286896672e-05 +2020-06-01T12:17:20.000000 4.216175549703712e+04 -5.076203804051146e+02 -5.908962803630816e+01 3.670706735200333e-02 3.074458079253527e+00 -5.582794799995700e-05 +2020-06-01T12:17:40.000000 4.216244480965125e+04 -4.461316594333785e+02 -5.909091613760464e+01 3.222324728089845e-02 3.074508792913854e+00 -4.952840944286432e-05 +2020-06-01T12:18:00.000000 4.216304444654114e+04 -3.846419895623919e+02 -5.909207796511972e+01 2.773935670871461e-02 3.074552967391556e+00 -4.322871065285443e-05 +2020-06-01T12:18:20.000000 4.216355440639205e+04 -3.231515015731731e+02 -5.909311351519761e+01 2.325540517180918e-02 3.074590602586952e+00 -3.692886508423758e-05 +2020-06-01T12:18:40.000000 4.216397468808010e+04 -2.616603262509925e+02 -5.909402278445216e+01 1.877140220686123e-02 3.074621698414262e+00 -3.062888619351432e-05 +2020-06-01T12:19:00.000000 4.216430529067197e+04 -2.001685943817643e+02 -5.909480576976664e+01 1.428735735060701e-02 3.074646254801617e+00 -2.432878743678879e-05 +2020-06-01T12:19:20.000000 4.216454621342525e+04 -1.386764367539689e+02 -5.909546246829418e+01 9.803280139981857e-03 3.074664271691054e+00 -1.802858227076647e-05 +2020-06-01T12:19:40.000000 4.216469745578810e+04 -7.718398415427747e+01 -5.909599287745723e+01 5.319180111800140e-03 3.074675749038519e+00 -1.172828415125444e-05 +2020-06-01T12:20:00.000000 4.216475901739954e+04 -1.569136737419563e+01 -5.909639699494819e+01 8.350668032395860e-04 3.074680686813868e+00 -5.427906537239052e-06 +2020-06-01T12:20:20.000000 4.216473089808923e+04 4.580128280020696e+01 -5.909667481872876e+01 -3.649050248907331e-03 3.074679085000859e+00 8.725371152246905e-07 +2020-06-01T12:20:40.000000 4.216461309787768e+04 1.072938355761083e+02 -5.909682634703063e+01 -8.133161507344304e-03 3.074670943597163e+00 7.173033347488999e-06 +2020-06-01T12:21:00.000000 4.216440561697601e+04 1.687861601662330e+02 -5.909685157835488e+01 -1.261725743516759e-02 3.074656262614354e+00 1.347356870173554e-05 +2020-06-01T12:21:20.000000 4.216410845578621e+04 2.302781257788959e+02 -5.909675051147233e+01 -1.710132849514269e-02 3.074635042077921e+00 1.977412971910050e-05 +2020-06-01T12:21:40.000000 4.216372161490090e+04 2.917696016274735e+02 -5.909652314542345e+01 -2.158536515039124e-02 3.074607282027256e+00 2.607470294144720e-05 +2020-06-01T12:22:00.000000 4.216324509510350e+04 3.532604569235884e+02 -5.909616947951839e+01 -2.606935786389621e-02 3.074572982515660e+00 3.237527491009253e-05 +2020-06-01T12:22:20.000000 4.216267889736816e+04 4.147505608794019e+02 -5.909568951333704e+01 -3.055329709866966e-02 3.074532143610341e+00 3.867583216543348e-05 +2020-06-01T12:22:40.000000 4.216202302285972e+04 4.762397827117900e+02 -5.909508324672877e+01 -3.503717331805472e-02 3.074484765392418e+00 4.497636124923330e-05 +2020-06-01T12:23:00.000000 4.216127747293382e+04 5.377279916366238e+02 -5.909435067981275e+01 -3.952097698531131e-02 3.074430847956912e+00 5.127684870241706e-05 +2020-06-01T12:23:20.000000 4.216044224913674e+04 5.992150568717660e+02 -5.909349181297786e+01 -4.400469856383310e-02 3.074370391412762e+00 5.757728106516369e-05 +2020-06-01T12:23:40.000000 4.215951735320559e+04 6.607008476400657e+02 -5.909250664688272e+01 -4.848832851736529e-02 3.074303395882802e+00 6.387764487851101e-05 +2020-06-01T12:24:00.000000 4.215850278706812e+04 7.221852331633718e+02 -5.909139518245541e+01 -5.297185730957069e-02 3.074229861503778e+00 7.017792668332204e-05 +2020-06-01T12:24:20.000000 4.215739855284281e+04 7.836680826700622e+02 -5.909015742089389e+01 -5.745527540457535e-02 3.074149788426348e+00 7.647811302032015e-05 +2020-06-01T12:24:40.000000 4.215620465283893e+04 8.451492653878901e+02 -5.908879336366580e+01 -6.193857326644977e-02 3.074063176815069e+00 8.277819043042749e-05 +2020-06-01T12:25:00.000000 4.215492108955637e+04 9.066286505520327e+02 -5.908730301250836e+01 -6.642174135979376e-02 3.073970026848409e+00 8.907814545435680e-05 +2020-06-01T12:25:20.000000 4.215354786568577e+04 9.681061073974247e+02 -5.908568636942849e+01 -7.090477014917906e-02 3.073870338718737e+00 9.537796463356910e-05 +2020-06-01T12:25:40.000000 4.215208498410844e+04 1.029581505166548e+03 -5.908394343670286e+01 -7.538765009971560e-02 3.073764112632337e+00 1.016776345091536e-04 +2020-06-01T12:26:00.000000 4.215053244789643e+04 1.091054713103786e+03 -5.908207421687778e+01 -7.987037167664157e-02 3.073651348809387e+00 1.079771416223546e-04 +2020-06-01T12:26:20.000000 4.214889026031248e+04 1.152525600456635e+03 -5.908007871276956e+01 -8.435292534541085e-02 3.073532047483985e+00 1.142764725140911e-04 +2020-06-01T12:26:40.000000 4.214715842480989e+04 1.213994036483609e+03 -5.907795692746339e+01 -8.883530157226956e-02 3.073406208904112e+00 1.205756137273817e-04 +2020-06-01T12:27:00.000000 4.214533694503279e+04 1.275459890442179e+03 -5.907570886431466e+01 -9.331749082337590e-02 3.073273833331668e+00 1.268745518039702e-04 +2020-06-01T12:27:20.000000 4.214342582481598e+04 1.336923031596224e+03 -5.907333452694905e+01 -9.779948356534481e-02 3.073134921042456e+00 1.331732732849966e-04 +2020-06-01T12:27:40.000000 4.214142506818471e+04 1.398383329218377e+03 -5.907083391926076e+01 -1.022812702654187e-01 3.072989472326176e+00 1.394717647142256e-04 +2020-06-01T12:28:00.000000 4.213933467935518e+04 1.459840652583400e+03 -5.906820704541405e+01 -1.067628413909825e-01 3.072837487486439e+00 1.457700126345343e-04 +2020-06-01T12:28:20.000000 4.213715466273394e+04 1.521294870975667e+03 -5.906545390984327e+01 -1.112441874101134e-01 3.072678966840744e+00 1.520680035888174e-04 +2020-06-01T12:28:40.000000 4.213488502291843e+04 1.582745853682520e+03 -5.906257451725210e+01 -1.157252987910913e-01 3.072513910720502e+00 1.583657241207004e-04 +2020-06-01T12:29:00.000000 4.213252576469649e+04 1.644193470001878e+03 -5.905956887261321e+01 -1.202061660029575e-01 3.072342319471024e+00 1.646631607754681e-04 +2020-06-01T12:29:20.000000 4.213007689304676e+04 1.705637589234720e+03 -5.905643698117050e+01 -1.246867795149648e-01 3.072164193451517e+00 1.709603000960786e-04 +2020-06-01T12:29:40.000000 4.212753841313840e+04 1.767078080693558e+03 -5.905317884843577e+01 -1.291671297971961e-01 3.071979533035087e+00 1.772571286291127e-04 +2020-06-01T12:30:00.000000 4.212491033033105e+04 1.828514813695309e+03 -5.904979448019154e+01 -1.336472073200444e-01 3.071788338608736e+00 1.835536329193551e-04 +2020-06-01T12:30:20.000000 4.212219265017522e+04 1.889947657565611e+03 -5.904628388248967e+01 -1.381270025545269e-01 3.071590610573374e+00 1.898497995130953e-04 +2020-06-01T12:30:40.000000 4.211938537841175e+04 1.951376481637527e+03 -5.904264706165171e+01 -1.426065059721912e-01 3.071386349343800e+00 1.961456149567470e-04 +2020-06-01T12:31:00.000000 4.211648852097206e+04 2.012801155255613e+03 -5.903888402426831e+01 -1.470857080454123e-01 3.071175555348702e+00 2.024410657980948e-04 +2020-06-01T12:31:20.000000 4.211350208397823e+04 2.074221547769132e+03 -5.903499477720005e+01 -1.515645992468967e-01 3.070958229030679e+00 2.087361385849763e-04 +2020-06-01T12:31:40.000000 4.211042607374276e+04 2.135637528539620e+03 -5.903097932757702e+01 -1.560431700502347e-01 3.070734370846209e+00 2.150308198662046e-04 +2020-06-01T12:32:00.000000 4.210726049676878e+04 2.197048966934997e+03 -5.902683768279947e+01 -1.605214109294710e-01 3.070503981265680e+00 2.213250961903285e-04 +2020-06-01T12:32:20.000000 4.210400535974976e+04 2.258455732336022e+03 -5.902256985053594e+01 -1.649993123595757e-01 3.070267060773347e+00 2.276189541081144e-04 +2020-06-01T12:32:40.000000 4.210066066956996e+04 2.319857694128375e+03 -5.901817583872597e+01 -1.694768648158662e-01 3.070023609867389e+00 2.339123801695235e-04 +2020-06-01T12:33:00.000000 4.209722643330369e+04 2.381254721715034e+03 -5.901365565557705e+01 -1.739540587749096e-01 3.069773629059838e+00 2.402053609268114e-04 +2020-06-01T12:33:20.000000 4.209370265821611e+04 2.442646684502264e+03 -5.900900930956723e+01 -1.784308847135023e-01 3.069517118876653e+00 2.464978829320316e-04 +2020-06-01T12:33:40.000000 4.209008935176265e+04 2.504033451911724e+03 -5.900423680944404e+01 -1.829073331095521e-01 3.069254079857648e+00 2.527899327380588e-04 +2020-06-01T12:34:00.000000 4.208638652158916e+04 2.565414893373409e+03 -5.899933816422343e+01 -1.873833944415621e-01 3.068984512556546e+00 2.590814968997882e-04 +2020-06-01T12:34:20.000000 4.208259417553203e+04 2.626790878329512e+03 -5.899431338319244e+01 -1.918590591889156e-01 3.068708417540950e+00 2.653725619710848e-04 +2020-06-01T12:34:40.000000 4.207871232161798e+04 2.688161276232278e+03 -5.898916247590672e+01 -1.963343178317166e-01 3.068425795392348e+00 2.716631145077406e-04 +2020-06-01T12:35:00.000000 4.207474096806403e+04 2.749525956549185e+03 -5.898388545219078e+01 -2.008091608511689e-01 3.068136646706103e+00 2.779531410671256e-04 +2020-06-01T12:35:20.000000 4.207068012327770e+04 2.810884788755726e+03 -5.897848232213942e+01 -2.052835787290493e-01 3.067840972091479e+00 2.842426282065100e-04 +2020-06-01T12:35:40.000000 4.206652979585681e+04 2.872237642343260e+03 -5.897295309611643e+01 -2.097575619482811e-01 3.067538772171594e+00 2.905315624846699e-04 +2020-06-01T12:36:00.000000 4.206228999458950e+04 2.933584386812198e+03 -5.896729778475490e+01 -2.142311009924357e-01 3.067230047583478e+00 2.968199304614206e-04 +2020-06-01T12:36:20.000000 4.205796072845417e+04 2.994924891679726e+03 -5.896151639895711e+01 -2.187041863462969e-01 3.066914798978008e+00 3.031077186978318e-04 +2020-06-01T12:36:40.000000 4.205354200661970e+04 3.056259026472096e+03 -5.895560894989604e+01 -2.231768084952984e-01 3.066593027019965e+00 3.093949137548153e-04 +2020-06-01T12:37:00.000000 4.204903383844499e+04 3.117586660733029e+03 -5.894957544901157e+01 -2.276489579261366e-01 3.066264732387984e+00 3.156815021964921e-04 +2020-06-01T12:37:20.000000 4.204443623347938e+04 3.178907664017225e+03 -5.894341590801451e+01 -2.321206251262979e-01 3.065929915774591e+00 3.219674705867174e-04 +2020-06-01T12:37:40.000000 4.203974920146249e+04 3.240221905892726e+03 -5.893713033888518e+01 -2.365918005842286e-01 3.065588577886182e+00 3.282528054902727e-04 +2020-06-01T12:38:00.000000 4.203497275232392e+04 3.301529255945450e+03 -5.893071875387219e+01 -2.410624747896699e-01 3.065240719443009e+00 3.345374934739742e-04 +2020-06-01T12:38:20.000000 4.203010689618366e+04 3.362829583772762e+03 -5.892418116549344e+01 -2.455326382331843e-01 3.064886341179214e+00 3.408215211057718e-04 +2020-06-01T12:38:40.000000 4.202515164335188e+04 3.424122758986619e+03 -5.891751758653717e+01 -2.500022814063885e-01 3.064525443842801e+00 3.471048749539996e-04 +2020-06-01T12:39:00.000000 4.202010700432879e+04 3.485408651216174e+03 -5.891072803005872e+01 -2.544713948021414e-01 3.064158028195640e+00 3.533875415899352e-04 +2020-06-01T12:39:20.000000 4.201497298980480e+04 3.546687130105015e+03 -5.890381250938488e+01 -2.589399689143437e-01 3.063784095013463e+00 3.596695075843340e-04 +2020-06-01T12:39:40.000000 4.200974961066026e+04 3.607958065313301e+03 -5.889677103810931e+01 -2.634079942380929e-01 3.063403645085863e+00 3.659507595109538e-04 +2020-06-01T12:40:00.000000 4.200443687796605e+04 3.669221326514133e+03 -5.888960363009723e+01 -2.678754612694199e-01 3.063016679216312e+00 3.722312839431527e-04 +2020-06-01T12:40:20.000000 4.199903480298249e+04 3.730476783401129e+03 -5.888231029948051e+01 -2.723423605058390e-01 3.062623198222117e+00 3.785110674574773e-04 +2020-06-01T12:40:40.000000 4.199354339716058e+04 3.791724305679621e+03 -5.887489106066221e+01 -2.768086824457096e-01 3.062223202934479e+00 3.847900966303997e-04 +2020-06-01T12:41:00.000000 4.198796267214073e+04 3.852963763076563e+03 -5.886734592831204e+01 -2.812744175889546e-01 3.061816694198416e+00 3.910683580413637e-04 +2020-06-01T12:41:20.000000 4.198229263975376e+04 3.914195025332502e+03 -5.885967491737066e+01 -2.857395564364781e-01 3.061403672872829e+00 3.973458382699806e-04 +2020-06-01T12:41:40.000000 4.197653331202045e+04 3.975417962205308e+03 -5.885187804304791e+01 -2.902040894904367e-01 3.060984139830474e+00 4.036225238973817e-04 +2020-06-01T12:42:00.000000 4.197068470115113e+04 4.036632443473677e+03 -5.884395532082046e+01 -2.946680072544929e-01 3.060558095957929e+00 4.098984015075298e-04 +2020-06-01T12:42:20.000000 4.196474681954648e+04 4.097838338930828e+03 -5.883590676643579e+01 -2.991313002333592e-01 3.060125542155654e+00 4.161734576846686e-04 +2020-06-01T12:42:40.000000 4.195871967979695e+04 4.159035518388363e+03 -5.882773239590907e+01 -3.035939589330753e-01 3.059686479337955e+00 4.224476790155031e-04 +2020-06-01T12:43:00.000000 4.195260329468264e+04 4.220223851679209e+03 -5.881943222552537e+01 -3.080559738612256e-01 3.059240908432956e+00 4.287210520875993e-04 +2020-06-01T12:43:20.000000 4.194639767717377e+04 4.281403208651307e+03 -5.881100627183732e+01 -3.125173355264795e-01 3.058788830382657e+00 4.349935634910198e-04 +2020-06-01T12:43:40.000000 4.194010284043010e+04 4.342573459174926e+03 -5.880245455166743e+01 -3.169780344391222e-01 3.058330246142878e+00 4.412651998168076e-04 +2020-06-01T12:44:00.000000 4.193371879780146e+04 4.403734473136326e+03 -5.879377708210655e+01 -3.214380611105928e-01 3.057865156683304e+00 4.475359476579238e-04 +2020-06-01T12:44:20.000000 4.192724556282711e+04 4.464886120444696e+03 -5.878497388051412e+01 -3.258974060539938e-01 3.057393562987419e+00 4.538057936092357e-04 +2020-06-01T12:44:40.000000 4.192068314923641e+04 4.526028271025436e+03 -5.877604496451848e+01 -3.303560597835968e-01 3.056915466052590e+00 4.600747242672033e-04 +2020-06-01T12:45:00.000000 4.191403157094803e+04 4.587160794827647e+03 -5.876699035201592e+01 -3.348140128153915e-01 3.056430866889977e+00 4.663427262306111e-04 +2020-06-01T12:45:20.000000 4.190729084207047e+04 4.648283561818435e+03 -5.875781006117196e+01 -3.392712556666685e-01 3.055939766524601e+00 4.726097860993827e-04 +2020-06-01T12:45:40.000000 4.190046097690218e+04 4.709396441984766e+03 -5.874850411042093e+01 -3.437277788561571e-01 3.055442165995308e+00 4.788758904755594e-04 +2020-06-01T12:46:00.000000 4.189354198993061e+04 4.770499305337007e+03 -5.873907251846594e+01 -3.481835729042799e-01 3.054938066354759e+00 4.851410259625522e-04 +2020-06-01T12:46:20.000000 4.188653389583311e+04 4.831592021906651e+03 -5.872951530427615e+01 -3.526386283329902e-01 3.054427468669439e+00 4.914051791675573e-04 +2020-06-01T12:46:40.000000 4.187943670947663e+04 4.892674461744416e+03 -5.871983248709302e+01 -3.570929356656324e-01 3.053910374019676e+00 4.976683366970270e-04 +2020-06-01T12:47:00.000000 4.187225044591761e+04 4.953746494923251e+03 -5.871002408642345e+01 -3.615464854271601e-01 3.053386783499614e+00 5.039304851613120e-04 +2020-06-01T12:47:20.000000 4.186497512040178e+04 5.014807991540568e+03 -5.870009012204432e+01 -3.659992681442994e-01 3.052856698217193e+00 5.101916111719388e-04 +2020-06-01T12:47:40.000000 4.185761074836460e+04 5.075858821712482e+03 -5.869003061399911e+01 -3.704512743451310e-01 3.052320119294209e+00 5.164517013433001e-04 +2020-06-01T12:48:00.000000 4.185015734543068e+04 5.136898855581328e+03 -5.867984558260132e+01 -3.749024945596335e-01 3.051777047866230e+00 5.227107422910240e-04 +2020-06-01T12:48:20.000000 4.184261492741424e+04 5.197927963308952e+03 -5.866953504843240e+01 -3.793529193192002e-01 3.051227485082670e+00 5.289687206328433e-04 +2020-06-01T12:48:40.000000 4.183498351031858e+04 5.258946015083270e+03 -5.865909903234223e+01 -3.838025391571131e-01 3.050671432106725e+00 5.352256229885449e-04 +2020-06-01T12:49:00.000000 4.182726311033673e+04 5.319952881112667e+03 -5.864853755544678e+01 -3.882513446081350e-01 3.050108890115430e+00 5.414814359813162e-04 +2020-06-01T12:49:20.000000 4.181945374385051e+04 5.380948431632521e+03 -5.863785063913312e+01 -3.926993262089876e-01 3.049539860299581e+00 5.477361462347959e-04 +2020-06-01T12:49:40.000000 4.181155542743132e+04 5.441932536899380e+03 -5.862703830505496e+01 -3.971464744979239e-01 3.048964343863820e+00 5.539897403756296e-04 +2020-06-01T12:50:00.000000 4.180356817783970e+04 5.502905067195037e+03 -5.861610057513422e+01 -4.015927800150272e-01 3.048382342026564e+00 5.602422050326477e-04 +2020-06-01T12:50:20.000000 4.179549201202548e+04 5.563865892824963e+03 -5.860503747155906e+01 -4.060382333020973e-01 3.047793856020045e+00 5.664935268378536e-04 +2020-06-01T12:50:40.000000 4.178732694712729e+04 5.624814884122063e+03 -5.859384901679006e+01 -4.104828249029221e-01 3.047198887090254e+00 5.727436924232028e-04 +2020-06-01T12:51:00.000000 4.177907300047327e+04 5.685751911440457e+03 -5.858253523355079e+01 -4.149265453628255e-01 3.046597436497023e+00 5.789926884254742e-04 +2020-06-01T12:51:20.000000 4.177073018958025e+04 5.746676845163512e+03 -5.857109614483580e+01 -4.193693852292546e-01 3.045989505513924e+00 5.852405014823235e-04 +2020-06-01T12:51:40.000000 4.176229853215448e+04 5.807589555696452e+03 -5.855953177390664e+01 -4.238113350512374e-01 3.045375095428358e+00 5.914871182340779e-04 +2020-06-01T12:52:00.000000 4.175377804609095e+04 5.868489913473321e+03 -5.854784214429240e+01 -4.282523853798950e-01 3.044754207541482e+00 5.977325253237193e-04 +2020-06-01T12:52:20.000000 4.174516874947357e+04 5.929377788953068e+03 -5.853602727978988e+01 -4.326925267681501e-01 3.044126843168244e+00 6.039767093964998e-04 +2020-06-01T12:52:40.000000 4.173647066057525e+04 5.990253052622129e+03 -5.852408720446366e+01 -4.371317497709222e-01 3.043493003637359e+00 6.102196571002699e-04 +2020-06-01T12:53:00.000000 4.172768379785796e+04 6.051115574990979e+03 -5.851202194264643e+01 -4.415700449448711e-01 3.042852690291348e+00 6.164613550849889e-04 +2020-06-01T12:53:20.000000 4.171880817997210e+04 6.111965226600729e+03 -5.849983151893754e+01 -4.460074028488785e-01 3.042205904486467e+00 6.227017900036493e-04 +2020-06-01T12:53:40.000000 4.170984382575729e+04 6.172801878017003e+03 -5.848751595820479e+01 -4.504438140436057e-01 3.041552647592765e+00 6.289409485114086e-04 +2020-06-01T12:54:00.000000 4.170079075424169e+04 6.233625399833781e+03 -5.847507528558298e+01 -4.548792690917677e-01 3.040892920994058e+00 6.351788172661087e-04 +2020-06-01T12:54:20.000000 4.169164898464243e+04 6.294435662671979e+03 -5.846250952647513e+01 -4.593137585580347e-01 3.040226726087930e+00 6.414153829279052e-04 +2020-06-01T12:54:40.000000 4.168241853636486e+04 6.355232537183414e+03 -5.844981870654914e+01 -4.637472730093183e-01 3.039554064285698e+00 6.476506321608271e-04 +2020-06-01T12:55:00.000000 4.167309942900344e+04 6.416015894044621e+03 -5.843700285174497e+01 -4.681798030143219e-01 3.038874937012475e+00 6.538845516291847e-04 +2020-06-01T12:55:20.000000 4.166369168234076e+04 6.476785603964448e+03 -5.842406198826438e+01 -4.726113391440944e-01 3.038189345707093e+00 6.601171280026981e-04 +2020-06-01T12:55:40.000000 4.165419531634859e+04 6.537541537676683e+03 -5.841099614257995e+01 -4.770418719714926e-01 3.037497291822177e+00 6.663483479520682e-04 +2020-06-01T12:56:00.000000 4.164461035118653e+04 6.598283565948414e+03 -5.839780534143041e+01 -4.814713920717895e-01 3.036798776824055e+00 6.725781981511359e-04 +2020-06-01T12:56:20.000000 4.163493680720325e+04 6.659011559572206e+03 -5.838448961182206e+01 -4.858998900221064e-01 3.036093802192849e+00 6.788066652764773e-04 +2020-06-01T12:56:40.000000 4.162517470493531e+04 6.719725389374516e+03 -5.837104898102686e+01 -4.903273564020247e-01 3.035382369422375e+00 6.850337360080370e-04 +2020-06-01T12:57:00.000000 4.161532406510821e+04 6.780424926207381e+03 -5.835748347658608e+01 -4.947537817929794e-01 3.034664480020240e+00 6.912593970274764e-04 +2020-06-01T12:57:20.000000 4.160538490863527e+04 6.841110040957172e+03 -5.834379312630593e+01 -4.991791567788986e-01 3.033940135507745e+00 6.974836350204018e-04 +2020-06-01T12:57:40.000000 4.159535725661845e+04 6.901780604538337e+03 -5.832997795826064e+01 -5.036034719457487e-01 3.033209337419943e+00 7.037064366745962e-04 +2020-06-01T12:58:00.000000 4.158524113034789e+04 6.962436487896638e+03 -5.831603800078998e+01 -5.080267178817659e-01 3.032472087305623e+00 7.099277886815115e-04 +2020-06-01T12:58:20.000000 4.157503655130207e+04 7.023077562008491e+03 -5.830197328250284e+01 -5.124488851774126e-01 3.031728386727305e+00 7.161476777344611e-04 +2020-06-01T12:58:40.000000 4.156474354114733e+04 7.083703697882966e+03 -5.828778383227232e+01 -5.168699644255207e-01 3.030978237261212e+00 7.223660905307820e-04 +2020-06-01T12:59:00.000000 4.155436212173828e+04 7.144314766560402e+03 -5.827346967924105e+01 -5.212899462211917e-01 3.030221640497298e+00 7.285830137697079e-04 +2020-06-01T12:59:20.000000 4.154389231511745e+04 7.204910639113693e+03 -5.825903085281367e+01 -5.257088211618913e-01 3.029458598039223e+00 7.347984341552976e-04 +2020-06-01T12:59:40.000000 4.153333414351581e+04 7.265491186645347e+03 -5.824446738266613e+01 -5.301265798472311e-01 3.028689111504395e+00 7.410123383927807e-04 +2020-06-01T13:00:00.000000 4.152268762935179e+04 7.326056280293878e+03 -5.822977929873841e+01 -5.345432128794397e-01 3.027913182523888e+00 7.472247131914804e-04 diff --git a/data/tests/ccsds/oem/LEO_10s.oem b/data/tests/ccsds/oem/LEO_10s.oem new file mode 100644 index 00000000..86b9a344 --- /dev/null +++ b/data/tests/ccsds/oem/LEO_10s.oem @@ -0,0 +1,384 @@ +CCSDS_OEM_VERS = 2.0 + +COMMENT Orbit data are consistent with planetary ephemeris DE-430 + +CREATION_DATE = 2020-06-01T00:34:28 +ORIGINATOR = Test + +META_START +OBJECT_NAME = TEST_OBJ +OBJECT_ID = 0000-000A +CENTER_NAME = Earth +REF_FRAME = ICRF +TIME_SYSTEM = UTC +START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_STOP_TIME = 2020-06-01T13:00:00.000000 +STOP_TIME = 2020-06-01T13:00:00.000000 +INTERPOLATION = Lagrange +INTERPOLATION_DEGREE = 7 +META_STOP + +COMMENT Vehicle's position at any requested time was actually computed using an algorithm, not an interpolation of a table of ephemeris. + +2020-06-01T12:00:00.000000 -4.706641952872011e+03 -2.918623186846944e+03 3.932995817738559e+03 6.077667602389965e-01 -6.470290930680426e+00 -4.059846290755485e+00 +2020-06-01T12:00:10.000000 -4.700265430169727e+03 -2.983139300409308e+03 3.892147727341395e+03 6.675304710453071e-01 -6.432795791294935e+00 -4.109703057519924e+00 +2020-06-01T12:00:20.000000 -4.693291684781027e+03 -3.047276370669087e+03 3.850803678824201e+03 7.272101199321003e-01 -6.394482807751816e+00 -4.159036763181199e+00 +2020-06-01T12:00:30.000000 -4.685721595284556e+03 -3.111026243314468e+03 3.808968934370739e+03 7.867981122680069e-01 -6.355356833818136e+00 -4.207841100442664e+00 +2020-06-01T12:00:40.000000 -4.677556116154978e+03 -3.174380813095389e+03 3.766648818906370e+03 8.462868638321894e-01 -6.315422827800630e+00 -4.256109828677017e+00 +2020-06-01T12:00:50.000000 -4.668796277654296e+03 -3.237332024862532e+03 3.723848719429807e+03 9.056688017906455e-01 -6.274685851971593e+00 -4.303836774764866e+00 +2020-06-01T12:01:00.000000 -4.659443185713036e+03 -3.299871874607716e+03 3.680574084331019e+03 9.649363656794152e-01 -6.233151071975111e+00 -4.351015833932221e+00 +2020-06-01T12:01:10.000000 -4.649498021801580e+03 -3.361992410493455e+03 3.636830422704669e+03 1.024082008383894e+00 -6.190823756221972e+00 -4.397640970576250e+00 +2020-06-01T12:01:20.000000 -4.638962042792268e+03 -3.423685733875928e+03 3.592623303655368e+03 1.083098197117632e+00 -6.147709275269945e+00 -4.443706219083486e+00 +2020-06-01T12:01:30.000000 -4.627836580810544e+03 -3.484944000326180e+03 3.547958355592025e+03 1.141977414405668e+00 -6.103813101186566e+00 -4.489205684643304e+00 +2020-06-01T12:01:40.000000 -4.616123043077509e+03 -3.545759420640802e+03 3.502841265516133e+03 1.200712159063435e+00 -6.059140806899353e+00 -4.534133544051186e+00 +2020-06-01T12:01:50.000000 -4.603822911742201e+03 -3.606124261846201e+03 3.457277778302863e+03 1.259294947175898e+00 -6.013698065531801e+00 -4.578484046503268e+00 +2020-06-01T12:02:00.000000 -4.590937743703589e+03 -3.666030848199918e+03 3.411273695970938e+03 1.317718313079672e+00 -5.967490649721602e+00 -4.622251514385893e+00 +2020-06-01T12:02:10.000000 -4.577469170423499e+03 -3.725471562179835e+03 3.364834876948969e+03 1.375974810339659e+00 -5.920524430928235e+00 -4.665430344052479e+00 +2020-06-01T12:02:20.000000 -4.563418897729462e+03 -3.784438845470672e+03 3.317967235330145e+03 1.434057012728776e+00 -5.872805378722244e+00 -4.708015006595651e+00 +2020-06-01T12:02:30.000000 -4.548788705607662e+03 -3.842925199940826e+03 3.270676740121833e+03 1.491957515204705e+00 -5.824339560062208e+00 -4.750000048608095e+00 +2020-06-01T12:02:40.000000 -4.533580447986358e+03 -3.900923188612860e+03 3.222969414486396e+03 1.549668934886236e+00 -5.775133138556050e+00 -4.791380092935644e+00 +2020-06-01T12:02:50.000000 -4.517796052509134e+03 -3.958425436626153e+03 3.174851334975467e+03 1.607183912028244e+00 -5.725192373708464e+00 -4.832149839420446e+00 +2020-06-01T12:03:00.000000 -4.501437520299353e+03 -4.015424632192055e+03 3.126328630755376e+03 1.664495110994896e+00 -5.674523620152898e+00 -4.872304065636105e+00 +2020-06-01T12:03:10.000000 -4.484506925712824e+03 -4.071913527543125e+03 3.077407482825847e+03 1.721595221233844e+00 -5.623133326868691e+00 -4.911837627612849e+00 +2020-06-01T12:03:20.000000 -4.467006416084219e+03 -4.127884939868857e+03 3.028094123233729e+03 1.778476958242156e+00 -5.571028036387249e+00 -4.950745460551290e+00 +2020-06-01T12:03:30.000000 -4.448938211459444e+03 -4.183331752251730e+03 2.978394834275086e+03 1.835133064539600e+00 -5.518214383977946e+00 -4.989022579530110e+00 +2020-06-01T12:03:40.000000 -4.430304604322235e+03 -4.238246914587415e+03 2.928315947694557e+03 1.891556310631418e+00 -5.464699096825150e+00 -5.026664080200276e+00 +2020-06-01T12:03:50.000000 -4.411107959308994e+03 -4.292623444500620e+03 2.877863843875861e+03 1.947739495973177e+00 -5.410488993188467e+00 -5.063665139470812e+00 +2020-06-01T12:04:00.000000 -4.391350712914063e+03 -4.346454428252259e+03 2.827044951025376e+03 2.003675449933014e+00 -5.355590981548576e+00 -5.100021016184278e+00 +2020-06-01T12:04:10.000000 -4.371035373186218e+03 -4.399733021637332e+03 2.775865744348819e+03 2.059357032750304e+00 -5.300012059739249e+00 -5.135727051781930e+00 +2020-06-01T12:04:20.000000 -4.350164519415090e+03 -4.452452450872938e+03 2.724332745223847e+03 2.114777136490652e+00 -5.243759314067399e+00 -5.170778670956494e+00 +2020-06-01T12:04:30.000000 -4.328740801808033e+03 -4.504606013480569e+03 2.672452520361210e+03 2.169928686000916e+00 -5.186839918414647e+00 -5.205171382297630e+00 +2020-06-01T12:04:40.000000 -4.306766941157377e+03 -4.556187079155718e+03 2.620231680965393e+03 2.224804639857589e+00 -5.129261133330363e+00 -5.238900778922379e+00 +2020-06-01T12:04:50.000000 -4.284245728499149e+03 -4.607189090629902e+03 2.567676881885224e+03 2.279397991313107e+00 -5.071030305108004e+00 -5.271962539097137e+00 +2020-06-01T12:05:00.000000 -4.261180024760059e+03 -4.657605564525496e+03 2.514794820757997e+03 2.333701769241239e+00 -5.012154864846412e+00 -5.304352426848490e+00 +2020-06-01T12:05:10.000000 -4.237572760398421e+03 -4.707430092196606e+03 2.461592237150257e+03 2.387709039073583e+00 -4.952642327501606e+00 -5.336066292560874e+00 +2020-06-01T12:05:20.000000 -4.213426935031972e+03 -4.756656340565029e+03 2.408075911690245e+03 2.441412903737561e+00 -4.892500290921230e+00 -5.367100073564220e+00 +2020-06-01T12:05:30.000000 -4.188745617059942e+03 -4.805278052942252e+03 2.354252665196498e+03 2.494806504585388e+00 -4.831736434869173e+00 -5.397449794708640e+00 +2020-06-01T12:05:40.000000 -4.163531943272351e+03 -4.853289049846087e+03 2.300129357798153e+03 2.547883022323860e+00 -4.770358520032924e+00 -5.427111568928766e+00 +2020-06-01T12:05:50.000000 -4.137789118453885e+03 -4.900683229802241e+03 2.245712888052958e+03 2.600635677933992e+00 -4.708374387022834e+00 -5.456081597794165e+00 +2020-06-01T12:06:00.000000 -4.111520414974665e+03 -4.947454570140411e+03 2.191010192057483e+03 2.653057733591196e+00 -4.645791955354252e+00 -5.484356172048911e+00 +2020-06-01T12:06:10.000000 -4.084729172374439e+03 -4.993597127778132e+03 2.136028242551392e+03 2.705142493578344e+00 -4.582619222417712e+00 -5.511931672139098e+00 +2020-06-01T12:06:20.000000 -4.057418796936941e+03 -5.039105039993225e+03 2.080774048020210e+03 2.756883305192655e+00 -4.518864262439427e+00 -5.538804568725962e+00 +2020-06-01T12:06:30.000000 -4.029592761255180e+03 -5.083972525188055e+03 2.025254651787824e+03 2.808273559650023e+00 -4.454535225424605e+00 -5.564971423189037e+00 +2020-06-01T12:06:40.000000 -4.001254603787212e+03 -5.128193883641420e+03 1.969477131109111e+03 2.859306692981862e+00 -4.389640336092684e+00 -5.590428888114032e+00 +2020-06-01T12:06:50.000000 -3.972407928404658e+03 -5.171763498249838e+03 1.913448596254299e+03 2.909976186926591e+00 -4.324187892798307e+00 -5.615173707769516e+00 +2020-06-01T12:07:00.000000 -3.943056403929655e+03 -5.214675835259544e+03 1.857176189589868e+03 2.960275569817110e+00 -4.258186266439768e+00 -5.639202718569954e+00 +2020-06-01T12:07:10.000000 -3.913203763664632e+03 -5.256925444986523e+03 1.800667084653898e+03 3.010198417461215e+00 -4.191643899355799e+00 -5.662512849526055e+00 +2020-06-01T12:07:20.000000 -3.882853804915049e+03 -5.298506962523117e+03 1.743928485230141e+03 3.059738354013206e+00 -4.124569304214312e+00 -5.685101122680507e+00 +2020-06-01T12:07:30.000000 -3.852010388498854e+03 -5.339415108438418e+03 1.686967624413342e+03 3.108889052844824e+00 -4.056971062883206e+00 -5.706964653532172e+00 +2020-06-01T12:07:40.000000 -3.820677438251264e+03 -5.379644689463474e+03 1.629791763672696e+03 3.157644237405419e+00 -3.988857825294099e+00 -5.728100651445808e+00 +2020-06-01T12:07:50.000000 -3.788858940520022e+03 -5.419190599165400e+03 1.572408191912881e+03 3.205997682075560e+00 -3.920238308295794e+00 -5.748506420047547e+00 +2020-06-01T12:08:00.000000 -3.756558943650791e+03 -5.458047818612074e+03 1.514824224527295e+03 3.253943213016518e+00 -3.851121294492612e+00 -5.768179357608159e+00 +2020-06-01T12:08:10.000000 -3.723781557465550e+03 -5.496211417022936e+03 1.457047202451438e+03 3.301474709009835e+00 -3.781515631076192e+00 -5.787116957410955e+00 +2020-06-01T12:08:20.000000 -3.690530952732129e+03 -5.533676552409369e+03 1.399084491209630e+03 3.348586102291445e+00 -3.711430228643850e+00 -5.805316808106897e+00 +2020-06-01T12:08:30.000000 -3.656811360625729e+03 -5.570438472202182e+03 1.340943479960931e+03 3.395271379377002e+00 -3.640874060009022e+00 -5.822776594054680e+00 +2020-06-01T12:08:40.000000 -3.622627072181245e+03 -5.606492513868636e+03 1.282631580538743e+03 3.441524581881551e+00 -3.569856158998440e+00 -5.839494095647692e+00 +2020-06-01T12:08:50.000000 -3.587982437738603e+03 -5.641834105515785e+03 1.224156226490476e+03 3.487339807329305e+00 -3.498385619242903e+00 -5.855467189625655e+00 +2020-06-01T12:09:00.000000 -3.552881866379751e+03 -5.676458766482464e+03 1.165524872111659e+03 3.532711209956728e+00 -3.426471592955968e+00 -5.870693849372763e+00 +2020-06-01T12:09:10.000000 -3.517329825356695e+03 -5.710362107919537e+03 1.106744991477491e+03 3.577633001507957e+00 -3.354123289702908e+00 -5.885172145201179e+00 +2020-06-01T12:09:20.000000 -3.481330839513665e+03 -5.743539833356017e+03 1.047824077473909e+03 3.622099452019798e+00 -3.281349975163591e+00 -5.898900244619562e+00 +2020-06-01T12:09:30.000000 -3.444889490698734e+03 -5.775987739255188e+03 9.887696408227265e+02 3.666104890601250e+00 -3.208160969881794e+00 -5.911876412587777e+00 +2020-06-01T12:09:40.000000 -3.408010417170419e+03 -5.807701715555958e+03 9.295892091066388e+02 3.709643706201863e+00 -3.134565648009434e+00 -5.924099011756436e+00 +2020-06-01T12:09:50.000000 -3.370698312994821e+03 -5.838677746202961e+03 8.702903257911622e+02 3.752710348372738e+00 -3.060573436040424e+00 -5.935566502691869e+00 +2020-06-01T12:10:00.000000 -3.332957927435792e+03 -5.868911909663564e+03 8.108805492439675e+02 3.795299328018322e+00 -2.986193811536257e+00 -5.946277444086439e+00 +2020-06-01T12:10:10.000000 -3.294794064337389e+03 -5.898400379432176e+03 7.513674517529167e+02 3.837405218139051e+00 -2.911436301843929e+00 -5.956230492953856e+00 +2020-06-01T12:10:20.000000 -3.256211581499774e+03 -5.927139424521153e+03 6.917586185427405e+02 3.879022654564050e+00 -2.836310482807004e+00 -5.965424404809520e+00 +2020-06-01T12:10:30.000000 -3.217215390046902e+03 -5.955125409939645e+03 6.320616467871837e+02 3.920146336676044e+00 -2.760825977466173e+00 -5.973858033836427e+00 +2020-06-01T12:10:40.000000 -3.177810453786745e+03 -5.982354797159075e+03 5.722841446235575e+02 3.960771028125246e+00 -2.684992454755577e+00 -5.981530333035380e+00 +2020-06-01T12:10:50.000000 -3.138001788566757e+03 -6.008824144564573e+03 5.124337301633021e+02 4.000891557533472e+00 -2.608819628191505e+00 -5.988440354360671e+00 +2020-06-01T12:11:00.000000 -3.097794461618007e+03 -6.034530107895638e+03 4.525180305009459e+02 4.040502819190501e+00 -2.532317254551086e+00 -5.994587248840671e+00 +2020-06-01T12:11:10.000000 -3.057193590897274e+03 -6.059469440670719e+03 3.925446807240264e+02 4.079599773737457e+00 -2.455495132549428e+00 -5.999970266683071e+00 +2020-06-01T12:11:20.000000 -3.016204344419551e+03 -6.083638994600188e+03 3.325213229210871e+02 4.118177448841693e+00 -2.378363101507761e+00 -6.004588757365195e+00 +2020-06-01T12:11:30.000000 -2.974831939583383e+03 -6.107035719986280e+03 2.724556051867473e+02 4.156230939862337e+00 -2.300931040013547e+00 -6.008442169709292e+00 +2020-06-01T12:11:40.000000 -2.933081642491223e+03 -6.129656666108496e+03 2.123551806298391e+02 4.193755410503365e+00 -2.223208864578940e+00 -6.011530051942396e+00 +2020-06-01T12:11:50.000000 -2.890958767264068e+03 -6.151498981595477e+03 1.522277063789782e+02 4.230746093456700e+00 -2.145206528292627e+00 -6.013852051741196e+00 +2020-06-01T12:12:00.000000 -2.848468675345606e+03 -6.172559914785259e+03 9.208084258677776e+01 4.267198291036458e+00 -2.066934019462813e+00 -6.015407916261775e+00 +2020-06-01T12:12:10.000000 -2.805616774805229e+03 -6.192836814069417e+03 3.192225143604528e+01 4.303107375799784e+00 -1.988401360260590e+00 -6.016197492154057e+00 +2020-06-01T12:12:20.000000 -2.762408519630994e+03 -6.212327128225562e+03 -2.824040385523783e+01 4.338468791158559e+00 -1.909618605354993e+00 -6.016220725561178e+00 +2020-06-01T12:12:30.000000 -2.718849409019226e+03 -6.231028406734820e+03 -8.839946002936485e+01 4.373278051979354e+00 -1.830595840545125e+00 -6.015477662103646e+00 +2020-06-01T12:12:40.000000 -2.674944986655973e+03 -6.248938300086720e+03 -1.485472547822320e+02 4.407530745172304e+00 -1.751343181388142e+00 -6.013968446848406e+00 +2020-06-01T12:12:50.000000 -2.630700839994037e+03 -6.266054560069578e+03 -2.086761277592007e+02 4.441222530269335e+00 -1.671870771822000e+00 -6.011693324262726e+00 +2020-06-01T12:13:00.000000 -2.586122599523804e+03 -6.282375040047460e+03 -2.687784215469722e+02 4.474349139989921e+00 -1.592188782787100e+00 -6.008652638153038e+00 +2020-06-01T12:13:10.000000 -2.541215938038856e+03 -6.297897695223002e+03 -3.288464826684273e+02 4.506906380796647e+00 -1.512307410841983e+00 -6.004846831588624e+00 +2020-06-01T12:13:20.000000 -2.495986569895421e+03 -6.312620582886892e+03 -3.888726625725598e+02 4.538890133437916e+00 -1.432236876778751e+00 -6.000276446810375e+00 +2020-06-01T12:13:30.000000 -2.450440250266996e+03 -6.326541862653007e+03 -4.488493186276714e+02 4.570296353480393e+00 -1.351987424232828e+00 -5.994942125124392e+00 +2020-06-01T12:13:40.000000 -2.404582774392999e+03 -6.339659796680080e+03 -5.087688151103566e+02 4.601121071828771e+00 -1.271569318292151e+00 -5.988844606780730e+00 +2020-06-01T12:13:50.000000 -2.358419976824219e+03 -6.351972749878749e+03 -5.686235241936494e+02 4.631360395233476e+00 -1.190992844105264e+00 -5.981984730837283e+00 +2020-06-01T12:14:00.000000 -2.311957730660193e+03 -6.363479190105604e+03 -6.284058269359435e+02 4.661010506787920e+00 -1.110268305483570e+00 -5.974363435008541e+00 +2020-06-01T12:14:10.000000 -2.265201946784125e+03 -6.374177688342645e+03 -6.881081142648815e+02 4.690067666411962e+00 -1.029406023506467e+00 -5.965981755499882e+00 +2020-06-01T12:14:20.000000 -2.218158573092939e+03 -6.384066918862570e+03 -7.477227879624231e+02 4.718528211324515e+00 -9.484163351228108e-01 -5.956840826826858e+00 +2020-06-01T12:14:30.000000 -2.170833593720103e+03 -6.393145659381097e+03 -8.072422616467418e+02 4.746388556503875e+00 -8.673095917504952e-01 -5.946941881619847e+00 +2020-06-01T12:14:40.000000 -2.123233028257478e+03 -6.401412791194053e+03 -8.666589617521182e+02 4.773645195135376e+00 -7.860961578778364e-01 -5.936286250414164e+00 +2020-06-01T12:14:50.000000 -2.075362930970831e+03 -6.408867299301150e+03 -9.259653285062025e+02 4.800294699046890e+00 -7.047864096634603e-01 -5.924875361425705e+00 +2020-06-01T12:15:00.000000 -2.027229390010198e+03 -6.415508272515847e+03 -9.851538169070676e+02 4.826333719132898e+00 -6.233907335336787e-01 -5.912710740311715e+00 +2020-06-01T12:15:10.000000 -1.978838526618431e+03 -6.421334903560762e+03 -1.044216897694793e+03 4.851758985764806e+00 -5.419195247840590e-01 -5.899794009917763e+00 +2020-06-01T12:15:20.000000 -1.930196494334128e+03 -6.426346489149365e+03 -1.103147058322427e+03 4.876567309189773e+00 -4.603831861786230e-01 -5.886126890010057e+00 +2020-06-01T12:15:30.000000 -1.881309478189836e+03 -6.430542430053847e+03 -1.161936803924428e+03 4.900755579917199e+00 -3.787921265488929e-01 -5.871711196993457e+00 +2020-06-01T12:15:40.000000 -1.832183693909025e+03 -6.433922231158445e+03 -1.220578658280810e+03 4.924320769092012e+00 -2.971567593968729e-01 -5.856548843615700e+00 +2020-06-01T12:15:50.000000 -1.782825387097833e+03 -6.436485501499299e+03 -1.279065164778352e+03 4.947259928855574e+00 -2.154875014979390e-01 -5.840641838657667e+00 +2020-06-01T12:16:00.000000 -1.733240832431826e+03 -6.438231954290142e+03 -1.337388887371482e+03 4.969570192695116e+00 -1.337947715019289e-01 -5.823992286608915e+00 +2020-06-01T12:16:10.000000 -1.683436332842773e+03 -6.439161406934150e+03 -1.395542411534683e+03 4.991248775778557e+00 -5.208898854247161e-02 -5.806602387330888e+00 +2020-06-01T12:16:20.000000 -1.633418218699625e+03 -6.439273781021597e+03 -1.453518345216138e+03 5.012292975278505e+00 2.961942915715084e-02 -5.788474435705106e+00 +2020-06-01T12:16:30.000000 -1.583192846985696e+03 -6.438569102313908e+03 -1.511309319786845e+03 5.032700170683243e+00 1.113200656756498e-01 -5.769610821268031e+00 +2020-06-01T12:16:40.000000 -1.532766600475882e+03 -6.437047500713620e+03 -1.568907990983411e+03 5.052467824094109e+00 1.930025087785739e-01 -5.750014027833240e+00 +2020-06-01T12:16:50.000000 -1.482145886908914e+03 -6.434709210220707e+03 -1.626307039848587e+03 5.071593480510838e+00 2.746563513031618e-01 -5.729686633099937e+00 +2020-06-01T12:17:00.000000 -1.431337138154415e+03 -6.431554568874764e+03 -1.683499173671211e+03 5.090074768105321e+00 3.562711925445332e-01 -5.708631308246923e+00 +2020-06-01T12:17:10.000000 -1.380346809384631e+03 -6.427584018683521e+03 -1.740477126914684e+03 5.107909398480303e+00 4.378366396274843e-01 -5.686850817516128e+00 +2020-06-01T12:17:20.000000 -1.329181378233772e+03 -6.422798105537823e+03 -1.797233662149051e+03 5.125095166917849e+00 5.193423088882204e-01 -5.664348017780408e+00 +2020-06-01T12:17:30.000000 -1.277847343964590e+03 -6.417197479112528e+03 -1.853761570971991e+03 5.141629952613160e+00 6.007778272398430e-01 -5.641125858100964e+00 +2020-06-01T12:17:40.000000 -1.226351226624110e+03 -6.410782892753899e+03 -1.910053674931301e+03 5.157511718897418e+00 6.821328335449256e-01 -5.617187379269835e+00 +2020-06-01T12:17:50.000000 -1.174699566205339e+03 -6.403555203353247e+03 -1.966102826437188e+03 5.172738513446544e+00 7.633969799733563e-01 -5.592535713342045e+00 +2020-06-01T12:18:00.000000 -1.122898921799293e+03 -6.395515371207229e+03 -2.021901909674251e+03 5.187308468478284e+00 8.445599333649837e-01 -5.567174083153574e+00 +2020-06-01T12:18:10.000000 -1.070955870750921e+03 -6.386664459864171e+03 -2.077443841506069e+03 5.201219800936325e+00 9.256113765810717e-01 -5.541105801828119e+00 +2020-06-01T12:18:20.000000 -1.018877007810467e+03 -6.377003635957279e+03 -2.132721572375367e+03 5.214470812661774e+00 1.006541009853462e+00 -5.514334272271563e+00 +2020-06-01T12:18:30.000000 -9.666689442828463e+02 -6.366534169023972e+03 -2.187728087200363e+03 5.227059890552443e+00 1.087338552129703e+00 -5.486862986653842e+00 +2020-06-01T12:18:40.000000 -9.143383071780625e+02 -6.355257431312372e+03 -2.242456406262957e+03 5.238985506708944e+00 1.167993742408649e+00 -5.458695525880476e+00 +2020-06-01T12:18:50.000000 -8.618917383572719e+02 -6.343174897573840e+03 -2.296899586094870e+03 5.250246218569067e+00 1.248496341075123e+00 -5.429835559050870e+00 +2020-06-01T12:19:00.000000 -8.093358936792223e+02 -6.330288144842787e+03 -2.351050720355735e+03 5.260840669028995e+00 1.328836131225615e+00 -5.400286842906401e+00 +2020-06-01T12:19:10.000000 -7.566774421449828e+02 -6.316598852202342e+03 -2.404902940708462e+03 5.270767586553101e+00 1.409002919990288e+00 -5.370053221265692e+00 +2020-06-01T12:19:20.000000 -7.039230650424759e+02 -6.302108800538376e+03 -2.458449417684396e+03 5.280025785269832e+00 1.488986539844998e+00 -5.339138624451236e+00 +2020-06-01T12:19:30.000000 -6.510794550888243e+02 -6.286819872278682e+03 -2.511683361547904e+03 5.288614165057034e+00 1.568776849921411e+00 -5.307547068702139e+00 +2020-06-01T12:19:40.000000 -5.981533155722949e+02 -6.270734051120386e+03 -2.564598023151337e+03 5.296531711613653e+00 1.648363737307665e+00 -5.275282655578082e+00 +2020-06-01T12:19:50.000000 -5.451513594954096e+02 -6.253853421743742e+03 -2.617186694785301e+03 5.303777496520195e+00 1.727737118341493e+00 -5.242349571351947e+00 +2020-06-01T12:20:00.000000 -4.920803087137073e+02 -6.236180169513236e+03 -2.669442711023170e+03 5.310350677286273e+00 1.806886939899742e+00 -5.208752086391720e+00 +2020-06-01T12:20:10.000000 -4.389468930778543e+02 -6.217716580165877e+03 -2.721359449558357e+03 5.316250497386579e+00 1.885803180676801e+00 -5.174494554532679e+00 +2020-06-01T12:20:20.000000 -3.857578495734759e+02 -6.198465039486510e+03 -2.772930332036341e+03 5.321476286284831e+00 1.964475852458009e+00 -5.139581412438798e+00 +2020-06-01T12:20:30.000000 -3.325199214613395e+02 -6.178428032970739e+03 -2.824148824879333e+03 5.326027459445489e+00 2.042895001384368e+00 -5.104017178954761e+00 +2020-06-01T12:20:40.000000 -2.792398574173101e+02 -6.157608145474984e+03 -2.875008440105092e+03 5.329903518333879e+00 2.121050709210089e+00 -5.067806454447762e+00 +2020-06-01T12:20:50.000000 -2.259244106724789e+02 -6.136008060854359e+03 -2.925502736138311e+03 5.333104050404173e+00 2.198933094551581e+00 -5.030953920140231e+00 +2020-06-01T12:21:00.000000 -1.725803381536009e+02 -6.113630561587554e+03 -2.975625318616467e+03 5.335628729076237e+00 2.276532314128959e+00 -4.993464337432293e+00 +2020-06-01T12:21:10.000000 -1.192143996232351e+02 -6.090478528389714e+03 -3.025369841188049e+03 5.337477313700286e+00 2.353838563999290e+00 -4.955342547215499e+00 +2020-06-01T12:21:20.000000 -6.583335682138561e+01 -6.066554939813130e+03 -3.074730006303679e+03 5.338649649510247e+00 2.430842080780351e+00 -4.916593469177688e+00 +2020-06-01T12:21:30.000000 -1.244397260609353e+01 -6.041862871835390e+03 -3.123699566000936e+03 5.339145667565415e+00 2.507533142867628e+00 -4.877222101098327e+00 +2020-06-01T12:21:40.000000 4.094698990408315e+01 -6.016405497435353e+03 -3.172272322682152e+03 5.338965384681222e+00 2.583902071641521e+00 -4.837233518135124e+00 +2020-06-01T12:21:50.000000 9.433276818574498e+01 -5.990186086158301e+03 -3.220442129882847e+03 5.338108903348131e+00 2.659939232663426e+00 -4.796632872103828e+00 +2020-06-01T12:22:00.000000 1.477066011694171e+02 -5.963208003666860e+03 -3.268202893037103e+03 5.336576411639696e+00 2.735635036868834e+00 -4.755425390746203e+00 +2020-06-01T12:22:10.000000 2.010617300932659e+02 -5.935474711282151e+03 -3.315548570231777e+03 5.334368183109350e+00 2.810979941745430e+00 -4.713616376992979e+00 +2020-06-01T12:22:20.000000 2.543913993569500e+02 -5.906989765512578e+03 -3.362473172954501e+03 5.331484576676313e+00 2.885964452503737e+00 -4.671211208217765e+00 +2020-06-01T12:22:30.000000 3.076888573764372e+02 -5.877756817570124e+03 -3.408970766835455e+03 5.327926036500147e+00 2.960579123241073e+00 -4.628215335481994e+00 +2020-06-01T12:22:40.000000 3.609473574335740e+02 -5.847779612876139e+03 -3.455035472379866e+03 5.323693091845533e+00 3.034814558091526e+00 -4.584634282773642e+00 +2020-06-01T12:22:50.000000 4.141601585275888e+02 -5.817061990555660e+03 -3.500661465692835e+03 5.318786356934719e+00 3.108661412369314e+00 -4.540473646238583e+00 +2020-06-01T12:23:00.000000 4.673205262228855e+02 -5.785607882919383e+03 -3.545842979198088e+03 5.313206530791273e+00 3.182110393701437e+00 -4.495739093402992e+00 +2020-06-01T12:23:10.000000 5.204217334985948e+02 -5.753421314934481e+03 -3.590574302348359e+03 5.306954397071219e+00 3.255152263153246e+00 -4.450436362389098e+00 +2020-06-01T12:23:20.000000 5.734570615906555e+02 -5.720506403686146e+03 -3.634849782325449e+03 5.300030823885617e+00 3.327777836337559e+00 -4.404571261126395e+00 +2020-06-01T12:23:30.000000 6.264198008376665e+02 -5.686867357825196e+03 -3.678663824736359e+03 5.292436763611869e+00 3.399977984521099e+00 -4.358149666552082e+00 +2020-06-01T12:23:40.000000 6.793032515219809e+02 -5.652508477006586e+03 -3.722010894298627e+03 5.284173252694909e+00 3.471743635716749e+00 -4.311177523807578e+00 +2020-06-01T12:23:50.000000 7.321007247082608e+02 -5.617434151317128e+03 -3.764885515517709e+03 5.275241411438675e+00 3.543065775765271e+00 -4.263660845428590e+00 +2020-06-01T12:24:00.000000 7.848055430829457e+02 -5.581648860690777e+03 -3.807282273358132e+03 5.265642443786836e+00 3.613935449410613e+00 -4.215605710526740e+00 +2020-06-01T12:24:10.000000 8.374110417881511e+02 -5.545157174314796e+03 -3.849195813904695e+03 5.255377637094642e+00 3.684343761360885e+00 -4.167018263966961e+00 +2020-06-01T12:24:20.000000 8.899105692543334e+02 -5.507963750025303e+03 -3.890620845015327e+03 5.244448361890394e+00 3.754281877339959e+00 -4.117904715539069e+00 +2020-06-01T12:24:30.000000 9.422974880318073e+02 -5.470073333691032e+03 -3.931552136967239e+03 5.232856071627283e+00 3.823741025130622e+00 -4.068271339121627e+00 +2020-06-01T12:24:40.000000 9.945651756185515e+02 -5.431490758587974e+03 -3.971984523093162e+03 5.220602302425543e+00 3.892712495605396e+00 -4.018124471841963e+00 +2020-06-01T12:24:50.000000 1.046707025283857e+03 -5.392220944764180e+03 -4.011912900408939e+03 5.207688672806170e+00 3.961187643745170e+00 -3.967470513231001e+00 +2020-06-01T12:25:00.000000 1.098716446893407e+03 -5.352268898392112e+03 -4.051332230234878e+03 5.194116883413769e+00 4.029157889652322e+00 -3.916315924369978e+00 +2020-06-01T12:25:10.000000 1.150586867727684e+03 -5.311639711113287e+03 -4.090237538806116e+03 5.179888716731422e+00 4.096614719548255e+00 -3.864667227034963e+00 +2020-06-01T12:25:20.000000 1.202311733298401e+03 -5.270338559372866e+03 -4.128623917874635e+03 5.165006036785963e+00 4.163549686761302e+00 -3.812531002836231e+00 +2020-06-01T12:25:30.000000 1.253884508164333e+03 -5.228370703741702e+03 -4.166486525305270e+03 5.149470788844146e+00 4.229954412707367e+00 -3.759913892349148e+00 +2020-06-01T12:25:40.000000 1.305298676740172e+03 -5.185741488232529e+03 -4.203820585659221e+03 5.133284999100460e+00 4.295820587854264e+00 -3.706822594245467e+00 +2020-06-01T12:25:50.000000 1.356547744104595e+03 -5.142456339604205e+03 -4.240621390771165e+03 5.116450774355898e+00 4.361139972678682e+00 -3.653263864416959e+00 +2020-06-01T12:26:00.000000 1.407625236806060e+03 -5.098520766656085e+03 -4.276884300317698e+03 5.098970301687324e+00 4.425904398613230e+00 -3.599244515094341e+00 +2020-06-01T12:26:10.000000 1.458524703662182e+03 -5.053940359514463e+03 -4.312604742376001e+03 5.080845848110181e+00 4.490105768979240e+00 -3.544771413963313e+00 +2020-06-01T12:26:20.000000 1.509239716558183e+03 -5.008720788909287e+03 -4.347778213973513e+03 5.062079760230623e+00 4.553736059911097e+00 -3.489851483276643e+00 +2020-06-01T12:26:30.000000 1.559763871241687e+03 -4.962867805440414e+03 -4.382400281630159e+03 5.042674463890443e+00 4.616787321270107e+00 -3.434491698960075e+00 +2020-06-01T12:26:40.000000 1.610090788111480e+03 -4.916387238837116e+03 -4.416466581889781e+03 5.022632463804664e+00 4.679251677544262e+00 -3.378699089717137e+00 +2020-06-01T12:26:50.000000 1.660214113005993e+03 -4.869284997206772e+03 -4.449972821843565e+03 5.001956343188652e+00 4.741121328740915e+00 -3.322480736128776e+00 +2020-06-01T12:27:00.000000 1.710127517985607e+03 -4.821567066274992e+03 -4.482914779644922e+03 4.980648763379894e+00 4.802388551266189e+00 -3.265843769748530e+00 +2020-06-01T12:27:10.000000 1.759824702112526e+03 -4.773239508617728e+03 -4.515288305014019e+03 4.958712463449515e+00 4.863045698794450e+00 -3.208795372196367e+00 +2020-06-01T12:27:20.000000 1.809299392225947e+03 -4.724308462883674e+03 -4.547089319734645e+03 4.936150259807912e+00 4.923085203126478e+00 -3.151342774246789e+00 +2020-06-01T12:27:30.000000 1.858545343713516e+03 -4.674780143009373e+03 -4.578313818140549e+03 4.912965045801313e+00 4.982499575036563e+00 -3.093493254916033e+00 +2020-06-01T12:27:40.000000 1.907556341277675e+03 -4.624660837425487e+03 -4.608957867593494e+03 4.889159791302613e+00 5.041281405107831e+00 -3.035254140544785e+00 +2020-06-01T12:27:50.000000 1.956326199700221e+03 -4.573956908254130e+03 -4.639017608951862e+03 4.864737542292031e+00 5.099423364559028e+00 -2.976632803878273e+00 +2020-06-01T12:28:00.000000 2.004848764601063e+03 -4.522674790497835e+03 -4.668489257030777e+03 4.839701420432711e+00 5.156918206058862e+00 -2.917636663142094e+00 +2020-06-01T12:28:10.000000 2.053117913192573e+03 -4.470820991222042e+03 -4.697369101051407e+03 4.814054622637792e+00 5.213758764528979e+00 -2.858273181118237e+00 +2020-06-01T12:28:20.000000 2.101127555029333e+03 -4.418402088728438e+03 -4.725653505082347e+03 4.787800420631839e+00 5.269937957936065e+00 -2.798549864215934e+00 +2020-06-01T12:28:30.000000 2.148871632755291e+03 -4.365424731720148e+03 -4.753338908471136e+03 4.760942160502593e+00 5.325448788074893e+00 -2.738474261541004e+00 +2020-06-01T12:28:40.000000 2.196344122844250e+03 -4.311895638459800e+03 -4.780421826266761e+03 4.733483262248523e+00 5.380284341338252e+00 -2.678053963962122e+00 +2020-06-01T12:28:50.000000 2.243539036337281e+03 -4.257821595919738e+03 -4.806898849632375e+03 4.705427219317294e+00 5.434437789477592e+00 -2.617296603175658e+00 +2020-06-01T12:29:00.000000 2.290450419574031e+03 -4.203209458924883e+03 -4.832766646249066e+03 4.676777598139714e+00 5.487902390351224e+00 -2.556209850767314e+00 +2020-06-01T12:29:10.000000 2.337072354921845e+03 -4.148066149286981e+03 -4.858021960710223e+03 4.647538037653790e+00 5.540671488664578e+00 -2.494801417271688e+00 +2020-06-01T12:29:20.000000 2.383398961496239e+03 -4.092398654934350e+03 -4.882661614905711e+03 4.617712248826193e+00 5.592738516695261e+00 -2.433079051231492e+00 +2020-06-01T12:29:30.000000 2.429424395880704e+03 -4.036214029030806e+03 -4.906682508397865e+03 4.587304014163357e+00 5.644096995012107e+00 -2.371050538251952e+00 +2020-06-01T12:29:40.000000 2.475142852838769e+03 -3.979519389089874e+03 -4.930081618787201e+03 4.556317187218816e+00 5.694740533180253e+00 -2.308723700055131e+00 +2020-06-01T12:29:50.000000 2.520548566021853e+03 -3.922321916081741e+03 -4.952856002068661e+03 4.524755692093183e+00 5.744662830456411e+00 -2.246106393532619e+00 +2020-06-01T12:30:00.000000 2.565635808673565e+03 -3.864628853531392e+03 -4.975002792979055e+03 4.492623522926750e+00 5.793857676475082e+00 -2.183206509794570e+00 +2020-06-01T12:30:10.000000 2.610398894329228e+03 -3.806447506608635e+03 -4.996519205335681e+03 4.459924743386420e+00 5.842318951924307e+00 -2.120031973214747e+00 +2020-06-01T12:30:20.000000 2.654832177493894e+03 -3.747785241234337e+03 -5.017402532356650e+03 4.426663486157091e+00 5.890040629193404e+00 -2.056590740500370e+00 +2020-06-01T12:30:30.000000 2.698930054355514e+03 -3.688649483125716e+03 -5.037650146992210e+03 4.392843952399327e+00 5.937016773053473e+00 -1.992890799699577e+00 +2020-06-01T12:30:40.000000 2.742686963448998e+03 -3.629047716893816e+03 -5.057259502224199e+03 4.358470411230981e+00 5.983241541280148e+00 -1.928940169273100e+00 +2020-06-01T12:30:50.000000 2.786097386336883e+03 -3.568987485104791e+03 -5.076228131367592e+03 4.323547199188835e+00 6.028709185289437e+00 -1.864746897130526e+00 +2020-06-01T12:31:00.000000 2.829155848282239e+03 -3.508476387338805e+03 -5.094553648360661e+03 4.288078719684948e+00 6.073414050760542e+00 -1.800319059670456e+00 +2020-06-01T12:31:10.000000 2.871856918913840e+03 -3.447522079245376e+03 -5.112233748044698e+03 4.252069442459368e+00 6.117350578245780e+00 -1.735664760821898e+00 +2020-06-01T12:31:20.000000 2.914195212887284e+03 -3.386132271591046e+03 -5.129266206434655e+03 4.215523903025733e+00 6.160513303771635e+00 -1.670792131083028e+00 +2020-06-01T12:31:30.000000 2.956165390538978e+03 -3.324314729302791e+03 -5.145648880979793e+03 4.178446702113162e+00 6.202896859427674e+00 -1.605709326560021e+00 +2020-06-01T12:31:40.000000 2.997762158536818e+03 -3.262077270502755e+03 -5.161379710815323e+03 4.140842505099911e+00 6.244495973947593e+00 -1.540424528002525e+00 +2020-06-01T12:31:50.000000 3.038980270522878e+03 -3.199427765540300e+03 -5.176456717003701e+03 4.102716041443957e+00 6.285305473277493e+00 -1.474945939840870e+00 +2020-06-01T12:32:00.000000 3.079814527751713e+03 -3.136374136016695e+03 -5.190878002766612e+03 4.064072104106535e+00 6.325320281135294e+00 -1.409281789220997e+00 +2020-06-01T12:32:10.000000 3.120259779722990e+03 -3.072924353803652e+03 -5.204641753707609e+03 4.024915548970798e+00 6.364535419559790e+00 -1.343440325037136e+00 +2020-06-01T12:32:20.000000 3.160310924806241e+03 -3.009086440060050e+03 -5.217746238024099e+03 3.985251294256466e+00 6.402946009447660e+00 -1.277429816968031e+00 +2020-06-01T12:32:30.000000 3.199962910863281e+03 -2.944868464237436e+03 -5.230189806711061e+03 3.945084319926108e+00 6.440547271083592e+00 -1.211258554505884e+00 +2020-06-01T12:32:40.000000 3.239210735861391e+03 -2.880278543085729e+03 -5.241970893753821e+03 3.904419667089026e+00 6.477334524657111e+00 -1.144934845990488e+00 +2020-06-01T12:32:50.000000 3.278049448482098e+03 -2.815324839651090e+03 -5.253088016311796e+03 3.863262437398685e+00 6.513303190770744e+00 -1.078467017639540e+00 +2020-06-01T12:33:00.000000 3.316474148723855e+03 -2.750015562268630e+03 -5.263539774892608e+03 3.821617792445485e+00 6.548448790937734e+00 -1.011863412577687e+00 +2020-06-01T12:33:10.000000 3.354479988497821e+03 -2.684358963552206e+03 -5.273324853515875e+03 3.779490953143732e+00 6.582766948069497e+00 -9.451323898684806e-01 +2020-06-01T12:33:20.000000 3.392062172218644e+03 -2.618363339376614e+03 -5.282442019868128e+03 3.736887199114216e+00 6.616253386953280e+00 -8.782823235409902e-01 +2020-06-01T12:33:30.000000 3.429215957388172e+03 -2.552037027857226e+03 -5.290890125447347e+03 3.693811868061017e+00 6.648903934719629e+00 -8.113216016197088e-01 +2020-06-01T12:33:40.000000 3.465936655173013e+03 -2.485388408324153e+03 -5.298668105698152e+03 3.650270355144876e+00 6.680714521299235e+00 -7.442586251514046e-01 +2020-06-01T12:33:50.000000 3.502219630976163e+03 -2.418425900292088e+03 -5.305774980137019e+03 3.606268112349137e+00 6.711681179870808e+00 -6.771018072336852e-01 +2020-06-01T12:34:00.000000 3.538060305001811e+03 -2.351157962425601e+03 -5.312209852468048e+03 3.561810647843728e+00 6.741800047297629e+00 -6.098595720405952e-01 +2020-06-01T12:34:10.000000 3.573454152818083e+03 -2.283593091492748e+03 -5.317971910689282e+03 3.516903525336256e+00 6.771067364558619e+00 -5.425403538428382e-01 +2020-06-01T12:34:20.000000 3.608396705895183e+03 -2.215739821348433e+03 -5.323060427186752e+03 3.471552363443431e+00 6.799479477153544e+00 -4.751525960593698e-01 +2020-06-01T12:34:30.000000 3.642883552170267e+03 -2.147606721850727e+03 -5.327474758823971e+03 3.425762835014264e+00 6.827032835526323e+00 -4.077047502471476e-01 +2020-06-01T12:34:40.000000 3.676910336575145e+03 -2.079202397831122e+03 -5.331214347016646e+03 3.379540666487602e+00 6.853723995452616e+00 -3.402052751477903e-01 +2020-06-01T12:34:50.000000 3.710472761571034e+03 -2.010535488035287e+03 -5.334278717799991e+03 3.332891637227238e+00 6.879549618429099e+00 -2.726626357088885e-01 +2020-06-01T12:35:00.000000 3.743566587674656e+03 -1.941614664063416e+03 -5.336667481885920e+03 3.285821578856128e+00 6.904506472050418e+00 -2.050853021071470e-01 +2020-06-01T12:35:10.000000 3.776187633977769e+03 -1.872448629306994e+03 -5.338380334710458e+03 3.238336374583430e+00 6.928591430377297e+00 -1.374817487734157e-01 +2020-06-01T12:35:20.000000 3.808331778659048e+03 -1.803046117883464e+03 -5.339417056471346e+03 3.190441958530487e+00 6.951801474292806e+00 -6.986045341684349e-02 +2020-06-01T12:35:30.000000 3.839994959490860e+03 -1.733415893564056e+03 -5.339777512155972e+03 3.142144315047647e+00 6.974133691850951e+00 -2.229896047311998e-03 +2020-06-01T12:35:40.000000 3.871173174336515e+03 -1.663566748703876e+03 -5.339461651559452e+03 3.093449478032443e+00 6.995585278612254e+00 6.540144199856131e-02 +2020-06-01T12:35:50.000000 3.901862481643931e+03 -1.593507503161544e+03 -5.338469509292995e+03 3.044363530235809e+00 7.016153537972855e+00 1.330250790364925e-01 +2020-06-01T12:36:00.000000 3.932059000929745e+03 -1.523247003221356e+03 -5.336801204782496e+03 2.994892602569557e+00 7.035835881480806e+00 2.006325340098869e-01 +2020-06-01T12:36:10.000000 3.961758913257665e+03 -1.452794120509684e+03 -5.334456942257353e+03 2.945042873406739e+00 7.054629829143509e+00 2.682153274673067e-01 +2020-06-01T12:36:20.000000 3.990958461709231e+03 -1.382157750909615e+03 -5.331437010729547e+03 2.894820567877990e+00 7.072533009724917e+00 3.357649825385899e-01 +2020-06-01T12:36:30.000000 4.019653951848504e+03 -1.311346813470394e+03 -5.327741783962856e+03 2.844231957162072e+00 7.089543161033031e+00 4.032730259136050e-01 +2020-06-01T12:36:40.000000 4.047841752178593e+03 -1.240370249316457e+03 -5.323371720432432e+03 2.793283357772983e+00 7.105658130196976e+00 4.707309888182785e-01 +2020-06-01T12:36:50.000000 4.075518294590979e+03 -1.169237020553951e+03 -5.318327363274563e+03 2.741981130843564e+00 7.120875873933519e+00 5.381304079912893e-01 +2020-06-01T12:37:00.000000 4.102680074809024e+03 -1.097956109171241e+03 -5.312609340226524e+03 2.690331681401214e+00 7.135194458804668e+00 6.054628266632147e-01 +2020-06-01T12:37:10.000000 4.129323652822723e+03 -1.026536515940499e+03 -5.306218363556829e+03 2.638341457642081e+00 7.148612061464060e+00 6.727197955326343e-01 +2020-06-01T12:37:20.000000 4.155445653316170e+03 -9.549872593170402e+02 -5.299155229985837e+03 2.586016950200322e+00 7.161126968893419e+00 7.398928737415897e-01 +2020-06-01T12:37:30.000000 4.181042766089465e+03 -8.833173743318979e+02 -5.291420820596049e+03 2.533364691410958e+00 7.172737578629088e+00 8.069736298564478e-01 +2020-06-01T12:37:40.000000 4.206111746471159e+03 -8.115359114870298e+02 -5.283016100733291e+03 2.480391254569417e+00 7.183442398978305e+00 8.739536428419381e-01 +2020-06-01T12:37:50.000000 4.230649415724053e+03 -7.396519356471258e+02 -5.273942119897773e+03 2.427103253188015e+00 7.193240049224515e+00 9.408245030385122e-01 +2020-06-01T12:38:00.000000 4.254652661444238e+03 -6.676745249268693e+02 -5.264200011625278e+03 2.373507340244447e+00 7.202129259823341e+00 1.007577813140461e+00 +2020-06-01T12:38:10.000000 4.278118437951267e+03 -5.956127695804021e+02 -5.253790993358984e+03 2.319610207429525e+00 7.210108872587330e+00 1.074205189170607e+00 +2020-06-01T12:38:20.000000 4.301043766671944e+03 -5.234757708851738e+02 -5.242716366311122e+03 2.265418584386958e+00 7.217177840861041e+00 1.140698261457783e+00 +2020-06-01T12:38:30.000000 4.323425736515868e+03 -4.512726400268114e+02 -5.230977515315016e+03 2.210939237952029e+00 7.223335229684674e+00 1.207048675612844e+00 +2020-06-01T12:38:40.000000 4.345261504243280e+03 -3.790124969813432e+02 -5.218575908667599e+03 2.156178971382817e+00 7.228580215948397e+00 1.273248093503172e+00 +2020-06-01T12:38:50.000000 4.366548294825876e+03 -3.067044693949991e+02 -5.205513097961632e+03 2.101144623588458e+00 7.232912088535067e+00 1.339288194229960e+00 +2020-06-01T12:39:00.000000 4.387283401798926e+03 -2.343576914651039e+02 -5.191790717908803e+03 2.045843068353467e+00 7.236330248452770e+00 1.405160675101459e+00 +2020-06-01T12:39:10.000000 4.407464187606311e+03 -1.619813028171123e+02 -5.177410486152644e+03 1.990281213555704e+00 7.238834208956863e+00 1.470857252608252e+00 +2020-06-01T12:39:20.000000 4.427088083937590e+03 -8.958444738199647e+01 -5.162374203071793e+03 1.934466000381997e+00 7.240423595660682e+00 1.536369663397638e+00 +2020-06-01T12:39:30.000000 4.446152592056867e+03 -1.717627227209551e+01 -5.146683751573708e+03 1.878404402537500e+00 7.241098146636212e+00 1.601689665246428e+00 +2020-06-01T12:39:40.000000 4.464655283124435e+03 5.523407334435697e+01 -5.130341096878179e+03 1.822103425452152e+00 7.240857712502941e+00 1.666809038035388e+00 +2020-06-01T12:39:50.000000 4.482593798509322e+03 1.276374393678956e+02 -5.113348286291986e+03 1.765570105481851e+00 7.239702256506901e+00 1.731719584719360e+00 +2020-06-01T12:40:00.000000 4.499965850095249e+03 2.000246758961993e+02 -5.095707448972627e+03 1.708811509105037e+00 7.237631854587565e+00 1.796413132301602e+00 +2020-06-01T12:40:10.000000 4.516769220577108e+03 2.723866343495853e+02 -5.077420795683622e+03 1.651834732115790e+00 7.234646695434669e+00 1.860881532802341e+00 +2020-06-01T12:40:20.000000 4.533001763750454e+03 3.447141685989592e+02 -5.058490618539168e+03 1.594646898812931e+00 7.230747080532907e+00 1.925116664230308e+00 +2020-06-01T12:40:30.000000 4.548661404792594e+03 4.169981360964562e+02 -5.038919290739090e+03 1.537255161180735e+00 7.225933424196388e+00 1.989110431553087e+00 +2020-06-01T12:40:40.000000 4.563746140534819e+03 4.892293990001389e+02 -5.018709266295080e+03 1.479666698072381e+00 7.220206253590961e+00 2.052854767663344e+00 +2020-06-01T12:40:50.000000 4.578254039727188e+03 5.613988253049521e+02 -4.997863079746242e+03 1.421888714382533e+00 7.213566208745810e+00 2.116341634347616e+00 +2020-06-01T12:41:00.000000 4.592183243294920e+03 6.334972899702043e+02 -4.976383345865313e+03 1.363928440220149e+00 7.206014042552876e+00 2.179563023252826e+00 +2020-06-01T12:41:10.000000 4.605531964586206e+03 7.055156760499500e+02 -4.954272759355091e+03 1.305793130072567e+00 7.197550620755560e+00 2.242510956851978e+00 +2020-06-01T12:41:20.000000 4.618298489611588e+03 7.774448758182212e+02 -4.931534094535989e+03 1.247490061971400e+00 7.188176921925257e+00 2.305177489406633e+00 +2020-06-01T12:41:30.000000 4.630481177275580e+03 8.492757919003620e+02 -4.908170205022504e+03 1.189026536647728e+00 7.177894037426183e+00 2.367554707932221e+00 +2020-06-01T12:41:40.000000 4.642078459599242e+03 9.209993383994961e+02 -4.884184023391143e+03 1.130409876687253e+00 7.166703171368581e+00 2.429634733158777e+00 +2020-06-01T12:41:50.000000 4.653088841934209e+03 9.926064420232506e+02 -4.859578560838827e+03 1.071647425680166e+00 7.154605640550280e+00 2.491409720490370e+00 +2020-06-01T12:42:00.000000 4.663510903168702e+03 1.064088043209156e+03 -4.834356906831363e+03 1.012746547368618e+00 7.141602874385854e+00 2.552871860963853e+00 +2020-06-01T12:42:10.000000 4.673343295924818e+03 1.135435097254582e+03 -4.808522228741345e+03 9.537146247838678e-01 7.127696414823678e+00 2.614013382208404e+00 +2020-06-01T12:42:20.000000 4.682584746746801e+03 1.206638575436652e+03 -4.782077771478826e+03 8.945590593887575e-01 7.112887916251582e+00 2.674826549397749e+00 +2020-06-01T12:42:30.000000 4.691234056281346e+03 1.277689466139757e+03 -4.755026857109887e+03 8.352872702079359e-01 7.097179145389186e+00 2.735303666206728e+00 +2020-06-01T12:42:40.000000 4.699290099448417e+03 1.348578775975757e+03 -4.727372884467968e+03 7.759066929592864e-01 7.080571981169193e+00 2.795437075760757e+00 +2020-06-01T12:42:50.000000 4.706751825604275e+03 1.419297530907445e+03 -4.699119328753553e+03 7.164247791783590e-01 7.063068414604874e+00 2.855219161588063e+00 +2020-06-01T12:43:00.000000 4.713618258694881e+03 1.489836777366792e+03 -4.670269741126303e+03 6.568489953413115e-01 7.044670548646428e+00 2.914642348565686e+00 +2020-06-01T12:43:10.000000 4.719888497401143e+03 1.560187583375412e+03 -4.640827748285995e+03 5.971868219813555e-01 7.025380598023481e+00 2.973699103867012e+00 +2020-06-01T12:43:20.000000 4.725561715274761e+03 1.630341039660001e+03 -4.610797052045597e+03 5.374457528038619e-01 7.005200889076160e+00 3.032381937903713e+00 +2020-06-01T12:43:30.000000 4.730637160865780e+03 1.700288260768301e+03 -4.580181428893605e+03 4.776332937963995e-01 6.984133859572266e+00 3.090683405268063e+00 +2020-06-01T12:43:40.000000 4.735114157840768e+03 1.770020386182119e+03 -4.548984729547810e+03 4.177569623353706e-01 6.962182058512367e+00 3.148596105671055e+00 +2020-06-01T12:43:50.000000 4.738992105092294e+03 1.839528581428386e+03 -4.517210878499436e+03 3.578242862893433e-01 6.939348145921702e+00 3.206112684878512e+00 +2020-06-01T12:44:00.000000 4.742270476839195e+03 1.908804039189256e+03 -4.484863873547670e+03 2.978428031173789e-01 6.915634892628834e+00 3.263225835644919e+00 +2020-06-01T12:44:10.000000 4.744948822718161e+03 1.977837980407800e+03 -4.451947785325317e+03 2.378200589663412e-01 6.891045180031547e+00 3.319928298643569e+00 +2020-06-01T12:44:20.000000 4.747026767865959e+03 2.046621655392862e+03 -4.418466756815011e+03 1.777636077627861e-01 6.865581999849397e+00 3.376212863394191e+00 +2020-06-01T12:44:30.000000 4.748504012992738e+03 2.115146344921311e+03 -4.384425002855828e+03 1.176810103018411e-01 6.839248453862741e+00 3.432072369187924e+00 +2020-06-01T12:44:40.000000 4.749380334445949e+03 2.183403361337309e+03 -4.349826809641099e+03 5.757983333276078e-02 6.812047753638709e+00 3.487499706008394e+00 +2020-06-01T12:44:50.000000 4.749655584265605e+03 2.251384049647415e+03 -4.314676534207263e+03 -2.532351357018769e-03 6.783983220243856e+00 3.542487815449156e+00 +2020-06-01T12:45:00.000000 4.749329690229858e+03 2.319079788615893e+03 -4.278978603912456e+03 -6.264796786431921e-02 6.755058283942463e+00 3.597029691629365e+00 +2020-06-01T12:45:10.000000 4.748402655891644e+03 2.386481991854504e+03 -4.242737515906824e+03 -1.227594370932395e-01 6.725276483881869e+00 3.651118382104427e+00 +2020-06-01T12:45:20.000000 4.746874560605763e+03 2.453582108908530e+03 -4.205957836594564e+03 -1.828591776810583e-01 6.694641467764680e+00 3.704746988771824e+00 +2020-06-01T12:45:30.000000 4.744745559547412e+03 2.520371626342494e+03 -4.168644201084265e+03 -2.429396069288815e-01 6.663156991505184e+00 3.757908668776859e+00 +2020-06-01T12:45:40.000000 4.742015883720225e+03 2.586842068818930e+03 -4.130801312633605e+03 -3.029931417333821e-01 6.630826918874524e+00 3.810596635409507e+00 +2020-06-01T12:45:50.000000 4.738685839956201e+03 2.652985000175909e+03 -4.092433942082270e+03 -3.630121995220286e-01 6.597655221129711e+00 3.862804159001100e+00 +2020-06-01T12:46:00.000000 4.734755810905543e+03 2.718792024499247e+03 -4.053546927277731e+03 -4.229891991903829e-01 6.563645976630074e+00 3.914524567814169e+00 +2020-06-01T12:46:10.000000 4.730226255016967e+03 2.784254787193449e+03 -4.014145172490596e+03 -4.829165620442866e-01 6.528803370438292e+00 3.965751248929868e+00 +2020-06-01T12:46:20.000000 4.725097706509742e+03 2.849364976042958e+03 -3.974233647823465e+03 -5.427867127385839e-01 6.493131693909453e+00 4.016477649127694e+00 +2020-06-01T12:46:30.000000 4.719370775334389e+03 2.914114322277598e+03 -3.933817388607651e+03 -6.025920802270915e-01 6.456635344262716e+00 4.066697275764735e+00 +2020-06-01T12:46:40.000000 4.713046147125870e+03 2.978494601625036e+03 -3.892901494794713e+03 -6.623250987056136e-01 6.419318824142098e+00 4.116403697645408e+00 +2020-06-01T12:46:50.000000 4.706124583146071e+03 3.042497635364514e+03 -3.851491130336819e+03 -7.219782085630897e-01 6.381186741160617e+00 4.165590545889225e+00 +2020-06-01T12:47:00.000000 4.698606920217237e+03 3.106115291373885e+03 -3.809591522559372e+03 -7.815438573325398e-01 6.342243807430827e+00 4.214251514792180e+00 +2020-06-01T12:47:10.000000 4.690494070645434e+03 3.169339485173317e+03 -3.767207961524383e+03 -8.410145006454463e-01 6.302494839080283e+00 4.262380362683365e+00 +2020-06-01T12:47:20.000000 4.681787022135466e+03 3.232162180960403e+03 -3.724345799387153e+03 -9.003826031840090e-01 6.261944755754346e+00 4.309970912773948e+00 +2020-06-01T12:47:30.000000 4.672486837695154e+03 3.294575392644621e+03 -3.681010449742227e+03 -9.596406396395789e-01 6.220598580102237e+00 4.357017054002931e+00 +2020-06-01T12:47:40.000000 4.662594655530506e+03 3.356571184875254e+03 -3.637207386961352e+03 -1.018781095671111e+00 6.178461437248877e+00 4.403512741876395e+00 +2020-06-01T12:47:50.000000 4.652111688931504e+03 3.418141674060414e+03 -3.592942145526029e+03 -1.077796468861817e+00 6.135538554254966e+00 4.449451999297509e+00 +2020-06-01T12:48:00.000000 4.641039226147772e+03 3.479279029387778e+03 -3.548220319345950e+03 -1.136679269683532e+00 6.091835259556862e+00 4.494828917396393e+00 +2020-06-01T12:48:10.000000 4.629378630255260e+03 3.539975473831850e+03 -3.503047561075497e+03 -1.195422022455124e+00 6.047356982397739e+00 4.539637656346906e+00 +2020-06-01T12:48:20.000000 4.617131339012860e+03 3.600223285160874e+03 -3.457429581417761e+03 -1.254017266306759e+00 6.002109252239921e+00 4.583872446181290e+00 +2020-06-01T12:48:30.000000 4.604298864709170e+03 3.660014796936091e+03 -3.411372148422116e+03 -1.312457556143655e+00 5.956097698163901e+00 4.627527587596225e+00 +2020-06-01T12:48:40.000000 4.590882794000480e+03 3.719342399504206e+03 -3.364881086773547e+03 -1.370735463609387e+00 5.909328048252698e+00 4.670597452752132e+00 +2020-06-01T12:48:50.000000 4.576884787738680e+03 3.778198540983181e+03 -3.317962277075084e+03 -1.428843578049624e+00 5.861806128962242e+00 4.713076486064314e+00 +2020-06-01T12:49:00.000000 4.562306580788476e+03 3.836575728246014e+03 -3.270621655118958e+03 -1.486774507480503e+00 5.813537864473497e+00 4.754959204989835e+00 +2020-06-01T12:49:10.000000 4.547149981837107e+03 3.894466527891066e+03 -3.222865211154619e+03 -1.544520879550165e+00 5.764529276034786e+00 4.796240200802560e+00 +2020-06-01T12:49:20.000000 4.531416873192627e+03 3.951863567211076e+03 -3.174698989145655e+03 -1.602075342505886e+00 5.714786481285650e+00 4.836914139363561e+00 +2020-06-01T12:49:30.000000 4.515109210573196e+03 4.008759535153561e+03 -3.126129086019945e+03 -1.659430566159246e+00 5.664315693566897e+00 4.876975761882745e+00 +2020-06-01T12:49:40.000000 4.498229022886419e+03 4.065147183274159e+03 -3.077161650912879e+03 -1.716579242851088e+00 5.613123221216316e+00 4.916419885671860e+00 +2020-06-01T12:49:50.000000 4.480778412000069e+03 4.121019326682480e+03 -3.027802884402108e+03 -1.773514088415284e+00 5.561215466849181e+00 4.955241404890014e+00 +2020-06-01T12:50:00.000000 4.462759552501184e+03 4.176368844982093e+03 -2.978059037735847e+03 -1.830227843144018e+00 5.508598926623995e+00 4.993435291279961e+00 +2020-06-01T12:50:10.000000 4.444174691447982e+03 4.231188683201330e+03 -2.927936412052768e+03 -1.886713272749728e+00 5.455280189493515e+00 5.030996594896551e+00 +2020-06-01T12:50:20.000000 4.425026148109759e+03 4.285471852717992e+03 -2.877441357596022e+03 -1.942963169328371e+00 5.401265936441289e+00 5.067920444825337e+00 +2020-06-01T12:50:30.000000 4.405316313699328e+03 4.339211432174865e+03 -2.826580272919122e+03 -1.998970352319463e+00 5.346562939703539e+00 5.104202049892892e+00 +2020-06-01T12:50:40.000000 4.385047651093628e+03 4.392400568389940e+03 -2.775359604084359e+03 -2.054727669467936e+00 5.291178061974995e+00 5.139836699368185e+00 +2020-06-01T12:50:50.000000 4.364222694547396e+03 4.445032477254881e+03 -2.723785843855701e+03 -2.110227997780294e+00 5.235118255602640e+00 5.174819763653542e+00 +2020-06-01T12:51:00.000000 4.342844049394340e+03 4.497100444629113e+03 -2.671865530883762e+03 -2.165464244483475e+00 5.178390561762556e+00 5.209146694966769e+00 +2020-06-01T12:51:10.000000 4.320914391741352e+03 4.548597827222730e+03 -2.619605248884290e+03 -2.220429347978481e+00 5.121002109623732e+00 5.242813028013349e+00 +2020-06-01T12:51:20.000000 4.298436468151224e+03 4.599518053472834e+03 -2.567011625810240e+03 -2.275116278794293e+00 5.062960115496749e+00 5.275814380648500e+00 +2020-06-01T12:51:30.000000 4.275413095317319e+03 4.649854624410624e+03 -2.514091333015817e+03 -2.329518040538689e+00 5.004271881967343e+00 5.308146454530014e+00 +2020-06-01T12:51:40.000000 4.251847159727684e+03 4.699601114518788e+03 -2.460851084417633e+03 -2.383627670845895e+00 4.944944797018473e+00 5.339805035758624e+00 +2020-06-01T12:51:50.000000 4.227741617320248e+03 4.748751172582344e+03 -2.407297635644459e+03 -2.437438242323936e+00 4.884986333133776e+00 5.370785995510906e+00 +2020-06-01T12:52:00.000000 4.203099493128081e+03 4.797298522528466e+03 -2.353437783184956e+03 -2.490942863497316e+00 4.824404046390482e+00 5.401085290659020e+00 +2020-06-01T12:52:10.000000 4.177923880916170e+03 4.845236964257492e+03 -2.299278363527292e+03 -2.544134679746980e+00 4.763205575536814e+00 5.430698964380705e+00 +2020-06-01T12:52:20.000000 4.152217942808016e+03 4.892560374465100e+03 -2.244826252293223e+03 -2.597006874247723e+00 4.701398641055573e+00 5.459623146758072e+00 +2020-06-01T12:52:30.000000 4.125984908902410e+03 4.939262707455676e+03 -2.190088363365571e+03 -2.649552668903026e+00 4.638991044212863e+00 5.487854055365482e+00 +2020-06-01T12:52:40.000000 4.099228076882674e+03 4.985337995943898e+03 -2.135071648011405e+03 -2.701765325273870e+00 4.575990666095326e+00 5.515387995845312e+00 +2020-06-01T12:52:50.000000 4.071950811614687e+03 5.030780351849163e+03 -2.079783093997713e+03 -2.753638145506719e+00 4.512405466631075e+00 5.542221362473114e+00 +2020-06-01T12:53:00.000000 4.044156544736724e+03 5.075583967078694e+03 -2.024229724702456e+03 -2.805164473256038e+00 4.448243483598405e+00 5.568350638710597e+00 +2020-06-01T12:53:10.000000 4.015848774239390e+03 5.119743114301281e+03 -1.968418598220305e+03 -2.856337694603281e+00 4.383512831620801e+00 5.593772397746635e+00 +2020-06-01T12:53:20.000000 3.987031064038018e+03 5.163252147709741e+03 -1.912356806462589e+03 -2.907151238970464e+00 4.318221701149133e+00 5.618483303026508e+00 +2020-06-01T12:53:30.000000 3.957707043533305e+03 5.206105503775080e+03 -1.856051474252465e+03 -2.957598580031262e+00 4.252378357429508e+00 5.642480108768761e+00 +2020-06-01T12:53:40.000000 3.927880407165114e+03 5.248297701988919e+03 -1.799509758413825e+03 -3.007673236616091e+00 4.185991139458311e+00 5.665759660470185e+00 +2020-06-01T12:53:50.000000 3.897554913956234e+03 5.289823345595588e+03 -1.742738846856784e+03 -3.057368773612418e+00 4.119068458925048e+00 5.688318895397622e+00 +2020-06-01T12:54:00.000000 3.866734387047597e+03 5.330677122313545e+03 -1.685745957657766e+03 -3.106678802859946e+00 4.051618799142188e+00 5.710154843067405e+00 +2020-06-01T12:54:10.000000 3.835422713224233e+03 5.370853805046334e+03 -1.628538338134547e+03 -3.155596984040945e+00 3.983650713961886e+00 5.731264625712119e+00 +2020-06-01T12:54:20.000000 3.803623842432061e+03 5.410348252583090e+03 -1.571123263915846e+03 -3.204117025565673e+00 3.915172826679259e+00 5.751645458734681e+00 +2020-06-01T12:54:30.000000 3.771341787286846e+03 5.449155410286457e+03 -1.513508038009480e+03 -3.252232685450080e+00 3.846193828926568e+00 5.771294651148224e+00 +2020-06-01T12:54:40.000000 3.738580622573507e+03 5.487270310771121e+03 -1.455699989862274e+03 -3.299937772190060e+00 3.776722479551283e+00 5.790209606004205e+00 +2020-06-01T12:54:50.000000 3.705344484736786e+03 5.524688074570332e+03 -1.397706474418060e+03 -3.347226145628546e+00 3.706767603484208e+00 5.808387820806256e+00 +2020-06-01T12:55:00.000000 3.671637571364472e+03 5.561403910790108e+03 -1.339534871172522e+03 -3.394091717815217e+00 3.636338090597242e+00 5.825826887910302e+00 +2020-06-01T12:55:10.000000 3.637464140659650e+03 5.597413117754309e+03 -1.281192583220854e+03 -3.440528453862712e+00 3.565442894545312e+00 5.842524494912056e+00 +2020-06-01T12:55:20.000000 3.602828510906670e+03 5.632711083635851e+03 -1.222687036304727e+03 -3.486530372793427e+00 3.494091031600779e+00 5.858478425019789e+00 +2020-06-01T12:55:30.000000 3.567735059928707e+03 5.667293287076505e+03 -1.164025677854382e+03 -3.532091548380035e+00 3.422291579475666e+00 5.873686557413596e+00 +2020-06-01T12:55:40.000000 3.532188224533834e+03 5.701155297797067e+03 -1.105215976026061e+03 -3.577206109981348e+00 3.350053676129851e+00 5.888146867590944e+00 +2020-06-01T12:55:50.000000 3.496192499957609e+03 5.734292777191927e+03 -1.046265418738983e+03 -3.621868243366840e+00 3.277386518573906e+00 5.901857427697355e+00 +2020-06-01T12:56:00.000000 3.459752439293260e+03 5.766701478914732e+03 -9.871815127067343e+02 -3.666072191537265e+00 3.204299361656654e+00 5.914816406843454e+00 +2020-06-01T12:56:10.000000 3.422872652915699e+03 5.798377249450789e+03 -9.279717824641997e+02 -3.709812255537029e+00 3.130801516842410e+00 5.927022071407975e+00 +2020-06-01T12:56:20.000000 3.385557807897315e+03 5.829316028676424e+03 -8.686437693967684e+02 -3.753082795256780e+00 3.056902350982258e+00 5.938472785325136e+00 +2020-06-01T12:56:30.000000 3.347812627415674e+03 5.859513850407020e+03 -8.092050307612052e+02 -3.795878230230351e+00 2.982611285070650e+00 5.949167010358718e+00 +2020-06-01T12:56:40.000000 3.309641890152091e+03 5.888966842932780e+03 -7.496631387064052e+02 -3.838193040423129e+00 2.907937792993526e+00 5.959103306360929e+00 +2020-06-01T12:56:50.000000 3.271050429685573e+03 5.917671229540107e+03 -6.900256792922262e+02 -3.880021767010275e+00 2.832891400269496e+00 5.968280331516470e+00 +2020-06-01T12:57:00.000000 3.232043133874393e+03 5.945623329023108e+03 -6.303002515041467e+02 -3.921359013149455e+00 2.757481682777136e+00 5.976696842572099e+00 +2020-06-01T12:57:10.000000 3.192624944234021e+03 5.972819556180033e+03 -5.704944662666218e+02 -3.962199444742536e+00 2.681718265476607e+00 5.984351695051147e+00 +2020-06-01T12:57:20.000000 3.152800855305792e+03 5.999256422297676e+03 -5.106159454552595e+02 -4.002537791188956e+00 2.605610821122038e+00 5.991243843452927e+00 +2020-06-01T12:57:30.000000 3.112575914017731e+03 6.024930535623253e+03 -4.506723209052657e+02 -4.042368846130877e+00 2.529169068963598e+00 5.997372341437393e+00 +2020-06-01T12:57:40.000000 3.071955219038606e+03 6.049838601822848e+03 -3.906712334191785e+02 -4.081687468188486e+00 2.452402773442461e+00 6.002736341994505e+00 +2020-06-01T12:57:50.000000 3.030943920124860e+03 6.073977424426586e+03 -3.306203317731346e+02 -4.120488581685808e+00 2.375321742877921e+00 6.007335097598415e+00 +2020-06-01T12:58:00.000000 2.989547217459140e+03 6.097343905261350e+03 -2.705272717212196e+02 -4.158767177367629e+00 2.297935828145501e+00 6.011167960346532e+00 +2020-06-01T12:58:10.000000 2.947770360984033e+03 6.119935044869053e+03 -2.103997149984859e+02 -4.196518313105856e+00 2.220254921348974e+00 6.014234382083208e+00 +2020-06-01T12:58:20.000000 2.905618649726107e+03 6.141747942912901e+03 -1.502453283240423e+02 -4.233737114596605e+00 2.142288954484293e+00 6.016533914508128e+00 +2020-06-01T12:58:30.000000 2.863097431113918e+03 6.162779798569858e+03 -9.007178239932554e+01 -4.270418776048319e+00 2.064047898093887e+00 6.018066209269436e+00 +2020-06-01T12:58:40.000000 2.820212100290904e+03 6.183027910908733e+03 -2.988675091190664e+01 -4.306558560856805e+00 1.985541759920555e+00 6.018831018041205e+00 +2020-06-01T12:58:50.000000 2.776968099418940e+03 6.202489679256109e+03 3.030209046810927e+01 -4.342151802273343e+00 1.906780583547557e+00 6.018828192585737e+00 +2020-06-01T12:59:00.000000 2.733370916977559e+03 6.221162603547502e+03 9.048706508703376e+01 -4.377193904059689e+00 1.827774447038234e+00 6.018057684800198e+00 +2020-06-01T12:59:10.000000 2.689426087053860e+03 6.239044284666206e+03 1.506604963110659e+02 -4.411680341134853e+00 1.748533461564331e+00 6.016519546747862e+00 +2020-06-01T12:59:20.000000 2.645139188630168e+03 6.256132424766549e+03 2.108147085257330e+02 -4.445606660208350e+00 1.669067770034947e+00 6.014213930673823e+00 +2020-06-01T12:59:30.000000 2.600515844860995e+03 6.272424827585287e+03 2.709420281396730e+02 -4.478968480405426e+00 1.589387545713552e+00 6.011141089005144e+00 +2020-06-01T12:59:40.000000 2.555561722346572e+03 6.287919398737999e+03 3.310347845857567e+02 -4.511761493879551e+00 1.509502990833834e+00 6.007301374335505e+00 +2020-06-01T12:59:50.000000 2.510282530399604e+03 6.302614146001934e+03 3.910853113216811e+02 -4.543981466413849e+00 1.429424335210429e+00 6.002695239394392e+00 +2020-06-01T13:00:00.000000 2.464684020305504e+03 6.316507179585064e+03 4.510859468329136e+02 -4.575624238012422e+00 1.349161834842474e+00 5.997323237000519e+00 diff --git a/data/tests/ccsds/oem/MEO_60s.oem b/data/tests/ccsds/oem/MEO_60s.oem new file mode 100644 index 00000000..515beb47 --- /dev/null +++ b/data/tests/ccsds/oem/MEO_60s.oem @@ -0,0 +1,84 @@ +CCSDS_OEM_VERS = 2.0 + +COMMENT Orbit data are consistent with planetary ephemeris DE-430 + +CREATION_DATE = 2020-06-01T01:26:47 +ORIGINATOR = Test + +META_START +OBJECT_NAME = TEST_OBJ +OBJECT_ID = 0000-000A +CENTER_NAME = Earth +REF_FRAME = ICRF +TIME_SYSTEM = UTC +START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_START_TIME = 2020-06-01T12:00:00.000000 +USEABLE_STOP_TIME = 2020-06-01T13:00:00.000000 +STOP_TIME = 2020-06-01T13:00:00.000000 +INTERPOLATION = Lagrange +INTERPOLATION_DEGREE = 5 +META_STOP + +COMMENT Vehicle's position at any requested time was actually computed using an algorithm, not an interpolation of a table of ephemeris. + +2020-06-01T12:00:00.000000 2.865691508757101e+02 -2.139941760551576e+04 1.634195486175098e+04 2.767385843060133e+00 1.626117031305496e+00 2.072579173220514e+00 -5.850288281974870e-06 4.368474131730205e-04 -3.336668825987843e-04 +2020-06-01T12:01:00.000000 4.525991399948996e+02 -2.130106534115885e+04 1.646570790840699e+04 2.766933135431601e+00 1.652268219511481e+00 2.052482913456063e+00 -9.239913995211514e-06 4.348537217713309e-04 -3.362045371977072e-04 +2020-06-01T12:02:00.000000 6.185958681347711e+02 -2.120114761297122e+04 1.658825062632493e+04 2.766277061803237e+00 1.678298862536459e+00 2.032235099098199e+00 -1.262911858106230e-05 4.328292613440295e-04 -3.387186747655105e-04 +2020-06-01T12:03:00.000000 7.845471342631230e+02 -2.109967170894462e+04 1.670957396457769e+04 2.765417653988115e+00 1.704207118011987e+00 2.011837146127448e+00 -1.601768385396274e-05 4.307741579321410e-04 -3.412091302106990e-04 +2020-06-01T12:04:00.000000 9.504407396467855e+02 -2.099664502738599e+04 1.682966895749312e+04 2.764354956891274e+00 1.729991151180912e+00 1.991290480394419e+00 -1.940539160077215e-05 4.286885392041162e-04 -3.436757396208059e-04 +2020-06-01T12:05:00.000000 1.116264488637525e+03 -2.089207507645835e+04 1.694852672524347e+04 2.763089028511356e+00 1.755649134995042e+00 1.970596537548931e+00 -2.279202358087251e-05 4.265725344579776e-04 -3.461183402718331e-04 +2020-06-01T12:06:00.000000 1.282006189458394e+03 -2.078596947371456e+04 1.706613847443238e+04 2.761619939942118e+00 1.781179250212834e+00 1.949756762968524e+00 -2.617736153335337e-05 4.244262746141785e-04 -3.485367706273604e-04 +2020-06-01T12:07:00.000000 1.447653654988591e+03 -2.067833594562726e+04 1.718249549867490e+04 2.759947775373428e+00 1.806579685496637e+00 1.928772611687142e+00 -2.956118718231959e-05 4.222498922186140e-04 -3.509308703484268e-04 +2020-06-01T12:08:00.000000 1.613194703550546e+03 -2.056918232711051e+04 1.729758917917622e+04 2.758072632092082e+00 1.831848637510238e+00 1.907645548322970e+00 -3.294328223977661e-05 4.200435214358182e-04 -3.533004802974367e-04 +2020-06-01T12:09:00.000000 1.778617159693725e+03 -2.045851656103759e+04 1.741141098530398e+04 2.755994620482352e+00 1.856984311015775e+00 1.886377047006344e+00 -3.632342841286551e-05 4.178072980486176e-04 -3.556454425425984e-04 +2020-06-01T12:10:00.000000 1.943908854981666e+03 -2.034634669775195e+04 1.752395247515699e+04 2.753713864026007e+00 1.881984918971006e+00 1.864968591307182e+00 -3.970140741303263e-05 4.155413594615676e-04 -3.579656003627358e-04 +2020-06-01T12:11:00.000000 2.109057628778859e+03 -2.023268089457283e+04 1.763520529612921e+04 2.751230499301966e+00 1.906848682626447e+00 1.843421674162187e+00 -4.307700095551637e-05 4.132458446927159e-04 -3.602607982554668e-04 +2020-06-01T12:12:00.000000 2.274051329034716e+03 -2.011752741529440e+04 1.774516118547029e+04 2.748544675985967e+00 1.931573831622043e+00 1.821737797801546e+00 -4.644999076809708e-05 4.109208943731873e-04 -3.625308819388248e-04 +2020-06-01T12:13:00.000000 2.438877813069923e+03 -2.000089462968017e+04 1.785381197084017e+04 2.745656556849537e+00 1.956158604084042e+00 1.799918473675567e+00 -4.982015859483347e-05 4.085666507475303e-04 -3.647756983616631e-04 +2020-06-01T12:14:00.000000 2.603524948362620e+03 -1.988279101295001e+04 1.796114957086129e+04 2.742566317758839e+00 1.980601246721801e+00 1.777965222380618e+00 -5.318728621003999e-05 4.061832576779043e-04 -3.669950957031981e-04 +2020-06-01T12:15:00.000000 2.767980613335725e+03 -1.976322514526334e+04 1.806716599566366e+04 2.739274147672743e+00 2.004900014924627e+00 1.755879573585231e+00 -5.655115540731770e-05 4.037708606245836e-04 -3.691889233840350e-04 +2020-06-01T12:16:00.000000 2.932232698137085e+03 -1.964220571119486e+04 1.817185334742857e+04 2.735780248641622e+00 2.029053172857611e+00 1.733663065955271e+00 -5.991154802250720e-05 4.013296066666157e-04 -3.713570320704636e-04 +2020-06-01T12:17:00.000000 3.096269105431891e+03 -1.951974149920488e+04 1.827520382092538e+04 2.732084835804278e+00 2.053058993559044e+00 1.711317247079210e+00 -6.326824592863023e-05 3.988596444839390e-04 -3.734992736777269e-04 +2020-06-01T12:18:00.000000 3.260077751181984e+03 -1.939584140110503e+04 1.837720970404466e+04 2.728188137385934e+00 2.076915759035981e+00 1.688843673392894e+00 -6.662103104926919e-05 3.963611243688197e-04 -3.756155013800903e-04 +2020-06-01T12:19:00.000000 3.423646565436123e+03 -1.927051441151593e+04 1.847786337832742e+04 2.724090394694776e+00 2.100621760361278e+00 1.666243910103839e+00 -6.996968535269477e-05 3.938341982069913e-04 -3.777055696135093e-04 +2020-06-01T12:20:00.000000 3.586963493108391e+03 -1.914376962732207e+04 1.857715731948845e+04 2.719791862119260e+00 2.124175297768900e+00 1.643519531115388e+00 -7.331399087521251e-05 3.912790195008233e-04 -3.797693340841251e-04 +2020-06-01T12:21:00.000000 3.750016494771663e+03 -1.901561624711714e+04 1.867508409793656e+04 2.715292807123546e+00 2.147574680751325e+00 1.620672118950256e+00 -7.665372971193718e-05 3.886957433467470e-04 -3.818066517725838e-04 +2020-06-01T12:22:00.000000 3.912793547433732e+03 -1.888606357064782e+04 1.877163637928892e+04 2.710593510244299e+00 2.170818228154446e+00 1.597703264673929e+00 -7.998868402784964e-05 3.860845264420962e-04 -3.838173809419476e-04 +2020-06-01T12:23:00.000000 4.075282645326299e+03 -1.875512099824815e+04 1.886680692488184e+04 2.705694265085827e+00 2.193904268274274e+00 1.574614567817467e+00 -8.331863606603696e-05 3.834455270856536e-04 -3.858013811429067e-04 +2020-06-01T12:24:00.000000 4.237471800687261e+03 -1.862279803027057e+04 1.896058859227613e+04 2.700595378315440e+00 2.216831138952647e+00 1.551407636300098e+00 -8.664336815423312e-05 3.807789051780601e-04 -3.877585132219396e-04 +2020-06-01T12:25:00.000000 4.399349044546290e+03 -1.848910426650926e+04 1.905297433575851e+04 2.695297169658135e+00 2.239597187673389e+00 1.528084086351264e+00 -8.996266270583941e-05 3.780848222117019e-04 -3.896886393239949e-04 +2020-06-01T12:26:00.000000 4.560902427504659e+03 -1.835404940562049e+04 1.914395720683710e+04 2.689799971891378e+00 2.262200771657640e+00 1.504645542432533e+00 -9.327630222929840e-05 3.753634412774767e-04 -3.915916229044739e-04 +2020-06-01T12:27:00.000000 4.722120020521304e+03 -1.821764324453373e+04 1.923353035473425e+04 2.684104130839011e+00 2.284640257960044e+00 1.481093637158672e+00 -9.658406933722397e-05 3.726149270642236e-04 -3.934673287326310e-04 +2020-06-01T12:28:00.000000 4.882989915694749e+03 -1.807989567785963e+04 1.932168702687244e+04 2.678210005364914e+00 2.306914023564350e+00 1.457430011218768e+00 -9.988574674956641e-05 3.698394458541863e-04 -3.953156228981529e-04 +2020-06-01T12:29:00.000000 5.043500227045102e+03 -1.794081669729134e+04 1.940842056935685e+04 2.672117967366429e+00 2.329020455479021e+00 1.433656313296666e+00 -1.031811172984101e-04 3.670371655210709e-04 -3.971363728202195e-04 +2020-06-01T12:30:00.000000 5.203639091295316e+03 -1.780041639100010e+04 1.949372442745274e+04 2.665828401767386e+00 2.350957950832713e+00 1.409774199991033e+00 -1.064699639380013e-04 3.642082555325219e-04 -3.989294472531679e-04 +2020-06-01T12:31:00.000000 5.363394668653463e+03 -1.765870494302515e+04 1.957759214605790e+04 2.659341706510532e+00 2.372724916969930e+00 1.385785335735045e+00 -1.097520697496588e-04 3.613528869473446e-04 -4.006947162938606e-04 +2020-06-01T12:32:00.000000 5.522755143593176e+03 -1.751569263265783e+04 1.966001737017033e+04 2.652658292549754e+00 2.394319771546411e+00 1.361691392715614e+00 -1.130272179500824e-04 3.584712324157599e-04 -4.024320513884314e-04 +2020-06-01T12:33:00.000000 5.681708725634786e+03 -1.737138983382022e+04 1.974099384535106e+04 2.645778583841705e+00 2.415740942624633e+00 1.337494050792231e+00 -1.162951918969679e-04 3.555634661776021e-04 -4.041413253392733e-04 +2020-06-01T12:34:00.000000 5.840243650124738e+03 -1.722580701443787e+04 1.982051541818209e+04 2.638703017337234e+00 2.436986868769095e+00 1.313194997415303e+00 -1.195557750940652e-04 3.526297640609986e-04 -4.058224123162891e-04 +2020-06-01T12:35:00.000000 5.998348179015218e+03 -1.707895473580612e+04 1.989857603671992e+04 2.631432042972385e+00 2.458055999141591e+00 1.288795927543913e+00 -1.228087511988425e-04 3.496703034792839e-04 -4.074751878585958e-04 +2020-06-01T12:36:00.000000 6.156010601642023e+03 -1.693084365195282e+04 1.997516975094290e+04 2.623966123658874e+00 2.478946793596351e+00 1.264298543563530e+00 -1.260539040344952e-04 3.466852634369750e-04 -4.090995288848274e-04 +2020-06-01T12:37:00.000000 6.313219235505093e+03 -1.678148450899244e+04 2.005029071319476e+04 2.616305735273984e+00 2.499657722775519e+00 1.239704555202849e+00 -1.292910175901447e-04 3.436748245223444e-04 -4.106953137025108e-04 +2020-06-01T12:38:00.000000 6.469962427044092e+03 -1.663088814447670e+04 2.012393317862290e+04 2.608451366650518e+00 2.520187268203965e+00 1.215015679450308e+00 -1.325198760294897e-04 3.406391689070387e-04 -4.122624220138720e-04 +2020-06-01T12:39:00.000000 6.626228552416146e+03 -1.647906548673864e+04 2.019609150561132e+04 2.600403519565920e+00 2.540533922384451e+00 1.190233640470193e+00 -1.357402637019702e-04 3.375784803485689e-04 -4.138007349226742e-04 +2020-06-01T12:40:00.000000 6.782006018272737e+03 -1.632602755423104e+04 2.026676015620856e+04 2.592162708730955e+00 2.560696188892719e+00 1.165360169518264e+00 -1.389519651481803e-04 3.344929441889953e-04 -4.153101349456042e-04 +2020-06-01T12:41:00.000000 6.937283262535509e+03 -1.617178545485861e+04 2.033593369655107e+04 2.583729461778121e+00 2.580672582472443e+00 1.140397004856731e+00 -1.421547651030790e-04 3.313827473480167e-04 -4.167905060165924e-04 +2020-06-01T12:42:00.000000 7.092048755169611e+03 -1.601635038530634e+04 2.040360679728065e+04 2.575104319249722e+00 2.600461629129870e+00 1.115345891669023e+00 -1.453484485088379e-04 3.282480783288555e-04 -4.182417334985234e-04 +2020-06-01T12:43:00.000000 7.246290998959858e+03 -1.585973363035950e+04 2.046977423395761e+04 2.566287834585122e+00 2.620061866228922e+00 1.090208581973735e+00 -1.485328005197124e-04 3.250891272132568e-04 -4.196637041883282e-04 +2020-06-01T12:44:00.000000 7.399998530282830e+03 -1.570194656222045e+04 2.053443088746797e+04 2.557280574107845e+00 2.639471842585772e+00 1.064986834538419e+00 -1.517076065142890e-04 3.219060856653928e-04 -4.210563063274085e-04 +2020-06-01T12:45:00.000000 7.553159919881480e+03 -1.554300063981730e+04 2.059757174442604e+04 2.548083117011879e+00 2.658690118563820e+00 1.039682414792554e+00 -1.548726520944370e-04 3.186991469229652e-04 -4.224194296107027e-04 +2020-06-01T12:46:00.000000 7.705763773634502e+03 -1.538290740810954e+04 2.065919189757169e+04 2.538696055348124e+00 2.677715266167938e+00 1.014297094740180e+00 -1.580277231023652e-04 3.154685058030178e-04 -4.237529651917595e-04 +2020-06-01T12:47:00.000000 7.857798733329552e+03 -1.522167849738587e+04 2.071928654616173e+04 2.529119994009532e+00 2.696545869139412e+00 9.888326528720883e-01 -1.611726056232334e-04 3.122143586974182e-04 -4.250568056954650e-04 +2020-06-01T12:48:00.000000 8.009253477431860e+03 -1.505932562255743e+04 2.077785099635710e+04 2.519355550716558e+00 2.715180523050159e+00 9.632908740772080e-01 -1.643070859922653e-04 3.089369035704692e-04 -4.263308452255102e-04 +2020-06-01T12:49:00.000000 8.160116721854049e+03 -1.489586058244505e+04 2.083488066160379e+04 2.509403356001620e+00 2.733617835397270e+00 9.376735495537264e-01 -1.674309508127747e-04 3.056363399656694e-04 -4.275749793714557e-04 +2020-06-01T12:50:00.000000 8.310377220725532e+03 -1.473129525906041e+04 2.089037106300874e+04 2.499264053192973e+00 2.751856425697556e+00 9.119824767196185e-01 -1.705439869468879e-04 3.023128689897379e-04 -4.287891052191919e-04 +2020-06-01T12:51:00.000000 8.460023767156443e+03 -1.456564161688369e+04 2.094431782971037e+04 2.488938298398967e+00 2.769894925581274e+00 8.862194591225586e-01 -1.736459815417826e-04 2.989666933269455e-04 -4.299731213614709e-04 +2020-06-01T12:52:00.000000 8.609045194008550e+03 -1.439891170213118e+04 2.099671669924392e+04 2.478426760490327e+00 2.787731978887049e+00 8.603863063492648e-01 -1.767367220292788e-04 2.955980172295847e-04 -4.311269279043729e-04 +2020-06-01T12:53:00.000000 8.757430374657477e+03 -1.423111764202187e+04 2.104756351790075e+04 2.467730121083254e+00 2.805366241755531e+00 8.344848339344468e-01 -1.798159961375456e-04 2.922070465195365e-04 -4.322504264786368e-04 +2020-06-01T12:54:00.000000 8.905168223758536e+03 -1.406227164403533e+04 2.109685424108278e+04 2.456849074521199e+00 2.822796382723679e+00 8.085168632690279e-01 -1.828835918890406e-04 2.887939885765776e-04 -4.333435202455370e-04 +2020-06-01T12:55:00.000000 9.052247698006515e+03 -1.389238599516763e+04 2.114458493365069e+04 2.445784327856984e+00 2.840021082818166e+00 7.824842215080317e-01 -1.859392976301930e-04 2.853590523566858e-04 -4.344061139122106e-04 +2020-06-01T12:56:00.000000 9.198657796903473e+03 -1.372147306117462e+04 2.119075177026769e+04 2.434536600832811e+00 2.857039035650168e+00 7.563887414774545e-01 -1.889829020224099e-04 2.819024483738775e-04 -4.354381137376154e-04 +2020-06-01T12:57:00.000000 9.344387563514483e+03 -1.354954528581780e+04 2.123535103573640e+04 2.423106625861596e+00 2.873848947508301e+00 7.302322615810751e-01 -1.920141940612776e-04 2.784243887059745e-04 -4.364394275385823e-04 +2020-06-01T12:58:00.000000 9.489426085230096e+03 -1.337661519009926e+04 2.127837912533083e+04 2.411495148006245e+00 2.890449537452799e+00 7.040166257065109e-01 -1.950329630741885e-04 2.749250869862405e-04 -4.374099647086078e-04 +2020-06-01T12:59:00.000000 9.633762494522583e+03 -1.320269537149316e+04 2.131983254512291e+04 2.399702924959699e+00 2.906839537408585e+00 6.777436831303489e-01 -1.980389987482998e-04 2.714047584139894e-04 -4.383496362177999e-04 +2020-06-01T13:00:00.000000 9.777385969706267e+03 -1.302779850316984e+04 2.135970791230246e+04 2.387730727022708e+00 2.923017692259432e+00 6.514152884233598e-01 -2.010320911238022e-04 2.678636197418034e-04 -4.392583546315799e-04 diff --git a/pyproject.toml b/pyproject.toml index 06fadf36..424c8b5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)" ] -dependencies = ["pandas~=1.5.0", "plotly~=5.13.0", "pytest~=7.2.0", "pyarrow~=11.0.0", "scipy~=1.10.1"] +dependencies = ["pandas~=2.0.0", "plotly~=5.13.0", "pytest~=7.2.0", "pyarrow~=11.0.0", "scipy~=1.10.1", "python-slugify~=8.0.1"] [tool.yapf] based_on_style = "google" diff --git a/python/nyx_space/plots/__init__.py b/python/nyx_space/plots/__init__.py index 2ccba441..534c2874 100644 --- a/python/nyx_space/plots/__init__.py +++ b/python/nyx_space/plots/__init__.py @@ -17,14 +17,13 @@ """ from .gauss_markov import plot_gauss_markov -from .od import plot_covar, plot_estimates, plot_state_deviation, plot_measurements +from .od import plot_covar, plot_estimates, plot_measurements from .traj import plot_traj, plot_ground_track __all__ = [ "plot_gauss_markov", "plot_covar", "plot_estimates", - "plot_state_deviation", "plot_traj", "plot_ground_track", "plot_measurements", diff --git a/python/nyx_space/plots/od.py b/python/nyx_space/plots/od.py index a92974d0..74b457ad 100644 --- a/python/nyx_space/plots/od.py +++ b/python/nyx_space/plots/od.py @@ -20,18 +20,24 @@ from .utils import plot_with_error, plot_line, finalize_plot, colors +import re +from slugify import slugify + import pandas as pd import numpy as np from scipy.stats import norm -from scipy.special import erfcinv def plot_estimates( dfs, title, + cov_frame="RIC", + cov_fmt="sqrt", + cov_sigma=3.0, msr_df=None, - time_col_name="Epoch:GregorianUtc", + time_col_name="Epoch:Gregorian UTC", + ref_traj=None, html_out=None, copyright=None, pos_fig=None, @@ -44,8 +50,12 @@ def plot_estimates( Args: dfs (pandas.DataFrame): The data frame containing the orbit determination solution (or a list thereof) title (str): The title of the plot + cov_frame (str): Frame in which to plot the covariance, defaults to RIC frame. File also contains the integration frame. + cov_fmt (str): Defines how to plot the covariance, defaults to "sqrt" (applies np.sqrt to the column), which will convert the covariance from an expectation to an uncertainty. + cov_sigma (float): Defines the number fo sigmas of uncertainty or covariance to plot, uses [68, 95, 99.7% rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule) for 1.0, 2.0, and 3.0 of sigma. msr_df (pandas.DataFrame): The data frame containing the measurements time_col_name (str): The name of the time column + ref_trajs (pandas.DataFrame): If provided, the difference between the reference trajectory and the estimates will be plotted instead of the estimates alone (or list thereof). html_out (str): The name of the HTML file to save the plot to copyright (str): The copyright to display on the plot pos_fig (plotly.graph_objects.Figure): The figure to plot the position estimates on @@ -56,13 +66,16 @@ def plot_estimates( if not isinstance(dfs, list): dfs = [dfs] + if ref_traj is not None and not isinstance(ref_traj, list): + ref_traj = [ref_traj] + if pos_fig is None: pos_fig = go.Figure() if vel_fig is None: vel_fig = go.Figure() - for df in dfs: + for num, df in enumerate(dfs): try: orig_tim_col = df[time_col_name] except KeyError: @@ -70,7 +83,7 @@ def plot_estimates( try: col_name = [x for x in df.columns if x.startswith("Epoch")][0] except IndexError: - raise KeyError("Could not find any time column") + raise KeyError("Could not find any Epoch column") print(f"Could not find time column {time_col_name}, using `{col_name}`") orig_tim_col = df[col_name] @@ -78,95 +91,143 @@ def plot_estimates( time_col = pd.to_datetime(orig_tim_col) x_title = "Epoch {}".format(time_col_name[-3:]) + # Check that the requested covariance frame exists + frames = set( + [ + re.search(r"\((.*?)\)", c).group(1) + for c in df.columns + if c.startswith("Covariance") + ] + ) + if cov_frame not in frames: + raise ValueError( + f"Covariance frame `{cov_frame}` not in one of the available frames from dataframe: {frames}" + ) covar = {} # Reference the covariance frames - for covar_var in [ - "cx_x", - "cy_y", - "cz_z", - "cx_dot_x_dot", - "cy_dot_y_dot", - "cz_dot_z_dot", - ]: - possible_frames = ["", ":ric", ":rcn", ":vnc"] - for frame in possible_frames: - if f"{covar_var}{frame}" in df: - covar[covar_var] = f"{covar_var}{frame}" - - if covar_var not in covar: - raise KeyError(f"Cannot find {covar_var} covariance column") + for covar_var, covar_col in { + "cx_x": "Covariance XX", + "cy_y": "Covariance YY", + "cz_z": "Covariance ZZ", + "cx_dot_x_dot": "Covariance VxVx", + "cy_dot_y_dot": "Covariance VyVy", + "cz_dot_z_dot": "Covariance VzVz", + }.items(): + # Create a new column with the transformed covariance. (e.g. "Covariance VzVz (RIC) 1.0-sigma sqrt") + cov_col_name = f"{covar_col} ({cov_frame}) {cov_sigma}-sigma {cov_fmt}" + # Transform the current column + df[cov_col_name] = eval(f"np.{cov_fmt}")(df[f"{covar_col} ({cov_frame})"]) + covar[f"{covar_var}"] = cov_col_name + + plt_df = df + plt_columns = [ + "x (km)", + "y (km)", + "z (km)", + "vx (km/s)", + "vy (km/s)", + "vz (km/s)", + ] + plt_modifier = "" + + if ref_traj is not None: + # Merge both dataframes, keeping all of the values + merged_df = pd.merge( + df, + ref_traj[num], + on=time_col_name, + how="outer", + suffixes=("_od", "_ref_traj"), + ) + if merged_df.shape[0] > max(df.shape[0], ref_traj[num].shape[0]): + print( + "Reference epochs differ between dataframes, performing a merge by closest key on TAI epoch instead" + ) + merged_df = pd.merge_asof( + df, ref_traj[num], on="Epoch:TAI (s)", suffixes=("_od", "_ref_traj") + ) + + # Add the difference to reference to the dataframe + for coord in plt_columns: + merged_df[f"delta {coord}"] = ( + merged_df[f"{coord}_od"] - merged_df[f"{coord}_ref_traj"] + ) + # Set the plotted dataframe to this one + plt_df = merged_df + plt_columns = [f"delta {c}" for c in plt_columns] + plt_modifier = " error" plot_with_error( pos_fig, - df, + plt_df, time_col, - "Estimate:X (km)", + plt_columns[0], covar["cx_x"], "blue", x_title, - "Position estimate (km)", + f"Position estimate {plt_modifier}(km)", f"Position estimates: {title}", copyright, ) plot_with_error( pos_fig, - df, + plt_df, time_col, - "Estimate:Y (km)", + plt_columns[1], covar["cy_y"], "green", x_title, - "Position estimate (km)", + f"Position estimate {plt_modifier}(km)", f"Position estimates: {title}", copyright, ) plot_with_error( pos_fig, - df, + plt_df, time_col, - "Estimate:Z (km)", + plt_columns[2], covar["cz_z"], "orange", x_title, - "Position estimate (km)", + f"Position estimate {plt_modifier}(km)", f"Position estimates: {title}", copyright, ) plot_with_error( vel_fig, - df, + plt_df, time_col, - "Estimate:VX (km/s)", + plt_columns[3], covar["cx_dot_x_dot"], "blue", x_title, - "Velocity estimate (km)", + f"Velocity estimate {plt_modifier}(km/s)", f"Velocity estimates: {title}", copyright, ) plot_with_error( vel_fig, - df, + plt_df, time_col, - "Estimate:VY (km/s)", + plt_columns[4], covar["cy_dot_y_dot"], "green", x_title, - "Velocity estimate (km)", + f"Velocity estimate {plt_modifier}(km/s)", f"Velocity estimates: {title}", copyright, ) plot_with_error( vel_fig, - df, + plt_df, time_col, - "Estimate:VZ (km/s)", + plt_columns[5], covar["cz_dot_z_dot"], "orange", x_title, - "Velocity estimate (km)", + f"Velocity estimate {plt_modifier}(km/s)", f"Velocity estimates: {title}", copyright, ) @@ -203,8 +264,11 @@ def plot_estimates( def plot_covar( dfs, title, + cov_frame="RIC", + cov_fmt="sqrt", + cov_sigma=3.0, msr_df=None, - time_col_name="Epoch:GregorianUtc", + time_col_name="Epoch:Gregorian UTC", html_out=None, copyright=None, pos_fig=None, @@ -217,6 +281,9 @@ def plot_covar( Args: dfs (pandas.DataFrame): The data frame containing the orbit determination solution (or a list thereof) title (str): The title of the plot + cov_frame (str): Frame in which to plot the covariance, defaults to RIC frame. File also contains the integration frame. + cov_fmt (str): Defines how to plot the covariance, defaults to "sqrt" (applies np.sqrt to the column), which will convert the covariance from an expectation to an uncertainty. + cov_sigma (float): Defines the number fo sigmas of uncertainty or covariance to plot, uses [68, 95, 99.7% rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule) for 1.0, 2.0, and 3.0 of sigma. msr_df (pandas.DataFrame): The data frame containing the measurements time_col_name (str): The name of the time column html_out (str): The name of the HTML file to save the plot to @@ -243,7 +310,7 @@ def plot_covar( try: col_name = [x for x in df.columns if x.startswith("Epoch")][0] except IndexError: - raise KeyError("Could not find any time column") + raise KeyError("Could not find any Epoch column") print(f"Could not find time column {time_col_name}, using `{col_name}`") orig_tim_col = df[col_name] @@ -251,24 +318,34 @@ def plot_covar( time_col = pd.to_datetime(orig_tim_col) x_title = "Epoch {}".format(time_col_name[-3:]) + # Check that the requested covariance frame exists + frames = set( + [ + re.search(r"\((.*?)\)", c).group(1) + for c in df.columns + if c.startswith("Covariance") + ] + ) + if cov_frame not in frames: + raise ValueError( + f"Covariance frame `{cov_frame}` not in one of the available frames from dataframe: {frames}" + ) covar = {} # Reference the covariance frames - for covar_var in [ - "cx_x", - "cy_y", - "cz_z", - "cx_dot_x_dot", - "cy_dot_y_dot", - "cz_dot_z_dot", - ]: - possible_frames = ["", ":ric", ":rcn", ":vnc"] - for frame in possible_frames: - if f"{covar_var}{frame}" in df: - covar[covar_var] = f"{covar_var}{frame}" - - if covar_var not in covar: - raise KeyError(f"Cannot find {covar_var} covariance column") + for covar_var, covar_col in { + "cx_x": "Covariance XX", + "cy_y": "Covariance YY", + "cz_z": "Covariance ZZ", + "cx_dot_x_dot": "Covariance VxVx", + "cy_dot_y_dot": "Covariance VyVy", + "cz_dot_z_dot": "Covariance VzVz", + }.items(): + # Create a new column with the transformed covariance. (e.g. "Covariance VzVz (RIC) 1.0-sigma sqrt") + cov_col_name = f"{covar_col} ({cov_frame}) {cov_sigma}-sigma {cov_fmt}" + # Transform the current column + df[cov_col_name] = eval(f"np.{cov_fmt}")(df[f"{covar_col} ({cov_frame})"]) + covar[f"{covar_var}"] = cov_col_name plot_line( pos_fig, @@ -277,7 +354,7 @@ def plot_covar( covar["cx_x"], "blue", x_title, - "Position covariance (km)", + f"Position covariance {cov_sigma}-sigma (km)", title, copyright, ) @@ -288,7 +365,7 @@ def plot_covar( covar["cy_y"], "green", x_title, - "Position covariance (km)", + f"Position covariance {cov_sigma}-sigma (km)", title, copyright, ) @@ -299,7 +376,7 @@ def plot_covar( covar["cz_z"], "orange", x_title, - "Position covariance (km)", + f"Position covariance {cov_sigma}-sigma (km)", title, copyright, ) @@ -320,7 +397,7 @@ def plot_covar( covar["cx_dot_x_dot"], "blue", x_title, - "Velocity covariance (km/s)", + f"Velocity covariance {cov_sigma}-sigma (km/s)", title, copyright, ) @@ -331,7 +408,7 @@ def plot_covar( covar["cy_dot_y_dot"], "green", x_title, - "Velocity covariance (km/s)", + f"Velocity covariance {cov_sigma}-sigma (km/s)", title, copyright, ) @@ -342,7 +419,7 @@ def plot_covar( covar["cz_dot_z_dot"], "orange", x_title, - "Velocity covariance (km/s)", + f"Velocity covariance {cov_sigma}-sigma (km/s)", title, copyright, ) @@ -385,220 +462,10 @@ def plot_covar( return pos_fig, vel_fig -def plot_state_deviation( - dfs, - title, - msr_df=None, - traj_err=None, - time_col_name="Epoch:GregorianUtc", - html_out=None, - copyright=None, - pos_fig=None, - vel_fig=None, - show=True, -): - """ - Plot the state deviation from an orbit determination solution - - Args: - dfs (pandas.DataFrame): The data frame containing the orbit determination solution (or list thereof) - title (str): The title of the plot - msr_df (pandas.DataFrame): The data frame containing the measurements - traj_err (pandas.DataFrame): The data frame containing the trajectory error compared to truth (or list thereof) - time_col_name (str): The name of the time column - html_out (str): The name of the HTML file to save the plot to - copyright (str): The copyright to display on the plot - pos_fig (plotly.graph_objects.Figure): The figure to plot the position estimates on - vel_fig (plotly.graph_objects.Figure): The figure to plot the velocity estimates on - show (bool): Whether to show the plot. If set to false, the figure will be returned. - """ - - if not isinstance(dfs, list): - dfs = [dfs] - - if pos_fig is None: - pos_fig = go.Figure() - - if vel_fig is None: - vel_fig = go.Figure() - - x_title = "" # Forward declaration, will store the last time column name - - for df in dfs: - try: - orig_tim_col = df[time_col_name] - except KeyError: - # Find the time column - try: - col_name = [x for x in df.columns if x.startswith("Epoch")][0] - except IndexError: - raise KeyError("Could not find any time column") - print(f"Could not find time column {time_col_name}, using `{col_name}`") - orig_tim_col = df[col_name] - - # Build a Python datetime column - time_col = pd.to_datetime(orig_tim_col) - x_title = "Epoch {}".format(time_col_name[-3:]) - - # If there is a trajectory error, rename the state deviations with the actual trajectory errors - if traj_err is not None: - df["delta_x"] = traj_err["x_err_km"] - df["delta_y"] = traj_err["y_err_km"] - df["delta_z"] = traj_err["z_err_km"] - - df["delta_vx"] = traj_err["vx_err_km_s"] - df["delta_vy"] = traj_err["vy_err_km_s"] - df["delta_vz"] = traj_err["vz_err_km_s"] - print("Overwrote the state deviations with the trajectory error") - - covar = {} - - # Reference the covariance frames - for covar_var in [ - "cx_x", - "cy_y", - "cz_z", - "cx_dot_x_dot", - "cy_dot_y_dot", - "cz_dot_z_dot", - ]: - possible_frames = ["", ":ric", ":rcn", ":vnc"] - for frame in possible_frames: - if f"{covar_var}{frame}" in df: - covar[covar_var] = f"{covar_var}{frame}" - - if covar_var not in covar: - raise KeyError(f"Cannot find {covar_var} covariance column") - - plot_with_error( - pos_fig, - df, - time_col, - "delta_x", - covar["cx_x"], - "blue", - x_title, - "Position deviation (km)", - title, - copyright, - ) - plot_with_error( - pos_fig, - df, - time_col, - "delta_y", - covar["cy_y"], - "green", - x_title, - "Position deviation (km)", - title, - copyright, - ) - plot_with_error( - pos_fig, - df, - time_col, - "delta_z", - covar["cz_z"], - "orange", - x_title, - "Position deviation (km)", - title, - copyright, - ) - # Autoscale - hwpt = int(len(df) / 2) - max_cov_y = max( - max(df[covar["cx_x"]][hwpt:]), - max(df[covar["cy_y"]][hwpt:]), - max(df[covar["cz_z"]][hwpt:]), - ) - - pos_fig.update_layout(yaxis_range=[-1.5 * max_cov_y, 1.5 * max_cov_y]) - - plot_with_error( - vel_fig, - df, - time_col, - "delta_vx", - covar["cx_dot_x_dot"], - "blue", - x_title, - "Velocity deviation (km/s)", - title, - copyright, - ) - plot_with_error( - vel_fig, - df, - time_col, - "delta_vy", - covar["cy_dot_y_dot"], - "green", - x_title, - "Velocity deviation (km/s)", - title, - copyright, - ) - plot_with_error( - vel_fig, - df, - time_col, - "delta_vz", - covar["cz_dot_z_dot"], - "orange", - x_title, - "Velocity deviation (km/s)", - title, - copyright, - ) - # Autoscale - hwpt = int(len(df) / 2) - max_cov_y = max( - max(df[covar["cx_dot_x_dot"]][hwpt:]), - max(df[covar["cy_dot_y_dot"]][hwpt:]), - max(df[covar["cz_dot_z_dot"]][hwpt:]), - ) - - vel_fig.update_layout(yaxis_range=[-1.5 * max_cov_y, 1.5 * max_cov_y]) - - if msr_df is not None: - # Plot the measurements on both plots - pos_fig = plot_measurements( - msr_df, title, time_col_name, fig=pos_fig, show=False - ) - - vel_fig = plot_measurements( - msr_df, title, time_col_name, fig=vel_fig, show=False - ) - - finalize_plot(pos_fig, title, x_title, "Position deviation (km)") - finalize_plot(vel_fig, title, x_title, "Velocity deviation (km/s)") - - if html_out: - html_out = html_out.replace(".html", "_{}.html") - - this_output = html_out.format("pos_dev") - with open(this_output, "w") as f: - f.write(pos_fig.to_html()) - print(f"Saved HTML to {this_output}") - - this_output = html_out.format("vel_dev") - with open(this_output, "w") as f: - f.write(vel_fig.to_html()) - print(f"Saved HTML to {this_output}") - - if show: - pos_fig.show() - vel_fig.show() - else: - return pos_fig, vel_fig - - def plot_measurements( dfs, title, - time_col_name="Epoch:GregorianUtc", + time_col_name="Epoch:Gregorian UTC", html_out=None, copyright=None, fig=None, @@ -622,7 +489,7 @@ def plot_measurements( try: col_name = [x for x in df.columns if x.startswith("Epoch")][0] except IndexError: - raise KeyError("Could not find any time column") + raise KeyError("Could not find any Epoch column") print(f"Could not find time column {time_col_name}, using `{col_name}`") orig_tim_col = df[col_name] @@ -695,10 +562,12 @@ def plot_measurements( def plot_residuals( df, title, - kind="prefit", - time_col_name="Epoch:GregorianUtc", + kind="Prefit", + time_col_name="Epoch:Gregorian UTC", msr_df=None, copyright=None, + html_out=None, + show=True, ): """ Plot of residuals, with 3-σ lines @@ -711,7 +580,7 @@ def plot_residuals( try: col_name = [x for x in df.columns if x.startswith("Epoch")][0] except IndexError: - raise KeyError("Could not find any time column") + raise KeyError("Could not find any Epoch column") print(f"Could not find time column {time_col_name}, using `{col_name}`") orig_tim_col = df[col_name] @@ -722,7 +591,7 @@ def plot_residuals( plt_any = False for col in df.columns: - if col.endswith(kind): + if col.startswith(kind): fig = go.Figure() residuals = df[col] @@ -742,16 +611,32 @@ def plot_residuals( three_sig_color = colors["red_ish"] three_sig_color = f"rgb({int(three_sig_color[0])}, {int(three_sig_color[1])}, {int(three_sig_color[2])})" fig.add_hline( - y=mean + 3 * std, line_dash="dash", line_color=three_sig_color + y=mean + 3 * std, + line_dash="dash", + line_color=three_sig_color, + annotation_text="+ 3σ", ) fig.add_hline( - y=mean - 3 * std, line_dash="dash", line_color=three_sig_color + y=mean - 3 * std, + line_dash="dash", + line_color=three_sig_color, + annotation_text="- 3σ", ) # Add the 1-σ lines one_sig_color = colors["bright_green"] one_sig_color = f"rgb({int(one_sig_color[0])}, {int(one_sig_color[1])}, {int(one_sig_color[2])})" - fig.add_hline(y=mean + std, line_dash="dot", line_color=one_sig_color) - fig.add_hline(y=mean - std, line_dash="dot", line_color=one_sig_color) + fig.add_hline( + y=mean + std, + line_dash="dot", + line_color=one_sig_color, + annotation_text="+ 1σ", + ) + fig.add_hline( + y=mean - std, + line_dash="dot", + line_color=one_sig_color, + annotation_text="- 1σ", + ) if msr_df is not None: # Plot the measurements on both plots @@ -762,20 +647,31 @@ def plot_residuals( finalize_plot( fig, title=f"{title} {col}", xtitle=x_title, copyright=copyright ) - fig.show() + plt_any = True + if html_out: + this_output = html_out.replace(".html", f"_{slugify(col)}.html") + with open(this_output, "w") as f: + f.write(fig.to_html()) + print(f"Saved HTML to {this_output}") + + if show: + fig.show() + if not plt_any: raise ValueError(f"No columns ending with {kind} found -- nothing plotted") -def plot_residual_histogram(df, title, kind="prefit", copyright=None): +def plot_residual_histogram( + df, title, kind="Prefit", copyright=None, html_out=None, show=True +): """ Histogram of residuals """ for col in df.columns: - if col.endswith(kind): + if col.startswith(kind): residuals = df[col] fig = go.Figure() fig.add_trace( @@ -802,110 +698,11 @@ def plot_residual_histogram(df, title, kind="prefit", copyright=None): finalize_plot(fig, title, xtitle=None, copyright=copyright) - fig.show() - + if html_out: + this_output = html_out.replace(".html", f"_{slugify(col)}.html") + with open(this_output, "w") as f: + f.write(fig.to_html()) + print(f"Saved HTML to {this_output}") -def plot_residual_qq( - df, - title, - col="residual_ratio", - copyright=None, -): - """ - Plot of residuals, with 3-σ lines - """ - - residuals = df[col] - - # Determine the number of residuals - # n = len(residuals) - quantiles = np.arange(0, 1.01, 0.01) - - # Calculate the residuals quantiles using np.quantile: - - residual_quantiles = np.quantile(residuals, quantiles) - - # Calculate the theoretical quantiles for a normal distribution with mean=0 and std=1: - - normal_quantiles = np.sqrt(2) * erfcinv(2 * quantiles) - - # Create figure - fig = go.Figure() - - # Add scatter trace of residual vs normal quantiles - fig.add_trace( - go.Scatter( - x=normal_quantiles, y=residual_quantiles, mode="markers", name="Quantiles" - ) - ) - - # Add line representing normal distribution - fig.add_trace( - go.Scatter( - x=[min(normal_quantiles), max(normal_quantiles)], - y=[min(normal_quantiles), max(normal_quantiles)], - mode="lines", - name="Normal", - line=dict(color="red"), - ) - ) - - # Update layout - fig.update_layout( - title="Normal Q-Q Plot", - xaxis_title="Theoretical Quantiles", - yaxis_title="Residual Quantiles", - ) - - # Display interactive plot - finalize_plot(fig, title, copyright=copyright) - fig.show() - - -def plot_qq(df, title, col="residual_ratio", copyright=None): - # Your list of residuals - residuals = df[col] - - # Standardize the residuals - standardized_residuals = (residuals - np.mean(residuals)) / np.std(residuals) - - # Compute the theoretical normal quantiles (percentiles) - residuals_sorted = np.sort(standardized_residuals) - theoretical_quantiles = norm.ppf( - np.linspace(0.5 / len(residuals), 1 - 0.5 / len(residuals), len(residuals)) - ) - - # Create the Q-Q plot using Plotly - fig = go.Figure() - - # Add the scatter plot of standardized residuals - fig.add_trace( - go.Scatter( - x=theoretical_quantiles, - y=residuals_sorted, - mode="markers", - name="Residuals", - ) - ) - - # Add the 45-degree line representing the normal distribution - min_value = np.min([theoretical_quantiles.min(), residuals_sorted.min()]) - max_value = np.max([theoretical_quantiles.max(), residuals_sorted.max()]) - fig.add_shape( - type="line", - x0=min_value, - x1=max_value, - y0=min_value, - y1=max_value, - yref="y", - xref="x", - line=dict(color="red"), - name="Normal Distribution", - ) - - fig.update_layout( - title="Q-Q Plot", - xaxis_title="Theoretical Normal Quantiles", - yaxis_title="Sorted Standardized Residuals", - ) - fig.show() + if show: + fig.show() diff --git a/python/nyx_space/plots/traj.py b/python/nyx_space/plots/traj.py index a70a5536..7b967784 100644 --- a/python/nyx_space/plots/traj.py +++ b/python/nyx_space/plots/traj.py @@ -194,7 +194,7 @@ def plot_ground_track( def plot_orbit_elements( dfs, title, - names, + names=[], html_out=None, copyright=None, show=True, @@ -221,7 +221,8 @@ def plot_orbit_elements( if not isinstance(names, list): names = [names] - assert len(names) == len(dfs), "Number of names must match number of dataframes" + if len(names) > 1: + assert len(names) == len(dfs), "Number of names must match number of dataframes" columns = [ "sma (km)", @@ -241,8 +242,13 @@ def plot_orbit_elements( for col in columns: for k, df in enumerate(dfs): + try: + name = f"{names[k]} {col}" + except IndexError: + name = col + fig.add_trace( - go.Scatter(x=df["Epoch"], y=df[col], name=f"{names[k]} {col}"), + go.Scatter(x=df["Epoch"], y=df[col], name=name), row=row_i + 1, col=col_i + 1, ) diff --git a/src/cosmic/eclipse.rs b/src/cosmic/eclipse.rs index 8dc00709..03b7f550 100644 --- a/src/cosmic/eclipse.rs +++ b/src/cosmic/eclipse.rs @@ -132,6 +132,16 @@ impl fmt::Display for EclipseLocator { } impl EclipseLocator { + /// Creates a new typical eclipse locator. + /// The light source is the Sun, and the shadow bodies are the Earth and the Moon. + pub fn cislunar(cosm: Arc) -> Self { + Self { + light_source: cosm.frame("Sun J2000"), + shadow_bodies: vec![cosm.frame("EME2000"), cosm.frame("Moon J2000")], + cosm, + } + } + /// Compute the visibility/eclipse between an observer and an observed state pub fn compute(&self, observer: &Orbit) -> EclipseState { let mut state = EclipseState::Visibilis; diff --git a/src/cosmic/orbit.rs b/src/cosmic/orbit.rs index 9a8d5901..4c8984bb 100644 --- a/src/cosmic/orbit.rs +++ b/src/cosmic/orbit.rs @@ -1396,7 +1396,7 @@ impl Orbit { /// Returns the semi minor axis in km, includes code for a hyperbolic orbit pub fn semi_minor_axis_km(&self) -> f64 { if self.ecc() <= 1.0 { - ((self.sma_km() * self.ecc()).powi(2) - self.sma_km().powi(2)).sqrt() + self.sma_km() * (1.0 - self.ecc().powi(2)).sqrt() } else { self.hmag_km2_s().powi(2) / (self.frame.gm() * (self.ecc().powi(2) - 1.0).sqrt()) } diff --git a/src/io/formatter.rs b/src/io/formatter.rs deleted file mode 100644 index 1ca0fce6..00000000 --- a/src/io/formatter.rs +++ /dev/null @@ -1,1012 +0,0 @@ -/* - Nyx, blazing fast astrodynamics - Copyright (C) 2023 Christopher Rabotin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -use super::EpochFormat; -use crate::cosmic::{Cosm, Frame, Orbit}; -use crate::linalg::allocator::Allocator; -use crate::linalg::DefaultAllocator; -use crate::md::StateParameter; -use crate::od::estimate::NavSolution; -use crate::{NyxError, State}; -use serde::ser::SerializeSeq; -use serde::{Serialize, Serializer}; -use serde_derive::Deserialize; -use std::cmp::PartialEq; -use std::collections::HashMap; -use std::fmt; -use std::str::FromStr; -use std::sync::Arc; - -#[derive(Deserialize)] -pub struct OutputSerde { - pub filename: String, - /// If not specified, the standard - pub headers: Option>, -} - -impl OutputSerde { - pub fn to_state_formatter(&self, cosm: Arc) -> Result { - match &self.headers { - Some(hdr) => StateFormatter::from_headers( - hdr.iter().map(|s| s.as_str()).collect::>(), - self.filename.clone(), - cosm, - ), - None => Ok(StateFormatter::default(self.filename.clone(), cosm)), - } - } - - pub fn to_nav_sol_formatter(&self, cosm: Arc) -> Result { - match &self.headers { - Some(hdr) => { - NavSolutionFormatter::from_headers(hdr.to_vec(), self.filename.clone(), cosm) - } - None => Ok(NavSolutionFormatter::default(self.filename.clone(), cosm)), - } - } -} - -/// Allowed headers, with an optional frame. -/// TODO: Support units -#[allow(non_camel_case_types)] -#[derive(Clone, Debug, PartialEq)] -pub struct StateHeader { - /// Stores either the state parameter or the epoch - pub param: StateParameter, - pub frame_name: Option, - pub epoch_fmt: Option, -} - -impl From for StateHeader { - fn from(param: StateParameter) -> Self { - StateHeader { - param, - frame_name: None, - epoch_fmt: if param == StateParameter::Epoch { - Some(EpochFormat::GregorianUtc) - } else { - None - }, - } - } -} - -impl fmt::Display for StateHeader { - // Prints the Keplerian orbital elements with units - fn fmt(&self, fh: &mut fmt::Formatter) -> fmt::Result { - let fmtd = match self.param { - StateParameter::X - | StateParameter::Y - | StateParameter::Z - | StateParameter::ApoapsisRadius - | StateParameter::PeriapsisRadius - | StateParameter::GeodeticHeight - | StateParameter::SemiMinorAxis - | StateParameter::SemiParameter - | StateParameter::SMA - | StateParameter::Rmag => { - format!("{:?} (km)", self.param) - } - StateParameter::VX | StateParameter::VY | StateParameter::VZ | StateParameter::Vmag => { - format!("{:?} (km/s)", self.param) - } - StateParameter::AoL - | StateParameter::AoP - | StateParameter::Declination - | StateParameter::EccentricAnomaly - | StateParameter::GeodeticLatitude - | StateParameter::GeodeticLongitude - | StateParameter::Inclination - | StateParameter::MeanAnomaly - | StateParameter::RightAscension - | StateParameter::RAAN - | StateParameter::TrueAnomaly - | StateParameter::TrueLongitude => { - format!("{:?} (deg)", self.param) - } - _ => format!("{:?}", self.param), - }; - write!(fh, "{fmtd}")?; - if let Some(frame) = &self.frame_name { - write!(fh, ":{frame}")?; - } else if let Some(epoch_fmt) = self.epoch_fmt { - write!(fh, ":{epoch_fmt:?}")?; - } - Ok(()) - } -} - -impl Serialize for StateHeader { - /// NOTE: This is not part of unit testing because there is no deseralization of Orbit (yet) - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("{self}")) - } -} - -/// Allowed headers, with an optional frame. -/// TODO: Support units -#[allow(non_camel_case_types)] -#[derive(Clone, Debug, PartialEq)] -pub enum NavSolutionHeader { - /// The epoch in the specified format - Epoch(EpochFormat), - /// Headers for the estimated state - EstimatedState(Vec), - /// Headers for the nominal state - NominalState(Vec), - /// Orbit deviation X (km) - Delta_x, - /// Orbit deviation Y (km) - Delta_y, - /// Orbit deviation Z (km) - Delta_z, - /// Orbit deviation VX (km/s) - Delta_vx, - /// Orbit deviation VY (km/s) - Delta_vy, - /// Orbit deviation VZ (km/s) - Delta_vz, - /// Covariance matrix [1,1] - Cx_x { frame: Option }, - /// Covariance matrix [2,1] - Cy_x { frame: Option }, - /// Covariance matrix [2,2] - Cy_y { frame: Option }, - /// Covariance matrix [3,1] - Cz_x { frame: Option }, - /// Covariance matrix [3,2] - Cz_y { frame: Option }, - /// Covariance matrix [3,3] - Cz_z { frame: Option }, - /// Covariance matrix [4,1] - Cx_dot_x { frame: Option }, - /// Covariance matrix [4,2] - Cx_dot_y { frame: Option }, - /// Covariance matrix [4,3] - Cx_dot_z { frame: Option }, - /// Covariance matrix [4,4] - Cx_dot_x_dot { frame: Option }, - /// Covariance matrix [5,1] - Cy_dot_x { frame: Option }, - /// Covariance matrix [5,2] - Cy_dot_y { frame: Option }, - /// Covariance matrix [5,3] - Cy_dot_z { frame: Option }, - /// Covariance matrix [5,4] - Cy_dot_x_dot { frame: Option }, - /// Covariance matrix [5,5] - Cy_dot_y_dot { frame: Option }, - /// Covariance matrix [6,1] - Cz_dot_x { frame: Option }, - /// Covariance matrix [6,2] - Cz_dot_y { frame: Option }, - /// Covariance matrix [6,3] - Cz_dot_z { frame: Option }, - /// Covariance matrix [6,4] - Cz_dot_x_dot { frame: Option }, - /// Covariance matrix [6,5] - Cz_dot_y_dot { frame: Option }, - /// Covariance matrix [6,6] - Cz_dot_z_dot { frame: Option }, - /// Boolean specifying whether this is a prediction or not - Prediction, - /// Norm of the position items of the covariance (Cx_x, Cy_y, Cz_z) - Covar_pos, - /// Norm of the velocity items of the covartiance (Cx_dot_x_dot, Cy_dot_y_dot, Cz_dot_z_dot) - Covar_vel, -} - -impl fmt::Display for NavSolutionHeader { - fn fmt(&self, fh: &mut fmt::Formatter) -> fmt::Result { - match self { - NavSolutionHeader::Epoch(efmt) => write!(fh, "Epoch:{efmt:?}"), - NavSolutionHeader::EstimatedState(hdr) => { - let mut seq = Vec::with_capacity(hdr.len()); - for element in hdr { - seq.push(format!("Estimate:{element}")); - } - write!(fh, "{}", seq.join(",")) - } - NavSolutionHeader::NominalState(hdr) => { - let mut seq = Vec::with_capacity(hdr.len()); - for element in hdr { - seq.push(format!("Nominal:{element}")); - } - write!(fh, "{}", seq.join(",")) - } - NavSolutionHeader::Delta_x => write!(fh, "delta_x"), - NavSolutionHeader::Delta_y => write!(fh, "delta_y"), - NavSolutionHeader::Delta_z => write!(fh, "delta_z"), - NavSolutionHeader::Delta_vx => write!(fh, "delta_vx"), - NavSolutionHeader::Delta_vy => write!(fh, "delta_vy"), - NavSolutionHeader::Delta_vz => write!(fh, "delta_vz"), - NavSolutionHeader::Cx_x { frame } => { - if let Some(f) = frame { - write!(fh, "cx_x:{f}") - } else { - write!(fh, "cx_x") - } - } - NavSolutionHeader::Cy_x { frame } => { - if let Some(f) = frame { - write!(fh, "cy_x:{f}") - } else { - write!(fh, "cy_x") - } - } - NavSolutionHeader::Cy_y { frame } => { - if let Some(f) = frame { - write!(fh, "cy_y:{f}") - } else { - write!(fh, "cy_y") - } - } - NavSolutionHeader::Cz_x { frame } => { - if let Some(f) = frame { - write!(fh, "cz_x:{f}") - } else { - write!(fh, "cz_x") - } - } - NavSolutionHeader::Cz_y { frame } => { - if let Some(f) = frame { - write!(fh, "cz_y:{f}") - } else { - write!(fh, "cz_y") - } - } - NavSolutionHeader::Cz_z { frame } => { - if let Some(f) = frame { - write!(fh, "cz_z:{f}") - } else { - write!(fh, "cz_z") - } - } - NavSolutionHeader::Cx_dot_x { frame } => { - if let Some(f) = frame { - write!(fh, "cx_dot_x:{f}") - } else { - write!(fh, "cx_dot_x") - } - } - NavSolutionHeader::Cx_dot_y { frame } => { - if let Some(f) = frame { - write!(fh, "cx_dot_y:{f}") - } else { - write!(fh, "cx_dot_y") - } - } - NavSolutionHeader::Cx_dot_z { frame } => { - if let Some(f) = frame { - write!(fh, "cx_dot_z:{f}") - } else { - write!(fh, "cx_dot_z") - } - } - NavSolutionHeader::Cx_dot_x_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cx_dot_x_dot:{f}") - } else { - write!(fh, "cx_dot_x_dot") - } - } - NavSolutionHeader::Cy_dot_x { frame } => { - if let Some(f) = frame { - write!(fh, "cy_dot_x:{f}") - } else { - write!(fh, "cy_dot_x") - } - } - NavSolutionHeader::Cy_dot_y { frame } => { - if let Some(f) = frame { - write!(fh, "cy_dot_y:{f}") - } else { - write!(fh, "cy_dot_y") - } - } - NavSolutionHeader::Cy_dot_z { frame } => { - if let Some(f) = frame { - write!(fh, "cy_dot_z:{f}") - } else { - write!(fh, "cy_dot_z") - } - } - NavSolutionHeader::Cy_dot_x_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cy_dot_x_dot:{f}") - } else { - write!(fh, "cy_dot_x_dot") - } - } - NavSolutionHeader::Cy_dot_y_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cy_dot_y_dot:{f}") - } else { - write!(fh, "cy_dot_y_dot") - } - } - NavSolutionHeader::Cz_dot_x { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_x:{f}") - } else { - write!(fh, "cz_dot_x") - } - } - NavSolutionHeader::Cz_dot_y { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_y:{f}") - } else { - write!(fh, "cz_dot_y") - } - } - NavSolutionHeader::Cz_dot_z { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_z:{f}") - } else { - write!(fh, "cz_dot_z") - } - } - NavSolutionHeader::Cz_dot_x_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_x_dot:{f}") - } else { - write!(fh, "cz_dot_x_dot") - } - } - NavSolutionHeader::Cz_dot_y_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_y_dot:{f}") - } else { - write!(fh, "cz_dot_y_dot") - } - } - NavSolutionHeader::Cz_dot_z_dot { frame } => { - if let Some(f) = frame { - write!(fh, "cz_dot_z_dot:{f}") - } else { - write!(fh, "cz_dot_z_dot") - } - } - NavSolutionHeader::Prediction => write!(fh, "prediction"), - NavSolutionHeader::Covar_pos => write!(fh, "covar_position"), - NavSolutionHeader::Covar_vel => write!(fh, "covar_velocity"), - } - } -} - -impl Serialize for NavSolutionHeader { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - NavSolutionHeader::EstimatedState(hdr) => { - let mut seq = serializer.serialize_seq(Some(hdr.len()))?; - for element in hdr { - seq.serialize_element(&format!("Estimate:{element}"))?; - } - seq.end() - } - NavSolutionHeader::NominalState(hdr) => { - let mut seq = serializer.serialize_seq(Some(hdr.len()))?; - for element in hdr { - seq.serialize_element(&format!("Nominal:{element}"))?; - } - seq.end() - } - _ => serializer.serialize_str(&format!("{self}")), - } - } -} - -/// A formatter for states -#[derive(Clone)] -pub struct StateFormatter { - pub filename: String, - pub headers: Vec, - frames: HashMap, - cosm: Arc, -} - -impl StateFormatter { - /// ``` - /// extern crate nyx_space as nyx; - /// use nyx::io::formatter::StateFormatter; - /// use nyx::cosmic::Cosm; - /// - /// let cosm = Cosm::de438(); - /// // In this case, we're initializing the formatter to output the AoL and the eccentric anomaly in the EME2000 frame. - /// let hdrs = vec!["AoL", "ea:eme2000"]; - /// StateFormatter::from_headers(hdrs, "nope".to_string(), cosm); - /// ``` - pub fn from_headers( - headers: Vec<&str>, - filename: String, - cosm: Arc, - ) -> Result { - let mut frames = HashMap::new(); - let mut hdrs = Vec::with_capacity(20); - // Rebuild the header tokens - for hdr in &headers { - let splt: Vec<&str> = hdr.split(':').collect(); - - match splt[0].to_lowercase().as_str() { - "epoch" => { - let epoch_fmt = if splt.len() == 2 { - match EpochFormat::from_str(splt[1]) { - Ok(e) => e, - Err(_) => { - return Err(NyxError::LoadingError(format!( - "Unknown epoch format {}", - splt[1] - ))) - } - } - } else { - EpochFormat::GregorianUtc - }; - - let hdr = StateHeader { - param: StateParameter::Epoch, - frame_name: None, - epoch_fmt: Some(epoch_fmt), - }; - - hdrs.push(hdr); - } - _ => { - let frame_name = if splt.len() == 2 { - Some(splt[1].to_owned()) - } else { - None - }; - - let param = StateParameter::from_str(splt[0].to_lowercase().as_str())?; - - let hdr = StateHeader { - param, - frame_name, - epoch_fmt: None, - }; - - hdrs.push(hdr); - } - } - - if splt.len() == 2 && splt[0].to_lowercase() != "epoch" { - // Get the frame - match splt[1].to_lowercase().as_str() { - "ric" => frames.insert(splt[1].to_string(), Frame::RIC), - "rcn" => frames.insert(splt[1].to_string(), Frame::RCN), - "vnc" => frames.insert(splt[1].to_string(), Frame::VNC), - _ => frames.insert(splt[1].to_string(), cosm.try_frame(splt[1])?), - }; - } - } - - Ok(Self { - filename, - headers: hdrs, - frames, - cosm, - }) - } - - /// Default headers are [Epoch (GregorianTai), X, Y, Z, VX, VY, VZ], where position is in km and velocity in km/s. - pub fn default(filename: String, cosm: Arc) -> Self { - Self { - filename, - headers: vec![ - From::from(StateParameter::Epoch), - From::from(StateParameter::X), - From::from(StateParameter::Y), - From::from(StateParameter::Z), - From::from(StateParameter::VX), - From::from(StateParameter::VY), - From::from(StateParameter::VZ), - ], - frames: HashMap::new(), - cosm, - } - } - - pub fn fmt(&self, orbit: &Orbit) -> Vec { - // Start by computing the state in all of the frames needed - let mut mapped = HashMap::new(); - for (name, frame) in &self.frames { - let mapped_orbit = match frame { - Frame::RCN | Frame::RIC | Frame::VNC => { - orbit.with_position_rotated_by(orbit.dcm_from_traj_frame(*frame).unwrap()) - } - _ => self.cosm.frame_chg(orbit, *frame), - }; - mapped.insert(name.to_lowercase(), mapped_orbit); - } - let mut formatted = Vec::with_capacity(self.headers.len()); - - for hdr in &self.headers { - // Grab the state in the other frame if needed - let orbit = if hdr.frame_name.is_some() { - &mapped[&hdr.frame_name.as_ref().unwrap().to_lowercase()] - } else { - orbit - }; - - formatted.push(match hdr.param { - StateParameter::Epoch => hdr.epoch_fmt.as_ref().unwrap().format(orbit.epoch), - _ => match orbit.value(hdr.param) { - Ok(value) => format!("{value:.16}"), - Err(e) => { - unimplemented!("{:?} cannot yet be formatted (yet): {}", hdr.param, e) - } - }, - }); - } - - formatted - } -} - -/// A formatter for navigation solution -pub struct NavSolutionFormatter { - pub filename: String, - pub headers: Vec, - pub estimated_headers: StateFormatter, - pub nominal_headers: StateFormatter, - frames: HashMap, - cosm: Arc, -} - -impl NavSolutionFormatter { - /// ``` - /// extern crate nyx_space as nyx; - /// use nyx::io::formatter::NavSolutionFormatter; - /// use nyx::cosmic::Cosm; - /// - /// let cosm = Cosm::de438(); - /// // In this case, we're initializing the formatter to output the AoL and the eccentric anomaly in the EME2000 frame. - /// let hdrs = vec!["estimate:AoL".to_string(), "nominal:ea:eme2000".to_string(), "delta_x".to_string()]; - /// NavSolutionFormatter::from_headers(hdrs, "nope".to_string(), cosm); - /// ``` - pub fn from_headers( - headers: Vec, - filename: String, - cosm: Arc, - ) -> Result { - let mut frames = HashMap::new(); - let mut hdrs = Vec::with_capacity(40); - let mut est_hdrs = Vec::with_capacity(20); - let mut nom_hdrs = Vec::with_capacity(20); - // Rebuild the header tokens - for hdr in &headers { - let lowered = hdr.to_lowercase(); - let splt: Vec<&str> = lowered.split(':').collect(); - - // Check the frames for the nominal and estimated state - // The frames for the covariance are checked blow - let frame_name = if splt.len() == 3 { - // Check that the frame is valid - let name = splt[2].to_owned(); - // Get the frame - frames.insert(name.clone(), cosm.try_frame(&name)?); - Some(name) - } else if splt.len() == 2 - && splt[0].to_lowercase() != "epoch" - && splt[0].to_lowercase() != "nominal" - && splt[0].to_lowercase() != "estimate" - { - // See if this is a frame name - match splt[1].to_lowercase().as_str() { - "ric" => frames.insert(splt[1].to_string(), Frame::RIC), - "rcn" => frames.insert(splt[1].to_string(), Frame::RCN), - "vnc" => frames.insert(splt[1].to_string(), Frame::VNC), - _ => { - if let Ok(frame) = cosm.try_frame(splt[1]) { - frames.insert(splt[1].to_string(), frame) - } else { - None - } - } - }; - Some(splt[1].to_string()) - } else { - None - }; - - match splt[0].to_lowercase().as_str() { - "epoch" => { - hdrs.push(NavSolutionHeader::Epoch(if splt.len() == 2 { - EpochFormat::from_str(splt[1]).unwrap() - } else { - EpochFormat::GregorianUtc - })); - } - "delta_x" => hdrs.push(NavSolutionHeader::Delta_x), - "delta_y" => hdrs.push(NavSolutionHeader::Delta_y), - "delta_z" => hdrs.push(NavSolutionHeader::Delta_z), - "delta_vx" => hdrs.push(NavSolutionHeader::Delta_vx), - "delta_vy" => hdrs.push(NavSolutionHeader::Delta_vy), - "delta_vz" => hdrs.push(NavSolutionHeader::Delta_vz), - "cx_x" => hdrs.push(NavSolutionHeader::Cx_x { frame: frame_name }), - "cy_x" => hdrs.push(NavSolutionHeader::Cy_x { frame: frame_name }), - "cy_y" => hdrs.push(NavSolutionHeader::Cy_y { frame: frame_name }), - "cz_x" => hdrs.push(NavSolutionHeader::Cz_x { frame: frame_name }), - "cz_y" => hdrs.push(NavSolutionHeader::Cz_y { frame: frame_name }), - "cz_z" => hdrs.push(NavSolutionHeader::Cz_z { frame: frame_name }), - "cx_dot_x" => hdrs.push(NavSolutionHeader::Cx_dot_x { frame: frame_name }), - "cx_dot_y" => hdrs.push(NavSolutionHeader::Cx_dot_y { frame: frame_name }), - "cx_dot_z" => hdrs.push(NavSolutionHeader::Cx_dot_z { frame: frame_name }), - "cx_dot_x_dot" => hdrs.push(NavSolutionHeader::Cx_dot_x_dot { frame: frame_name }), - "cy_dot_x" => hdrs.push(NavSolutionHeader::Cy_dot_x { frame: frame_name }), - "cy_dot_y" => hdrs.push(NavSolutionHeader::Cy_dot_y { frame: frame_name }), - "cy_dot_z" => hdrs.push(NavSolutionHeader::Cy_dot_z { frame: frame_name }), - "cy_dot_x_dot" => hdrs.push(NavSolutionHeader::Cy_dot_x_dot { frame: frame_name }), - "cy_dot_y_dot" => hdrs.push(NavSolutionHeader::Cy_dot_y_dot { frame: frame_name }), - "cz_dot_x" => hdrs.push(NavSolutionHeader::Cz_dot_x { frame: frame_name }), - "cz_dot_y" => hdrs.push(NavSolutionHeader::Cz_dot_y { frame: frame_name }), - "cz_dot_z" => hdrs.push(NavSolutionHeader::Cz_dot_z { frame: frame_name }), - "cz_dot_x_dot" => hdrs.push(NavSolutionHeader::Cz_dot_x_dot { frame: frame_name }), - "cz_dot_y_dot" => hdrs.push(NavSolutionHeader::Cz_dot_y_dot { frame: frame_name }), - "cz_dot_z_dot" => hdrs.push(NavSolutionHeader::Cz_dot_z_dot { frame: frame_name }), - "prediction" => hdrs.push(NavSolutionHeader::Prediction), - "covar_position" => hdrs.push(NavSolutionHeader::Covar_pos), - "covar_velocity" => hdrs.push(NavSolutionHeader::Covar_vel), - "estimate" | "nominal" => { - let param = StateParameter::from_str(splt[1].to_lowercase().as_str()) - .expect("Unknown parameter"); - - let state_hdr = StateHeader { - param, - frame_name, - epoch_fmt: None, - }; - - if splt[0] == "estimate" { - est_hdrs.push(state_hdr); - } else { - nom_hdrs.push(state_hdr); - } - } - _ => { - return Err(NyxError::CustomError(format!( - "unknown header `{}`", - splt[0] - ))) - } - } - } - - // Add the nominal and estimate headers (needed to add the header row) - hdrs.push(NavSolutionHeader::EstimatedState(est_hdrs.clone())); - hdrs.push(NavSolutionHeader::NominalState(nom_hdrs.clone())); - - Ok(Self { - filename, - headers: hdrs, - nominal_headers: StateFormatter { - filename: "file_should_not_exist".to_owned(), - headers: nom_hdrs, - frames: frames.clone(), - cosm: cosm.clone(), - }, - estimated_headers: StateFormatter { - filename: "file_should_not_exist".to_owned(), - headers: est_hdrs, - frames: frames.clone(), - cosm: cosm.clone(), - }, - frames, - cosm, - }) - } - - /// Default headers are [Epoch (GregorianTai), X, Y, Z, VX, VY, VZ], where position is in km and velocity in km/s. - pub fn default(filename: String, cosm: Arc) -> Self { - let est_hdrs = vec![ - From::from(StateParameter::X), - From::from(StateParameter::Y), - From::from(StateParameter::Z), - From::from(StateParameter::VX), - From::from(StateParameter::VY), - From::from(StateParameter::VZ), - ]; - Self { - filename, - headers: vec![ - NavSolutionHeader::Epoch(EpochFormat::GregorianTai), - NavSolutionHeader::Delta_x, - NavSolutionHeader::Delta_y, - NavSolutionHeader::Delta_z, - NavSolutionHeader::Delta_vx, - NavSolutionHeader::Delta_vy, - NavSolutionHeader::Delta_vz, - NavSolutionHeader::Cx_x { frame: None }, - NavSolutionHeader::Cy_y { frame: None }, - NavSolutionHeader::Cz_z { frame: None }, - NavSolutionHeader::Cx_dot_x_dot { frame: None }, - NavSolutionHeader::Cy_dot_y_dot { frame: None }, - NavSolutionHeader::Cz_dot_z_dot { frame: None }, - NavSolutionHeader::EstimatedState(est_hdrs.clone()), - NavSolutionHeader::Prediction, - ], - nominal_headers: StateFormatter { - filename: "file_should_not_exist".to_owned(), - headers: Vec::new(), - frames: HashMap::new(), - cosm: cosm.clone(), - }, - estimated_headers: StateFormatter { - filename: "file_should_not_exist".to_owned(), - headers: est_hdrs, - frames: HashMap::new(), - cosm: cosm.clone(), - }, - frames: HashMap::new(), - cosm, - } - } - - pub fn fmt>(&self, sol: &S) -> Vec - where - DefaultAllocator: Allocator::Size> - + Allocator::Size, ::Size> - + Allocator::VecLength>, - { - // Start by computing the state in all of the frames needed - let mut dcms = HashMap::new(); - let orbit = sol.orbital_state(); - for (name, frame) in &self.frames { - let mapping_dcm = match frame { - Frame::RCN | Frame::RIC | Frame::VNC => { - orbit.dcm6x6_from_traj_frame(*frame).unwrap() - } - _ => self - .cosm - .try_dcm_from_to(&orbit.frame, frame, orbit.epoch) - .unwrap(), - }; - dcms.insert(name.to_lowercase(), mapping_dcm); - } - - let mut formatted = Vec::new(); - - for hdr in &self.headers { - match hdr { - NavSolutionHeader::EstimatedState(_) => { - // The formatter is already initialized - for fmtval in self.estimated_headers.fmt(&sol.orbital_state()) { - formatted.push(fmtval); - } - } - NavSolutionHeader::NominalState(_) => { - // The formatter is already initialized - for fmtval in self.nominal_headers.fmt(&sol.expected_state()) { - formatted.push(fmtval); - } - } - NavSolutionHeader::Epoch(efmt) => formatted.push(efmt.format(sol.epoch())), - NavSolutionHeader::Delta_x => { - formatted.push(format!("{:.16e}", sol.state_deviation()[0])) - } - NavSolutionHeader::Delta_y => { - formatted.push(format!("{:.16e}", sol.state_deviation()[1])) - } - NavSolutionHeader::Delta_z => { - formatted.push(format!("{:.16e}", sol.state_deviation()[2])) - } - NavSolutionHeader::Delta_vx => { - formatted.push(format!("{:.16e}", sol.state_deviation()[3])) - } - NavSolutionHeader::Delta_vy => { - formatted.push(format!("{:.16e}", sol.state_deviation()[4])) - } - NavSolutionHeader::Delta_vz => { - formatted.push(format!("{:.16e}", sol.state_deviation()[5])) - } - NavSolutionHeader::Cx_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(0, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(0, 0))), - }, - NavSolutionHeader::Cy_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(1, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(1, 0))), - }, - NavSolutionHeader::Cy_y { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(1, 1)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(1, 1))), - }, - NavSolutionHeader::Cz_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(2, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(2, 0))), - }, - NavSolutionHeader::Cz_y { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(2, 1)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(2, 1))), - }, - NavSolutionHeader::Cz_z { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(2, 2)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(2, 2))), - }, - NavSolutionHeader::Cx_dot_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(3, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(3, 0))), - }, - NavSolutionHeader::Cx_dot_y { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(3, 1)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(3, 1))), - }, - NavSolutionHeader::Cx_dot_z { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(3, 2)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(3, 2))), - }, - NavSolutionHeader::Cx_dot_x_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(3, 3)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(3, 3))), - }, - NavSolutionHeader::Cy_dot_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(4, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(4, 0))), - }, - NavSolutionHeader::Cy_dot_y { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(4, 1)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(4, 1))), - }, - NavSolutionHeader::Cy_dot_z { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(4, 2)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(4, 2))), - }, - NavSolutionHeader::Cy_dot_x_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(4, 3)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(4, 3))), - }, - NavSolutionHeader::Cy_dot_y_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(4, 4)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(4, 4))), - }, - NavSolutionHeader::Cz_dot_x { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 0)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 0))), - }, - NavSolutionHeader::Cz_dot_y { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 1)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 1))), - }, - NavSolutionHeader::Cz_dot_z { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 2)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 2))), - }, - NavSolutionHeader::Cz_dot_x_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 3)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 3))), - }, - NavSolutionHeader::Cz_dot_y_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 4)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 4))), - }, - NavSolutionHeader::Cz_dot_z_dot { frame } => match frame { - Some(frame_name) => { - // Rotate the covariance - let covar = dcms[frame_name] * sol.covar() * dcms[frame_name].transpose(); - formatted.push(format!("{:.16e}", covar[(5, 5)])) - } - None => formatted.push(format!("{:.16e}", sol.covar_ij(5, 5))), - }, - NavSolutionHeader::Prediction => formatted.push(format!("{}", sol.predicted())), - NavSolutionHeader::Covar_pos => formatted.push(format!( - "{:.16e}", - (sol.covar_ij(0, 0).powi(2) - + sol.covar_ij(1, 1).powi(2) - + sol.covar_ij(2, 2).powi(2)) - .sqrt() - )), - NavSolutionHeader::Covar_vel => formatted.push(format!( - "{:.16e}", - (sol.covar_ij(3, 3).powi(2) - + sol.covar_ij(4, 4).powi(2) - + sol.covar_ij(5, 5).powi(2)) - .sqrt() - )), - }; - } - - formatted - } -} diff --git a/src/io/mod.rs b/src/io/mod.rs index 14e1ac60..1e815bab 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -17,9 +17,11 @@ */ use crate::errors::NyxError; +use crate::md::StateParameter; use crate::time::Epoch; use crate::Orbit; pub(crate) mod watermark; +use hifitime::prelude::{Format, Formatter}; use hifitime::Duration; use serde::de::DeserializeOwned; use serde::ser::SerializeSeq; @@ -28,12 +30,11 @@ use serde::{Serialize, Serializer}; use serde_yaml::Error as YamlError; use std::collections::HashMap; use std::convert::From; -use std::fmt; use std::fmt::Debug; use std::fs::File; use std::io::BufReader; use std::io::Error as IoError; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; @@ -44,23 +45,123 @@ use crate::cosmic::{Cosm, Frame}; pub mod cosmo; pub mod dynamics; pub mod estimate; -pub mod formatter; /// Handles reading from frames defined in input files pub mod frame_serde; /// Handles loading of gravity models using files of NASA PDS and GMAT COF. Several gunzipped files are provided with nyx. pub mod gravity; pub mod matrices; pub mod orbit; -pub mod quantity; -/// Handles reading random variables -pub mod rv; -pub mod scenario; pub mod tracking_data; pub mod trajectory_data; use std::io; use thiserror::Error; +#[cfg(feature = "python")] +use pyo3::prelude::*; + +/// Configuration for exporting a trajectory to parquet. +#[derive(Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr( + feature = "python", + pyo3( + text_signature = "(timestamp=None, fields=None, start_epoch=None, step=None, end_epoch=None, metadata=None)" + ) +)] +pub struct ExportCfg { + /// Fields to export, if unset, defaults to all possible fields. + pub fields: Option>, + /// Start epoch to export, defaults to the start of the trajectory + pub start_epoch: Option, + /// End epoch to export, defaults to the end of the trajectory + pub end_epoch: Option, + /// An optional step, defaults to every state in the trajectory (which likely isn't equidistant) + pub step: Option, + /// Additional metadata to store in the Parquet metadata + pub metadata: Option>, + /// Set to true to append the timestamp to the filename + pub timestamp: bool, +} + +impl ExportCfg { + /// Initialize a new configuration with the given metadata entries. + pub fn from_metadata(metadata: Vec<(String, String)>) -> Self { + let mut me = ExportCfg { + metadata: Some(HashMap::new()), + ..Default::default() + }; + for (k, v) in metadata { + me.metadata.as_mut().unwrap().insert(k, v); + } + me + } + + /// Initialize a new default configuration but timestamp the filename. + pub fn timestamped() -> Self { + Self { + timestamp: true, + ..Default::default() + } + } + + pub fn append_field(&mut self, field: StateParameter) { + if let Some(fields) = self.fields.as_mut() { + fields.push(field); + } else { + self.fields = Some(vec![field]); + } + } + + pub fn set_step(&mut self, step: Duration) { + self.step = Some(step); + } + + /// Modifies the provided path to include the timestamp if required. + pub(crate) fn actual_path>(&self, path: P) -> PathBuf { + let mut path_buf = path.as_ref().to_path_buf(); + if self.timestamp { + if let Some(file_name) = path_buf.file_name() { + if let Some(file_name_str) = file_name.to_str() { + if let Some(extension) = path_buf.extension() { + let stamp = Formatter::new( + Epoch::now().unwrap(), + Format::from_str("%Y-%m-%dT%H-%M-%S").unwrap(), + ); + let ext = extension.to_str().unwrap(); + let file_name = file_name_str.replace(&format!(".{ext}"), ""); + let new_file_name = format!("{file_name}-{stamp}.{}", ext); + path_buf.set_file_name(new_file_name); + } + } + } + }; + path_buf + } +} + +#[cfg(feature = "python")] +#[pymethods] +impl ExportCfg { + #[new] + fn py_new( + timestamp: Option, + fields: Option>, + start_epoch: Option, + end_epoch: Option, + metadata: Option>, + ) -> Self { + Self { + timestamp: timestamp.unwrap_or(false), + fields, + start_epoch, + end_epoch, + metadata, + ..Default::default() + } + } +} + #[derive(Error, Debug)] pub enum ConfigError { #[error("Failed to read configuration file: {0}")] @@ -233,8 +334,6 @@ pub enum ParsingError { OD(String), UseOdInstead, UseMdInstead, - EpochFormat, - CovarFormat, FileNotFound(String), FileNotUTF8(String), SequenceNotFound(String), @@ -255,114 +354,3 @@ impl From for ParsingError { Self::ExecutionError(error) } } - -/// Specifies the format of the Epoch during serialization -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum EpochFormat { - /// Default is GregorianUtc, as defined in [hifitime](https://docs.rs/hifitime/). - GregorianUtc, - GregorianTai, - MjdTai, - MjdTt, - MjdUtc, - JdeEt, - JdeTai, - JdeTt, - JdeUtc, - /// Seconds past a provided TAI Epoch - TaiSecs(f64), - /// Days past a provided TAI Epoch - TaiDays(f64), -} - -impl EpochFormat { - pub fn format(&self, dt: Epoch) -> String { - match *self { - EpochFormat::GregorianUtc => format!("{dt}"), - EpochFormat::GregorianTai => format!("{dt:x}",), - EpochFormat::MjdTai => format!("{:.9}", dt.to_mjd_tai_days()), - EpochFormat::MjdTt => format!("{:.9}", dt.to_mjd_tt_days()), - EpochFormat::MjdUtc => format!("{:.9}", dt.to_mjd_utc_days()), - EpochFormat::JdeEt => format!("{:.9}", dt.to_jde_et_days()), - EpochFormat::JdeTai => format!("{:.9}", dt.to_jde_tai_days()), - EpochFormat::JdeTt => format!("{:.9}", dt.to_jde_tt_days()), - EpochFormat::JdeUtc => format!("{:.9}", dt.to_jde_utc_days()), - EpochFormat::TaiSecs(e) => format!("{:.9}", dt.to_tai_seconds() - e), - EpochFormat::TaiDays(e) => format!("{:.9}", dt.to_tai_days() - e), - } - } -} - -impl fmt::Display for EpochFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - EpochFormat::GregorianUtc => write!(f, "Gregorian UTC"), - EpochFormat::GregorianTai => write!(f, "Gregorian TAI"), - EpochFormat::MjdTai => write!(f, "MJD TAI"), - EpochFormat::MjdTt => write!(f, "MJD TT"), - EpochFormat::MjdUtc => write!(f, "MJD UTC"), - EpochFormat::JdeEt => write!(f, "JDE ET"), - EpochFormat::JdeTai => write!(f, "JDE TAI"), - EpochFormat::JdeTt => write!(f, "JDE TT"), - EpochFormat::JdeUtc => write!(f, "JDE UTC"), - EpochFormat::TaiSecs(_) => write!(f, "TAI+ s"), - EpochFormat::TaiDays(_) => write!(f, "TAI+ days"), - } - } -} - -impl FromStr for EpochFormat { - type Err = ParsingError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().replace(' ', "").as_str() { - "gregorianutc" => Ok(EpochFormat::GregorianUtc), - "gregoriantai" => Ok(EpochFormat::GregorianTai), - "mjdtai" => Ok(EpochFormat::MjdTai), - "mjdtt" => Ok(EpochFormat::MjdTt), - "mjdutc" => Ok(EpochFormat::MjdUtc), - "jdeet" => Ok(EpochFormat::JdeEt), - "jdetai" => Ok(EpochFormat::JdeTai), - "jdett" => Ok(EpochFormat::JdeTt), - "jdeutc" => Ok(EpochFormat::JdeUtc), - _ => Err(ParsingError::EpochFormat), - } - } -} - -/// Specifies the format of the covariance during serialization -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CovarFormat { - /// Default: allows plotting the variance of the elements instead of the covariance - Sqrt, - /// Keeps the covariance as computed, i.e. one sigma (~68%), causes e.g. positional elements in km^2. - Sigma1, - /// Three sigma covers about 99.7% of the distribution - Sigma3, - /// Allows specifying a custom multiplication factor of each element of the covariance. - MulSigma(f64), -} - -impl fmt::Display for CovarFormat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - CovarFormat::Sqrt => write!(f, "exptd_val_"), - CovarFormat::Sigma1 => write!(f, "covar_"), - CovarFormat::Sigma3 => write!(f, "3sig_covar"), - CovarFormat::MulSigma(x) => write!(f, "{x}sig_covar"), - } - } -} - -impl FromStr for CovarFormat { - type Err = ParsingError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().replace(' ', "").as_str() { - "sqrt" => Ok(CovarFormat::Sqrt), - "1sigma" | "sigma1" => Ok(CovarFormat::Sigma1), - "3sigma" | "sigma3" => Ok(CovarFormat::Sigma3), - _ => Err(ParsingError::CovarFormat), - } - } -} diff --git a/src/io/quantity.rs b/src/io/quantity.rs deleted file mode 100644 index e601ce9f..00000000 --- a/src/io/quantity.rs +++ /dev/null @@ -1,153 +0,0 @@ -/* - Nyx, blazing fast astrodynamics - Copyright (C) 2023 Christopher Rabotin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -use hifitime::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; -use regex::Regex; - -pub use super::ParsingError; - -#[derive(Copy, Clone, Debug)] -pub enum Quantity { - /// Stores the distance in kilometers - Distance(f64), - /// Stores the velocity in km/s - Velocity(f64), - /// Stores the duration in seconds - Duration(f64), -} - -impl Quantity { - /// Returns the value of this quantity in kilometers for distances and kilometers per second for velocities. - pub fn v(self) -> f64 { - match self { - Self::Distance(v) => v, - Self::Velocity(v) => v, - Self::Duration(v) => v, - } - } -} - -/// Parse a duration -/// -/// ``` -/// extern crate nyx_space as nyx; -/// -/// use nyx::io::quantity::parse_duration; -/// use std::f64::EPSILON; -/// -/// assert!((parse_duration("1 * days").unwrap().v() - 86_400.0).abs() < EPSILON); -/// assert!((parse_duration("1 days").unwrap().v() - 86_400.0).abs() < EPSILON); -/// assert!((parse_duration("1* day").unwrap().v() - 86_400.0).abs() < EPSILON); -/// assert!((parse_duration("1 *d").unwrap().v() - 86_400.0).abs() < EPSILON); -/// assert!((parse_duration("1 * h").unwrap().v() - 3_600.0).abs() < EPSILON); -/// assert!((parse_duration("1 h").unwrap().v() - 3_600.0).abs() < EPSILON); -/// assert!((parse_duration("1.0000 * hour").unwrap().v() - 3_600.0).abs() < EPSILON); -/// assert!((parse_duration("1.0000 * hours").unwrap().v() - 3_600.0).abs() < EPSILON); -/// assert!((parse_duration("1.0 * min").unwrap().v() - 60.0).abs() < EPSILON); -/// assert!((parse_duration("1. * s").unwrap().v() - 1.0).abs() < EPSILON); -/// assert!((parse_duration("1 * s").unwrap().v() - 1.0).abs() < EPSILON); -/// assert!((parse_duration("1 s").unwrap().v() - 1.0).abs() < EPSILON); -/// ``` -pub fn parse_duration(duration: &str) -> Result { - let reg = Regex::new(r"^(\d+\.?\d*)\W*(\w+)$").unwrap(); - match reg.captures(duration) { - Some(cap) => { - let mut time_s = cap[1].to_owned().parse::().unwrap(); - match cap[2].to_owned().to_lowercase().as_str() { - "days" | "day" | "d" => time_s *= SECONDS_PER_DAY, - "hours" | "hour" | "h" => time_s *= SECONDS_PER_HOUR, - "min" | "mins" | "minute" | "minutes" | "m" => time_s *= SECONDS_PER_MINUTE, - "s" | "sec" | "secs" => time_s *= 1.0, - _ => { - return Err(ParsingError::Duration(format!( - "unknown duration unit in `{duration}`", - ))) - } - } - Ok(Quantity::Duration(time_s)) - } - None => Err(ParsingError::Duration(format!( - "Could not parse stopping condition: `{duration}`", - ))), - } -} - -/// Parse a distance or velocity -/// -/// ``` -/// extern crate nyx_space as nyx; -/// use nyx::io::quantity::parse_quantity; -/// use std::f64::EPSILON; -/// -/// assert!((parse_quantity("1.0 km").unwrap().v() - 1.0).abs() < EPSILON); -/// assert!((parse_quantity("-1.3 mm").unwrap().v() - -1.3e-6).abs() < EPSILON); -/// assert!((parse_quantity("3.4e3 m/s").unwrap().v() - 3.4).abs() < EPSILON); -/// assert!((parse_quantity("3.4e0 km/s").unwrap().v() - 3.4).abs() < EPSILON); -/// assert!((parse_quantity("3.4e-3 Mm/s").unwrap().v() - 3.4).abs() < EPSILON); -/// assert!((parse_quantity("7 m").unwrap().v() - 7e-3).abs() < EPSILON); -/// assert!((parse_quantity("-6 km/h").unwrap().v() - -6.0/3_600.0).abs() < EPSILON); -/// ``` -pub fn parse_quantity(input: &str) -> Result { - let reg = - Regex::new(r#"(-?\d+\.?\d*(?:e-?\d+\.?\d*)?)\W*([G|M|k|m|u|n]?)m/?([h|s])?"#).unwrap(); - - match reg.captures(input) { - Some(cap) => { - let mut value = cap[1].to_owned().parse::().unwrap(); - // The second group can be empty, in which case the input was in meters. - match cap[2].to_owned().as_str() { - "G" => value *= 1e6, - "M" => value *= 1e3, - "k" => value *= 1.0, - "" => value *= 1e-3, - "m" => value *= 1e-6, - "u" => value *= 1e-9, - "n" => value *= 1e-12, - _ => { - return Err(ParsingError::Distance(format!( - "unknown distance multiplier unit in `{input}`", - ))) - } - } - if let Some(time_div) = cap.get(3) { - // This is a velocity - match time_div.as_str().to_lowercase().as_str() { - "h" => value /= SECONDS_PER_HOUR, - "s" => value *= 1.0, - _ => { - return Err(ParsingError::Velocity(format!( - "unknown time divisor unit in `{input}`", - ))) - } - } - Ok(Quantity::Velocity(value)) - } else { - Ok(Quantity::Distance(value)) - } - } - None => { - // Try to parse as a duration - match parse_duration(input) { - Ok(v) => Ok(v), - Err(_) => Err(ParsingError::Quantity(format!( - "Could not understand quantity: `{input}`", - ))), - } - } - } -} diff --git a/src/io/rv.rs b/src/io/rv.rs deleted file mode 100644 index a4eeba0f..00000000 --- a/src/io/rv.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Nyx, blazing fast astrodynamics - Copyright (C) 2023 Christopher Rabotin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -use serde_derive::Deserialize; - -#[derive(Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum Distribution { - Normal { mean: f64, std_dev: f64 }, - Cauchy { median: f64, scale: f64 }, - Exponential { lambda: f64 }, - Poisson { lambda: f64 }, -} - -#[test] -fn test_deser_distr() { - use toml; - - let _std_norm: Distribution = toml::from_str( - r#"[normal] - mean = 0.0 - std_dev = 1.0"#, - ) - .unwrap(); - - let _cauchy: Distribution = toml::from_str( - r#"[cauchy] - median = 0.0 - scale = 1.0"#, - ) - .unwrap(); - - let _my_exp: Distribution = toml::from_str( - r#"[exponential] - lambda = 0.5"#, - ) - .unwrap(); - - let _fish: Distribution = toml::from_str( - r#"[poisson] - lambda = 10.0 - "#, - ) - .unwrap(); -} - -#[test] -fn test_deser_distr_multi() { - use std::collections::HashMap; - use toml; - - #[derive(Deserialize)] - struct MapRv { - pub rvs: HashMap, - } - - let _as_map: MapRv = toml::from_str( - r#"rvs = { one = { normal = { mean = 0.0, std_dev = 0.2 } }, two = { poisson = { lambda = 10.0 } } }"#, - ) - .unwrap(); - println!("{:?}", _as_map.rvs.keys()); -} diff --git a/src/io/scenario.rs b/src/io/scenario.rs deleted file mode 100644 index 06b3822c..00000000 --- a/src/io/scenario.rs +++ /dev/null @@ -1,694 +0,0 @@ -/* - Nyx, blazing fast astrodynamics - Copyright (C) 2023 Christopher Rabotin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -use super::formatter::OutputSerde; -use super::gravity::HarmonicsMem; -use super::quantity::*; -use super::rv::Distribution; -use super::ParsingError; -use crate::cosmic::{Frame, Orbit}; -use crate::md::{Event, StateParameter}; -use crate::time::Epoch; -use crate::NyxError; -use serde_derive::Deserialize; -use std::collections::HashMap; -use std::str::FromStr; - -#[derive(Deserialize)] -pub struct StateSerde { - pub frame: Option, - pub epoch: String, - pub x: Option, - pub y: Option, - pub z: Option, - pub vx: Option, - pub vy: Option, - pub vz: Option, - pub position: Option>, - pub velocity: Option>, - pub sma: Option, - pub ecc: Option, - pub inc: Option, - pub raan: Option, - pub aop: Option, - pub ta: Option, - pub unit_position: Option, - pub unit_velocity: Option, -} - -impl StateSerde { - pub fn as_state(&self, frame: Frame) -> Result { - let epoch = match Epoch::from_str(&self.epoch) { - Ok(epoch) => epoch, - Err(e) => return Err(ParsingError::Quantity(format!("{e}"))), - }; - - // Rebuild a valid state from the three different initializations - if self.x.is_some() - && self.y.is_some() - && self.z.is_some() - && self.vx.is_some() - && self.vy.is_some() - && self.vz.is_some() - { - let pos_mul = match &self.unit_position { - Some(unit) => match unit.to_lowercase().as_str() { - "km" => 1.0, - "m" => 1e-3, - "cm" => 1e-5, - _ => { - return Err(ParsingError::Distance(format!( - "unknown unit `{unit}` when converting state" - ))) - } - }, - None => 1.0, - }; - - let vel_mul = match &self.unit_velocity { - Some(unit) => match unit.to_lowercase().as_str() { - "km/s" => 1.0, - "m/s" => 1e-3, - "cm/s" => 1e-5, - _ => { - return Err(ParsingError::Velocity(format!( - "unknown unit `{unit}` when converting state" - ))) - } - }, - None => 1.0, - }; - - Ok(Orbit::cartesian( - self.x.unwrap() * pos_mul, - self.y.unwrap() * pos_mul, - self.z.unwrap() * pos_mul, - self.vx.unwrap() * vel_mul, - self.vy.unwrap() * vel_mul, - self.vz.unwrap() * vel_mul, - epoch, - frame, - )) - } else if self.position.is_some() && self.velocity.is_some() { - let position = self.position.as_ref().unwrap(); - let velocity = self.velocity.as_ref().unwrap(); - if position.len() != 3 || velocity.len() != 3 { - return Err(ParsingError::IllDefined( - "Orbit ill defined: position and velocity arrays must be of size 3 exactly" - .to_string(), - )); - } - let x = parse_quantity(&position[0])?.v(); - let y = parse_quantity(&position[1])?.v(); - let z = parse_quantity(&position[2])?.v(); - let vx = parse_quantity(&velocity[0])?.v(); - let vy = parse_quantity(&velocity[1])?.v(); - let vz = parse_quantity(&velocity[2])?.v(); - - Ok(Orbit::cartesian(x, y, z, vx, vy, vz, epoch, frame)) - } else if self.sma.is_some() - && self.ecc.is_some() - && self.inc.is_some() - && self.raan.is_some() - && self.aop.is_some() - && self.ta.is_some() - { - Ok(Orbit::keplerian( - self.sma.unwrap(), - self.ecc.unwrap(), - self.inc.unwrap(), - self.raan.unwrap(), - self.aop.unwrap(), - self.ta.unwrap(), - epoch, - frame, - )) - } else { - Err(ParsingError::IllDefined("Orbit ill defined: specify either {position, velocity}, or {x,y,z,vx,vy,vz}, or Keplerian elements".to_string())) - } - } -} - -/// A state difference -#[derive(Deserialize)] -pub struct DeltaStateSerde { - pub inherit: String, - pub x: Option, - pub y: Option, - pub z: Option, - pub vx: Option, - pub vy: Option, - pub vz: Option, - pub position: Option>, - pub velocity: Option>, - pub sma: Option, - pub ecc: Option, - pub inc: Option, - pub raan: Option, - pub aop: Option, - pub ta: Option, - pub unit_position: Option, - pub unit_velocity: Option, -} - -impl DeltaStateSerde { - pub fn as_state(&self, base: Orbit) -> Result { - let frame = base.frame; - let epoch = base.epoch; - // Rebuild a valid state from the three different initializations - if self.x.is_some() - || self.y.is_some() - || self.z.is_some() - || self.vx.is_some() - || self.vy.is_some() - || self.vz.is_some() - { - let pos_mul = match &self.unit_position { - Some(unit) => match unit.to_lowercase().as_str() { - "km" => 1.0, - "m" => 1e-3, - "cm" => 1e-5, - _ => panic!("unknown unit `{unit}`"), - }, - None => 1.0, - }; - - let vel_mul = match &self.unit_velocity { - Some(unit) => match unit.to_lowercase().as_str() { - "km/s" => 1.0, - "m/s" => 1e-3, - "cm/s" => 1e-5, - _ => panic!("unknown unit `{unit}`"), - }, - None => 1.0, - }; - - Ok(Orbit::cartesian( - self.x.unwrap_or(0.0) * pos_mul, - self.y.unwrap_or(0.0) * pos_mul, - self.z.unwrap_or(0.0) * pos_mul, - self.vx.unwrap_or(0.0) * vel_mul, - self.vy.unwrap_or(0.0) * vel_mul, - self.vz.unwrap_or(0.0) * vel_mul, - epoch, - frame, - ) + base) - } else if self.position.is_some() || self.velocity.is_some() { - let (x, y, z) = match &self.position { - Some(position) => { - if position.len() != 3 { - return Err(ParsingError::IllDefined( - "Orbit ill defined: position arrays must be of size 3 exactly" - .to_string(), - )); - } - let x = parse_quantity(&position[0])?.v(); - let y = parse_quantity(&position[1])?.v(); - let z = parse_quantity(&position[2])?.v(); - (x, y, z) - } - None => (0.0, 0.0, 0.0), - }; - - let (vx, vy, vz) = match &self.velocity { - Some(velocity) => { - if velocity.len() != 3 { - return Err(ParsingError::IllDefined( - "Orbit ill defined: velocity arrays must be of size 3 exactly" - .to_string(), - )); - } - let vx = parse_quantity(&velocity[0])?.v(); - let vy = parse_quantity(&velocity[1])?.v(); - let vz = parse_quantity(&velocity[2])?.v(); - (vx, vy, vz) - } - None => (0.0, 0.0, 0.0), - }; - - Ok(Orbit::cartesian(x, y, z, vx, vy, vz, epoch, frame) + base) - } else if self.sma.is_some() - || self.ecc.is_some() - || self.inc.is_some() - || self.raan.is_some() - || self.aop.is_some() - || self.ta.is_some() - { - let sma = match self.sma { - Some(sma) => sma + base.sma_km(), - None => base.sma_km(), - }; - let ecc = match self.ecc { - Some(ecc) => ecc + base.ecc(), - None => base.ecc(), - }; - let inc = match self.inc { - Some(inc) => inc + base.inc_deg(), - None => base.inc_deg(), - }; - let raan = match self.raan { - Some(raan) => raan + base.raan_deg(), - None => base.raan_deg(), - }; - let aop = match self.aop { - Some(aop) => aop + base.aop_deg(), - None => base.aop_deg(), - }; - let ta = match self.ta { - Some(ta) => ta + base.ta_deg(), - None => base.ta_deg(), - }; - Ok(Orbit::keplerian(sma, ecc, inc, raan, aop, ta, epoch, frame) + base) - } else { - Err(ParsingError::IllDefined("Delta state ill defined: specify either {position, velocity}, or {x,y,z,vx,vy,vz}, or Keplerian elements".to_string())) - } - } -} - -#[derive(Deserialize)] -pub struct OrbitalDynamicsSerde { - pub initial_state: String, - /// If unspecified, the frame of the state is assumed - pub integration_frame: Option, - pub point_masses: Option>, - pub accel_models: Option>, -} - -#[derive(Deserialize)] -pub struct SpacecraftSerde { - pub dry_mass: f64, - pub fuel_mass: Option, - pub orbital_dynamics: String, - pub force_models: Option>, -} - -#[derive(Deserialize)] -pub struct SolarPressureSerde { - pub sc_area: f64, - #[serde(default = "default_cr")] - pub cr: f64, - #[serde(default = "default_phi")] - pub phi: f64, -} - -fn default_cr() -> f64 { - 1.8 -} -fn default_phi() -> f64 { - 1367.0 -} - -#[derive(Deserialize)] -pub struct Harmonics { - pub frame: String, - pub degree: usize, - pub order: Option, - pub file: String, -} - -impl Harmonics { - pub fn load(&self) -> Result { - let gunzipped = self.file.contains("gz"); - let order = self.order.unwrap_or(0); - if self.file.contains("cof") { - HarmonicsMem::from_cof(self.file.as_str(), self.degree, order, gunzipped) - } else if self.file.contains("sha") { - HarmonicsMem::from_shadr(self.file.as_str(), self.degree, order, gunzipped) - } else if self.file.contains("EGM") { - HarmonicsMem::from_egm(self.file.as_str(), self.degree, order, gunzipped) - } else { - panic!("could not guess file format from name"); - } - } -} - -#[derive(Deserialize)] -pub struct AccelModel { - pub harmonics: HashMap, -} - -#[derive(Deserialize)] -pub struct ForceModel { - pub srp: HashMap, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum PropagatorKind { - Dormand45, - Dormand78, - Fehlberg45, - CashKarp45, - Rk89, - Rk4, - Verner56, -} - -#[derive(Deserialize)] -pub struct PropagatorSerde { - /// Name of the string being associated with this propagator - pub dynamics: String, - /// Name of the stopping condition used - pub stop_cond: String, - /// The tolerance of the propagator (default to 1e-12) - pub tolerance: Option, - pub output: Option, - /// If no kind is specified, an RK89 will be used - pub kind: Option, -} - -#[derive(Deserialize)] -pub struct OdpSerde { - /// Name of the navigation propagator - pub navigation_prop: String, - /// Name of the measurement generator/reader - pub measurements: String, - /// Name of the initial estimate name - pub initial_estimate: String, - /// Measurement noise - pub msr_noise: Vec, - /// Optional SNC - pub snc: Option>, - /// Optional SNC turn-off time - pub snc_disable: Option, - /// Optional SNC exponential decay - pub snc_decay: Option>, - /// Set the number of measurements to switch to an EKF - pub ekf_msr_trigger: Option, - /// Set the acceptable time between measurements - pub ekf_disable_time: Option, - /// An optional output of a NavSolution - pub output: Option, -} - -#[derive(Deserialize)] -pub struct EstimateSerde { - /// The initial state estimate, must be a reference to a state - pub state: String, - /// The full covariance matrix, must be 6x6 - pub covar_mat: Option>>, - /// The diagonal of the covariance matrix (a single vector of 6 values) - pub covar_diag: Option>, -} - -#[derive(Deserialize)] -pub struct MeasurementSerde { - /// Name of the truth propagator, if unspecified then the output must be specified - pub propagator: Option, - /// Names of the measurement devices to use - pub msr_device: Vec, - /// Name of the output file to store the measurements - pub output: Option, - /// Optionally specify whether to use the file if it exists - pub use_file_if_available: Option, -} - -#[derive(Deserialize)] -pub struct ScenarioSerde { - pub sequence: Vec, - pub propagator: HashMap, - pub state: HashMap, - pub delta_state: Option>, - pub orbital_dynamics: HashMap, - pub spacecraft: HashMap, - pub accel_models: Option>, - pub force_models: Option>, - pub output: HashMap, - pub distr: Option>, - pub odp: Option>, - pub estimate: Option>, - pub measurements: Option>, - pub conditions: Option>, -} - -#[derive(Clone, Deserialize)] -pub struct ConditionSerde { - /// Pattern must be `event_name = event_value`, e.g. `TA = 159` - pub event: String, - pub search_until: String, - pub hits: Option, -} - -impl ConditionSerde { - pub fn to_condition(&self) -> Event { - let rplt = self.event.replace('=', ""); - let parts: Vec<&str> = rplt.split(' ').collect(); - let parameter = StateParameter::from_str(parts[0]).unwrap(); - let value = if parts.len() == 2 { - match parts[1].trim().parse::() { - Ok(val) => val, - Err(e) => { - warn!( - "Could not understand value `{}` in parameter: {}", - parts[1], e - ); - 0.0 - } - } - } else { - 0.0 - }; - - Event::new(parameter, value) - } -} - -#[test] -fn test_md_scenario() { - use toml; - - let scen: ScenarioSerde = toml::from_str( - r#" - sequence = ["prop_name"] - - [state.STATE_NAME] - x = -2436.45 - y = -2436.45 - z = 6891.037 - vx = 5.088611 - vy = -5.088611 - vz = 0.0 - frame = "EME2000" - epoch = "MJD 51544.5 TAI" # or "2018-09-15T00:15:53.098 UTC" - unit_position = "km" # Default value if unspecified - unit_velocity = "km/s" # Default value if unspecified - - [state.another_state] - # Mix and match units - position = ["-3436.45 km", "-3436.45 km", "6891.037e3 m"] - velocity = ["5.088611 km/s", "-5.088611 km/s", "0.0 cm/s"] - frame = "EME2000" - epoch = "MJD 51540.5 TAI" - - [state.kepler_proud] - sma = 7000 - inc = 27.4 # degrees - ecc = 0.001 - raan = 45 - aop = 45 - ta = 180 - frame = "EME2000" - epoch = "MJD 51540.5 TAI" - - [delta_state.my_delta] - inherit = "another_state" - x = 1.0 # Will add 1 km to the x component - - [delta_state.my_delta2] - inherit = "another_state" - velocity = ["1 m/s", "2 m/s", "-5.0 m/s"] - - [delta_state.my_delta3] - inherit = "another_state" - vx = 5e-3 - - [orbital_dynamics.conf_name] - integration_frame = "EME2000" - initial_state = "state_name" - point_masses = ["Sun", "Earth", "Jupiter", "Luna"] - accel_models = ["jgm3_70x70"] - - [orbital_dynamics.two_body_dyn] - initial_state = "state_name" - - [spacecraft.sc1] - dry_mass = 100.0 - fuel_mass = 20.0 - orbital_dynamics = "conf_name" - force_models = ["my_frc"] - - [propagator.prop_name] - flavor = "rk89" # If unspecified, the default propagator is used - dynamics = "sc1" # Use "sc1" spacecraft dynamics - stop_cond = "MJD 51540.5 TAI" - output = "my_csv" - - [output.my_csv] - filename = "./data/scenario-run.csv" - headers = ["epoch:GregorianUtc", "x", "y", "z", "vx", "vy", "vz", "rmag:Luna"] - - [propagator.simple] - dynamics = "two_body" # Use "sc1" spacecraft dynamics - stop_cond = "1 * day" - - [accel_models.my_models.harmonics.jgm3_70x70] - frame = "EME2000" - degree = 70 - order = 70 - file = "data/JGM3.cof.gz" - - [force_models.my_frc.srp.my_srp] - sc_area = 1.0 # in meters squared - cr = 1.5 # Defaults to 1.8 - - [condition.third_apo] - kind = "apoapse" - search_until = "MJD 51540.5 TAI" - hits = 3 # Stopping condition triggered on third apoapse passage - - "#, - ) - .unwrap(); - - assert_eq!(scen.state.len(), 3); - assert_eq!(scen.delta_state.unwrap().len(), 3); - assert_eq!(scen.orbital_dynamics.len(), 2); - assert_eq!(scen.propagator.len(), 2); - assert_eq!(scen.accel_models.as_ref().unwrap().len(), 1); - assert_eq!( - scen.accel_models.as_ref().unwrap()["my_models"] - .harmonics - .len(), - 1 - ); - assert_eq!(scen.sequence.len(), 1); -} - -#[test] -#[ignore = "OD scenario disabled during rewrite"] -fn test_od_scenario() { - use toml; - - let scen: ScenarioSerde = toml::from_str( - r#" - sequence = ["my_flt"] - - [state.state_name] - x = -2436.45 - y = -2436.45 - z = 6891.037 - vx = 5.088611 - vy = -5.088611 - vz = 0.0 - frame = "EME2000" - epoch = "MJD 51544.5 TAI" # or "2018-09-15T00:15:53.098 UTC" - unit_position = "km" # Default value if unspecified - unit_velocity = "km/s" # Default value if unspecified - - [orbital_dynamics.conf_name] - integration_frame = "EME2000" - initial_state = "state_name" - point_masses = ["Sun", "Earth", "Jupiter", "Luna"] - accel_models = ["my_models"] - - [spacecraft.sc1] - dry_mass = 100.0 - fuel_mass = 20.0 - orbital_dynamics = "conf_name" - force_models = ["my_srp"] - - [propagator.nav_prop] - dynamics = "sc1" - stop_cond = "3.5 days" - output = "my_csv" - tolerance = 1e-9 - - [propagator.truth_propagator] - dynamics = "sc1" - stop_cond = "3.5 days" - output = "my_csv" - - [accel_models.my_models.harmonics.jgm3_70x70] - frame = "EME2000" - degree = 70 - order = 70 - file = "data/JGM3.cof.gz" - - [output.my_csv] - filename = "./data/truth.csv" - headers = ["epoch:GregorianUtc", "x", "y", "z", "vx", "vy", "vz", "rmag:Luna"] - - [odp.my_flt] - navigation_prop = "nav_prop" - initial_estimate = "my_estimate" - msr_noise = [1e-6, 1e-3] - snc = [1e-8, 1e-8, 1e-8] - snc_disable = "120 * sec" - snc_decay = ["20 * min", "20 min", "15 min"] - measurements = "msr_sim" # Or provide a file name - ekf_msr_trigger = 30 - ekf_disable_time = "3600 s" # If no measurements for an hour, disable the EKF - output = "estimate_csv" - - [output.estimate_csv] - filename = "./data/estimates.csv" - headers = ["epoch:GregorianUtc", "delta_x", "delta_y", "delta_z", "delta_vx", "delta_vy", "delta_vz"] # If unset, default will be used - - [measurements.msr_sim] - propagator = "truth_propagator" - msr_device = ["dss13", "dss65", "dss34"] - output = "msr_sim.csv" - use_file_if_available = true - - [stations.dss13] - elevation = 10.0 - latitude = 40.427_222 - longitude = 4.250_556 - height = 0.834_939 - range_noise = 0.1 - range_rate_noise = 0.01 - - [stations.dss65] - inherit = "dss65" # Name of the station, built-in - elevation = 10.0 - range_noise = 0.1 - range_rate_noise = 0.01 - - [estimate.my_estimate] - state = "state_name" - #covar_mat = [[1e1, 1e1, 1e1, 1e1, 1e1, 1e1],[1e1, 1e1, 1e1, 1e1, 1e1, 1e1],[1e1, 1e1, 1e1, 1e1, 1e1, 1e1],[1e1, 1e1, 1e1, 1e1, 1e1, 1e1],[1e1, 1e1, 1e1, 1e1, 1e1, 1e1],[1e1, 1e1, 1e1, 1e1, 1e1, 1e1]] - covar_diag = [1e1, 1e1, 1e1, 1e-2, 1e-2, 1e-2] - "#, - ) - .unwrap(); - - assert_eq!(scen.state.len(), 1); - assert_eq!(scen.orbital_dynamics.len(), 1); - assert_eq!(scen.propagator.len(), 2); - assert_eq!(scen.accel_models.as_ref().unwrap().len(), 1); - assert_eq!( - scen.accel_models.as_ref().unwrap()["my_models"] - .harmonics - .len(), - 1 - ); - assert_eq!(scen.sequence.len(), 1); - assert_eq!(scen.odp.unwrap().len(), 1); - assert_eq!(scen.measurements.unwrap().len(), 1); - assert_eq!(scen.estimate.unwrap().len(), 1); -} diff --git a/src/io/trajectory_data.rs b/src/io/trajectory_data.rs index 8610534f..8c6c86f6 100644 --- a/src/io/trajectory_data.rs +++ b/src/io/trajectory_data.rs @@ -27,6 +27,8 @@ use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; use std::fs::File; use std::{collections::HashMap, error::Error, fmt::Display, path::Path}; +#[cfg(feature = "python")] +use crate::Spacecraft; #[cfg(feature = "python")] use pyo3::prelude::*; @@ -257,6 +259,22 @@ impl DynamicTrajectory { Self::from_parquet(path).map_err(|e| NyxError::CustomError(e.to_string())) } + /// Converts the provided CCSDS OEM file (first argument) into a Nyx trajectory Parquet file (second argument) and given a template spacecraft (third argument). + #[staticmethod] + #[pyo3(text_signature = "(oem_path, parquet_path, spacecraft_template)")] + fn convert_oem_to_parquet( + oem_path: String, + parquet_path: String, + spacecraft_template: Spacecraft, + ) -> Result<(), NyxError> { + let traj = Traj::::from_oem_file(oem_path, spacecraft_template)?; + // Convert to parquet + traj.to_parquet_simple(parquet_path) + .map_err(|e| NyxError::CustomError(e.to_string()))?; + + Ok(()) + } + fn __repr__(&self) -> String { format!("{self}") } diff --git a/src/md/mod.rs b/src/md/mod.rs index 86eb36a8..bbb96307 100644 --- a/src/md/mod.rs +++ b/src/md/mod.rs @@ -17,14 +17,16 @@ */ use crate::errors::NyxError; -use crate::io::formatter::StateFormatter; use crate::{Orbit, Spacecraft}; use std::error::Error; use std::fmt; -use std::fs::File; pub mod prelude { - pub use super::{optimizer::*, trajectory::Traj, Ephemeris, Event, ScTraj, StateParameter}; + pub use super::{ + optimizer::*, + trajectory::{ExportCfg, Interpolatable, Traj}, + Ephemeris, Event, ScTraj, StateParameter, + }; pub use crate::cosmic::{ try_achieve_b_plane, BPlane, BPlaneTarget, Bodies, Cosm, Frame, GuidanceMode, LightTimeCalc, Orbit, OrbitDual, @@ -58,47 +60,6 @@ pub use param::StateParameter; pub use opti::target_variable::{Variable, Vary}; -/// A Mission Design handler -pub trait MdHdlr: Send + Sync { - fn handle(&mut self, state: &StateType); -} - -pub struct OrbitStateOutput { - csv_out: csv::Writer, - fmtr: StateFormatter, -} - -impl OrbitStateOutput { - pub fn new(fmtr: StateFormatter) -> Result { - match csv::Writer::from_path(fmtr.filename.clone()) { - Ok(mut wtr) => { - wtr.serialize(&fmtr.headers) - .expect("could not write headers"); - info!("Saving output to {}", fmtr.filename); - - Ok(Self { csv_out: wtr, fmtr }) - } - Err(e) => Err(NyxError::ExportError(e.to_string())), - } - } -} - -impl MdHdlr for OrbitStateOutput { - fn handle(&mut self, state: &Spacecraft) { - self.csv_out - .serialize(self.fmtr.fmt(&state.orbit)) - .expect("could not format state"); - } -} - -impl MdHdlr for OrbitStateOutput { - fn handle(&mut self, state: &Orbit) { - self.csv_out - .serialize(self.fmtr.fmt(state)) - .expect("could not format state"); - } -} - #[allow(clippy::result_large_err)] #[derive(Clone, PartialEq, Debug)] pub enum TargetingError { diff --git a/src/md/trajectory/interpolatable.rs b/src/md/trajectory/interpolatable.rs index 324708fc..ac911cf8 100644 --- a/src/md/trajectory/interpolatable.rs +++ b/src/md/trajectory/interpolatable.rs @@ -47,6 +47,9 @@ where /// List of state parameters that will be exported to a trajectory file in addition to the epoch (provided in this different formats). fn export_params() -> Vec; + + /// Returns the orbit + fn orbit(&self) -> &Orbit; } impl Interpolatable for Orbit { @@ -150,6 +153,10 @@ impl Interpolatable for Orbit { ] .concat() } + + fn orbit(&self) -> &Orbit { + self + } } impl Interpolatable for Spacecraft { @@ -222,4 +229,8 @@ impl Interpolatable for Spacecraft { ] .concat() } + + fn orbit(&self) -> &Orbit { + &self.orbit + } } diff --git a/src/md/trajectory/mod.rs b/src/md/trajectory/mod.rs index 2d728fbc..4b004366 100644 --- a/src/md/trajectory/mod.rs +++ b/src/md/trajectory/mod.rs @@ -17,18 +17,20 @@ */ mod interpolatable; +mod orbit_traj; +mod sc_traj; mod traj; mod traj_it; pub use interpolatable::Interpolatable; pub(crate) use interpolatable::INTERPOLATION_SAMPLES; -use serde::{Deserialize, Serialize}; pub use traj::Traj; +pub use crate::io::ExportCfg; + use super::StateParameter; use crate::time::{Duration, Epoch}; -use std::collections::HashMap; use std::error::Error; use std::fmt; @@ -68,46 +70,3 @@ impl fmt::Display for TrajError { } impl Error for TrajError {} - -/// Configuration for exporting a trajectory to parquet. -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct ExportCfg { - /// Fields to export, if unset, defaults to all possible fields. - pub fields: Option>, - /// Start epoch to export, defaults to the start of the trajectory - pub start_epoch: Option, - /// End epoch to export, defaults to the end of the trajectory - pub end_epoch: Option, - /// An optional step, defaults to every state in the trajectory (which likely isn't equidistant) - pub step: Option, - /// Additional metadata to store in the Parquet metadata - pub metadata: Option>, - /// Set to true to append the timestamp to the filename - pub timestamp: bool, -} - -impl ExportCfg { - /// Initialize a new configuration with the given metadata entries. - pub fn from_metadata(metadata: Vec<(String, String)>) -> Self { - let mut me = ExportCfg { - metadata: Some(HashMap::new()), - ..Default::default() - }; - for (k, v) in metadata { - me.metadata.as_mut().unwrap().insert(k, v); - } - me - } - - pub fn append_field(&mut self, field: StateParameter) { - if let Some(fields) = self.fields.as_mut() { - fields.push(field); - } else { - self.fields = Some(vec![field]); - } - } - - pub fn set_step(&mut self, step: Duration) { - self.step = Some(step); - } -} diff --git a/src/md/trajectory/orbit_traj.rs b/src/md/trajectory/orbit_traj.rs new file mode 100644 index 00000000..d799c424 --- /dev/null +++ b/src/md/trajectory/orbit_traj.rs @@ -0,0 +1,281 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use super::TrajError; +use super::{ExportCfg, Traj}; +use crate::cosmic::{Cosm, Frame, Orbit}; +use crate::errors::NyxError; +use crate::md::prelude::StateParameter; +use crate::md::EventEvaluator; +use crate::time::Epoch; +use crate::time::TimeUnits; +use crate::Spacecraft; +use std::collections::HashMap; +use std::error::Error; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Instant; + +impl Traj { + /// Allows converting the source trajectory into the (almost) equivalent trajectory in another frame. + /// This simply converts each state into the other frame and may lead to aliasing due to the Nyquist–Shannon sampling theorem. + #[allow(clippy::map_clone)] + pub fn to_frame(&self, new_frame: Frame, cosm: Arc) -> Result { + if self.states.is_empty() { + return Err(NyxError::Trajectory(TrajError::CreationError( + "No trajectory to convert".to_string(), + ))); + } + let start_instant = Instant::now(); + let mut traj = Self::new(); + for state in &self.states { + traj.states.push(cosm.frame_chg(state, new_frame)); + } + traj.finalize(); + + info!( + "Converted trajectory from {} to {} in {} ms: {traj}", + self.first().frame, + new_frame, + (Instant::now() - start_instant).as_millis() + ); + Ok(traj) + } + + /// Exports this trajectory to the provided filename in parquet format with only the epoch, the geodetic latitude, longitude, and height at one state per minute. + /// Must provide a body fixed frame to correctly compute the latitude and longitude. + #[allow(clippy::identity_op)] + pub fn to_groundtrack_parquet>( + &self, + path: P, + body_fixed_frame: Frame, + events: Option>>, + metadata: Option>, + cosm: Arc, + ) -> Result> { + let traj = self.to_frame(body_fixed_frame, cosm)?; + + let mut cfg = ExportCfg::default(); + cfg.append_field(StateParameter::GeodeticLatitude); + cfg.append_field(StateParameter::GeodeticLongitude); + cfg.append_field(StateParameter::GeodeticHeight); + cfg.append_field(StateParameter::Rmag); + cfg.set_step(1.minutes()); + cfg.metadata = metadata; + + traj.to_parquet(path, events, cfg) + } + + /// Convert this orbit trajectory into a spacecraft trajectory by copying the provided template and setting its orbit state to that of each state of the trajectory + pub fn upcast(&self, template: Spacecraft) -> Traj { + let mut out = Traj::new(); + for orbit in &self.states { + out.states.push(template.with_orbit(*orbit)); + } + out + } + + /// Initialize a new orbit trajectory from the path to a CCSDS OEM file. + /// + /// # Limitations + /// 1. Only text versions of the OEM format are supported + /// 2. The covariance information, if present, is ignored + /// 3. Only one spacecraft per OEM file is supported. + /// + /// # Thanks + /// GPT-4 because I didn't want to spend too much time coding this up since it'll be a feature in ANISE. + pub fn from_oem_file>(path: P) -> Result { + let cosm = Cosm::de438(); + // Open the file + let file = + File::open(path).map_err(|e| NyxError::CCSDS(format!("File opening error: {e}")))?; + let mut reader = BufReader::new(file); + + // Read the file contents into a buffer + let mut buffer = String::new(); + reader + .read_to_string(&mut buffer) + .map_err(|e| NyxError::CCSDS(format!("File read error: {e}")))?; + + // Split the file contents into lines + let lines: Vec<&str> = buffer.lines().collect(); + + // Parse the Orbit Element messages + let mut frame: Option = None; + let mut time_system = String::new(); + + let ignored_tokens = ["CCSDS_OMM_VERS", "CREATION_DATE", "ORIGINATOR"]; + + let mut traj = Self::default(); + + let mut start = false; + + 'lines: for (lno, line) in lines.iter().enumerate() { + for tok in &ignored_tokens { + if line.starts_with(tok) { + // Ignore this token + continue 'lines; + } + } + if line.starts_with("OBJECT_NAME") { + // Extract the object ID from the line + let parts: Vec<&str> = line.split('=').collect(); + let name = parts[1].trim().to_string(); + debug!("[line: {lno}] Found object {name}"); + traj.name = Some(name); + } else if line.starts_with("REF_FRAME") { + let parts: Vec<&str> = line.split('=').collect(); + let mut ref_frame = parts[1].trim(); + if ref_frame == "ICRF" { + ref_frame = "EME2000"; + } + frame = Some(cosm.try_frame(ref_frame)?); + } else if line.starts_with("TIME_SYSTEM") { + let parts: Vec<&str> = line.split('=').collect(); + time_system = parts[1].trim().to_string(); + debug!("[line: {lno}] Found time system `{time_system}`"); + } else if line.starts_with("META_STOP") { + // We can start parsing now + start = true; + } else if !line.is_empty() && start { + // Split the line into components + let parts: Vec<&str> = line.split_whitespace().collect(); + + // Extract the values + let epoch_str = format!("{} {time_system}", parts[0]); + match parts[1].parse::() { + Ok(x_km) => { + // Look good! + let y_km = parts[2].parse::().unwrap(); + let z_km = parts[3].parse::().unwrap(); + let vx_km_s = parts[4].parse::().unwrap(); + let vy_km_s = parts[5].parse::().unwrap(); + let vz_km_s = parts[6].parse::().unwrap(); + + debug!("[line: {lno}] Parsing epoch `{epoch_str}`"); + let orbit = Orbit { + epoch: Epoch::from_str(epoch_str.trim()).map_err(|e| { + NyxError::CCSDS(format!("Parsing epoch error: {e}")) + })?, + x_km, + y_km, + z_km, + vx_km_s, + vy_km_s, + vz_km_s, + frame: frame.unwrap(), + stm: None, + }; + + traj.states.push(orbit); + } + Err(_) => { + // Probably a comment + continue; + } + }; + } + } + + Ok(traj) + } +} + +#[test] +fn test_load_oem_leo() { + use pretty_env_logger; + use std::env; + + // All three samples were taken from https://github.com/bradsease/oem/blob/main/tests/samples/real/ + let path: PathBuf = [ + env!("CARGO_MANIFEST_DIR"), + "data", + "tests", + "ccsds", + "oem", + "LEO_10s.oem", + ] + .iter() + .collect(); + + if pretty_env_logger::try_init().is_err() { + println!("could not init env_logger"); + } + + let leo_traj: Traj = Traj::::from_oem_file(path).unwrap(); + + assert_eq!(leo_traj.states.len(), 361); + assert_eq!(leo_traj.name.unwrap(), "TEST_OBJ".to_string()); +} + +#[test] +fn test_load_oem_meo() { + use pretty_env_logger; + use std::env; + + // All three samples were taken from https://github.com/bradsease/oem/blob/main/tests/samples/real/ + let path: PathBuf = [ + env!("CARGO_MANIFEST_DIR"), + "data", + "tests", + "ccsds", + "oem", + "MEO_60s.oem", + ] + .iter() + .collect(); + + if pretty_env_logger::try_init().is_err() { + println!("could not init env_logger"); + } + + let leo_traj = Traj::::from_oem_file(path).unwrap(); + + assert_eq!(leo_traj.states.len(), 61); + assert_eq!(leo_traj.name.unwrap(), "TEST_OBJ".to_string()); +} + +#[test] +fn test_load_oem_geo() { + use pretty_env_logger; + use std::env; + + // All three samples were taken from https://github.com/bradsease/oem/blob/main/tests/samples/real/ + let path: PathBuf = [ + env!("CARGO_MANIFEST_DIR"), + "data", + "tests", + "ccsds", + "oem", + "GEO_20s.oem", + ] + .iter() + .collect(); + + if pretty_env_logger::try_init().is_err() { + println!("could not init env_logger"); + } + + let leo_traj: Traj = Traj::::from_oem_file(path).unwrap(); + + assert_eq!(leo_traj.states.len(), 181); + assert_eq!(leo_traj.name.unwrap(), "TEST_OBJ".to_string()); +} diff --git a/src/md/trajectory/sc_traj.rs b/src/md/trajectory/sc_traj.rs new file mode 100644 index 00000000..059983f7 --- /dev/null +++ b/src/md/trajectory/sc_traj.rs @@ -0,0 +1,116 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use super::TrajError; +use super::{ExportCfg, Traj}; +use crate::cosmic::{Cosm, Frame, Orbit, Spacecraft}; +use crate::errors::NyxError; +use crate::md::prelude::StateParameter; +use crate::md::EventEvaluator; +use crate::time::{Duration, TimeUnits}; +use std::collections::HashMap; +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Instant; + +impl Traj { + /// Allows converting the source trajectory into the (almost) equivalent trajectory in another frame + #[allow(clippy::map_clone)] + pub fn to_frame(&self, new_frame: Frame, cosm: Arc) -> Result { + if self.states.is_empty() { + return Err(NyxError::Trajectory(TrajError::CreationError( + "No trajectory to convert".to_string(), + ))); + } + let start_instant = Instant::now(); + let mut traj = Self::new(); + for state in &self.states { + traj.states + .push(state.with_orbit(cosm.frame_chg(&state.orbit, new_frame))); + } + traj.finalize(); + + info!( + "Converted trajectory from {} to {} in {} ms: {traj}", + self.first().orbit.frame, + new_frame, + (Instant::now() - start_instant).as_millis() + ); + Ok(traj) + } + + /// A shortcut to `to_parquet_with_cfg` + pub fn to_parquet_with_step>( + &self, + path: P, + step: Duration, + ) -> Result<(), Box> { + self.to_parquet_with_cfg( + path, + ExportCfg { + step: Some(step), + ..Default::default() + }, + )?; + + Ok(()) + } + + /// Exports this trajectory to the provided filename in parquet format with only the epoch, the geodetic latitude, longitude, and height at one state per minute. + /// Must provide a body fixed frame to correctly compute the latitude and longitude. + #[allow(clippy::identity_op)] + pub fn to_groundtrack_parquet>( + &self, + path: P, + body_fixed_frame: Frame, + events: Option>>, + metadata: Option>, + cosm: Arc, + ) -> Result> { + let traj = self.to_frame(body_fixed_frame, cosm)?; + + let mut cfg = ExportCfg::default(); + cfg.append_field(StateParameter::GeodeticLatitude); + cfg.append_field(StateParameter::GeodeticLongitude); + cfg.append_field(StateParameter::GeodeticHeight); + cfg.append_field(StateParameter::Rmag); + cfg.set_step(1.minutes()); + cfg.metadata = metadata; + + traj.to_parquet(path, events, cfg) + } + + /// Convert this spacecraft trajectory into an Orbit trajectory, loosing all references to the spacecraft + pub fn downcast(&self) -> Traj { + let mut out = Traj::new(); + for sc_state in &self.states { + out.states.push(sc_state.orbit); + } + out + } + + /// Initialize a new spacecraft trajectory from the path to a CCSDS OEM file. + /// + /// CCSDS OEM only contains the orbit information, so you must provide a template spacecraft since we'll upcast the orbit trajectory into a spacecraft trajectory. + pub fn from_oem_file>(path: P, template: Spacecraft) -> Result { + let traj = Traj::::from_oem_file(path)?; + + Ok(traj.upcast(template)) + } +} diff --git a/src/md/trajectory/traj.rs b/src/md/trajectory/traj.rs index 12f2712a..658b2ec4 100644 --- a/src/md/trajectory/traj.rs +++ b/src/md/trajectory/traj.rs @@ -19,7 +19,6 @@ use super::traj_it::TrajIterator; use super::{ExportCfg, INTERPOLATION_SAMPLES}; use super::{Interpolatable, TrajError}; -use crate::cosmic::{Cosm, Frame, Orbit, Spacecraft}; use crate::errors::NyxError; use crate::io::watermark::pq_writer; use crate::linalg::allocator::Allocator; @@ -27,10 +26,9 @@ use crate::linalg::DefaultAllocator; use crate::md::prelude::{GuidanceMode, StateParameter}; use crate::md::EventEvaluator; use crate::time::{Duration, Epoch, TimeSeries, TimeUnits, Unit}; -use arrow::array::{ArrayRef, Float64Array, StringArray}; +use arrow::array::{Array, Float64Builder, StringBuilder}; use arrow::datatypes::{DataType, Field, Schema}; use arrow::record_batch::RecordBatch; -use hifitime::prelude::{Format, Formatter}; use parquet::arrow::ArrowWriter; use rayon::prelude::*; use std::collections::HashMap; @@ -40,10 +38,8 @@ use std::fs::File; use std::iter::Iterator; use std::ops; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::mpsc::channel; use std::sync::Arc; -use std::time::Instant; /// Store a trajectory of any State. #[derive(Clone, PartialEq)] @@ -443,6 +439,12 @@ where events: Option>>, cfg: ExportCfg, ) -> Result> { + let tick = Epoch::now().unwrap(); + info!("Exporting trajectory to parquet file..."); + + // Grab the path here before we move stuff. + let path_buf = cfg.actual_path(path); + // Build the schema let mut hdrs = vec![ Field::new("Epoch:Gregorian UTC", DataType::Utf8, false), @@ -482,140 +484,62 @@ where // Build the schema let schema = Arc::new(Schema::new(hdrs)); - let mut record = Vec::new(); - - // Build the states iterator - - if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() { - let start = if let Some(start) = cfg.start_epoch { - start - } else { - self.first().epoch() - }; - - let end = if let Some(end) = cfg.end_epoch { - end - } else { - self.last().epoch() - }; - - let step = if let Some(step) = cfg.step { - step - } else { - 1.minutes() - }; - - // Build all of the records - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - data.push(format!("{}", s.epoch())); - } - record.push(Arc::new(StringArray::from(data)) as ArrayRef); + let mut record: Vec> = Vec::new(); + + // Build the states iterator -- this does require copying the current states but I can't either get a reference or a copy of all the states. + let states = if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() { + // Must interpolate the data! + let start = cfg.start_epoch.unwrap_or_else(|| self.first().epoch()); + let end = cfg.end_epoch.unwrap_or_else(|| self.last().epoch()); + let step = cfg.step.unwrap_or_else(|| 1.minutes()); + self.every_between(step, start, end).collect::>() + } else { + self.states.to_vec() + }; - // TDB epoch - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - data.push(format!("{:x}", s.epoch())); - } - record.push(Arc::new(StringArray::from(data)) as ArrayRef); + // Build all of the records - // TAI Epoch seconds - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - data.push(s.epoch().to_tai_seconds()); - } - record.push(Arc::new(Float64Array::from(data)) as ArrayRef); - - // Add all of the fields - // This is super ugly, but I can't seem to convert the TrajIterator into an `Iter` - for field in fields { - if field == StateParameter::GuidanceMode { - // This is the only string field - record.push(Arc::new(StringArray::from({ - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - let mode = GuidanceMode::from(s.value(field).unwrap()); - - data.push(format!("{mode:?}")); - } - data - })) as ArrayRef); - } else { - record.push(Arc::new(Float64Array::from({ - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - data.push(s.value(field).unwrap()); - } - data - })) as ArrayRef); - } - } - // Add all of the evaluated events - if let Some(events) = events { - for event in events { - record.push(Arc::new(Float64Array::from({ - let mut data = Vec::new(); - for s in self.every_between(step, start, end) { - data.push(event.eval(&s)); - } - data - })) as ArrayRef); + // Epochs + let mut utc_epoch = StringBuilder::new(); + let mut tai_epoch = StringBuilder::new(); + let mut tai_s = Float64Builder::new(); + for s in &states { + utc_epoch.append_value(format!("{}", s.epoch())); + tai_epoch.append_value(format!("{:x}", s.epoch())); + tai_s.append_value(s.epoch().to_tai_seconds()); + } + record.push(Arc::new(utc_epoch.finish())); + record.push(Arc::new(tai_epoch.finish())); + record.push(Arc::new(tai_s.finish())); + + // Add all of the fields + for field in fields { + if field == StateParameter::GuidanceMode { + let mut guid_mode = StringBuilder::new(); + for s in &states { + guid_mode + .append_value(format!("{:?}", GuidanceMode::from(s.value(field).unwrap()))); } - } - } else { - // Build all of the records - record.push(Arc::new(StringArray::from( - self.states - .iter() - .map(|s| format!("{}", s.epoch())) - .collect::>(), - )) as ArrayRef); - - // TDB epoch - record.push(Arc::new(StringArray::from( - self.states - .iter() - .map(|s| format!("{:x}", s.epoch())) - .collect::>(), - )) as ArrayRef); - - // TDB Epoch seconds - record.push(Arc::new(Float64Array::from( - self.states - .iter() - .map(|s| s.epoch().to_tai_seconds()) - .collect::>(), - )) as ArrayRef); - - // Add all of the fields - for field in fields { - if field == StateParameter::GuidanceMode { - record.push(Arc::new(StringArray::from( - self.states - .iter() - .map(|s| format!("{:?}", GuidanceMode::from(s.value(field).unwrap()))) - .collect::>(), - )) as ArrayRef); - } else { - record.push(Arc::new(Float64Array::from( - self.states - .iter() - .map(|s| s.value(field).unwrap()) - .collect::>(), - )) as ArrayRef); + record.push(Arc::new(guid_mode.finish())); + } else { + let mut data = Float64Builder::new(); + for s in &states { + data.append_value(s.value(field).unwrap()); } + record.push(Arc::new(data.finish())); } + } + info!("Serialized {} states", states.len()); - // Add all of the evaluated events - if let Some(events) = events { - for event in events { - record.push(Arc::new(Float64Array::from({ - self.states - .iter() - .map(|s| event.eval(s)) - .collect::>() - })) as ArrayRef); + // Add all of the evaluated events + if let Some(events) = events { + info!("Evaluating {} event(s)", events.len()); + for event in events { + let mut data = Float64Builder::new(); + for s in &states { + data.append_value(event.eval(s)); } + record.push(Arc::new(data.finish())); } } @@ -630,24 +554,6 @@ where let props = pq_writer(Some(metadata)); - let mut path_buf = path.as_ref().to_path_buf(); - - if cfg.timestamp { - if let Some(file_name) = path_buf.file_name() { - if let Some(file_name_str) = file_name.to_str() { - if let Some(extension) = path_buf.extension() { - let stamp = Formatter::new( - Epoch::now().unwrap(), - Format::from_str("%Y-%m-%dT%H-%M-%S").unwrap(), - ); - let new_file_name = - format!("{file_name_str}-{stamp}.{}", extension.to_str().unwrap()); - path_buf.set_file_name(new_file_name); - } - } - } - }; - let file = File::create(&path_buf)?; let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap(); @@ -656,9 +562,32 @@ where writer.close()?; // Return the path this was written to - info!("Trajectory written to {}", path_buf.display()); + let tock_time = Epoch::now().unwrap() - tick; + info!( + "Trajectory written to {} in {tock_time}", + path_buf.display() + ); Ok(path_buf) } + + /// Allows resampling this trajectory at a fixed interval instead of using the propagator step size. + /// This may lead to aliasing due to the Nyquist–Shannon sampling theorem. + pub fn resample(&self, step: Duration) -> Result { + if self.states.is_empty() { + return Err(NyxError::Trajectory(TrajError::CreationError( + "No trajectory to convert".to_string(), + ))); + } + + let mut traj = Self::new(); + for state in self.every(step) { + traj.states.push(state); + } + + traj.finalize(); + + Ok(traj) + } } impl ops::Add for Traj @@ -728,125 +657,6 @@ where } } -impl Traj { - /// Allows converting the source trajectory into the (almost) equivalent trajectory in another frame. - /// This simply converts each state into the other frame and may lead to aliasing due to the Nyquist–Shannon sampling theorem. - #[allow(clippy::map_clone)] - pub fn to_frame(&self, new_frame: Frame, cosm: Arc) -> Result { - if self.states.is_empty() { - return Err(NyxError::Trajectory(TrajError::CreationError( - "No trajectory to convert".to_string(), - ))); - } - let start_instant = Instant::now(); - let mut traj = Self::new(); - for state in &self.states { - traj.states.push(cosm.frame_chg(state, new_frame)); - } - traj.finalize(); - - info!( - "Converted trajectory from {} to {} in {} ms: {traj}", - self.first().frame, - new_frame, - (Instant::now() - start_instant).as_millis() - ); - Ok(traj) - } - - /// Exports this trajectory to the provided filename in parquet format with only the epoch, the geodetic latitude, longitude, and height at one state per minute. - /// Must provide a body fixed frame to correctly compute the latitude and longitude. - #[allow(clippy::identity_op)] - pub fn to_groundtrack_parquet>( - &self, - path: P, - body_fixed_frame: Frame, - events: Option>>, - metadata: Option>, - cosm: Arc, - ) -> Result> { - let traj = self.to_frame(body_fixed_frame, cosm)?; - - let mut cfg = ExportCfg::default(); - cfg.append_field(StateParameter::GeodeticLatitude); - cfg.append_field(StateParameter::GeodeticLongitude); - cfg.append_field(StateParameter::GeodeticHeight); - cfg.append_field(StateParameter::Rmag); - cfg.set_step(1.minutes()); - cfg.metadata = metadata; - - traj.to_parquet(path, events, cfg) - } -} - -impl Traj { - /// Allows converting the source trajectory into the (almost) equivalent trajectory in another frame - #[allow(clippy::map_clone)] - pub fn to_frame(&self, new_frame: Frame, cosm: Arc) -> Result { - if self.states.is_empty() { - return Err(NyxError::Trajectory(TrajError::CreationError( - "No trajectory to convert".to_string(), - ))); - } - let start_instant = Instant::now(); - let mut traj = Self::new(); - for state in &self.states { - traj.states - .push(state.with_orbit(cosm.frame_chg(&state.orbit, new_frame))); - } - traj.finalize(); - - info!( - "Converted trajectory from {} to {} in {} ms: {traj}", - self.first().orbit.frame, - new_frame, - (Instant::now() - start_instant).as_millis() - ); - Ok(traj) - } - - /// A shortcut to `to_parquet_with_csv` - pub fn to_parquet_with_step>( - &self, - path: P, - step: Duration, - ) -> Result<(), Box> { - self.to_parquet_with_cfg( - path, - ExportCfg { - step: Some(step), - ..Default::default() - }, - )?; - - Ok(()) - } - - /// Exports this trajectory to the provided filename in parquet format with only the epoch, the geodetic latitude, longitude, and height at one state per minute. - /// Must provide a body fixed frame to correctly compute the latitude and longitude. - #[allow(clippy::identity_op)] - pub fn to_groundtrack_parquet>( - &self, - path: P, - body_fixed_frame: Frame, - events: Option>>, - metadata: Option>, - cosm: Arc, - ) -> Result> { - let traj = self.to_frame(body_fixed_frame, cosm)?; - - let mut cfg = ExportCfg::default(); - cfg.append_field(StateParameter::GeodeticLatitude); - cfg.append_field(StateParameter::GeodeticLongitude); - cfg.append_field(StateParameter::GeodeticHeight); - cfg.append_field(StateParameter::Rmag); - cfg.set_step(1.minutes()); - cfg.metadata = metadata; - - traj.to_parquet(path, events, cfg) - } -} - impl fmt::Display for Traj where DefaultAllocator: diff --git a/src/md/trajectory/traj_it.rs b/src/md/trajectory/traj_it.rs index d3ae16ff..f8563824 100644 --- a/src/md/trajectory/traj_it.rs +++ b/src/md/trajectory/traj_it.rs @@ -46,16 +46,14 @@ where if next_epoch >= self.traj.first().epoch() && next_epoch <= self.traj.last().epoch() { + let msg = format!( + "!!! [BUG] TrajIterator: {e} not found but should be present in {} !", + self.traj + ); if log_enabled!(log::Level::Error) { - error!("[!!!] BUG [!!!]"); - error!("[!!!]\t{e}\t[!!!]"); - error!("[!!!]\t{}\t[!!!]", self.traj); - error!("[!!!] [!!!]"); + error!("{msg}"); } else { - println!("[!!!] BUG [!!!]"); - println!("[!!!]\t{e}\t[!!!]"); - println!("[!!!]\t{}\t[!!!]", self.traj); - println!("[!!!] [!!!]"); + println!("{msg}"); }; } None diff --git a/src/od/estimate.rs b/src/od/estimate/kfestimate.rs similarity index 58% rename from src/od/estimate.rs rename to src/od/estimate/kfestimate.rs index 7f8a6bdc..5ffd3cd3 100644 --- a/src/od/estimate.rs +++ b/src/od/estimate/kfestimate.rs @@ -16,10 +16,8 @@ along with this program. If not, see . */ -use super::State; -use super::{CovarFormat, EpochFormat}; +use super::{Estimate, State}; use crate::cosmic::Orbit; -use crate::hifitime::Epoch; use crate::linalg::allocator::Allocator; use crate::linalg::{DefaultAllocator, DimName, Matrix, OMatrix, OVector, Vector6, U6}; use crate::mc::GaussianGenerator; @@ -27,102 +25,9 @@ use crate::md::StateParameter; use rand::SeedableRng; use rand_distr::Distribution; use rand_pcg::Pcg64Mcg; -use serde::ser::SerializeSeq; -use serde::{Serialize, Serializer}; use std::cmp::PartialEq; use std::fmt; -/// Stores an Estimate, as the result of a `time_update` or `measurement_update`. -pub trait Estimate -where - Self: Clone + PartialEq + Sized + fmt::Display, - DefaultAllocator: Allocator::Size> - + Allocator::Size, ::Size> - + Allocator::VecLength>, -{ - /// An empty estimate. This is useful if wanting to store an estimate outside the scope of a filtering loop. - fn zeros(state: T) -> Self; - /// Epoch of this Estimate - fn epoch(&self) -> Epoch { - self.state().epoch() - } - // Sets the epoch - fn set_epoch(&mut self, dt: Epoch) { - self.state().set_epoch(dt); - } - /// The estimated state - fn state(&self) -> T { - self.nominal_state().add(self.state_deviation()) - } - /// The state deviation as computed by the filter. - fn state_deviation(&self) -> OVector::Size>; - /// The nominal state as reported by the filter dynamics - fn nominal_state(&self) -> T; - /// The Covariance of this estimate. Will return the predicted covariance if this is a time update/prediction. - fn covar(&self) -> OMatrix::Size, ::Size>; - /// The predicted covariance of this estimate from the time update - fn predicted_covar(&self) -> OMatrix::Size, ::Size>; - /// Sets the state deviation. - fn set_state_deviation(&mut self, new_state: OVector::Size>); - /// Sets the Covariance of this estimate - fn set_covar(&mut self, new_covar: OMatrix::Size, ::Size>); - /// Whether or not this is a predicted estimate from a time update, or an estimate from a measurement - fn predicted(&self) -> bool; - /// The STM used to compute this Estimate - fn stm(&self) -> &OMatrix::Size, ::Size>; - /// The Epoch format upon serialization - fn epoch_fmt(&self) -> EpochFormat; - /// The covariance format upon serialization - fn covar_fmt(&self) -> CovarFormat; - /// Returns whether this estimate is within some bound - /// The 68-95-99.7 rule is a good way to assess whether the filter is operating normally - fn within_sigma(&self, sigma: f64) -> bool { - let state = self.state_deviation(); - let covar = self.covar(); - for i in 0..state.len() { - let bound = covar[(i, i)].sqrt() * sigma; - if state[i] > bound || state[i] < -bound { - return false; - } - } - true - } - /// Returns whether this estimate is within 3 sigma, which represent 99.7% for a Normal distribution - fn within_3sigma(&self) -> bool { - self.within_sigma(3.0) - } - /// Returns the header - fn header(epoch_fmt: EpochFormat, covar_fmt: CovarFormat) -> Vec { - let dim = ::Size::dim(); - let mut hdr_v = Vec::with_capacity(3 * dim + 1); - hdr_v.push(format!("{epoch_fmt}")); - for i in 0..dim { - hdr_v.push(format!("state_{i}")); - } - // Serialize the covariance - for i in 0..dim { - for j in 0..dim { - hdr_v.push(format!("{covar_fmt}_{i}_{j}")); - } - } - hdr_v - } - /// Returns the default header - fn default_header() -> Vec { - Self::header(EpochFormat::GregorianUtc, CovarFormat::Sqrt) - } - - /// Returns the covariance element at position (i, j) formatted with this estimate's covariance formatter - fn covar_ij(&self, i: usize, j: usize) -> f64 { - match self.covar_fmt() { - CovarFormat::Sqrt => self.covar()[(i, j)].sqrt(), - CovarFormat::Sigma1 => self.covar()[(i, j)], - CovarFormat::Sigma3 => self.covar()[(i, j)] * 3.0, - CovarFormat::MulSigma(x) => self.covar()[(i, j)] * x, - } - } -} - /// Kalman filter Estimate #[derive(Debug, Copy, Clone, PartialEq)] pub struct KfEstimate @@ -147,10 +52,6 @@ where pub predicted: bool, /// The STM used to compute this Estimate pub stm: OMatrix::Size, ::Size>, - /// The Epoch format upon serialization - pub epoch_fmt: EpochFormat, - /// The covariance format upon serialization - pub covar_fmt: CovarFormat, } impl KfEstimate @@ -175,8 +76,6 @@ where covar_bar: covar, predicted: true, stm: OMatrix::::Size, ::Size>::identity(), - epoch_fmt: EpochFormat::GregorianUtc, - covar_fmt: CovarFormat::Sqrt, } } @@ -190,8 +89,6 @@ where covar_bar: covar, predicted: true, stm: OMatrix::::Size, ::Size>::identity(), - epoch_fmt: EpochFormat::GregorianUtc, - covar_fmt: CovarFormat::Sqrt, } } } @@ -239,8 +136,6 @@ impl KfEstimate { covar_bar: covar, predicted: true, stm: OMatrix::::identity(), - epoch_fmt: EpochFormat::GregorianUtc, - covar_fmt: CovarFormat::Sqrt, } } } @@ -263,8 +158,6 @@ where covar_bar: OMatrix::::Size, ::Size>::zeros(), predicted: true, stm: OMatrix::::Size, ::Size>::identity(), - epoch_fmt: EpochFormat::GregorianUtc, - covar_fmt: CovarFormat::Sqrt, } } @@ -290,12 +183,6 @@ where fn stm(&self) -> &OMatrix::Size, ::Size> { &self.stm } - fn epoch_fmt(&self) -> EpochFormat { - self.epoch_fmt - } - fn covar_fmt(&self) -> CovarFormat { - self.covar_fmt - } fn set_state_deviation(&mut self, new_state: OVector::Size>) { self.state_deviation = new_state; } @@ -356,84 +243,11 @@ where } } -impl Serialize for KfEstimate -where - DefaultAllocator: Allocator::Size> - + Allocator::Size, ::Size> - + Allocator::Size> - + Allocator::VecLength> - + Allocator::Size, ::Size>, - ::Size>>::Buffer: Copy, - ::Size, ::Size>>::Buffer: Copy, -{ - /// Serializes the estimate - fn serialize(&self, serializer: O) -> Result - where - O: Serializer, - { - let dim = ::Size::dim(); - let mut seq = serializer.serialize_seq(Some(dim * 3 + 1))?; - match self.epoch_fmt { - EpochFormat::GregorianUtc => seq.serialize_element(&format!("{}", self.epoch()))?, - EpochFormat::GregorianTai => seq.serialize_element(&format!("{}", self.epoch()))?, - EpochFormat::MjdTai => seq.serialize_element(&self.epoch().to_mjd_tai_days())?, - EpochFormat::MjdTt => seq.serialize_element(&self.epoch().to_mjd_tt_days())?, - EpochFormat::MjdUtc => seq.serialize_element(&self.epoch().to_mjd_utc_days())?, - EpochFormat::JdeEt => seq.serialize_element(&self.epoch().to_jde_et_days())?, - EpochFormat::JdeTai => seq.serialize_element(&self.epoch().to_jde_tai_days())?, - EpochFormat::JdeTt => seq.serialize_element(&self.epoch().to_jde_tt_days())?, - EpochFormat::JdeUtc => seq.serialize_element(&self.epoch().to_jde_utc_days())?, - EpochFormat::TaiSecs(e) => { - seq.serialize_element(&(self.epoch().to_tai_seconds() - e))? - } - EpochFormat::TaiDays(e) => seq.serialize_element(&(self.epoch().to_tai_days() - e))?, - } - // Serialize the state - for i in 0..dim { - seq.serialize_element(&self.state_deviation[i])?; - } - // Serialize the covariance - for i in 0..dim { - for j in 0..dim { - let ser_covar = match self.covar_fmt { - CovarFormat::Sqrt => self.covar[(i, j)].sqrt(), - CovarFormat::Sigma1 => self.covar[(i, j)], - CovarFormat::Sigma3 => self.covar[(i, j)] * 3.0, - CovarFormat::MulSigma(x) => self.covar[(i, j)] * x, - }; - seq.serialize_element(&ser_covar)?; - } - } - seq.end() - } -} - -/// A trait to store a navigation solution, can be used in conjunction with KfEstimate -pub trait NavSolution: Estimate -where - T: State, - DefaultAllocator: Allocator::Size> - + Allocator::Size, ::Size> - + Allocator::VecLength>, -{ - fn orbital_state(&self) -> Orbit; - /// Returns the nominal state as computed by the dynamics - fn expected_state(&self) -> Orbit; -} - -impl NavSolution for KfEstimate { - fn orbital_state(&self) -> Orbit { - self.state() - } - fn expected_state(&self) -> Orbit { - self.nominal_state() - } -} - #[test] fn test_estimate_from_disp() { use crate::cosmic::Cosm; use crate::utils::rss_orbit_errors; + use hifitime::Epoch; let cosm = Cosm::de438(); let eme2k = cosm.frame("EME2000"); diff --git a/src/od/estimate/mod.rs b/src/od/estimate/mod.rs new file mode 100644 index 00000000..a4f291d4 --- /dev/null +++ b/src/od/estimate/mod.rs @@ -0,0 +1,109 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use super::State; +use crate::cosmic::Orbit; +use crate::hifitime::Epoch; +use crate::linalg::allocator::Allocator; +use crate::linalg::{DefaultAllocator, OMatrix, OVector}; +use std::cmp::PartialEq; +use std::fmt; + +pub mod residual; +pub use residual::Residual; +pub mod kfestimate; +pub use kfestimate::KfEstimate; + +/// Stores an Estimate, as the result of a `time_update` or `measurement_update`. +pub trait Estimate +where + Self: Clone + PartialEq + Sized + fmt::Display, + DefaultAllocator: Allocator::Size> + + Allocator::Size, ::Size> + + Allocator::VecLength>, +{ + /// An empty estimate. This is useful if wanting to store an estimate outside the scope of a filtering loop. + fn zeros(state: T) -> Self; + /// Epoch of this Estimate + fn epoch(&self) -> Epoch { + self.state().epoch() + } + // Sets the epoch + fn set_epoch(&mut self, dt: Epoch) { + self.state().set_epoch(dt); + } + /// The estimated state + fn state(&self) -> T { + self.nominal_state().add(self.state_deviation()) + } + /// The state deviation as computed by the filter. + fn state_deviation(&self) -> OVector::Size>; + /// The nominal state as reported by the filter dynamics + fn nominal_state(&self) -> T; + /// The Covariance of this estimate. Will return the predicted covariance if this is a time update/prediction. + fn covar(&self) -> OMatrix::Size, ::Size>; + /// The predicted covariance of this estimate from the time update + fn predicted_covar(&self) -> OMatrix::Size, ::Size>; + /// Sets the state deviation. + fn set_state_deviation(&mut self, new_state: OVector::Size>); + /// Sets the Covariance of this estimate + fn set_covar(&mut self, new_covar: OMatrix::Size, ::Size>); + /// Whether or not this is a predicted estimate from a time update, or an estimate from a measurement + fn predicted(&self) -> bool; + /// The STM used to compute this Estimate + fn stm(&self) -> &OMatrix::Size, ::Size>; + /// Returns whether this estimate is within some bound + /// The 68-95-99.7 rule is a good way to assess whether the filter is operating normally + fn within_sigma(&self, sigma: f64) -> bool { + let state = self.state_deviation(); + let covar = self.covar(); + for i in 0..state.len() { + let bound = covar[(i, i)].sqrt() * sigma; + if state[i] > bound || state[i] < -bound { + return false; + } + } + true + } + /// Returns whether this estimate is within 3 sigma, which represent 99.7% for a Normal distribution + fn within_3sigma(&self) -> bool { + self.within_sigma(3.0) + } +} + +/// A trait to store a navigation solution, can be used in conjunction with KfEstimate +pub trait NavSolution: Estimate +where + T: State, + DefaultAllocator: Allocator::Size> + + Allocator::Size, ::Size> + + Allocator::VecLength>, +{ + fn orbital_state(&self) -> Orbit; + /// Returns the nominal state as computed by the dynamics + fn expected_state(&self) -> Orbit; +} + +impl NavSolution for KfEstimate { + fn orbital_state(&self) -> Orbit { + self.state() + } + fn expected_state(&self) -> Orbit { + self.nominal_state() + } +} diff --git a/src/od/estimate/param.rs b/src/od/estimate/param.rs new file mode 100644 index 00000000..292bbc1d --- /dev/null +++ b/src/od/estimate/param.rs @@ -0,0 +1,274 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use crate::md::StateParameter; + +use super::EpochFormat; +use std::cmp::PartialEq; +use std::fmt; + +/// Allowed headers, with an optional frame. +/// TODO: Support units +#[allow(non_camel_case_types)] +#[derive(Clone, Debug, PartialEq)] +pub enum EstimateParameter { + /// The epoch in the specified format + Epoch(EpochFormat), + /// Parameters of the estimated state + EstimatedState(Vec), + /// Parameters of the nominal state + NominalState(Vec), + /// Orbit deviation X (km) + Delta_x, + /// Orbit deviation Y (km) + Delta_y, + /// Orbit deviation Z (km) + Delta_z, + /// Orbit deviation VX (km/s) + Delta_vx, + /// Orbit deviation VY (km/s) + Delta_vy, + /// Orbit deviation VZ (km/s) + Delta_vz, + /// Covariance matrix [1,1] + Cx_x { frame: Option }, + /// Covariance matrix [2,1] + Cy_x { frame: Option }, + /// Covariance matrix [2,2] + Cy_y { frame: Option }, + /// Covariance matrix [3,1] + Cz_x { frame: Option }, + /// Covariance matrix [3,2] + Cz_y { frame: Option }, + /// Covariance matrix [3,3] + Cz_z { frame: Option }, + /// Covariance matrix [4,1] + Cx_dot_x { frame: Option }, + /// Covariance matrix [4,2] + Cx_dot_y { frame: Option }, + /// Covariance matrix [4,3] + Cx_dot_z { frame: Option }, + /// Covariance matrix [4,4] + Cx_dot_x_dot { frame: Option }, + /// Covariance matrix [5,1] + Cy_dot_x { frame: Option }, + /// Covariance matrix [5,2] + Cy_dot_y { frame: Option }, + /// Covariance matrix [5,3] + Cy_dot_z { frame: Option }, + /// Covariance matrix [5,4] + Cy_dot_x_dot { frame: Option }, + /// Covariance matrix [5,5] + Cy_dot_y_dot { frame: Option }, + /// Covariance matrix [6,1] + Cz_dot_x { frame: Option }, + /// Covariance matrix [6,2] + Cz_dot_y { frame: Option }, + /// Covariance matrix [6,3] + Cz_dot_z { frame: Option }, + /// Covariance matrix [6,4] + Cz_dot_x_dot { frame: Option }, + /// Covariance matrix [6,5] + Cz_dot_y_dot { frame: Option }, + /// Covariance matrix [6,6] + Cz_dot_z_dot { frame: Option }, + /// Boolean specifying whether this is a prediction or not + Prediction, + /// Norm of the position items of the covariance (Cx_x, Cy_y, Cz_z) + Covar_pos, + /// Norm of the velocity items of the covariance (Cx_dot_x_dot, Cy_dot_y_dot, Cz_dot_z_dot) + Covar_vel, +} + +impl fmt::Display for EstimateParameter { + fn fmt(&self, fh: &mut fmt::Formatter) -> fmt::Result { + match self { + EstimateParameter::Epoch(efmt) => write!(fh, "Epoch:{efmt:?}"), + EstimateParameter::EstimatedState(hdr) => { + let mut seq = Vec::with_capacity(hdr.len()); + for element in hdr { + seq.push(format!("Estimate:{element}")); + } + write!(fh, "{}", seq.join(",")) + } + EstimateParameter::NominalState(hdr) => { + let mut seq = Vec::with_capacity(hdr.len()); + for element in hdr { + seq.push(format!("Nominal:{element}")); + } + write!(fh, "{}", seq.join(",")) + } + EstimateParameter::Delta_x => write!(fh, "delta_x"), + EstimateParameter::Delta_y => write!(fh, "delta_y"), + EstimateParameter::Delta_z => write!(fh, "delta_z"), + EstimateParameter::Delta_vx => write!(fh, "delta_vx"), + EstimateParameter::Delta_vy => write!(fh, "delta_vy"), + EstimateParameter::Delta_vz => write!(fh, "delta_vz"), + EstimateParameter::Cx_x { frame } => { + if let Some(f) = frame { + write!(fh, "cx_x:{f}") + } else { + write!(fh, "cx_x") + } + } + EstimateParameter::Cy_x { frame } => { + if let Some(f) = frame { + write!(fh, "cy_x:{f}") + } else { + write!(fh, "cy_x") + } + } + EstimateParameter::Cy_y { frame } => { + if let Some(f) = frame { + write!(fh, "cy_y:{f}") + } else { + write!(fh, "cy_y") + } + } + EstimateParameter::Cz_x { frame } => { + if let Some(f) = frame { + write!(fh, "cz_x:{f}") + } else { + write!(fh, "cz_x") + } + } + EstimateParameter::Cz_y { frame } => { + if let Some(f) = frame { + write!(fh, "cz_y:{f}") + } else { + write!(fh, "cz_y") + } + } + EstimateParameter::Cz_z { frame } => { + if let Some(f) = frame { + write!(fh, "cz_z:{f}") + } else { + write!(fh, "cz_z") + } + } + EstimateParameter::Cx_dot_x { frame } => { + if let Some(f) = frame { + write!(fh, "cx_dot_x:{f}") + } else { + write!(fh, "cx_dot_x") + } + } + EstimateParameter::Cx_dot_y { frame } => { + if let Some(f) = frame { + write!(fh, "cx_dot_y:{f}") + } else { + write!(fh, "cx_dot_y") + } + } + EstimateParameter::Cx_dot_z { frame } => { + if let Some(f) = frame { + write!(fh, "cx_dot_z:{f}") + } else { + write!(fh, "cx_dot_z") + } + } + EstimateParameter::Cx_dot_x_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cx_dot_x_dot:{f}") + } else { + write!(fh, "cx_dot_x_dot") + } + } + EstimateParameter::Cy_dot_x { frame } => { + if let Some(f) = frame { + write!(fh, "cy_dot_x:{f}") + } else { + write!(fh, "cy_dot_x") + } + } + EstimateParameter::Cy_dot_y { frame } => { + if let Some(f) = frame { + write!(fh, "cy_dot_y:{f}") + } else { + write!(fh, "cy_dot_y") + } + } + EstimateParameter::Cy_dot_z { frame } => { + if let Some(f) = frame { + write!(fh, "cy_dot_z:{f}") + } else { + write!(fh, "cy_dot_z") + } + } + EstimateParameter::Cy_dot_x_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cy_dot_x_dot:{f}") + } else { + write!(fh, "cy_dot_x_dot") + } + } + EstimateParameter::Cy_dot_y_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cy_dot_y_dot:{f}") + } else { + write!(fh, "cy_dot_y_dot") + } + } + EstimateParameter::Cz_dot_x { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_x:{f}") + } else { + write!(fh, "cz_dot_x") + } + } + EstimateParameter::Cz_dot_y { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_y:{f}") + } else { + write!(fh, "cz_dot_y") + } + } + EstimateParameter::Cz_dot_z { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_z:{f}") + } else { + write!(fh, "cz_dot_z") + } + } + EstimateParameter::Cz_dot_x_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_x_dot:{f}") + } else { + write!(fh, "cz_dot_x_dot") + } + } + EstimateParameter::Cz_dot_y_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_y_dot:{f}") + } else { + write!(fh, "cz_dot_y_dot") + } + } + EstimateParameter::Cz_dot_z_dot { frame } => { + if let Some(f) = frame { + write!(fh, "cz_dot_z_dot:{f}") + } else { + write!(fh, "cz_dot_z_dot") + } + } + EstimateParameter::Prediction => write!(fh, "prediction"), + EstimateParameter::Covar_pos => write!(fh, "covar_position"), + EstimateParameter::Covar_vel => write!(fh, "covar_velocity"), + } + } +} diff --git a/src/od/estimate/residual.rs b/src/od/estimate/residual.rs new file mode 100644 index 00000000..c6971b37 --- /dev/null +++ b/src/od/estimate/residual.rs @@ -0,0 +1,104 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use crate::hifitime::Epoch; +use crate::linalg::allocator::Allocator; +use crate::linalg::{DefaultAllocator, DimName, OVector}; +use std::fmt; + +/// Stores an Estimate, as the result of a `time_update` or `measurement_update`. +#[derive(Debug, Clone, PartialEq)] +pub struct Residual +where + M: DimName, + DefaultAllocator: Allocator, +{ + /// Date time of this Residual + pub epoch: Epoch, + /// The prefit residual in the units of the measurement type + pub prefit: OVector, + /// The postfit residual in the units of the measurement type + pub postfit: OVector, + /// The prefit residual ratio, i.e. `r' * (H*P*H')^-1 * r`, where `r` is the prefit residual, `H` is the sensitivity matrix, and `P` is the covariance matrix. + pub ratio: f64, + /// Whether or not this was rejected + pub rejected: bool, +} + +impl Residual +where + M: DimName, + DefaultAllocator: Allocator, +{ + /// An empty estimate. This is useful if wanting to store an estimate outside the scope of a filtering loop. + pub fn zeros() -> Self { + Self { + epoch: Epoch::from_tai_seconds(0.0), + prefit: OVector::::zeros(), + postfit: OVector::::zeros(), + ratio: 0.0, + rejected: true, + } + } + + /// Flags a Residual as rejected. + pub fn rejected(epoch: Epoch, prefit: OVector, ratio: f64) -> Self { + Self { + epoch, + prefit, + postfit: OVector::::zeros(), + ratio, + rejected: true, + } + } + + pub fn new( + epoch: Epoch, + prefit: OVector, + postfit: OVector, + ratio: f64, + ) -> Self { + Self { + epoch, + prefit, + postfit, + ratio, + rejected: false, + } + } +} + +impl fmt::Display for Residual +where + M: DimName, + DefaultAllocator: Allocator + Allocator, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Prefit {} Postfit {}", &self.prefit, &self.postfit) + } +} + +impl fmt::LowerExp for Residual +where + M: DimName, + DefaultAllocator: Allocator + Allocator, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Prefit {:e} Postfit {:e}", &self.prefit, &self.postfit) + } +} diff --git a/src/od/filter/kalman.rs b/src/od/filter/kalman.rs new file mode 100644 index 00000000..caaa0c65 --- /dev/null +++ b/src/od/filter/kalman.rs @@ -0,0 +1,444 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use crate::linalg::allocator::Allocator; +use crate::linalg::{DefaultAllocator, DimName, OMatrix, OVector, U3}; + +pub use crate::errors::NyxError; +pub use crate::od::estimate::{Estimate, KfEstimate, Residual}; +pub use crate::od::snc::SNC; +use crate::od::{Filter, State}; +pub use crate::time::{Epoch, Unit}; + +/// Defines both a Classical and an Extended Kalman filter (CKF and EKF) +/// T: Type of state +/// A: Acceleration size (for SNC) +/// M: Measurement size (used for the sensitivity matrix) +#[derive(Debug, Clone)] +#[allow(clippy::upper_case_acronyms)] +pub struct KF +where + A: DimName, + M: DimName, + T: State, + DefaultAllocator: Allocator + + Allocator::Size> + + Allocator::VecLength> + + Allocator + + Allocator + + Allocator::Size> + + Allocator::Size, ::Size> + + Allocator + + Allocator::Size, A> + + Allocator::Size> + + Allocator::Size> + + Allocator::Size, ::Size>, + ::Size>>::Buffer: Copy, + ::Size, ::Size>>::Buffer: Copy, +{ + /// The previous estimate used in the KF computations. + pub prev_estimate: KfEstimate, + /// Sets the Measurement noise (usually noted R) + pub measurement_noise: OMatrix, + /// A sets of process noise (usually noted Q), must be ordered chronologically + pub process_noise: Vec>, + /// Determines whether this KF should operate as a Conventional/Classical Kalman filter or an Extended Kalman Filter. + /// Recall that one should switch to an Extended KF only once the estimate is good (i.e. after a few good measurement updates on a CKF). + pub ekf: bool, + h_tilde: OMatrix::Size>, + h_tilde_updated: bool, + prev_used_snc: usize, +} + +impl KF +where + A: DimName, + M: DimName, + T: State, + DefaultAllocator: Allocator + + Allocator::Size> + + Allocator::VecLength> + + Allocator + + Allocator + + Allocator::Size> + + Allocator::Size, M> + + Allocator::Size, ::Size> + + Allocator + + Allocator::Size, A> + + Allocator::Size> + + Allocator::Size> + + Allocator::Size, ::Size>, + ::Size>>::Buffer: Copy, + ::Size, ::Size>>::Buffer: Copy, +{ + /// Initializes this KF with an initial estimate, measurement noise, and one process noise + pub fn new( + initial_estimate: KfEstimate, + process_noise: SNC, + measurement_noise: OMatrix, + ) -> Self { + assert_eq!( + A::dim() % 3, + 0, + "SNC can only be applied to accelerations multiple of 3" + ); + + // Set the initial epoch of the SNC + let mut process_noise = process_noise; + process_noise.init_epoch = Some(initial_estimate.epoch()); + + Self { + prev_estimate: initial_estimate, + measurement_noise, + process_noise: vec![process_noise], + ekf: false, + h_tilde: OMatrix::::Size>::zeros(), + h_tilde_updated: false, + prev_used_snc: 0, + } + } + + /// Initializes this KF with an initial estimate, measurement noise, and several process noise + /// WARNING: SNCs MUST be ordered chronologically! They will be selected automatically by walking + /// the list of SNCs backward until one can be applied! + pub fn with_sncs( + initial_estimate: KfEstimate, + process_noises: Vec>, + measurement_noise: OMatrix, + ) -> Self { + assert_eq!( + A::dim() % 3, + 0, + "SNC can only be applied to accelerations multiple of 3" + ); + let mut process_noises = process_noises; + // Set the initial epoch of the SNC + for snc in &mut process_noises { + snc.init_epoch = Some(initial_estimate.epoch()); + } + + Self { + prev_estimate: initial_estimate, + measurement_noise, + process_noise: process_noises, + ekf: false, + h_tilde: OMatrix::::Size>::zeros(), + h_tilde_updated: false, + prev_used_snc: 0, + } + } +} + +impl KF +where + M: DimName, + T: State, + DefaultAllocator: Allocator + + Allocator::Size> + + Allocator::VecLength> + + Allocator + + Allocator::Size> + + Allocator::Size, M> + + Allocator::Size, ::Size> + + Allocator + + Allocator::Size, U3> + + Allocator::Size> + + Allocator::Size> + + Allocator::Size, ::Size>, + ::Size>>::Buffer: Copy, + ::Size, ::Size>>::Buffer: Copy, +{ + /// Initializes this KF without SNC + pub fn no_snc(initial_estimate: KfEstimate, measurement_noise: OMatrix) -> Self { + Self { + prev_estimate: initial_estimate, + measurement_noise, + process_noise: Vec::new(), + ekf: false, + h_tilde: OMatrix::::Size>::zeros(), + h_tilde_updated: false, + prev_used_snc: 0, + } + } +} + +impl Filter for KF +where + A: DimName, + M: DimName, + T: State, + DefaultAllocator: Allocator + + Allocator::Size> + + Allocator::VecLength> + + Allocator + + Allocator + + Allocator::Size> + + Allocator::Size, M> + + Allocator::Size, ::Size> + + Allocator + + Allocator::Size, A> + + Allocator::Size> + + Allocator::Size> + + Allocator::Size, ::Size> + + Allocator, M>, + ::Size>>::Buffer: Copy, + ::Size, ::Size>>::Buffer: Copy, +{ + type Estimate = KfEstimate; + + fn measurement_noise(&self, _epoch: Epoch) -> &OMatrix { + &self.measurement_noise + } + + /// Returns the previous estimate + fn previous_estimate(&self) -> &Self::Estimate { + &self.prev_estimate + } + + fn set_previous_estimate(&mut self, est: &Self::Estimate) { + self.prev_estimate = *est; + } + + /// Update the sensitivity matrix (or "H tilde"). This function **must** be called prior to each + /// call to `measurement_update`. + fn update_h_tilde(&mut self, h_tilde: OMatrix::Size>) { + self.h_tilde = h_tilde; + self.h_tilde_updated = true; + } + + /// Computes a time update/prediction (i.e. advances the filter estimate with the updated STM). + /// + /// May return a FilterError if the STM was not updated. + fn time_update(&mut self, nominal_state: T) -> Result { + let stm = nominal_state.stm()?; + let mut covar_bar = stm * self.prev_estimate.covar * stm.transpose(); + + // Try to apply an SNC, if applicable + for (i, snc) in self.process_noise.iter().enumerate().rev() { + if let Some(snc_matrix) = snc.to_matrix(nominal_state.epoch()) { + // Check if we're using another SNC than the one before + if self.prev_used_snc != i { + info!("Switched to {}-th {}", i, snc); + self.prev_used_snc = i; + } + + // Let's compute the Gamma matrix, an approximation of the time integral + // which assumes that the acceleration is constant between these two measurements. + let mut gamma = OMatrix::::Size, A>::zeros(); + let delta_t = (nominal_state.epoch() - self.prev_estimate.epoch()).to_seconds(); + for blk in 0..A::dim() / 3 { + for i in 0..3 { + let idx_i = i + A::dim() * blk; + let idx_j = i + 3 * blk; + let idx_k = i + 3 + A::dim() * blk; + // For first block + // (0, 0) (1, 1) (2, 2) <=> \Delta t^2/2 + // (3, 0) (4, 1) (5, 2) <=> \Delta t + // Second block + // (6, 3) (7, 4) (8, 5) <=> \Delta t^2/2 + // (9, 3) (10, 4) (11, 5) <=> \Delta t + // * \Delta t^2/2 + // (i, i) when blk = 0 + // (i + A::dim() * blk, i + 3) when blk = 1 + // (i + A::dim() * blk, i + 3 * blk) + // * \Delta t + // (i + 3, i) when blk = 0 + // (i + 3, i + 9) when blk = 1 (and I think i + 12 + 3) + // (i + 3 + A::dim() * blk, i + 3 * blk) + gamma[(idx_i, idx_j)] = delta_t.powi(2) / 2.0; + gamma[(idx_k, idx_j)] = delta_t; + } + } + // Let's add the process noise + covar_bar += &gamma * snc_matrix * &gamma.transpose(); + // And break so we don't add any more process noise + break; + } + } + + let state_bar = if self.ekf { + OVector::::Size>::zeros() + } else { + stm * self.prev_estimate.state_deviation + }; + let estimate = KfEstimate { + nominal_state, + state_deviation: state_bar, + covar: covar_bar, + covar_bar, + stm, + predicted: true, + }; + self.prev_estimate = estimate; + // Update the prev epoch for all SNCs + for snc in &mut self.process_noise { + snc.prev_epoch = Some(self.prev_estimate.epoch()); + } + Ok(estimate) + } + + /// Computes the measurement update with a provided real observation and computed observation. + /// + /// May return a FilterError if the STM or sensitivity matrices were not updated. + fn measurement_update( + &mut self, + nominal_state: T, + real_obs: &OVector, + computed_obs: &OVector, + resid_ratio_check: Option, + ) -> Result<(Self::Estimate, Residual), NyxError> { + if !self.h_tilde_updated { + return Err(NyxError::SensitivityNotUpdated); + } + + let stm = nominal_state.stm()?; + + let epoch = nominal_state.epoch(); + + let mut covar_bar = stm * self.prev_estimate.covar * stm.transpose(); + let mut snc_used = false; + // Try to apply an SNC, if applicable + for (i, snc) in self.process_noise.iter().enumerate().rev() { + if let Some(snc_matrix) = snc.to_matrix(epoch) { + // Check if we're using another SNC than the one before + if self.prev_used_snc != i { + info!("Switched to {}-th {}", i, snc); + self.prev_used_snc = i; + } + + // Let's compute the Gamma matrix, an approximation of the time integral + // which assumes that the acceleration is constant between these two measurements. + let mut gamma = OMatrix::::Size, A>::zeros(); + let delta_t = (epoch - self.prev_estimate.epoch()).to_seconds(); + for blk in 0..A::dim() / 3 { + for i in 0..3 { + let idx_i = i + A::dim() * blk; + let idx_j = i + 3 * blk; + let idx_k = i + 3 + A::dim() * blk; + // For first block + // (0, 0) (1, 1) (2, 2) <=> \Delta t^2/2 + // (3, 0) (4, 1) (5, 2) <=> \Delta t + // Second block + // (6, 3) (7, 4) (8, 5) <=> \Delta t^2/2 + // (9, 3) (10, 4) (11, 5) <=> \Delta t + // * \Delta t^2/2 + // (i, i) when blk = 0 + // (i + A::dim() * blk, i + 3) when blk = 1 + // (i + A::dim() * blk, i + 3 * blk) + // * \Delta t + // (i + 3, i) when blk = 0 + // (i + 3, i + 9) when blk = 1 (and I think i + 12 + 3) + // (i + 3 + A::dim() * blk, i + 3 * blk) + gamma[(idx_i, idx_j)] = delta_t.powi(2) / 2.0; + gamma[(idx_k, idx_j)] = delta_t; + } + } + // Let's add the process noise + covar_bar += &gamma * snc_matrix * &gamma.transpose(); + snc_used = true; + // And break so we don't add any more process noise + break; + } + } + + if !snc_used { + debug!("@{} No SNC", epoch); + } + + let h_tilde_t = &self.h_tilde.transpose(); + let h_p_ht = &self.h_tilde * covar_bar * h_tilde_t; + + // Compute observation deviation (usually marked as y_i) + let prefit = real_obs - computed_obs; + + // Compute the prefit ratio + let ratio_mat = prefit.transpose() * &h_p_ht * &prefit; + let ratio = ratio_mat[0]; + + if let Some(ratio_thresh) = resid_ratio_check { + if ratio > ratio_thresh { + warn!("{epoch} msr rejected: residual ratio {ratio} > {ratio_thresh}"); + // Perform only a time update and return + let pred_est = self.time_update(nominal_state)?; + return Ok((pred_est, Residual::rejected(epoch, prefit, ratio))); + } else { + debug!("{epoch} msr accepted: residual ratio {ratio} < {ratio_thresh}"); + } + } + + // Compute the Kalman gain but first adding the measurement noise to H⋅P⋅H^T + let mut invertible_part = h_p_ht + &self.measurement_noise; + if !invertible_part.try_inverse_mut() { + return Err(NyxError::SingularKalmanGain); + } + + let gain = covar_bar * h_tilde_t * &invertible_part; + + // Compute the state estimate + let (state_hat, res) = if self.ekf { + let state_hat = &gain * &prefit; + let postfit = &prefit - (&self.h_tilde * state_hat); + (state_hat, Residual::new(epoch, prefit, postfit, ratio)) + } else { + // Must do a time update first + let state_bar = stm * self.prev_estimate.state_deviation; + let postfit = &prefit - (&self.h_tilde * state_bar); + ( + state_bar + &gain * &postfit, + Residual::new(epoch, prefit, postfit, ratio), + ) + }; + + // Compute covariance (Joseph update) + let first_term = OMatrix::::Size, ::Size>::identity() + - &gain * &self.h_tilde; + let covar = first_term * covar_bar * first_term.transpose() + + &gain * &self.measurement_noise * &gain.transpose(); + + // And wrap up + let estimate = KfEstimate { + nominal_state, + state_deviation: state_hat, + covar, + covar_bar, + stm, + predicted: false, + }; + + self.h_tilde_updated = false; + self.prev_estimate = estimate; + // Update the prev epoch for all SNCs + for snc in &mut self.process_noise { + snc.prev_epoch = Some(self.prev_estimate.epoch()); + } + Ok((estimate, res)) + } + + fn is_extended(&self) -> bool { + self.ekf + } + + fn set_extended(&mut self, status: bool) { + self.ekf = status; + } + + /// Overwrites all of the process noises to the one provided + fn set_process_noise(&mut self, snc: SNC) { + self.process_noise = vec![snc]; + } +} diff --git a/src/od/filter/mod.rs b/src/od/filter/mod.rs new file mode 100644 index 00000000..58d5dc2c --- /dev/null +++ b/src/od/filter/mod.rs @@ -0,0 +1,97 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use self::kalman::Residual; + +use super::estimate::Estimate; +use super::snc::SNC; +pub use crate::dynamics::{Dynamics, NyxError}; +use crate::linalg::allocator::Allocator; +use crate::linalg::{DefaultAllocator, DimName, OMatrix, OVector}; +use crate::time::Epoch; +pub use crate::{cosmic::Cosm, State, TimeTagged}; +pub mod kalman; + +/// Defines a Filter trait where S is the size of the estimated state, A the number of acceleration components of the EOMs (used for process noise matrix size), M the size of the measurements. +pub trait Filter +where + A: DimName, + M: DimName, + T: State, + DefaultAllocator: Allocator + + Allocator::Size> + + Allocator::VecLength> + + Allocator + + Allocator + + Allocator::Size> + + Allocator::Size, ::Size> + + Allocator + + Allocator::Size, A> + + Allocator::Size>, +{ + type Estimate: Estimate; + + /// Returns the previous estimate + fn previous_estimate(&self) -> &Self::Estimate; + + /// Set the previous estimate + fn set_previous_estimate(&mut self, est: &Self::Estimate); + + /// Update the sensitivity matrix (or "H tilde"). This function **must** be called prior to each + /// call to `measurement_update`. + fn update_h_tilde(&mut self, h_tilde: OMatrix::Size>); + + /// Computes a time update/prediction at the provided nominal state (i.e. advances the filter estimate with the updated STM). + /// + /// Returns an error if the STM was not updated. + fn time_update(&mut self, nominal_state: T) -> Result; + + /// Computes the measurement update with a provided real observation and computed observation. + /// + /// The nominal state is the state used for the computed observation. + /// The real observation is the observation that was actually measured. + /// The computed observation is the observation that was computed from the nominal state. + /// + /// Returns the updated estimate and the residual. The residual may be zero if the residual ratio check prevented the ingestion of this measurement. + /// + /// # Arguments + /// + /// * `nominal_state`: the nominal state at which the observation was computed. + /// * `real_obs`: the real observation that was measured. + /// * `computed_obs`: the computed observation from the nominal state. + /// * `resid_ratio_check`: the ratio below which the measurement is considered to be valid. + fn measurement_update( + &mut self, + nominal_state: T, + real_obs: &OVector, + computed_obs: &OVector, + resid_ratio_check: Option, + ) -> Result<(Self::Estimate, Residual), NyxError>; + + /// Returns whether the filter is an extended filter (e.g. EKF) + fn is_extended(&self) -> bool; + + /// Sets the filter to be extended or not depending on the value of status + fn set_extended(&mut self, status: bool); + + /// Sets the process noise matrix of the estimated state + fn set_process_noise(&mut self, snc: SNC); + + /// Returns the measurement noise used at this given epoch + fn measurement_noise(&self, epoch: Epoch) -> &OMatrix; +} diff --git a/src/od/kalman.rs b/src/od/kalman.rs index 7482667c..1093750f 100644 --- a/src/od/kalman.rs +++ b/src/od/kalman.rs @@ -22,7 +22,7 @@ use crate::linalg::{DefaultAllocator, DimName, OMatrix, OVector, U3}; pub use super::estimate::{Estimate, KfEstimate}; pub use super::residual::Residual; pub use super::snc::SNC; -use super::{CovarFormat, EpochFormat, Filter, State}; +use super::{Filter, State}; pub use crate::errors::NyxError; pub use crate::time::{Epoch, Unit}; @@ -63,8 +63,6 @@ where pub ekf: bool, h_tilde: OMatrix::Size>, h_tilde_updated: bool, - epoch_fmt: EpochFormat, // Stored here only for simplification, kinda ugly - covar_fmt: CovarFormat, // Idem prev_used_snc: usize, } diff --git a/src/od/mod.rs b/src/od/mod.rs index c8fceb31..c096fdad 100644 --- a/src/od/mod.rs +++ b/src/od/mod.rs @@ -24,10 +24,8 @@ use crate::Orbit; pub use crate::{cosmic::Cosm, State, TimeTagged}; use std::sync::Arc; -use crate::io::{CovarFormat, EpochFormat}; - -/// Provides the Kalman filters. The [examples](https://github.com/ChristopherRabotin/nyx/tree/master/examples) folder may help in the setup. -pub mod kalman; +pub mod filter; +pub use filter::Filter; /// Provides a range and range rate measuring models. mod ground_station; @@ -36,9 +34,6 @@ pub use ground_station::GroundStation; /// Provides Estimate handling functionalities. pub mod estimate; -/// Provides Residual handling functionalities. -pub mod residual; - /// Provides noise modeling pub mod noise; @@ -59,11 +54,10 @@ pub mod snc; pub mod prelude { pub use super::estimate::*; + pub use super::filter::kalman::*; pub use super::ground_station::*; - pub use super::kalman::*; pub use super::msr::*; pub use super::process::*; - pub use super::residual::*; pub use super::simulator::arc::TrackingArcSim; pub use super::simulator::*; pub use super::snc::*; @@ -72,75 +66,6 @@ pub mod prelude { pub use crate::time::{Duration, Epoch, TimeUnits, Unit}; } -/// Defines a Filter trait where S is the size of the estimated state, A the number of acceleration components of the EOMs (used for process noise matrix size), M the size of the measurements. -pub trait Filter -where - A: DimName, - M: DimName, - T: State, - DefaultAllocator: Allocator - + Allocator::Size> - + Allocator::VecLength> - + Allocator - + Allocator - + Allocator::Size> - + Allocator::Size, ::Size> - + Allocator - + Allocator::Size, A> - + Allocator::Size>, -{ - type Estimate: estimate::Estimate; - - /// Returns the previous estimate - fn previous_estimate(&self) -> &Self::Estimate; - - /// Set the previous estimate - fn set_previous_estimate(&mut self, est: &Self::Estimate); - - /// Update the sensitivity matrix (or "H tilde"). This function **must** be called prior to each - /// call to `measurement_update`. - fn update_h_tilde(&mut self, h_tilde: OMatrix::Size>); - - /// Computes a time update/prediction at the provided nominal state (i.e. advances the filter estimate with the updated STM). - /// - /// Returns an error if the STM was not updated. - fn time_update(&mut self, nominal_state: T) -> Result; - - /// Computes the measurement update with a provided real observation and computed observation. - /// - /// The nominal state is the state used for the computed observation. - /// The real observation is the observation that was actually measured. - /// The computed observation is the observation that was computed from the nominal state. - /// - /// Returns the updated estimate and the residual. The residual may be zero if the residual ratio check prevented the ingestion of this measurement. - /// - /// # Arguments - /// - /// * `nominal_state`: the nominal state at which the observation was computed. - /// * `real_obs`: the real observation that was measured. - /// * `computed_obs`: the computed observation from the nominal state. - /// * `resid_ratio_check`: the ratio below which the measurement is considered to be valid. - fn measurement_update( - &mut self, - nominal_state: T, - real_obs: &OVector, - computed_obs: &OVector, - resid_ratio_check: Option, - ) -> Result<(Self::Estimate, residual::Residual), NyxError>; - - /// Returns whether the filter is an extended filter (e.g. EKF) - fn is_extended(&self) -> bool; - - /// Sets the filter to be extended or not depending on the value of status - fn set_extended(&mut self, status: bool); - - /// Sets the process noise matrix of the estimated state - fn set_process_noise(&mut self, snc: snc::SNC); - - /// Returns the measurement noise used at this given epoch - fn measurement_noise(&self, epoch: Epoch) -> &OMatrix; -} - /// A trait defining a measurement that can be used in the orbit determination process. pub trait Measurement: Copy + TimeTagged { /// Defines how much data is measured. For example, if measuring range and range rate, this should be of size 2 (nalgebra::U2). diff --git a/src/od/msr/arc.rs b/src/od/msr/arc.rs index 265e1d26..89f91a15 100644 --- a/src/od/msr/arc.rs +++ b/src/od/msr/arc.rs @@ -22,21 +22,20 @@ use std::fmt::{Debug, Display}; use std::fs::File; use std::ops::RangeBounds; use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::Arc; use crate::cosmic::Cosm; use crate::io::watermark::pq_writer; -use crate::io::{ConfigError, ConfigRepr}; +use crate::io::{ConfigError, ConfigRepr, ExportCfg}; use crate::linalg::allocator::Allocator; use crate::linalg::{DefaultAllocator, DimName}; use crate::md::trajectory::Interpolatable; use crate::od::{Measurement, TrackingDeviceSim}; use crate::State; -use arrow::array::{ArrayRef, Float64Array, StringArray}; +use arrow::array::{Array, Float64Builder, StringBuilder}; use arrow::datatypes::{DataType, Field, Schema}; use arrow::record_batch::RecordBatch; -use hifitime::prelude::{Duration, Epoch, Format, Formatter}; +use hifitime::prelude::{Duration, Epoch}; use parquet::arrow::ArrowWriter; /// Tracking arc contains the tracking data generated by the tracking devices defined in this structure. @@ -78,16 +77,25 @@ where &self, path: P, ) -> Result> { - self.to_parquet(path, None, false) + self.to_parquet(path, ExportCfg::default()) } /// Store this tracking arc to a parquet file, with optional metadata and a timestamp appended to the filename. pub fn to_parquet + Debug>( &self, path: P, - extra_metadata: Option>, - timestamp: bool, + cfg: ExportCfg, ) -> Result> { + let path_buf = cfg.actual_path(path); + + if cfg.step.is_some() { + warn!("The `step` parameter in the export is not supported for tracking arcs."); + } + + if cfg.fields.is_some() { + warn!("The `fields` parameter in the export is not supported for tracking arcs."); + } + // Build the schema let mut hdrs = vec![ Field::new("Epoch:Gregorian UTC", DataType::Utf8, false), @@ -102,53 +110,67 @@ where // Build the schema let schema = Arc::new(Schema::new(hdrs)); - let mut record = Vec::new(); + let mut record: Vec> = Vec::new(); - // Build all of the records - record.push(Arc::new(StringArray::from( - self.measurements - .iter() - .map(|m| format!("{}", m.1.epoch())) - .collect::>(), - )) as ArrayRef); - - record.push(Arc::new(StringArray::from( - self.measurements - .iter() - .map(|m| format!("{:x}", m.1.epoch())) - .collect::>(), - )) as ArrayRef); - - record.push(Arc::new(Float64Array::from( - self.measurements - .iter() - .map(|m| m.1.epoch().to_tai_seconds()) - .collect::>(), - )) as ArrayRef); - - record.push(Arc::new(StringArray::from( - self.measurements - .iter() - .map(|m| m.0.clone()) - .collect::>(), - )) as ArrayRef); - - // Now comes the measurement data + // Build the measurement iterator + + let measurements = + if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() { + let start = cfg + .start_epoch + .unwrap_or_else(|| self.measurements.first().unwrap().1.epoch()); + let end = cfg + .end_epoch + .unwrap_or_else(|| self.measurements.last().unwrap().1.epoch()); + + info!("Exporting measurements from {start} to {end}."); - for obs_no in 0..Msr::MeasurementSize::USIZE { - record.push(Arc::new(Float64Array::from( self.measurements .iter() - .map(|m| m.1.observation()[obs_no]) - .collect::>(), - )) as ArrayRef); + .filter(|msr| msr.1.epoch() >= start && msr.1.epoch() <= end) + .cloned() + .collect() + } else { + self.measurements.to_vec() + }; + + // Build all of the records + + // Epochs + let mut utc_epoch = StringBuilder::new(); + let mut tai_epoch = StringBuilder::new(); + let mut tai_s = Float64Builder::new(); + for m in &measurements { + utc_epoch.append_value(format!("{}", m.1.epoch())); + tai_epoch.append_value(format!("{:x}", m.1.epoch())); + tai_s.append_value(m.1.epoch().to_tai_seconds()); + } + record.push(Arc::new(utc_epoch.finish())); + record.push(Arc::new(tai_epoch.finish())); + record.push(Arc::new(tai_s.finish())); + + // Device names + let mut device_names = StringBuilder::new(); + for m in &measurements { + device_names.append_value(m.0.clone()); + } + record.push(Arc::new(device_names.finish())); + + // Measurement data + for obs_no in 0..Msr::MeasurementSize::USIZE { + let mut data_builder = Float64Builder::new(); + + for m in &measurements { + data_builder.append_value(m.1.observation()[obs_no]); + } + record.push(Arc::new(data_builder.finish())); } // Serialize all of the devices and add that to the parquet file too. let mut metadata = HashMap::new(); metadata.insert("devices".to_string(), self.device_cfg.clone()); metadata.insert("Purpose".to_string(), "Tracking Arc Data".to_string()); - if let Some(add_meta) = extra_metadata { + if let Some(add_meta) = cfg.metadata { for (k, v) in add_meta { metadata.insert(k, v); } @@ -156,24 +178,6 @@ where let props = pq_writer(Some(metadata)); - let mut path_buf = path.as_ref().to_path_buf(); - - if timestamp { - if let Some(file_name) = path_buf.file_name() { - if let Some(file_name_str) = file_name.to_str() { - if let Some(extension) = path_buf.extension() { - let stamp = Formatter::new( - Epoch::now().unwrap(), - Format::from_str("%Y-%m-%dT%H-%M-%S").unwrap(), - ); - let new_file_name = - format!("{file_name_str}-{stamp}.{}", extension.to_str().unwrap()); - path_buf.set_file_name(new_file_name); - } - } - } - }; - let file = File::create(&path_buf)?; let mut writer = ArrowWriter::try_new(file, schema.clone(), props).unwrap(); @@ -182,7 +186,7 @@ where writer.write(&batch)?; writer.close()?; - info!("Serialized {self} to {path:?}"); + info!("Serialized {self} to {}", path_buf.display()); // Return the path this was written to Ok(path_buf) diff --git a/src/od/process/export.rs b/src/od/process/export.rs new file mode 100644 index 00000000..6f79c5f2 --- /dev/null +++ b/src/od/process/export.rs @@ -0,0 +1,365 @@ +/* + Nyx, blazing fast astrodynamics + Copyright (C) 2023 Christopher Rabotin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +use crate::io::watermark::pq_writer; +use crate::io::ExportCfg; +use crate::linalg::allocator::Allocator; +use crate::linalg::{DefaultAllocator, DimName}; +use crate::md::prelude::Frame; +use crate::md::trajectory::Interpolatable; +pub use crate::od::estimate::*; +pub use crate::od::ground_station::*; +pub use crate::od::snc::*; +pub use crate::od::*; +use crate::propagators::error_ctrl::ErrorCtrl; +pub use crate::time::{Duration, Unit}; +use crate::State; +use arrow::array::{Array, Float64Builder, StringBuilder}; +use arrow::datatypes::{DataType, Field, Schema}; +use arrow::record_batch::RecordBatch; +use parquet::arrow::ArrowWriter; +use std::collections::HashMap; +use std::error::Error; +use std::fs::File; +use std::ops::Add; +use std::path::{Path, PathBuf}; + +use super::ODProcess; + +impl< + 'a, + D: Dynamics, + E: ErrorCtrl, + Msr: Measurement, + A: DimName, + S: EstimateFrom + Interpolatable, + K: Filter, + > ODProcess<'a, D, E, Msr, A, S, K> +where + D::StateType: Interpolatable + Add::Size>, Output = D::StateType>, + ::VecLength>>::Buffer: Send, + DefaultAllocator: Allocator::Size> + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator + + Allocator::Size> + + Allocator::Size> + + Allocator::Size, Msr::MeasurementSize> + + Allocator::Size, Msr::MeasurementSize> + + Allocator::Size, ::Size> + + Allocator::Size, ::Size> + + Allocator::VecLength> + + Allocator + + Allocator + + Allocator::Size, A> + + Allocator::Size> + + Allocator::Size> + + Allocator::VecLength> + + Allocator::Size, ::Size> + + Allocator::Size, A> + + Allocator::Size>, +{ + /// Store the estimates and residuals in a parquet file + pub fn to_parquet>( + &self, + path: P, + cfg: ExportCfg, + ) -> Result> { + if self.estimates.is_empty() { + return Err(Box::new(NyxError::CustomError( + "No data: run the ODProcess before exporting it.".to_string(), + ))); + } else if self.estimates.len() != self.residuals.len() { + return Err(Box::new(NyxError::CustomError( + "Estimates and residuals are not aligned.".to_string(), + ))); + } + + let tick = Epoch::now().unwrap(); + info!("Exporting orbit determination result to parquet file..."); + + if cfg.step.is_some() { + warn!("The `step` parameter in the export is not supported for orbit determination exports."); + } + + // Grab the path here before we move stuff. + let path_buf = cfg.actual_path(path); + + // Build the schema + let mut hdrs = vec![ + Field::new("Epoch:Gregorian UTC", DataType::Utf8, false), + Field::new("Epoch:Gregorian TAI", DataType::Utf8, false), + Field::new("Epoch:TAI (s)", DataType::Float64, false), + ]; + + let frame_name = self.estimates[0].state().frame(); + + let more_meta = Some(vec![("Frame".to_string(), format!("{}", frame_name))]); + + let mut fields = match cfg.fields { + Some(fields) => fields, + None => S::export_params(), + }; + + // Check that we can retrieve this information + fields.retain(|param| match self.estimates[0].state().value(*param) { + Ok(_) => true, + Err(_) => { + warn!("Removed unavailable field `{param}` from orbit determination export",); + false + } + }); + + for field in &fields { + hdrs.push(field.to_field(more_meta.clone())); + } + + let cov_hdrs = match ::Size::dim() { + 6 => { + // Add orbit 1-sigma covariance info, plotting to perform computations as desired + vec![ + "Covariance XX", + "Covariance XY", + "Covariance XZ", + "Covariance XVx", + "Covariance XVy", + "Covariance XVz", + "Covariance YY", + "Covariance YZ", + "Covariance YVx", + "Covariance YVy", + "Covariance YVz", + "Covariance ZZ", + "Covariance ZVx", + "Covariance ZVy", + "Covariance ZVz", + "Covariance VxVx", + "Covariance VxVy", + "Covariance VxVz", + "Covariance VyVy", + "Covariance VyVz", + "Covariance VzVz", + ] + } + _ => todo!( + "exporting a state of size {} is not yet supported", + ::Size::dim() + ), + }; + + // Add the covariance in the integration frame + for hdr in &cov_hdrs { + hdrs.push(Field::new( + format!("{hdr} ({frame_name})"), + DataType::Float64, + false, + )); + } + + // Add the covariance in the RIC frame + for hdr in &cov_hdrs { + hdrs.push(Field::new(format!("{hdr} (RIC)"), DataType::Float64, false)); + } + + // Add the fields of the residuals + let mut msr_fields = Vec::new(); + for f in Msr::fields() { + msr_fields.push( + f.clone() + .with_nullable(true) + .with_name(format!("Prefit residual: {}", f.name())), + ); + } + for f in Msr::fields() { + msr_fields.push( + f.clone() + .with_nullable(true) + .with_name(format!("Postfit residual: {}", f.name())), + ); + } + + msr_fields.push(Field::new("Residual ratio", DataType::Float64, true)); + + hdrs.append(&mut msr_fields); + + // Build the schema + let schema = Arc::new(Schema::new(hdrs)); + let mut record: Vec> = Vec::new(); + + // Build the states iterator -- this does require copying the current states but I can't either get a reference or a copy of all the states. + let (estimates, residuals) = + if cfg.start_epoch.is_some() || cfg.end_epoch.is_some() || cfg.step.is_some() { + // Must interpolate the data! + let start = cfg + .start_epoch + .unwrap_or_else(|| self.estimates.first().unwrap().state().epoch()); + let end = cfg + .end_epoch + .unwrap_or_else(|| self.estimates.last().unwrap().state().epoch()); + + let mut residuals: Vec>> = + Vec::with_capacity(self.residuals.len()); + let mut estimates = Vec::with_capacity(self.estimates.len()); + + for (estimate, residual) in self.estimates.iter().zip(self.residuals.iter()) { + if estimate.epoch() >= start && estimate.epoch() <= end { + estimates.push(estimate.clone()); + residuals.push(residual.clone()); + } + } + + (estimates, residuals) + } else { + (self.estimates.to_vec(), self.residuals.to_vec()) + }; + + // Build all of the records + + // Epochs + let mut utc_epoch = StringBuilder::new(); + let mut tai_epoch = StringBuilder::new(); + let mut tai_s = Float64Builder::new(); + for s in &estimates { + utc_epoch.append_value(format!("{}", s.epoch())); + tai_epoch.append_value(format!("{:x}", s.epoch())); + tai_s.append_value(s.epoch().to_tai_seconds()); + } + record.push(Arc::new(utc_epoch.finish())); + record.push(Arc::new(tai_epoch.finish())); + record.push(Arc::new(tai_s.finish())); + + // Add all of the fields + for field in fields { + let mut data = Float64Builder::new(); + for s in &estimates { + data.append_value(s.state().value(field).unwrap()); + } + record.push(Arc::new(data.finish())); + } + // Add the 1-sigma covariance in the integration frame + for i in 0..::Size::dim() { + for j in i..::Size::dim() { + let mut data = Float64Builder::new(); + for s in &estimates { + data.append_value(s.covar()[(i, j)]); + } + record.push(Arc::new(data.finish())); + } + } + // Add the 1-sigma covariance in the RIC frame + let mut ric_covariances = Vec::new(); + + for s in &estimates { + let dcm6x6 = s + .state() + .orbit() + .dcm6x6_from_traj_frame(Frame::RIC) + .unwrap(); + // Create a full DCM and only rotate the orbit part of it. + let mut dcm = OMatrix::::identity(); + for i in 0..6 { + for j in i..6 { + dcm[(i, j)] = dcm6x6[(i, j)]; + } + } + let ric_covar = &dcm * s.covar() * &dcm.transpose(); + + ric_covariances.push(ric_covar); + } + + // Now store the RIC covariance data. + for i in 0..::Size::dim() { + for j in i..::Size::dim() { + let mut data = Float64Builder::new(); + for cov in ric_covariances.iter().take(estimates.len()) { + data.append_value(cov[(i, j)]); + } + record.push(Arc::new(data.finish())); + } + } + + // Finally, add the residuals. + // Prefits + for i in 0..Msr::MeasurementSize::dim() { + let mut data = Float64Builder::new(); + for resid_opt in &residuals { + if let Some(resid) = resid_opt { + data.append_value(resid.prefit[i]); + } else { + data.append_null(); + } + } + record.push(Arc::new(data.finish())); + } + // Postfit + for i in 0..Msr::MeasurementSize::dim() { + let mut data = Float64Builder::new(); + for resid_opt in &residuals { + if let Some(resid) = resid_opt { + data.append_value(resid.postfit[i]); + } else { + data.append_null(); + } + } + record.push(Arc::new(data.finish())); + } + // Residual ratio (unique entry regardless of the size) + let mut data = Float64Builder::new(); + for resid_opt in &residuals { + if let Some(resid) = resid_opt { + data.append_value(resid.ratio); + } else { + data.append_null(); + } + } + record.push(Arc::new(data.finish())); + + info!("Serialized {} estimates and residuals", estimates.len()); + + // Serialize all of the devices and add that to the parquet file too. + let mut metadata = HashMap::new(); + metadata.insert( + "Purpose".to_string(), + "Orbit determination results".to_string(), + ); + if let Some(add_meta) = cfg.metadata { + for (k, v) in add_meta { + metadata.insert(k, v); + } + } + + let props = pq_writer(Some(metadata)); + + let file = File::create(&path_buf)?; + let mut writer = ArrowWriter::try_new(file, schema.clone(), props)?; + + let batch = RecordBatch::try_new(schema, record)?; + writer.write(&batch)?; + writer.close()?; + + // Return the path this was written to + let tock_time = Epoch::now().unwrap() - tick; + info!( + "Orbit determination results written to {} in {tock_time}", + path_buf.display() + ); + Ok(path_buf) + } +} diff --git a/src/od/process/mod.rs b/src/od/process/mod.rs index 238e7db0..eeb7eec1 100644 --- a/src/od/process/mod.rs +++ b/src/od/process/mod.rs @@ -19,14 +19,10 @@ use crate::linalg::allocator::Allocator; use crate::linalg::{DefaultAllocator, DimName}; use crate::md::trajectory::{Interpolatable, Traj}; - pub use crate::od::estimate::*; pub use crate::od::ground_station::*; -pub use crate::od::kalman::*; -pub use crate::od::residual::*; pub use crate::od::snc::*; pub use crate::od::*; - use crate::propagators::error_ctrl::ErrorCtrl; use crate::propagators::PropInstance; pub use crate::time::{Duration, Unit}; @@ -34,15 +30,14 @@ use crate::State; mod conf; pub use conf::{IterationConf, SmoothingArc}; mod trigger; -pub use trigger::{CkfTrigger, EkfTrigger, KfTrigger}; +pub use trigger::EkfTrigger; mod rejectcrit; - +use self::msr::arc::TrackingArc; +pub use self::rejectcrit::FltResid; use std::collections::HashMap; use std::marker::PhantomData; use std::ops::Add; - -use self::msr::arc::TrackingArc; -pub use self::rejectcrit::FltResid; +mod export; /// An orbit determination process. Note that everything passed to this structure is moved. #[allow(clippy::upper_case_acronyms)] @@ -84,7 +79,7 @@ pub struct ODProcess< /// Vector of estimates available after a pass pub estimates: Vec, /// Vector of residuals available after a pass - pub residuals: Vec>, + pub residuals: Vec>>, pub ekf_trigger: Option, /// Residual rejection criteria allows preventing bad measurements from affecting the estimation. pub resid_crit: Option, @@ -128,6 +123,29 @@ where + Allocator::Size, A> + Allocator::Size>, { + /// Initialize a new orbit determination process with an optional trigger to switch from a CKF to an EKF. + pub fn new( + prop: PropInstance<'a, D, E>, + kf: K, + ekf_trigger: Option, + resid_crit: Option, + cosm: Arc, + ) -> Self { + let init_state = prop.state; + Self { + prop, + kf, + estimates: Vec::with_capacity(10_000), + residuals: Vec::with_capacity(10_000), + ekf_trigger, + resid_crit, + cosm, + init_state, + _marker: PhantomData::, + } + } + + /// Initialize a new orbit determination process with an Extended Kalman filter. The switch from a classical KF to an EKF is based on the provided trigger. pub fn ekf( prop: PropInstance<'a, D, E>, kf: K, @@ -250,49 +268,27 @@ where Ok(smoothed) } - /// Returns the root mean square of the prefit residuals - /// - /// # WARNING: - /// The units will be garbage here if rows in the measurements have different units. - pub fn rms_prefit_residual(&self) -> f64 { - let mut sum = 0.0; - for residual in &self.residuals { - sum += residual.prefit.dot(&residual.prefit); - } - (sum / (self.residuals.len() as f64)).sqrt() - } - - /// Returns the root mean square of the postfit residuals - /// - /// # WARNING: - /// The units will be garbage here if rows in the measurements have different units. - pub fn rms_postfit_residual(&self) -> f64 { - let mut sum = 0.0; - for residual in &self.residuals { - sum += residual.postfit.dot(&residual.postfit); - } - (sum / (self.residuals.len() as f64)).sqrt() - } - /// Returns the root mean square of the prefit residual ratios pub fn rms_residual_ratios(&self) -> f64 { let mut sum = 0.0; - for residual in &self.residuals { + for residual in self.residuals.iter().flatten() { sum += residual.ratio.powi(2); } (sum / (self.residuals.len() as f64)).sqrt() } /// Allows iterating on the filter solution. Requires specifying a smoothing condition to know where to stop the smoothing. - pub fn iterate_arc( + pub fn iterate( &mut self, - arc: &TrackingArc, + measurements: &[(String, Msr)], + devices: &mut HashMap, + step_size: Duration, config: IterationConf, ) -> Result<(), NyxError> where Dev: TrackingDeviceSim, { - let measurements = &arc.measurements; + // TODO(now): Add ExportCfg to iterate and to process so the data can be exported as we process it. Consider a thread writing with channel for faster serialization. let mut best_rms = self.rms_residual_ratios(); let mut previous_rms = best_rms; @@ -308,11 +304,12 @@ where "Filter converged to absolute tolerance ({:.2e} < {:.2e}) after {} iterations", best_rms, config.absolute_tol, iter_cnt ); - return Ok(()); + break; } iter_cnt += 1; + // Prevent infinite loop when iterating prior to turning on the EKF. if let Some(trigger) = &mut self.ekf_trigger { trigger.reset(); } @@ -326,11 +323,12 @@ where // Reset the propagator self.prop.state = self.init_state; // Empty the estimates and add the first smoothed estimate as the initial estimate - self.estimates = Vec::with_capacity(measurements.len()); - // self.estimates.push(smoothed[0].clone()); + self.estimates = Vec::with_capacity(measurements.len().max(self.estimates.len())); + self.residuals = Vec::with_capacity(measurements.len().max(self.estimates.len())); + self.kf.set_previous_estimate(&smoothed[0]); // And re-run the filter - self.process_arc::(arc)?; + self.process::(measurements, devices, step_size)?; // Compute the new RMS let new_rms = self.rms_residual_ratios(); @@ -347,7 +345,8 @@ where "Filter converged to relative tolerance ({:.2e} < {:.2e}) after {} iterations", cur_rel_rms, config.relative_tol, iter_cnt ); - return Ok(()); + + break; } if new_rms > previous_rms { @@ -366,7 +365,7 @@ where return Err(NyxError::MaxIterReached(msg)); } else { error!("{}", msg); - return Ok(()); + break; } } else { warn!("Filter iteration caused divergence {} of {} acceptable subsequent divergences", divergence_cnt, config.max_divergences); @@ -393,10 +392,29 @@ where return Err(NyxError::MaxIterReached(msg)); } else { error!("{}", msg); - return Ok(()); + break; } } } + + Ok(()) + } + + /// Allows iterating on the filter solution. Requires specifying a smoothing condition to know where to stop the smoothing. + pub fn iterate_arc( + &mut self, + arc: &TrackingArc, + config: IterationConf, + ) -> Result<(), NyxError> + where + Dev: TrackingDeviceSim, + { + let mut devices = arc.rebuild_devices::(self.cosm.clone()).unwrap(); + + let measurements = &arc.measurements; + let step_size = arc.min_duration_sep().unwrap(); + + self.iterate(measurements, &mut devices, step_size, config) } /// Process the provided tracking arc for this orbit determination process. @@ -517,16 +535,10 @@ where self.kf.update_h_tilde(h_tilde); - let resid_ratio_check = match self.resid_crit { - None => None, - Some(flt) => { - if msr_accepted_cnt < flt.min_accepted { - None - } else { - Some(flt.num_sigmas) - } - } - }; + let resid_ratio_check = self + .resid_crit + .filter(|flt| msr_accepted_cnt >= flt.min_accepted) + .map(|flt| flt.num_sigmas); match self.kf.measurement_update( nominal_state, @@ -566,7 +578,7 @@ where self.prop.state.reset_stm(); self.estimates.push(estimate); - self.residuals.push(residual); + self.residuals.push(Some(residual)); } Err(e) => return Err(e), } @@ -597,6 +609,8 @@ where // State deviation is always zero for an EKF time update // therefore we don't do anything different for an extended filter self.estimates.push(est); + // We push None so that the residuals and estimates are aligned + self.residuals.push(None); } Err(e) => return Err(e), } @@ -616,10 +630,11 @@ where Ok(()) } - /// Allows for covariance mapping without processing measurements - pub fn map_covar(&mut self, end_epoch: Epoch) -> Result<(), NyxError> { + /// Continuously predicts the trajectory until the provided end epoch, with covariance mapping at each step. In other words, this performs a time update. + pub fn predict_until(&mut self, max_step: Duration, end_epoch: Epoch) -> Result<(), NyxError> { let prop_time = end_epoch - self.kf.previous_estimate().epoch(); info!("Propagating for {prop_time} seconds and mapping covariance",); + self.prop.set_step(max_step, false); loop { let mut epoch = self.prop.state.epoch(); @@ -642,6 +657,7 @@ where // State deviation is always zero for an EKF time update // therefore we don't do anything different for an extended filter self.estimates.push(est); + self.residuals.push(None); } Err(e) => return Err(e), } @@ -654,7 +670,14 @@ where Ok(()) } - /// Builds the navigation trajectory for the estimated state only (no covariance until https://gitlab.com/nyx-space/nyx/-/issues/199!) + /// Continuously predicts the trajectory for the provided duration, with covariance mapping at each step. In other words, this performs a time update. + pub fn predict_for(&mut self, max_step: Duration, duration: Duration) -> Result<(), NyxError> { + let end_epoch = self.kf.previous_estimate().epoch() + duration; + + self.predict_until(max_step, end_epoch) + } + + /// Builds the navigation trajectory for the estimated state only pub fn to_traj(&self) -> Result, NyxError> where DefaultAllocator: Allocator::VecLength>, diff --git a/src/od/process/rejectcrit.rs b/src/od/process/rejectcrit.rs index 875a943a..55a6be22 100644 --- a/src/od/process/rejectcrit.rs +++ b/src/od/process/rejectcrit.rs @@ -23,9 +23,14 @@ use pyo3::prelude::*; use serde_derive::{Deserialize, Serialize}; use std::sync::Arc; -/// Reject measurements with a residual ratio greater than the provided value. +/// Reject measurements with a residual ratio greater than the provided sigmas values. Will only be turned used if at least min_accepted measurements have been processed so far. +/// If unsure, use the default: `FltResid::default()` in Rust, and `FltResid()` in Python (i.e. construct without arguments). #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "python", pyclass)] +#[cfg_attr( + feature = "python", + pyo3(text_signature = "(min_accepted=None, num_sigmas=None)") +)] pub struct FltResid { /// Minimum number of accepted measurements before applying the rejection criteria. pub min_accepted: usize, @@ -36,6 +41,18 @@ pub struct FltResid { #[cfg(feature = "python")] #[pymethods] impl FltResid { + #[new] + fn py_new(min_accepted: Option, num_sigmas: Option) -> Self { + let mut me = Self::default(); + if let Some(min_accepted) = min_accepted { + me.min_accepted = min_accepted; + } + if let Some(num_sigmas) = num_sigmas { + me.num_sigmas = num_sigmas; + } + me + } + #[getter] fn get_min_accepted(&self) -> usize { self.min_accepted diff --git a/src/od/process/trigger.rs b/src/od/process/trigger.rs index 0cc1d1dc..79cedaae 100644 --- a/src/od/process/trigger.rs +++ b/src/od/process/trigger.rs @@ -16,68 +16,14 @@ along with this program. If not, see . */ +use hifitime::Epoch; + +use super::estimate::Estimate; use crate::linalg::allocator::Allocator; use crate::linalg::DefaultAllocator; - -pub use crate::od::estimate::*; -pub use crate::od::ground_station::*; -pub use crate::od::kalman::*; -pub use crate::od::residual::*; -pub use crate::od::snc::*; -pub use crate::od::*; - pub use crate::time::{Duration, Unit}; use crate::State; -use self::prelude::IterationConf; - -/// A trait detailing when to switch to from a CKF to an EKF -pub trait KfTrigger { - fn enable_ekf(&mut self, est: &E) -> bool - where - E: Estimate, - DefaultAllocator: Allocator::Size> - + Allocator::VecLength> - + Allocator::Size, ::Size>; - - /// Set whether the EKF trigger should be inhibited. This is useful when smoothing for example. - fn set_inhibit(&mut self, inhibit: bool); - - /// Reset the trigger - fn reset(&mut self); - - /// Return true if the filter should not longer be as extended. - /// By default, this returns false, i.e. when a filter has been switched to an EKF, it will - /// remain as such. - fn disable_ekf(&mut self, _epoch: Epoch) -> bool { - false - } - - /// If some iteration configuration is returned, the filter will iterate with it before enabling the EKF. - fn iteration_config(&self) -> Option { - None - } -} - -/// CkfTrigger will never switch a KF to an EKF -pub struct CkfTrigger; - -impl KfTrigger for CkfTrigger { - fn enable_ekf(&mut self, _est: &E) -> bool - where - E: Estimate, - DefaultAllocator: Allocator::Size> - + Allocator::VecLength> - + Allocator::Size, ::Size>, - { - false - } - - fn set_inhibit(&mut self, _inhibit: bool) {} - - fn reset(&mut self) {} -} - /// An EkfTrigger on the number of measurements processed and a time between measurements. pub struct EkfTrigger { pub num_msrs: usize, diff --git a/src/od/residual.rs b/src/od/residual.rs deleted file mode 100644 index 59f905d9..00000000 --- a/src/od/residual.rs +++ /dev/null @@ -1,171 +0,0 @@ -/* - Nyx, blazing fast astrodynamics - Copyright (C) 2023 Christopher Rabotin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -use super::EpochFormat; -use crate::hifitime::Epoch; -use crate::linalg::allocator::Allocator; -use crate::linalg::{DefaultAllocator, DimName, OVector}; -use serde::ser::SerializeSeq; -use serde::{Serialize, Serializer}; -use std::fmt; - -/// Stores an Estimate, as the result of a `time_update` or `measurement_update`. -#[derive(Debug, Clone, PartialEq)] -pub struct Residual -where - M: DimName, - DefaultAllocator: Allocator + Allocator, -{ - /// Date time of this Residual - pub epoch: Epoch, - /// The prefit residual in the units of the measurement type - pub prefit: OVector, - /// The postfit residual in the units of the measurement type - pub postfit: OVector, - /// The prefit residual ratio, i.e. `r' * (H*P*H')^-1 * r`, where `r` is the prefit residual, `H` is the sensitivity matrix, and `P` is the covariance matrix. - pub ratio: f64, - /// The Epoch format upon serialization - pub epoch_fmt: EpochFormat, - /// Whether or not this was rejected - pub rejected: bool, -} - -impl Residual -where - M: DimName, - DefaultAllocator: Allocator + Allocator, -{ - /// An empty estimate. This is useful if wanting to store an estimate outside the scope of a filtering loop. - pub fn zeros() -> Self { - Self { - epoch: Epoch::from_tai_seconds(0.0), - prefit: OVector::::zeros(), - postfit: OVector::::zeros(), - ratio: 0.0, - epoch_fmt: EpochFormat::GregorianUtc, - rejected: true, - } - } - - /// Flags a Residual as rejected. - pub fn rejected(epoch: Epoch, prefit: OVector, ratio: f64) -> Self { - Self { - epoch, - prefit, - postfit: OVector::::zeros(), - ratio, - epoch_fmt: EpochFormat::GregorianUtc, - rejected: true, - } - } - - pub fn header(epoch_fmt: EpochFormat) -> Vec { - let mut hdr_v = Vec::with_capacity(2 * M::dim() + 1); - hdr_v.push(format!("{epoch_fmt}")); - // Serialize the prefit - for i in 0..M::dim() { - hdr_v.push(format!("prefit_{i}")); - } - // Serialize the postfit - for i in 0..M::dim() { - hdr_v.push(format!("postfit_{i}")); - } - hdr_v - } - - pub fn default_header() -> Vec { - Self::header(EpochFormat::GregorianUtc) - } - - pub fn new( - epoch: Epoch, - prefit: OVector, - postfit: OVector, - ratio: f64, - ) -> Self { - Self { - epoch, - prefit, - postfit, - ratio, - epoch_fmt: EpochFormat::GregorianUtc, - rejected: false, - } - } -} - -impl fmt::Display for Residual -where - M: DimName, - DefaultAllocator: - Allocator + Allocator + Allocator + Allocator, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Prefit {} Postfit {}", &self.prefit, &self.postfit) - } -} - -impl fmt::LowerExp for Residual -where - M: DimName, - DefaultAllocator: - Allocator + Allocator + Allocator + Allocator, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Prefit {:e} Postfit {:e}", &self.prefit, &self.postfit) - } -} - -impl Serialize for Residual -where - M: DimName, - DefaultAllocator: - Allocator + Allocator + Allocator + Allocator, -{ - /// Serializes the estimate - fn serialize(&self, serializer: O) -> Result - where - O: Serializer, - { - let mut seq = serializer.serialize_seq(Some(2 * M::dim() + 1))?; - match self.epoch_fmt { - EpochFormat::GregorianUtc => seq.serialize_element(&format!("{}", self.epoch))?, - EpochFormat::GregorianTai => seq.serialize_element(&format!("{:x}", self.epoch))?, - EpochFormat::MjdTai => seq.serialize_element(&self.epoch.to_mjd_tai_days())?, - EpochFormat::MjdTt => seq.serialize_element(&self.epoch.to_mjd_tt_days())?, - EpochFormat::MjdUtc => seq.serialize_element(&self.epoch.to_mjd_utc_days())?, - EpochFormat::JdeEt => seq.serialize_element(&self.epoch.to_jde_et_days())?, - EpochFormat::JdeTai => seq.serialize_element(&self.epoch.to_jde_tai_days())?, - EpochFormat::JdeTt => seq.serialize_element(&self.epoch.to_jde_tt_days())?, - EpochFormat::JdeUtc => seq.serialize_element(&self.epoch.to_jde_utc_days())?, - EpochFormat::TaiSecs(e) => seq.serialize_element(&(self.epoch.to_tai_seconds() - e))?, - EpochFormat::TaiDays(e) => seq.serialize_element(&(self.epoch.to_tai_days() - e))?, - } - // Serialize the prefit - for i in 0..M::dim() { - seq.serialize_element(&self.prefit[(i, 0)])?; - } - // Serialize the postfit - for i in 0..M::dim() { - seq.serialize_element(&self.postfit[(i, 0)])?; - } - // Serialize the ratio - seq.serialize_element(&self.ratio)?; - seq.end() - } -} diff --git a/src/python/mission_design/mod.rs b/src/python/mission_design/mod.rs index 3743d83e..a2448c52 100644 --- a/src/python/mission_design/mod.rs +++ b/src/python/mission_design/mod.rs @@ -17,7 +17,7 @@ */ use crate::io::trajectory_data::DynamicTrajectory; -use crate::io::ConfigError; +use crate::io::{ConfigError, ExportCfg}; use crate::md::prelude::{PropOpts, Propagator, SpacecraftDynamics}; use crate::md::{Event, StateParameter}; @@ -42,6 +42,7 @@ pub(crate) fn register_md(py: Python<'_>, parent_module: &PyModule) -> PyResult< sm.add_class::()?; sm.add_class::()?; sm.add_class::()?; + sm.add_class::()?; sm.add_function(wrap_pyfunction!(propagate, sm)?)?; py_run!( diff --git a/src/python/mission_design/trajectory.rs b/src/python/mission_design/trajectory.rs index eeee21a6..4b4088c8 100644 --- a/src/python/mission_design/trajectory.rs +++ b/src/python/mission_design/trajectory.rs @@ -16,7 +16,7 @@ along with this program. If not, see . */ -use hifitime::{Epoch, Unit}; +use hifitime::{Duration, Epoch, Unit}; use pyo3::prelude::*; use crate::md::trajectory::ExportCfg; @@ -54,6 +54,13 @@ impl Traj { *self.inner.last() } + /// Copies this object and resamples it with the provided step size + fn resample(&self, step: Duration) -> Result { + let inner = self.inner.resample(step)?; + + Ok(Self { inner }) + } + /// Finds a specific event in a trajectory. /// /// If a start or end epoch is provided (or both are provided), this function will return a list of a single event. diff --git a/src/python/orbit_determination/mod.rs b/src/python/orbit_determination/mod.rs index 56bf8134..7874f60a 100644 --- a/src/python/orbit_determination/mod.rs +++ b/src/python/orbit_determination/mod.rs @@ -21,6 +21,7 @@ use std::collections::HashMap; use crate::cosmic::Cosm; use crate::io::tracking_data::DynamicTrackingArc; use crate::io::trajectory_data::DynamicTrajectory; +use crate::io::ExportCfg; use crate::od::msr::RangeDoppler; use crate::od::noise::GaussMarkov; use crate::od::process::FltResid; @@ -35,7 +36,7 @@ mod process; mod trkconfig; use estimate::OrbitEstimate; -use process::process_tracking_arc; +use process::{predictor, process_tracking_arc}; pub(crate) fn register_od(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { let sm = PyModule::new(py, "_nyx_space.orbit_determination")?; @@ -47,7 +48,9 @@ pub(crate) fn register_od(py: Python<'_>, parent_module: &PyModule) -> PyResult< sm.add_class::()?; sm.add_class::()?; sm.add_class::()?; + sm.add_class::()?; sm.add_function(wrap_pyfunction!(process_tracking_arc, sm)?)?; + sm.add_function(wrap_pyfunction!(predictor, sm)?)?; py_run!( py, @@ -91,14 +94,13 @@ impl GroundTrackingArcSim { pub fn generate_measurements( &mut self, path: String, - metadata: Option>, - timestamp: bool, + export_cfg: ExportCfg, ) -> Result { let cosm = Cosm::de438(); let arc = self.inner.generate_measurements(cosm)?; // Save the tracking arc - let maybe = arc.to_parquet(path, metadata, timestamp); + let maybe = arc.to_parquet(path, export_cfg); match maybe { Ok(path) => Ok(format!("{}", path.to_str().unwrap())), diff --git a/src/python/orbit_determination/process.rs b/src/python/orbit_determination/process.rs index fe955b7e..3032d597 100644 --- a/src/python/orbit_determination/process.rs +++ b/src/python/orbit_determination/process.rs @@ -16,15 +16,16 @@ along with this program. If not, see . */ -use hifitime::Duration; +use hifitime::{Duration, Epoch}; use nalgebra::Matrix2; use pyo3::prelude::*; use crate::{ io::tracking_data::DynamicTrackingArc, + io::ExportCfg, md::prelude::{Cosm, Propagator, SpacecraftDynamics}, od::{ - kalman::KF, + filter::kalman::KF, process::{EkfTrigger, FltResid, ODProcess}, }, NyxError, Spacecraft, @@ -32,10 +33,11 @@ use crate::{ use super::{estimate::OrbitEstimate, GroundStation}; -/// Propagates the provided spacecraft with the provided dynamics until the provided stopping condition (duration, epoch, or event [and optionally the count]). +/// Runs an orbit determination process given the dynamics, the initial spacecraft object and its orbit estimate, the measurement noise for the filter, the tracking arc data. +/// You must also provide an export path and optionally and export configuration to export the results to a Parquet file. #[pyfunction] #[pyo3( - text_signature = "(dynamics, spacecraft, initial_estimate, measurement_noise, arc, ekf_num_meas, ekf_disable_time, resid_crit=None)" + text_signature = "(dynamics, spacecraft, initial_estimate, measurement_noise, arc, export_path, export_cfg, ekf_num_meas=None, ekf_disable_time=None, resid_crit=None, predict_until=None, predict_for=None, predict_step=None)" )] pub(crate) fn process_tracking_arc( dynamics: SpacecraftDynamics, @@ -43,10 +45,15 @@ pub(crate) fn process_tracking_arc( initial_estimate: OrbitEstimate, measurement_noise: Vec, arc: &DynamicTrackingArc, - ekf_num_meas: usize, - ekf_disable_time: Duration, + export_path: String, + export_cfg: Option, + ekf_num_meas: Option, + ekf_disable_time: Option, resid_crit: Option, -) -> Result, NyxError> { + predict_until: Option, + predict_for: Option, + predict_step: Option, +) -> Result { // TODO: Return a navigation trajectory or use a class that mimics the better ODProcess -- https://github.com/nyx-space/nyx/issues/134 let msr_noise = Matrix2::from_iterator(measurement_noise); @@ -58,23 +65,96 @@ pub(crate) fn process_tracking_arc( let prop = Propagator::default(dynamics); let prop_est = prop.with(init_sc); - let mut odp = ODProcess::ekf( - prop_est, - kf, - EkfTrigger::new(ekf_num_meas, ekf_disable_time), - resid_crit, - Cosm::de438(), - ); + if (ekf_disable_time.is_some() && ekf_num_meas.is_none()) + || (ekf_disable_time.is_none() && ekf_num_meas.is_some()) + { + return Err(NyxError::CustomError(format!( + "For an EKF trigger, you must provide both a disable time and a num measurements." + ))); + } + + let trigger = match ekf_num_meas { + Some(ekf_num_meas) => Some(EkfTrigger::new(ekf_num_meas, ekf_disable_time.unwrap())), + None => None, + }; + + let mut odp = ODProcess::new(prop_est, kf, trigger, resid_crit, Cosm::de438()); let concrete_arc = arc.to_tracking_arc()?; odp.process_arc::(&concrete_arc).unwrap(); - // Now build a vector of orbit estimates. - let mut rslt = Vec::with_capacity(odp.estimates.len()); - for est in odp.estimates { - rslt.push(OrbitEstimate(est)); + if let Some(epoch) = predict_until { + let max_step = + predict_step.ok_or_else(|| NyxError::CustomError("predict_step unset".to_string()))?; + odp.predict_until(max_step, epoch).unwrap(); + } else if let Some(duration) = predict_for { + let max_step = + predict_step.ok_or_else(|| NyxError::CustomError("predict_step unset".to_string()))?; + odp.predict_for(max_step, duration).unwrap(); } - Ok(rslt) + let maybe = odp.to_parquet( + export_path, + export_cfg.unwrap_or_else(|| ExportCfg::default()), + ); + + match maybe { + Ok(path) => Ok(format!("{}", path.to_str().unwrap())), + Err(e) => Err(NyxError::CustomError(e.to_string())), + } +} + +/// Runs an orbit determination prediction-only process given the dynamics, the initial spacecraft object, its orbit estimate, the desired step size, and either a prediction epoch or a prediction duration +/// You must also provide an export path and optionally and export configuration to export the results to a Parquet file. +#[pyfunction] +#[pyo3( + text_signature = "(dynamics, spacecraft, initial_estimate, step, export_path, export_cfg, predict_until=None, predict_for=None)" +)] +pub(crate) fn predictor( + dynamics: SpacecraftDynamics, + spacecraft: Spacecraft, + initial_estimate: OrbitEstimate, + step: Duration, + export_path: String, + export_cfg: Option, + predict_until: Option, + predict_for: Option, +) -> Result { + // TODO: Return a navigation trajectory or use a class that mimics the better ODProcess -- https://github.com/nyx-space/nyx/issues/134 + let msr_noise = Matrix2::from_iterator(vec![1e-10, 0.0, 0.0, 1e-10]); + + let init_sc = spacecraft.with_orbit(initial_estimate.0.nominal_state.with_stm()); + + // Build KF without SNC + let kf = KF::no_snc(initial_estimate.0, msr_noise); + + let prop = Propagator::default(dynamics); + let prop_est = prop.with(init_sc); + + if (predict_until.is_some() && predict_for.is_some()) + || (predict_until.is_none() && predict_for.is_none()) + { + return Err(NyxError::CustomError(format!( + "You must provide either the predict_until argument or the predict_for argument" + ))); + } + + let mut odp = ODProcess::ckf(prop_est, kf, None, Cosm::de438()); + + if let Some(epoch) = predict_until { + odp.predict_until(step, epoch).unwrap(); + } else if let Some(duration) = predict_for { + odp.predict_for(step, duration).unwrap(); + } + + let maybe = odp.to_parquet( + export_path, + export_cfg.unwrap_or_else(|| ExportCfg::default()), + ); + + match maybe { + Ok(path) => Ok(format!("{}", path.to_str().unwrap())), + Err(e) => Err(NyxError::CustomError(e.to_string())), + } } diff --git a/tests/mission_design/multishoot/mod.rs b/tests/mission_design/multishoot/mod.rs index 769af0a4..405dd5fa 100644 --- a/tests/mission_design/multishoot/mod.rs +++ b/tests/mission_design/multishoot/mod.rs @@ -125,7 +125,10 @@ fn alt_orbit_raising() { .for_duration_with_traj(start.period()) .unwrap() .1 - .to_parquet_with_step(output_path.join("multishoot_start.csv"), 2 * Unit::Second) + .to_parquet_with_step( + output_path.join("multishoot_start.parquet"), + 2 * Unit::Second, + ) .unwrap(); // Propagate the initial orbit too @@ -133,7 +136,10 @@ fn alt_orbit_raising() { .for_duration_with_traj(target.period()) .unwrap() .1 - .to_parquet_with_step(output_path.join("multishoot_target.csv"), 2 * Unit::Second) + .to_parquet_with_step( + output_path.join("multishoot_target.parquet"), + 2 * Unit::Second, + ) .unwrap(); // Just propagate this spacecraft for one orbit for plotting @@ -143,7 +149,10 @@ fn alt_orbit_raising() { .unwrap(); end_traj - .to_parquet_with_step(output_path.join("multishoot_to_end.csv"), 2 * Unit::Second) + .to_parquet_with_step( + output_path.join("multishoot_to_end.parquet"), + 2 * Unit::Second, + ) .unwrap(); // Check that error is 50km or less. That isn't great, but I blame that on the scenario and the final node being optimized. @@ -347,7 +356,10 @@ fn vmag_orbit_raising() { .iter() .enumerate() { - traj.to_parquet_with_step(&format!("multishoot_to_node_{}.csv", i), 2 * Unit::Second) - .unwrap(); + traj.to_parquet_with_step( + &format!("multishoot_to_node_{}.parquet", i), + 2 * Unit::Second, + ) + .unwrap(); } } diff --git a/tests/orbit_determination/mod.rs b/tests/orbit_determination/mod.rs index 1ffefad7..d0c2692b 100644 --- a/tests/orbit_determination/mod.rs +++ b/tests/orbit_determination/mod.rs @@ -1,4 +1,3 @@ -extern crate csv; extern crate nalgebra as na; extern crate nyx_space as nyx; @@ -40,14 +39,6 @@ fn empty_estimate() { assert!(empty.predicted, "expected predicted to be true"); } -#[test] -fn csv_serialize_empty_estimate() { - use std::io; - let empty = KfEstimate::zeros(Orbit::zeros()); - let mut wtr = csv::Writer::from_writer(io::stdout()); - wtr.serialize(empty).expect("could not write to stdout"); -} - #[test] fn filter_errors() { let initial_estimate = KfEstimate::zeros(Orbit::zeros()); diff --git a/tests/orbit_determination/multi_body.rs b/tests/orbit_determination/multi_body.rs index 3f76492e..2f40b3d2 100644 --- a/tests/orbit_determination/multi_body.rs +++ b/tests/orbit_determination/multi_body.rs @@ -18,7 +18,6 @@ fn od_val_multi_body_ckf_perfect_stations() { if pretty_env_logger::try_init().is_err() { println!("could not init env_logger"); } - use std::io; let cosm = Cosm::de438(); @@ -113,8 +112,6 @@ fn od_val_multi_body_ckf_perfect_stations() { odp.process_arc::(&arc).unwrap(); - let mut wtr = csv::Writer::from_writer(io::stdout()); - let mut printed = false; let mut last_est = None; for (no, est) in odp.estimates.iter().enumerate() { if no == 0 { @@ -136,15 +133,10 @@ fn od_val_multi_body_ckf_perfect_stations() { est.state_deviation().norm() ); - if !printed { - wtr.serialize(*est).expect("could not write to stdout"); - printed = true; - } - last_est = Some(est); } - for res in &odp.residuals { + for res in odp.residuals.iter().flatten() { assert!( res.postfit.norm() < 2e-16, "postfit should be zero (perfect dynamics) ({:e})", @@ -174,7 +166,6 @@ fn multi_body_ckf_covar_map() { if pretty_env_logger::try_init().is_err() { println!("could not init env_logger"); } - use std::io; let cosm = Cosm::de438(); @@ -271,7 +262,7 @@ fn multi_body_ckf_covar_map() { } // Note that we check the residuals separately from the estimates because we have many predicted estimates which do not have any associated residuals. - for res in odp.residuals.iter() { + for res in odp.residuals.iter().flatten() { assert!( res.postfit.norm() < 2e-16, "postfit should be zero (perfect dynamics) ({:e})", @@ -283,9 +274,6 @@ fn multi_body_ckf_covar_map() { let est = odp.estimates.last().unwrap(); - let mut wtr = csv::Writer::from_writer(io::stdout()); - wtr.serialize(*est).expect("could not write to stdout"); - println!("{:.2e}", est.state_deviation().norm()); println!("{:.2e}", est.covar.norm()); diff --git a/tests/orbit_determination/resid_reject.rs b/tests/orbit_determination/resid_reject.rs index 1d5714de..da1874a4 100644 --- a/tests/orbit_determination/resid_reject.rs +++ b/tests/orbit_determination/resid_reject.rs @@ -1,5 +1,3 @@ -extern crate csv; - use pretty_env_logger::try_init; use rstest::*; @@ -214,7 +212,7 @@ fn od_resid_reject_all_ckf_two_way( ) .unwrap(); - for residual in odp.residuals.iter() { + for residual in odp.residuals.iter().flatten() { assert!(residual.rejected, "{} was not rejected!", residual.epoch); } @@ -280,7 +278,7 @@ fn od_resid_reject_default_ckf_two_way( // With the default configuration, the filter converges very fast since we have similar dynamics. - for residual in odp.residuals.iter() { + for residual in odp.residuals.iter().flatten() { assert!(!residual.rejected, "{} was rejected!", residual.epoch); } diff --git a/tests/orbit_determination/robust.rs b/tests/orbit_determination/robust.rs index 4da4f314..a9c74994 100644 --- a/tests/orbit_determination/robust.rs +++ b/tests/orbit_determination/robust.rs @@ -1,10 +1,9 @@ -extern crate csv; extern crate nyx_space as nyx; extern crate pretty_env_logger; use nyx::cosmic::{Bodies, Cosm, Orbit}; use nyx::dynamics::orbital::OrbitalDynamics; -use nyx::io::formatter::NavSolutionFormatter; +use nyx::io::ExportCfg; use nyx::linalg::{Matrix2, Vector2}; use nyx::md::StateParameter; use nyx::od::noise::GaussMarkov; @@ -12,8 +11,10 @@ use nyx::od::prelude::*; use nyx::propagators::{PropOpts, Propagator, RK4Fixed}; use nyx::time::{Epoch, TimeUnits, Unit}; use nyx::utils::rss_orbit_errors; +use polars::prelude::*; use std::collections::HashMap; use std::env; +use std::fs::File; use std::path::PathBuf; /* @@ -130,7 +131,7 @@ fn od_robust_test_ekf_realistic_one_way() { .iter() .collect(); - arc.to_parquet_simple(path).unwrap(); + arc.to_parquet_simple(&path).unwrap(); // Now that we have the truth data, let's start an OD with no noise at all and compute the estimates. // We expect the estimated orbit to be _nearly_ perfect because we've removed Saturn from the estimated trajectory @@ -172,69 +173,11 @@ fn od_robust_test_ekf_realistic_one_way() { odp.process_arc::(&remaining).unwrap(); - let estimate_fmtr = NavSolutionFormatter::default("robustness_test.csv".to_owned(), cosm); - - let mut wtr = csv::Writer::from_path("robustness_test.csv").unwrap(); - wtr.serialize(&estimate_fmtr.headers) - .expect("could not write to output file"); - - let mut err_wtr = csv::Writer::from_path("robustness_test_traj_err.csv").unwrap(); - err_wtr - .serialize(vec![ - "epoch", - "x_err_km", - "y_err_km", - "z_err_km", - "vx_err_km_s", - "vy_err_km_s", - "vz_err_km_s", - ]) - .expect("could not write to output file"); - - for est in &odp.estimates { - // Format the estimate - wtr.serialize(estimate_fmtr.fmt(est)) - .expect("could not write to CSV"); - // Add the error data - let truth_state = traj.at(est.epoch()).unwrap(); - let err = truth_state - est.state(); - err_wtr - .serialize(vec![ - est.epoch().to_string(), - format!("{}", err.x_km), - format!("{}", err.y_km), - format!("{}", err.z_km), - format!("{}", err.vx_km_s), - format!("{}", err.vy_km_s), - format!("{}", err.vz_km_s), - ]) - .expect("could not write to CSV"); - } - - let mut resid_wtr = csv::Writer::from_path("robustness_test_residuals.csv").unwrap(); - resid_wtr - .serialize(vec![ - "epoch", - "range_prefit", - "doppler_prefit", - "range_postfit", - "doppler_postfit", - "residual_ratio", - ]) - .expect("could not write to output file"); - - for res in &odp.residuals { - resid_wtr - .serialize(vec![ - res.epoch.to_string(), - format!("{}", res.prefit[0]), - format!("{}", res.prefit[1]), - format!("{}", res.postfit[0]), - format!("{}", res.postfit[1]), - format!("{}", res.ratio), - ]) - .expect("could not write to CSV"); - } + odp.to_parquet( + path.with_file_name("robustness_test_one_way.parquet"), + ExportCfg::timestamped(), + ) + .unwrap(); // Check that the covariance deflated let est = &odp.estimates[odp.estimates.len() - 1]; @@ -400,15 +343,14 @@ fn od_robust_test_ekf_realistic_two_way() { let arc = arc_sim.generate_measurements(cosm.clone()).unwrap(); // And serialize to disk - let path: PathBuf = [ - &env::var("CARGO_MANIFEST_DIR").unwrap(), - "output_data", - "ekf_robust_two_way_msr.parquet", - ] - .iter() - .collect(); + let path: PathBuf = [&env::var("CARGO_MANIFEST_DIR").unwrap(), "output_data"] + .iter() + .collect(); - arc.to_parquet_simple(path).unwrap(); + traj.to_parquet_simple(path.with_file_name("ekf_robust_two_way_traj.parquet")) + .unwrap(); + arc.to_parquet_simple(path.with_file_name("ekf_robust_two_way_msr.parquet")) + .unwrap(); println!("{arc}"); @@ -442,6 +384,14 @@ fn od_robust_test_ekf_realistic_two_way() { .map(|dev| (dev.name.clone(), dev)) .collect::>(); + // Check that exporting an empty set returns an error. + assert!(odp + .to_parquet( + path.with_file_name("robustness_test_two_way.parquet"), + ExportCfg::timestamped(), + ) + .is_err()); + odp.process( &arc.measurements, &mut devices_map, @@ -449,70 +399,128 @@ fn od_robust_test_ekf_realistic_two_way() { ) .unwrap(); - let estimate_fmtr = - NavSolutionFormatter::default("robustness_test_two_way.csv".to_owned(), cosm); - - let mut wtr = csv::Writer::from_path("robustness_test_two_way.csv").unwrap(); - wtr.serialize(&estimate_fmtr.headers) - .expect("could not write to output file"); - - let mut err_wtr = csv::Writer::from_path("robustness_test_traj_err_two_way.csv").unwrap(); - err_wtr - .serialize(vec![ - "epoch", - "x_err_km", - "y_err_km", - "z_err_km", - "vx_err_km_s", - "vy_err_km_s", - "vz_err_km_s", + let mut num_residual_none = 0; + let mut num_residual_some = 0; + odp.residuals.iter().for_each(|opt_v| match opt_v { + Some(_) => num_residual_some += 1, + None => { + num_residual_none += 1; + } + }); + + // Export as Parquet + let timestamped_path = odp + .to_parquet( + path.with_file_name("robustness_test_two_way.parquet"), + ExportCfg::timestamped(), + ) + .unwrap(); + + // Read in the Parquet file and assert proper data was written. + + // let df = LazyFrame::scan_parquet(timestamped_path, Default::default()).unwrap(); + let df = ParquetReader::new(File::open(timestamped_path).unwrap()) + .finish() + .unwrap(); + + // Note: this also checks that the columns that match the given measurement kind exist. + let df_residuals = df + .columns([ + "Prefit residual: Range (km)", + "Prefit residual: Doppler (km/s)", + "Postfit residual: Range (km)", + "Postfit residual: Doppler (km/s)", + "Residual ratio", ]) - .expect("could not write to output file"); - - for est in &odp.estimates { - // Format the estimate - wtr.serialize(estimate_fmtr.fmt(est)) - .expect("could not write to CSV"); - // Add the error data - let truth_state = traj.at(est.epoch()).unwrap(); - let err = truth_state - est.state(); - err_wtr - .serialize(vec![ - est.epoch().to_string(), - format!("{}", err.x_km), - format!("{}", err.y_km), - format!("{}", err.z_km), - format!("{}", err.vx_km_s), - format!("{}", err.vy_km_s), - format!("{}", err.vz_km_s), - ]) - .expect("could not write to CSV"); + .unwrap(); + + for series in df_residuals.iter() { + assert_eq!(series.len(), odp.estimates.len()); + let mut num_none = 0; + let mut num_some = 0; + series + .f64() + .unwrap() + .into_iter() + .for_each(|opt_v| match opt_v { + Some(_) => num_some += 1, + None => { + num_none += 1; + } + }); + + assert_eq!(num_none, num_residual_none); + assert_eq!(num_some, num_residual_some); } - let mut resid_wtr = csv::Writer::from_path("robustness_test_residuals_two_way.csv").unwrap(); - resid_wtr - .serialize(vec![ - "epoch", - "range_prefit", - "doppler_prefit", - "range_postfit", - "doppler_postfit", - "residual_ratio", + // Check that the position and velocity estimates are present, along with the epochs + assert!(df + .columns([ + "Epoch:Gregorian UTC", + "Epoch:Gregorian TAI", + "Epoch:TAI (s)", + "x (km)", + "y (km)", + "z (km)", + "vx (km/s)", + "vy (km/s)", + "vz (km/s)", ]) - .expect("could not write to output file"); - - for res in &odp.residuals { - resid_wtr - .serialize(vec![ - res.epoch.to_string(), - format!("{}", res.prefit[0]), - format!("{}", res.prefit[1]), - format!("{}", res.postfit[0]), - format!("{}", res.postfit[1]), - format!("{}", res.ratio), - ]) - .expect("could not write to CSV"); - } + .is_ok()); + + // Check that the covariance in the integration frame is present + assert!(df + .columns([ + "Covariance XX (Earth J2000)", + "Covariance XY (Earth J2000)", + "Covariance XZ (Earth J2000)", + "Covariance XVx (Earth J2000)", + "Covariance XVy (Earth J2000)", + "Covariance XVz (Earth J2000)", + "Covariance YY (Earth J2000)", + "Covariance YZ (Earth J2000)", + "Covariance YVx (Earth J2000)", + "Covariance YVy (Earth J2000)", + "Covariance YVz (Earth J2000)", + "Covariance ZZ (Earth J2000)", + "Covariance ZVx (Earth J2000)", + "Covariance ZVy (Earth J2000)", + "Covariance ZVz (Earth J2000)", + "Covariance VxVx (Earth J2000)", + "Covariance VxVy (Earth J2000)", + "Covariance VxVz (Earth J2000)", + "Covariance VyVy (Earth J2000)", + "Covariance VyVz (Earth J2000)", + "Covariance VzVz (Earth J2000)", + ]) + .is_ok()); + + // Check that the covariance in the RIC frame is present + assert!(df + .columns([ + "Covariance XX (RIC)", + "Covariance XY (RIC)", + "Covariance XZ (RIC)", + "Covariance XVx (RIC)", + "Covariance XVy (RIC)", + "Covariance XVz (RIC)", + "Covariance YY (RIC)", + "Covariance YZ (RIC)", + "Covariance YVx (RIC)", + "Covariance YVy (RIC)", + "Covariance YVz (RIC)", + "Covariance ZZ (RIC)", + "Covariance ZVx (RIC)", + "Covariance ZVy (RIC)", + "Covariance ZVz (RIC)", + "Covariance VxVx (RIC)", + "Covariance VxVy (RIC)", + "Covariance VxVz (RIC)", + "Covariance VyVy (RIC)", + "Covariance VyVz (RIC)", + "Covariance VzVz (RIC)", + ]) + .is_ok()); // Check that the covariance deflated let est = &odp.estimates[odp.estimates.len() - 1]; diff --git a/tests/orbit_determination/spacecraft.rs b/tests/orbit_determination/spacecraft.rs index 1616e602..2c5dec10 100644 --- a/tests/orbit_determination/spacecraft.rs +++ b/tests/orbit_determination/spacecraft.rs @@ -1,11 +1,9 @@ -extern crate csv; extern crate nyx_space as nyx; extern crate pretty_env_logger; use nyx::cosmic::{Bodies, Cosm, Orbit, Spacecraft}; use nyx::dynamics::orbital::OrbitalDynamics; use nyx::dynamics::spacecraft::{SolarPressure, SpacecraftDynamics}; -use nyx::io::formatter::NavSolutionFormatter; use nyx::linalg::{Matrix2, Matrix6, Vector2, Vector6}; use nyx::md::trajectory::ExportCfg; use nyx::md::{Event, StateParameter}; @@ -141,7 +139,7 @@ fn od_val_sc_mb_srp_reals_duals_models() { let arc = arc_sim.generate_measurements(cosm.clone()).unwrap(); - arc.to_parquet(path.with_file_name("sc_msr_arc.parquet"), None, false) + arc.to_parquet_simple(path.with_file_name("sc_msr_arc.parquet")) .unwrap(); // Now that we have the truth data, let's start an OD with no noise at all and compute the estimates. @@ -175,12 +173,12 @@ fn od_val_sc_mb_srp_reals_duals_models() { odp.process_arc::(&arc).unwrap(); - // Initialize the formatter - let estimate_fmtr = NavSolutionFormatter::default("sc_ckf.csv".to_owned(), cosm); + odp.to_parquet( + path.with_file_name("spacecraft_od_results.parquet"), + ExportCfg::timestamped(), + ) + .unwrap(); - let mut wtr = csv::Writer::from_path("sc_ckf.csv").unwrap(); - wtr.serialize(&estimate_fmtr.headers) - .expect("could not write to stdout"); for (no, est) in odp.estimates.iter().enumerate() { if no == 0 { // Skip the first estimate which is the initial estimate provided by user @@ -199,13 +197,9 @@ fn od_val_sc_mb_srp_reals_duals_models() { "estimate error should be zero (perfect dynamics) ({:e})", est.state_deviation().norm() ); - - // Format the estimate - wtr.serialize(estimate_fmtr.fmt(est)) - .expect("could not write to CSV"); } - for res in &odp.residuals { + for res in odp.residuals.iter().flatten() { assert!( res.postfit.norm() < 1e-5, "postfit should be zero (perfect dynamics) ({:e})", diff --git a/tests/orbit_determination/two_body.rs b/tests/orbit_determination/two_body.rs index 58c88e88..cda71326 100644 --- a/tests/orbit_determination/two_body.rs +++ b/tests/orbit_determination/two_body.rs @@ -1,13 +1,11 @@ -extern crate csv; extern crate nyx_space as nyx; extern crate pretty_env_logger; use nyx::cosmic::{Cosm, Orbit}; use nyx::dynamics::orbital::OrbitalDynamics; use nyx::dynamics::sph_harmonics::Harmonics; -use nyx::io::formatter::NavSolutionFormatter; -use nyx::io::gravity::*; use nyx::io::ConfigRepr; +use nyx::io::{gravity::*, ExportCfg}; use nyx::linalg::{Matrix2, Matrix6, Vector2, Vector6}; use nyx::od::noise::GaussMarkov; use nyx::od::prelude::*; @@ -358,7 +356,6 @@ fn od_tb_val_ckf_fixed_step_perfect_stations() { if pretty_env_logger::try_init().is_err() { println!("could not init env_logger"); } - use std::io; let cosm = Cosm::de438(); let iau_earth = cosm.frame("IAU Earth"); @@ -454,12 +451,12 @@ fn od_tb_val_ckf_fixed_step_perfect_stations() { odp.process_arc::(&arc).unwrap(); - // Initialize the formatter - let estimate_fmtr = NavSolutionFormatter::default("tb_ckf.csv".to_owned(), cosm); + let path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "output_data", "tb_ckf.parquet"] + .iter() + .collect(); + + odp.to_parquet(path, ExportCfg::default()).unwrap(); - let mut wtr = csv::Writer::from_writer(io::stdout()); - wtr.serialize(&estimate_fmtr.headers) - .expect("could not write to stdout"); // Check that we have as many estimates as steps taken by the propagator. // Note that this test cannot work when using a variable step propagator in that same setup. // We're adding +1 because the propagation time is inclusive on both ends. @@ -483,7 +480,6 @@ fn od_tb_val_ckf_fixed_step_perfect_stations() { "Different number of estimates received" ); - let mut printed = false; for (no, est) in odp.estimates.iter().enumerate() { if no == 0 { // Skip the first estimate which is the initial estimate provided by user @@ -504,16 +500,9 @@ fn od_tb_val_ckf_fixed_step_perfect_stations() { "estimate error should be zero (perfect dynamics) ({:e})", est.state_deviation().norm() ); - - if !printed { - // Format the estimate - wtr.serialize(estimate_fmtr.fmt(est)) - .expect("could not write to stdout"); - printed = true; - } } - for res in &odp.residuals { + for res in odp.residuals.iter().flatten() { assert!( res.postfit.norm() < 1e-12, "postfit should be zero (perfect dynamics) ({:e})", @@ -871,9 +860,7 @@ fn od_tb_ckf_fixed_step_perfect_stations_snc_covar_map() { odp.process_arc::(&arc).unwrap(); - let mut wtr = csv::Writer::from_path("./estimation.csv").unwrap(); - - // Let's export these to a CSV file, and also check that the covariance never falls below our sigma squared values + // Let's check that the covariance never falls below our sigma squared values for (no, est) in odp.estimates.iter().enumerate() { if no == 1 { println!("{}", est); @@ -901,8 +888,6 @@ fn od_tb_ckf_fixed_step_perfect_stations_snc_covar_map() { est.covar[(i, i)] ); } - - wtr.serialize(*est).expect("could not write to stdout"); } // Check the final estimate @@ -929,7 +914,7 @@ fn od_tb_ckf_map_covar() { let cosm = Cosm::de438(); // Define the propagator information. - let prop_time = 2 * Unit::Day; + let duration = 2 * Unit::Day; let step_size = 10.0 * Unit::Second; // Define state information. @@ -971,7 +956,7 @@ fn od_tb_ckf_map_covar() { KF, nalgebra::Const<2>>, > = ODProcess::ckf(prop_est, ckf, None, cosm); - odp.map_covar(dt + prop_time).unwrap(); + odp.predict_for(30.seconds(), duration).unwrap(); // Check that the covariance inflated (we don't get the norm of the estimate because it's zero without any truth data) let estimates = odp.estimates; @@ -1101,9 +1086,8 @@ fn od_tb_val_harmonics_ckf_fixed_step_perfect() { let mut odp = ODProcess::ckf(prop_est, ckf, None, cosm); odp.process_arc::(&arc).unwrap(); - let mut wtr = csv::Writer::from_path("./estimation.csv").unwrap(); - // Let's export these to a CSV file, and also check that the covariance never falls below our sigma squared values + // Let's check that the covariance never falls below our sigma squared values for (no, est) in odp.estimates.iter().enumerate() { if no == 1 { println!("{}", est); @@ -1121,8 +1105,6 @@ fn od_tb_val_harmonics_ckf_fixed_step_perfect() { "estimate error should be good (perfect dynamics) ({:e})", est.state_deviation().norm() ); - - wtr.serialize(*est).expect("could not write to stdout"); } // Check the final estimate @@ -1255,9 +1237,7 @@ fn od_tb_ckf_fixed_step_perfect_stations_several_snc_covar_map() { odp.process_arc::(&arc).unwrap(); - let mut wtr = csv::Writer::from_path("./estimation.csv").unwrap(); - - // Let's export these to a CSV file, and also check that the covariance never falls below our sigma squared values + // Let's check that the covariance never falls below our sigma squared values for (no, est) in odp.estimates.iter().enumerate() { if no == 1 { println!("{}", est); @@ -1276,8 +1256,6 @@ fn od_tb_ckf_fixed_step_perfect_stations_several_snc_covar_map() { i ); } - - wtr.serialize(*est).expect("could not write to stdout"); } // Check the final estimate diff --git a/tests/orbit_determination/xhat_dev.rs b/tests/orbit_determination/xhat_dev.rs index da6bf7ba..1f8ac559 100644 --- a/tests/orbit_determination/xhat_dev.rs +++ b/tests/orbit_determination/xhat_dev.rs @@ -1,4 +1,3 @@ -extern crate csv; extern crate nyx_space as nyx; extern crate pretty_env_logger; diff --git a/tests/propagation/trajectory.rs b/tests/propagation/trajectory.rs index 77575244..724fe9c7 100644 --- a/tests/propagation/trajectory.rs +++ b/tests/propagation/trajectory.rs @@ -2,12 +2,12 @@ extern crate nyx_space as nyx; extern crate pretty_env_logger; use hifitime::TimeUnits; +use nyx::cosmic::eclipse::EclipseLocator; use nyx::cosmic::{Cosm, GuidanceMode, Orbit, Spacecraft}; use nyx::dynamics::guidance::{GuidanceLaw, Ruggiero, Thruster}; use nyx::dynamics::{OrbitalDynamics, SpacecraftDynamics}; use nyx::io::trajectory_data::DynamicTrajectory; -use nyx::md::prelude::Objective; -use nyx::md::trajectory::Interpolatable; +use nyx::md::prelude::{ExportCfg, Interpolatable, Objective}; use nyx::md::StateParameter; use nyx::propagators::*; use nyx::time::{Epoch, TimeSeries, Unit}; @@ -120,11 +120,19 @@ fn traj_ephem_forward() { .iter() .collect(); - ephem.to_parquet_simple(&path).unwrap(); + let exported_path = ephem + .to_parquet( + path, + Some(vec![ + &EclipseLocator::cislunar(cosm.clone()).to_penumbra_event() + ]), + ExportCfg::timestamped(), + ) + .unwrap(); // Reload this trajectory and make sure that it matches - let dyn_traj = DynamicTrajectory::from_parquet(path).unwrap(); + let dyn_traj = DynamicTrajectory::from_parquet(exported_path).unwrap(); let concrete_traj = dyn_traj.to_traj::().unwrap(); if ephem != concrete_traj { diff --git a/tests/python/test_mission_design.py b/tests/python/test_mission_design.py index aa003094..030b91b7 100644 --- a/tests/python/test_mission_design.py +++ b/tests/python/test_mission_design.py @@ -8,6 +8,7 @@ StateParameter, Event, SpacecraftDynamics, + DynamicTrajectory ) from nyx_space.time import Duration, Unit, Epoch @@ -86,6 +87,9 @@ def test_propagate(): ) assert abs(rslt_apo.orbit.ta_deg() - 180.0) <= 1e-6 + # Resample the trajectory at fixed step size of 25 seconds + traj = traj.resample(Unit.Second * 25.0) + # Export this trajectory with additional metadata and the events traj.to_parquet( "lofi_with_events.parquet", metadata={"test key": "test value"}, events=[event] @@ -106,6 +110,10 @@ def test_build_spacecraft(): """ Tests that we can build a spacecraft from scratch without an input file """ + # Initialize logging + FORMAT = "%(levelname)s %(name)s %(filename)s:%(lineno)d\t%(message)s" + logging.basicConfig(format=FORMAT) + logging.getLogger().setLevel(logging.INFO) cosm = Cosm.de438() eme2k = cosm.frame("EME2000") # Earth Mean Equator J2000 @@ -126,7 +134,19 @@ def test_build_spacecraft(): sc.drag().cd == 2.2 ) # Default value, but the area is zero, so it doesn't have any effect + # Using this spacecraft as a template, let's load an OEM file, convert it to Parquet, and ensure we can load it back in. + # The orbit data will be overwritten with data from the OEM file. + root = Path(__file__).joinpath("../../../").resolve() + + config_path = root.joinpath("./data/tests/ccsds/oem/LEO_10s.oem") + output_path = root.joinpath("./output_data/LEO_10s.parquet") + # Convert + DynamicTrajectory.convert_oem_to_parquet(str(config_path), str(output_path), sc) + # Reload + traj = DynamicTrajectory(str(output_path)) + print(traj) + if __name__ == "__main__": - test_propagate() + # test_propagate() test_build_spacecraft() diff --git a/tests/python/test_orbit_determination.py b/tests/python/test_orbit_determination.py index 03cff22b..77bf8108 100644 --- a/tests/python/test_orbit_determination.py +++ b/tests/python/test_orbit_determination.py @@ -1,6 +1,8 @@ import logging from pathlib import Path import numpy as np +import pandas as pd +import sys from nyx_space.orbit_determination import ( GroundStation, @@ -8,11 +10,20 @@ TrkConfig, OrbitEstimate, process_tracking_arc, + predictor, DynamicTrackingArc, + ExportCfg, ) from nyx_space.mission_design import DynamicTrajectory, SpacecraftDynamics, propagate from nyx_space.cosmic import Spacecraft from nyx_space.time import Unit +from nyx_space.plots.od import ( + plot_covar, + plot_estimates, + plot_residuals, + plot_residual_histogram, +) +from nyx_space.plots.traj import plot_orbit_elements def test_filter_arc(): @@ -32,8 +43,10 @@ def test_filter_arc(): # An propagate for two periods (we only care about the trajectory) _, traj = propagate(sc, dynamics["hifi"], sc.orbit.period() * 2) + # Resample the trajectory at fixed step size + traj = traj.resample(Unit.Second * 10.0) # And save the trajectory - traj_file = str(outpath.joinpath("./od_val_with_arc_truth_ephem.parquet")) + traj_file = str(outpath.joinpath("./python_ref_traj.parquet")) traj.to_parquet(traj_file) # Now starts the measurement generation @@ -60,50 +73,109 @@ def test_filter_arc(): # Load the trajectory traj = DynamicTrajectory(traj_file) print(f"Loaded {traj}") + + # Set up the export -- We'll use the same config set up for both measurements and output of OD process + cfg = ExportCfg(timestamp=True, metadata={"test key": "test value"}) + # Build the simulated tracking arc, setting the seed to zero arc_sim = GroundTrackingArcSim(devices, traj, trk_cfg, 0) # Generate the measurements - path = arc_sim.generate_measurements( - str(outpath.joinpath("./from_python.parquet")), - timestamp=True, - metadata={"test key": "test value"}, + msr_path = arc_sim.generate_measurements( + str(outpath.joinpath("./msr.parquet")), cfg ) - print(f"Saved {arc_sim} to {path}") + print(f"Saved {arc_sim} to {msr_path}") # Now let's filter this same data. # Load the tracking arc - arc = DynamicTrackingArc(path) + arc = DynamicTrackingArc(msr_path) # Create the orbit estimate with the covariance diagonal (100 km on position and 1 km/s on velocity) orbit_est = OrbitEstimate( sc.orbit, covar=np.diag([100.0, 100.0, 100.0, 1.0, 1.0, 1.0]) ) - msr_noise = [1e-3, 0, 0, 1e-6] # TODO: Convert this to a numpy matrix + msr_noise = [1e-3, 0, 0, 1e-6] # Switch from sequential to EKF after 100 measurements ekf_num_msr_trig = 100 # Unless there is a 2 hour gap in the measurements, and then switch back to classical ekf_disable_time = Unit.Hour * 2 - estimates = process_tracking_arc( + rslt_path = process_tracking_arc( dynamics["hifi"], sc, orbit_est, msr_noise, arc, + str(outpath.joinpath("./od_result.parquet")), + cfg, ekf_num_msr_trig, ekf_disable_time, + # predict_for=Unit.Hour * 12, # You can predict from the final estimate by uncommenting this line. ) - assert len(estimates) == 1063 - assert len([est for est in estimates if not est.is_predicted]) == 762 + print(f"Stored to {rslt_path}") + + # Load the results + oddf = pd.read_parquet(rslt_path) + # Load the reference trajectory + ref_traj = pd.read_parquet(traj_file) + # Load the measurements + msr_df = pd.read_parquet(msr_path) + + # There seems to be a bug when exporting the HTML on Github action windows + # cf. https://github.com/nyx-space/nyx/actions/runs/5064848025/jobs/9092830624 + if sys.platform != "win32": + # We'll plot the difference between the reference trajectory and the OD results, with the measurement bands overlaid. + plot_estimates( + oddf, + "OD results from Python", + cov_frame="RIC", + ref_traj=ref_traj, + msr_df=msr_df, + html_out=str(outpath.joinpath("./od_estimate_plots.html")), + show=False, + ) - # TODO: Add more tests + # Let's also plot the reference and the OD result's orbital elements + plot_orbit_elements( + [ref_traj, oddf], + "Python OD result", + ["Reference", "OD"], + html_out=str(outpath.joinpath("./od_vs_ref_elements.html")), + show=False, + ) + + # More often, the covariance is a better indicator + plot_covar( + oddf, + "OD 1-sigma covar from Python", + cov_sigma=1.0, + msr_df=msr_df, + html_out=str(outpath.joinpath("./od_covar_plots.html")), + show=False, + ) + + # Now, we'll plot the prefit residuals + plot_residuals( + oddf, + "OD residuals", + msr_df=msr_df, + html_out=str(outpath.joinpath("./od_residual_plots.html")), + show=False, + ) + # And the postfit histograms + plot_residual_histogram( + oddf, + "OD residuals", + kind="Postfit", + html_out=str(outpath.joinpath("./od_residual_histograms.html")), + show=False, + ) def test_one_way_msr(): """ - Test that we can perform a one-way measurement + Test that we can manually perform a one-way measurement """ # Base path @@ -132,5 +204,67 @@ def test_one_way_msr(): assert abs(doppler_km_s - -0.2498238312640348) < 0.1 +def test_pure_prediction(): + # Initialize logging + FORMAT = "%(levelname)s %(name)s %(asctime)-15s %(filename)s:%(lineno)d %(message)s" + logging.basicConfig(format=FORMAT) + logging.getLogger().setLevel(logging.INFO) + + # Base path + root = Path(__file__).joinpath("../../../").resolve() + config_path = root.joinpath("./data/tests/config/") + outpath = root.joinpath("output_data/") + + # Load the dynamics and spacecraft + sc = Spacecraft.load(str(config_path.joinpath("spacecraft.yaml"))) + dynamics = SpacecraftDynamics.load_named(str(config_path.joinpath("dynamics.yaml"))) + + # Set up the export -- We'll use the same config set up for both measurements and output of OD process + cfg = ExportCfg(timestamp=True, metadata={"test key": "test value"}) + + # We'll assume that we have a good estimate of the spacecraft's orbit before we predict it forward in time + # Hence, create the orbit estimate with the covariance diagonal (100 m on position and 50 m/s on velocity) + orbit_est = OrbitEstimate( + sc.orbit, + covar=np.diag([100.0e-3, 100.0e-3, 100.0e-3, 50.0e-3, 50.0e-3, 50.0e-3]), + ) + + rslt_path = predictor( + dynamics["hifi"], + sc, + orbit_est, + Unit.Second * 15.0, + str(outpath.joinpath("./od_pred_result.parquet")), + cfg, + predict_for=Unit.Hour * 12, + ) + + print(f"Stored to {rslt_path}") + + # Load the prediction results + oddf = pd.read_parquet(rslt_path) + + # There seems to be a bug when exporting the HTML on Github action windows + # cf. https://github.com/nyx-space/nyx/actions/runs/5064848025/jobs/9092830624 + if sys.platform != "win32": + # Let's plot the OD result's orbital elements + plot_orbit_elements( + oddf, + "OD prediction result", + html_out=str(outpath.joinpath("./od_pred_elements.html")), + show=False, + ) + + # More often, the covariance is a better indicator + plot_covar( + oddf, + "OD 1-sigma covar from Python", + cov_sigma=1.0, + cov_frame="Earth J2000", + html_out=str(outpath.joinpath("./od_pred_covar_plots.html")), + show=False, + ) + + if __name__ == "__main__": - test_one_way_msr() + test_pure_prediction()