SatVisMars 0.0.1__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.
SatVisMars/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path as FilePath
4
+ from typing import Optional
5
+
6
+ import matplotlib.animation as animation
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ from matplotlib.path import Path
10
+ from matplotlib.patches import PathPatch
11
+ from matplotlib.widgets import Slider
12
+
13
+ from .calc import Visibility
14
+
15
+ MARS_MAP_PATH = FilePath(__file__).resolve().parent / "img" / "mars_names.png"
16
+
17
+
18
+ class VisibilityWidget:
19
+ """Animate the Phobos visibility footprint over an equirectangular Mars map."""
20
+
21
+ def __init__(
22
+ self,
23
+ visibility: Optional[Visibility] = None,
24
+ image_path: FilePath = MARS_MAP_PATH,
25
+ footprint_points: int = 500,
26
+ ):
27
+ self.visibility = visibility or Visibility()
28
+ self.image_path = image_path
29
+ self.footprint_points = footprint_points
30
+ self._fig = None
31
+ self._ax = None
32
+ self._long_slider = None
33
+ self._shade_patch = None
34
+ self._boundary_lines: list = []
35
+
36
+
37
+ def _setup_figure(self) -> None:
38
+ self._fig, self._ax = plt.subplots(figsize=(12, 6))
39
+
40
+ # Set up sliders for longitude and latitude
41
+ self._long_slider_ax = self._fig.add_axes((0.2, 0.01, 0.6, 0.03))
42
+ self._long_slider = Slider(
43
+ self._long_slider_ax,
44
+ "Longitude",
45
+ valmin=-180,
46
+ valmax=180,
47
+ valinit=0,
48
+ valstep=0.1,
49
+ orientation="horizontal",
50
+ )
51
+
52
+ # Add slider for radius ratio, see calc.py for more details
53
+ self._radius_ratio_ax = self._fig.add_axes((0.03, 0.2, 0.03, 0.6))
54
+ self._radius_ratio_slider = Slider(
55
+ self._radius_ratio_ax,
56
+ "Sphere Radius Ratio",
57
+ valmin=1e-3,
58
+ valmax=1.0,
59
+ valinit=self.visibility.radius_ratio,
60
+ valstep=1e-5,
61
+ orientation="vertical",
62
+ )
63
+ self._radius_ratio_slider.label.set_position((-0.5, 0.5))
64
+ self._radius_ratio_slider.label.set_rotation(90)
65
+ self._radius_ratio_slider.label.set_ha("center")
66
+ self._radius_ratio_slider.label.set_va("center")
67
+
68
+ # Add slider for satellite orbit ratio, see calc.py for more details
69
+ self._satellite_orbit_ratio_ax = self._fig.add_axes((0.92, 0.2, 0.03, 0.6))
70
+ self._satellite_orbit_ratio_slider = Slider(
71
+ self._satellite_orbit_ratio_ax,
72
+ "Satellite Orbit Ratio",
73
+ valmin=1.0,
74
+ valmax=50,
75
+ valinit=self.visibility.satellite_orbit_ratio,
76
+ valstep=0.1,
77
+ orientation="vertical",
78
+ )
79
+ self._satellite_orbit_ratio_slider.label.set_position((1.5, 0.5))
80
+ self._satellite_orbit_ratio_slider.label.set_rotation(-90)
81
+ self._satellite_orbit_ratio_slider.label.set_ha("center")
82
+ self._satellite_orbit_ratio_slider.label.set_va("center")
83
+
84
+ map_image = plt.imread(self.image_path)
85
+ self._ax.imshow(
86
+ map_image,
87
+ extent=(-180.0, 180.0, -90.0, 90.0),
88
+ origin="upper",
89
+ aspect="auto",
90
+ )
91
+ self._ax.set_xlim(-180, 180)
92
+ self._ax.set_ylim(-90, 90)
93
+ self._ax.set_xlabel("Longitude (°)")
94
+ self._ax.set_ylabel("Latitude (°)")
95
+ self._ax.set_title("Satellite Visibility Footprint", fontsize=16, pad=12)
96
+ self._ax.grid(linestyle="--", color="white", alpha=0.35)
97
+
98
+ self._shade_patch = PathPatch(
99
+ Path([[0, 0]]),
100
+ facecolor="red",
101
+ alpha=0.1,
102
+ linewidth=0,
103
+ edgecolor="none",
104
+ )
105
+ self._ax.add_patch(self._shade_patch)
106
+ (line_east,) = self._ax.plot([], [], color="darkred", linewidth=2)
107
+ (line_west,) = self._ax.plot([], [], color="darkred", linewidth=2)
108
+ self._boundary_lines = [line_east, line_west]
109
+
110
+ @staticmethod
111
+ def _world_mask_path(hole_lons: np.ndarray, hole_lats: np.ndarray) -> Path:
112
+ """Build a world-spanning polygon with the visibility footprint cut out."""
113
+ outer = np.array(
114
+ [[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]
115
+ )
116
+ hole = np.column_stack([hole_lons, hole_lats])
117
+ if not np.allclose(hole[0], hole[-1]):
118
+ hole = np.vstack([hole, hole[0]])
119
+
120
+ vertices = np.vstack([outer, hole])
121
+ codes = np.concatenate(
122
+ [
123
+ [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY],
124
+ [Path.MOVETO] + [Path.LINETO] * (len(hole) - 2) + [Path.CLOSEPOLY],
125
+ ]
126
+ )
127
+ return Path(vertices, codes)
128
+
129
+ def _update(self, _val: float | None = None) -> None:
130
+ """Redraw footprint from current slider values."""
131
+ self.visibility.update_params(
132
+ self._radius_ratio_slider.val,
133
+ self._satellite_orbit_ratio_slider.val,
134
+ )
135
+ subpoint_lon = self._long_slider.val
136
+ phi_deg, lon_east, lon_west = self.visibility.footprint_boundaries(
137
+ subpoint_lon, self.footprint_points
138
+ )
139
+ poly_lons, poly_lats = self.visibility.footprint_polygon(
140
+ subpoint_lon, self.footprint_points
141
+ )
142
+
143
+ self._shade_patch.set_path(self._world_mask_path(poly_lons, poly_lats))
144
+ self._boundary_lines[0].set_data(lon_east, phi_deg)
145
+ self._boundary_lines[1].set_data(lon_west, phi_deg)
146
+ self._fig.canvas.draw_idle()
147
+
148
+ def interact(self):
149
+ self._setup_figure()
150
+ self._long_slider.on_changed(self._update)
151
+ self._radius_ratio_slider.on_changed(self._update)
152
+ self._satellite_orbit_ratio_slider.on_changed(self._update)
153
+ self._update()
154
+
155
+ plt.show()
SatVisMars/calc.py ADDED
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Union
4
+
5
+ import numpy as np
6
+
7
+ ArrayLike = Union[float, np.ndarray]
8
+
9
+
10
+ class Visibility:
11
+ """
12
+ Visibility region of an spherical satellite on the equirectangular projected
13
+ sphere host surface using the spherical law of cosines:
14
+
15
+ cos(alpha) = cos(theta) * cos(phi - phi0)
16
+
17
+ where alpha is the maximum elevation angle (latitude) such that the satellite is visible,
18
+ theta is surface latitude, phi is surface longitude, and phi0 is the sub-satellite longitude.
19
+ Note that this assumes perfect equitorial orbit, where the satellite is always on top of the equator.
20
+
21
+ Parameters:
22
+ radius_ratio (float): Ratio of the satellite radius (r) to Mars surface radius (R)
23
+ satellite_orbit_ratio (float): Ratio of the satellite orbit radius (d) to R + r
24
+ """
25
+ # Mess around with these values to see how the visibility footprint changes
26
+ def __init__(self, radius_ratio: float = 0.1, satellite_orbit_ratio: float = 2.0):
27
+ """
28
+ Parameters:
29
+ radius_ratio: Ratio of the satellite radius (r) to the host surface radius (R)
30
+ satellite_orbit_ratio: Ratio of the satellite orbit radius (d) to R + r
31
+ """
32
+ self.radius_ratio = radius_ratio
33
+ self.satellite_orbit_ratio = satellite_orbit_ratio
34
+ self._recalculate()
35
+
36
+ def _recalculate(self) -> None:
37
+ # Distances in unitless form
38
+ self.r = 1.0
39
+ self.R = self.r / self.radius_ratio
40
+ self.d = self.satellite_orbit_ratio * (self.R + self.r)
41
+ self.k = (self.R - self.r) / self.d
42
+ self.theta_max = np.arccos(self.k)
43
+ self.max_visibility_deg = np.degrees(self.theta_max)
44
+
45
+ def update_params(
46
+ self, radius_ratio: float, satellite_orbit_ratio: float
47
+ ) -> None:
48
+ """Recompute geometry when slider values change."""
49
+ self.radius_ratio = radius_ratio
50
+ self.satellite_orbit_ratio = satellite_orbit_ratio
51
+ self._recalculate()
52
+
53
+ def _latitude_grid(self, n_points: int = 500) -> np.ndarray:
54
+ return np.linspace(-self.theta_max, self.theta_max, n_points)
55
+
56
+ def longitude_offset(self, latitudes_rad: np.ndarray) -> np.ndarray:
57
+ """Angular longitude offset from the sub-satellite point at each latitude."""
58
+ cos_delta = np.clip(self.k / np.cos(latitudes_rad), -1.0, 1.0)
59
+ return np.arccos(cos_delta)
60
+
61
+ def footprint_boundaries(
62
+ self, subpoint_longitude_deg: float, n_points: int = 500
63
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
64
+ """
65
+ Return latitude (deg) and east/west boundary longitudes (deg) for a
66
+ given Phobos sub-satellite longitude.
67
+ """
68
+ phi = self._latitude_grid(n_points)
69
+ delta_lambda_deg = np.degrees(self.longitude_offset(phi))
70
+ phi_deg = np.degrees(phi)
71
+
72
+ lon_east = subpoint_longitude_deg + delta_lambda_deg
73
+ lon_west = subpoint_longitude_deg - delta_lambda_deg
74
+ return phi_deg, lon_east, lon_west
75
+
76
+ def footprint_polygon(
77
+ self, subpoint_longitude_deg: float, n_points: int = 500
78
+ ) -> tuple[np.ndarray, np.ndarray]:
79
+ """Return a closed polygon (longitudes, latitudes) tracing the footprint."""
80
+ phi_deg, lon_east, lon_west = self.footprint_boundaries(
81
+ subpoint_longitude_deg, n_points
82
+ )
83
+
84
+ west_edge_lons = lon_west
85
+ west_edge_lats = phi_deg
86
+ east_edge_lons = lon_east[::-1]
87
+ east_edge_lats = phi_deg[::-1]
88
+
89
+ lons = np.concatenate([west_edge_lons, east_edge_lons])
90
+ lats = np.concatenate([west_edge_lats, east_edge_lats])
91
+ return lons, lats
92
+
93
+ @staticmethod
94
+ def lon_lat_to_pixel(
95
+ lon_deg: ArrayLike,
96
+ lat_deg: ArrayLike,
97
+ image_width: int = 2048,
98
+ image_height: int = 1024,
99
+ ) -> tuple[np.ndarray, np.ndarray]:
100
+ """
101
+ Map geographic coordinates to pixel coordinates on an equirectangular
102
+ image whose center is (0° latitude, 0° longitude).
103
+ """
104
+ lon = np.asarray(lon_deg, dtype=float)
105
+ lat = np.asarray(lat_deg, dtype=float)
106
+ x = (lon + 180.0) / 360.0 * image_width
107
+ y = (90.0 - lat) / 180.0 * image_height
108
+ return x, y
109
+
110
+ @staticmethod
111
+ def pixel_to_lon_lat(
112
+ x: ArrayLike,
113
+ y: ArrayLike,
114
+ image_width: int = 2048,
115
+ image_height: int = 1024,
116
+ ) -> tuple[np.ndarray, np.ndarray]:
117
+ """Inverse of lon_lat_to_pixel for equirectangular images."""
118
+ px = np.asarray(x, dtype=float)
119
+ py = np.asarray(y, dtype=float)
120
+ lon = px / image_width * 360.0 - 180.0
121
+ lat = 90.0 - py / image_height * 180.0
122
+ return lon, lat
Binary file
Binary file
Binary file
Binary file
Binary file
SatVisMars/main.py ADDED
@@ -0,0 +1,12 @@
1
+ from .animate_slider import VisibilityWidget
2
+ from .calc import Visibility
3
+
4
+
5
+ def main() -> None:
6
+ visibility = Visibility(radius_ratio=0.1, satellite_orbit_ratio=2.0)
7
+ widget = VisibilityWidget(visibility=visibility)
8
+ widget.interact()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: SatVisMars
3
+ Version: 0.0.1
4
+ Summary: Interactive satellite visibility visualization for Martian moons
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: numpy
9
+ Requires-Dist: matplotlib
10
+ Dynamic: license-file
11
+
12
+ # MartianMoonVis
13
+ An educational tool to visualize the visibility of satellites 🌕 from the surface of Mars.
14
+
15
+ Mars has two moons--Phobos and Deimos—-but you're not restricted to only the parameters of the exisiting moons, use sliders to play around with different combinations of the distance and size of the satellite.
16
+
17
+ ![Widget Screenshot](./img/widget.png)
18
+
19
+ ## How to run
20
+ After cloning the repo, run python main.py
21
+
22
+ `git clone https://github.com/VismayaRP/MartianEclipses`
23
+
24
+ `python main.py`
25
+
26
+
27
+ [![A rectangular badge, half black half purple containing the text made at Code Astro](https://img.shields.io/badge/Made%20at-Code/Astro-blueviolet.svg)](https://semaphorep.github.io/codeastro/)
28
+ ![Static Badge](https://img.shields.io/badge/license-MIT-blue?link=https%3A%2F%2Fopensource.org%2Flicense%2Fmit)
@@ -0,0 +1,15 @@
1
+ SatVisMars/__init__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
2
+ SatVisMars/animate_slider.py,sha256=NaroqeWL53z2qv9JpixvrneTqDE8qFL50_U_S_m9h6E,5667
3
+ SatVisMars/calc.py,sha256=4H9tLEaVhLtuKEKpbuwjaoqVv9sg8K7zYE5Hwti05lw,4690
4
+ SatVisMars/main.py,sha256=Dsih5Y_OMci33ba0hPr878g5tlteR2FF5lVnFpq29sI,284
5
+ SatVisMars/img/Phobos.png,sha256=p84XDfrdzjSUW-iz8G7fyBh2lAeXTXx42bUr1yWXkSc,17190381
6
+ SatVisMars/img/mars_coords.png,sha256=vSJ30dNtH1ACVwc0ZwCt9MHpjERz5IEepk1cuXfWWLM,3183701
7
+ SatVisMars/img/mars_name_coord.png,sha256=dwsljDfpYoOdcmsK7JcpWjbz1ekgzE9b8fsUPb1gaZ0,3281236
8
+ SatVisMars/img/mars_names.png,sha256=HtBFGm6ylvwca52dDM6IVHXcQVV6INAhGag3NnDg3OQ,3258873
9
+ SatVisMars/img/mars_nocoord.png,sha256=nom6wX6G-9DEneGd-U1_0o3yjBE89d3S8_v0bQcL5BU,3166823
10
+ satvismars-0.0.1.dist-info/licenses/LICENSE,sha256=pAZXnNE2dxxwXFIduGyn1gpvPefJtUYOYZOi3yeGG94,1068
11
+ satvismars-0.0.1.dist-info/METADATA,sha256=OFoR4XBYke6A-PhDL2ih9g0RbTHWT5NKHUFVy2pTfxU,1094
12
+ satvismars-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ satvismars-0.0.1.dist-info/entry_points.txt,sha256=Ge8XplPhfY7xuJTiGgscpSVSyWTCUtCebeoxTyRv6yM,52
14
+ satvismars-0.0.1.dist-info/top_level.txt,sha256=zLrkJpfJdphJVC8AqhmNK0PfIo4FFTWRqjO4iGU64l4,11
15
+ satvismars-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ SatVisMars = SatVisMars.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
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 @@
1
+ SatVisMars