orbital-pv 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. orbital_pv-0.3.0/LICENSE +21 -0
  2. orbital_pv-0.3.0/PKG-INFO +209 -0
  3. orbital_pv-0.3.0/README.md +151 -0
  4. orbital_pv-0.3.0/apps/space-pv-api/src/space_pv_api/__init__.py +1 -0
  5. orbital_pv-0.3.0/apps/space-pv-api/src/space_pv_api/config.py +13 -0
  6. orbital_pv-0.3.0/apps/space-pv-api/src/space_pv_api/main.py +202 -0
  7. orbital_pv-0.3.0/apps/space-pv-api/src/space_pv_api/schemas.py +61 -0
  8. orbital_pv-0.3.0/apps/space-pv-cli/src/space_pv_cli/__init__.py +0 -0
  9. orbital_pv-0.3.0/apps/space-pv-cli/src/space_pv_cli/main.py +61 -0
  10. orbital_pv-0.3.0/orbital_pv.egg-info/PKG-INFO +209 -0
  11. orbital_pv-0.3.0/orbital_pv.egg-info/SOURCES.txt +69 -0
  12. orbital_pv-0.3.0/orbital_pv.egg-info/dependency_links.txt +1 -0
  13. orbital_pv-0.3.0/orbital_pv.egg-info/entry_points.txt +2 -0
  14. orbital_pv-0.3.0/orbital_pv.egg-info/requires.txt +44 -0
  15. orbital_pv-0.3.0/orbital_pv.egg-info/top_level.txt +11 -0
  16. orbital_pv-0.3.0/packages/space-pv-analytics/src/space_pv_analytics/__init__.py +17 -0
  17. orbital_pv-0.3.0/packages/space-pv-analytics/src/space_pv_analytics/performance.py +149 -0
  18. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/__init__.py +25 -0
  19. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/birds.py +27 -0
  20. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/cdaweb.py +34 -0
  21. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/celestrak.py +31 -0
  22. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/http_base.py +47 -0
  23. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/naif.py +23 -0
  24. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/satnogs.py +43 -0
  25. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/space_track.py +37 -0
  26. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/sscweb.py +27 -0
  27. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/swpc.py +28 -0
  28. orbital_pv-0.3.0/packages/space-pv-connectors/src/space_pv_connectors/tabular.py +124 -0
  29. orbital_pv-0.3.0/packages/space-pv-core/src/space_pv_core/__init__.py +19 -0
  30. orbital_pv-0.3.0/packages/space-pv-core/src/space_pv_core/constants.py +7 -0
  31. orbital_pv-0.3.0/packages/space-pv-core/src/space_pv_core/models.py +71 -0
  32. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/__init__.py +18 -0
  33. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/astropy_engine.py +17 -0
  34. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/attitude.py +39 -0
  35. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/geometry.py +100 -0
  36. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/skyfield_engine.py +39 -0
  37. orbital_pv-0.3.0/packages/space-pv-orbit/src/space_pv_orbit/spice_engine.py +35 -0
  38. orbital_pv-0.3.0/packages/space-pv-orchestration/src/space_pv_orchestration/__init__.py +5 -0
  39. orbital_pv-0.3.0/packages/space-pv-orchestration/src/space_pv_orchestration/celery_app.py +28 -0
  40. orbital_pv-0.3.0/packages/space-pv-orchestration/src/space_pv_orchestration/config.py +7 -0
  41. orbital_pv-0.3.0/packages/space-pv-orchestration/src/space_pv_orchestration/tasks.py +35 -0
  42. orbital_pv-0.3.0/packages/space-pv-pvmodel/src/space_pv_pvmodel/__init__.py +10 -0
  43. orbital_pv-0.3.0/packages/space-pv-pvmodel/src/space_pv_pvmodel/desoto.py +46 -0
  44. orbital_pv-0.3.0/packages/space-pv-pvmodel/src/space_pv_pvmodel/model.py +66 -0
  45. orbital_pv-0.3.0/packages/space-pv-space-env/src/space_pv_space_env/__init__.py +3 -0
  46. orbital_pv-0.3.0/packages/space-pv-space-env/src/space_pv_space_env/solar.py +18 -0
  47. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/__init__.py +41 -0
  48. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/config.py +12 -0
  49. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/duckdb_store.py +121 -0
  50. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/jobs.py +59 -0
  51. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/map_store.py +201 -0
  52. orbital_pv-0.3.0/packages/space-pv-storage/src/space_pv_storage/postgres.py +53 -0
  53. orbital_pv-0.3.0/pyproject.toml +158 -0
  54. orbital_pv-0.3.0/setup.cfg +4 -0
  55. orbital_pv-0.3.0/src/orbitalpv/__init__.py +77 -0
  56. orbital_pv-0.3.0/src/orbitalpv/analytics.py +19 -0
  57. orbital_pv-0.3.0/src/orbitalpv/data.py +5 -0
  58. orbital_pv-0.3.0/src/orbitalpv/environment.py +5 -0
  59. orbital_pv-0.3.0/src/orbitalpv/orbit.py +145 -0
  60. orbital_pv-0.3.0/src/orbitalpv/pv.py +17 -0
  61. orbital_pv-0.3.0/tests/test_attitude.py +12 -0
  62. orbital_pv-0.3.0/tests/test_connector_validation.py +17 -0
  63. orbital_pv-0.3.0/tests/test_models.py +8 -0
  64. orbital_pv-0.3.0/tests/test_orbit_geometry.py +28 -0
  65. orbital_pv-0.3.0/tests/test_performance.py +20 -0
  66. orbital_pv-0.3.0/tests/test_platform_structure.py +13 -0
  67. orbital_pv-0.3.0/tests/test_public_api.py +32 -0
  68. orbital_pv-0.3.0/tests/test_pvmodel_math.py +8 -0
  69. orbital_pv-0.3.0/tests/test_satnogs_connector.py +5 -0
  70. orbital_pv-0.3.0/tests/test_space_env.py +14 -0
  71. orbital_pv-0.3.0/tests/test_space_gis_structure.py +12 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbital-pv
