Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,29 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libclang-dev libopencv-dev

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

# poetry==2.1.4 has a bug
- name: Install dependencies
run: |
pip install poetry
POETRY_VIRTUALENVS_CREATE=false poetry install
pip install poetry==2.1.3
poetry install
env:
POETRY_VIRTUALENVS_CREATE: false

- name: Pre-commit checks
run: |
Expand All @@ -42,3 +50,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --features=improc,gaia
55 changes: 42 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,13 @@ jobs:

make-release:
name: Create Release
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

steps:

- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies and create source distribution
run: |
pip install poetry
poetry build --format sdist

- name: Create Release
id: create_release
uses: actions/create-release@v1
Expand All @@ -42,6 +32,36 @@ jobs:
draft: false
prerelease: false

build-python-sdist:
name: Build Python sdist
runs-on: ubuntu-22.04

needs:
- make-release

steps:

- name: Checkout code
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libclang-dev libopencv-dev

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

# poetry==2.1.4 has a bug
- name: Install dependencies and create source distribution
run: |
pip install poetry==2.1.3
poetry build --format sdist
env:
POETRY_VIRTUALENVS_CREATE: false

- name: Upload sdist
run: |
for file in ./dist/*; do
Expand All @@ -68,15 +88,23 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libclang-dev libopencv-dev

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

# poetry==2.1.4 has a bug
- name: Install dependencies and build the wheel
run: |
pip install poetry
pip install poetry==2.1.3
poetry build --format wheel
env:
POETRY_VIRTUALENVS_CREATE: false

- name: Upload wheel
run: |
Expand All @@ -90,7 +118,7 @@ jobs:

pypi-publish:
name: Upload release to PyPI
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

environment:
name: pypi
Expand All @@ -104,6 +132,7 @@ jobs:

needs:
- build-python-wheels
- build-python-sdist

steps:

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/version_bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:

jobs:
bump-version:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

if: "!contains(github.event.head_commit.message, 'ci: bump')"

Expand Down
17 changes: 15 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@ edition = "2021"

[lib]
name = "ruststartracker"
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]

[dependencies]
itertools = "0.13.0"
kdtree = "0.7.0"
maths-rs = "0.2.6"
nalgebra = "0.31.4"
nalgebra = "0.33.2"
polyfit-rs = "0.2.1"
pyo3 = { version = "0.21.0", features = ["extension-module"] }
numpy = "0.21.0"
opencv = { version = "0.94.4", optional = true }
chrono = "0.4.41"
csv = "1.3.1"
serde = { version = "1.0.219", features = ["derive"] }

[dev-dependencies]
rand = "0.9.1"
rand_distr = "0.5.1"

[features]
default = []
improc = ["opencv"]
gaia = []
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ Features:

## Example

### Rust

See [examples/basic.rs](examples/basic.rs)

```rust
// Get catalog positions
let catalog: StarCatalog = StarCatalog::from_gaia(max_magnitude: ...).unwrap();
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(epoch: ..., observer_position: ...);
let stars_mag: Vec<f32> = catalog.magnitudes();

// Create StarTracker instance (reuse this)
let star_matcher = StarMatcher::new(
stars_xyz,
stars_mag,
max_lookup_magnitude: ...
max_inter_star_angle: ...,
inter_star_angle_tolerance: ...,
min_matches: ...,
timeout: ...
);

// Normalized observation in the camera frame
let obs_xyz_camera: Vec<[f32; 3]> = ...

let result = star_matcher.find(&obs_xyz_camera);
println!("Result: {:?}", result);
```

### Python
```python
import ruststartracker

Expand Down Expand Up @@ -51,11 +80,6 @@ print(result)

- Install with `pip install ruststartracker` (Currently only ARM/x86 Linux wheels available).

## TODOs

- Improve error messages.
- Return more diagnostic data.

## Attributions

### Gaia Data
Expand Down
7 changes: 6 additions & 1 deletion build_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ def build_script() -> None:
if not gaia_file.exists():
download_gaia_data(gaia_file)

subprocess.check_call(["cargo", "build", "--release"], cwd=cwd) # noqa: S603, S607
subprocess.check_call( # noqa: S603
["cargo", "build", "--release", "--features", "improc,gaia"], # noqa: S607
cwd=cwd,
stdout=None,
stderr=None,
)
shutil.copy(
cwd / "target/release/libruststartracker.so", cwd / "ruststartracker/libruststartracker.so"
)
Expand Down
35 changes: 35 additions & 0 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use ruststartracker::star::StarMatcher;
use ruststartracker::starcat::StarCatalog;

fn get_observations() -> Vec<[f32; 3]> {
// For demo purposes we extract some bright stars from the catalog.

// Get catalog positions
let catalog = StarCatalog::new_from_gaia(Some(5.0)).unwrap();
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(Some(2025.0), None);

// Get some observations from the catalog
stars_xyz
.iter()
.filter(|x| x[1] > f32::cos(0.5))
.map(|x| *x)
.collect()
}

fn main() {
// Get catalog positions
let catalog = StarCatalog::new_from_gaia(Some(6.0)).unwrap();
let stars_xyz: Vec<[f32; 3]> = catalog.normalized_positions(Some(2025.0), None);
let stars_mag: Vec<f32> = catalog.magnitudes();

// Create StarTracker instance (reuse this)
let star_matcher = StarMatcher::new(stars_xyz, &stars_mag, 5.0, 1.0, 0.002, 10, 0.2).unwrap();

// Get observation in the camera frame (provide this function)
let obs_xyz_camera: Vec<[f32; 3]> = get_observations();

// Lookup attitude
let result = star_matcher.find(&obs_xyz_camera).unwrap();

println!("Result: {:?}", result);
}
45 changes: 38 additions & 7 deletions ruststartracker/libruststartracker.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ from collections.abc import Iterator

import numpy as np
import numpy.typing as npt
from typing_extensions import Self

class StarMatcher:
def __init__(
self,
stars_xyz: npt.NDArray[np.float32],
stars_mag: npt.NDArray[np.float32],
max_inter_star_angle: float,
max_lookup_magnitude: float,
inter_star_angle_tolerance: float,
n_minimum_matches: int,
timeout_secs: float,
Expand All @@ -19,7 +22,7 @@ class StarMatcher:
npt.NDArray[np.uint32],
npt.NDArray[np.uint32],
int,
list[list[float]],
npt.NDArray[np.float32],
float,
]: ...

Expand All @@ -44,15 +47,43 @@ class IterTriangleFinder:
class UnitVectorLookup:
def __init__(self, vec: npt.NDArray[np.float32]) -> None: ...
def lookup_nearest(self, key: npt.NDArray[np.float32]) -> int: ...
def get_inter_star_index_numpy(
self, vec: npt.NDArray[np.float32], angle_threshold: float
) -> tuple[list[list[int]], list[float], list[float]]: ...
def get_inter_star_index(
self, vec: npt.NDArray[np.float32], angle_threshold: float
self,
stars: npt.NDArray[np.float32],
magnitudes: npt.NDArray[np.float32],
max_angle_rad: float,
max_magnitude: float,
) -> tuple[list[list[int]], list[float], list[float]]: ...
def look_up_close_angles(
self, vectors: npt.NDArray[np.float32], max_angle_rad: float
self,
vectors: npt.NDArray[np.float32],
magnitudes: npt.NDArray[np.float32],
max_angle_rad: float,
max_magnitude: float,
) -> list[tuple[list[float], float]]: ...
def look_up_close_angles_naive(
self, vectors: npt.NDArray[np.float32], max_angle_rad: float
self,
vectors: npt.NDArray[np.float32],
magnitudes: npt.NDArray[np.float32],
max_angle_rad: float,
max_magnitude: float,
) -> list[tuple[list[float], float]]: ...

def get_threshold_from_histogram(
img: npt.NDArray[np.uint8],
*,
fraction: float,
) -> int: ...
def extract_observations(
img: npt.NDArray[np.uint8],
threshold: int,
min_star_area: int,
max_star_area: int,
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: ...

class StarCatalog:
@classmethod
def from_gaia(cls, *, max_magnitude: float | None) -> Self: ...
def normalized_positions(
self, *, epoch: float | None, observer_position: np.ndarray | None
) -> npt.NDArray[np.float32]: ...
Loading