starhash 1.0.0__py3-none-any.whl

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.
starhash/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
starhash/core.py ADDED
@@ -0,0 +1,194 @@
1
+ import logging
2
+ from importlib.resources import files
3
+ from importlib.resources.abc import Traversable
4
+ from pathlib import Path
5
+
6
+ import rich_click as click
7
+ from click import Context, FloatRange
8
+ from ff3 import FF3Cipher
9
+ from healpy import ang2pix, nside2npix, nside2resol, pix2ang
10
+ from rich.logging import RichHandler
11
+
12
+ from starhash import __version__
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Reference implementation details
17
+ STARHASH_KEY = b"starhash!"
18
+ STARHASH_TWEAK = b"opensource"
19
+ HEALPIX_NSIDE = 65536
20
+ WORD_SEPARATOR = "-"
21
+
22
+ # Paths to bundled wordlists
23
+ EFF_LARGEWORDLIST_PATH = files("starhash.data").joinpath("eff_largewordlist.txt")
24
+ ASTROWORDLIST_PATH = files("starhash.data").joinpath("astronames_boost.txt")
25
+ COMBOLIST_PATH = files("starhash.data").joinpath("combolist.txt")
26
+
27
+
28
+ def setup_logger(level: int | str = logging.DEBUG) -> None:
29
+ """
30
+ Add a rich-formatted logger for development purposes with color output.
31
+ """
32
+ logger.setLevel(level=level)
33
+
34
+ handler = RichHandler(
35
+ rich_tracebacks=True,
36
+ tracebacks_show_locals=True,
37
+ markup=True,
38
+ show_time=True,
39
+ show_level=True,
40
+ show_path=True,
41
+ )
42
+ handler.setLevel(level=level)
43
+
44
+ formatter = logging.Formatter(fmt="%(message)s", datefmt="[%Y-%m-%d %H:%M:%S]")
45
+ handler.setFormatter(formatter)
46
+
47
+ logger.addHandler(handler)
48
+
49
+
50
+ class StarHash:
51
+ """Class to generate human-readable names from astronomical coordinates."""
52
+
53
+ def __init__(
54
+ self,
55
+ key: bytes = STARHASH_KEY,
56
+ tweak: bytes = STARHASH_TWEAK,
57
+ healpix_nside: int = HEALPIX_NSIDE,
58
+ wordlist_path: Path | Traversable = COMBOLIST_PATH,
59
+ k_words: int = 3,
60
+ init_logger: bool = False,
61
+ ) -> None:
62
+ self.word_separator = WORD_SEPARATOR
63
+ self.wordlist: list[str] = []
64
+ self.k_words = k_words
65
+ self.wordlist_path = wordlist_path
66
+ self.num_words: int = 0
67
+ self.healpix_nside = healpix_nside
68
+ self.healpix_resol = nside2resol(self.healpix_nside, arcmin=True)
69
+ self.npix = nside2npix(self.healpix_nside)
70
+
71
+ # Set size of padding appropriate to max healpix idx
72
+ self.padding_length = len(str(self.npix))
73
+
74
+ if init_logger:
75
+ setup_logger(logging.DEBUG)
76
+
77
+ self.coverage = 0
78
+ logger.debug(
79
+ "healpix grid properties: %i pixels with size %.1f arcsec",
80
+ self.npix,
81
+ self.healpix_resol * 60,
82
+ )
83
+
84
+ # Pad key to correct length
85
+ self.key = key.ljust(32, b"\x00").hex()
86
+
87
+ # Tweak must be 8 bytes long
88
+ self.tweak = tweak[:8].hex()
89
+
90
+ self.cipher = FF3Cipher(key=self.key, tweak=self.tweak)
91
+ self.load_wordlist()
92
+
93
+ def load_wordlist(self) -> None:
94
+ """Load the word list specified in the class."""
95
+ with self.wordlist_path.open() as f:
96
+ self.wordlist = f.read().splitlines()
97
+ self.num_words = len(self.wordlist)
98
+ logger.debug("%s unique words in word list", self.num_words)
99
+ self.coverage = pow(self.num_words, self.k_words) / self.npix
100
+ logger.debug("Grid coverage factor: %.3f x", self.coverage)
101
+
102
+ if self.coverage < 1:
103
+ raise IncompleteHEALPIXCoverageError
104
+
105
+ def coordinate_to_words(self, ra: float, dec: float) -> str:
106
+ idx = ang2pix(self.healpix_nside, ra, dec, lonlat=True)
107
+ stridx = str(idx).zfill(self.padding_length)
108
+
109
+ idx_encrypted = int(self.cipher.encrypt(stridx))
110
+
111
+ # Repeated modulo division recovers the indices
112
+ words: list[str] = []
113
+ temp = idx_encrypted
114
+ for _ in range(self.k_words):
115
+ words.append(self.wordlist[temp % self.num_words])
116
+
117
+ temp //= self.num_words
118
+
119
+ return self.word_separator.join(words)
120
+
121
+ def words_to_coordinate(self, input_word_str: str) -> tuple[float, float]:
122
+ indices = [self.wordlist.index(w) for w in input_word_str.split(self.word_separator)]
123
+
124
+ # Undo the modulo procedure above to recover the original index
125
+ idx_encrypted = sum(word_idx * (self.num_words**i) for i, word_idx in enumerate(indices))
126
+
127
+ stridx_encrypted = str(idx_encrypted).zfill(self.padding_length)
128
+ stridx_decrypted = self.cipher.decrypt(stridx_encrypted)
129
+ idx = int(stridx_decrypted)
130
+
131
+ if idx > self.npix:
132
+ raise InvalidHEALPIXIndexError
133
+
134
+ ra, dec = pix2ang(self.healpix_nside, idx, lonlat=True)
135
+
136
+ return ra, dec
137
+
138
+
139
+ def collate_wordlist() -> list[str]:
140
+ wordlist = []
141
+ with EFF_LARGEWORDLIST_PATH.open() as f:
142
+ logger.debug("Loading EFF wordlist")
143
+
144
+ for line in f.read().splitlines():
145
+ if "#" not in line:
146
+ try:
147
+ wordlist.append(line.split("\t")[1].replace(" ", "").replace("-", " "))
148
+ except IndexError:
149
+ logger.exception(line)
150
+ continue
151
+ with ASTROWORDLIST_PATH.open() as f:
152
+ logger.debug("Loading astronomical wordlist")
153
+ wordlist.extend(f.read().splitlines())
154
+
155
+ return sorted(set(wordlist))
156
+
157
+
158
+ class IncompleteHEALPIXCoverageError(Exception):
159
+ """The chosen StarHash parameters do not fully cover the chosen HEALPIX grid."""
160
+
161
+
162
+ class InvalidHEALPIXIndexError(Exception):
163
+ """The given coordinates map to an invalid HEALPIX index."""
164
+
165
+
166
+ @click.group(context_settings={"show_default": True})
167
+ @click.version_option(__version__, prog_name="starhash-cli")
168
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging output")
169
+ @click.pass_context
170
+ def cli(ctx: Context, verbose: bool) -> None:
171
+ ctx.obj = StarHash(init_logger=verbose)
172
+
173
+
174
+ @cli.command()
175
+ @click.option("--ra", "-r", type=FloatRange(min=0, max=360), required=True)
176
+ @click.option("--dec", "-d", type=FloatRange(min=-90, max=90), required=True)
177
+ @click.pass_obj
178
+ def get_name_from_coord(sh: StarHash, ra: float, dec: float) -> None:
179
+ """Get name corresponding to ra, dec."""
180
+ name = sh.coordinate_to_words(ra, dec)
181
+ click.echo(name)
182
+
183
+
184
+ @cli.command()
185
+ @click.argument("name", type=str)
186
+ @click.pass_obj
187
+ def get_coord_from_name(sh: StarHash, name: str) -> None:
188
+ """Get ra and dec corresponding to name."""
189
+ ra, dec = sh.words_to_coordinate(name)
190
+ click.echo(f"{ra} {dec}")
191
+
192
+
193
+ if __name__ == "__main__":
194
+ cli()
File without changes
@@ -0,0 +1,226 @@
1
+ paynegaposchkin
2
+ swannleavitt
3
+ cannon
4
+ fleming
5
+ burbidge
6
+ faber
7
+ rubin
8
+ bellburnell
9
+ herschel
10
+ messier
11
+ galileo
12
+ kepler
13
+ copernicus
14
+ brahe
15
+ newton
16
+ halley
17
+ hale
18
+ shapley
19
+ baade
20
+ sagan
21
+ hoyle
22
+ hawking
23
+ chandrasekhar
24
+ eddington
25
+ bethe
26
+ gamow
27
+ hubble
28
+ zwicky
29
+ penrose
30
+ flare
31
+ burst
32
+ grb
33
+ tidal
34
+ disruption
35
+ accretion
36
+ disk
37
+ jet
38
+ outflow
39
+ wind
40
+ shock
41
+ wave
42
+ pulse
43
+ period
44
+ phase
45
+ cycle
46
+ precession
47
+ nutation
48
+ libration
49
+ moon
50
+ sun
51
+ star
52
+ planet
53
+ asteroid
54
+ comet
55
+ meteor
56
+ meteorite
57
+ bolide
58
+ pulsar
59
+ quasar
60
+ blazar
61
+ magnetar
62
+ neutron
63
+ mercury
64
+ venus
65
+ earth
66
+ mars
67
+ jupiter
68
+ saturn
69
+ uranus
70
+ neptune
71
+ pluto
72
+ ceres
73
+ vesta
74
+ pallas
75
+ juno
76
+ haumea
77
+ makemake
78
+ eris
79
+ sedna
80
+ orcus
81
+ quaoar
82
+ titan
83
+ europa
84
+ io
85
+ ganymede
86
+ callisto
87
+ enceladus
88
+ triton
89
+ charon
90
+ phobos
91
+ deimos
92
+ oberon
93
+ miranda
94
+ ariel
95
+ umbriel
96
+ titania
97
+ rhea
98
+ iapetus
99
+ dione
100
+ tethys
101
+ goto
102
+ zwicky
103
+ hubble
104
+ rubin
105
+ webb
106
+ wise
107
+ gaia
108
+ tess
109
+ kepler
110
+ spitzer
111
+ cheops
112
+ herschel
113
+ planck
114
+ chandra
115
+ fermi
116
+ swift
117
+ integral
118
+ rosat
119
+ nustar
120
+ atlas
121
+ panstarrs
122
+ asassn
123
+ ogle
124
+ macho
125
+ decam
126
+ lsst
127
+ subaru
128
+ keck
129
+ gemini
130
+ magellan
131
+ vlt
132
+ lamost
133
+ orion
134
+ ursa
135
+ draco
136
+ cygnus
137
+ aquila
138
+ lyra
139
+ cassiopeia
140
+ andromeda
141
+ perseus
142
+ cepheus
143
+ hercules
144
+ bootes
145
+ virgo
146
+ leo
147
+ gemini
148
+ cancer
149
+ taurus
150
+ aries
151
+ scorpius
152
+ sagittarius
153
+ capricornus
154
+ aquarius
155
+ pisces
156
+ libra
157
+ ophiuchus
158
+ serpens
159
+ corona
160
+ pegasus
161
+ auriga
162
+ canis
163
+ major
164
+ minor
165
+ lupus
166
+ centaurus
167
+ crux
168
+ carina
169
+ vela
170
+ puppis
171
+ hydra
172
+ crater
173
+ corvus
174
+ columba
175
+ lepus
176
+ eridanus
177
+ phoenix
178
+ tucana
179
+ grus
180
+ pavo
181
+ indus
182
+ galaxy
183
+ spiral
184
+ elliptical
185
+ irregular
186
+ lenticular
187
+ barred
188
+ andromeda
189
+ milky
190
+ way
191
+ magellanic
192
+ cloud
193
+ whirlpool
194
+ sombrero
195
+ pinwheel
196
+ triangulum
197
+ centaurus
198
+ virgo
199
+ fornax
200
+ sculptor
201
+ pegasus
202
+ exoplanet
203
+ szyzgy
204
+ culmination
205
+ opposition
206
+ ouamuamua
207
+ borisov
208
+ desi
209
+ sloan
210
+ astrolabe
211
+ sextant
212
+ quadrant
213
+ alien
214
+ transit
215
+ conjunction
216
+ parallax
217
+ hypatia
218
+ loeb
219
+ aberration
220
+ albedo
221
+ altitude
222
+ annulus
223
+ apex
224
+ apogee
225
+ arcsecond
226
+ asterism