Skip to content

equirectangular

cosmica.visualization.equirectangular

__all__ module-attribute

__all__ = [
    "draw_countries",
    "draw_coverage_area",
    "draw_lat_lon_grid",
    "draw_snapshot",
    "draw_snapshot_movie",
    "draw_urban_areas",
]

logger module-attribute

logger = getLogger(__name__)

draw_countries

draw_countries(
    *,
    ax: Axes,
    zorder: int = 0,
    ocean_color: ColorType = "#caecfc",
    countries_color: ColorType = "#fdfbe6"
) -> Axes
Source code in src/cosmica/visualization/equirectangular.py
54
55
56
57
58
59
60
61
62
63
64
65
def draw_countries(
    *,
    ax: Axes,
    zorder: int = 0,
    ocean_color: ColorType = "#caecfc",
    countries_color: ColorType = "#fdfbe6",
) -> Axes:
    ax.set_facecolor(ocean_color)
    df_countries = _load_shapefile_from_assets_dir("ne_50m_admin_0_countries_lakes.shp")
    df_countries.plot(ax=ax, color=countries_color, edgecolor="grey", linewidth=0.5, zorder=zorder)
    ax.grid(color="black", alpha=0.2)
    return ax

draw_coverage_area

draw_coverage_area(
    *,
    dynamics_data: DynamicsData[ConstellationSatellite[T]],
    ax: Axes,
    min_elevation: float,
    face_color: str = "red",
    face_alpha: float = 0.25,
    draw_edges: bool = True,
    edge_color: str = "red",
    edge_alpha: float = 1.0,
    edge_linewidth: float | None = None,
    edge_linestyle: str | None = None
) -> Axes

Draw the coverage area of the satellites in the constellation.

Note that this function is not optimized for performance, and may take a few minutes to complete.

PARAMETER DESCRIPTION
dynamics_data

Dynamics data with no time dimension.

TYPE: DynamicsData[ConstellationSatellite[T]]

ax

Matplotlib axes.

TYPE: Axes

min_elevation

Minimum elevation angle in radians.

TYPE: float

face_color

Color of the coverage area. Used as the colors parameter of contourf function.

TYPE: str DEFAULT: 'red'

face_alpha

Transparency of the coverage area. Used as the alpha parameter of contourf function.

TYPE: float DEFAULT: 0.25

draw_edges

Whether to draw the edges of the coverage area

TYPE: bool DEFAULT: True

edge_color

Color of the coverage area edge. Used as the colors parameter of contour function.

TYPE: str DEFAULT: 'red'

edge_alpha

Transparency of the coverage area edge. Used as the alpha parameter of contour function.

TYPE: float DEFAULT: 1.0

edge_linewidth

Width of the coverage area edge. Used as the linewidths parameter of contour function.

TYPE: float | None DEFAULT: None

edge_linestyle

Style of the coverage area edge. Used as the linestyles parameter of contour function.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Axes

Matplotlib axes.

Source code in src/cosmica/visualization/equirectangular.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
def draw_coverage_area[T](
    *,
    dynamics_data: Annotated[
        DynamicsData[ConstellationSatellite[T]],
        Doc("Dynamics data with no time dimension."),
    ],
    ax: Annotated[Axes, Doc("Matplotlib axes.")],
    min_elevation: Annotated[float, Doc("Minimum elevation angle in radians.")],
    face_color: Annotated[
        str,
        Doc("Color of the coverage area. Used as the `colors` parameter of `contourf` function."),
    ] = "red",
    face_alpha: Annotated[
        float,
        Doc("Transparency of the coverage area. Used as the `alpha` parameter of `contourf` function."),
    ] = 0.25,
    draw_edges: Annotated[bool, Doc("Whether to draw the edges of the coverage area")] = True,
    edge_color: Annotated[
        str,
        Doc("Color of the coverage area edge. Used as the `colors` parameter of `contour` function."),
    ] = "red",
    edge_alpha: Annotated[
        float,
        Doc("Transparency of the coverage area edge. Used as the `alpha` parameter of `contour` function."),
    ] = 1.0,
    edge_linewidth: Annotated[
        float | None,
        Doc("Width of the coverage area edge. Used as the `linewidths` parameter of `contour` function."),
    ] = None,
    edge_linestyle: Annotated[
        str | None,
        Doc("Style of the coverage area edge. Used as the `linestyles` parameter of `contour` function."),
    ] = None,
) -> Annotated[Axes, Doc("Matplotlib axes.")]:
    """Draw the coverage area of the satellites in the constellation.

    Note that this function is not optimized for performance, and may take a few minutes to complete.
    """
    latitudes = np.radians(np.linspace(-90, 90, 180))
    longitudes = np.radians(np.linspace(-180, 180, 360))
    lat_grid, lon_grid = np.meshgrid(latitudes, longitudes)

    elevation_angles = {}
    for sat, pos_ecef in dynamics_data.satellite_position_ecef.items():
        elevation_angles[sat] = np.zeros_like(lat_grid)
        for i in range(len(latitudes)):
            for j in range(len(longitudes)):
                _azimuth, elevation, _srange = ecef2aer(
                    x=pos_ecef[0],
                    y=pos_ecef[1],
                    z=pos_ecef[2],
                    lat0=latitudes[i],
                    lon0=longitudes[j],
                    h0=0,
                    deg=False,
                )
                elevation_angles[sat][j, i] = elevation

    for elevation in elevation_angles.values():
        cs = ax.contourf(
            np.degrees(lon_grid),
            np.degrees(lat_grid),
            np.degrees(elevation),
            levels=[np.degrees(min_elevation), np.inf],
            colors=face_color,
            alpha=face_alpha,
        )

        if draw_edges:
            ax.contour(
                cs,
                levels=cs.levels,
                colors=edge_color,
                alpha=edge_alpha,
                linewidths=edge_linewidth,
                linestyles=edge_linestyle,
            )

    return ax

