-
Notifications
You must be signed in to change notification settings - Fork 22
geocode SLC integration test #46
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
Changes from 1 commit
6b02edd
fd880ed
9f0b607
97c4461
2984d49
1abbfe4
92daa0f
0164652
ebfea34
13cb5b2
7a5655c
6442402
cd70f7c
6e3069c
8eff671
175ab31
c65ace4
17a70ff
dbf457b
7e34b3f
35cb9e8
5b4dc91
07c83e4
b30f823
18c9cb4
4ddf5f8
aa1540c
8e10100
88f0487
d438b58
dd2e841
e693f51
aeb64ac
3f72ad3
b95f45c
8953342
dd7b5e1
fa33f64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
add environment yaml to help create clean/minimal environment add bounds check to margin application
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| name: compass | ||
|
||
| channels: | ||
| - conda-forge | ||
| - nodefaults | ||
| dependencies: | ||
| # isce3 can be added if forge version is sufficient | ||
| - gdal>=3.0 | ||
| - h5py | ||
| - lxml | ||
| - numpy | ||
| - pandas | ||
| - pyproj | ||
| - pytest | ||
| - python>=3.9 | ||
|
||
| - requests | ||
| - ruamel.yaml | ||
| - scipy | ||
| - shapely | ||
| - yamale | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,25 +9,48 @@ | |
|
|
||
| from compass.utils import iono | ||
|
|
||
|
|
||
| def download_if_needed(local_path): | ||
LiangJYu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # check if test inputs and reference files exists; download if not found. | ||
| ''' | ||
| Check if given path to file exists. Download if it from zenodo does not. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| local_path: str | ||
| Path to file | ||
| ''' | ||
| # return if file is found | ||
| if os.path.isfile(local_path): | ||
| return | ||
|
|
||
| check_internet_connection() | ||
|
|
||
| dataset_url = 'https://zenodo.org/record/7668411/files/' | ||
| dst_dir, file_name = os.path.split(local_path) | ||
|
|
||
| # create destination directory if it does not exist | ||
| if dst_dir: | ||
| os.makedirs(dst_dir, exist_ok=True) | ||
|
|
||
| # download data | ||
| dataset_url = 'https://zenodo.org/record/7668411/files/' | ||
|
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 if we should have a file inside the repository having all hard-coded paths. In this way, we do not need to hunt them when they change 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. |
||
| target_url = f'{dataset_url}/{file_name}' | ||
| with open(local_path, 'wb') as f: | ||
| f.write(requests.get(target_url).content) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def unit_test_paths(): | ||
| test_paths = types.SimpleNamespace() | ||
| def geocode_slc_params(): | ||
| ''' | ||
| Parameters to be used by geocode SLC unit test | ||
|
|
||
| Returns | ||
| ------- | ||
| test_params: SimpleNamespace | ||
| SimpleNamespace containing geocode SLC unit test parameters | ||
| ''' | ||
| test_params = types.SimpleNamespace() | ||
|
|
||
| # burst ID and date of burst | ||
| burst_id = 't064_135523_iw2' | ||
| b_date = '20221016' | ||
|
|
||
|
|
@@ -39,34 +62,40 @@ def unit_test_paths(): | |
|
|
||
| # paths for template and actual runconfig | ||
| gslc_template_path = f'{test_data_path}/geo_cslc_s1_template.yaml' | ||
| test_paths.gslc_cfg_path = f'{test_data_path}/geo_cslc_s1.yaml' | ||
| test_params.gslc_cfg_path = f'{test_data_path}/geo_cslc_s1.yaml' | ||
|
|
||
| # read runconfig template, replace pieces, write to runconfig | ||
| with open(gslc_template_path, 'r') as f_template, \ | ||
| open(test_paths.gslc_cfg_path, 'w') as f_cfg: | ||
| open(test_params.gslc_cfg_path, 'w') as f_cfg: | ||
| cfg = f_template.read().replace('@TEST_PATH@', str(test_path)).\ | ||
| replace('@DATA_PATH@', test_data_path).\ | ||
| replace('@BURST_ID@', burst_id) | ||
| f_cfg.write(cfg) | ||
|
|
||
| # check for files and download as needed | ||
| # files needed for geocode SLC unit test | ||
| test_files = ['S1A_IW_SLC__1SDV_20221016T015043_20221016T015111_045461_056FC0_6681.zip', | ||
| 'orbits/S1A_OPER_AUX_POEORB_OPOD_20221105T083813_V20221015T225942_20221017T005942.EOF', | ||
| 'test_dem.tiff', 'test_burst_map.sqlite3', | ||
| '2022-10-16_0000_Rosamond-corner-reflectors.csv'] | ||
| test_files = [f'{test_data_path}/{test_file}' for test_file in test_files] | ||
|
|
||
| # parallel download of test files | ||
| # parallel download of test files (if necessary) | ||
| pool = mp.Pool(len(test_files)) | ||
| _ = pool.map(download_if_needed, test_files) | ||
| pool.close() | ||
| pool.join() | ||
|
|
||
| test_paths.corner_coord_csv_path = test_files[-1] | ||
| test_paths.output_hdf5 = f'{test_path}/product/{burst_id}/{b_date}/{burst_id}_{b_date}.h5' | ||
| test_paths.grid_group_path = '/science/SENTINEL1/CSLC/grids' | ||
| # path to file containing corner reflectors | ||
| test_params.corner_coord_csv_path = test_files[-1] | ||
|
|
||
| # path the output HDF5 | ||
| test_params.output_hdf5 = f'{test_path}/product/{burst_id}/{b_date}/{burst_id}_{b_date}.h5' | ||
|
|
||
| return test_paths | ||
| # path to groups and datasets in output HDF5 | ||
| test_params.grid_group_path = '/science/SENTINEL1/CSLC/grids' | ||
| test_params.raster_path = f'{test_params.grid_group_path}/VV' | ||
|
|
||
| return test_params | ||
|
|
||
| @pytest.fixture(scope='session') | ||
| def ionex_params(download_data=True): | ||
|
|
@@ -81,9 +110,8 @@ def ionex_params(download_data=True): | |
|
|
||
| Returns | ||
| ------- | ||
| tec_file: str | ||
| Path to local or downloaded TEC file to | ||
| use in the unit test | ||
| tec_file: SimpleNamespace | ||
LiangJYu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| SimpleNamespace containing parameters needed for ionex unit test | ||
| ''' | ||
| test_params = types.SimpleNamespace() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,27 +8,55 @@ | |
| from compass.utils.geo_runconfig import GeoRunConfig | ||
|
|
||
|
|
||
| def test_geocode_slc_run(unit_test_paths): | ||
| def test_geocode_slc_run(geocode_slc_params): | ||
| ''' | ||
| run s1_geocode_slc to ensure it does not crash | ||
| Run s1_geocode_slc to ensure it does not crash | ||
|
|
||
| Parameters | ||
| ---------- | ||
| geocode_slc_params: SimpleNamespace | ||
| SimpleNamespace containing geocode SLC unit test parameters | ||
| ''' | ||
| # load yaml to cfg | ||
| cfg = GeoRunConfig.load_from_yaml(unit_test_paths.gslc_cfg_path, | ||
| cfg = GeoRunConfig.load_from_yaml(geocode_slc_params.gslc_cfg_path, | ||
| workflow_name='s1_cslc_geo') | ||
|
|
||
| # pass cfg to s1_geocode_slc | ||
| s1_geocode_slc.run(cfg) | ||
|
|
||
| def get_nearest_index(arr, val): | ||
|
|
||
| def _get_nearest_index(arr, val): | ||
| ''' | ||
| Find index of element in given array closest to given value | ||
|
|
||
| Parameters | ||
| ---------- | ||
| arr: np.ndarray | ||
| 1D array to be searched | ||
| val: float | ||
| Number to be searched for | ||
|
|
||
| Returns | ||
| ------- | ||
| _: int | ||
| Index of element in arr where val is closest | ||
| ''' | ||
| return np.abs(arr - val).argmin() | ||
|
|
||
| def get_reflectors_extents_slice(unit_test_paths, margin=50): | ||
|
|
||
| def _get_reflectors_bounding_slice(geocode_slc_params): | ||
| ''' | ||
| get max and min lat, lon | ||
| Get latitude, longitude slice that contains all the corner reflectors in | ||
| CSV list of corner reflectors | ||
|
|
||
| Parameters | ||
| ---------- | ||
| geocode_slc_params: SimpleNamespace | ||
| SimpleNamespace containing geocode SLC unit test parameters | ||
| ''' | ||
| # extract from HDF5 | ||
| with h5py.File(unit_test_paths.output_hdf5, 'r') as h5_obj: | ||
| grid_group = h5_obj[unit_test_paths.grid_group_path] | ||
| with h5py.File(geocode_slc_params.output_hdf5, 'r') as h5_obj: | ||
| grid_group = h5_obj[geocode_slc_params.grid_group_path] | ||
|
|
||
| # create projection to covert from UTM to LLH | ||
| epsg = int(grid_group['projection'][()]) | ||
|
|
@@ -43,33 +71,50 @@ def get_reflectors_extents_slice(unit_test_paths, margin=50): | |
| lats = np.array([np.degrees(proj.inverse([x_coords_utm[0], y, 0])[1]) | ||
| for y in y_coords_utm]) | ||
|
|
||
| # get array shape for later check of slice with margins applied | ||
| height, width = h5_obj[geocode_slc_params.raster_path].shape | ||
|
|
||
| # extract all lat/lon corner reflector coordinates | ||
| corner_lats = [] | ||
| corner_lons = [] | ||
| with open(unit_test_paths.corner_coord_csv_path, 'r') as csvfile: | ||
| with open(geocode_slc_params.corner_coord_csv_path, 'r') as csvfile: | ||
| corner_reader = csv.DictReader(csvfile) | ||
| for row in corner_reader: | ||
| corner_lats.append(float(row['Latitude (deg)'])) | ||
| corner_lons.append(float(row['Longitude (deg)'])) | ||
|
|
||
| i_max_lat = get_nearest_index(lats, np.max(corner_lats)) | ||
| i_min_lat = get_nearest_index(lats, np.min(corner_lats)) | ||
| i_max_lon = get_nearest_index(lons, np.max(corner_lons)) | ||
| i_min_lon = get_nearest_index(lons, np.min(corner_lons)) | ||
| # find nearest index for min/max of lats/lons and apply margin | ||
| # apply margin to bounding box and ensure raster bounds are not exceeded | ||
| # application of margin y indices reversed due descending order lats vector | ||
| margin = 50 | ||
| i_max_y = max(_get_nearest_index(lats, np.max(corner_lats)) - margin, 0) | ||
| i_min_y = min(_get_nearest_index(lats, np.min(corner_lats)) + margin, | ||
| height - 1) | ||
| i_max_x = min(_get_nearest_index(lons, np.max(corner_lons)) + margin, | ||
| width - 1) | ||
| i_min_x = max(_get_nearest_index(lons, np.min(corner_lons)) - margin, 0) | ||
|
|
||
| return np.s_[i_max_lat - margin:i_min_lat + margin, | ||
| i_min_lon - margin:i_max_lon + margin] | ||
| # return as slice | ||
| # y indices reversed to account for descending order lats vector | ||
| return np.s_[i_max_y:i_min_y, i_min_x:i_max_x] | ||
|
|
||
| def test_geocode_slc_validate(unit_test_paths): | ||
|
|
||
| def test_geocode_slc_validate(geocode_slc_params): | ||
|
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. Does this test implicitly rely on If so, maybe we should make 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. By default it looks like tests are run in the order they are found (sorry I wasn't able to find anything definitive in the pytest docs). Anecdotally, swapping the order to order of the tests in code supports this. Test order can be set if it becomes an issue. Making
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 would prefer to have 2 separate tests. 1) testing that the actual workflow runs 2) validate the geolocation after running. Can we separate those two tests into separate functions? |
||
| ''' | ||
| check for reflectors in geocoded output | ||
| Check for reflectors in geocoded output | ||
|
|
||
| Parameters | ||
| ---------- | ||
| geocode_slc_params: SimpleNamespace | ||
| SimpleNamespace containing geocode SLC unit test parameters | ||
| ''' | ||
| s_ = get_reflectors_extents_slice(unit_test_paths) | ||
| # get slice where corner reflectors should be | ||
| s_ = _get_reflectors_bounding_slice(geocode_slc_params) | ||
|
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. The validation test I had in mind was a bit different and more in line with the validation activities that @seongsujeong is doing. We have geocoded a SLC, we do have the CR location from file, we can check that our geolocation is not messed up. To do so:
Was this the plan? 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. What you described was the plan but I wasn't able to find all them. This is just a quick check to see any CRs were found so the unit tests would basic validation. |
||
|
|
||
| with h5py.File(unit_test_paths.output_hdf5, 'r') as h5_obj: | ||
| src_path = f'{unit_test_paths.grid_group_path}/VV' | ||
| arr = h5_obj[src_path][()][s_] | ||
| print(arr.shape, s_) | ||
| # slice raster array | ||
| with h5py.File(geocode_slc_params.output_hdf5, 'r') as h5_obj: | ||
| arr = h5_obj[geocode_slc_params.raster_path][()][s_] | ||
LiangJYu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # check for bright spots in sliced array | ||
| corner_reflector_threshold = 3e3 | ||
|
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. How have you determined this threshold? Also, I do not fully understand the test. We are just testing for the presence of bright spots inside the processed CSLC? 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. Correct - this test only check for presence of bright spots in the processed CSLC within the area of the CRs. I was thinking to improve this to checking for individual CRs when we get all our corrections implemented. I determined the threshold by plotting where the CRs are and picked a seemingly appropriate value based on the colorbar. |
||
| assert np.any(arr > corner_reflector_threshold) | ||
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.
If these are getting created in the test directory and we want to ignore them, what if we use a temporary pytest directory that'll get removed?
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.
E.g. in conftest.py, using https://docs.pytest.org/en/7.1.x/how-to/tmp_path.html#the-tmp-path-factory-fixture
But now that i've written this out, I'm thinking you may have stored it in the
tests/directory so that you could inspect the output, so feel free to ignore this suggestion if that's the caseThere 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.
Your reasoning for the scratch directory is spot on.
tempfile.TemporaryDirectorycould be used just for unit tests but would disable ability to check intermediate results.