diff --git a/.gitignore b/.gitignore index 4aefdee..dcd2ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -248,3 +248,6 @@ docs/_build/ # Pyenv .python-version + +debug.txt +star_map.png diff --git a/Cargo.lock b/Cargo.lock index 7b9b022..c746d13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "arbitrary" @@ -159,9 +168,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -177,9 +186,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.1.15" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", @@ -204,9 +213,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.16" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" dependencies = [ "clap_builder", "clap_derive", @@ -214,9 +223,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" dependencies = [ "anstream", "anstyle", @@ -455,9 +464,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -564,6 +573,16 @@ dependencies = [ "imgref", ] +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -613,6 +632,33 @@ dependencies = [ "adler2", ] +[[package]] +name = "nalgebra" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4b5f057b303842cf3262c27e465f4c303572e7f6b0648f60e16248ac3397f4" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -645,6 +691,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -925,6 +980,12 @@ dependencies = [ "rgb", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -947,9 +1008,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -960,6 +1021,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -968,18 +1038,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -1001,6 +1071,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1022,6 +1105,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "spec_math" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b214a4d084b0d4995f4237f681d6018f76c0aaeee52627d521667a29c15cab" + [[package]] name = "spin" version = "0.9.8" @@ -1033,13 +1122,15 @@ dependencies = [ [[package]] name = "starfinder" -version = "1.0.0" +version = "1.1.0" dependencies = [ "clap", "csv", "image", + "nalgebra", "pyo3", "serde", + "spec_math", "thiserror", ] @@ -1051,9 +1142,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1144,6 +1235,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1246,6 +1343,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 11803f2..a6b3dad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "starfinder" -version = "1.0.0" +version = "1.1.0" edition = "2021" license-file = "LICENSE" description = "starfinder is a Rust & Python package that provides functionality to read, process, and render star data from the Tycho-2 catalog. It's built with Rust for performance and exposes a Python API for ease of use." @@ -16,8 +16,10 @@ exclude = [ clap = { version = "4.5.16", features = ["derive"] } csv = "1.3.0" image = "0.25.2" +nalgebra = "0.33.0" pyo3 = "0.22.2" serde = { version = "1.0.209", features = ["derive"] } +spec_math = "0.1.5" thiserror = "1.0.63" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 3194848..6a3197b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,14 @@ `starfinder` is a Rust & Python package that provides functionality to read, process, and render star data from the Tycho-2 catalog. It's built with Rust for performance and exposes a Python API for ease of use. -https://archive.eso.org/ASTROM/TYC-2/data/ +## Setup -Download catalog.dat from there, put it in data/tycho2/ +1. Download the Tycho-2 catalog: + + - Visit https://archive.eso.org/ASTROM/TYC-2/data/ + - Download `catalog.dat` and place it in `data/tycho2/` + +2. Ensure your project structure looks like this: ``` . @@ -12,120 +17,116 @@ Download catalog.dat from there, put it in data/tycho2/ ├── Cargo.toml ├── README.md ├── data -│   └── tycho2 -│   ├── catalog.dat -│   ├── index.dat -│   ├── suppl_1.dat -│   └── suppl_2.dat +│ └── tycho2 +│ ├── catalog.dat +│ ├── index.dat +│ ├── suppl_1.dat +│ └── suppl_2.dat ├── poetry.lock ├── pyproject.toml -├── src -│   └── main.rs +└── src ``` -# Rust +## Running the Renderer -``` -cargo run --release -- --min-ra=0 --max-ra=60 --min-dec=-30 --max-dec=30 --max-magnitude=11 --width=1000 --height=800 --output=example.png -``` +### Using Cargo (Rust) -# C++ +To run the renderer with default settings: +```bash +cargo run ``` -mkdir build -cd build -cmake .. -make -render --max-ra=60 --min-dec=-30 --max-dec=30 --max-magnitude=11 --width=1000 --height=800 --output=example.png ../data/tycho2/catalog.dat + +To run with custom arguments: + +```bash +cargo run -- --roll 0.0 --fov-w 75.0 --fov-h 50.0 ``` -## Installation +### Command-line Arguments + +| Flag | Description | Default | Notes | +| --------------- | -------------------------------- | ------------------------- | ----------------- | +| --source, -s | Source file path | `data/tycho2/catalog.dat` | | +| --center-ra | FOV center point right ascension | 180.0 | In degrees | +| --center-dec | FOV center point declination | 0.0 | In degrees | +| --fov-w | Width of FOV | 60.0 | In degrees | +| --fov-h | Height of FOV | 45.0 | In degrees | +| --roll | Camera sensor roll | 0.0 | In degrees | +| --max-magnitude | Maximum visual magnitude | 12.0 | Lower is brighter | +| --lambda-nm | Targeted wavelength | 540.0 | In nanometers | +| --pixel-size-m | Simulated sensor pixel size | 3e-6 | In meters | +| --width | Output image width | 800 | In pixels | +| --height | Output image height | 600 | In pixels | +| --output, -o | Output filename | `star_map.png` | | + +## Python Installation and Usage + +### Installation -To install `starfinder`, you can use pip: -https://pyo3.rs/v0.22.2/getting-started +Ensure you have Python 3.8 or later, then: + +`bash` +pip install starfinder + +```` +Or if you want to install the package in development mode: ```bash pipx install maturin maturin develop -``` +```` -Note: This package requires Python 3.8 or later. - -## Usage - -Here's a basic example of how to use `starfinder`: +### Basic Usage ```python from starfinder import StarCatalogArgs, process_star_catalog_py -# Create arguments for star catalog processing args = StarCatalogArgs( - file="path/to/your/tycho2_catalog.dat", - display_count=10, - min_ra=0.0, - max_ra=360.0, - min_dec=-90.0, - max_dec=90.0, + source="data/tycho2/catalog.dat", + center_ra=180.0, + center_dec=0.0, + fov_w=60.0, + fov_h=45.0, + roll=0.0, max_magnitude=6.0, + lambda_nm=540.0, + pixel_size_m=3e-6, width=800, height=600, output="star_map.png" ) -# Process the star catalog process_star_catalog_py(args) ``` -This will read the Tycho-2 catalog, filter the stars based on the given parameters, and generate a star map image. - ## API Reference ### `StarCatalogArgs` -This class represents the arguments for star catalog processing. - Parameters: -- `file` (str): Path to the Tycho-2 catalog file -- `display_count` (int): Number of stars to display in the console output (0 for all) -- `min_ra` (float): Minimum Right Ascension in degrees -- `max_ra` (float): Maximum Right Ascension in degrees -- `min_dec` (float): Minimum Declination in degrees -- `max_dec` (float): Maximum Declination in degrees -- `max_magnitude` (float): Maximum visual magnitude (lower is brighter) -- `width` (int): Output image width in pixels -- `height` (int): Output image height in pixels -- `output` (str): Output image file name +- `source` (str): Path to the Tycho-2 catalog file +- `center_ra` (float): Right Ascension of FOV center (degrees) +- `center_dec` (float): Declination of FOV center (degrees) +- `fov_w` (float): FOV width (degrees) +- `fov_h` (float): FOV height (degrees) +- `roll` (float): Camera roll (degrees) +- `max_magnitude` (float): Maximum visual magnitude +- `lambda_nm` (float): Targeted wavelength (nanometers) +- `pixel_size_m` (float): Sensor pixel size (meters) +- `width` (int): Output image width (pixels) +- `height` (int): Output image height (pixels) +- `output` (str): Output image filename ### `process_star_catalog_py(args: StarCatalogArgs) -> None` -This function processes the star catalog based on the provided arguments. +Processes the star catalog based on the provided arguments. -## Example +## Contributing -Here's a more detailed example that demonstrates how to use `starfinder` to create a star map of the brightest stars: - -```python -from starfinder import StarCatalogArgs, process_star_catalog_py +Contributions to `starfinder` are welcome! Please feel free to submit a Pull Request. -# Create arguments for star catalog processing -args = StarCatalogArgs( - file="tycho2_catalog.dat", - display_count=20, # Display info for the 20 brightest stars - min_ra=0.0, - max_ra=360.0, - min_dec=-90.0, - max_dec=90.0, - max_magnitude=3.0, # Only include stars brighter than magnitude 3 - width=1200, - height=800, - output="bright_stars_map.png" -) - -# Process the star catalog -process_star_catalog_py(args) - -print(f"Star map has been generated: {args.output}") -``` +## License -This script will create a star map of the brightest stars (magnitude 3.0 or brighter) across the entire sky, output information about the 20 brightest stars to the console, and save the star map as "bright_stars_map.png". +This project is licensed under the GPLv3 License - see the LICENSE file for details. diff --git a/images/11367213.png b/images/11367213.png deleted file mode 100644 index 5841730..0000000 Binary files a/images/11367213.png and /dev/null differ diff --git a/images/star_map.png b/images/star_map.png deleted file mode 100644 index c924b33..0000000 Binary files a/images/star_map.png and /dev/null differ diff --git a/images/star_map2.png b/images/star_map2.png deleted file mode 100644 index 5140343..0000000 Binary files a/images/star_map2.png and /dev/null differ diff --git a/images/star_map5.png b/images/star_map5.png deleted file mode 100644 index 496cc4b..0000000 Binary files a/images/star_map5.png and /dev/null differ diff --git a/images/star_map6.png b/images/star_map6.png deleted file mode 100644 index 6b982f8..0000000 Binary files a/images/star_map6.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index c8beae6..60d3aa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "maturin" [project] name = "starfinder" -version = "1.0.0" +version = "1.1.0" +description = "Generate images of the sky w/ accurate stars for star sensor simulation" requires-python = ">=3.8" classifiers = [ "Programming Language :: Rust", @@ -15,8 +16,8 @@ classifiers = [ dynamic = ["version"] authors = [ { name="Sulaiman Khan Ghori", email="sulaiman.ghori@outlook.com" }, + { name="Layton Miller", email="layton.r.miller@gmail.com" }, ] -description = "Generate images of the sky w/ accurate stars for star sensor simulation" readme = "README.md" [project.urls] diff --git a/src/fov.rs b/src/fov.rs new file mode 100644 index 0000000..57cdd1c --- /dev/null +++ b/src/fov.rs @@ -0,0 +1,120 @@ +use nalgebra::{SMatrix, Vector3}; +use std::collections::HashSet; +use std::f64::consts::PI; + +use crate::types::{CartesianCoords, EquatorialCoords}; + +pub const GRID_RESOLUTION: f64 = 360.0; + +pub fn get_fov( + center: EquatorialCoords, + fov_w: f64, + fov_h: f64, + roll: f64, +) -> HashSet { + // Config params for function + let view_init_center = EquatorialCoords { + ra: 180.0_f64.to_radians(), + dec: 0.0_f64.to_radians(), + }; + let ra_dif = center.ra - view_init_center.ra; + let dec_dif = center.dec - view_init_center.dec; + // Calculate FOV bounds in cartesian coords + let fov_w_half = fov_w / 2.0; + let fov_h_half = fov_h / 2.0; + + let step_size = 2.0 * PI / GRID_RESOLUTION; + let bottom_left = EquatorialCoords { + ra: view_init_center.ra - fov_w_half, + dec: view_init_center.dec - fov_h_half, + }; + let fov_steps_w = ((fov_w / step_size).ceil() + 1.0) as i32; + let fov_steps_h = ((fov_h / step_size).ceil() + 1.0) as i32; + let mut scatter_shot: Vec = Vec::new(); + + for y in 0..fov_steps_h { + for x in 0..fov_steps_w { + scatter_shot.push( + EquatorialCoords { + ra: bottom_left.ra + (x as f64 * step_size), + dec: bottom_left.dec + (y as f64 * step_size), + } + .to_cartesian(), + ); + } + } + + let c_cartesian = center.to_cartesian(); + let y_roll_axis = CartesianCoords { + x: -c_cartesian.y, + y: c_cartesian.x, + z: 0.0, + }; + + let x_roll = SMatrix::::new( + // Row 1 + roll.cos(), + ra_dif.sin(), + 0.0, + // Row 2 + ra_dif.sin(), + ra_dif.cos(), + 0.0, + // Row 3 + 0.0, + 0.0, + (1.0 - ra_dif.cos()) + ra_dif.cos(), + ); + let y_roll = SMatrix::::new( + // Row 1 + y_roll_axis.x.powi(2) * (1.0 - dec_dif.cos()) + dec_dif.cos(), + y_roll_axis.x * y_roll_axis.y * (1.0 - dec_dif.cos()) - (y_roll_axis.z * dec_dif.sin()), + y_roll_axis.x * y_roll_axis.z * (1.0 - dec_dif.cos()) + (y_roll_axis.y * dec_dif.sin()), + // Row 2 + y_roll_axis.x * y_roll_axis.y * (1.0 - dec_dif.cos()) + (y_roll_axis.z * dec_dif.sin()), + y_roll_axis.y.powi(2) * (1.0 - dec_dif.cos()) + dec_dif.cos(), + y_roll_axis.y * y_roll_axis.z * (1.0 - dec_dif.cos()) - (y_roll_axis.x * dec_dif.sin()), + // Row 3 + y_roll_axis.x * y_roll_axis.z * (1.0 - dec_dif.cos()) - (y_roll_axis.y * dec_dif.sin()), + y_roll_axis.y * y_roll_axis.z * (1.0 - dec_dif.cos()) + (y_roll_axis.x * dec_dif.sin()), + y_roll_axis.z.powi(2) * (1.0 - dec_dif.cos()) + dec_dif.cos(), + ); + let z_roll = SMatrix::::new( + // Row 1 + c_cartesian.x.powi(2) * (1.0 - roll.cos()) + roll.cos(), + c_cartesian.x * c_cartesian.y * (1.0 - roll.cos()) - (c_cartesian.z * roll.sin()), + c_cartesian.x * c_cartesian.z * (1.0 - roll.cos()) + (c_cartesian.y * roll.sin()), + // Row 2 + c_cartesian.x * c_cartesian.y * (1.0 - roll.cos()) + (c_cartesian.z * roll.sin()), + c_cartesian.y.powi(2) * (1.0 - roll.cos()) + roll.cos(), + c_cartesian.y * c_cartesian.z * (1.0 - roll.cos()) - (c_cartesian.x * roll.sin()), + // Row 3 + c_cartesian.x * c_cartesian.z * (1.0 - roll.cos()) - (c_cartesian.y * roll.sin()), + c_cartesian.y * c_cartesian.z * (1.0 - roll.cos()) + (c_cartesian.x * roll.sin()), + c_cartesian.z.powi(2) * (1.0 - roll.cos()) + roll.cos(), + ); + + let transform = x_roll * y_roll * z_roll; + let mut final_grid: HashSet = HashSet::new(); + for p in scatter_shot { + let vec: Vector3 = Vector3::new(p.x, p.y, p.z); + let transformed = transform * vec; + let grid_coord = CartesianCoords { + x: transformed[(0, 0)], + y: transformed[(1, 0)], + z: transformed[(2, 0)], + } + .to_equatorial() + .to_grid(); + final_grid.insert(grid_coord); + } + + final_grid +} +/* +pub fn equatorial_to_grid(p: &EquatorialCoords) -> EquatorialCoords { + EquatorialCoords { + ra: ((p.ra / 2.0 * PI) * (1.0 - (2.0 * p.dec / PI).abs()) * APPROX_RES).round(), + dec: ((2.0 * p.dec / PI) * APPROX_RES).round(), + } +}*/ diff --git a/src/lib.rs b/src/lib.rs index 25ede67..2fa614e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,32 +1,38 @@ -pub mod render; -pub mod star_catalog; +pub mod fov; +pub mod parsing_utils; +pub mod rendering; pub mod types; +use crate::parsing_utils::read_stars; +use crate::rendering::render_stars; +use crate::types::EquatorialCoords; + use clap::Parser; use pyo3::prelude::*; use std::time::Instant; -use crate::render::render_stars; -use crate::star_catalog::read_stars; - #[pyclass] #[derive(Parser, Debug, Clone)] pub struct StarCatalogArgs { #[pyo3(get, set)] - pub file: String, + pub source: String, #[pyo3(get, set)] - pub display_count: usize, + pub center_ra: f64, #[pyo3(get, set)] - pub min_ra: f64, + pub center_dec: f64, #[pyo3(get, set)] - pub max_ra: f64, + pub fov_w: f64, #[pyo3(get, set)] - pub min_dec: f64, + pub fov_h: f64, #[pyo3(get, set)] - pub max_dec: f64, + pub roll: f64, #[pyo3(get, set)] pub max_magnitude: f64, #[pyo3(get, set)] + pub lambda_nm: f64, + #[pyo3(get, set)] + pub pixel_size_m: f64, + #[pyo3(get, set)] pub width: u32, #[pyo3(get, set)] pub height: u32, @@ -38,25 +44,29 @@ pub struct StarCatalogArgs { impl StarCatalogArgs { #[new] fn new( - file: String, - display_count: usize, - min_ra: f64, - max_ra: f64, - min_dec: f64, - max_dec: f64, + source: String, + center_ra: f64, + center_dec: f64, + fov_w: f64, + fov_h: f64, + roll: f64, max_magnitude: f64, + lambda_nm: f64, + pixel_size_m: f64, width: u32, height: u32, output: String, ) -> Self { Self { - file, - display_count, - min_ra, - max_ra, - min_dec, - max_dec, + source, + center_ra, + center_dec, + fov_w, + fov_h, + roll, max_magnitude, + lambda_nm, + pixel_size_m, width, height, output, @@ -64,60 +74,56 @@ impl StarCatalogArgs { } } -pub fn process_star_catalog(args: StarCatalogArgs) -> Result<(), Box> { - println!("Reading stars from: {}", args.file); - println!("RA range: {} to {}", args.min_ra, args.max_ra); - println!("Dec range: {} to {}", args.min_dec, args.max_dec); - println!("Max magnitude: {}", args.max_magnitude); - - let start = Instant::now(); - let stars = read_stars( - &args.file, - args.min_ra, - args.max_ra, - args.min_dec, - args.max_dec, - args.max_magnitude, - )?; - let read_duration = start.elapsed(); +pub fn process_star_catalog(args: &StarCatalogArgs) -> Result<(), Box> { + let run_start = Instant::now(); + let center_ra = args.center_ra.to_radians(); + let center_dec = args.center_dec.to_radians(); + let roll = args.roll.to_radians(); + let fov_w = args.fov_w.to_radians(); + let fov_h = args.fov_h.to_radians(); - println!("Time taken to read and filter stars: {:?}", read_duration); - println!("Total stars after filtering: {}", stars.len()); + // 1) Rotate FOV by specified roll + let get_fov_start = Instant::now(); + let center = EquatorialCoords { + ra: center_ra, + dec: center_dec, + }; + let rolled_fov = fov::get_fov(center, fov_w, fov_h, roll); + println!("Total FOV retrieval time: {:?}", get_fov_start.elapsed()); - println!("\nFirst {} stars:", args.display_count); - for (i, star) in stars.iter().enumerate() { - if i >= args.display_count && args.display_count != 0 { - break; - } - println!( - "Star {}: RA={:.2}, Dec={:.2}, Mag={:.2}", - i, star.ra_deg, star.de_deg, star.mag - ); - } + // 2) Read stars and filter against rolled_fov to create subset of stars in view of the image + let read_stars_start = Instant::now(); + let stars_in_fov = read_stars(&args.source, rolled_fov, args.max_magnitude)?; + println!( + "Total time to read and parse stars: {:?}", + read_stars_start.elapsed() + ); - let render_start = Instant::now(); + // 3) Render stars in FOV + let render_stars_start = Instant::now(); let img = render_stars( - &stars, + stars_in_fov, args.width, args.height, - args.min_ra, - args.max_ra, - args.min_dec, - args.max_dec, + center, + fov_w, + fov_h, + roll, ); img.save(&args.output)?; - let render_duration = render_start.elapsed(); + println!( + "Total parse and write stars: {:?}", + render_stars_start.elapsed() + ); - println!("Time taken to render and save image: {:?}", render_duration); - println!("Image saved as: {}", args.output); - println!("Total time elapsed: {:?}", start.elapsed()); + println!("Total run time elapsed: {:?}", run_start.elapsed()); Ok(()) } #[pyfunction] fn process_star_catalog_py(args: StarCatalogArgs) -> PyResult<()> { - process_star_catalog(args) + process_star_catalog(&args) .map_err(|e| PyErr::new::(e.to_string())) } diff --git a/src/main.rs b/src/main.rs index df235ff..41a548e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,16 @@ use clap::Parser; +use std::path::PathBuf; +use std::time::Instant; -use starfinder::{process_star_catalog, StarCatalogArgs}; +use starfinder::fov::get_fov; +use starfinder::parsing_utils::read_stars; +use starfinder::rendering::render_stars; +use starfinder::types::EquatorialCoords; +/// CLI Arguments #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct CliArgs { +pub struct Args { /// Path to the Tycho-2 catalog file #[arg( short, @@ -12,32 +18,41 @@ struct CliArgs { value_name = "FILE", default_value = "data/tycho2/catalog.dat" )] - file: String, + source: PathBuf, - /// Number of stars to display (0 for all) - #[arg(short, long, default_value_t = 10)] - display_count: usize, + /// Right Ascension of camera view center point (degrees) + #[arg(long, default_value_t = 180.0)] + center_ra: f64, - /// Minimum Right Ascension (degrees) + /// Declination of camera view center point (degrees) #[arg(long, default_value_t = 0.0)] - min_ra: f64, + center_dec: f64, - /// Maximum Right Ascension (degrees) - #[arg(long, default_value_t = 360.0)] - max_ra: f64, + /// Width of field of view. With 0 roll, corresponds to right ascension (degrees) + #[arg(long, default_value_t = 60.0)] + fov_w: f64, - /// Minimum Declination (degrees) - #[arg(long, default_value_t = -90.0)] - min_dec: f64, + /// Height of field of view. With 0 roll, corresponds to declination (degrees) + #[arg(long, default_value_t = 45.0)] + fov_h: f64, - /// Maximum Declination (degrees) - #[arg(long, default_value_t = 90.0)] - max_dec: f64, + /// Roll of the camera view (degrees) + #[arg(long, default_value_t = 0.0)] + roll: f64, /// Maximum visual magnitude (lower is brighter) - #[arg(short, long, default_value_t = 6.0)] + #[arg(long, default_value_t = 12.0)] max_magnitude: f64, + /// Targeted wavelength - critical for airy disc rendering (nanometers). Default to visible + #[arg(long, default_value_t = 540.0)] + lambda_nm: f64, + + /// Camera pixel size in meters (should be tiny, like e-6). Default is 3e-6, assuming higher + /// precision optics + #[arg(long, default_value_t = 3e-6)] + pixel_size_m: f64, + /// Output image width in pixels #[arg(long, default_value_t = 800)] width: u32, @@ -52,20 +67,70 @@ struct CliArgs { } fn main() -> Result<(), Box> { - let cli_args = CliArgs::parse(); - - let args = StarCatalogArgs { - file: cli_args.file, - display_count: cli_args.display_count, - min_ra: cli_args.min_ra, - max_ra: cli_args.max_ra, - min_dec: cli_args.min_dec, - max_dec: cli_args.max_dec, - max_magnitude: cli_args.max_magnitude, - width: cli_args.width, - height: cli_args.height, - output: cli_args.output, + let run_start = Instant::now(); + let args = Args::parse(); + let center_ra = args.center_ra.to_radians(); + let center_dec = args.center_dec.to_radians(); + let roll = args.roll.to_radians(); + let fov_w = args.fov_w.to_radians(); + let fov_h = args.fov_h.to_radians(); + + /*println!("================ Cmd args list ==============="); + println!("Reading stars from: {:?}", args.source); + println!("Center - RA (deg): {}", args.center_ra); + println!("Center - RA (rad): {}", center_ra); + println!("Center - Dec (deg): {}", args.center_dec); + println!("Center - Dec (rad): {}", center_dec); + println!("FOV width (deg): {}", args.fov_w_deg); + println!("FOV width (rad): {}", fov_w); + println!("FOV height (deg): {}", args.fov_h_deg); + println!("FOV height (rad): {}", fov_h); + println!("Roll (deg): {}", args.roll_deg); + println!("Roll (rad): {}", roll); + println!("Max magnitude: {}", args.max_magnitude); + println!("Lambda (wavelength) nm: {}", args.lambda_nm); + println!("Pixel size (meters): {}", args.pixel_size_m); + println!("Output image width: {}", args.width); + println!("Output image height: {}", args.height); + println!("Output image height: {}", args.height); + println!("Output filename: {}", args.output); + println!("============ End of cmd args list ============");*/ + + // 1) Rotate FOV by specified roll + let get_fov_start = Instant::now(); + let center = EquatorialCoords { + ra: center_ra, + dec: center_dec, }; + let rolled_fov = get_fov(center, fov_w, fov_h, roll); + println!("Total FOV retrieval time: {:?}", get_fov_start.elapsed()); + + // 2) Read stars and filter against rolled_fov to create subset of stars in view of the image + let read_stars_start = Instant::now(); + let stars_in_fov = read_stars(args.source, rolled_fov, args.max_magnitude)?; + println!( + "Total time to read and parse stars: {:?}", + read_stars_start.elapsed() + ); + + // 4) Render stars in FOV + let render_stars_start = Instant::now(); + let img = render_stars( + stars_in_fov, + args.width, + args.height, + center, + fov_w, + fov_h, + roll, + ); + img.save(&args.output)?; + println!( + "Total parse and write stars: {:?}", + render_stars_start.elapsed() + ); + + println!("Total run time elapsed: {:?}", run_start.elapsed()); - process_star_catalog(args) + Ok(()) } diff --git a/src/star_catalog.rs b/src/parsing_utils.rs similarity index 69% rename from src/star_catalog.rs rename to src/parsing_utils.rs index 3d2638b..c41231b 100644 --- a/src/star_catalog.rs +++ b/src/parsing_utils.rs @@ -1,18 +1,34 @@ use csv::ReaderBuilder; +use std::collections::HashSet; use std::fs::File; -use std::io::{self}; +use std::io; +use thiserror::Error; -use crate::types::{CatalogError, Star}; +use crate::types::{EquatorialCoords, Star}; + +/// Errors that can occur during star catalog reading. +#[derive(Error, Debug)] +pub enum CatalogError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + #[error("CSV parsing error: {0}")] + Csv(#[from] csv::Error), + #[error("Missing field: {0}")] + MissingField(String), + #[error("Parse error: {0}")] + Parse(String), + #[error("Missing magnitude")] + MissingMagnitude, +} /// Reads and filters stars from the Tycho-2 catalog file. /// https://heasarc.gsfc.nasa.gov/w3browse/all/tycho2.html /// http://tdc-www.harvard.edu/catalogs/tycho2.format.html +/// All values should be in radians. While star coords in the catalog are in degrees, the coordinate +/// calculations make use of trig functions which deal in radians - this will be our default mode pub fn read_stars>( path: P, - min_ra: f64, - max_ra: f64, - min_dec: f64, - max_dec: f64, + filter_grid: HashSet, max_magnitude: f64, ) -> Result, CatalogError> { let file = File::open(path)?; @@ -29,18 +45,8 @@ pub fn read_stars>( let record = result?; match parse_star_record(&record) { Ok(star) => { - if star.ra_deg >= min_ra - && star.ra_deg <= max_ra - && star.de_deg >= min_dec - && star.de_deg <= max_dec - && star.mag <= max_magnitude - { - if i % 10000 == 0 { - println!( - "Star {}: RA={}, Dec={}, Mag={}", - i, star.ra_deg, star.de_deg, star.mag - ); - } + let grid_coords = &star.coords.to_grid(); + if star.mag < max_magnitude && filter_grid.contains(&grid_coords) { stars.push(star); } } @@ -63,33 +69,22 @@ pub fn read_stars>( } /// Parses a single record from the catalog into a Star struct. -fn parse_star_record(record: &csv::StringRecord) -> Result { +pub fn parse_star_record(record: &csv::StringRecord) -> Result { let ra = parse_field(record, 24, "RA")?; let dec = parse_field(record, 25, "Dec")?; let mag = parse_magnitude(record)?; Ok(Star { - ra_deg: ra, - de_deg: dec, + coords: EquatorialCoords { + ra: ra.to_radians(), + dec: dec.to_radians(), + }, mag, }) } -/// Parses a field from the record, returning a helpful error if parsing fails. -fn parse_field( - record: &csv::StringRecord, - index: usize, - field_name: &str, -) -> Result { - record - .get(index) - .ok_or_else(|| CatalogError::MissingField(field_name.to_string()))? - .trim() - .parse() - .map_err(|_| CatalogError::Parse(format!("Failed to parse {}", field_name))) -} - -fn parse_magnitude(record: &csv::StringRecord) -> Result { +/// Get magnitude off a star record +pub fn parse_magnitude(record: &csv::StringRecord) -> Result { let bt_mag = parse_field(record, 17, "BT magnitude").ok(); let vt_mag = parse_field(record, 19, "VT magnitude").ok(); @@ -112,3 +107,17 @@ fn parse_magnitude(record: &csv::StringRecord) -> Result { (None, None) => Err(CatalogError::MissingMagnitude), } } + +/// Parses a field from the record, returning a helpful error if parsing fails. +pub fn parse_field( + record: &csv::StringRecord, + index: usize, + field_name: &str, +) -> Result { + record + .get(index) + .ok_or_else(|| CatalogError::MissingField(field_name.to_string()))? + .trim() + .parse() + .map_err(|_| CatalogError::Parse(format!("Failed to parse {}", field_name))) +} diff --git a/src/render.rs b/src/render.rs deleted file mode 100644 index 5f8707b..0000000 --- a/src/render.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::types::Star; -use image::{ImageBuffer, Rgb}; - -pub fn render_stars( - stars: &[Star], - width: u32, - height: u32, - min_ra: f64, - max_ra: f64, - min_dec: f64, - max_dec: f64, -) -> ImageBuffer, Vec> { - let mut img = ImageBuffer::new(width, height); - - // Find the minimum and maximum magnitudes in the dataset - let min_mag = stars.iter().map(|s| s.mag).fold(f64::INFINITY, f64::min); - let max_mag = stars - .iter() - .map(|s| s.mag) - .fold(f64::NEG_INFINITY, f64::max); - - println!("Magnitude range: {:.3} to {:.3}", min_mag, max_mag); - - for star in stars { - let x = ((star.ra_deg - min_ra) / (max_ra - min_ra) * width as f64) as u32; - let y = ((star.de_deg - min_dec) / (max_dec - min_dec) * height as f64) as u32; - - if x < width && y < height { - // Inverse the magnitude scale (brighter stars have lower magnitudes) - let normalized_mag = (max_mag - star.mag) / (max_mag - min_mag); - - // Apply a non-linear scaling to emphasize brighter stars - let brightness = (normalized_mag.powf(2.5) * 255.0) as u8; - - let color = Rgb([brightness, brightness, brightness]); - img.put_pixel(x, y, color); - } - } - - img -} diff --git a/src/rendering.rs b/src/rendering.rs new file mode 100644 index 0000000..048909b --- /dev/null +++ b/src/rendering.rs @@ -0,0 +1,127 @@ +use image::{ImageBuffer, Rgb}; +use nalgebra::SMatrix; + +use crate::types::{EquatorialCoords, Star}; + +pub fn render_stars( + stars: Vec, + width: u32, + height: u32, + fov_center: EquatorialCoords, + fov_w: f64, + fov_h: f64, + fov_roll: f64, +) -> ImageBuffer, Vec> { + let mut img = ImageBuffer::new(width, height); + + // Find the minimum and maximum magnitudes in the dataset + let min_mag = stars.iter().map(|s| s.mag).fold(f64::INFINITY, f64::min); + let max_mag = stars + .iter() + .map(|s| s.mag) + .fold(f64::NEG_INFINITY, f64::max); + let z_roll_mat = SMatrix::::new( + fov_roll.cos(), + -fov_roll.sin(), + fov_roll.sin(), + fov_roll.cos(), + ); + let pixel_ratio_w = width as f64 / fov_w; + let pixel_ratio_h = height as f64 / fov_h; + let x_offset = width as f64 / 2.0; + let y_offset = width as f64 / 2.0; + + println!("Attempting to render {} stars", stars.len()); + let mut stars_rendered = 0; + + for star in stars { + let std_star_coords = star.coords.to_standard(fov_center); + let std_star_mat = SMatrix::::new(std_star_coords.x, std_star_coords.y); + let final_star_pos = z_roll_mat * std_star_mat; + + let x = final_star_pos.x * pixel_ratio_w + x_offset; + let y = final_star_pos.y * pixel_ratio_h + y_offset; + + if x < 0.0 || x > width as f64 || y < 0.0 || y > height as f64 { + continue; + } + + // Inverse the magnitude scale (brighter stars have lower magnitudes) + let normalized_mag = (max_mag - star.mag) / (max_mag - min_mag); + // Apply a non-linear scaling to emphasize brighter stars + let brightness = (normalized_mag.powf(2.0) * 255.0) as u8; + let color = Rgb([brightness, brightness, brightness]); + + img.put_pixel(x as u32, y as u32, color); + stars_rendered = stars_rendered + 1; + } + + println!("Actually rendered {} stars", stars_rendered); + + img +} + +/* +/// Airy disc radius - calculate physical radius, then convert to pixel scale +pub fn calc_airy_disc_radius(lambda: f64, aperture_diameter: f64, focal_length: f64, pixel_size_m: f64) -> f64 { + // Physical radius on the sensor in meters + let size = 1.22 * lambda * focal_length / aperture_diameter; + // Return radius in pixels + size / pixel_size_m +} + +/// Spatial frequency scaled as related to the diffraction pattern. Ooh! Ahh! +pub fn calc_spatial_frequency(lambda: f64, aperture_diameter: f64, focal_length: f64) -> f64 { + 2.0 * PI / lambda * aperture_diameter / focal_length +} + +/// Calculate the airy disc intensity at a given radius. Spatial frequency should be pre-calcualted +/// to avoid extra operations per loop. It is given by: +/// 2.0 * PI / lambda * aperture_diameter / focal_length +/// where lambda is wavelength of light. All arguments are in meters. There is a helper method +/// defined to do this for you (see calc_spatial_frequency) +/// +/// Airy Disc formula: +/// I(r) = I_0 * (2 * J1(kr) / kr)^2 +/// Where: +/// I(r) = Intensity at distance r from the center of the airy disc +/// I_0 = Max intensity at the center of the disc (the star itself/point light source, i.e. r = 0) +/// J1(x) = First order Bessel function of the first kind (the magic sauce) +/// k = 2 * pi / wavelength * (aperture_diameter / focal_length) -- incorporates camera params +/// r = Radial distance from the center of the disc + +pub fn calc_airy_intensity_at_radius(r: f64, spatial_frequency: f64) -> f64 { + // Bessel argument, which will give us our intensity at radius r + let kr = spatial_frequency * r; + + // At the center, we're at full intensity + if (kr == 0.0) { + return 1.0; + } + + let bessel = j1(kr); + let intensity = (2.0 * bessel / kr).powi(2); + intensity +} + +/// Appy PSF to an image at a given location +fn apply_psf(image: &mut [Vec], star_x: usize, star_y: usize, intensity: f64, radius: f64, wavelength: f64, aperture_diameter: f64, focal_length: f64) { + let size = (2.0 * radius).ceil() as usize; + let half_size = size / 2; + + for dx in 0..size { + for dy in 0..size { + let x = star_x as isize + dx as isize - half_size as isize; + let y = star_y as isize + dy as isize - half_size as isize; + + // Ensure the coordinates are within image bounds + if x >= 0 && x < image.len() as isize && y >= 0 && y < image[0].len() as isize { + let r = ((dx as f64 - half_size as f64).powi(2) + (dy as f64 - half_size as f64).powi(2)).sqrt(); + // let airy_value = airy_disc_intensity(r, wavelength, aperture_diameter, focal_length); + + // Accumulate intensity at the pixel + // image[x as usize][y as usize] += intensity * airy_value; + } + } + } +}*/ diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index edbabc0..0000000 --- a/src/types.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::io; -use thiserror::Error; - -/// Represents a star with its right ascension, declination, and magnitude. -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Star { - pub ra_deg: f64, - pub de_deg: f64, - pub mag: f64, -} - -/// Errors that can occur during star catalog reading. -#[derive(Error, Debug)] -pub enum CatalogError { - #[error("IO error: {0}")] - Io(#[from] io::Error), - #[error("CSV parsing error: {0}")] - Csv(#[from] csv::Error), - #[error("Missing field: {0}")] - MissingField(String), - #[error("Parse error: {0}")] - Parse(String), - #[error("Missing magnitude")] - MissingMagnitude, -} diff --git a/src/types/coords.rs b/src/types/coords.rs new file mode 100644 index 0000000..ec13572 --- /dev/null +++ b/src/types/coords.rs @@ -0,0 +1,93 @@ +use crate::fov::GRID_RESOLUTION; +use serde::{Deserialize, Serialize}; +use std::f64::consts::PI; +use std::hash::{Hash, Hasher}; + +#[derive(Debug, Deserialize, Serialize, Clone, Copy)] +pub struct StandardCoords { + pub x: f64, + pub y: f64, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy)] +pub struct CartesianCoords { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl CartesianCoords { + pub fn to_equatorial(&self) -> EquatorialCoords { + let ra: f64; + + if self.y < 0.0 { + ra = (2.0 * PI) - self.x.acos(); + } else { + ra = self.x.acos(); + } + + EquatorialCoords { + ra, + dec: self.z.asin(), + } + } +} + + +#[derive(Debug, Deserialize, Serialize, Clone, Copy)] +pub struct EquatorialCoords { + pub ra: f64, + pub dec: f64, +} + +impl EquatorialCoords { + /// Calculate a point's standard coordinates on the plane tangent to the celestial sphere, whose + /// center point sits tangent to the sphere where the camera's central (z) axis meets it + pub fn to_standard(&self, center: EquatorialCoords) -> StandardCoords { + // Right ascension and declination of current object, in radians + let ra = self.ra; + let dec = self.dec; + // Right ascension and declination of center point of tangent plane, in radians + let cra = center.ra; + let cdec = center.dec; + + StandardCoords { + x: (dec.cos() * (ra - cra).sin()) + / ((cdec.cos() * dec.cos() * (ra - cra).cos()) + (dec.sin() * cdec.sin())), + y: ((cdec.sin() * dec.cos() * (ra - cra).cos()) - (cdec.cos() * dec.sin())) + / ((cdec.cos() * dec.cos() * (ra - cra).cos()) + (dec.sin() * cdec.sin())), + } + } + + pub fn to_cartesian(&self) -> CartesianCoords { + CartesianCoords { + x: self.ra.cos(), + y: self.ra.sin(), + z: self.dec.sin(), + } + } + + pub fn to_grid(&self) -> EquatorialCoords { + EquatorialCoords { + ra: ((self.ra / 2.0 * PI) * (1.0 - (2.0 * self.dec / PI).abs()) * GRID_RESOLUTION).round(), + dec: ((2.0 * self.dec / PI) * GRID_RESOLUTION).round(), + } + } +} + +impl PartialEq for EquatorialCoords { + fn eq(&self, other: &Self) -> bool { + self.ra == other.ra && self.dec == other.dec + } +} + +impl Eq for EquatorialCoords { +} + +impl Hash for EquatorialCoords { + fn hash(&self, state: &mut H) { + (self.ra as i32).hash(state); + (self.dec as i32).hash(state); + } +} + diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..4d64f97 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,5 @@ +mod coords; +mod star; + +pub use coords::*; +pub use star::*; diff --git a/src/types/star.rs b/src/types/star.rs new file mode 100644 index 0000000..f99c3b6 --- /dev/null +++ b/src/types/star.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::EquatorialCoords; + +/// Represents a star with its right ascension, declination, and magnitude. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Star { + pub coords: EquatorialCoords, + pub mag: f64, +}