ruststartracker 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/PKG-INFO +41 -6
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/README.md +40 -5
- ruststartracker-0.2.4/build_script.py +126 -0
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/pyproject.toml +4 -2
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/catalog.py +29 -9
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/libruststartracker.pyi +40 -7
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/star.py +30 -5
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/test_backend.py +17 -6
- ruststartracker-0.2.4/ruststartracker/test_catalog.py +101 -0
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/test_integration.py +3 -1
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/test_star.py +45 -12
- ruststartracker-0.2.2/build_script.py +0 -60
- ruststartracker-0.2.2/ruststartracker/test_catalog.py +0 -47
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/LICENSE +0 -0
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/__init__.py +0 -0
- {ruststartracker-0.2.2 → ruststartracker-0.2.4}/ruststartracker/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ruststartracker
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Lightweight Python Star Tracker With Rust Backend
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Nicolas Tobler
|
|
@@ -34,6 +34,35 @@ Features:
|
|
|
34
34
|
|
|
35
35
|
## Example
|
|
36
36
|
|
|
37
|
+
### Rust
|
|
38
|
+
|
|
39
|
+
See [examples/basic.rs](examples/basic.rs)
|
|
40
|
+
|
|
41
|
+
```rust
|
|
42
|
+
// Get catalog positions
|
|
43
|
+
let catalog: StarCatalog = StarCatalog::from_gaia(max_magnitude: ...).unwrap();
|
|
44
|
+
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(epoch: ..., observer_position: ...);
|
|
45
|
+
let stars_mag: Vec<f32> = catalog.magnitudes();
|
|
46
|
+
|
|
47
|
+
// Create StarTracker instance (reuse this)
|
|
48
|
+
let star_matcher = StarMatcher::new(
|
|
49
|
+
stars_xyz,
|
|
50
|
+
stars_mag,
|
|
51
|
+
max_lookup_magnitude: ...
|
|
52
|
+
max_inter_star_angle: ...,
|
|
53
|
+
inter_star_angle_tolerance: ...,
|
|
54
|
+
min_matches: ...,
|
|
55
|
+
timeout: ...
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Normalized observation in the camera frame
|
|
59
|
+
let obs_xyz_camera: Vec<[f32; 3]> = ...
|
|
60
|
+
|
|
61
|
+
let result = star_matcher.find(&obs_xyz_camera);
|
|
62
|
+
println!("Result: {:?}", result);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Python
|
|
37
66
|
```python
|
|
38
67
|
import ruststartracker
|
|
39
68
|
|
|
@@ -72,11 +101,6 @@ print(result)
|
|
|
72
101
|
|
|
73
102
|
- Install with `pip install ruststartracker` (Currently only ARM/x86 Linux wheels available).
|
|
74
103
|
|
|
75
|
-
## TODOs
|
|
76
|
-
|
|
77
|
-
- Improve error messages.
|
|
78
|
-
- Return more diagnostic data.
|
|
79
|
-
|
|
80
104
|
## Attributions
|
|
81
105
|
|
|
82
106
|
### Gaia Data
|
|
@@ -89,3 +113,14 @@ Gaia DR3 data is © European Space Agency and is released under the [**Creative
|
|
|
89
113
|
> Gaia Collaboration, Vallenari et al. (2022), *A\&A* **674**, A1.
|
|
90
114
|
> [DOI: 10.1051/0004-6361/202243940](https://doi.org/10.1051/0004-6361/202243940)
|
|
91
115
|
|
|
116
|
+
### Hipparcos and Tycho Data
|
|
117
|
+
|
|
118
|
+
This project includes data from the European Space Agency (ESA) mission **Hipparcos**.
|
|
119
|
+
|
|
120
|
+
The Hipparcos and Tycho Catalogues were processed by the Hipparcos and Tycho Data Analysis Consortium.
|
|
121
|
+
|
|
122
|
+
The Hipparcos and Tycho Catalogues are © European Space Agency and are released under the [**Creative Commons Attribution 3.0 IGO (CC BY 3.0 IGO)**](https://creativecommons.org/licenses/by/3.0/igo/) license.
|
|
123
|
+
|
|
124
|
+
> Perryman, M. A. C., et al. (1997), *Astronomy & Astrophysics* **323**, L49-L52.
|
|
125
|
+
1997A&A...323L..49P
|
|
126
|
+
|
|
@@ -13,6 +13,35 @@ Features:
|
|
|
13
13
|
|
|
14
14
|
## Example
|
|
15
15
|
|
|
16
|
+
### Rust
|
|
17
|
+
|
|
18
|
+
See [examples/basic.rs](examples/basic.rs)
|
|
19
|
+
|
|
20
|
+
```rust
|
|
21
|
+
// Get catalog positions
|
|
22
|
+
let catalog: StarCatalog = StarCatalog::from_gaia(max_magnitude: ...).unwrap();
|
|
23
|
+
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(epoch: ..., observer_position: ...);
|
|
24
|
+
let stars_mag: Vec<f32> = catalog.magnitudes();
|
|
25
|
+
|
|
26
|
+
// Create StarTracker instance (reuse this)
|
|
27
|
+
let star_matcher = StarMatcher::new(
|
|
28
|
+
stars_xyz,
|
|
29
|
+
stars_mag,
|
|
30
|
+
max_lookup_magnitude: ...
|
|
31
|
+
max_inter_star_angle: ...,
|
|
32
|
+
inter_star_angle_tolerance: ...,
|
|
33
|
+
min_matches: ...,
|
|
34
|
+
timeout: ...
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Normalized observation in the camera frame
|
|
38
|
+
let obs_xyz_camera: Vec<[f32; 3]> = ...
|
|
39
|
+
|
|
40
|
+
let result = star_matcher.find(&obs_xyz_camera);
|
|
41
|
+
println!("Result: {:?}", result);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Python
|
|
16
45
|
```python
|
|
17
46
|
import ruststartracker
|
|
18
47
|
|
|
@@ -51,11 +80,6 @@ print(result)
|
|
|
51
80
|
|
|
52
81
|
- Install with `pip install ruststartracker` (Currently only ARM/x86 Linux wheels available).
|
|
53
82
|
|
|
54
|
-
## TODOs
|
|
55
|
-
|
|
56
|
-
- Improve error messages.
|
|
57
|
-
- Return more diagnostic data.
|
|
58
|
-
|
|
59
83
|
## Attributions
|
|
60
84
|
|
|
61
85
|
### Gaia Data
|
|
@@ -67,3 +91,14 @@ Gaia DR3 data is © European Space Agency and is released under the [**Creative
|
|
|
67
91
|
|
|
68
92
|
> Gaia Collaboration, Vallenari et al. (2022), *A\&A* **674**, A1.
|
|
69
93
|
> [DOI: 10.1051/0004-6361/202243940](https://doi.org/10.1051/0004-6361/202243940)
|
|
94
|
+
|
|
95
|
+
### Hipparcos and Tycho Data
|
|
96
|
+
|
|
97
|
+
This project includes data from the European Space Agency (ESA) mission **Hipparcos**.
|
|
98
|
+
|
|
99
|
+
The Hipparcos and Tycho Catalogues were processed by the Hipparcos and Tycho Data Analysis Consortium.
|
|
100
|
+
|
|
101
|
+
The Hipparcos and Tycho Catalogues are © European Space Agency and are released under the [**Creative Commons Attribution 3.0 IGO (CC BY 3.0 IGO)**](https://creativecommons.org/licenses/by/3.0/igo/) license.
|
|
102
|
+
|
|
103
|
+
> Perryman, M. A. C., et al. (1997), *Astronomy & Astrophysics* **323**, L49-L52.
|
|
104
|
+
1997A&A...323L..49P
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Build rust backend.
|
|
2
|
+
|
|
3
|
+
This script is automatically run when the pyproject is installed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import csv
|
|
7
|
+
import io
|
|
8
|
+
import pathlib
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def download_gaia_data(output_file: pathlib.Path) -> None:
|
|
14
|
+
"""Download Gaia data and save it to the specified output file.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
output_file: Path to save the downloaded Gaia data (csv).
|
|
18
|
+
"""
|
|
19
|
+
from astroquery.gaia import Gaia
|
|
20
|
+
|
|
21
|
+
Gaia.ROW_LIMIT = -1
|
|
22
|
+
|
|
23
|
+
query = """
|
|
24
|
+
SELECT source_id, ra, dec, parallax, pmra, pmdec, phot_g_mean_mag
|
|
25
|
+
FROM gaiadr3.gaia_source
|
|
26
|
+
WHERE phot_g_mean_mag < 7
|
|
27
|
+
AND parallax > 0
|
|
28
|
+
AND astrometric_params_solved = 31
|
|
29
|
+
AND visibility_periods_used >= 8
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
print("Executing query on Gaia database...")
|
|
33
|
+
job = Gaia.launch_job_async(
|
|
34
|
+
query,
|
|
35
|
+
dump_to_file=True,
|
|
36
|
+
output_format="csv",
|
|
37
|
+
output_file=str(output_file.absolute()),
|
|
38
|
+
verbose=True,
|
|
39
|
+
)
|
|
40
|
+
if not job.is_finished():
|
|
41
|
+
print("Job did not finish successfully.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
print(f"Saved to: {output_file}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def download_hipparcos_data(output_file: pathlib.Path) -> None:
|
|
48
|
+
"""Download Hipparcos data and save it to the specified output file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
output_file: Path to save the downloaded Hipparcos data (csv).
|
|
52
|
+
"""
|
|
53
|
+
from astroquery.vizier import Vizier
|
|
54
|
+
|
|
55
|
+
Vizier.ROW_LIMIT = -1
|
|
56
|
+
|
|
57
|
+
print("Executing query on Hipparcos database...")
|
|
58
|
+
query = Vizier.query_constraints(
|
|
59
|
+
catalog=["I/311/hip2"],
|
|
60
|
+
Hpmag="<7",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not query:
|
|
64
|
+
print("Job did not finish successfully.")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Initialize a string buffer to write the CSV data to
|
|
68
|
+
csv_output = io.StringIO()
|
|
69
|
+
writer = csv.writer(csv_output)
|
|
70
|
+
|
|
71
|
+
# Write the header row
|
|
72
|
+
# Use column names that are common in Gaia and Hipparcos
|
|
73
|
+
header = ["source_id", "ra", "dec", "parallax", "pmra", "pmdec", "phot_g_mean_mag"]
|
|
74
|
+
writer.writerow(header)
|
|
75
|
+
|
|
76
|
+
# Iterate over the rows of the Astropy table
|
|
77
|
+
for row in query[0]:
|
|
78
|
+
# Extract the required data points
|
|
79
|
+
source_id = row["HIP"]
|
|
80
|
+
ra = row["RArad"]
|
|
81
|
+
dec = row["DErad"]
|
|
82
|
+
parallax = row["Plx"]
|
|
83
|
+
pmra = row["pmRA"]
|
|
84
|
+
pmdec = row["pmDE"]
|
|
85
|
+
# Use Vmag as the proxy for phot_g_mean_mag
|
|
86
|
+
phot_g_mean_mag = row["Hpmag"]
|
|
87
|
+
|
|
88
|
+
# Create a list for the new row and write it to the CSV writer
|
|
89
|
+
new_row = [source_id, ra, dec, parallax, pmra, pmdec, phot_g_mean_mag]
|
|
90
|
+
writer.writerow(new_row)
|
|
91
|
+
|
|
92
|
+
# Get the complete CSV string from the buffer
|
|
93
|
+
csv_string = csv_output.getvalue()
|
|
94
|
+
|
|
95
|
+
# You can save this string to a file
|
|
96
|
+
with output_file.open("w") as f:
|
|
97
|
+
f.write(csv_string)
|
|
98
|
+
|
|
99
|
+
print(f"Saved to: {output_file}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_script() -> None:
|
|
103
|
+
"""Build rust backend and move shared library to correct folder."""
|
|
104
|
+
cwd = pathlib.Path(__file__).parent.expanduser().absolute()
|
|
105
|
+
|
|
106
|
+
gaia_file = cwd / "ruststartracker/gaia_data_j2016.csv"
|
|
107
|
+
if not gaia_file.exists():
|
|
108
|
+
download_gaia_data(gaia_file)
|
|
109
|
+
|
|
110
|
+
hipparcos_file = cwd / "ruststartracker/hipparcos_data_j1991.25.csv"
|
|
111
|
+
if not hipparcos_file.exists():
|
|
112
|
+
download_hipparcos_data(hipparcos_file)
|
|
113
|
+
|
|
114
|
+
subprocess.check_call( # noqa: S603
|
|
115
|
+
["cargo", "build", "--release", "--features", "improc,gaia,hipparcos"], # noqa: S607
|
|
116
|
+
cwd=cwd,
|
|
117
|
+
stdout=None,
|
|
118
|
+
stderr=None,
|
|
119
|
+
)
|
|
120
|
+
shutil.copy(
|
|
121
|
+
cwd / "target/release/libruststartracker.so", cwd / "ruststartracker/libruststartracker.so"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
build_script()
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ruststartracker"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Lightweight Python Star Tracker With Rust Backend"
|
|
5
5
|
authors = ["Nicolas Tobler <nitobler@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
include = [
|
|
9
9
|
{ path = "ruststartracker/gaia_data_j2016.csv", format = ["sdist", "wheel"] },
|
|
10
|
+
{ path = "ruststartracker/hipparcos_data_j1991.25.csv", format = ["sdist", "wheel"] },
|
|
10
11
|
{ path = "ruststartracker/libruststartracker.so", format = ["wheel"] },
|
|
11
12
|
]
|
|
12
13
|
|
|
@@ -33,6 +34,7 @@ scipy-stubs = "^1.14.1.5"
|
|
|
33
34
|
ruff = "^0.9.3"
|
|
34
35
|
bump-my-version = "^1.2.0"
|
|
35
36
|
astroquery = "^0.4.10"
|
|
37
|
+
typing-extensions = "^4.12.2"
|
|
36
38
|
|
|
37
39
|
[tool.poetry.build]
|
|
38
40
|
script = "build_script.py"
|
|
@@ -42,7 +44,7 @@ requires = ["poetry-core", "astroquery>=0.4.10"]
|
|
|
42
44
|
build-backend = "poetry.core.masonry.api"
|
|
43
45
|
|
|
44
46
|
[tool.bumpversion]
|
|
45
|
-
current_version = "0.2.
|
|
47
|
+
current_version = "0.2.4"
|
|
46
48
|
commit = true
|
|
47
49
|
tag = true
|
|
48
50
|
tag_name = "v{new_version}"
|
|
@@ -7,14 +7,17 @@ import pathlib
|
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import numpy.typing as npt
|
|
10
|
+
from typing_extensions import Self
|
|
10
11
|
|
|
11
12
|
AU: float = 149597870.693
|
|
12
13
|
"""Astronomical unit."""
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
GAIA_CATALOG_FILE = pathlib.Path(__file__).parent.expanduser().absolute() / "gaia_data_j2016.csv"
|
|
16
|
+
"""Location of the internal Gaia star catalog file."""
|
|
17
|
+
HIPPARCOS_CATALOG_FILE = (
|
|
18
|
+
pathlib.Path(__file__).parent.expanduser().absolute() / "hipparcos_data_j1991.25.csv"
|
|
16
19
|
)
|
|
17
|
-
"""Location of the internal star catalog file."""
|
|
20
|
+
"""Location of the internal Hipparcos star catalog file."""
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def time_to_epoch(t: datetime.datetime) -> float:
|
|
@@ -44,24 +47,41 @@ class StarCatalog:
|
|
|
44
47
|
"""Proper motion of the declination in mad."""
|
|
45
48
|
magnitude: npt.NDArray[np.float32]
|
|
46
49
|
"""Magnitude values."""
|
|
47
|
-
epoch: float
|
|
50
|
+
epoch: float
|
|
48
51
|
"""Epoch of the catalog in years."""
|
|
49
52
|
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_gaia(cls, *, max_magnitude: float = 6.0) -> Self:
|
|
55
|
+
"""Read internally provided Gaia catalog file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
max_magnitude: Maximum magnitude to include.
|
|
59
|
+
"""
|
|
60
|
+
return cls(GAIA_CATALOG_FILE, epoch=2016.0, max_magnitude=max_magnitude)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_hipparcos(cls, *, max_magnitude: float = 6.0) -> Self:
|
|
64
|
+
"""Read internally provided Hipparcos catalog file.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
max_magnitude: Maximum magnitude to include.
|
|
68
|
+
"""
|
|
69
|
+
return cls(HIPPARCOS_CATALOG_FILE, epoch=1991.25, max_magnitude=max_magnitude)
|
|
70
|
+
|
|
50
71
|
def __init__(
|
|
51
|
-
self, filename: pathlib.Path | str
|
|
72
|
+
self, filename: pathlib.Path | str, *, epoch: float, max_magnitude: float = 6.0
|
|
52
73
|
) -> None:
|
|
53
74
|
"""Read catalog from file.
|
|
54
75
|
|
|
55
76
|
Args:
|
|
56
77
|
filename: Star catalog filename.
|
|
78
|
+
epoch: Epoch of the catalog in years.
|
|
57
79
|
max_magnitude: Maximum magnitude to include.
|
|
58
80
|
"""
|
|
59
|
-
|
|
60
|
-
filename = INTERNAL_CATALOG_FILE if filename is None else pathlib.Path(filename)
|
|
61
|
-
|
|
81
|
+
self.epoch = epoch
|
|
62
82
|
keep_columns = ("ra", "dec", "parallax", "pmra", "pmdec", "phot_g_mean_mag")
|
|
63
83
|
|
|
64
|
-
with filename.open("r") as f:
|
|
84
|
+
with pathlib.Path(filename).open("r") as f:
|
|
65
85
|
it = csv.reader(f, delimiter=",", strict=True)
|
|
66
86
|
|
|
67
87
|
# Get columns
|
|
@@ -2,12 +2,15 @@ from collections.abc import Iterator
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import numpy.typing as npt
|
|
5
|
+
from typing_extensions import Self
|
|
5
6
|
|
|
6
7
|
class StarMatcher:
|
|
7
8
|
def __init__(
|
|
8
9
|
self,
|
|
9
10
|
stars_xyz: npt.NDArray[np.float32],
|
|
11
|
+
stars_mag: npt.NDArray[np.float32],
|
|
10
12
|
max_inter_star_angle: float,
|
|
13
|
+
max_lookup_magnitude: float,
|
|
11
14
|
inter_star_angle_tolerance: float,
|
|
12
15
|
n_minimum_matches: int,
|
|
13
16
|
timeout_secs: float,
|
|
@@ -19,7 +22,7 @@ class StarMatcher:
|
|
|
19
22
|
npt.NDArray[np.uint32],
|
|
20
23
|
npt.NDArray[np.uint32],
|
|
21
24
|
int,
|
|
22
|
-
|
|
25
|
+
npt.NDArray[np.float32],
|
|
23
26
|
float,
|
|
24
27
|
]: ...
|
|
25
28
|
|
|
@@ -44,15 +47,45 @@ class IterTriangleFinder:
|
|
|
44
47
|
class UnitVectorLookup:
|
|
45
48
|
def __init__(self, vec: npt.NDArray[np.float32]) -> None: ...
|
|
46
49
|
def lookup_nearest(self, key: npt.NDArray[np.float32]) -> int: ...
|
|
47
|
-
def get_inter_star_index_numpy(
|
|
48
|
-
self, vec: npt.NDArray[np.float32], angle_threshold: float
|
|
49
|
-
) -> tuple[list[list[int]], list[float], list[float]]: ...
|
|
50
50
|
def get_inter_star_index(
|
|
51
|
-
self,
|
|
51
|
+
self,
|
|
52
|
+
stars: npt.NDArray[np.float32],
|
|
53
|
+
magnitudes: npt.NDArray[np.float32],
|
|
54
|
+
max_angle_rad: float,
|
|
55
|
+
max_magnitude: float,
|
|
52
56
|
) -> tuple[list[list[int]], list[float], list[float]]: ...
|
|
53
57
|
def look_up_close_angles(
|
|
54
|
-
self,
|
|
58
|
+
self,
|
|
59
|
+
vectors: npt.NDArray[np.float32],
|
|
60
|
+
magnitudes: npt.NDArray[np.float32],
|
|
61
|
+
max_angle_rad: float,
|
|
62
|
+
max_magnitude: float,
|
|
55
63
|
) -> list[tuple[list[float], float]]: ...
|
|
56
64
|
def look_up_close_angles_naive(
|
|
57
|
-
self,
|
|
65
|
+
self,
|
|
66
|
+
vectors: npt.NDArray[np.float32],
|
|
67
|
+
magnitudes: npt.NDArray[np.float32],
|
|
68
|
+
max_angle_rad: float,
|
|
69
|
+
max_magnitude: float,
|
|
58
70
|
) -> list[tuple[list[float], float]]: ...
|
|
71
|
+
|
|
72
|
+
def get_threshold_from_histogram(
|
|
73
|
+
img: npt.NDArray[np.uint8],
|
|
74
|
+
*,
|
|
75
|
+
fraction: float,
|
|
76
|
+
) -> int: ...
|
|
77
|
+
def extract_observations(
|
|
78
|
+
img: npt.NDArray[np.uint8],
|
|
79
|
+
threshold: int,
|
|
80
|
+
min_star_area: int,
|
|
81
|
+
max_star_area: int,
|
|
82
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: ...
|
|
83
|
+
|
|
84
|
+
class StarCatalog:
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_gaia(cls, *, max_magnitude: float | None) -> Self: ...
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_hipparcos(cls, *, max_magnitude: float | None) -> Self: ...
|
|
89
|
+
def normalized_positions(
|
|
90
|
+
self, *, epoch: float | None, observer_position: np.ndarray | None
|
|
91
|
+
) -> npt.NDArray[np.float32]: ...
|
|
@@ -87,8 +87,10 @@ class StarTracker:
|
|
|
87
87
|
def __init__(
|
|
88
88
|
self,
|
|
89
89
|
stars_xyz: npt.NDArray[np.float32],
|
|
90
|
+
stars_mag: npt.NDArray[np.float32],
|
|
90
91
|
camera_params: CameraParameters,
|
|
91
92
|
*,
|
|
93
|
+
max_lookup_magnitude: float | None = None,
|
|
92
94
|
max_inter_star_angle: float | None = None,
|
|
93
95
|
inter_star_angle_tolerance: float = 0.0008,
|
|
94
96
|
n_minimum_matches: int = 10,
|
|
@@ -98,11 +100,15 @@ class StarTracker:
|
|
|
98
100
|
|
|
99
101
|
Args:
|
|
100
102
|
stars_xyz: Positions of catalog stars
|
|
103
|
+
stars_mag: Magnitudes of catalog stars
|
|
101
104
|
camera_params: Calibrated camera parameters
|
|
105
|
+
max_lookup_magnitude: Maximum magnitude of stars used in the triangulation. Reducing
|
|
106
|
+
this number means only bright stars are used for triangulation. This results in
|
|
107
|
+
faster lookup performance.
|
|
102
108
|
max_inter_star_angle: Maximum angle between stars that should be indexed.
|
|
103
109
|
Calculating large inter star angles is expensive. If None, the angle is
|
|
104
110
|
calculated from the camera field of view.
|
|
105
|
-
inter_star_angle_tolerance: Tolerance for inter star angle matching.
|
|
111
|
+
inter_star_angle_tolerance: Tolerance for inter star angle matching in rad.
|
|
106
112
|
n_minimum_matches: Minimum amount of required matches for a successful
|
|
107
113
|
attitude estimation
|
|
108
114
|
timeout_secs: Maximum allowed search time in seconds. A StarTrackerError is raised
|
|
@@ -127,8 +133,13 @@ class StarTracker:
|
|
|
127
133
|
dot_products = (corner_coords_xyz * np.array([0, 0, 1], dtype=np.float32)).sum(axis=-1)
|
|
128
134
|
max_inter_star_angle = float(np.arccos(dot_products.max())) * 2
|
|
129
135
|
|
|
136
|
+
if max_lookup_magnitude is None:
|
|
137
|
+
max_lookup_magnitude = 100.0 # A very faint star. Almost infinity
|
|
138
|
+
|
|
130
139
|
self._star_matcher = ruststartracker.libruststartracker.StarMatcher(
|
|
131
140
|
np.ascontiguousarray(stars_xyz, dtype=np.float32),
|
|
141
|
+
np.ascontiguousarray(stars_mag, dtype=np.float32),
|
|
142
|
+
float(max_lookup_magnitude),
|
|
132
143
|
float(max_inter_star_angle),
|
|
133
144
|
float(inter_star_angle_tolerance),
|
|
134
145
|
int(n_minimum_matches),
|
|
@@ -178,6 +189,20 @@ class StarTracker:
|
|
|
178
189
|
max_star_area=max_star_area,
|
|
179
190
|
)
|
|
180
191
|
|
|
192
|
+
if threshold is None:
|
|
193
|
+
threshold = ruststartracker.libruststartracker.get_threshold_from_histogram(
|
|
194
|
+
img, fraction=0.99
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
centroids, intensities = ruststartracker.libruststartracker.extract_observations(
|
|
198
|
+
img,
|
|
199
|
+
threshold,
|
|
200
|
+
min_star_area,
|
|
201
|
+
max_star_area,
|
|
202
|
+
)
|
|
203
|
+
centroids = np.array(centroids, dtype=np.float32)
|
|
204
|
+
intensities = np.array(intensities, dtype=np.float32)
|
|
205
|
+
|
|
181
206
|
# At least 3 observations are required (one triangle)
|
|
182
207
|
if len(centroids) < 3:
|
|
183
208
|
raise StarTrackerError("Found too few star candidates (< 3) to continue.")
|
|
@@ -238,12 +263,12 @@ class StarTracker:
|
|
|
238
263
|
quat, match_ids, obs_indices, n_matches, matched_obs, duration_s = result
|
|
239
264
|
|
|
240
265
|
return StarTrackerResult(
|
|
241
|
-
quat=
|
|
242
|
-
match_ids=
|
|
266
|
+
quat=quat,
|
|
267
|
+
match_ids=match_ids,
|
|
243
268
|
n_matches=n_matches,
|
|
244
269
|
duration_s=duration_s,
|
|
245
|
-
mached_obs_x=
|
|
246
|
-
obs_indices=
|
|
270
|
+
mached_obs_x=matched_obs,
|
|
271
|
+
obs_indices=obs_indices,
|
|
247
272
|
)
|
|
248
273
|
|
|
249
274
|
|
|
@@ -54,9 +54,12 @@ def test_unit_vector_lookup():
|
|
|
54
54
|
close_indices_gt = np.concatenate(results, axis=-1).T[args]
|
|
55
55
|
angles_gt = angles[args]
|
|
56
56
|
|
|
57
|
-
close_indices, angles, poly = uvl.
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
close_indices, angles, poly = uvl.get_inter_star_index(
|
|
58
|
+
np.array(vec[:, :3], dtype=np.float32),
|
|
59
|
+
np.ones(len(vec), dtype=np.float32),
|
|
60
|
+
angle_threshold,
|
|
61
|
+
10,
|
|
62
|
+
)
|
|
60
63
|
close_indices = np.array(close_indices)
|
|
61
64
|
angles = np.array(angles)
|
|
62
65
|
poly = np.array(poly)
|
|
@@ -101,6 +104,8 @@ def test_star_matcher():
|
|
|
101
104
|
vec = rng.normal(size=[n_cat_stars, 3]).astype(np.float32)
|
|
102
105
|
vec /= np.linalg.norm(vec, axis=-1, keepdims=True)
|
|
103
106
|
|
|
107
|
+
magnitudes = rng.uniform(0, 10, size=vec.shape[:1]).astype(np.float32)
|
|
108
|
+
|
|
104
109
|
key = rng.normal(size=[3]).astype(np.float32)
|
|
105
110
|
key /= np.linalg.norm(key, axis=-1, keepdims=True)
|
|
106
111
|
|
|
@@ -113,10 +118,16 @@ def test_star_matcher():
|
|
|
113
118
|
|
|
114
119
|
rot = scipy.spatial.transform.Rotation.from_rotvec([1, 1, 1])
|
|
115
120
|
|
|
116
|
-
obs_rotated = rot.apply(obs)
|
|
121
|
+
obs_rotated = rot.apply(obs).astype(np.float32)
|
|
117
122
|
|
|
118
123
|
index = libruststartracker.StarMatcher(
|
|
119
|
-
vec,
|
|
124
|
+
vec,
|
|
125
|
+
magnitudes,
|
|
126
|
+
10,
|
|
127
|
+
np.radians(10).item(),
|
|
128
|
+
np.radians(0.1).item(),
|
|
129
|
+
4,
|
|
130
|
+
999.0,
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
res = index.find(obs_rotated)
|
|
@@ -124,7 +135,7 @@ def test_star_matcher():
|
|
|
124
135
|
assert res is not None
|
|
125
136
|
|
|
126
137
|
quat, match_ids, obs_indices, n_matches, matched_obs, time_s = res
|
|
127
|
-
np.testing.assert_allclose(quat, rot.inv().as_quat())
|
|
138
|
+
np.testing.assert_allclose(quat, rot.inv().as_quat(), rtol=1e-6)
|
|
128
139
|
assert n_matches >= 4
|
|
129
140
|
assert len(obs_index) == len(match_ids)
|
|
130
141
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import astropy.time # type: ignore[import]
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import ruststartracker.catalog
|
|
9
|
+
import ruststartracker.libruststartracker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_time_to_epoch():
|
|
13
|
+
np.testing.assert_allclose(
|
|
14
|
+
ruststartracker.catalog.time_to_epoch(
|
|
15
|
+
datetime.datetime.fromisoformat("2000-01-01T11:58:56")
|
|
16
|
+
),
|
|
17
|
+
2000.0,
|
|
18
|
+
rtol=1e-20,
|
|
19
|
+
atol=1 / (365 * 86400),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
"iso_date",
|
|
25
|
+
[
|
|
26
|
+
"2000-01-01T11:58:56",
|
|
27
|
+
"2024-01-01T11:58:56",
|
|
28
|
+
],
|
|
29
|
+
)
|
|
30
|
+
def test_time_to_epoch_astropy(iso_date: str):
|
|
31
|
+
ground_truth = float(astropy.time.Time(iso_date).jyear) # type: ignore
|
|
32
|
+
np.testing.assert_allclose(
|
|
33
|
+
ruststartracker.catalog.time_to_epoch(datetime.datetime.fromisoformat(iso_date)),
|
|
34
|
+
ground_truth,
|
|
35
|
+
rtol=1e-20,
|
|
36
|
+
atol=64 / (365 * 86400),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_extract_observations():
|
|
41
|
+
positions = ruststartracker.catalog.StarCatalog.from_gaia().normalized_positions()
|
|
42
|
+
|
|
43
|
+
assert positions.ndim == 2
|
|
44
|
+
assert positions.shape[1] == 3
|
|
45
|
+
np.testing.assert_allclose(np.linalg.norm(positions, axis=-1), 1.0, rtol=1e-5)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_gaia_python_rust():
|
|
49
|
+
t0 = time.monotonic()
|
|
50
|
+
positions = ruststartracker.catalog.StarCatalog.from_gaia().normalized_positions(epoch=2025.0)
|
|
51
|
+
print(f"Python catalog took {time.monotonic() - t0:.3f} seconds")
|
|
52
|
+
t0 = time.monotonic()
|
|
53
|
+
positions2 = ruststartracker.libruststartracker.StarCatalog.from_gaia(
|
|
54
|
+
max_magnitude=6.0
|
|
55
|
+
).normalized_positions(epoch=2025.0, observer_position=None)
|
|
56
|
+
print(f"Rust catalog took {time.monotonic() - t0:.3f} seconds")
|
|
57
|
+
np.testing.assert_allclose(positions, positions2, rtol=1e-5, atol=1e-5)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_hipparcos_python_rust():
|
|
61
|
+
t0 = time.monotonic()
|
|
62
|
+
positions = ruststartracker.catalog.StarCatalog.from_hipparcos().normalized_positions(
|
|
63
|
+
epoch=2025.0
|
|
64
|
+
)
|
|
65
|
+
print(f"Python catalog took {time.monotonic() - t0:.3f} seconds")
|
|
66
|
+
t0 = time.monotonic()
|
|
67
|
+
positions2 = ruststartracker.libruststartracker.StarCatalog.from_hipparcos(
|
|
68
|
+
max_magnitude=6.0
|
|
69
|
+
).normalized_positions(epoch=2025.0, observer_position=None)
|
|
70
|
+
print(f"Rust catalog took {time.monotonic() - t0:.3f} seconds")
|
|
71
|
+
np.testing.assert_allclose(positions, positions2, rtol=1e-5, atol=1e-5)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_compare_hipparcos_gaia():
|
|
75
|
+
def filt(cat: ruststartracker.catalog.StarCatalog, m: float = 0):
|
|
76
|
+
positions = cat.normalized_positions(epoch=2025.0)
|
|
77
|
+
mags = cat.magnitude
|
|
78
|
+
mask = (positions[..., 2] > 0.9) * (mags > 5 - m) * (mags < 8 - m)
|
|
79
|
+
return positions[mask]
|
|
80
|
+
|
|
81
|
+
positions_hipparcos = filt(ruststartracker.catalog.StarCatalog.from_hipparcos(max_magnitude=8))
|
|
82
|
+
positions_gaia = filt(ruststartracker.catalog.StarCatalog.from_gaia(max_magnitude=8), m=0.9)
|
|
83
|
+
|
|
84
|
+
dists = np.linalg.norm(
|
|
85
|
+
positions_hipparcos[np.newaxis, :, :] - positions_gaia[:, np.newaxis, :], axis=-1
|
|
86
|
+
)
|
|
87
|
+
matches = np.min(dists, axis=0) < 0.00005
|
|
88
|
+
|
|
89
|
+
assert np.mean(matches > 0.9)
|
|
90
|
+
|
|
91
|
+
if False:
|
|
92
|
+
import matplotlib.pyplot as plt
|
|
93
|
+
|
|
94
|
+
plt.plot(positions_gaia[..., 0], positions_gaia[..., 1], "+", label="gaia")
|
|
95
|
+
plt.plot(positions_hipparcos[..., 0], positions_hipparcos[..., 1], "x", label="hipparcos")
|
|
96
|
+
plt.legend()
|
|
97
|
+
plt.show()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
pytest.main([__file__])
|
|
@@ -24,8 +24,9 @@ def prepare() -> tuple[ruststartracker.StarTracker, np.ndarray]:
|
|
|
24
24
|
|
|
25
25
|
dist_coefs = np.array([-0.44120807, -0.15954202, 0.00767012, -0.00213292, -1.64788247])
|
|
26
26
|
|
|
27
|
-
catalog = ruststartracker.StarCatalog()
|
|
27
|
+
catalog = ruststartracker.StarCatalog.from_gaia()
|
|
28
28
|
star_catalog_vecs = catalog.normalized_positions(epoch=2024)
|
|
29
|
+
star_catalog_magnitudes = catalog.magnitude
|
|
29
30
|
|
|
30
31
|
camera_params = ruststartracker.CameraParameters(
|
|
31
32
|
camera_matrix=camera_matrix,
|
|
@@ -35,6 +36,7 @@ def prepare() -> tuple[ruststartracker.StarTracker, np.ndarray]:
|
|
|
35
36
|
|
|
36
37
|
st = ruststartracker.StarTracker(
|
|
37
38
|
star_catalog_vecs,
|
|
39
|
+
star_catalog_magnitudes,
|
|
38
40
|
camera_params,
|
|
39
41
|
inter_star_angle_tolerance=np.radians(0.05).item(),
|
|
40
42
|
n_minimum_matches=5,
|
|
@@ -5,16 +5,29 @@ import pytest
|
|
|
5
5
|
import scipy.spatial
|
|
6
6
|
|
|
7
7
|
import ruststartracker
|
|
8
|
+
import ruststartracker.libruststartracker
|
|
8
9
|
import ruststartracker.star
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
@pytest.mark.parametrize("impl", ["python", "rust"])
|
|
13
|
+
def test_extract_observations(impl: str):
|
|
14
|
+
size_x, size_y = (960, 480)
|
|
13
15
|
img = np.zeros((size_y, size_x), np.uint8)
|
|
14
|
-
points = np.array([(3, 5), (23, 13)])
|
|
16
|
+
points = np.array([(3, 5), (23, 13), (30, 50), (230, 130)])
|
|
15
17
|
for x, y in points:
|
|
16
18
|
img[y - 1 : y + 3, x - 1 : x + 3] = 50
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
t0 = time.monotonic()
|
|
21
|
+
if impl == "python":
|
|
22
|
+
centers, intensities = ruststartracker.star._extract_observations(img, threshold=30)
|
|
23
|
+
else:
|
|
24
|
+
centers, intensities = ruststartracker.libruststartracker.extract_observations(
|
|
25
|
+
img, 30, 3, 300
|
|
26
|
+
)
|
|
27
|
+
print(f"Extracting observations took {time.monotonic() - t0:.5f} seconds")
|
|
28
|
+
|
|
29
|
+
assert isinstance(centers, np.ndarray)
|
|
30
|
+
assert isinstance(intensities, np.ndarray)
|
|
18
31
|
np.testing.assert_almost_equal(centers, points + 0.5)
|
|
19
32
|
np.testing.assert_almost_equal(intensities, 50 * 16)
|
|
20
33
|
|
|
@@ -28,6 +41,8 @@ def setup():
|
|
|
28
41
|
vec = rng.normal(size=[n_cat_stars, 3]).astype(np.float32)
|
|
29
42
|
vec /= np.linalg.norm(vec, axis=-1, keepdims=True)
|
|
30
43
|
|
|
44
|
+
mag = rng.uniform(0, 10, size=vec.shape[:1]).astype(np.float32)
|
|
45
|
+
|
|
31
46
|
angle_threshold = np.radians(10)
|
|
32
47
|
dotp = np.sum([0, 0, 1] * vec, axis=-1)
|
|
33
48
|
threshold = np.cos(angle_threshold).item()
|
|
@@ -55,17 +70,18 @@ def setup():
|
|
|
55
70
|
image_patch = img[y - 1 : y + 2, x - 1 : x + 2]
|
|
56
71
|
image_patch[:] = 50
|
|
57
72
|
|
|
58
|
-
return img, vec, camera_params
|
|
73
|
+
return img, vec, mag, pixel_in_frame, camera_params
|
|
59
74
|
|
|
60
75
|
|
|
61
76
|
def test_star_matcher_success(setup):
|
|
62
|
-
img, vec, camera_params = setup
|
|
77
|
+
img, vec, mag, _, camera_params = setup
|
|
63
78
|
|
|
64
79
|
rot = scipy.spatial.transform.Rotation.from_rotvec([1, 1, 1])
|
|
65
80
|
vec = rot.inv().apply(vec)
|
|
66
81
|
|
|
67
82
|
st = ruststartracker.StarTracker(
|
|
68
83
|
vec,
|
|
84
|
+
mag,
|
|
69
85
|
camera_params,
|
|
70
86
|
inter_star_angle_tolerance=np.radians(0.1).item(),
|
|
71
87
|
n_minimum_matches=6,
|
|
@@ -78,34 +94,51 @@ def test_star_matcher_success(setup):
|
|
|
78
94
|
|
|
79
95
|
|
|
80
96
|
def test_star_matcher_exhaust(setup):
|
|
81
|
-
img, vec, camera_params = setup
|
|
97
|
+
img, vec, mag, _, camera_params = setup
|
|
82
98
|
st = ruststartracker.StarTracker(
|
|
83
99
|
vec,
|
|
100
|
+
mag,
|
|
84
101
|
camera_params,
|
|
85
102
|
inter_star_angle_tolerance=np.radians(0.001).item(),
|
|
86
103
|
n_minimum_matches=500,
|
|
87
104
|
timeout_secs=999.0,
|
|
88
105
|
)
|
|
89
|
-
with pytest.raises(ruststartracker.StarTrackerError, match="
|
|
106
|
+
with pytest.raises(ruststartracker.StarTrackerError, match="SearchExhausted"):
|
|
90
107
|
st.process_image(img)
|
|
91
108
|
|
|
92
109
|
|
|
93
110
|
def test_star_matcher_timout(setup):
|
|
94
|
-
img, vec, camera_params = setup
|
|
95
|
-
timeout = 0.
|
|
111
|
+
img, vec, mag, _, camera_params = setup
|
|
112
|
+
timeout = 0.0002
|
|
96
113
|
st = ruststartracker.StarTracker(
|
|
97
114
|
vec,
|
|
115
|
+
mag,
|
|
98
116
|
camera_params,
|
|
99
117
|
inter_star_angle_tolerance=np.radians(0.1).item(),
|
|
100
118
|
n_minimum_matches=500,
|
|
101
119
|
timeout_secs=timeout,
|
|
102
120
|
)
|
|
103
121
|
t = time.monotonic()
|
|
104
|
-
with pytest.raises(ruststartracker.StarTrackerError, match="Timeout
|
|
122
|
+
with pytest.raises(ruststartracker.StarTrackerError, match="Timeout"):
|
|
105
123
|
st.process_image(img)
|
|
106
124
|
passed_time = time.monotonic() - t
|
|
107
125
|
assert passed_time > timeout
|
|
108
126
|
|
|
109
127
|
|
|
128
|
+
def test_star_matcher_not_enough_stars(setup):
|
|
129
|
+
_, vec, mag, pixel_in_frame, camera_params = setup
|
|
130
|
+
timeout = 0.2
|
|
131
|
+
st = ruststartracker.StarTracker(
|
|
132
|
+
vec,
|
|
133
|
+
mag,
|
|
134
|
+
camera_params,
|
|
135
|
+
inter_star_angle_tolerance=np.radians(0.1).item(),
|
|
136
|
+
n_minimum_matches=500,
|
|
137
|
+
timeout_secs=timeout,
|
|
138
|
+
)
|
|
139
|
+
with pytest.raises(ruststartracker.StarTrackerError, match="NotEnoughStars"):
|
|
140
|
+
st.process_image_coordiantes(pixel_in_frame[:2])
|
|
141
|
+
|
|
142
|
+
|
|
110
143
|
if __name__ == "__main__":
|
|
111
|
-
pytest.main([__file__])
|
|
144
|
+
pytest.main([__file__, "--capture=no"])
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"""Build rust backend.
|
|
2
|
-
|
|
3
|
-
This script is automatically run when the pyproject is installed.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import pathlib
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def download_gaia_data(output_file: pathlib.Path) -> None:
|
|
12
|
-
"""Download Gaia data and save it to the specified output file.
|
|
13
|
-
|
|
14
|
-
Args:
|
|
15
|
-
output_file: Path to save the downloaded Gaia data (csv).
|
|
16
|
-
"""
|
|
17
|
-
from astroquery.gaia import Gaia
|
|
18
|
-
|
|
19
|
-
Gaia.ROW_LIMIT = 10000
|
|
20
|
-
|
|
21
|
-
query = """
|
|
22
|
-
SELECT source_id, ra, dec, parallax, pmra, pmdec, phot_g_mean_mag
|
|
23
|
-
FROM gaiadr3.gaia_source
|
|
24
|
-
WHERE phot_g_mean_mag < 7
|
|
25
|
-
AND parallax > 0
|
|
26
|
-
AND astrometric_params_solved = 31
|
|
27
|
-
AND visibility_periods_used >= 8
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
print("Executing query on Gaia database...")
|
|
31
|
-
job = Gaia.launch_job_async(
|
|
32
|
-
query,
|
|
33
|
-
dump_to_file=True,
|
|
34
|
-
output_format="csv",
|
|
35
|
-
output_file=str(output_file.absolute()),
|
|
36
|
-
verbose=True,
|
|
37
|
-
)
|
|
38
|
-
if not job.is_finished():
|
|
39
|
-
print("Job did not finish successfully.")
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
print(f"Saved to: {output_file}")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def build_script() -> None:
|
|
46
|
-
"""Build rust backend and move shared library to correct folder."""
|
|
47
|
-
cwd = pathlib.Path(__file__).parent.expanduser().absolute()
|
|
48
|
-
|
|
49
|
-
gaia_file = cwd / "ruststartracker/gaia_data_j2016.csv"
|
|
50
|
-
if not gaia_file.exists():
|
|
51
|
-
download_gaia_data(gaia_file)
|
|
52
|
-
|
|
53
|
-
subprocess.check_call(["cargo", "build", "--release"], cwd=cwd) # noqa: S603, S607
|
|
54
|
-
shutil.copy(
|
|
55
|
-
cwd / "target/release/libruststartracker.so", cwd / "ruststartracker/libruststartracker.so"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if __name__ == "__main__":
|
|
60
|
-
build_script()
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
|
-
import astropy.time # type: ignore[import]
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
import ruststartracker.catalog
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_time_to_epoch():
|
|
11
|
-
np.testing.assert_allclose(
|
|
12
|
-
ruststartracker.catalog.time_to_epoch(
|
|
13
|
-
datetime.datetime.fromisoformat("2000-01-01T11:58:56")
|
|
14
|
-
),
|
|
15
|
-
2000.0,
|
|
16
|
-
rtol=1e-20,
|
|
17
|
-
atol=1 / (365 * 86400),
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.mark.parametrize(
|
|
22
|
-
"iso_date",
|
|
23
|
-
[
|
|
24
|
-
"2000-01-01T11:58:56",
|
|
25
|
-
"2024-01-01T11:58:56",
|
|
26
|
-
],
|
|
27
|
-
)
|
|
28
|
-
def test_time_to_epoch_astropy(iso_date: str):
|
|
29
|
-
ground_truth = float(astropy.time.Time(iso_date).jyear) # type: ignore
|
|
30
|
-
np.testing.assert_allclose(
|
|
31
|
-
ruststartracker.catalog.time_to_epoch(datetime.datetime.fromisoformat(iso_date)),
|
|
32
|
-
ground_truth,
|
|
33
|
-
rtol=1e-20,
|
|
34
|
-
atol=64 / (365 * 86400),
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_extract_observations():
|
|
39
|
-
positions = ruststartracker.catalog.StarCatalog().normalized_positions()
|
|
40
|
-
|
|
41
|
-
assert positions.ndim == 2
|
|
42
|
-
assert positions.shape[1] == 3
|
|
43
|
-
np.testing.assert_allclose(np.linalg.norm(positions, axis=-1), 1.0, rtol=1e-5)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if __name__ == "__main__":
|
|
47
|
-
pytest.main([__file__])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|