-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix track number/burst id calculation #77
Changes from 32 commits
58c0648
eb8b2f3
556e44d
42df314
63859d8
2c904d5
af6d4ca
53dc5ea
0f86d66
2ed1623
cbabae1
98eb819
dba751c
2daef08
a1e13c4
2236cf7
7e95f3c
2382df4
9e1b544
1c1dc57
5d19f7d
fcf833d
110b06c
eae3490
23c02d4
e3cd8be
c2b8602
a43a60b
0fa41e5
cf85764
84888ce
6565058
7cc6150
ddd1dc1
b227f84
2db91b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
include src/s1reader/data/sentinel1_track_burst_id.txt | ||
recursive-include src/s1reader/data/ * | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import datetime | ||
from dataclasses import dataclass | ||
from typing import ClassVar | ||
|
||
import numpy as np | ||
|
||
|
||
@dataclass(frozen=True) | ||
class S1BurstId: | ||
T_beam: ClassVar[float] = 2.758273 # interval of one burst [s] | ||
T_pre: ClassVar[float] = 2.299849 # Preamble time interval [s] | ||
T_orb: ClassVar[float] = 12 * 24 * 3600 / 175 # Nominal orbit period [s] | ||
track_number: int | ||
esa_burst_id: int | ||
subswath: str | ||
|
||
@classmethod | ||
def from_burst_params( | ||
cls, | ||
sensing_time: datetime.datetime, | ||
ascending_node_dt: datetime.datetime, | ||
start_track: int, | ||
end_track: int, | ||
subswath: str, | ||
): | ||
"""Calculate the unique burst ID (track, ESA burstId, swath) of a burst. | ||
|
||
Accounts for equator crossing frames, where the current track number of | ||
a burst may change mid-frame. Uses the ESA convention defined in the | ||
Sentinel-1 Level 1 Detailed Algorithm Definition. | ||
|
||
Parameters | ||
---------- | ||
sensing_time : datetime | ||
Sensing time of the first input line of this burst [UTC] | ||
The XML tag is sensingTime in the annotation file. | ||
ascending_node_dt : datetime | ||
Time of the ascending node prior to the start of the scene. | ||
start_track : int | ||
Relative orbit number at the start of the acquisition, from 1-175. | ||
end_track : int | ||
Relative orbit number at the end of the acquisition. | ||
subswath : str, {'IW1', 'IW2', 'IW3'} | ||
Name of the subswath of the burst (not case sensitive). | ||
|
||
Returns | ||
------- | ||
S1BurstId | ||
The burst ID object containing track number + ESA's burstId number + swath ID. | ||
|
||
Notes | ||
----- | ||
The `start_track` and `end_track` parameters are used to determine if the | ||
scene crosses the equator. They are the same if the frame does not cross | ||
the equator. | ||
|
||
References | ||
---------- | ||
ESA Sentinel-1 Level 1 Detailed Algorithm Definition | ||
https://sentinels.copernicus.eu/documents/247904/1877131/S1-TN-MDA-52-7445_Sentinel-1+Level+1+Detailed+Algorithm+Definition_v2-4.pdf/83624863-6429-cfb8-2371-5c5ca82907b8 | ||
""" | ||
# Constants in Table 9-7 | ||
T_beam = 2.758273 # interval of one burst [s] | ||
T_pre = 2.299849 # Preamble time interval [s] | ||
T_orb = 12 * 24 * 3600 / 175 # Nominal orbit period [s] | ||
|
||
swath_num = int(subswath[-1]) | ||
# Since we only have access to the current subswath, we need to use the | ||
# burst-to-burst times to figure out | ||
# 1. if IW1 crossed the equator, and | ||
# 2. The mid-burst sensing time for IW2 | ||
# IW1 -> IW2 takes ~0.83220 seconds | ||
# IW2 -> IW3 takes ~1.07803 seconds | ||
# IW3 -> IW1 takes ~0.84803 seconds | ||
burst_times = np.array([0.832, 1.078, 0.848]) | ||
iw1_start_offsets = [ | ||
0, | ||
-burst_times[0], | ||
-burst_times[0] - burst_times[1], | ||
] | ||
offset = iw1_start_offsets[swath_num - 1] | ||
start_iw1 = sensing_time + datetime.timedelta(seconds=offset) | ||
|
||
start_iw1_to_mid_iw2 = burst_times[0] + burst_times[1] / 2 | ||
mid_iw2 = start_iw1 + datetime.timedelta(seconds=start_iw1_to_mid_iw2) | ||
|
||
has_anx_crossing = (end_track == (start_track + 1) % 175) | ||
|
||
time_since_anx_iw1 = (start_iw1 - ascending_node_dt).total_seconds() | ||
time_since_anx = (mid_iw2 - ascending_node_dt).total_seconds() | ||
|
||
if (time_since_anx_iw1 - T_orb) < 0: | ||
# Less than a full orbit has passed | ||
track_number = start_track | ||
else: | ||
track_number = end_track | ||
# Additional check for scenes which have a given ascending node | ||
# that's more than 1 orbit in the past | ||
if not has_anx_crossing: | ||
time_since_anx = time_since_anx - T_orb | ||
|
||
# Eq. 9-89: ∆tb = tb − t_anx + (r - 1)T_orb | ||
# tb: mid-burst sensing time (sensing_time) | ||
# t_anx: ascending node time (ascending_node_dt) | ||
# r: relative orbit number (relative_orbit_start) | ||
dt_b = time_since_anx + (start_track - 1) * T_orb | ||
|
||
# Eq. 9-91 : 1 + floor((∆tb − T_pre) / T_beam ) | ||
esa_burst_id = 1 + int(np.floor((dt_b - T_pre) / T_beam)) | ||
|
||
return cls(track_number, esa_burst_id, subswath) | ||
|
||
@classmethod | ||
def from_str(cls, burst_id_str: str): | ||
"""Parse a S1BurstId object from a string. | ||
|
||
Parameters | ||
---------- | ||
burst_id_str : str | ||
The burst ID string, e.g. "t123_456_iw1" | ||
|
||
Returns | ||
------- | ||
S1BurstId | ||
The burst ID object containing track number + ESA's burstId number + swath ID. | ||
""" | ||
track_number, esa_burst_id, subswath = burst_id_str.split("_") | ||
track_number = int(track_number[1:]) | ||
esa_burst_id = int(esa_burst_id) | ||
return cls(track_number, esa_burst_id, subswath.lower()) | ||
|
||
def as_str(self): | ||
# Form the unique JPL ID by combining track/burst/swath | ||
return f"t{self.track_number:03d}_{self.esa_burst_id:06d}_{self.subswath.lower()}" | ||
|
||
def __str__(self): | ||
return self.as_str() | ||
|
||
def __eq__(self, other) -> bool: | ||
# Allows for comparison with strings, as well as S1BurstId objects | ||
# e.g., you can filter down burst IDs with: | ||
# burst_ids = ["t012_024518_iw3", "t012_024519_iw3"] | ||
# bursts = [b for b in bursts if b.burst_id in burst_ids] | ||
if isinstance(other, str): | ||
return self.as_str() == other | ||
else: | ||
return super().__eq__(other) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,8 @@ | |
from osgeo import gdal | ||
|
||
from s1reader import s1_annotation | ||
from .s1_burst_id import S1BurstId | ||
|
||
|
||
# Other functionalities | ||
def polyfit(xin, yin, zin, azimuth_order, range_order, | ||
|
@@ -144,7 +146,7 @@ class Sentinel1BurstSlc: | |
doppler: Doppler | ||
range_bandwidth: float | ||
polarization: str # {VV, VH, HH, HV} | ||
burst_id: str # t{track_number}_{burst_index}_iw{1,2,3} | ||
burst_id: S1BurstId | ||
platform_id: str # S1{A,B} | ||
safe_filename: str # SAFE file name | ||
center: tuple # {center lon, center lat} in degrees | ||
|
@@ -660,7 +662,7 @@ def width(self): | |
@property | ||
def swath_name(self): | ||
'''Swath name in iw1, iw2, iw3.''' | ||
return self.burst_id.split('_')[2] | ||
return self.burst_id.swath_name | ||
|
||
@property | ||
def thermal_noise_lut(self): | ||
|
@@ -686,8 +688,9 @@ def eap_compensation_lut(self): | |
f' IPF version = {self.ipf_version}') | ||
|
||
return self.burst_eap.compute_eap_compensation_lut(self.width) | ||
def bbox(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason why we are removing this function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this once I realized why the the anti-meridian crossing bursts (international date line) have two polygons, since it's conventional to split the latlon that way, so this function is giving an incorrect bbox for those ones. I can leave it in and submit an issue to correct it in the future if you'd like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixing it might be as simple as
as long as we don't care that it returns longitudes greater than 180: In [5]: b = s1reader.load_bursts('S1A_IW_SLC__1SDV_20220110T181913_20220110T181938_041402_04EC40_E2D9.zip', None, 1)[0]
In [6]: b.border
Out[6]: [<shapely.geometry.polygon.Polygon object at 0x7fadef947670>, <shapely.geometry.polygon.Polygon object at 0x7fadef944d00>]
In [8]: b.border[0].bounds
Out[8]: (180.0, 52.19675210021637, 180.6997402722514, 52.44622930532407)
In [11]: import shapely.geometry
In [12]: shapely.geometry.MultiPolygon(b.border).bounds
Out[12]: (179.3998234765543, 52.19675210021637, 180.6997402722514, 52.50961732430616) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's submit an issue separately. We do not want to have longitudes greater than 180 (if this is the only fix) |
||
'''Returns the (west, south, east, north) bounding box of the burst.''' | ||
# Uses https://shapely.readthedocs.io/en/stable/manual.html#object.bounds | ||
# Returns a tuple of 4 floats representing (west, south, east, north) in degrees. | ||
return self.border[0].bounds | ||
|
||
@property | ||
def relative_orbit_number(self): | ||
'''Returns the relative orbit number of the burst.''' | ||
orbit_number_offset = 73 if self.platform_id == 'S1A' else 202 | ||
return (self.abs_orbit_number - orbit_number_offset) % 175 + 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a note: this will fix #85 by including all the files in the data folder