Skip to content
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

Merged
merged 36 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
58c0648
add more small testing data
scottstanie Oct 28, 2022
eb8b2f3
make a failing test for anx crossing
scottstanie Oct 28, 2022
556e44d
create methods for burst/track, fill tests
scottstanie Oct 28, 2022
42df314
fix erroneous burst id in existing test
scottstanie Oct 28, 2022
63859d8
add a sample burst db to compare
scottstanie Oct 29, 2022
2c904d5
add script for remaking the burst sample
scottstanie Oct 29, 2022
af6d4ca
add a geometry check for the esa database
scottstanie Oct 29, 2022
53dc5ea
perform test without pandas
scottstanie Oct 29, 2022
0f86d66
codacy items
scottstanie Oct 29, 2022
2ed1623
add the geometry comparison to the other test cases
scottstanie Oct 29, 2022
cbabae1
add two more test cases
scottstanie Nov 1, 2022
98eb819
refactor tests for new cases
scottstanie Nov 1, 2022
dba751c
redo logic for strange track175 case
scottstanie Nov 1, 2022
2daef08
update burst csv
scottstanie Nov 1, 2022
a1e13c4
fix first test problem, cut bursts
scottstanie Nov 1, 2022
2236cf7
get tests working again for track175 case
scottstanie Nov 1, 2022
7e95f3c
fix esa db csv
scottstanie Nov 1, 2022
2382df4
use nsmap instead of long manual urls
scottstanie Nov 1, 2022
9e1b544
remove testing script
scottstanie Nov 1, 2022
1c1dc57
working version on full orbit cycle
scottstanie Nov 1, 2022
5d19f7d
fix tests to check all subswaths
scottstanie Nov 1, 2022
fcf833d
try recursive include for circleci fail, codacy
scottstanie Nov 1, 2022
110b06c
retry manifest
scottstanie Nov 1, 2022
eae3490
Merge branch 'main' into burst-id-fix
scottstanie Nov 16, 2022
23c02d4
add better docstrings, add print missing tiff again, comment for star…
scottstanie Nov 18, 2022
e3cd8be
case sensitivity clarification
scottstanie Nov 18, 2022
c2b8602
revert tests folder back to current version
scottstanie Nov 18, 2022
a43a60b
fix unit test for correct burst id
scottstanie Nov 18, 2022
0fa41e5
Merge branch 'main' into burst-id-fix
scottstanie Nov 18, 2022
cf85764
adjust anx crossing to use mod, add comment on lxml nsmap
scottstanie Nov 18, 2022
84888ce
split out burst id into class
scottstanie Nov 21, 2022
6565058
fix burst load filtering for new class
scottstanie Nov 21, 2022
7cc6150
use the class vars in initialization
scottstanie Nov 21, 2022
ddd1dc1
formatting
scottstanie Nov 21, 2022
b227f84
just use __str__ instead of as_str, adjust as_dict
scottstanie Nov 21, 2022
2db91b1
address comments for clarity
scottstanie Nov 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MANIFEST.in
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/ *
Copy link
Contributor Author

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

147 changes: 147 additions & 0 deletions src/s1reader/s1_burst_id.py
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)
17 changes: 10 additions & 7 deletions src/s1reader/s1_burst_slc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason why we are removing this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this once I realized why the border was a list of Polygon's, when it seemed like it was always a list of one single Polygon: opera-adt/burst_db#1

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing it might be as simple as

    return shapely.geometry.MultiPolygon(b.border).bounds

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)

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Loading