draw_lat_lon_grid

draw_lat_lon_grid(*, ax: Axes) -> Axes
Source code in src/cosmica/visualization/equirectangular.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def draw_lat_lon_grid(*, ax: Axes) -> Axes:
    ax.set_ylabel("Latitude")
    ax.set_yticks(np.arange(-90, 90 + 1, 30))
    ax.set_yticklabels([f"{ytick}°" for ytick in np.arange(-90, 90 + 1, 30)])
    ax.set_xlabel("Longitude")
    ax.set_xticks(np.arange(-180, 180 + 1, 30))
    ax.set_xticklabels([f"{xtick}°" for xtick in np.arange(-180, 180 + 1, 30)])

    ax.set_ylim(-90, 90)
    ax.set_xlim(-180, 180)

    ax.set_aspect("equal")

    return ax

draw_snapshot

draw_snapshot(
    *,
    graph: Graph,
    dynamics_data: DynamicsData[Any],
    ax: Axes,
    with_labels: bool = False,
    focus_edges_list: (
        list[set[tuple[Node, Node]]] | None
    ) = None,
    focus_edges_label_list: list[str] | None = None
) -> Axes
Source code in src/cosmica/visualization/equirectangular.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
def draw_snapshot(  # noqa: C901, PLR0915
    *,
    graph: nx.Graph,
    dynamics_data: DynamicsData[Any],
    ax: Axes,
    with_labels: bool = False,
    focus_edges_list: list[set[tuple[Node, Node]]] | None = None,
    focus_edges_label_list: list[str] | None = None,
) -> Axes:
    # None のときのデフォルト値設定
    if focus_edges_list is None:
        focus_edges_list = []
    if focus_edges_label_list is None:
        # focus_edges と同じ数だけデフォルトのラベルを作成する
        focus_edges_label_list = [f"Focus edges {i}" for i in range(len(focus_edges_list))]

    if len(focus_edges_list) != len(focus_edges_label_list):
        msg = "focus_edges と focus_edges_label の要素数が一致していません。"
        raise ValueError(msg)

    constellation_satellites_to_draw = {node for node in graph.nodes if isinstance(node, ConstellationSatellite)}
    pos_constellation = {
        satellite: np.degrees(
            np.asarray(ecef2geodetic(*dynamics_data.satellite_position_ecef[satellite], deg=False))[(1, 0),],
        )
        for satellite in constellation_satellites_to_draw
    }

    user_satellites_to_draw = {node for node in graph.nodes if isinstance(node, UserSatellite)}
    pos_user_satellites = {
        satellite: np.degrees(
            np.asarray(ecef2geodetic(*dynamics_data.satellite_position_ecef[satellite], deg=False))[(1, 0),],
        )
        for satellite in user_satellites_to_draw
    }

    gateways = {node for node in graph.nodes if isinstance(node, Gateway)}
    pos_gateways = {gateway: np.degrees(np.array([gateway.longitude, gateway.latitude])) for gateway in gateways}

    on_ground_users = {node for node in graph.nodes if isinstance(node, StationaryOnGroundUser)}
    pos_ogu = {
        on_ground_user: np.degrees(np.array([on_ground_user.longitude, on_ground_user.latitude]))
        for on_ground_user in on_ground_users
    }

    internets = {node for node in graph.nodes if isinstance(node, Internet)}
    pos_internets = {internet: [np.nan, np.nan] for internet in internets}

    pos = pos_constellation | pos_user_satellites | pos_gateways | pos_ogu | pos_internets
    nodes_to_draw: set[Node] = constellation_satellites_to_draw | user_satellites_to_draw | gateways | on_ground_users

    with preserve_tick_params(ax):
        # Draw nodes
        # Draw constellation satellites
        nx.draw_networkx_nodes(
            graph,
            pos=pos,
            ax=ax,
            nodelist=set(pos_constellation),
            node_size=100,
            node_color="tab:blue",
            node_shape="s",
            alpha=0.7,
            label="Constellation satellite",
        )
        # Draw user satellites
        nx.draw_networkx_nodes(
            graph,
            pos=pos,
            ax=ax,
            nodelist=set(pos_user_satellites),
            node_size=120,
            node_color="tab:purple",
            node_shape="D",
            alpha=0.7,
            label="User satellite",
        )
        # Draw gateways
        nx.draw_networkx_nodes(
            graph,
            pos,
            nodelist=gateways,
            node_shape="^",
            node_color="tab:orange",
            node_size=150,
            alpha=0.7,
            label="Gateway",
            ax=ax,
        )
        # Draw on-ground users
        nx.draw_networkx_nodes(
            graph,
            pos,
            nodelist=on_ground_users,
            node_shape="o",
            node_color="tab:green",
            node_size=150,
            alpha=0.7,
            label="On-ground user",
            ax=ax,
        )
        if with_labels:
            # Draw labels of constellation
            nx.draw_networkx_labels(
                graph,
                pos,
                labels={node: str(node).split("-")[-1] for node in set(pos_constellation)},
                font_size=5,
                font_color="black",
                font_family="sans-serif",
                font_weight="normal",
                alpha=0.8,
                ax=ax,
            )
            # Draw labels of user satellites
            nx.draw_networkx_labels(
                graph,
                pos,
                labels={node: str(node).split("-")[-1] for node in set(pos_user_satellites)},
                font_size=5,
                font_color="purple",
                font_family="sans-serif",
                font_weight="normal",
                alpha=0.8,
                ax=ax,
            )
            # Draw labels of gateways
            nx.draw_networkx_labels(
                graph,
                pos,
                labels={node: str(node).split("-")[-1] for node in gateways},
                font_size=7,
                font_color="blue",
                font_family="sans-serif",
                font_weight="normal",
                alpha=0.8,
                ax=ax,
            )

        # Draw edges
        graph_pos_corrected = copy.deepcopy(graph)

        focus_edges_corrected_list = [focus_edges.copy() for focus_edges in focus_edges_list]

        # Re-draw the edges with the correct direction around the globe
        for u, v in graph.edges():
            if not (u in nodes_to_draw and v in nodes_to_draw):
                continue
            if abs(pos[u][0] - pos[v][0]) > 180:
                graph_pos_corrected.remove_edge(u, v)
                u_to_east = pos[u][0] < pos[v][0]

                dummy_u = _dummy_class_creator(type(u)).from_real(u)  # type: ignore[attr-defined,arg-type]
                pos[dummy_u] = pos[u].copy()
                pos[dummy_u][0] = pos[dummy_u][0] + 360 if u_to_east else pos[dummy_u][0] - 360

                dummy_v = _dummy_class_creator(type(v)).from_real(v)  # type: ignore[attr-defined,arg-type]
                pos[dummy_v] = pos[v].copy()
                pos[dummy_v][0] = pos[dummy_v][0] + 360 if not u_to_east else pos[dummy_v][0] - 360

                graph_pos_corrected.add_edge(u, dummy_v)
                graph_pos_corrected.add_edge(dummy_u, v)

                for focus_edges, focus_edges_corrected in zip(
                    focus_edges_list,
                    focus_edges_corrected_list,
                    strict=False,
                ):
                    for edge in focus_edges:
                        if (u, v) == edge or (u, v) == edge[::-1]:
                            focus_edges_corrected.remove(edge)
                            focus_edges_corrected.add((u, dummy_v))
                            focus_edges_corrected.add((dummy_u, v))

        intra_plane_edges = {
            (u, v)
            for u, v in graph_pos_corrected.edges()
            if isinstance(u, ConstellationSatellite)
            and isinstance(v, ConstellationSatellite)
            and u.id.plane_id == v.id.plane_id
        }
        inter_plane_edges = {
            (u, v)
            for u, v in graph_pos_corrected.edges()
            if isinstance(u, ConstellationSatellite)
            and isinstance(v, ConstellationSatellite)
            and u.id.plane_id != v.id.plane_id
        }
        constellation_gateway_edges = {
            (u, v)
            for u, v in graph_pos_corrected.edges()
            if (isinstance(u, ConstellationSatellite) and isinstance(v, Gateway))
            or (isinstance(u, Gateway) and isinstance(v, ConstellationSatellite))
        }
        constellation_usersatellite_edges = {
            (u, v)
            for u, v in graph_pos_corrected.edges()
            if (isinstance(u, ConstellationSatellite) and isinstance(v, UserSatellite))
            or (isinstance(u, UserSatellite) and isinstance(v, ConstellationSatellite))
        }
        inter_gateways_edges = {
            (u, v) for u, v in graph_pos_corrected.edges() if isinstance(u, Gateway) and isinstance(v, Gateway)
        }
        other_edges_to_draw = (
            {(u, v) for u, v in graph_pos_corrected.edges() if u in nodes_to_draw and v in nodes_to_draw}
            - intra_plane_edges
            - inter_plane_edges
            - constellation_gateway_edges
            - constellation_usersatellite_edges
            - inter_gateways_edges
        )

        # Draw intra-plane edges
        h1 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=intra_plane_edges,
            width=1,
            edge_color="tab:purple",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Intra-plane links",
        )
        _add_legend_to_edges(h1, "Intra-plane links", ax=ax)

        # Draw inter-plane edges
        h2 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=inter_plane_edges,
            width=1,
            edge_color="tab:green",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Inter-plane links",
        )
        _add_legend_to_edges(h2, "Inter-plane links", ax=ax)

        # Draw constellation-gateway edges
        h3 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=constellation_gateway_edges,
            width=1,
            edge_color="tab:brown",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Feeder links",
        )
        _add_legend_to_edges(h3, "Feeder links", ax=ax)

        # Draw constellation-usersatellite edges
        h4 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=constellation_usersatellite_edges,
            width=1,
            edge_color="tab:cyan",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Service links",
        )
        _add_legend_to_edges(h4, "Service links", ax=ax)

        # Draw inter-gateways edges
        h5 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=inter_gateways_edges,
            width=1,
            edge_color="tab:olive",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Gateway links",
        )
        _add_legend_to_edges(h5, "Gateway links", ax=ax)

        # Draw other edges
        h6 = nx.draw_networkx_edges(
            graph_pos_corrected,
            pos,
            edgelist=other_edges_to_draw,
            width=1,
            edge_color="tab:gray",
            ax=ax,
            arrows=True,
            alpha=0.7,
            arrowstyle="<|-|>",
            label="Other links",
        )
        _add_legend_to_edges(h6, "Other links", ax=ax)

        for focus_edges_corrected, focus_edges_label in zip(
            focus_edges_corrected_list,
            focus_edges_label_list,
            strict=False,
        ):
            if not focus_edges_corrected:
                continue  # 空集合ならスキップ
            h7 = nx.draw_networkx_edges(
                graph_pos_corrected,
                pos,
                edgelist=focus_edges_corrected,
                width=3,
                edge_color="tab:red",
                ax=ax,
                arrows=True,
                alpha=0.7,
                arrowstyle="-",
                label=focus_edges_label,
            )
            _add_legend_to_edges(h7, focus_edges_label, ax=ax)

    return ax

