Compare commits
1 Commits
devel
...
6103ab4d7d
| Author | SHA1 | Date | |
|---|---|---|---|
| 6103ab4d7d |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -5,30 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [1.3.1] - 2026-04-22
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `Body.EARTH` enum value with `rotation_hours = 23.934`, `orbital_hours = 8766.0`, discovery epoch 1543-01-01 (Copernicus), contact epoch 1969-07-20 (Apollo 11)
|
|
||||||
- `Luna` moon (Earth's Moon): tidally locked, `rotation_hours = orbital_hours = 655.72`, discovery epoch 1609-11-01 (Galileo), contact epoch 1969-07-20 (Apollo 11)
|
|
||||||
- `LUNA` constant exported from `moon.py`
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `Body.__getitem__` now uses 1-based indexing — `Body.MARS[1]` returns Phobos, `Body.MARS[2]` returns Deimos
|
|
||||||
|
|
||||||
## [1.3.0] - 2026-04-21
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `scripts/refresh_data.py` — fetches rotation periods, orbital periods, and discovery/contact dates from Wikidata SPARQL and regenerates `src/planetarytime/_data.py`; supports `--dry-run`; not part of the distributed package
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Planetary and moon data extracted into `src/planetarytime/_data.py` (`PLANET_ROWS`, `MOON_ROWS`) — single source of truth for all numerical constants
|
|
||||||
- `body.py`, `moon.py`, `epoch.py` now derive their data dictionaries from `_data.py` instead of hardcoding values inline
|
|
||||||
- `README.md` — updated usage examples with current output values, added Exceptions section, added Refreshing data section
|
|
||||||
|
|
||||||
## [1.2.0] - 2026-04-16
|
## [1.2.0] - 2026-04-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -83,4 +83,7 @@ Moon and dwarf planets (Pluto, Ceres, Eris) may be added later.
|
|||||||
- TODO: Implement core `PlanetaryTime` class
|
- TODO: Implement core `PlanetaryTime` class
|
||||||
- TODO: Implement conversion from Earth `datetime`
|
- TODO: Implement conversion from Earth `datetime`
|
||||||
- TODO: Implement `__str__` / `__repr__`
|
- TODO: Implement `__str__` / `__repr__`
|
||||||
|
- TODO: Write tests for conversion accuracy
|
||||||
|
- TODO: Write tests for epoch switching
|
||||||
|
- TODO: Populate README with usage examples
|
||||||
- TODO: Implement `scripts/refresh_data.py` — fetches rotation periods, orbital periods and discovery dates from Wikidata SPARQL endpoint and regenerates hardcoded data in `body.py`, `moon.py` and `epoch.py`; script is not part of the distributed package
|
- TODO: Implement `scripts/refresh_data.py` — fetches rotation periods, orbital periods and discovery dates from Wikidata SPARQL endpoint and regenerates hardcoded data in `body.py`, `moon.py` and `epoch.py`; script is not part of the distributed package
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -60,37 +60,32 @@ now = datetime.now(timezone.utc)
|
|||||||
# Mars time since discovery (Galileo, 1610)
|
# Mars time since discovery (Galileo, 1610)
|
||||||
pt = PlanetaryTime.from_earth(now, Body.MARS, EpochType.DISCOVERY)
|
pt = PlanetaryTime.from_earth(now, Body.MARS, EpochType.DISCOVERY)
|
||||||
print(pt)
|
print(pt)
|
||||||
# Year 217, Sol 579, 11:00:00 (Mars / discovery epoch)
|
# Year 415, Sol 668, 14:22:07 (Mars / discovery epoch)
|
||||||
|
|
||||||
print(pt.year) # 217
|
print(pt.year) # 415
|
||||||
print(pt.sol) # 579
|
print(pt.sol) # 668
|
||||||
print(pt.hour) # 11
|
print(pt.hour) # 14
|
||||||
print(pt.minute) # 0
|
print(pt.minute) # 22
|
||||||
print(pt.second) # 0
|
print(pt.second) # 7
|
||||||
print(pt.time) # "11:00:00"
|
|
||||||
print(pt.date) # "Year 217, Sol 579"
|
|
||||||
|
|
||||||
# Mars time since first contact (Viking 1, 1976)
|
# Mars time since first contact (Viking 1, 1976)
|
||||||
pt = PlanetaryTime.from_earth(now, Body.MARS, EpochType.CONTACT)
|
pt = PlanetaryTime.from_earth(now, Body.MARS, EpochType.CONTACT)
|
||||||
print(pt)
|
print(pt)
|
||||||
# Year 26, Sol 25, 15:00:00 (Mars / contact epoch)
|
# Year 25, Sol 23, 14:22:07 (Mars / contact epoch)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Moons
|
### Moons
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Titan (Saturn's largest moon) — accessible via Body.SATURN[0]
|
# Titan time since discovery (Huygens, 1655)
|
||||||
titan = Body.SATURN[0]
|
titan = Body.SATURN[0]
|
||||||
|
|
||||||
# Time since discovery (Christiaan Huygens, 1655)
|
|
||||||
pt = PlanetaryTime.from_earth(now, titan, EpochType.DISCOVERY)
|
pt = PlanetaryTime.from_earth(now, titan, EpochType.DISCOVERY)
|
||||||
print(pt)
|
print(pt)
|
||||||
# Year 8492, Sol 0, 344:00:00 (Titan / discovery epoch)
|
# Year 1, Sol 0, 08:11:45 (Titan / discovery epoch)
|
||||||
|
|
||||||
# Time since Huygens probe landing (2005-01-14)
|
# Titan time since Huygens probe landing (2005)
|
||||||
pt = PlanetaryTime.from_earth(now, titan, EpochType.CONTACT)
|
pt = PlanetaryTime.from_earth(now, titan, EpochType.CONTACT)
|
||||||
print(pt)
|
print(pt)
|
||||||
# Year 486, Sol 0, 282:00:00 (Titan / contact epoch)
|
|
||||||
|
|
||||||
# Check if a moon is tidally locked
|
# Check if a moon is tidally locked
|
||||||
print(titan.is_tidally_locked) # True
|
print(titan.is_tidally_locked) # True
|
||||||
@@ -99,32 +94,12 @@ print(titan.is_tidally_locked) # True
|
|||||||
### Epochs
|
### Epochs
|
||||||
|
|
||||||
| EpochType | Meaning |
|
| EpochType | Meaning |
|
||||||
|-----------------------|-------------------------------------------|
|
|--------------------|----------------------------------------------|
|
||||||
| `EpochType.DISCOVERY` | First recorded observation of the body |
|
| `EpochType.DISCOVERY` | First recorded observation of the body |
|
||||||
| `EpochType.CONTACT` | First probe landing or crewed landing |
|
| `EpochType.CONTACT` | First probe landing or crewed landing |
|
||||||
|
|
||||||
`EpochUnavailableError` is raised when `CONTACT` is requested for a body that has not been visited yet.
|
`EpochUnavailableError` is raised when `CONTACT` is requested for a body that has not been visited yet.
|
||||||
|
|
||||||
### Exceptions
|
|
||||||
|
|
||||||
```python
|
|
||||||
from planetarytime import EpochType, PlanetaryTime, Body
|
|
||||||
from planetarytime.exceptions import EpochUnavailableError, DatetimePrecedesEpochError
|
|
||||||
|
|
||||||
# Body with no contact yet
|
|
||||||
try:
|
|
||||||
PlanetaryTime.from_earth(now, Body.JUPITER, EpochType.CONTACT)
|
|
||||||
except EpochUnavailableError as e:
|
|
||||||
print(e) # No contact with Jupiter has occurred — contact epoch is unavailable.
|
|
||||||
|
|
||||||
# Datetime before the epoch
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
try:
|
|
||||||
PlanetaryTime.from_earth(datetime(1600, 1, 1, tzinfo=timezone.utc), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
except DatetimePrecedesEpochError as e:
|
|
||||||
print(e)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
This library uses [loguru](https://github.com/Delgan/loguru) for internal logging.
|
This library uses [loguru](https://github.com/Delgan/loguru) for internal logging.
|
||||||
@@ -146,17 +121,6 @@ import sys
|
|||||||
logger.add(sys.stderr, filter={"planetarytime": "WARNING"})
|
logger.add(sys.stderr, filter={"planetarytime": "WARNING"})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Refreshing data
|
|
||||||
|
|
||||||
Rotation periods, orbital periods, and discovery/contact dates are stored in [`src/planetarytime/_data.py`](src/planetarytime/_data.py). To regenerate this file from Wikidata:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/refresh_data.py # fetch and write _data.py
|
|
||||||
python scripts/refresh_data.py --dry-run # preview without writing
|
|
||||||
```
|
|
||||||
|
|
||||||
The script requires only the Python standard library. Run your test suite afterwards to verify the updated values.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planetarytime"
|
name = "planetarytime"
|
||||||
version = "1.3.1"
|
version = "1.2.0"
|
||||||
description = "Python library for representing and working with time on other bodies in the Solar System"
|
description = "Python library for representing and working with time on other bodies in the Solar System"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Jan Doubravský", email = "jan.doubravsky@gmail.com"}
|
{name = "Jan Doubravský", email = "jan.doubravsky@gmail.com"}
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
"""refresh_data.py — fetch planetary/moon data from Wikidata and regenerate _data.py.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python scripts/refresh_data.py [--dry-run]
|
|
||||||
|
|
||||||
Writes:
|
|
||||||
src/planetarytime/_data.py
|
|
||||||
|
|
||||||
Requires only the Python standard library (urllib).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import date, datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Paths
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
DATA_FILE = REPO_ROOT / "src" / "planetarytime" / "_data.py"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Wikidata SPARQL helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
SPARQL_ENDPOINT = "https://query.wikidata.org/sparql"
|
|
||||||
USER_AGENT = "planetarytime-refresh/1.0 (https://github.com/jan-doubravsky/planetarytime)"
|
|
||||||
|
|
||||||
|
|
||||||
def sparql_query(query: str) -> list[dict]:
|
|
||||||
"""Execute a SPARQL SELECT query against Wikidata and return the bindings."""
|
|
||||||
params = urllib.parse.urlencode({"query": query, "format": "json"})
|
|
||||||
url = f"{SPARQL_ENDPOINT}?{params}"
|
|
||||||
req = urllib.request.Request(url, headers={
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"Accept": "application/sparql-results+json",
|
|
||||||
})
|
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
||||||
data = json.loads(resp.read().decode())
|
|
||||||
return data["results"]["bindings"]
|
|
||||||
|
|
||||||
|
|
||||||
def _float(binding: dict, key: str) -> float | None:
|
|
||||||
v = binding.get(key, {}).get("value")
|
|
||||||
return float(v) if v is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
def _date(binding: dict, key: str) -> date | None:
|
|
||||||
v = binding.get(key, {}).get("value")
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
v = v.lstrip("+")
|
|
||||||
try:
|
|
||||||
return datetime.fromisoformat(v.replace("Z", "+00:00")).date()
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Planet data
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
PLANET_QUERY = """\
|
|
||||||
SELECT ?name ?rotationHours ?orbitalDays ?discoveryDate WHERE {
|
|
||||||
VALUES ?item {
|
|
||||||
wd:Q308 # Mercury
|
|
||||||
wd:Q313 # Venus
|
|
||||||
wd:Q111 # Mars
|
|
||||||
wd:Q319 # Jupiter
|
|
||||||
wd:Q193 # Saturn
|
|
||||||
wd:Q324 # Uranus
|
|
||||||
wd:Q332 # Neptune
|
|
||||||
}
|
|
||||||
?item rdfs:label ?name FILTER(LANG(?name) = "en").
|
|
||||||
OPTIONAL {
|
|
||||||
?item p:P2386 ?rotStmt.
|
|
||||||
?rotStmt ps:P2386 ?rotationHours.
|
|
||||||
?rotStmt psv:P2386/wikibase:quantityUnit wd:Q7727.
|
|
||||||
}
|
|
||||||
OPTIONAL {
|
|
||||||
?item p:P2257 ?orbStmt.
|
|
||||||
?orbStmt ps:P2257 ?orbitalDays.
|
|
||||||
?orbStmt psv:P2257/wikibase:quantityUnit wd:Q573.
|
|
||||||
}
|
|
||||||
OPTIONAL { ?item wdt:P575 ?discoveryDate. }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
CONTACT_QUERY = """\
|
|
||||||
SELECT ?name ?contactDate WHERE {
|
|
||||||
VALUES (?probe ?planet) {
|
|
||||||
(wd:Q1573 wd:Q308) # MESSENGER — Mercury
|
|
||||||
(wd:Q170 wd:Q313) # Venera 7 — Venus
|
|
||||||
(wd:Q160102 wd:Q111) # Viking 1 — Mars
|
|
||||||
}
|
|
||||||
?planet rdfs:label ?name FILTER(LANG(?name) = "en").
|
|
||||||
?probe wdt:P619 ?contactDate.
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlanetData:
|
|
||||||
name: str
|
|
||||||
rotation_hours: float
|
|
||||||
orbital_hours: float
|
|
||||||
discovery_date: date
|
|
||||||
contact_date: date | None = None
|
|
||||||
|
|
||||||
|
|
||||||
_FALLBACK_PLANETS: list[PlanetData] = [
|
|
||||||
PlanetData("Mercury", 1407.6, 87.97 * 24, date(1631, 11, 7), date(2011, 3, 18)),
|
|
||||||
PlanetData("Venus", 5832.5, 224.70 * 24, date(1610, 1, 1), date(1970, 12, 15)),
|
|
||||||
PlanetData("Mars", 24.6, 686.97 * 24, date(1610, 1, 1), date(1976, 7, 20)),
|
|
||||||
PlanetData("Jupiter", 9.9, 4332.59 * 24, date(1610, 1, 7), None),
|
|
||||||
PlanetData("Saturn", 10.7, 10759.22 * 24, date(1610, 7, 25), None),
|
|
||||||
PlanetData("Uranus", 17.2, 30688.50 * 24, date(1781, 3, 13), None),
|
|
||||||
PlanetData("Neptune", 16.1, 60182.00 * 24, date(1846, 9, 23), None),
|
|
||||||
]
|
|
||||||
|
|
||||||
_PLANET_ORDER = [p.name for p in _FALLBACK_PLANETS]
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_planets() -> list[PlanetData]:
|
|
||||||
print("Fetching planet data from Wikidata…")
|
|
||||||
try:
|
|
||||||
rows = sparql_query(PLANET_QUERY)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f" WARNING: planet query failed ({exc}), using fallback data.", file=sys.stderr)
|
|
||||||
return _FALLBACK_PLANETS
|
|
||||||
|
|
||||||
by_name: dict[str, PlanetData] = {}
|
|
||||||
for row in rows:
|
|
||||||
name = row["name"]["value"]
|
|
||||||
rot = _float(row, "rotationHours")
|
|
||||||
orb_days = _float(row, "orbitalDays")
|
|
||||||
disc = _date(row, "discoveryDate")
|
|
||||||
if rot is None or orb_days is None or disc is None:
|
|
||||||
continue
|
|
||||||
by_name[name] = PlanetData(name, abs(rot), orb_days * 24, disc)
|
|
||||||
|
|
||||||
try:
|
|
||||||
for row in sparql_query(CONTACT_QUERY):
|
|
||||||
name = row["name"]["value"]
|
|
||||||
d = _date(row, "contactDate")
|
|
||||||
if name in by_name and d is not None:
|
|
||||||
by_name[name].contact_date = d
|
|
||||||
except Exception as exc:
|
|
||||||
print(f" WARNING: contact query failed ({exc}).", file=sys.stderr)
|
|
||||||
|
|
||||||
fallback_map = {p.name: p for p in _FALLBACK_PLANETS}
|
|
||||||
result: list[PlanetData] = []
|
|
||||||
for name in _PLANET_ORDER:
|
|
||||||
if name in by_name:
|
|
||||||
result.append(by_name[name])
|
|
||||||
else:
|
|
||||||
print(f" WARNING: {name} missing from Wikidata, using fallback.", file=sys.stderr)
|
|
||||||
result.append(fallback_map[name])
|
|
||||||
|
|
||||||
print(f" Planets: {', '.join(p.name for p in result)}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Moon data
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
MOON_QUERY = """\
|
|
||||||
SELECT ?moonLabel ?rotationHours ?orbitalHours ?discoveryDate ?contactDate WHERE {
|
|
||||||
VALUES ?moon {
|
|
||||||
wd:Q40 # Phobos
|
|
||||||
wd:Q39 # Deimos
|
|
||||||
wd:Q36236 # Io
|
|
||||||
wd:Q36712 # Europa
|
|
||||||
wd:Q44537 # Ganymede
|
|
||||||
wd:Q44523 # Callisto
|
|
||||||
wd:Q2565 # Titan
|
|
||||||
wd:Q3532 # Enceladus
|
|
||||||
wd:Q3552 # Miranda
|
|
||||||
wd:Q3551 # Ariel
|
|
||||||
wd:Q3543 # Umbriel
|
|
||||||
wd:Q3555 # Titania
|
|
||||||
wd:Q3547 # Oberon
|
|
||||||
wd:Q3561 # Triton
|
|
||||||
}
|
|
||||||
?moon rdfs:label ?moonLabel FILTER(LANG(?moonLabel) = "en").
|
|
||||||
OPTIONAL {
|
|
||||||
?moon p:P2386 ?rotStmt.
|
|
||||||
?rotStmt ps:P2386 ?rotationHours.
|
|
||||||
?rotStmt psv:P2386/wikibase:quantityUnit wd:Q7727.
|
|
||||||
}
|
|
||||||
OPTIONAL {
|
|
||||||
?moon p:P2257 ?orbStmt.
|
|
||||||
?orbStmt ps:P2257 ?orbDays.
|
|
||||||
?orbStmt psv:P2257/wikibase:quantityUnit wd:Q573.
|
|
||||||
BIND(?orbDays * 24.0 AS ?orbitalHours)
|
|
||||||
}
|
|
||||||
OPTIONAL { ?moon wdt:P575 ?discoveryDate. }
|
|
||||||
OPTIONAL { ?moon wdt:P619 ?contactDate. }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MoonData:
|
|
||||||
name: str
|
|
||||||
rotation_hours: float
|
|
||||||
orbital_hours: float
|
|
||||||
is_tidally_locked: bool
|
|
||||||
discovery_date: date
|
|
||||||
contact_date: date | None = None
|
|
||||||
|
|
||||||
|
|
||||||
_FALLBACK_MOONS: list[MoonData] = [
|
|
||||||
MoonData("Phobos", 7.653, 7.653, True, date(1877, 8, 18)),
|
|
||||||
MoonData("Deimos", 30.312, 30.312, True, date(1877, 8, 12)),
|
|
||||||
MoonData("Io", 42.456, 42.456, True, date(1610, 1, 8)),
|
|
||||||
MoonData("Europa", 85.228, 85.228, True, date(1610, 1, 8)),
|
|
||||||
MoonData("Ganymede", 171.709, 171.709, True, date(1610, 1, 7)),
|
|
||||||
MoonData("Callisto", 400.535, 400.535, True, date(1610, 1, 7)),
|
|
||||||
MoonData("Titan", 382.690, 382.690, True, date(1655, 3, 25), date(2005, 1, 14)),
|
|
||||||
MoonData("Enceladus", 32.923, 32.923, True, date(1789, 8, 28)),
|
|
||||||
MoonData("Miranda", 33.923, 33.923, True, date(1948, 2, 16)),
|
|
||||||
MoonData("Ariel", 60.489, 60.489, True, date(1851, 10, 24)),
|
|
||||||
MoonData("Umbriel", 99.460, 99.460, True, date(1851, 10, 24)),
|
|
||||||
MoonData("Titania", 208.940, 208.940, True, date(1787, 1, 11)),
|
|
||||||
MoonData("Oberon", 323.117, 323.117, True, date(1787, 1, 11)),
|
|
||||||
MoonData("Triton", 141.045, 141.045, True, date(1846, 10, 10)),
|
|
||||||
]
|
|
||||||
|
|
||||||
_MOON_ORDER = [m.name for m in _FALLBACK_MOONS]
|
|
||||||
_TIDAL_THRESHOLD = 0.01
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_moons() -> list[MoonData]:
|
|
||||||
print("Fetching moon data from Wikidata…")
|
|
||||||
try:
|
|
||||||
rows = sparql_query(MOON_QUERY)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f" WARNING: moon query failed ({exc}), using fallback data.", file=sys.stderr)
|
|
||||||
return _FALLBACK_MOONS
|
|
||||||
|
|
||||||
by_name: dict[str, MoonData] = {}
|
|
||||||
for row in rows:
|
|
||||||
name = row["moonLabel"]["value"]
|
|
||||||
rot = _float(row, "rotationHours")
|
|
||||||
orb = _float(row, "orbitalHours")
|
|
||||||
disc = _date(row, "discoveryDate")
|
|
||||||
if rot is None or orb is None or disc is None:
|
|
||||||
continue
|
|
||||||
rot, orb = abs(rot), abs(orb)
|
|
||||||
locked = abs(rot - orb) / max(rot, orb) < _TIDAL_THRESHOLD
|
|
||||||
by_name[name] = MoonData(name, rot, orb, locked, disc, _date(row, "contactDate"))
|
|
||||||
|
|
||||||
fallback_map = {m.name: m for m in _FALLBACK_MOONS}
|
|
||||||
result: list[MoonData] = []
|
|
||||||
for name in _MOON_ORDER:
|
|
||||||
if name in by_name:
|
|
||||||
result.append(by_name[name])
|
|
||||||
else:
|
|
||||||
print(f" WARNING: {name} missing from Wikidata, using fallback.", file=sys.stderr)
|
|
||||||
result.append(fallback_map[name])
|
|
||||||
|
|
||||||
print(f" Moons: {', '.join(m.name for m in result)}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Code generation — writes only _data.py
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _dr(d: date) -> str:
|
|
||||||
return f"date({d.year}, {d.month:2d}, {d.day:2d})"
|
|
||||||
|
|
||||||
|
|
||||||
def _dr_opt(d: date | None) -> str:
|
|
||||||
return "None" if d is None else _dr(d)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_data_py(planets: list[PlanetData], moons: list[MoonData]) -> str:
|
|
||||||
lines = [
|
|
||||||
"# AUTO-GENERATED by scripts/refresh_data.py — do not edit by hand.",
|
|
||||||
"from datetime import date",
|
|
||||||
"",
|
|
||||||
"# (name, rotation_hours, orbital_hours, discovery_date, contact_date | None)",
|
|
||||||
"PLANET_ROWS: list[tuple[str, float, float, date, date | None]] = [",
|
|
||||||
]
|
|
||||||
for p in planets:
|
|
||||||
lines.append(
|
|
||||||
f" ({p.name!r:12s}, {p.rotation_hours:10.3f}, {p.orbital_hours:12.4f},"
|
|
||||||
f" {_dr(p.discovery_date)}, {_dr_opt(p.contact_date)}),"
|
|
||||||
)
|
|
||||||
lines += [
|
|
||||||
"]",
|
|
||||||
"",
|
|
||||||
"# (name, rotation_hours, orbital_hours, is_tidally_locked, discovery_date, contact_date | None)",
|
|
||||||
"MOON_ROWS: list[tuple[str, float, float, bool, date, date | None]] = [",
|
|
||||||
]
|
|
||||||
for m in moons:
|
|
||||||
lines.append(
|
|
||||||
f" ({m.name!r:12s}, {m.rotation_hours:8.3f}, {m.orbital_hours:8.3f},"
|
|
||||||
f" {str(m.is_tidally_locked):5s}, {_dr(m.discovery_date)}, {_dr_opt(m.contact_date)}),"
|
|
||||||
)
|
|
||||||
lines += ["]", ""]
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(description="Refresh planetary data from Wikidata.")
|
|
||||||
parser.add_argument("--dry-run", action="store_true", help="Print generated _data.py without writing.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
planets = fetch_planets()
|
|
||||||
moons = fetch_moons()
|
|
||||||
content = generate_data_py(planets, moons)
|
|
||||||
|
|
||||||
if args.dry_run:
|
|
||||||
print(f"\n# {DATA_FILE}\n{'=' * 60}")
|
|
||||||
print(content)
|
|
||||||
else:
|
|
||||||
DATA_FILE.write_text(content, encoding="utf-8")
|
|
||||||
print(f"\nWritten: {DATA_FILE}")
|
|
||||||
print("Run your test suite to verify the updated data.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# AUTO-GENERATED by scripts/refresh_data.py — do not edit by hand.
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
# (name, rotation_hours, orbital_hours, discovery_date, contact_date | None)
|
|
||||||
PLANET_ROWS: list[tuple[str, float, float, date, date | None]] = [
|
|
||||||
('Mercury' , 1407.600, 2111.2800, date(1631, 11, 7), date(2011, 3, 18)),
|
|
||||||
('Venus' , 5832.500, 5392.8000, date(1610, 1, 1), date(1970, 12, 15)),
|
|
||||||
('Earth' , 23.934, 8766.0000, date(1543, 1, 1), date(1969, 7, 20)),
|
|
||||||
('Mars' , 24.600, 16487.2800, date(1610, 1, 1), date(1976, 7, 20)),
|
|
||||||
('Jupiter' , 9.900, 103982.1600, date(1610, 1, 7), None),
|
|
||||||
('Saturn' , 10.700, 258221.2800, date(1610, 7, 25), None),
|
|
||||||
('Uranus' , 17.200, 736524.0000, date(1781, 3, 13), None),
|
|
||||||
('Neptune' , 16.100, 1444368.0000, date(1846, 9, 23), None),
|
|
||||||
]
|
|
||||||
|
|
||||||
# (name, rotation_hours, orbital_hours, is_tidally_locked, discovery_date, contact_date | None)
|
|
||||||
MOON_ROWS: list[tuple[str, float, float, bool, date, date | None]] = [
|
|
||||||
('Luna' , 655.720, 655.720, True , date(1609, 11, 1), date(1969, 7, 20)),
|
|
||||||
('Phobos' , 7.653, 7.653, True , date(1877, 8, 18), None),
|
|
||||||
('Deimos' , 30.312, 30.312, True , date(1877, 8, 12), None),
|
|
||||||
('Io' , 42.456, 42.456, True , date(1610, 1, 8), None),
|
|
||||||
('Europa' , 85.228, 85.228, True , date(1610, 1, 8), None),
|
|
||||||
('Ganymede' , 171.709, 171.709, True , date(1610, 1, 7), None),
|
|
||||||
('Callisto' , 400.535, 400.535, True , date(1610, 1, 7), None),
|
|
||||||
('Titan' , 382.690, 382.690, True , date(1655, 3, 25), date(2005, 1, 14)),
|
|
||||||
('Enceladus' , 32.923, 32.923, True , date(1789, 8, 28), None),
|
|
||||||
('Miranda' , 33.923, 33.923, True , date(1948, 2, 16), None),
|
|
||||||
('Ariel' , 60.489, 60.489, True , date(1851, 10, 24), None),
|
|
||||||
('Umbriel' , 99.460, 99.460, True , date(1851, 10, 24), None),
|
|
||||||
('Titania' , 208.940, 208.940, True , date(1787, 1, 11), None),
|
|
||||||
('Oberon' , 323.117, 323.117, True , date(1787, 1, 11), None),
|
|
||||||
('Triton' , 141.045, 141.045, True , date(1846, 10, 10), None),
|
|
||||||
]
|
|
||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from planetarytime._data import PLANET_ROWS
|
|
||||||
from planetarytime.moon import (
|
from planetarytime.moon import (
|
||||||
ARIEL,
|
ARIEL,
|
||||||
CALLISTO,
|
CALLISTO,
|
||||||
@@ -11,7 +10,6 @@ from planetarytime.moon import (
|
|||||||
EUROPA,
|
EUROPA,
|
||||||
GANYMEDE,
|
GANYMEDE,
|
||||||
IO,
|
IO,
|
||||||
LUNA,
|
|
||||||
MIRANDA,
|
MIRANDA,
|
||||||
Moon,
|
Moon,
|
||||||
OBERON,
|
OBERON,
|
||||||
@@ -28,7 +26,6 @@ class Body(Enum):
|
|||||||
|
|
||||||
MERCURY = "Mercury"
|
MERCURY = "Mercury"
|
||||||
VENUS = "Venus"
|
VENUS = "Venus"
|
||||||
EARTH = "Earth"
|
|
||||||
MARS = "Mars"
|
MARS = "Mars"
|
||||||
JUPITER = "Jupiter"
|
JUPITER = "Jupiter"
|
||||||
SATURN = "Saturn"
|
SATURN = "Saturn"
|
||||||
@@ -60,23 +57,35 @@ class Body(Enum):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> Moon:
|
def __getitem__(self, index: int) -> Moon:
|
||||||
"""Return the moon at the given 1-based index for this body."""
|
"""Return the moon at the given index for this body."""
|
||||||
return _MOONS[self][index - 1]
|
return _MOONS[self][index]
|
||||||
|
|
||||||
|
|
||||||
_ROTATION_HOURS: dict[Body, float] = {
|
_ROTATION_HOURS: dict[Body, float] = {
|
||||||
Body(row[0]): row[1] for row in PLANET_ROWS
|
Body.MERCURY: 1407.6,
|
||||||
|
Body.VENUS: 5832.5,
|
||||||
|
Body.MARS: 24.6,
|
||||||
|
Body.JUPITER: 9.9,
|
||||||
|
Body.SATURN: 10.7,
|
||||||
|
Body.URANUS: 17.2,
|
||||||
|
Body.NEPTUNE: 16.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Orbital periods in Earth hours
|
||||||
_ORBITAL_HOURS: dict[Body, float] = {
|
_ORBITAL_HOURS: dict[Body, float] = {
|
||||||
Body(row[0]): row[2] for row in PLANET_ROWS
|
Body.MERCURY: 87.97 * 24,
|
||||||
|
Body.VENUS: 224.70 * 24,
|
||||||
|
Body.MARS: 686.97 * 24,
|
||||||
|
Body.JUPITER: 4332.59 * 24,
|
||||||
|
Body.SATURN: 10759.22 * 24,
|
||||||
|
Body.URANUS: 30688.50 * 24,
|
||||||
|
Body.NEPTUNE: 60182.00 * 24,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Moons per body, ordered by orbital distance from the planet
|
# Moons per body, ordered by orbital distance from the planet
|
||||||
_MOONS: dict[Body, list[Moon]] = {
|
_MOONS: dict[Body, list[Moon]] = {
|
||||||
Body.MERCURY: [],
|
Body.MERCURY: [],
|
||||||
Body.VENUS: [],
|
Body.VENUS: [],
|
||||||
Body.EARTH: [LUNA],
|
|
||||||
Body.MARS: [PHOBOS, DEIMOS],
|
Body.MARS: [PHOBOS, DEIMOS],
|
||||||
Body.JUPITER: [IO, EUROPA, GANYMEDE, CALLISTO],
|
Body.JUPITER: [IO, EUROPA, GANYMEDE, CALLISTO],
|
||||||
Body.SATURN: [TITAN, ENCELADUS],
|
Body.SATURN: [TITAN, ENCELADUS],
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from planetarytime._data import PLANET_ROWS
|
|
||||||
from planetarytime.body import Body
|
from planetarytime.body import Body
|
||||||
from planetarytime.exceptions import EpochUnavailableError
|
from planetarytime.exceptions import EpochUnavailableError
|
||||||
from planetarytime.moon import Moon
|
from planetarytime.moon import Moon
|
||||||
@@ -16,22 +15,27 @@ class EpochType(Enum):
|
|||||||
CONTACT = "contact"
|
CONTACT = "contact"
|
||||||
|
|
||||||
|
|
||||||
def _dt(d: object) -> datetime:
|
# Discovery dates for Solar System bodies (UTC midnight).
|
||||||
from datetime import date as _date
|
|
||||||
assert isinstance(d, _date)
|
|
||||||
return datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def _dt_opt(d: object) -> datetime | None:
|
|
||||||
return None if d is None else _dt(d)
|
|
||||||
|
|
||||||
|
|
||||||
_DISCOVERY_DATES: dict[Body, datetime] = {
|
_DISCOVERY_DATES: dict[Body, datetime] = {
|
||||||
Body(row[0]): _dt(row[3]) for row in PLANET_ROWS
|
Body.MERCURY: datetime(1631, 11, 7, tzinfo=timezone.utc), # first recorded transit (Gassendi)
|
||||||
|
Body.VENUS: datetime(1610, 1, 1, tzinfo=timezone.utc), # telescopic observation (Galileo)
|
||||||
|
Body.MARS: datetime(1610, 1, 1, tzinfo=timezone.utc), # telescopic observation (Galileo)
|
||||||
|
Body.JUPITER: datetime(1610, 1, 7, tzinfo=timezone.utc), # moons discovered (Galileo)
|
||||||
|
Body.SATURN: datetime(1610, 7, 25, tzinfo=timezone.utc), # rings observed (Galileo)
|
||||||
|
Body.URANUS: datetime(1781, 3, 13, tzinfo=timezone.utc), # Herschel
|
||||||
|
Body.NEPTUNE: datetime(1846, 9, 23, tzinfo=timezone.utc), # Le Verrier / Galle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# First contact dates — automated probe landing or crewed landing.
|
||||||
|
# None means no contact has occurred yet.
|
||||||
_CONTACT_DATES: dict[Body, datetime | None] = {
|
_CONTACT_DATES: dict[Body, datetime | None] = {
|
||||||
Body(row[0]): _dt_opt(row[4]) for row in PLANET_ROWS
|
Body.MERCURY: datetime(2011, 3, 18, tzinfo=timezone.utc), # MESSENGER orbit insertion (closest approach)
|
||||||
|
Body.VENUS: datetime(1970, 12, 15, tzinfo=timezone.utc), # Venera 7 — first soft landing
|
||||||
|
Body.MARS: datetime(1976, 7, 20, tzinfo=timezone.utc), # Viking 1 — first soft landing
|
||||||
|
Body.JUPITER: None,
|
||||||
|
Body.SATURN: None,
|
||||||
|
Body.URANUS: None,
|
||||||
|
Body.NEPTUNE: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from planetarytime._data import MOON_ROWS
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Moon:
|
class Moon:
|
||||||
@@ -37,37 +35,125 @@ class Moon:
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
def _make_moon(name: str, rot: float, orb: float, locked: bool,
|
# ── Mars ──────────────────────────────────────────────────────────────────────
|
||||||
disc: object, contact: object) -> Moon:
|
|
||||||
from datetime import date as _date
|
|
||||||
def _dt(d: object) -> datetime | None:
|
|
||||||
if d is None:
|
|
||||||
return None
|
|
||||||
assert isinstance(d, _date)
|
|
||||||
return datetime(d.year, d.month, d.day, tzinfo=timezone.utc)
|
|
||||||
disc_dt = _dt(disc)
|
|
||||||
assert disc_dt is not None
|
|
||||||
return Moon(name=name, rotation_hours=rot, orbital_hours=orb,
|
|
||||||
is_tidally_locked=locked, discovery_date=disc_dt,
|
|
||||||
contact_date=_dt(contact))
|
|
||||||
|
|
||||||
|
PHOBOS = Moon(
|
||||||
|
name="Phobos",
|
||||||
|
rotation_hours=7.653,
|
||||||
|
orbital_hours=7.653,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1877, 8, 18, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
_MOONS_BY_NAME: dict[str, Moon] = {
|
DEIMOS = Moon(
|
||||||
row[0]: _make_moon(*row) for row in MOON_ROWS
|
name="Deimos",
|
||||||
}
|
rotation_hours=30.312,
|
||||||
|
orbital_hours=30.312,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1877, 8, 12, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
LUNA = _MOONS_BY_NAME["Luna"]
|
# ── Jupiter (Galilean moons) ──────────────────────────────────────────────────
|
||||||
PHOBOS = _MOONS_BY_NAME["Phobos"]
|
|
||||||
DEIMOS = _MOONS_BY_NAME["Deimos"]
|
IO = Moon(
|
||||||
IO = _MOONS_BY_NAME["Io"]
|
name="Io",
|
||||||
EUROPA = _MOONS_BY_NAME["Europa"]
|
rotation_hours=42.456,
|
||||||
GANYMEDE = _MOONS_BY_NAME["Ganymede"]
|
orbital_hours=42.456,
|
||||||
CALLISTO = _MOONS_BY_NAME["Callisto"]
|
is_tidally_locked=True,
|
||||||
TITAN = _MOONS_BY_NAME["Titan"]
|
discovery_date=datetime(1610, 1, 8, tzinfo=timezone.utc),
|
||||||
ENCELADUS = _MOONS_BY_NAME["Enceladus"]
|
)
|
||||||
MIRANDA = _MOONS_BY_NAME["Miranda"]
|
|
||||||
ARIEL = _MOONS_BY_NAME["Ariel"]
|
EUROPA = Moon(
|
||||||
UMBRIEL = _MOONS_BY_NAME["Umbriel"]
|
name="Europa",
|
||||||
TITANIA = _MOONS_BY_NAME["Titania"]
|
rotation_hours=85.228,
|
||||||
OBERON = _MOONS_BY_NAME["Oberon"]
|
orbital_hours=85.228,
|
||||||
TRITON = _MOONS_BY_NAME["Triton"]
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1610, 1, 8, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
GANYMEDE = Moon(
|
||||||
|
name="Ganymede",
|
||||||
|
rotation_hours=171.709,
|
||||||
|
orbital_hours=171.709,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1610, 1, 7, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
CALLISTO = Moon(
|
||||||
|
name="Callisto",
|
||||||
|
rotation_hours=400.535,
|
||||||
|
orbital_hours=400.535,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1610, 1, 7, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Saturn ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TITAN = Moon(
|
||||||
|
name="Titan",
|
||||||
|
rotation_hours=382.690,
|
||||||
|
orbital_hours=382.690,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1655, 3, 25, tzinfo=timezone.utc),
|
||||||
|
contact_date=datetime(2005, 1, 14, tzinfo=timezone.utc), # Huygens probe
|
||||||
|
)
|
||||||
|
|
||||||
|
ENCELADUS = Moon(
|
||||||
|
name="Enceladus",
|
||||||
|
rotation_hours=32.923,
|
||||||
|
orbital_hours=32.923,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1789, 8, 28, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Uranus ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
MIRANDA = Moon(
|
||||||
|
name="Miranda",
|
||||||
|
rotation_hours=33.923,
|
||||||
|
orbital_hours=33.923,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1948, 2, 16, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
ARIEL = Moon(
|
||||||
|
name="Ariel",
|
||||||
|
rotation_hours=60.489,
|
||||||
|
orbital_hours=60.489,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1851, 10, 24, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
UMBRIEL = Moon(
|
||||||
|
name="Umbriel",
|
||||||
|
rotation_hours=99.460,
|
||||||
|
orbital_hours=99.460,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1851, 10, 24, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
TITANIA = Moon(
|
||||||
|
name="Titania",
|
||||||
|
rotation_hours=208.940,
|
||||||
|
orbital_hours=208.940,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1787, 1, 11, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
OBERON = Moon(
|
||||||
|
name="Oberon",
|
||||||
|
rotation_hours=323.117,
|
||||||
|
orbital_hours=323.117,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1787, 1, 11, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Neptune ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TRITON = Moon(
|
||||||
|
name="Triton",
|
||||||
|
rotation_hours=141.045,
|
||||||
|
orbital_hours=141.045,
|
||||||
|
is_tidally_locked=True,
|
||||||
|
discovery_date=datetime(1846, 10, 10, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
|||||||
@@ -80,91 +80,3 @@ def test_repr_contains_year_and_sol() -> None:
|
|||||||
assert "PlanetaryTime(" in r
|
assert "PlanetaryTime(" in r
|
||||||
assert "year=" in r
|
assert "year=" in r
|
||||||
assert "sol=" in r
|
assert "sol=" in r
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Conversion accuracy
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_conversion_accuracy_sol_hour_minute() -> None:
|
|
||||||
"""26h 30m after epoch on Mars: sol 1, hour 1, minute 30."""
|
|
||||||
epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY)
|
|
||||||
# Mars sol = 25 h; 26h 30m = 1 sol + 1h 30m
|
|
||||||
pt = PlanetaryTime.from_earth(epoch_dt + timedelta(hours=26, minutes=30), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
assert pt.year == 0
|
|
||||||
assert pt.sol == 1
|
|
||||||
assert pt.hour == 1
|
|
||||||
assert pt.minute == 30
|
|
||||||
assert pt.second == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_conversion_accuracy_seconds() -> None:
|
|
||||||
"""45 seconds after epoch: only second counter advances."""
|
|
||||||
epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt = PlanetaryTime.from_earth(epoch_dt + timedelta(seconds=45), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
assert pt.year == 0
|
|
||||||
assert pt.sol == 0
|
|
||||||
assert pt.hour == 0
|
|
||||||
assert pt.minute == 0
|
|
||||||
assert pt.second == 45
|
|
||||||
|
|
||||||
|
|
||||||
def test_conversion_accuracy_year_boundary() -> None:
|
|
||||||
"""Exactly one Mars year after epoch lands on year 1, sol 0, 00:00:00."""
|
|
||||||
epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY)
|
|
||||||
one_year_seconds = Body.MARS.sols_per_year * Body.MARS.hours_per_sol * 3600
|
|
||||||
pt = PlanetaryTime.from_earth(epoch_dt + timedelta(seconds=one_year_seconds), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
assert pt.year == 1
|
|
||||||
assert pt.sol == 0
|
|
||||||
assert pt.hour == 0
|
|
||||||
assert pt.minute == 0
|
|
||||||
assert pt.second == 0
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Epoch switching
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_epoch_switching_discovery_vs_contact_differ() -> None:
|
|
||||||
"""Discovery (1610) and contact (1976) epochs give significantly different years."""
|
|
||||||
dt = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
||||||
pt_disc = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt_cont = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.CONTACT)
|
|
||||||
assert pt_disc.year > pt_cont.year
|
|
||||||
|
|
||||||
|
|
||||||
def test_epoch_switching_preserves_body_and_epoch_type() -> None:
|
|
||||||
"""Switching epoch type is reflected in the epoch_type property; body stays the same."""
|
|
||||||
dt = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
||||||
pt_disc = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt_cont = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.CONTACT)
|
|
||||||
assert pt_disc.body is Body.MARS
|
|
||||||
assert pt_cont.body is Body.MARS
|
|
||||||
assert pt_disc.epoch_type is EpochType.DISCOVERY
|
|
||||||
assert pt_cont.epoch_type is EpochType.CONTACT
|
|
||||||
|
|
||||||
|
|
||||||
def test_epoch_switching_contact_sol_count_is_smaller() -> None:
|
|
||||||
"""Contact epoch is later, so sol count from contact is smaller than from discovery."""
|
|
||||||
dt = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
||||||
pt_disc = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt_cont = PlanetaryTime.from_earth(dt, Body.MARS, EpochType.CONTACT)
|
|
||||||
total_sols_disc = pt_disc.year * Body.MARS.sols_per_year + pt_disc.sol
|
|
||||||
total_sols_cont = pt_cont.year * Body.MARS.sols_per_year + pt_cont.sol
|
|
||||||
assert total_sols_disc > total_sols_cont
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# time and date properties
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
def test_time_property_format() -> None:
|
|
||||||
epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt = PlanetaryTime.from_earth(epoch_dt + timedelta(hours=3, minutes=7, seconds=9), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
assert pt.time == "03:07:09"
|
|
||||||
|
|
||||||
|
|
||||||
def test_date_property_format() -> None:
|
|
||||||
epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY)
|
|
||||||
pt = PlanetaryTime.from_earth(epoch_dt + timedelta(hours=Body.MARS.hours_per_sol + 1), Body.MARS, EpochType.DISCOVERY)
|
|
||||||
assert pt.date == "Year 0, Sol 1"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user