Skip to content

Commit 6bd918c

Browse files
authored
Merge pull request #217 from Radio-Spectrum/feat/random_grid_transform
feat: service grid random transformation as parameter option
2 parents e48d0e8 + bd3dc2c commit 6bd918c

File tree

7 files changed

+166
-38
lines changed

7 files changed

+166
-38
lines changed

sharc/parameters/imt/parameters_imt_mss_dc.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ class ParametersServiceGrid(ParametersBase):
136136

137137
country_names: list[str] = field(default_factory=lambda: list([""]))
138138

139+
transform_grid_randomly: bool = False
140+
139141
# margin from inside of border [km]
140142
# if positive, makes border smaller by x km
141143
# if negative, makes border bigger by x km
@@ -197,7 +199,7 @@ def validate(self, ctx: str):
197199
raise ValueError(
198200
f"{ctx}.eligible_sats_margin_from_border needs to be a number")
199201

200-
self.reset_grid(ctx)
202+
self._load_geom_from_file_if_needed(ctx)
201203

202204
super().validate(ctx)
203205

@@ -218,13 +220,30 @@ def load_from_active_sat_conditions(
218220
if self.eligible_sats_margin_from_border is None:
219221
self.eligible_sats_margin_from_border = sat_is_active_if.lat_long_inside_country.margin_from_border
220222

221-
def reset_grid(self, ctx: str, force_update=False):
223+
def reset_grid(
224+
self,
225+
ctx: str,
226+
rng: np.random.RandomState,
227+
force_update=False,
228+
):
222229
"""
223230
After creating grid, there are some features that can only be implemented
224-
with knowledge of other parts of the simulator. This method's purpose is
225-
to run only once at the start of the simulation
231+
with knowledge of other parts of the simulator.
226232
"""
227-
if self.lon_lat_grid is not None and not force_update:
233+
self._load_geom_from_file_if_needed(ctx, force_update)
234+
235+
self.lon_lat_grid = generate_grid_in_multipolygon(
236+
self.grid_borders_polygon,
237+
self.beam_radius,
238+
self.transform_grid_randomly,
239+
rng
240+
)
241+
242+
self.ecef_grid = lla2ecef(
243+
self.lon_lat_grid[1], self.lon_lat_grid[0], 0)
244+
245+
def _load_geom_from_file_if_needed(self, ctx: str, force_update=False):
246+
if self.eligibility_polygon is not None and not force_update:
228247
return
229248
filtered_gdf = load_gdf(
230249
self.country_shapes_filename,
@@ -239,17 +258,13 @@ def reset_grid(self, ctx: str, force_update=False):
239258
shrinked = shrink_countries_by_km(
240259
filtered_gdf.geometry.values, self.grid_margin_from_border
241260
)
242-
polygon = shp.ops.unary_union(shrinked)
243-
assert polygon.is_valid, shp.validation.explain_validity(polygon)
244-
assert not polygon.is_empty, "Can't have a empty polygon as filter"
261+
self.grid_borders_polygon = shp.ops.unary_union(shrinked)
245262

246-
self.lon_lat_grid = generate_grid_in_multipolygon(
247-
polygon,
248-
self.beam_radius
249-
)
263+
assert self.grid_borders_polygon.is_valid, \
264+
shp.validation.explain_validity(self.grid_borders_polygon)
250265

251-
self.ecef_grid = lla2ecef(
252-
self.lon_lat_grid[1], self.lon_lat_grid[0], 0)
266+
assert not self.grid_borders_polygon.is_empty, \
267+
"Can't have a empty grid_borders_polygon as filter"
253268

254269
self.eligibility_polygon = shp.ops.unary_union(shrink_countries_by_km(
255270
filtered_gdf.geometry.values, self.eligible_sats_margin_from_border

sharc/satellite/utils/sat_utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ def calc_elevation(Le: np.ndarray,
115115
return np.degrees(elev_angle)
116116

117117

118+
def haversine(
119+
lon1: np.ndarray,
120+
lat1: np.ndarray,
121+
lon2: np.ndarray,
122+
lat2: np.ndarray,
123+
R=EARTH_RADIUS_M
124+
):
125+
"""Calculates great-circle distance between 2 points on the surface of Earth.
126+
Considers spherical earth.
127+
128+
Returns np.ndarray with N distances in meters
129+
"""
130+
lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
131+
dlon = lon2 - lon1
132+
dlat = lat2 - lat1
133+
a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2
134+
return 2 * R * np.arcsin(np.sqrt(a))
135+
136+
118137
if __name__ == "__main__":
119138
r1 = ecef2lla(7792.1450, 0, 0)
120139
print(r1)

sharc/support/sharc_geom.py

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,8 @@ def shrink_countries_by_km(
608608
def generate_grid_in_polygon(
609609
polygon: shp.geometry.Polygon,
610610
hexagon_radius: float,
611+
rotation_deg: typing.Optional[float] = None,
612+
translation: typing.Optional[typing.Tuple[float, float]] = None,
611613
):
612614
"""Generate a hexagonal grid inside a polygon and return points in EARTH_DEFAULT_CRS.
613615
@@ -621,6 +623,11 @@ def generate_grid_in_polygon(
621623
Polygon to fill with a grid.
622624
hexagon_radius : float
623625
Radius of the hexagons in the grid (in meters).
626+
rotation_deg : float or None, optional
627+
Rotation of the grid in degrees (default: None, no rotation).
628+
translation : tuple of float or None, optional
629+
Translation of the grid in meters (dx, dy) (default: None, no translation).
630+
The translation must not exceed the grid spacing in magnitude.
624631
625632
Returns
626633
-------
@@ -644,14 +651,18 @@ def generate_grid_in_polygon(
644651
# Transform to projection where unit is meters
645652
polygon_proj = shp.ops.transform(to_proj, polygon)
646653

647-
# create a bounding box, afterwards we filter to polygon site
648-
minx, miny, maxx, maxy = polygon_proj.bounds
654+
# Determine x/y spacing
649655
x_spacing = 3 * hexagon_radius
650656
y_spacing = hexagon_radius * np.sqrt(3) / 2
651657

652-
x_vals = np.arange(minx, maxx + x_spacing, x_spacing)
658+
# Create a bounding circle, afterwards we filter to polygon site
659+
cx, cy = polygon_proj.centroid.coords[0]
660+
minx, miny, maxx, maxy = polygon_proj.bounds
661+
bbox_diag = np.hypot(maxx - minx, maxy - miny)
662+
bound_radius = bbox_diag / 2 + 3 * hexagon_radius
653663

654-
y_vals = np.arange(miny, maxy + y_spacing, y_spacing)
664+
x_vals = np.arange(cx - bound_radius, cx + bound_radius + x_spacing, x_spacing)
665+
y_vals = np.arange(cy - bound_radius, cy + bound_radius + y_spacing, y_spacing)
655666

656667
x_vals, y_vals = np.meshgrid(x_vals, y_vals)
657668

@@ -661,6 +672,20 @@ def generate_grid_in_polygon(
661672
x_vals = x_vals.ravel()
662673
y_vals = y_vals.ravel()
663674

675+
if rotation_deg is not None:
676+
theta = np.deg2rad(rotation_deg)
677+
cos_t, sin_t = np.cos(theta), np.sin(theta)
678+
x_rot = cos_t * (x_vals - cx) - sin_t * (y_vals - cy) + cx
679+
y_rot = sin_t * (x_vals - cx) + cos_t * (y_vals - cy) + cy
680+
x_vals, y_vals = x_rot, y_rot
681+
682+
if translation is not None:
683+
dx, dy = translation
684+
if abs(dx) > x_spacing or abs(dy) > y_spacing:
685+
raise ValueError("generate_grid_in_polygon.translation (dx, dy) must not exceed grid spacing (x_spacing, y_spacing) in magnitude")
686+
x_vals += dx
687+
y_vals += dy
688+
664689
# Return to EARTH_DEFAULT_CRS
665690
xt, yt = from_proj(x_vals, y_vals)
666691

@@ -676,34 +701,66 @@ def generate_grid_in_polygon(
676701

677702
def generate_grid_in_multipolygon(
678703
poly: typing.Union[shp.geometry.MultiPolygon, shp.geometry.Polygon],
679-
km: float
704+
km: float,
705+
random_transform_on_grid: bool = False,
706+
rng: np.random.RandomState = None,
680707
) -> list[shp.geometry.MultiPolygon]:
681-
"""Generate a grid in a MultiPolygon or Polygon, shrinking each by a given number of kilometers.
708+
"""Generate a hexagonal grid in a MultiPolygon or Polygon,
709+
considering a hexagon radius in km.
682710
683-
For each polygon, create a grid and return a single 2xN array of longitudes and latitudes.
711+
For each polygon, create a grid and return a single 2xN array of longitudes and latitudes
712+
containing all grids.
684713
685714
Parameters
686715
----------
687716
poly : typing.Union[shp.geometry.MultiPolygon, shp.geometry.Polygon]
688717
The MultiPolygon or Polygon to process.
689718
km : float
690-
Number of kilometers to shrink each polygon by.
719+
Hexagon radius in km
720+
random_transform_on_grid : bool, optional
721+
Whether to apply a random rotation and translation to the grid (default: False).
722+
rng : np.random.RandomState, optional
723+
Random number generator to use if random_transform_on_grid is True (default: None).
691724
692725
Returns
693726
-------
694727
np.ndarray
695728
2xN array: first row is longitudes, second row is latitudes.
696729
"""
730+
if random_transform_on_grid:
731+
assert rng is not None
732+
697733
lons = []
698734
lats = []
699735

700736
if poly.geom_type == 'Polygon':
701-
x, y = generate_grid_in_polygon(poly, km)
737+
if random_transform_on_grid:
738+
x, y = generate_grid_in_polygon(
739+
poly, km,
740+
rng.uniform(-180., 180.),
741+
(
742+
3 * rng.uniform(-km, km),
743+
rng.uniform(-km, km) * np.sqrt(3) / 2
744+
)
745+
)
746+
else:
747+
x, y = generate_grid_in_polygon(poly, km)
702748
lons.extend(x)
703749
lats.extend(y)
704750
elif poly.geom_type == 'MultiPolygon':
705751
for p in poly.geoms:
706-
x, y = generate_grid_in_polygon(p, km)
752+
if random_transform_on_grid:
753+
x, y = generate_grid_in_polygon(
754+
p, km,
755+
rng.uniform(-180., 180.),
756+
(
757+
3 * rng.uniform(-km, km),
758+
rng.uniform(-km, km) * np.sqrt(3) / 2
759+
)
760+
)
761+
else:
762+
x, y = generate_grid_in_polygon(p, km)
763+
707764
lons.extend(x)
708765
lats.extend(y)
709766

sharc/topology/topology_imt_mss_dc.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,10 @@ def get_satellite_pointing(
373373
(before sharc's coordinate transformation, with distances in km)
374374
"""
375375
if orbit_params.beam_positioning.type == "SERVICE_GRID":
376-
orbit_params.beam_positioning.service_grid.validate("service_grid")
376+
# TODO: remove this reset_grid call from here.
377+
# Since it should be called once per drop, it shouldn't
378+
# be this deep in the program
379+
orbit_params.beam_positioning.service_grid.reset_grid("service_grid", random_number_gen)
377380

378381
# 2xN, (lon, lat)
379382
grid = orbit_params.beam_positioning.service_grid.lon_lat_grid

tests/parameters/parameters_for_testing.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ imt:
232232
min: 0.0
233233
max: 66.1
234234
service_grid:
235+
transform_grid_randomly: true # add per drop rand rotation + transl. to grid
235236
# by default this already gets shapefile from natural earth
236237
country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp
237238
# By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country`
@@ -1008,6 +1009,7 @@ mss_d2d:
10081009
min: 0.0
10091010
max: 66.1
10101011
service_grid:
1012+
transform_grid_randomly: true # add per drop rand rotation + transl. to grid
10111013
# by default this already gets shapefile from natural earth
10121014
country_shapes_filename: sharc/data/countries/ne_110m_admin_0_countries.shp
10131015
# By default, these are the same as taken from `sat_is_active_if.lat_long_inside_country`

tests/parameters/test_parameters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ def test_parameters_imt(self):
280280
self.assertEqual(
281281
self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.beam_radius,
282282
19000)
283+
self.assertEqual(
284+
self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.transform_grid_randomly,
285+
True)
283286
self.assertEqual(
284287
self.parameters.imt.topology.mss_dc.beam_positioning.service_grid.grid_margin_from_border,
285288
0.11)
@@ -630,6 +633,9 @@ def test_parametes_mss_d2d(self):
630633
self.assertEqual(
631634
self.parameters.mss_d2d.beam_positioning.service_grid.beam_radius,
632635
19001)
636+
self.assertEqual(
637+
self.parameters.mss_d2d.beam_positioning.service_grid.transform_grid_randomly,
638+
True)
633639
self.assertEqual(
634640
self.parameters.mss_d2d.beam_positioning.service_grid.grid_margin_from_border,
635641
0.11)

0 commit comments

Comments
 (0)