3
+ Version: 0.3.0
4
+ Summary: Open-source spacecraft photovoltaic performance analytics
5
+ Author: OrbitalPV contributors
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://julianascencio.github.io/orbital-pv/
8
+ Project-URL: Source, https://github.com/julianascencio/orbital-pv
9
+ Project-URL: Issues, https://github.com/julianascencio/orbital-pv/issues
10
+ Keywords: photovoltaics,space,satellite,orbit,solar,telemetry
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.13
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx<0.29,>=0.28
20
+ Requires-Dist: numpy>=2.3
21
+ Requires-Dist: pandas>=2.2
22
+ Requires-Dist: pvlib<0.16,>=0.15.2
23
+ Provides-Extra: orbit
24
+ Requires-Dist: astropy<9,>=8.0; extra == "orbit"
25
+ Requires-Dist: skyfield<2,>=1.55; extra == "orbit"
26
+ Requires-Dist: sgp4<3,>=2.27; extra == "orbit"
27
+ Requires-Dist: spiceypy<9,>=8.2; extra == "orbit"
28
+ Provides-Extra: excel
29
+ Requires-Dist: openpyxl>=3.1; extra == "excel"
30
+ Provides-Extra: radiation
31
+ Requires-Dist: spacepy>=0.7; extra == "radiation"
32
+ Provides-Extra: examples
33
+ Requires-Dist: matplotlib>=3.10; extra == "examples"
34
+ Provides-Extra: app
35
+ Requires-Dist: celery[redis]<6,>=5.6; extra == "app"
36
+ Requires-Dist: duckdb<2,>=1.5; extra == "app"
37
+ Requires-Dist: fastapi<1,>=0.141; extra == "app"
38
+ Requires-Dist: psycopg[binary]<4,>=3.2; extra == "app"
39
+ Requires-Dist: pydantic-settings<3,>=2.7; extra == "app"
40
+ Requires-Dist: sqlalchemy<3,>=2.0; extra == "app"
41
+ Requires-Dist: uvicorn[standard]<1,>=0.34; extra == "app"
42
+ Provides-Extra: full
43
+ Requires-Dist: astropy<9,>=8.0; extra == "full"
44
+ Requires-Dist: celery[redis]<6,>=5.6; extra == "full"
45
+ Requires-Dist: duckdb<2,>=1.5; extra == "full"
46
+ Requires-Dist: fastapi<1,>=0.141; extra == "full"
47
+ Requires-Dist: matplotlib>=3.10; extra == "full"
48
+ Requires-Dist: openpyxl>=3.1; extra == "full"
49
+ Requires-Dist: psycopg[binary]<4,>=3.2; extra == "full"
50
+ Requires-Dist: pydantic-settings<3,>=2.7; extra == "full"
51
+ Requires-Dist: sgp4<3,>=2.27; extra == "full"
52
+ Requires-Dist: skyfield<2,>=1.55; extra == "full"
53
+ Requires-Dist: spacepy>=0.7; extra == "full"
54
+ Requires-Dist: spiceypy<9,>=8.2; extra == "full"
55
+ Requires-Dist: sqlalchemy<3,>=2.0; extra == "full"
56
+ Requires-Dist: uvicorn[standard]<1,>=0.34; extra == "full"
57
+ Dynamic: license-file
58
+
59
+ # OrbitalPV
60
+
61
+ Open-source full-stack lab for **spacecraft photovoltaic performance analytics**: PV physics, orbit/Sun geometry, real spacecraft data connectors, degradation/anomaly analytics, reproducible research labs, and a web application platform.
62
+
63
+ The structure follows a modular separation-of-concerns approach: domain models, physics engines, connectors and analytics are independent workspace packages with consistent quality tooling.
64
+
65
+ ## Install from PyPI
66
+
67
+ ```bash
68
+ python -m pip install orbital-pv
69
+ ```
70
+
71
+ Use the public API as:
72
+
73
+ ```python
74
+ import orbitalpv as opv
75
+
76
+ positions_m = opv.circular_orbit_positions_m(
77
+ altitude_m=550_000.0,
78
+ inclination_deg=51.6,
79
+ )
80
+
81
+ g_am0 = opv.am0_irradiance(opv.ASTRONOMICAL_UNIT_M)
82
+ ```
83
+
84
+ Optional extras:
85
+
86
+ ```bash
87
+ python -m pip install "orbital-pv[examples]"
88
+ python -m pip install "orbital-pv[orbit]"
89
+ python -m pip install "orbital-pv[full]"
90
+ ```
91
+
92
+ ## Foundation stack
93
+
94
+ - **pvlib** — tested PV equations and I-V/single-diode reference models behind a space-specific adapter.
95
+ - **Skyfield + python-sgp4** — Earth-orbit catalog propagation from CCSDS OMM/TLE.
96
+ - **Astropy** — time, units, frames and baseline solar-system coordinates.
97
+ - **SpiceyPy / NASA SPICE** — mission-grade ephemeris, attitude, clocks and frame transforms.
98
+ - **SpacePy / later radiation engines** — radiation/environment workstream.
99
+ - **HTTP connectors** — CelesTrak, Space-Track, NASA SSCWeb, NAIF, CDAWeb/HAPI, NOAA SWPC, SatNOGS DB and BIRDS EPS.
100
+
101
+ `poliastro` is intentionally not a core dependency because its upstream repository is archived.
102
+
103
+ ## Repository map
104
+
105
+ ```text
106
+ orbital-pv/
107
+ ├── src/orbitalpv/ # public PyPI API
108
+ ├── apps/
109
+ │ ├── space-pv-cli/
110
+ │ ├── space-pv-api/
111
+ │ └── space-pv-web/
112
+ ├── packages/
113
+ │ ├── space-pv-core/
114
+ │ ├── space-pv-orbit/
115
+ │ ├── space-pv-pvmodel/
116
+ │ ├── space-pv-space-env/
117
+ │ ├── space-pv-connectors/
118
+ │ ├── space-pv-analytics/
119
+ │ ├── space-pv-storage/
120
+ │ └── space-pv-orchestration/
121
+ ├── examples/
122
+ ├── labs/
123
+ ├── data/{raw,bronze,silver,gold}/
124
+ ├── docs/
125
+ └── tests/
126
+ ```
127
+
128
+ The `space_pv_*` packages remain available as lower-level implementation modules for contributors and advanced use, while `orbitalpv` is the stable user-facing import.
129
+
130
+ ## Documentation
131
+
132
+ **https://julianascencio.github.io/orbital-pv/**
133
+
134
+ The documentation uses Sphinx, PyData Sphinx Theme, and Sphinx-Gallery. Gallery examples execute during the documentation build and use the same `orbitalpv` API installed from PyPI.
135
+
136
+ Build locally:
137
+
138
+ ```bash
139
+ make docs
140
+ ```
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ uv sync --all-packages --group dev --group lab --group orbit --group pv --group space --group app
146
+ uv run pytest
147
+ uv run ruff check .
148
+ uv run ty check
149
+ ```
150
+
151
+ ## Science pipeline
152
+
153
+ ```text
154
+ real orbit/telemetry/environment data
155
+ |
156
+ v
157
+ canonical time + frames
158
+ |
159
+ +---------+----------+
160
+ | |
161
+ orbit + Sun PV telemetry
162
+ | |
163
+ v |
164
+ eclipse + AOI + AM0 |
165
+ | |
166
+ +----------+---------+
167
+ v
168
+ expected PV performance
169
+ |
170
+ v
171
+ residual / degradation
172
+ |
173
+ v
174
+ anomaly + EOL analytics
175
+ ```
176
+
177
+ See `docs/foundation_stack.md`, `docs/connectors/catalog.md`, `docs/architecture.md` and `docs/state_of_art.md`.
178
+
179
+ ## Application platform
180
+
181
+ The web/application layer is intentionally separated from the scientific packages:
182
+
183
+ - **React + TypeScript + Vite** — frontend application.
184
+ - **FastAPI** — API and job submission boundary.
185
+ - **PostgreSQL** — durable multi-user/transactional application state and job metadata.
186
+ - **DuckDB** — local analytics, parquet-backed research datasets and fast time-series exploration.
187
+ - **Celery + Redis** — long-running ingestion, orbit, PV and analytics jobs.
188
+ - **Flower** — local Celery worker/task observability.
189
+
190
+ Start the development stack with:
191
+
192
+ ```bash
193
+ docker compose up --build
194
+ ```
195
+
196
+ Then open React on `http://localhost:5173`, FastAPI docs on `http://localhost:8000/docs`, and Flower on `http://localhost:5555`.
197
+
198
+ ## Space GIS and dynamic visualization
199
+
200
+ The platform separates scientific orbital reference-frame state from Earth-fixed GIS render products. PostGIS serves hot satellite positions, ground tracks, stations and footprints; DuckDB/Parquet retain analytical history and chart rollups; CesiumJS renders time-dynamic 3D orbital scenes. See `docs/space_gis_and_visualization.md`.
201
+
202
+ ## Literature library
203
+
204
+ A curated, source-tagged literature library is maintained in [`docs/literature/`](docs/literature/README.md). It covers the 2026 state of the art in space-PV cells and arrays, mission-aware performance modelling, radiation/degradation, digital twins, telemetry/AI, and open flight datasets/APIs.
205
+
206
+ ## Roadmap evidence
207
+
208
+ - `docs/literature/09_ieso_2026_workshop_roadmap.md` — 2026 IESO workshop targets translated into repository features and R&D priorities.
209
+ - `docs/product_feature_roadmap.md` — prioritized product epics derived from the literature and 2026 IESO workshop.
@@ -0,0 +1,151 @@
1
+ # OrbitalPV
2
+
3
+ Open-source full-stack lab for **spacecraft photovoltaic performance analytics**: PV physics, orbit/Sun geometry, real spacecraft data connectors, degradation/anomaly analytics, reproducible research labs, and a web application platform.
4
+
5
+ The structure follows a modular separation-of-concerns approach: domain models, physics engines, connectors and analytics are independent workspace packages with consistent quality tooling.
6
+
7
+ ## Install from PyPI
8
+
9
+ ```bash
10
+ python -m pip install orbital-pv
11
+ ```
12
+
13
+ Use the public API as:
14
+
15
+ ```python
16
+ import orbitalpv as opv
17
+
18
+ positions_m = opv.circular_orbit_positions_m(
19
+ altitude_m=550_000.0,
20
+ inclination_deg=51.6,
21
+ )
22
+
23
+ g_am0 = opv.am0_irradiance(opv.ASTRONOMICAL_UNIT_M)
24
+ ```
25
+
26
+ Optional extras:
27
+
28
+ ```bash
29
+ python -m pip install "orbital-pv[examples]"
30
+ python -m pip install "orbital-pv[orbit]"
31
+ python -m pip install "orbital-pv[full]"
32
+ ```
33
+
34
+ ## Foundation stack
35
+
36
+ - **pvlib** — tested PV equations and I-V/single-diode reference models behind a space-specific adapter.
37
+ - **Skyfield + python-sgp4** — Earth-orbit catalog propagation from CCSDS OMM/TLE.
38
+ - **Astropy** — time, units, frames and baseline solar-system coordinates.
39
+ - **SpiceyPy / NASA SPICE** — mission-grade ephemeris, attitude, clocks and frame transforms.
40
+ - **SpacePy / later radiation engines** — radiation/environment workstream.
41
+ - **HTTP connectors** — CelesTrak, Space-Track, NASA SSCWeb, NAIF, CDAWeb/HAPI, NOAA SWPC, SatNOGS DB and BIRDS EPS.
42
+
43
+ `poliastro` is intentionally not a core dependency because its upstream repository is archived.
44
+
45
+ ## Repository map
46
+
47
+ ```text
48
+ orbital-pv/
49
+ ├── src/orbitalpv/ # public PyPI API
50
+ ├── apps/
51
+ │ ├── space-pv-cli/
52
+ │ ├── space-pv-api/
53
+ │ └── space-pv-web/
54
+ ├── packages/
55
+ │ ├── space-pv-core/
56
+ │ ├── space-pv-orbit/
57
+ │ ├── space-pv-pvmodel/
58
+ │ ├── space-pv-space-env/
59
+ │ ├── space-pv-connectors/
60
+ │ ├── space-pv-analytics/
61
+ │ ├── space-pv-storage/
62
+ │ └── space-pv-orchestration/
63
+ ├── examples/
64
+ ├── labs/
65
+ ├── data/{raw,bronze,silver,gold}/
66
+ ├── docs/
67
+ └── tests/
68
+ ```
69
+
70
+ The `space_pv_*` packages remain available as lower-level implementation modules for contributors and advanced use, while `orbitalpv` is the stable user-facing import.
71
+
72
+ ## Documentation
73
+
74
+ **https://julianascencio.github.io/orbital-pv/**
75
+
76
+ The documentation uses Sphinx, PyData Sphinx Theme, and Sphinx-Gallery. Gallery examples execute during the documentation build and use the same `orbitalpv` API installed from PyPI.
77
+
78
+ Build locally:
79
+
80
+ ```bash
81
+ make docs
82
+ ```
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ uv sync --all-packages --group dev --group lab --group orbit --group pv --group space --group app
88
+ uv run pytest
89
+ uv run ruff check .
90
+ uv run ty check
91
+ ```
92
+
93
+ ## Science pipeline
94
+
95
+ ```text
96
+ real orbit/telemetry/environment data
97
+ |
98
+ v
99
+ canonical time + frames
100
+ |
101
+ +---------+----------+
102
+ | |
103
+ orbit + Sun PV telemetry
104
+ | |
105
+ v |
106
+ eclipse + AOI + AM0 |
107
+ | |
108
+ +----------+---------+
109
+ v
110
+ expected PV performance
111
+ |
112
+ v
113
+ residual / degradation
114
+ |
115
+ v
116
+ anomaly + EOL analytics
117
+ ```
118
+
119
+ See `docs/foundation_stack.md`, `docs/connectors/catalog.md`, `docs/architecture.md` and `docs/state_of_art.md`.
120
+
121
+ ## Application platform
122
+
123
+ The web/application layer is intentionally separated from the scientific packages:
124
+
125
+ - **React + TypeScript + Vite** — frontend application.
126
+ - **FastAPI** — API and job submission boundary.
127
+ - **PostgreSQL** — durable multi-user/transactional application state and job metadata.
128
+ - **DuckDB** — local analytics, parquet-backed research datasets and fast time-series exploration.
129
+ - **Celery + Redis** — long-running ingestion, orbit, PV and analytics jobs.
130
+ - **Flower** — local Celery worker/task observability.
131
+
132
+ Start the development stack with:
133
+
134
+ ```bash
135
+ docker compose up --build
136
+ ```
137
+
138
+ Then open React on `http://localhost:5173`, FastAPI docs on `http://localhost:8000/docs`, and Flower on `http://localhost:5555`.
139
+
140
+ ## Space GIS and dynamic visualization
141
+
142
+ The platform separates scientific orbital reference-frame state from Earth-fixed GIS render products. PostGIS serves hot satellite positions, ground tracks, stations and footprints; DuckDB/Parquet retain analytical history and chart rollups; CesiumJS renders time-dynamic 3D orbital scenes. See `docs/space_gis_and_visualization.md`.
143
+
144
+ ## Literature library
145
+
146
+ A curated, source-tagged literature library is maintained in [`docs/literature/`](docs/literature/README.md). It covers the 2026 state of the art in space-PV cells and arrays, mission-aware performance modelling, radiation/degradation, digital twins, telemetry/AI, and open flight datasets/APIs.
147
+
148
+ ## Roadmap evidence
149
+
150
+ - `docs/literature/09_ieso_2026_workshop_roadmap.md` — 2026 IESO workshop targets translated into repository features and R&D priorities.
151
+ - `docs/product_feature_roadmap.md` — prioritized product epics derived from the literature and 2026 IESO workshop.
@@ -0,0 +1 @@
1
+ """Space PV HTTP API."""
@@ -0,0 +1,13 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class APISettings(BaseSettings):
5
+ model_config = SettingsConfigDict(env_prefix="SPACE_PV_", env_file=".env", extra="ignore")
6
+
7
+ api_host: str = "0.0.0.0"
8
+ api_port: int = 8000
9
+ cors_origins: str = "http://localhost:5173"
10
+
11
+ @property
12
+ def allowed_origins(self) -> list[str]:
13
+ return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
@@ -0,0 +1,202 @@
1
+ import math
2
+ from contextlib import asynccontextmanager
3
+ from datetime import UTC, datetime, timedelta
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ from space_pv_orchestration.celery_app import celery_app
9
+ from space_pv_storage import (
10
+ Base,
11
+ JobRun,
12
+ attach_celery_task,
13
+ create_engine_from_settings,
14
+ create_job,
15
+ get_track_samples,
16
+ list_jobs,
17
+ list_latest_satellite_states,
18
+ session_factory,
19
+ )
20
+
21
+ from .config import APISettings
22
+ from .schemas import (
23
+ JobAccepted,
24
+ JobCreate,
25
+ JobRead,
26
+ MapSatelliteState,
27
+ MapScene,
28
+ MapTrack,
29
+ OrbitSample,
30
+ )
31
+
32
+ settings = APISettings()
33
+ engine = create_engine_from_settings()
34
+ SessionLocal = session_factory()
35
+
36
+
37
+ @asynccontextmanager
38
+ async def lifespan(_: FastAPI):
39
+ # Development bootstrap. Replace with Alembic migrations before production.
40
+ Base.metadata.create_all(engine)
41
+ yield
42
+
43
+
44
+ app = FastAPI(
45
+ title="Space PV Analytics API",
46
+ version="0.1.0",
47
+ description="API for satellite ingestion, orbit/PV computations and analytics jobs.",
48
+ lifespan=lifespan,
49
+ )
50
+ app.add_middleware(
51
+ CORSMiddleware,
52
+ allow_origins=settings.allowed_origins,
53
+ allow_credentials=True,
54
+ allow_methods=["*"],
55
+ allow_headers=["*"],
56
+ )
57
+
58
+
59
+ @app.get("/health")
60
+ def health() -> dict[str, str]:
61
+ return {"status": "ok", "service": "space-pv-api"}
62
+
63
+
64
+ @app.post("/api/v1/jobs", response_model=JobAccepted, status_code=202)
65
+ def submit_job(request: JobCreate) -> JobAccepted:
66
+ with SessionLocal() as session:
67
+ job = create_job(session, request.job_type, request.payload)
68
+ task = celery_app.send_task(
69
+ "space_pv.analytics.execute",
70
+ args=[job.id, request.job_type, request.payload],
71
+ queue="analytics",
72
+ )
73
+ attach_celery_task(session, job, task.id)
74
+ return JobAccepted(job_id=job.id, task_id=task.id, job_type=request.job_type)
75
+
76
+
77
+ def _job_to_schema(job: JobRun) -> JobRead:
78
+ return JobRead(
79
+ id=job.id,
80
+ task_name=job.task_name,
81
+ celery_task_id=job.celery_task_id,
82
+ status=job.status.value,
83
+ payload=job.payload,
84
+ result=job.result,
85
+ error=job.error,
86
+ created_at=job.created_at,
87
+ started_at=job.started_at,
88
+ finished_at=job.finished_at,
89
+ )
90
+
91
+
92
+ @app.get("/api/v1/jobs", response_model=list[JobRead])
93
+ def jobs(limit: int = 100) -> list[JobRead]:
94
+ with SessionLocal() as session:
95
+ return [_job_to_schema(job) for job in list_jobs(session, min(max(limit, 1), 500))]
96
+
97
+
98
+ @app.get("/api/v1/jobs/{job_id}", response_model=JobRead)
99
+ def get_job(job_id: str) -> JobRead:
100
+ with SessionLocal() as session:
101
+ job = session.get(JobRun, job_id)
102
+ if job is None:
103
+ raise HTTPException(status_code=404, detail="Job not found")
104
+ return _job_to_schema(job)
105
+
106
+
107
+ @app.get("/api/v1/map/satellites/latest", response_model=list[MapSatelliteState])
108
+ def latest_satellite_map_states(limit: int = 2000) -> list[MapSatelliteState]:
109
+ with SessionLocal() as session:
110
+ rows = list_latest_satellite_states(session, min(max(limit, 1), 10000))
111
+ return [MapSatelliteState(**row) for row in rows]
112
+
113
+
114
+ @app.get("/api/v1/map/satellites/{satellite_id}/track", response_model=MapTrack)
115
+ def satellite_track(
116
+ satellite_id: str,
117
+ start: datetime,
118
+ end: datetime,
119
+ lod: int = 1,
120
+ ) -> MapTrack:
121
+ if end <= start:
122
+ raise HTTPException(status_code=400, detail="end must be after start")
123
+ with SessionLocal() as session:
124
+ segments = get_track_samples(session, satellite_id, start, end, lod)
125
+ if not segments:
126
+ raise HTTPException(status_code=404, detail="No cached track for requested window")
127
+
128
+ samples: list[OrbitSample] = []
129
+ for segment in segments:
130
+ epoch = segment["start_at"]
131
+ for offset_s, lon, lat, altitude_m in segment["samples"]:
132
+ timestamp = epoch + timedelta(seconds=float(offset_s))
133
+ if start <= timestamp <= end:
134
+ samples.append(
135
+ OrbitSample(
136
+ timestamp=timestamp,
137
+ longitude_deg=float(lon),
138
+ latitude_deg=float(lat),
139
+ altitude_m=float(altitude_m),
140
+ )
141
+ )
142
+ return MapTrack(
143
+ satellite_id=satellite_id,
144
+ name=satellite_id,
145
+ source=str(segments[0]["source"]),
146
+ samples=samples,
147
+ )
148
+
149
+
150
+ def _demo_track(
151
+ name: str,
152
+ inclination_deg: float,
153
+ altitude_m: float,
154
+ phase_deg: float,
155
+ epoch: datetime,
156
+ ) -> MapTrack:
157
+ """Visual-only deterministic samples used to validate the Cesium pipeline.
158
+
159
+ These are deliberately not presented as propagated ephemerides. Real tracks come
160
+ from space-pv-orbit and are cached in gis.orbit_track_segments.
161
+ """
162
+ period_s = 95.0 * 60.0
163
+ samples: list[OrbitSample] = []
164
+ for offset_s in range(0, 2 * 60 * 60 + 1, 30):
165
+ theta = 2.0 * math.pi * offset_s / period_s + math.radians(phase_deg)
166
+ latitude = inclination_deg * math.sin(theta)
167
+ longitude = ((math.degrees(theta) * 0.78 + phase_deg + 180.0) % 360.0) - 180.0
168
+ samples.append(
169
+ OrbitSample(
170
+ timestamp=epoch + timedelta(seconds=offset_s),
171
+ longitude_deg=longitude,
172
+ latitude_deg=latitude,
173
+ altitude_m=altitude_m,
174
+ )
175
+ )
176
+ return MapTrack(
177
+ satellite_id=name.lower().replace(" ", "-"),
178
+ name=name,
179
+ source="visual_demo",
180
+ samples=samples,
181
+ )
182
+
183
+
184
+ @app.get("/api/v1/map/demo", response_model=MapScene)
185
+ def demo_map_scene() -> MapScene:
186
+ epoch = datetime.now(UTC).replace(microsecond=0)
187
+ return MapScene(
188
+ generated_at=epoch,
189
+ tracks=[
190
+ _demo_track("Demo LEO 51.6", 51.6, 420_000.0, 0.0, epoch),
191
+ _demo_track("Demo SSO 97.4", 82.0, 550_000.0, 115.0, epoch),
192
+ _demo_track("Demo LEO 35", 35.0, 650_000.0, 235.0, epoch),
193
+ ],
194
+ )
195
+
196
+
197
+ def run() -> None:
198
+ import uvicorn
199
+
200
+ uvicorn.run(
201
+ "space_pv_api.main:app", host=settings.api_host, port=settings.api_port, reload=False
202
+ )
@@ -0,0 +1,61 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class JobCreate(BaseModel):
8
+ job_type: str = Field(min_length=1, max_length=200)
9
+ payload: dict[str, Any] = Field(default_factory=dict)
10
+
11
+
12
+ class JobAccepted(BaseModel):
13
+ job_id: str
14
+ task_id: str
15
+ job_type: str
16
+ status: str = "queued"
17
+
18
+
19
+ class JobRead(BaseModel):
20
+ id: str
21
+ task_name: str
22
+ celery_task_id: str | None
23
+ status: str
24
+ payload: dict[str, Any]
25
+ result: dict[str, Any] | None
26
+ error: str | None
27
+ created_at: datetime
28
+ started_at: datetime | None
29
+ finished_at: datetime | None
30
+
31
+
32
+ class MapSatelliteState(BaseModel):
33
+ satellite_id: str
34
+ name: str
35
+ norad_id: int | None = None
36
+ observed_at: datetime
37
+ longitude_deg: float
38
+ latitude_deg: float
39
+ altitude_m: float
40
+ illumination_fraction: float | None = None
41
+ pv_power_w: float | None = None
42
+ source: str
43
+
44
+
45
+ class OrbitSample(BaseModel):
46
+ timestamp: datetime
47
+ longitude_deg: float
48
+ latitude_deg: float
49
+ altitude_m: float
50
+
51
+
52
+ class MapTrack(BaseModel):
53
+ satellite_id: str
54
+ name: str
55
+ source: str
56
+ samples: list[OrbitSample]
57
+
58
+
59
+ class MapScene(BaseModel):
60
+ generated_at: datetime
61
+ tracks: list[MapTrack]