draw_snapshot_movie

draw_snapshot_movie(
    *,
    graph: list[Graph],
    time_array: NDArray,
    dynamics_data: DynamicsData,
    time_index_for_plot: NDArray,
    fig: Figure,
    ax: Axes,
    interval_ms: int = 100
) -> FuncAnimation

Draw a snapshot of the network graph for a movie.

Source code in src/cosmica/visualization/equirectangular.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
def draw_snapshot_movie(
    *,
    graph: list[nx.Graph],
    time_array: npt.NDArray,
    dynamics_data: DynamicsData,
    time_index_for_plot: npt.NDArray,
    fig: Figure,
    ax: Axes,
    interval_ms: int = 100,
) -> FuncAnimation:
    """Draw a snapshot of the network graph for a movie."""

    def update(frame: int):  # noqa: ANN202
        ax.clear()

        time_index = time_index_for_plot[frame]

        title = f"Time: {np.datetime_as_string(time_array[time_index], unit='ms').split('T')[1]}"
        ax.set_title(title)

        draw_lat_lon_grid(ax=ax)
        draw_countries(ax=ax)
        draw_snapshot(
            graph=graph[time_index],
            dynamics_data=dynamics_data[time_index],
            ax=ax,
            with_labels=False,
        )
        ax.legend(loc="lower left")

    return FuncAnimation(fig, update, frames=len(time_index_for_plot), interval=interval_ms)

draw_urban_areas

draw_urban_areas(*, ax: Axes, zorder: int = 1) -> Axes
Source code in src/cosmica/visualization/equirectangular.py
68
69
70
71
def draw_urban_areas(*, ax: Axes, zorder: int = 1) -> Axes:
    df_urban_areas = _load_shapefile_from_assets_dir("ne_50m_urban_areas.shp")
    df_urban_areas.plot(ax=ax, color="red", zorder=zorder)
    return ax