sopran 0.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.
- sopran/__init__.py +86 -0
- sopran/bodies/__init__.py +3 -0
- sopran/bodies/moon.py +618 -0
- sopran/core/__init__.py +88 -0
- sopran/core/alignment.py +922 -0
- sopran/core/data.py +339 -0
- sopran/core/database.py +189 -0
- sopran/core/errors.py +37 -0
- sopran/core/loaders.py +32 -0
- sopran/core/pages.py +165 -0
- sopran/core/pipeline.py +405 -0
- sopran/core/plotting.py +499 -0
- sopran/core/project.py +462 -0
- sopran/core/schema.py +300 -0
- sopran/core/store.py +880 -0
- sopran/core/time.py +95 -0
- sopran/frames/__init__.py +179 -0
- sopran/maps/__init__.py +110 -0
- sopran/missions/__init__.py +1 -0
- sopran/missions/artemis/README.ja.md +39 -0
- sopran/missions/artemis/README.md +41 -0
- sopran/missions/artemis/__init__.py +3 -0
- sopran/missions/artemis/mission.py +529 -0
- sopran/missions/kaguya/ESA1.ja.md +56 -0
- sopran/missions/kaguya/ESA1.md +68 -0
- sopran/missions/kaguya/README.ja.md +32 -0
- sopran/missions/kaguya/README.md +58 -0
- sopran/missions/kaguya/__init__.py +37 -0
- sopran/missions/kaguya/data.py +356 -0
- sopran/missions/kaguya/files.py +98 -0
- sopran/missions/kaguya/lmag.py +209 -0
- sopran/missions/kaguya/mission.py +1545 -0
- sopran/missions/kaguya/pace.py +529 -0
- sopran/missions/kaguya/schema.py +52 -0
- sopran/missions/kaguya/sensors.py +45 -0
- sopran/schema_docs.py +119 -0
- sopran-0.0.0.dist-info/METADATA +582 -0
- sopran-0.0.0.dist-info/RECORD +42 -0
- sopran-0.0.0.dist-info/WHEEL +4 -0
- sopran-0.0.0.dist-info/entry_points.txt +2 -0
- sopran-0.0.0.dist-info/licenses/LICENSE +201 -0
- sopran-0.0.0.dist-info/licenses/NOTICE +9 -0
sopran/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Satellite Observation Package for Retrieval, Analysis, and Navigation."""
|
|
2
|
+
|
|
3
|
+
from sopran.bodies import Moon
|
|
4
|
+
from sopran.core import AlignmentResult
|
|
5
|
+
from sopran.core import BackendError, ConfigError, DecodeError, DownloadError
|
|
6
|
+
from sopran.core import Database, DatasetNotFoundError, GuidePage, InfoPage
|
|
7
|
+
from sopran.core import FeatureMatrix
|
|
8
|
+
from sopran.core import FrameTransformError, PipelineError, SchemaError
|
|
9
|
+
from sopran.core import InstrumentSchema, VariableSchema
|
|
10
|
+
from sopran.core import PlotArtifact, PlotItem, PlotPlan, PlotResult, ProductRef
|
|
11
|
+
from sopran.core import PlotStack, SampleSpec, SampleTable, SopranError, Store, TimeRange
|
|
12
|
+
from sopran.core import QuicklookResult
|
|
13
|
+
from sopran.core import TimeBins, align, day, histogram, line, lines, load, month, period
|
|
14
|
+
from sopran.core import spectrogram, stack, year
|
|
15
|
+
from sopran.core import time_bins
|
|
16
|
+
from sopran.core import validate_schema
|
|
17
|
+
from sopran.core.project import Project
|
|
18
|
+
from sopran.frames import FrameContext, FrameTransformPlan, normalize_frame
|
|
19
|
+
from sopran.maps import Region
|
|
20
|
+
from sopran.missions.artemis import Artemis
|
|
21
|
+
from sopran.missions.kaguya import Kaguya
|
|
22
|
+
|
|
23
|
+
__version__ = "0.0.0"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name: str):
|
|
27
|
+
if name in {"builtin_schemas", "schema_reference_markdown"}:
|
|
28
|
+
from importlib import import_module
|
|
29
|
+
|
|
30
|
+
return getattr(import_module("sopran.schema_docs"), name)
|
|
31
|
+
raise AttributeError(f"module 'sopran' has no attribute {name!r}")
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"GuidePage",
|
|
35
|
+
"InfoPage",
|
|
36
|
+
"AlignmentResult",
|
|
37
|
+
"Artemis",
|
|
38
|
+
"BackendError",
|
|
39
|
+
"ConfigError",
|
|
40
|
+
"Database",
|
|
41
|
+
"DatasetNotFoundError",
|
|
42
|
+
"DecodeError",
|
|
43
|
+
"DownloadError",
|
|
44
|
+
"FrameTransformError",
|
|
45
|
+
"FrameContext",
|
|
46
|
+
"FrameTransformPlan",
|
|
47
|
+
"FeatureMatrix",
|
|
48
|
+
"InstrumentSchema",
|
|
49
|
+
"Kaguya",
|
|
50
|
+
"Moon",
|
|
51
|
+
"PlotArtifact",
|
|
52
|
+
"PlotItem",
|
|
53
|
+
"PlotPlan",
|
|
54
|
+
"PlotResult",
|
|
55
|
+
"PlotStack",
|
|
56
|
+
"Project",
|
|
57
|
+
"PipelineError",
|
|
58
|
+
"ProductRef",
|
|
59
|
+
"QuicklookResult",
|
|
60
|
+
"Region",
|
|
61
|
+
"SampleSpec",
|
|
62
|
+
"SampleTable",
|
|
63
|
+
"SchemaError",
|
|
64
|
+
"SopranError",
|
|
65
|
+
"Store",
|
|
66
|
+
"TimeRange",
|
|
67
|
+
"TimeBins",
|
|
68
|
+
"VariableSchema",
|
|
69
|
+
"__version__",
|
|
70
|
+
"align",
|
|
71
|
+
"builtin_schemas",
|
|
72
|
+
"day",
|
|
73
|
+
"histogram",
|
|
74
|
+
"line",
|
|
75
|
+
"lines",
|
|
76
|
+
"load",
|
|
77
|
+
"month",
|
|
78
|
+
"normalize_frame",
|
|
79
|
+
"period",
|
|
80
|
+
"schema_reference_markdown",
|
|
81
|
+
"spectrogram",
|
|
82
|
+
"stack",
|
|
83
|
+
"time_bins",
|
|
84
|
+
"validate_schema",
|
|
85
|
+
"year",
|
|
86
|
+
]
|
sopran/bodies/moon.py
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sopran.core.pages import GuidePage, InfoPage
|
|
7
|
+
from sopran.core.schema import InstrumentSchema, VariableSchema
|
|
8
|
+
|
|
9
|
+
_GUIDE_LANGUAGES = ("ja", "en")
|
|
10
|
+
_PUBLIC_DOC_URL = "https://nkzono99.github.io/sopran/maps/moon/"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SurfacePlan:
|
|
15
|
+
body: str
|
|
16
|
+
product: str
|
|
17
|
+
parameters: dict[str, Any]
|
|
18
|
+
|
|
19
|
+
def to_metadata(self) -> dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"body": self.body,
|
|
22
|
+
"product": self.product,
|
|
23
|
+
"parameters": _metadata_value(self.parameters),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Moon:
|
|
28
|
+
"""Body-first entry point for Moon map products."""
|
|
29
|
+
|
|
30
|
+
name = "moon"
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self.dem = SurfaceEndpoint(self, "dem", "DEM")
|
|
34
|
+
self.svm = SurfaceEndpoint(self, "svm", "SVM")
|
|
35
|
+
self.shadow = SurfaceEndpoint(self, "shadow", "Shadow map")
|
|
36
|
+
self.illumination = SurfaceEndpoint(self, "illumination", "Illumination map")
|
|
37
|
+
self.sza = SurfaceEndpoint(self, "sza", "Solar zenith angle")
|
|
38
|
+
|
|
39
|
+
def info(self) -> InfoPage:
|
|
40
|
+
return InfoPage(
|
|
41
|
+
title="Moon",
|
|
42
|
+
lines=(
|
|
43
|
+
"dem: digital elevation model endpoint",
|
|
44
|
+
"svm: surface vector map endpoint",
|
|
45
|
+
"shadow: terrain-aware shadow map endpoint skeleton",
|
|
46
|
+
"illumination: terrain-aware illumination endpoint skeleton",
|
|
47
|
+
"sza: solar zenith angle planning endpoint skeleton",
|
|
48
|
+
"schema: "
|
|
49
|
+
+ _format_list(variable.name for variable in self.schema().variables),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def guide(self, *, language: str = "ja") -> GuidePage:
|
|
54
|
+
return _guide_page(
|
|
55
|
+
title="Moon Maps",
|
|
56
|
+
source="sopran.bodies.moon",
|
|
57
|
+
markdowns=_MOON_GUIDES,
|
|
58
|
+
language=language,
|
|
59
|
+
url=_PUBLIC_DOC_URL,
|
|
60
|
+
).with_schema(MOON_SURFACE_SCHEMA)
|
|
61
|
+
|
|
62
|
+
def help(self, *, language: str = "ja") -> GuidePage:
|
|
63
|
+
return self.guide(language=language)
|
|
64
|
+
|
|
65
|
+
def schema(self) -> InstrumentSchema:
|
|
66
|
+
return MOON_SURFACE_SCHEMA
|
|
67
|
+
|
|
68
|
+
def example(self) -> GuidePage:
|
|
69
|
+
return _example_page(
|
|
70
|
+
"Moon Maps Example",
|
|
71
|
+
"""# Moon Maps Example
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import sopran as spn
|
|
75
|
+
|
|
76
|
+
moon = spn.Moon()
|
|
77
|
+
region = spn.Region(lon=(120, 160), lat=(-45, -10), body="moon")
|
|
78
|
+
|
|
79
|
+
dem_plan = moon.dem.plan(
|
|
80
|
+
source="kaguya.tc.dem",
|
|
81
|
+
region=region,
|
|
82
|
+
resolution="512ppd",
|
|
83
|
+
projection="native",
|
|
84
|
+
)
|
|
85
|
+
shadow_plan = moon.shadow.plan(time="2008-02-01T12:00:00Z", dem=dem_plan)
|
|
86
|
+
sza_plan = moon.sza.plan(
|
|
87
|
+
time="2008-02-01T12:00:00Z",
|
|
88
|
+
region=region,
|
|
89
|
+
geometry_source="spice",
|
|
90
|
+
)
|
|
91
|
+
metadata = shadow_plan.to_metadata()
|
|
92
|
+
```
|
|
93
|
+
""",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def map(self, product: str) -> SurfaceEndpoint:
|
|
97
|
+
endpoints = {
|
|
98
|
+
"dem": self.dem,
|
|
99
|
+
"svm": self.svm,
|
|
100
|
+
"shadow": self.shadow,
|
|
101
|
+
"illumination": self.illumination,
|
|
102
|
+
"sza": self.sza,
|
|
103
|
+
}
|
|
104
|
+
try:
|
|
105
|
+
canonical = MOON_SURFACE_SCHEMA.variable(product).name
|
|
106
|
+
return endpoints[canonical]
|
|
107
|
+
except KeyError as exc:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"Unknown Moon surface product. Available products: "
|
|
110
|
+
+ _format_list(variable.name for variable in MOON_SURFACE_SCHEMA.variables)
|
|
111
|
+
+ ". Aliases: "
|
|
112
|
+
+ _format_list(
|
|
113
|
+
alias
|
|
114
|
+
for variable in MOON_SURFACE_SCHEMA.variables
|
|
115
|
+
for alias in variable.aliases
|
|
116
|
+
)
|
|
117
|
+
) from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SurfaceEndpoint:
|
|
121
|
+
def __init__(self, body: Moon, product: str, label: str) -> None:
|
|
122
|
+
self.body = body
|
|
123
|
+
self.product = product
|
|
124
|
+
self.label = label
|
|
125
|
+
|
|
126
|
+
def info(self) -> InfoPage:
|
|
127
|
+
schema = self.schema()
|
|
128
|
+
return InfoPage(
|
|
129
|
+
title=f"Moon.{self.product}",
|
|
130
|
+
lines=(
|
|
131
|
+
f"{self.label} surface product.",
|
|
132
|
+
"v0.1 implements planning only; load/compute backends are later milestones.",
|
|
133
|
+
"sources: " + _format_list(self.sources()),
|
|
134
|
+
"dims: " + _format_list(schema.dims),
|
|
135
|
+
f"units: {schema.units or 'none'}",
|
|
136
|
+
f"frame: {schema.frame or 'none'}",
|
|
137
|
+
"aliases: " + _format_list(schema.aliases),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def guide(self, *, language: str = "ja") -> GuidePage:
|
|
142
|
+
return _guide_page(
|
|
143
|
+
title=f"Moon {self.label}",
|
|
144
|
+
source=f"sopran.bodies.moon.{self.product}",
|
|
145
|
+
markdowns=_SURFACE_GUIDES.get(self.product, _MOON_GUIDES),
|
|
146
|
+
language=language,
|
|
147
|
+
url=_PUBLIC_DOC_URL,
|
|
148
|
+
).with_schema(_surface_product_schema(self.product))
|
|
149
|
+
|
|
150
|
+
def help(self, *, language: str = "ja") -> GuidePage:
|
|
151
|
+
return self.guide(language=language)
|
|
152
|
+
|
|
153
|
+
def schema(self) -> VariableSchema:
|
|
154
|
+
return MOON_SURFACE_SCHEMA.variable(self.product)
|
|
155
|
+
|
|
156
|
+
def example(self) -> GuidePage:
|
|
157
|
+
examples = {
|
|
158
|
+
"dem": (
|
|
159
|
+
"Moon DEM Example",
|
|
160
|
+
"""# Moon DEM Example
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import sopran as spn
|
|
164
|
+
|
|
165
|
+
moon = spn.Moon()
|
|
166
|
+
region = spn.Region(lon=(120, 160), lat=(-45, -10), body="moon")
|
|
167
|
+
|
|
168
|
+
dem_plan = moon.dem.plan(
|
|
169
|
+
source="kaguya.tc.dem",
|
|
170
|
+
region=region,
|
|
171
|
+
resolution="512ppd",
|
|
172
|
+
projection="native",
|
|
173
|
+
)
|
|
174
|
+
metadata = dem_plan.to_metadata()
|
|
175
|
+
```
|
|
176
|
+
""",
|
|
177
|
+
),
|
|
178
|
+
"shadow": (
|
|
179
|
+
"Moon Shadow Example",
|
|
180
|
+
"""# Moon Shadow Example
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import sopran as spn
|
|
184
|
+
|
|
185
|
+
moon = spn.Moon()
|
|
186
|
+
region = spn.Region(lon=(120, 160), lat=(-45, -10), body="moon")
|
|
187
|
+
dem_plan = moon.dem.plan(source="kaguya.tc.dem", region=region)
|
|
188
|
+
|
|
189
|
+
shadow_plan = moon.shadow.plan(
|
|
190
|
+
time="2008-02-01T12:00:00Z",
|
|
191
|
+
dem=dem_plan,
|
|
192
|
+
model="terrain_ray",
|
|
193
|
+
)
|
|
194
|
+
metadata = shadow_plan.to_metadata()
|
|
195
|
+
```
|
|
196
|
+
""",
|
|
197
|
+
),
|
|
198
|
+
"illumination": (
|
|
199
|
+
"Moon Illumination Example",
|
|
200
|
+
"""# Moon Illumination Example
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import sopran as spn
|
|
204
|
+
|
|
205
|
+
moon = spn.Moon()
|
|
206
|
+
dem_plan = moon.dem.plan(source="kaguya.tc.dem")
|
|
207
|
+
|
|
208
|
+
illumination_plan = moon.illumination.plan(
|
|
209
|
+
time="2008-02-01T12:00:00Z",
|
|
210
|
+
dem=dem_plan,
|
|
211
|
+
geometry_source="spice",
|
|
212
|
+
)
|
|
213
|
+
metadata = illumination_plan.to_metadata()
|
|
214
|
+
```
|
|
215
|
+
""",
|
|
216
|
+
),
|
|
217
|
+
"sza": (
|
|
218
|
+
"Moon Solar Zenith Angle Example",
|
|
219
|
+
"""# Moon Solar Zenith Angle Example
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
import sopran as spn
|
|
223
|
+
|
|
224
|
+
moon = spn.Moon()
|
|
225
|
+
region = spn.Region(lon=(120, 160), lat=(-45, -10), body="moon")
|
|
226
|
+
|
|
227
|
+
sza_plan = moon.sza.plan(
|
|
228
|
+
time="2008-02-01T12:00:00Z",
|
|
229
|
+
region=region,
|
|
230
|
+
geometry_source="spice",
|
|
231
|
+
)
|
|
232
|
+
metadata = sza_plan.to_metadata()
|
|
233
|
+
```
|
|
234
|
+
""",
|
|
235
|
+
),
|
|
236
|
+
"svm": (
|
|
237
|
+
"Moon SVM Example",
|
|
238
|
+
"""# Moon SVM Example
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
import sopran as spn
|
|
242
|
+
|
|
243
|
+
moon = spn.Moon()
|
|
244
|
+
region = spn.Region(lon=(120, 160), lat=(-45, -10), body="moon")
|
|
245
|
+
|
|
246
|
+
svm_plan = moon.svm.plan(source="kaguya.lism.svm", region=region)
|
|
247
|
+
metadata = svm_plan.to_metadata()
|
|
248
|
+
```
|
|
249
|
+
""",
|
|
250
|
+
),
|
|
251
|
+
}
|
|
252
|
+
title, markdown = examples.get(self.product, examples["dem"])
|
|
253
|
+
return _example_page(title, markdown)
|
|
254
|
+
|
|
255
|
+
def sources(self) -> tuple[str, ...]:
|
|
256
|
+
return _SURFACE_SOURCES.get(self.product, ())
|
|
257
|
+
|
|
258
|
+
def plan(self, **parameters: Any) -> SurfacePlan:
|
|
259
|
+
return SurfacePlan(
|
|
260
|
+
body=self.body.name,
|
|
261
|
+
product=self.product,
|
|
262
|
+
parameters=_surface_parameters(self.product, parameters),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def load(self, **parameters: Any) -> None:
|
|
266
|
+
plan = self.plan(**parameters)
|
|
267
|
+
raise NotImplementedError(f"Moon.{plan.product}.load() is not implemented yet")
|
|
268
|
+
|
|
269
|
+
def compute(self, **parameters: Any) -> None:
|
|
270
|
+
plan = self.plan(**parameters)
|
|
271
|
+
raise NotImplementedError(f"Moon.{plan.product}.compute() is not implemented yet")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
_MOON_GUIDES = {
|
|
275
|
+
"en": """# Moon Maps
|
|
276
|
+
|
|
277
|
+
SOPRAN uses a body-first API for Moon map products. Mission modules provide
|
|
278
|
+
provider-specific discovery, while `spn.Moon()` owns body-fixed DEM, SVM, SZA,
|
|
279
|
+
shadow, illumination, projection, and region semantics.
|
|
280
|
+
|
|
281
|
+
The v0.1 implementation is a planning skeleton. Terrain-aware shadow and
|
|
282
|
+
illumination backends will require DEM data, solar geometry, body shape, and
|
|
283
|
+
explicit longitude/projection metadata.
|
|
284
|
+
""",
|
|
285
|
+
"ja": """# Moon Maps
|
|
286
|
+
|
|
287
|
+
SOPRAN は月面マップを body-first API として扱います。mission module は
|
|
288
|
+
provider-specific discovery を担当し、`spn.Moon()` は月固定 DEM、SVM、SZA、shadow、
|
|
289
|
+
illumination、projection、region semantics を受け持ちます。
|
|
290
|
+
|
|
291
|
+
v0.1 実装は planning skeleton です。terrain-aware shadow と illumination backend では
|
|
292
|
+
DEM data、solar geometry、body shape、longitude/projection metadata を明示的に扱います。
|
|
293
|
+
""",
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_SURFACE_GUIDES = {
|
|
297
|
+
"dem": {
|
|
298
|
+
"en": """# Moon DEM
|
|
299
|
+
|
|
300
|
+
DEM products represent body-fixed lunar elevation rasters. Planned metadata
|
|
301
|
+
includes source, resolution, datum or shape model, longitude domain, projection,
|
|
302
|
+
and area-or-point interpretation.
|
|
303
|
+
""",
|
|
304
|
+
"ja": """# Moon DEM
|
|
305
|
+
|
|
306
|
+
DEM product は月固定の elevation raster を表します。予定している metadata には source、
|
|
307
|
+
resolution、datum または shape model、longitude domain、projection、area-or-point
|
|
308
|
+
interpretation を含めます。
|
|
309
|
+
""",
|
|
310
|
+
},
|
|
311
|
+
"svm": {
|
|
312
|
+
"en": """# Moon SVM
|
|
313
|
+
|
|
314
|
+
SVM products represent lunar surface vector maps or classified map layers.
|
|
315
|
+
They share the same body-fixed region and projection metadata as DEM products.
|
|
316
|
+
""",
|
|
317
|
+
"ja": """# Moon SVM
|
|
318
|
+
|
|
319
|
+
SVM product は lunar surface vector map または classified map layer を表します。
|
|
320
|
+
DEM product と同じ body-fixed region と projection metadata を共有します。
|
|
321
|
+
""",
|
|
322
|
+
},
|
|
323
|
+
"shadow": {
|
|
324
|
+
"en": """# Moon Shadow Map
|
|
325
|
+
|
|
326
|
+
Shadow products must be computed from DEM terrain, solar position, body shape,
|
|
327
|
+
and projection metadata. The current endpoint only records plans.
|
|
328
|
+
""",
|
|
329
|
+
"ja": """# Moon Shadow Map
|
|
330
|
+
|
|
331
|
+
Shadow product は DEM terrain、solar position、body shape、projection metadata から
|
|
332
|
+
計算する必要があります。現在の endpoint は plan の記録だけを行います。
|
|
333
|
+
""",
|
|
334
|
+
},
|
|
335
|
+
"illumination": {
|
|
336
|
+
"en": """# Moon Illumination Map
|
|
337
|
+
|
|
338
|
+
Illumination products will represent solar incidence and visibility derived
|
|
339
|
+
from DEM terrain and SPICE-backed solar geometry.
|
|
340
|
+
""",
|
|
341
|
+
"ja": """# Moon Illumination Map
|
|
342
|
+
|
|
343
|
+
Illumination product は DEM terrain と SPICE-backed solar geometry から導く
|
|
344
|
+
solar incidence と visibility を表す予定です。
|
|
345
|
+
""",
|
|
346
|
+
},
|
|
347
|
+
"sza": {
|
|
348
|
+
"en": """# Moon Solar Zenith Angle
|
|
349
|
+
|
|
350
|
+
SZA products represent solar zenith angle on the lunar surface. The planning
|
|
351
|
+
endpoint records time, region, geometry_source backend, and projection metadata
|
|
352
|
+
before SPICE-backed computation is implemented.
|
|
353
|
+
""",
|
|
354
|
+
"ja": """# Moon Solar Zenith Angle
|
|
355
|
+
|
|
356
|
+
SZA product は月面上の solar zenith angle を表します。planning endpoint は
|
|
357
|
+
SPICE-backed computation の実装前に、time、region、geometry_source backend、
|
|
358
|
+
projection metadata を記録します。
|
|
359
|
+
""",
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_SURFACE_SOURCES = {
|
|
364
|
+
"dem": ("kaguya.tc.dem", "lro.lola.dem"),
|
|
365
|
+
"svm": ("kaguya.lism.svm",),
|
|
366
|
+
"shadow": ("legacy.shadowmap_sza",),
|
|
367
|
+
"illumination": (),
|
|
368
|
+
"sza": ("computed.spice.sza",),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
MOON_SURFACE_SCHEMA = InstrumentSchema(
|
|
372
|
+
mission="moon",
|
|
373
|
+
instrument="surface",
|
|
374
|
+
variables=(
|
|
375
|
+
VariableSchema(
|
|
376
|
+
name="dem",
|
|
377
|
+
dims=("lat", "lon"),
|
|
378
|
+
units="m",
|
|
379
|
+
dtype="float64",
|
|
380
|
+
frame="Moon body-fixed",
|
|
381
|
+
description="Digital elevation model on a body-fixed lunar grid.",
|
|
382
|
+
aliases=("elevation", "height"),
|
|
383
|
+
),
|
|
384
|
+
VariableSchema(
|
|
385
|
+
name="svm",
|
|
386
|
+
dims=("lat", "lon"),
|
|
387
|
+
dtype="string",
|
|
388
|
+
frame="Moon body-fixed",
|
|
389
|
+
description="Surface vector map or classified lunar map layer.",
|
|
390
|
+
aliases=("surface_vector_map",),
|
|
391
|
+
),
|
|
392
|
+
VariableSchema(
|
|
393
|
+
name="shadow",
|
|
394
|
+
dims=("lat", "lon"),
|
|
395
|
+
units="fraction",
|
|
396
|
+
dtype="float64",
|
|
397
|
+
frame="Moon body-fixed",
|
|
398
|
+
description="terrain-aware shadow or shadow-fraction map.",
|
|
399
|
+
aliases=("shadow_map", "shadow_fraction"),
|
|
400
|
+
),
|
|
401
|
+
VariableSchema(
|
|
402
|
+
name="illumination",
|
|
403
|
+
dims=("lat", "lon"),
|
|
404
|
+
units="fraction",
|
|
405
|
+
dtype="float64",
|
|
406
|
+
frame="Moon body-fixed",
|
|
407
|
+
description="Illumination or visibility fraction derived from solar geometry.",
|
|
408
|
+
aliases=("illumination_map", "visibility"),
|
|
409
|
+
),
|
|
410
|
+
VariableSchema(
|
|
411
|
+
name="sza",
|
|
412
|
+
dims=("lat", "lon"),
|
|
413
|
+
units="deg",
|
|
414
|
+
dtype="float64",
|
|
415
|
+
frame="Moon body-fixed",
|
|
416
|
+
description="Solar zenith angle on the lunar surface.",
|
|
417
|
+
aliases=("solar_zenith_angle",),
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _metadata_value(value: Any) -> Any:
|
|
424
|
+
if hasattr(value, "to_metadata"):
|
|
425
|
+
return value.to_metadata()
|
|
426
|
+
if isinstance(value, dict):
|
|
427
|
+
return {str(key): _metadata_value(item) for key, item in value.items()}
|
|
428
|
+
if isinstance(value, (tuple, list)):
|
|
429
|
+
return [_metadata_value(item) for item in value]
|
|
430
|
+
return value
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _format_list(values) -> str:
|
|
434
|
+
items = tuple(str(value) for value in values)
|
|
435
|
+
return ", ".join(items) if items else "none"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _surface_parameters(product: str, parameters: dict[str, Any]) -> dict[str, Any]:
|
|
439
|
+
normalized = dict(parameters)
|
|
440
|
+
if product in {"shadow", "illumination"} and (
|
|
441
|
+
"method" in normalized or "model" in normalized
|
|
442
|
+
):
|
|
443
|
+
model = _surface_model(normalized)
|
|
444
|
+
normalized["method"] = model
|
|
445
|
+
normalized["model"] = model
|
|
446
|
+
if (
|
|
447
|
+
product == "sza"
|
|
448
|
+
or "geometry_source" in normalized
|
|
449
|
+
or "geometry" in normalized
|
|
450
|
+
or "ephemeris" in normalized
|
|
451
|
+
):
|
|
452
|
+
default_geometry = "spice" if product == "sza" else None
|
|
453
|
+
geometry = _geometry_source(normalized, default=default_geometry)
|
|
454
|
+
normalized["geometry"] = geometry
|
|
455
|
+
normalized["geometry_source"] = geometry
|
|
456
|
+
reference = _coordinate_reference(normalized)
|
|
457
|
+
normalized["lon_domain"] = _canonical_lon_domain(
|
|
458
|
+
str(normalized.get("lon_domain", reference.get("lon_domain", "0_360")))
|
|
459
|
+
)
|
|
460
|
+
normalized["lon_direction"] = _canonical_lon_direction(
|
|
461
|
+
str(
|
|
462
|
+
normalized.get(
|
|
463
|
+
"lon_direction",
|
|
464
|
+
reference.get("lon_direction", "east_positive"),
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
normalized["lat_type"] = _canonical_lat_type(
|
|
469
|
+
str(normalized.get("lat_type", reference.get("lat_type", "planetocentric")))
|
|
470
|
+
)
|
|
471
|
+
normalized["shape"] = _canonical_shape(
|
|
472
|
+
str(normalized.get("shape", reference.get("shape", "spherical")))
|
|
473
|
+
)
|
|
474
|
+
datum = normalized.get("datum", reference.get("datum"))
|
|
475
|
+
if datum is not None:
|
|
476
|
+
normalized["datum"] = str(datum)
|
|
477
|
+
normalized["projection"] = _canonical_projection(
|
|
478
|
+
str(normalized.get("projection", reference.get("projection", "native")))
|
|
479
|
+
)
|
|
480
|
+
normalized["area_or_point"] = _canonical_area_or_point(
|
|
481
|
+
str(normalized.get("area_or_point", reference.get("area_or_point", "area")))
|
|
482
|
+
)
|
|
483
|
+
return normalized
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _surface_model(parameters: dict[str, Any]) -> str:
|
|
487
|
+
method = parameters.get("method")
|
|
488
|
+
model = parameters.get("model")
|
|
489
|
+
if method is not None and model is not None and str(method) != str(model):
|
|
490
|
+
raise ValueError("method and model must match when both are provided")
|
|
491
|
+
value = method if method is not None else model
|
|
492
|
+
if value is None:
|
|
493
|
+
raise ValueError("method/model cannot be empty")
|
|
494
|
+
return str(value)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _geometry_source(parameters: dict[str, Any], *, default: str | None) -> str:
|
|
498
|
+
value = parameters.get(
|
|
499
|
+
"geometry_source",
|
|
500
|
+
parameters.get("geometry", parameters.get("ephemeris", default)),
|
|
501
|
+
)
|
|
502
|
+
if value is None:
|
|
503
|
+
raise ValueError("geometry_source cannot be empty")
|
|
504
|
+
return str(value)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _coordinate_reference(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
508
|
+
region = _metadata_value(parameters.get("region"))
|
|
509
|
+
if isinstance(region, dict):
|
|
510
|
+
return region
|
|
511
|
+
dem = _metadata_value(parameters.get("dem"))
|
|
512
|
+
if isinstance(dem, dict) and isinstance(dem.get("parameters"), dict):
|
|
513
|
+
return dem["parameters"]
|
|
514
|
+
return {}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _canonical_lon_domain(lon_domain: str) -> str:
|
|
518
|
+
if lon_domain == "minus180_180":
|
|
519
|
+
return "-180_180"
|
|
520
|
+
if lon_domain in {"0_360", "-180_180"}:
|
|
521
|
+
return lon_domain
|
|
522
|
+
raise ValueError("lon_domain must be '0_360', '-180_180', or 'minus180_180'")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _canonical_lon_direction(lon_direction: str) -> str:
|
|
526
|
+
if lon_direction in {"east_positive", "west_positive"}:
|
|
527
|
+
return lon_direction
|
|
528
|
+
raise ValueError("lon_direction must be 'east_positive' or 'west_positive'")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _canonical_lat_type(lat_type: str) -> str:
|
|
532
|
+
if lat_type in {"planetocentric", "planetographic"}:
|
|
533
|
+
return lat_type
|
|
534
|
+
raise ValueError("lat_type must be 'planetocentric' or 'planetographic'")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _canonical_shape(shape: str) -> str:
|
|
538
|
+
aliases = {
|
|
539
|
+
"sphere": "spherical",
|
|
540
|
+
"spice": "spice_body_radii",
|
|
541
|
+
"body_radii": "spice_body_radii",
|
|
542
|
+
}
|
|
543
|
+
canonical = aliases.get(shape, shape)
|
|
544
|
+
allowed = {"spherical", "ellipsoid", "triaxial", "spice_body_radii"}
|
|
545
|
+
if canonical in allowed:
|
|
546
|
+
return canonical
|
|
547
|
+
raise ValueError(
|
|
548
|
+
"shape must be one of spherical, ellipsoid, triaxial, "
|
|
549
|
+
"spice_body_radii, sphere, spice, or body_radii"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _canonical_projection(projection: str) -> str:
|
|
554
|
+
aliases = {
|
|
555
|
+
"polar_stereo": "polar_stereographic",
|
|
556
|
+
}
|
|
557
|
+
canonical = aliases.get(projection, projection)
|
|
558
|
+
allowed = {
|
|
559
|
+
"equirectangular",
|
|
560
|
+
"polar_stereographic",
|
|
561
|
+
"orthographic",
|
|
562
|
+
"azimuthal_equidistant",
|
|
563
|
+
"lambert",
|
|
564
|
+
"native",
|
|
565
|
+
}
|
|
566
|
+
if canonical in allowed:
|
|
567
|
+
return canonical
|
|
568
|
+
raise ValueError(
|
|
569
|
+
"projection must be one of equirectangular, polar_stereographic, "
|
|
570
|
+
"orthographic, azimuthal_equidistant, lambert, native, or polar_stereo"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _canonical_area_or_point(area_or_point: str) -> str:
|
|
575
|
+
if area_or_point in {"area", "point"}:
|
|
576
|
+
return area_or_point
|
|
577
|
+
raise ValueError("area_or_point must be 'area' or 'point'")
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _guide_page(
|
|
581
|
+
*,
|
|
582
|
+
title: str,
|
|
583
|
+
source: str,
|
|
584
|
+
markdowns: dict[str, str],
|
|
585
|
+
language: str,
|
|
586
|
+
url: str | None = None,
|
|
587
|
+
) -> GuidePage:
|
|
588
|
+
if language not in _GUIDE_LANGUAGES:
|
|
589
|
+
raise ValueError(f"Moon guide language is not available: {language}")
|
|
590
|
+
return GuidePage(
|
|
591
|
+
title=title,
|
|
592
|
+
markdown=markdowns[language],
|
|
593
|
+
source=source,
|
|
594
|
+
url=url,
|
|
595
|
+
language=language,
|
|
596
|
+
available_languages=_GUIDE_LANGUAGES,
|
|
597
|
+
translations={
|
|
598
|
+
available_language: markdowns[available_language]
|
|
599
|
+
for available_language in _GUIDE_LANGUAGES
|
|
600
|
+
if available_language != language
|
|
601
|
+
},
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _surface_product_schema(product: str) -> InstrumentSchema:
|
|
606
|
+
return InstrumentSchema(
|
|
607
|
+
mission="moon",
|
|
608
|
+
instrument=f"surface.{product}",
|
|
609
|
+
variables=(MOON_SURFACE_SCHEMA.variable(product),),
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _example_page(title: str, markdown: str) -> GuidePage:
|
|
614
|
+
return GuidePage(
|
|
615
|
+
title=title,
|
|
616
|
+
markdown=markdown,
|
|
617
|
+
source="sopran.bodies.moon.examples",
|
|
618
|
+
)
|