Skip to content

Commit d4349b4

Browse files
committed
Bugfix polygon vertices ordering
Fixes an issue where the edge vertices ordering can be incorrectly flipped. Change also removes unnecessary arguments from `discretize_all_edge_boundaries`, and propagates this change to methods that call this function.
1 parent 54fd444 commit d4349b4

File tree

4 files changed

+163
-28
lines changed

4 files changed

+163
-28
lines changed

regions/_utils/spherical_helpers.py

Lines changed: 160 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -316,22 +316,38 @@ def _add_tan_pts_if_in_pa_range(
316316
return coord_list
317317

318318

319-
def _validate_vertices_ordering(verts, gc, centroid):
320-
pas_verts = gc.center.position_angle(verts).to(u.deg)
319+
def _check_edge_lt_pi(pas_verts, wrap_ang):
320+
pas_verts_wrap = pas_verts.wrap_at(wrap_ang)
321+
322+
is_valid_arc_length = ((pas_verts_wrap[1] - pas_verts_wrap[0]).to(u.deg) <= 180 * u.deg)
323+
324+
return pas_verts_wrap, is_valid_arc_length
325+
326+
327+
def _validate_vertices_ordering(verts, gc, gc_center=None):
328+
if gc is not None:
329+
gc_center = gc.center
330+
pas_verts = gc_center.position_angle(verts).to(u.deg)
321331

322-
# Check ordering of vertices pas:
323-
pa_centroid = gc.center.position_angle(centroid).to(u.deg)
324332
wrap_ang = pas_verts[0]
325-
pas_verts_wrap = pas_verts.wrap_at(wrap_ang)
326-
pa_centroid_wrap = pa_centroid.wrap_at(wrap_ang)
327-
in_range_lon = (pa_centroid_wrap >= pas_verts_wrap[0]) & (
328-
pa_centroid_wrap <= pas_verts_wrap[1]
329-
)
330-
if not in_range_lon:
331-
# If not in range, invert the vertices PA ordering:
332-
pas_verts = pas_verts[::-1]
333-
wrap_ang = pas_verts[0]
334-
pas_verts_wrap = pas_verts.wrap_at(wrap_ang)
333+
334+
# Check ordering of vertices pas:
335+
336+
# Principle: ALL EDGES must be <= 180 deg of length
337+
# So difference of pa[1] - pa[0] <= 180 deg
338+
# If not, swap the order.
339+
340+
pas_verts_wrap, is_valid_arc_length = _check_edge_lt_pi(pas_verts, wrap_ang)
341+
342+
if not is_valid_arc_length:
343+
wrap_ang_opp = pas_verts[-1]
344+
pas_verts_wrap_opp, is_valid_arc_length_opp = _check_edge_lt_pi(
345+
pas_verts[::-1], wrap_ang_opp
346+
)
347+
if is_valid_arc_length_opp:
348+
return pas_verts_wrap_opp, wrap_ang_opp
349+
350+
raise ValueError('Invalid arc') # should never occur
335351

336352
return pas_verts_wrap, wrap_ang
337353

@@ -400,7 +416,7 @@ def get_edge_raw_lonlat_bounds_circ_edges(vertices, centroid, gcs):
400416
# PAs from gc center to vertices:
401417
verts = SkyCoord(np.concatenate([[vertices[i - 1]], [vertices[i]]]))
402418

403-
pas_verts_wrap, wrap_ang = _validate_vertices_ordering(verts, gc, centroid)
419+
pas_verts_wrap, wrap_ang = _validate_vertices_ordering(verts, gc)
404420

405421
# --------------------------------------------------------
406422
# Latitude tangent points from bound circle as len 2 SkyCoord:
@@ -409,7 +425,8 @@ def get_edge_raw_lonlat_bounds_circ_edges(vertices, centroid, gcs):
409425
tan_lat_pts = _get_circle_latitude_tangent_points(gc.center, gc.radius)
410426

411427
lats_list = _add_tan_pts_if_in_pa_range(
412-
lats_list, tan_lat_pts, gc, wrap_ang, pas_verts_wrap, coord='lat'
428+
lats_list, tan_lat_pts, gc, wrap_ang, pas_verts_wrap,
429+
coord='lat'
413430
)
414431

415432
# --------------------------------------------------------
@@ -419,7 +436,8 @@ def get_edge_raw_lonlat_bounds_circ_edges(vertices, centroid, gcs):
419436
tan_lon_pts = _get_circle_longitude_tangent_points(gc.center, gc.radius)
420437
if tan_lon_pts is not None:
421438
lons_list = _add_tan_pts_if_in_pa_range(
422-
lons_list, tan_lon_pts, gc, wrap_ang, pas_verts_wrap, coord='lon'
439+
lons_list, tan_lon_pts, gc, wrap_ang, pas_verts_wrap,
440+
coord='lon'
423441
)
424442

425443
lons_arr = [lons_list.min(), lons_list.max()]
@@ -432,7 +450,90 @@ def get_edge_raw_lonlat_bounds_circ_edges(vertices, centroid, gcs):
432450
return Longitude(lons_arr).to(u.deg), Latitude(lats_arr).to(u.deg)
433451

434452

435-
def _discretize_edge_boundary(vertices, circ, centroid, n_points):
453+
def get_line_edge_raw_lonlat_bounds_circ_edges(coords, center):
454+
"""
455+
Get the raw longitude / latitude bounds from the circle edges of
456+
spherical sky region.
457+
458+
Parameters
459+
----------
460+
coords : `~astropy.coordinates.SkyCoord`
461+
The start, end of the line as a SkyCoord.
462+
463+
center : `~astropy.coordinates.SkyCoord`
464+
The line center as a SkyCoord.
465+
466+
Returns
467+
-------
468+
longitude_limits, latitude_limits: `~astropy.coordinates.Longitude`
469+
Length two |Longitude| and |Latitude| with the computed
470+
longitude/latitude bounds from the polygon edges.
471+
"""
472+
# Consider lon/lat of vertices: may produce min/max bounds:
473+
vrepr = coords.represent_as('spherical')
474+
475+
# Special handling:
476+
# Exclude vertices from longitude bounds if any is on a pole
477+
lons_list = []
478+
lats_list = []
479+
for v in vrepr:
480+
if np.abs(v.lat.to(u.deg).deg) < 90:
481+
lons_list.append(v.lon)
482+
lats_list.append(v.lat)
483+
lons_list = Longitude(lons_list, unit=u.radian)
484+
lats_list = Latitude(lats_list, unit=u.radian)
485+
486+
# Need to also check for "out bulging" from edges,
487+
# as far as latitude/lon bounds:
488+
# eg, 2 vertices at ~60deg: the gc arc goes ~closer to the pole;
489+
# a circle centered close to the pole but not extending over it:
490+
# WIDE lon bounds
491+
492+
gc_center = cross_product_skycoord2skycoord(coords[0], coords[1])
493+
gc_radius = 90 * u.deg
494+
495+
pas_verts_wrap, wrap_ang = _validate_vertices_ordering(
496+
coords, None, gc_center=gc_center
497+
)
498+
499+
# --------------------------------------------------------
500+
# Latitude tangent points from bound circle as len 2 SkyCoord:
501+
# Only add to the list if the tangent point is located along this edge
502+
tan_lat_pts = _get_circle_latitude_tangent_points(gc_center, gc_radius)
503+
504+
lats_list = _add_tan_pts_if_in_pa_range(
505+
lats_list, tan_lat_pts, None, wrap_ang, pas_verts_wrap, coord='lat',
506+
gc_center=gc_center
507+
)
508+
509+
# --------------------------------------------------------
510+
# Longitude tangent points from bound circle as len 2 SkyCoord:
511+
# Only add to the list if the tangent point is located along this edge
512+
513+
tan_lon_pts = _get_circle_longitude_tangent_points(gc_center, gc_radius)
514+
if tan_lon_pts is not None:
515+
lons_list = _add_tan_pts_if_in_pa_range(
516+
lons_list, tan_lon_pts, None, wrap_ang, pas_verts_wrap, coord='lon',
517+
gc_center=gc_center
518+
)
519+
520+
lons_arr = [lons_list.min(), lons_list.max()]
521+
lats_arr = [lats_list.min(), lats_list.max()]
522+
523+
# --------------------------------------------------------
524+
# Invert longitude order if centroid is outside of range:
525+
lons_arr = _validate_lon_bounds_ordering(lons_arr, center)
526+
527+
return Longitude(lons_arr).to(u.deg), Latitude(lats_arr).to(u.deg)
528+
529+
530+
def _discretize_edge_boundary(vertices, circ, n_points,
531+
circ_center=None, circ_radius=None):
532+
533+
if circ is not None:
534+
circ_center = circ.center
535+
circ_radius = circ.radius
536+
436537
# Discretize an edge boundary defined by a circle, geodetic or not:
437538
# either great circle arc, or a span of a non-great circle
438539
# (e.g., constant lat edges of RangeSphericalSkyRegion)
@@ -441,7 +542,9 @@ def _discretize_edge_boundary(vertices, circ, centroid, n_points):
441542
# connecting circle center to the two vertices bounding that edge:
442543
pas_verts = circ.center.position_angle(vertices).to(u.deg)
443544

444-
pas_verts_wrap, wrap_ang = _validate_vertices_ordering(vertices, circ, centroid)
545+
pas_verts_wrap, wrap_ang = _validate_vertices_ordering(
546+
vertices, circ, gc_center=circ_center,
547+
)
445548

446549
# Need to wrap angles to calculate span: wrap at lower value:
447550
pas_verts_wrap = pas_verts.wrap_at(wrap_ang)
@@ -453,12 +556,47 @@ def _discretize_edge_boundary(vertices, circ, centroid, n_points):
453556

454557
# Calculate directional offsets to get boundary discretization,
455558
# with vertices as SkyCoords
456-
bound_verts = circ.center.directional_offset_by(theta, circ.radius)
559+
bound_verts = circ.center.directional_offset_by(theta, circ_radius)
457560

458561
return bound_verts
459562

460563

461-
def discretize_all_edge_boundaries(vertices, circs, centroid, n_points):
564+
def discretize_line_boundary(coords, n_points):
565+
"""
566+
Discretize all edge boundaries for spherical sky regions.
567+
568+
Parameters
569+
----------
570+
coords : `~astropy.coordinates.SkyCoord`
571+
The start, end of the line as a SkyCoord.
572+
573+
n_points : int
574+
The number of points for discretization along each edge.
575+
576+
Returns
577+
-------
578+
edge_bound_verts : `~astropy.coordinates.SkyCoord`
579+
The discretized boundary edge vertices.
580+
"""
581+
# Iterate over full set of vertices & boundary circles for
582+
# a region (polygon or range)
583+
584+
gc_center = cross_product_skycoord2skycoord(coords[0], coords[1])
585+
gc_radius = 90 * u.deg
586+
587+
bound_verts = _discretize_edge_boundary(coords, None, n_points,
588+
circ_center=gc_center, circ_radius=gc_radius)
589+
590+
return SkyCoord(
591+
SkyCoord(
592+
bound_verts,
593+
representation_type=UnitSphericalRepresentation,
594+
),
595+
representation_type=SphericalRepresentation,
596+
)
597+
598+
599+
def discretize_all_edge_boundaries(vertices, circs, n_points):
462600
"""
463601
Discretize all edge boundaries for spherical sky regions.
464602
@@ -470,9 +608,6 @@ def discretize_all_edge_boundaries(vertices, circs, centroid, n_points):
470608
circs : `~regions.CircleSphericalSkyRegion`
471609
The circle boundaries as CircleSphericalSkyRegion instances.
472610
473-
centroid : `~astropy.coordinates.SkyCoord`
474-
The region centroid as a SkyCoord.
475-
476611
n_points : int
477612
The number of points for discretization along each edge.
478613
@@ -489,7 +624,7 @@ def discretize_all_edge_boundaries(vertices, circs, centroid, n_points):
489624
# PAs from gc center to vertices:
490625
verts = SkyCoord(np.concatenate([[vertices[i - 1]], [vertices[i]]]))
491626

492-
bound_verts = _discretize_edge_boundary(verts, circ, centroid, n_points)
627+
bound_verts = _discretize_edge_boundary(verts, circ, n_points)
493628

494629
if all_edge_bound_verts is None:
495630
all_edge_bound_verts = bound_verts

regions/shapes/lune.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def transform_to(self, frame, merge_attributes=True):
184184

185185
def discretize_boundary(self, n_points=100):
186186
bound_verts = discretize_all_edge_boundaries(
187-
self.vertices, self._edge_circs, self.centroid, n_points
187+
self.vertices, self._edge_circs, n_points
188188
)
189189
return PolygonSphericalSkyRegion(bound_verts)
190190

regions/shapes/polygon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ def transform_to(self, frame, merge_attributes=True):
597597

598598
def discretize_boundary(self, n_points=10):
599599
bound_verts = discretize_all_edge_boundaries(
600-
self.vertices, self._edge_circs, self.centroid, n_points
600+
self.vertices, self._edge_circs, n_points
601601
)
602602
return PolygonSphericalSkyRegion(bound_verts)
603603

regions/shapes/range.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ def discretize_boundary(self, n_points=10):
825825
return self.longitude_bounds.discretize_boundary(n_points=n_points)
826826
elif (bound_nverts == 4) | (bound_nverts == 3):
827827
bound_verts = discretize_all_edge_boundaries(
828-
self.vertices, self._edge_circs, self.centroid, n_points
828+
self.vertices, self._edge_circs, n_points
829829
)
830830
return PolygonSphericalSkyRegion(bound_verts)
831831
elif bound_nverts == 6:

0 commit comments

Comments
 (0)