-
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 30 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 |
---|---|---|
|
@@ -686,8 +686,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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,9 +32,6 @@ def as_datetime(t_str): | |
---------- | ||
t_str : string | ||
Time string to be parsed. (e.g., "2021-12-10T12:00:0.0") | ||
fmt : string | ||
Format of string provided. Defaults to az time format found in annotation XML. | ||
(e.g., "%Y-%m-%dT%H:%M:%S.%f"). | ||
|
||
Returns: | ||
------ | ||
|
@@ -268,23 +265,62 @@ def get_burst_centers_and_boundaries(tree): | |
return center_pts, boundary_pts | ||
|
||
def get_ipf_version(tree: ET): | ||
'''Extract the IPF version from the ET of manifest.safe | ||
''' | ||
# path to xmlData in manifest | ||
xml_meta_path = 'metadataSection/metadataObject/metadataWrap/xmlData' | ||
'''Extract the IPF version from the ET of manifest.safe. | ||
|
||
# piecemeal build path to software path to access version attrib | ||
esa_http = '{http://www.esa.int/safe/sentinel-1.0}' | ||
processing = xml_meta_path + f'/{esa_http}processing' | ||
facility = processing + f'/{esa_http}facility' | ||
software = facility + f'/{esa_http}software' | ||
Parameters | ||
---------- | ||
tree : xml.etree.ElementTree | ||
ElementTree containing the parsed 'manifest.safe' file. | ||
|
||
Returns | ||
------- | ||
ipf_version : version.Version | ||
IPF version of the burst. | ||
''' | ||
# get version from software element | ||
software_elem = tree.find(software) | ||
search_term, nsmap = _get_manifest_pattern(tree, ['processing', 'facility', 'software']) | ||
software_elem = tree.find(search_term, nsmap) | ||
ipf_version = version.parse(software_elem.attrib['version']) | ||
|
||
return ipf_version | ||
|
||
def get_start_end_track(tree: ET): | ||
'''Extract the start/end relative orbits from manifest.safe file''' | ||
scottstanie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
search_term, nsmap = _get_manifest_pattern(tree, ['orbitReference', 'relativeOrbitNumber']) | ||
elem_start, elem_end = tree.findall(search_term, nsmap) | ||
return int(elem_start.text), int(elem_end.text) | ||
|
||
|
||
def _get_manifest_pattern(tree: ET, keys: list): | ||
'''Get the search path to extract data from the ET of manifest.safe. | ||
|
||
Parameters | ||
---------- | ||
tree : xml.etree.ElementTree | ||
ElementTree containing the parsed 'manifest.safe' file. | ||
keys : list | ||
List of keys to search for in the manifest file. | ||
|
||
Returns | ||
------- | ||
str | ||
Search path to extract from the ET of the manifest.safe XML. | ||
|
||
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. Nice improvement of the 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. good idea, i added a comment and a link to the lxml document where it came from |
||
''' | ||
# https://lxml.de/tutorial.html#namespaces | ||
# Get the namespace from the root element to avoid full urls | ||
# This is a dictionary with a short name containing the labels in the | ||
# XML tree, and the fully qualified URL as the value. | ||
try: | ||
nsmap = tree.nsmap | ||
except AttributeError: | ||
nsmap = tree.getroot().nsmap | ||
# path to xmlData in manifest | ||
xml_meta_path = 'metadataSection/metadataObject/metadataWrap/xmlData' | ||
vbrancat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
safe_terms = "/".join([f'safe:{key}' for key in keys]) | ||
return f'{xml_meta_path}/{safe_terms}', nsmap | ||
|
||
|
||
def get_path_aux_cal(directory_aux_cal: str, str_annotation: str): | ||
''' | ||
Decide which aux_cal to load | ||
|
@@ -323,8 +359,7 @@ def get_path_aux_cal(directory_aux_cal: str, str_annotation: str): | |
list_aux_cal = glob.glob(f'{directory_aux_cal}/{str_platform}_AUX_CAL_V*.SAFE.zip') | ||
|
||
if len(list_aux_cal) == 0: | ||
raise ValueError( 'Cannot find AUX_CAL files from directory: ' | ||
f'{directory_aux_cal}') | ||
raise ValueError(f'Cannot find AUX_CAL files from {directory_aux_cal} .') | ||
|
||
format_datetime = '%Y%m%dT%H%M%S' | ||
|
||
|
@@ -457,27 +492,18 @@ def burst_from_xml(annotation_path: str, orbit_path: str, tiff_path: str, | |
bursts : list | ||
List of Sentinel1BurstSlc objects found in annotation XML. | ||
''' | ||
|
||
# a dict where the key is the track number and the value is a list of | ||
# two integers for the start and stop burst number | ||
track_burst_num = get_track_burst_num() | ||
|
||
_, tail = os.path.split(annotation_path) | ||
platform_id, swath_name, _, pol = [x.upper() for x in tail.split('-')[:4]] | ||
safe_filename = os.path.basename(annotation_path.split('.SAFE')[0]) | ||
|
||
# For IW mode, one burst has a duration of ~2.75 seconds and a burst | ||
# overlap of approximately ~0.4 seconds. | ||
# https://sentinels.copernicus.eu/web/sentinel/user-guides/sentinel-1-sar/product-types-processing-levels/level-1 | ||
# Additional precision calculated from averaging the differences between | ||
# burst sensing starts in prototyping test data | ||
burst_interval = 2.758277 | ||
|
||
# parse manifest.safe to retrieve IPF version | ||
manifest_path = os.path.dirname(annotation_path).replace('annotation','') + 'manifest.safe' | ||
with open_method(manifest_path, 'r') as f_manifest: | ||
tree_manfest = ET.parse(f_manifest) | ||
ipf_version = get_ipf_version(tree_manfest) | ||
tree_manifest = ET.parse(f_manifest) | ||
scottstanie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ipf_version = get_ipf_version(tree_manifest) | ||
# Parse out the start/end track to determine if we have an | ||
# equator crossing (for the burst_id calculation). | ||
start_track, end_track = get_start_end_track(tree_manifest) | ||
|
||
# Load the Product annotation - for EAP calibration | ||
with open_method(annotation_path, 'r') as f_lads: | ||
|
@@ -543,7 +569,7 @@ def burst_from_xml(annotation_path: str, orbit_path: str, tiff_path: str, | |
slant_range_time = float(image_info_element.find('slantRangeTime').text) | ||
ascending_node_time = as_datetime(image_info_element.find('ascendingNodeTime').text) | ||
|
||
downlink_element = tree.find('generalAnnotation/downlinkInformationList/downlinkInformation') | ||
downlink_element = tree.find('generalAnnotation/downlinkInformationList/downlinkInformation') | ||
prf_raw_data = float(downlink_element.find('prf').text) | ||
rank = int(downlink_element.find('downlinkValues/rank').text) | ||
range_chirp_ramp_rate = float(downlink_element.find('downlinkValues/txPulseRampRate').text) | ||
|
@@ -565,8 +591,6 @@ def burst_from_xml(annotation_path: str, orbit_path: str, tiff_path: str, | |
range_window_coeff = float(rng_processing_element.find('windowCoefficient').text) | ||
|
||
orbit_number = int(tree.find('adsHeader/absoluteOrbitNumber').text) | ||
orbit_number_offset = 73 if platform_id == 'S1A' else 202 | ||
track_number = (orbit_number - orbit_number_offset) % 175 + 1 | ||
|
||
center_pts, boundary_pts = get_burst_centers_and_boundaries(tree) | ||
|
||
|
@@ -593,28 +617,25 @@ def burst_from_xml(annotation_path: str, orbit_path: str, tiff_path: str, | |
burst_list_elements = tree.find('swathTiming/burstList') | ||
n_bursts = int(burst_list_elements.attrib['count']) | ||
bursts = [[]] * n_bursts | ||
sensing_starts = [[]] * n_bursts | ||
sensing_times = [[]] * n_bursts | ||
|
||
for i, burst_list_element in enumerate(burst_list_elements): | ||
# get burst timing | ||
d_seconds = 0.5 * (n_lines - 1) * azimuth_time_interval | ||
scottstanie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Zero Doppler azimuth time of the first line of this burst | ||
sensing_start = as_datetime(burst_list_element.find('azimuthTime').text) | ||
sensing_starts[i] = sensing_start | ||
sensing_times[i] = as_datetime(burst_list_element.find('sensingTime').text) | ||
dt = sensing_times[i] - ascending_node_time | ||
# local burst_num within one track, starting from 0 | ||
burst_num = int((dt.seconds + dt.microseconds / 1e6) // burst_interval) | ||
azimuth_time_mid = sensing_start + datetime.timedelta(seconds=d_seconds) | ||
|
||
# convert the local burst_num to the global burst_num, starting from 1 | ||
burst_num += track_burst_num[track_number][0] | ||
# Sensing time of the first input line of this burst [UTC] | ||
sensing_time = as_datetime(burst_list_element.find('sensingTime').text) | ||
# Create the burst ID to match the ESA ID scheme | ||
burst_id = get_burst_id( | ||
sensing_time, ascending_node_time, start_track, end_track, swath_name | ||
) | ||
|
||
# choose nearest azimuth FM rate | ||
d_seconds = 0.5 * (n_lines - 1) * azimuth_time_interval | ||
sensing_mid = sensing_start + datetime.timedelta(seconds=d_seconds) | ||
az_fm_rate = get_nearest_polynomial(sensing_mid, az_fm_rate_list) | ||
az_fm_rate = get_nearest_polynomial(azimuth_time_mid, az_fm_rate_list) | ||
|
||
# choose nearest doppler | ||
poly1d = get_nearest_polynomial(sensing_mid, doppler_list) | ||
poly1d = get_nearest_polynomial(azimuth_time_mid, doppler_list) | ||
lut2d = doppler_poly1d_to_lut2d(poly1d, starting_range, | ||
range_pxl_spacing, (n_lines, n_samples), | ||
azimuth_time_interval) | ||
|
@@ -643,9 +664,6 @@ def burst_from_xml(annotation_path: str, orbit_path: str, tiff_path: str, | |
last_sample = min(last_valid_samples[first_valid_line], | ||
last_valid_samples[last_line]) | ||
|
||
|
||
burst_id = f't{track_number:03d}_{burst_num}_{swath_name.lower()}' | ||
|
||
# Extract burst-wise information for Calibration, Noise, and EAP correction | ||
if calibration_annotation is None: | ||
burst_calibration = None | ||
|
@@ -785,7 +803,7 @@ def _burst_from_zip(zip_path: str, id_str: str, orbit_path: str, flag_apply_eap: | |
path : str | ||
Path to zip file. | ||
id_str: str | ||
Identifcation of desired burst. Format: iw[swath_num]-slc-[pol] | ||
Identification of desired burst. Format: iw[swath_num]-slc-[pol] | ||
orbit_path : str | ||
Path the orbit file. | ||
|
||
|
@@ -819,6 +837,7 @@ def _burst_from_zip(zip_path: str, id_str: str, orbit_path: str, flag_apply_eap: | |
flag_apply_eap=flag_apply_eap) | ||
return bursts | ||
|
||
|
||
def _burst_from_safe_dir(safe_dir_path: str, id_str: str, orbit_path: str, flag_apply_eap: bool): | ||
'''Find bursts in a Sentinel-1 SAFE structured directory. | ||
|
||
|
@@ -827,7 +846,7 @@ def _burst_from_safe_dir(safe_dir_path: str, id_str: str, orbit_path: str, flag_ | |
path : str | ||
Path to SAFE directory. | ||
id_str: str | ||
Identifcation of desired burst. Format: iw[swath_num]-slc-[pol] | ||
Identification of desired burst. Format: iw[swath_num]-slc-[pol] | ||
orbit_path : str | ||
Path the orbit file. | ||
|
||
|
@@ -866,3 +885,99 @@ def _burst_from_safe_dir(safe_dir_path: str, id_str: str, orbit_path: str, flag_ | |
bursts = burst_from_xml(f_annotation, orbit_path, f_tiff, iw2_f_annotation, | ||
flag_apply_eap=flag_apply_eap) | ||
return bursts | ||
|
||
|
||
def get_burst_id( | ||
sensing_time: datetime.datetime, | ||
ascending_node_dt: datetime.datetime, | ||
start_track: int, | ||
end_track: int, | ||
subswath_name: str, | ||
) -> int: | ||
scottstanie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Calculate burst ID and current track number of a burst. | ||
|
||
Accounts for equator crossing frames, and 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_name : str, {'IW1', 'IW2', 'IW3'} | ||
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. Is this case-sensitive? 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. nope, and I'll add a comment saying so |
||
Name of the subswath of the burst (not case sensitive). | ||
|
||
Returns | ||
------- | ||
relative_orbit : int | ||
Relative orbit number (track number) at the current burst. | ||
burst_id : int | ||
The burst ID matching ESA's relative numbering scheme. | ||
scottstanie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
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. Wondering again whether we should have a separate file in the repository where to insert all the constants 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. Same comment here- I can move these to a constants file if you'd like, but i'm unsure if the constants would be easier to interpret if they're moved away from function that uses them. If we're planning on making a big 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 don't think there's enough for a dedicated constants file. From a quick glance, I didn't see anything beyond 3 constants below in the repo. What do you about moving from typing import ClassVar
from dataclasses import dataclass
@dataclass
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_name: str
@classmethod
def params_to_id(cls,
sensing_time: datetime.datetime,
ascending_node_dt: datetime.datetime,
start_track: int,
end_track: int,
subswath_name: str):
# do same computations
return cls(track_numer, esa_burst_id, subswath)
@property
def as_str(self):
return f"t{self.track_number:03d}_{self.esa_burst_id:06d}_{self.subswath_name.lower()}" We can then change this from Benefits:
|
||
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_name[-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)) | ||
# Form the unique JPL ID by combining track/burst/swath | ||
return f"t{track_number:03d}_{esa_burst_id:06d}_{subswath_name.lower()}" |
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