sarkit-convert 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,11 +8,13 @@ Convert a complex image(s) from the Sentinel SAFE to SICD(s).
8
8
  """
9
9
 
10
10
  import argparse
11
+ import contextlib
12
+ import datetime
13
+ import functools
11
14
  import pathlib
12
15
 
13
16
  import dateutil.parser
14
- import lxml.builder
15
- import lxml.etree as et
17
+ import lxml.etree
16
18
  import numpy as np
17
19
  import numpy.linalg as npl
18
20
  import numpy.polynomial.polynomial as npp
@@ -138,6 +140,20 @@ def _collect_base_info(root_node):
138
140
  # TOPSAR - closest SICD analog is Dynamic Stripmap
139
141
  base_info["mode_type"] = "DYNAMIC STRIPMAP"
140
142
 
143
+ relative_orbit_num = int(
144
+ root_node.findtext(".//{*}relativeOrbitNumber[@type='start']")
145
+ )
146
+ base_info["parameters"] = {
147
+ "RELATIVE_ORBIT_NUM": relative_orbit_num,
148
+ }
149
+
150
+ base_info["start_time_anx"] = float(
151
+ root_node.findtext(".//{*}acquisitionPeriod//{*}startTimeANX")
152
+ )
153
+ base_info["acq_start_str"] = root_node.findtext(
154
+ ".//{*}acquisitionPeriod//{*}startTime"
155
+ )
156
+
141
157
  # Image Creation
142
158
  processing = root_node.find(
143
159
  "./metadataSection/metadataObject[@ID='processing']/metadataWrap/xmlData/{*}processing",
@@ -148,9 +164,6 @@ def _collect_base_info(root_node):
148
164
  f"{software.attrib['name']} {software.attrib['version']}"
149
165
  )
150
166
  base_info["creation_date_time"] = dateutil.parser.parse(processing.attrib["stop"])
151
- base_info["creation_site"] = (
152
- f"{facility.attrib['name']}, {facility.attrib['site']}, {facility.attrib['country']}"
153
- )
154
167
 
155
168
  # Radar Collection
156
169
  polarizations = root_node.findall(
@@ -164,6 +177,28 @@ def _collect_base_info(root_node):
164
177
  return base_info
165
178
 
166
179
 
180
+ @functools.lru_cache
181
+ def _window_params(window_name, coefficient):
182
+ window_name = window_name.upper()
183
+ if window_name == "HAMMING":
184
+ wgts = scipy.signal.windows.general_hamming(512, float(coefficient), sym=True)
185
+ elif window_name == "KAISER":
186
+ wgts = scipy.signal.windows.kaiser(512, float(coefficient), sym=True)
187
+ else: # Default to UNIFORM
188
+ window_name = "UNIFORM"
189
+ coefficient = None
190
+ wgts = np.ones(256)
191
+
192
+ broadening_factor = utils.broadening_from_amp(wgts)
193
+
194
+ return {
195
+ "window_name": window_name,
196
+ "coefficient": coefficient,
197
+ "wgts": wgts,
198
+ "broadening_factor": broadening_factor,
199
+ }
200
+
201
+
167
202
  def _collect_swath_info(product_root_node):
168
203
  swath_info = dict()
169
204
  burst_list = product_root_node.findall("./swathTiming/burstList/burst")
@@ -173,12 +208,25 @@ def _collect_swath_info(product_root_node):
173
208
  swath_info["mode_id"] = product_root_node.find("./adsHeader/mode").text
174
209
  t_slice = _get_slice(product_root_node)
175
210
  swath = _get_swath(product_root_node)
211
+
212
+ mission_data_take_id = product_root_node.find("./adsHeader/missionDataTakeId").text
176
213
  swath_info["parameters"] = {
177
214
  "SLICE": t_slice,
178
215
  "SWATH": swath,
179
216
  "ORBIT_SOURCE": "SLC_INTERNAL",
217
+ "MISSION_DATA_TAKE_ID": mission_data_take_id,
180
218
  }
181
219
 
220
+ slice_list = product_root_node.findall(
221
+ "./imageAnnotation/imageInformation/sliceList/slice"
222
+ )
223
+ swath_info["sensing_start"] = dateutil.parser.parse(
224
+ slice_list[int(t_slice) - 1].find("./sensingStartTime").text
225
+ )
226
+ swath_info["sensing_stop"] = dateutil.parser.parse(
227
+ slice_list[int(t_slice) - 1].find("./sensingStopTime").text
228
+ )
229
+
182
230
  # Radar Collection
183
231
  center_frequency = float(
184
232
  product_root_node.find(
@@ -226,8 +274,6 @@ def _collect_swath_info(product_root_node):
226
274
  "./generalAnnotation/downlinkInformationList/downlinkInformation/prf"
227
275
  ).text
228
276
  )
229
- swath_info["t_start"] = 0
230
- swath_info["ipp_start"] = 0
231
277
  swath_info["ipp_poly"] = (0, swath_info["prf"])
232
278
 
233
279
  # Image Formation
@@ -235,7 +281,6 @@ def _collect_swath_info(product_root_node):
235
281
  f"{swath_info['tx_polarization']}:{swath_info['rcv_polarization']}"
236
282
  )
237
283
  swath_info["image_form_algo"] = "RMA"
238
- swath_info["t_start_proc"] = 0
239
284
  swath_info["tx_freq_proc"] = swath_info["tx_freq"]
240
285
  swath_info["image_beam_comp"] = "SV"
241
286
  swath_info["az_autofocus"] = "NO"
@@ -312,20 +357,15 @@ def _collect_swath_info(product_root_node):
312
357
  range_proc = product_root_node.find(
313
358
  "./imageAnnotation/processingInformation/swathProcParamsList/swathProcParams/rangeProcessing"
314
359
  )
315
- swath_info["row_window_name"] = range_proc.find("./windowType").text.upper()
316
- swath_info["row_params"] = range_proc.find("./windowCoefficient").text
317
- if swath_info["row_window_name"] == "HAMMING":
318
- swath_info["row_wgts"] = scipy.signal.windows.general_hamming(
319
- 512, float(swath_info["row_params"]), sym=True
320
- )
321
- elif swath_info["row_window_name"] == "KAISER":
322
- swath_info["row_wgts"] = scipy.signal.windows.kaiser(
323
- 512, float(swath_info["row_params"]), sym=True
324
- )
325
- else: # Default to UNIFORM
326
- swath_info["row_window_name"] = "UNIFORM"
327
- swath_info["row_params"] = None
328
- swath_info["row_wgts"] = np.ones(256)
360
+
361
+ window_info = _window_params(
362
+ range_proc.find("./windowType").text,
363
+ range_proc.find("./windowCoefficient").text,
364
+ )
365
+ swath_info["row_window_name"] = window_info["window_name"]
366
+ swath_info["row_params"] = window_info["coefficient"]
367
+ swath_info["row_wgts"] = window_info["wgts"]
368
+ swath_info["row_broadening_factor"] = window_info["broadening_factor"]
329
369
 
330
370
  swath_info["row_ss"] = (_constants.speed_of_light / 2) * delta_tau_s
331
371
  swath_info["row_sgn"] = -1
@@ -352,34 +392,29 @@ def _collect_swath_info(product_root_node):
352
392
  ).text
353
393
  )
354
394
 
355
- swath_info["col_window_name"] = az_proc.find("./windowType").text.upper()
356
- swath_info["col_params"] = az_proc.find("./windowCoefficient").text
357
- if swath_info["col_window_name"] == "HAMMING":
358
- swath_info["col_wgts"] = scipy.signal.windows.general_hamming(
359
- 512, float(swath_info["col_params"]), sym=True
360
- )
361
- elif swath_info["col_window_name"] == "KAISER":
362
- swath_info["col_wgts"] = scipy.signal.windows.kaiser(
363
- 512, float(swath_info["col_params"]), sym=True
364
- )
365
- else: # Default to UNIFORM
366
- swath_info["col_window_name"] = "UNIFORM"
367
- swath_info["col_params"] = None
368
- swath_info["col_wgts"] = np.ones(256)
395
+ window_info = _window_params(
396
+ az_proc.find("./windowType").text, az_proc.find("./windowCoefficient").text
397
+ )
398
+ swath_info["col_window_name"] = window_info["window_name"]
399
+ swath_info["col_params"] = window_info["coefficient"]
400
+ swath_info["col_wgts"] = window_info["wgts"]
401
+ swath_info["col_broadening_factor"] = window_info["broadening_factor"]
369
402
 
370
403
  swath_info["col_sgn"] = -1
371
404
  swath_info["col_kctr"] = 0.0
372
405
  swath_info["col_imp_res_bw"] = dop_bw * swath_info["ss_zd_s"] / swath_info["col_ss"]
373
406
 
374
- row_broadening_factor = utils.broadening_from_amp(swath_info["row_wgts"])
375
- col_broadening_factor = utils.broadening_from_amp(swath_info["col_wgts"])
376
- swath_info["row_imp_res_wid"] = row_broadening_factor / swath_info["row_imp_res_bw"]
377
- swath_info["col_imp_res_wid"] = col_broadening_factor / swath_info["col_imp_res_bw"]
407
+ swath_info["row_imp_res_wid"] = (
408
+ swath_info["row_broadening_factor"] / swath_info["row_imp_res_bw"]
409
+ )
410
+ swath_info["col_imp_res_wid"] = (
411
+ swath_info["col_broadening_factor"] / swath_info["col_imp_res_bw"]
412
+ )
378
413
 
379
414
  return swath_info
380
415
 
381
416
 
382
- def _collect_burst_info(product_root_node, swath_info):
417
+ def _collect_burst_info(product_root_node, base_info, swath_info):
383
418
  burst_list = product_root_node.findall("./swathTiming/burstList/burst")
384
419
  # parse the geolocation information - for SCP calculation
385
420
  geo_grid_point_list = product_root_node.findall(
@@ -549,7 +584,7 @@ def _collect_burst_info(product_root_node, swath_info):
549
584
  )
550
585
 
551
586
  # common use for the fitting efforts
552
- poly_order = 2
587
+ poly_order = 4
553
588
  grid_samples = poly_order + 4
554
589
  num_rows = swath_info["num_rows"]
555
590
  num_cols = swath_info["num_cols"]
@@ -584,12 +619,13 @@ def _collect_burst_info(product_root_node, swath_info):
584
619
  demod_phase = eta_arg * f_eta_c[:, np.newaxis]
585
620
  total_phase = deramp_phase + demod_phase
586
621
 
587
- phase = utils.polyfit2d(
622
+ phase = utils.polyfit2d_tol(
588
623
  coords_rg_2d.flatten(),
589
624
  coords_az_2d.flatten(),
590
625
  total_phase.flatten(),
591
626
  poly_order,
592
- poly_order,
627
+ poly_order + 1,
628
+ 1e-2,
593
629
  )
594
630
 
595
631
  # DeltaKCOAPoly is derivative of phase in azimuth/Col direction
@@ -608,12 +644,13 @@ def _collect_burst_info(product_root_node, swath_info):
608
644
  )
609
645
  time_coa_sampled = time_ca_sampled + dop_centroid_sampled / doppler_rate_sampled
610
646
 
611
- burst_info["time_coa_poly_coefs"] = utils.polyfit2d(
647
+ burst_info["time_coa_poly_coefs"] = utils.polyfit2d_tol(
612
648
  coords_rg_2d.flatten(),
613
649
  coords_az_2d.flatten(),
614
650
  time_coa_sampled.flatten(),
615
651
  poly_order,
616
652
  poly_order,
653
+ 1e-3,
617
654
  )
618
655
 
619
656
  full_img_verticies = np.array(
@@ -694,6 +731,28 @@ def _collect_burst_info(product_root_node, swath_info):
694
731
 
695
732
  return row_uvect_ecf, np.cross(uspz, row_uvect_ecf)
696
733
 
734
+ def _compute_burst_id(base_info, swath_info, burst_info):
735
+ """Add `burst_id` to each burst info in `swaths_info` based on DI-MPC-IPFDPM, Issue 2.5, Clause 9.25"""
736
+ t_orb = 12 * 24 * 3600 / 175
737
+ t_pre = {"IW": 2.299849, "EW": 2.299970}[swath_info["mode_id"]]
738
+ t_beam = {"IW": 2.758273, "EW": 3.038376}[swath_info["mode_id"]]
739
+
740
+ acq_start_utc = datetime.datetime.fromisoformat(
741
+ base_info["acq_start_str"]
742
+ ).replace(tzinfo=datetime.timezone.utc)
743
+ t_anx = acq_start_utc - datetime.timedelta(
744
+ milliseconds=base_info["start_time_anx"]
745
+ )
746
+
747
+ scp_tcoa = burst_info["time_coa_poly_coefs"][0, 0]
748
+ t_b = (
749
+ burst_info["collect_start"] + datetime.timedelta(seconds=scp_tcoa)
750
+ ).replace(tzinfo=datetime.timezone.utc)
751
+ delta_t_b = (t_b - t_anx).total_seconds() + (
752
+ base_info["parameters"]["RELATIVE_ORBIT_NUM"] - 1
753
+ ) * t_orb
754
+ return int(1 + (delta_t_b - t_pre) // t_beam)
755
+
697
756
  def _finalize_stripmap():
698
757
  burst_info = dict()
699
758
 
@@ -711,38 +770,34 @@ def _collect_burst_info(product_root_node, swath_info):
711
770
  (num_rows - 1, 0),
712
771
  ]
713
772
 
714
- start = dateutil.parser.parse(
715
- product_root_node.find(
716
- "./generalAnnotation/downlinkInformationList/downlinkInformation/firstLineSensingTime"
717
- ).text
718
- )
719
- stop = dateutil.parser.parse(
720
- product_root_node.find(
721
- "./generalAnnotation/downlinkInformationList/downlinkInformation/lastLineSensingTime"
722
- ).text
723
- )
724
- slice = int(_get_slice(product_root_node))
773
+ t_slice = int(_get_slice(product_root_node))
725
774
  swath = _get_swath(product_root_node)
726
775
  burst_info["core_name"] = (
727
- f"{start.strftime('%d%b%YT%H%M%S').upper()}_{product_root_node.find('./adsHeader/missionId').text}{product_root_node.find('./adsHeader/missionDataTakeId').text}_{slice:02d}_{swath}_01"
776
+ f"{swath_info['sensing_start'].strftime('%d%b%YT%H%M%S').upper()}_{product_root_node.find('./adsHeader/missionId').text}{product_root_node.find('./adsHeader/missionDataTakeId').text}_{t_slice:02d}_{swath}_01"
728
777
  )
729
778
 
730
- burst_info["parameters"] = {"BURST": f"{1:d}"}
779
+ burst_info["parameters"] = {"BURST": f"{1:02d}"}
780
+
731
781
  prf = float(
732
782
  product_root_node.find(
733
783
  "./generalAnnotation/downlinkInformationList/downlinkInformation/prf"
734
784
  ).text
735
785
  )
736
- duration = (stop - start).total_seconds()
786
+ duration = (
787
+ swath_info["sensing_stop"] - swath_info["sensing_start"]
788
+ ).total_seconds()
737
789
 
738
- burst_info["collect_start"] = start
790
+ burst_info["collect_start"] = swath_info["sensing_start"]
739
791
  burst_info["collect_duration"] = duration
792
+ burst_info["ipp_set_tstart"] = 0
740
793
  burst_info["ipp_set_tend"] = duration
794
+ burst_info["ipp_set_ippstart"] = 0
741
795
  burst_info["ipp_set_ippend"] = round(duration * prf) - 1
796
+ burst_info["tstart_proc"] = 0
742
797
  burst_info["tend_proc"] = duration
743
798
 
744
799
  burst_info["arp_poly_coefs"] = _compute_arp_poly_coefs(
745
- product_root_node, start
800
+ product_root_node, swath_info["sensing_start"]
746
801
  ).T
747
802
 
748
803
  azimuth_time_first_line = dateutil.parser.parse(
@@ -750,9 +805,14 @@ def _collect_burst_info(product_root_node, swath_info):
750
805
  "./imageAnnotation/imageInformation/productFirstLineUtcTime"
751
806
  ).text
752
807
  )
753
- first_line_relative_start = (azimuth_time_first_line - start).total_seconds()
808
+ first_line_relative_start = (
809
+ azimuth_time_first_line - swath_info["sensing_start"]
810
+ ).total_seconds()
754
811
  _, _ = _calc_rma_and_grid_info(
755
- swath_info, burst_info, first_line_relative_start, start
812
+ swath_info,
813
+ burst_info,
814
+ first_line_relative_start,
815
+ swath_info["sensing_start"],
756
816
  )
757
817
  burst_info["scp_ecf"], burst_info["scp_llh"] = _update_geo_data_info(
758
818
  swath_info, burst_info
@@ -767,6 +827,7 @@ def _collect_burst_info(product_root_node, swath_info):
767
827
  burst_info_list = []
768
828
 
769
829
  scps = _get_scps(swath_info, len(burst_list))
830
+ t_slice = int(_get_slice(product_root_node))
770
831
  for j, burst in enumerate(burst_list):
771
832
  # set preliminary geodata (required for projection)
772
833
  burst_info = dict()
@@ -791,42 +852,53 @@ def _collect_burst_info(product_root_node, swath_info):
791
852
  (last_row, first_col),
792
853
  ]
793
854
 
794
- # This is the first and last zero doppler times of the columns in the burst.
795
- # Not really CollectStart and CollectDuration in SICD (first last pulse time)
796
- start = dateutil.parser.parse(burst.find("./azimuthTime").text)
797
- t_slice = int(_get_slice(product_root_node))
798
855
  swath = _get_swath(product_root_node)
856
+ # Use burst index for burst_id to maintain SARPy compatibility
799
857
  burst_info["core_name"] = (
800
- f"{start.strftime('%d%b%YT%H%M%S').upper()}_{product_root_node.find('./adsHeader/missionId').text}{product_root_node.find('./adsHeader/missionDataTakeId').text}_{t_slice:02d}_{swath}_{j + 1:02d}"
858
+ f"{swath_info['sensing_start'].strftime('%d%b%YT%H%M%S').upper()}_{product_root_node.find('./adsHeader/missionId').text}{product_root_node.find('./adsHeader/missionDataTakeId').text}_{t_slice:02d}_{swath}_{j + 1:02d}"
859
+ )
860
+ arp_poly_coefs = _compute_arp_poly_coefs(
861
+ product_root_node, swath_info["sensing_start"]
801
862
  )
802
-
803
- burst_info["parameters"] = {"BURST": f"{j + 1:d}"}
804
- arp_poly_coefs = _compute_arp_poly_coefs(product_root_node, start)
805
863
  burst_info["arp_poly_coefs"] = arp_poly_coefs.T
806
- early, late = _calc_rma_and_grid_info(swath_info, burst_info, 0, start)
807
- new_start = start + np.timedelta64(np.int64(early * 1e6), "us")
808
- duration = late - early
864
+
865
+ az_time = dateutil.parser.parse(burst.find("./azimuthTime").text)
866
+ first_line_relative_start = (
867
+ az_time - swath_info["sensing_start"]
868
+ ).total_seconds()
869
+ early, late = _calc_rma_and_grid_info(
870
+ swath_info,
871
+ burst_info,
872
+ first_line_relative_start,
873
+ swath_info["sensing_start"],
874
+ )
809
875
  prf = float(
810
876
  product_root_node.find(
811
877
  "./generalAnnotation/downlinkInformationList/downlinkInformation/prf"
812
878
  ).text
813
879
  )
814
- burst_info["collect_start"] = new_start
815
- burst_info["collect_duration"] = duration
816
- burst_info["ipp_set_tend"] = duration
817
- burst_info["ipp_set_ippend"] = round(duration * prf) - 1
818
- burst_info["tend_proc"] = duration
819
-
820
- # adjust time offset
821
- burst_info["time_coa_poly_coefs"][0, 0] -= early
822
- burst_info["time_ca_poly_coefs"][0] -= early
823
-
824
- arp_poly_coefs = np.array(
825
- [
826
- _shift(arp_poly_coefs[i], t_0=-early)
827
- for i in range(len(arp_poly_coefs))
828
- ]
829
- )
880
+ burst_info["collect_start"] = swath_info["sensing_start"]
881
+ burst_info["collect_duration"] = (
882
+ swath_info["sensing_stop"] - swath_info["sensing_start"]
883
+ ).total_seconds()
884
+ burst_info["ipp_set_tstart"] = early
885
+ burst_info["ipp_set_tend"] = late
886
+ burst_info["ipp_set_ippstart"] = round(early * prf)
887
+ burst_info["ipp_set_ippend"] = round(late * prf) - 1
888
+ burst_info["tstart_proc"] = early
889
+ burst_info["tend_proc"] = late
890
+
891
+ if burst.find("./burstId") is not None:
892
+ burst_id = burst.findtext("./burstId")
893
+ else:
894
+ burst_id = _compute_burst_id(base_info, swath_info, burst_info)
895
+
896
+ # Keep BURST as the index to maintain SARPy compatibility
897
+ burst_info["parameters"] = {
898
+ "BURST": f"{j + 1:02d}",
899
+ "BURST_ID": burst_id,
900
+ }
901
+
830
902
  burst_info["arp_poly_coefs"] = arp_poly_coefs.T
831
903
 
832
904
  burst_info["scp_ecf"], burst_info["scp_llh"] = _update_geo_data_info(
@@ -848,7 +920,7 @@ def _collect_burst_info(product_root_node, swath_info):
848
920
 
849
921
  def _calc_radiometric_info(cal_file_name, swath_info, burst_info_list):
850
922
  """Compute radiometric polys"""
851
- cal_root_node = et.parse(cal_file_name).getroot()
923
+ cal_root_node = lxml.etree.parse(cal_file_name).getroot()
852
924
  cal_vector_list = cal_root_node.findall(
853
925
  "./{*}calibrationVectorList/{*}calibrationVector"
854
926
  )
@@ -884,14 +956,15 @@ def _calc_radiometric_info(cal_file_name, swath_info, burst_info_list):
884
956
  gamma = 1.0 / (gamma * gamma)
885
957
 
886
958
  for idx, burst_info in enumerate(burst_info_list):
887
- valid_lines = (line >= idx * lines_per_burst) & (
888
- line < (idx + 1) * lines_per_burst
889
- )
959
+ burst_first_line = idx * lines_per_burst
960
+ burst_last_line = burst_first_line + lines_per_burst
961
+ valid_lines = (line >= burst_first_line) & (line < burst_last_line)
890
962
  valid_count = np.sum(valid_lines)
891
963
  if valid_count == 0:
892
964
  # this burst contained no useful calibration data
893
965
  return
894
966
 
967
+ max_poly_order = 4
895
968
  first_row = swath_info["first_row"]
896
969
  first_col = swath_info["first_col"]
897
970
  scp_row = swath_info["scp_pixel"][0]
@@ -899,32 +972,45 @@ def _calc_radiometric_info(cal_file_name, swath_info, burst_info_list):
899
972
  row_ss = swath_info["row_ss"]
900
973
  col_ss = swath_info["col_ss"]
901
974
  coords_rg = (pixel[valid_lines] + first_row - scp_row) * row_ss
902
- coords_az = (line[valid_lines] + first_col - scp_col) * col_ss
975
+ coords_az = (
976
+ line[valid_lines] + first_col - scp_col - burst_first_line
977
+ ) * col_ss
903
978
  # NB: coords_rg = (valid_count, M) and coords_az = (valid_count, )
904
979
  coords_az = np.repeat(coords_az, pixel.shape[1])
905
980
  if valid_count > 1:
906
981
  coords_az = coords_az.reshape((valid_count, -1))
982
+ max_poly_order_rg = min(max_poly_order, len(coords_rg) - 1)
983
+ max_poly_order_az = min(max_poly_order, len(coords_az) - 1)
907
984
 
908
- burst_info["radiometric"]["sigma_zero_poly_coefs"] = utils.polyfit2d(
985
+ sigma_vals = sigma[valid_lines, :].flatten()
986
+ sigma_tol = np.maximum(np.std(sigma_vals) * 1e-1, np.mean(sigma_vals) * 1e-2)
987
+ burst_info["radiometric"]["sigma_zero_poly_coefs"] = utils.polyfit2d_tol(
909
988
  coords_rg.flatten(),
910
989
  coords_az.flatten(),
911
- sigma[valid_lines, :].flatten(),
912
- 2,
913
- 2,
990
+ sigma_vals,
991
+ max_poly_order_rg,
992
+ max_poly_order_az,
993
+ sigma_tol,
914
994
  )
915
- burst_info["radiometric"]["beta_zero_poly_coefs"] = utils.polyfit2d(
995
+ beta_vals = beta[valid_lines, :].flatten()
996
+ beta_tol = np.maximum(np.std(beta_vals) * 1e-1, np.mean(beta_vals) * 1e-2)
997
+ burst_info["radiometric"]["beta_zero_poly_coefs"] = utils.polyfit2d_tol(
916
998
  coords_rg.flatten(),
917
999
  coords_az.flatten(),
918
- beta[valid_lines, :].flatten(),
919
- 2,
920
- 2,
1000
+ beta_vals,
1001
+ max_poly_order_rg,
1002
+ max_poly_order_az,
1003
+ beta_tol,
921
1004
  )
922
- burst_info["radiometric"]["gamma_zero_poly_coefs"] = utils.polyfit2d(
1005
+ gamma_vals = gamma[valid_lines, :].flatten()
1006
+ gamma_tol = np.maximum(np.std(gamma_vals) * 1e-1, np.mean(gamma_vals) * 1e-2)
1007
+ burst_info["radiometric"]["gamma_zero_poly_coefs"] = utils.polyfit2d_tol(
923
1008
  coords_rg.flatten(),
924
1009
  coords_az.flatten(),
925
- gamma[valid_lines, :].flatten(),
926
- 2,
927
- 2,
1010
+ gamma_vals,
1011
+ max_poly_order_rg,
1012
+ max_poly_order_az,
1013
+ gamma_tol,
928
1014
  )
929
1015
 
930
1016
  range_weight_f = azimuth_weight_f = 1.0
@@ -950,7 +1036,7 @@ def _calc_radiometric_info(cal_file_name, swath_info, burst_info_list):
950
1036
 
951
1037
  def _calc_noise_level_info(noise_file_name, swath_info, burst_info_list):
952
1038
  """Compute noise poly"""
953
- noise_root_node = et.parse(noise_file_name).getroot()
1039
+ noise_root_node = lxml.etree.parse(noise_file_name).getroot()
954
1040
  mode_id = swath_info["mode_id"]
955
1041
  lines_per_burst = swath_info["num_cols"]
956
1042
  range_size_pixels = swath_info["num_rows"]
@@ -1017,7 +1103,7 @@ def _calc_noise_level_info(noise_file_name, swath_info, burst_info_list):
1017
1103
  else:
1018
1104
  azimuth_line, azimuth_noise = None, None
1019
1105
 
1020
- rg_poly_order = min(5, range_pixel[0].size - 1)
1106
+ rg_poly_order = min(4, range_pixel[0].size - 1)
1021
1107
  first_row = swath_info["first_row"]
1022
1108
  first_col = swath_info["first_col"]
1023
1109
  scp_row = swath_info["scp_pixel"][0]
@@ -1033,12 +1119,13 @@ def _calc_noise_level_info(noise_file_name, swath_info, burst_info_list):
1033
1119
 
1034
1120
  coords_az_2d, coords_rg_2d = np.meshgrid(coords_az, coords_rg)
1035
1121
 
1036
- noise_poly = utils.polyfit2d(
1122
+ noise_poly = utils.polyfit2d_tol(
1037
1123
  coords_rg_2d.flatten(),
1038
1124
  coords_az_2d.flatten(),
1039
1125
  np.array(range_noise).flatten(),
1040
1126
  rg_poly_order,
1041
1127
  az_poly_order,
1128
+ 1e-2,
1042
1129
  )
1043
1130
  else:
1044
1131
  # TOPSAR has single LUT per burst
@@ -1083,9 +1170,8 @@ def _calc_noise_level_info(noise_file_name, swath_info, burst_info_list):
1083
1170
 
1084
1171
 
1085
1172
  def _complete_filename(swath_info, burst_info, filename_template):
1086
- core_name = burst_info["core_name"]
1087
- burst = core_name[-2:]
1088
1173
  swath = swath_info["parameters"]["SWATH"]
1174
+ burst = burst_info["parameters"]["BURST"]
1089
1175
  polarization = swath_info["tx_rcv_polarization_proc"].replace(":", "")
1090
1176
  formatted_name = filename_template.name.format(
1091
1177
  swath=swath, burst=burst, pol=polarization
@@ -1096,335 +1182,244 @@ def _complete_filename(swath_info, burst_info, filename_template):
1096
1182
 
1097
1183
 
1098
1184
  def _create_sicd_xml(base_info, swath_info, burst_info, classification):
1099
- em = lxml.builder.ElementMaker(namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]})
1100
-
1101
- # Collection Info
1102
- collection_info_node = em.CollectionInfo(
1103
- em.CollectorName(swath_info["collector_name"]),
1104
- em.CoreName(burst_info["core_name"]),
1105
- em.CollectType(base_info["collect_type"]),
1106
- em.RadarMode(
1107
- em.ModeType(base_info["mode_type"]),
1108
- em.ModeID(swath_info["mode_id"]),
1109
- ),
1110
- em.Classification(classification),
1111
- em.Parameter({"name": "SLICE"}, swath_info["parameters"]["SLICE"]),
1112
- em.Parameter({"name": "BURST"}, burst_info["parameters"]["BURST"]),
1113
- em.Parameter({"name": "SWATH"}, swath_info["parameters"]["SWATH"]),
1114
- em.Parameter(
1115
- {"name": "ORBIT_SOURCE"}, swath_info["parameters"]["ORBIT_SOURCE"]
1116
- ),
1117
- )
1118
-
1119
- # Image Creation
1120
- image_creation_node = em.ImageCreation(
1121
- em.Application(base_info["creation_application"]),
1122
- em.DateTime(base_info["creation_date_time"].strftime("%Y-%m-%dT%H:%M:%SZ")),
1123
- em.Site(base_info["creation_site"]),
1124
- em.Profile(f"sarkit-convert {__version__}"),
1185
+ sicd_xml_obj = lxml.etree.Element(
1186
+ f"{{{NSMAP['sicd']}}}SICD", nsmap={None: NSMAP["sicd"]}
1125
1187
  )
1188
+ sicd_ew = sksicd.ElementWrapper(sicd_xml_obj)
1126
1189
 
1127
- # Image Data
1128
- image_data_node = em.ImageData(
1129
- em.PixelType(swath_info["pixel_type"]),
1130
- em.NumRows(str(swath_info["num_rows"])),
1131
- em.NumCols(str(swath_info["num_cols"])),
1132
- em.FirstRow(str(swath_info["first_row"])),
1133
- em.FirstCol(str(swath_info["first_col"])),
1134
- em.FullImage(
1135
- em.NumRows(str(swath_info["num_rows"])),
1136
- em.NumCols(str(swath_info["num_cols"])),
1137
- ),
1138
- em.SCPPixel(
1139
- em.Row(str(swath_info["scp_pixel"][0])),
1140
- em.Col(str(swath_info["scp_pixel"][1])),
1141
- ),
1142
- em.ValidData(
1143
- {"size": "4"},
1144
- em.Vertex(
1145
- {"index": "1"},
1146
- em.Row(str(burst_info["valid_data"][0][0])),
1147
- em.Col(str(burst_info["valid_data"][0][1])),
1148
- ),
1149
- em.Vertex(
1150
- {"index": "2"},
1151
- em.Row(str(burst_info["valid_data"][1][0])),
1152
- em.Col(str(burst_info["valid_data"][1][1])),
1153
- ),
1154
- em.Vertex(
1155
- {"index": "3"},
1156
- em.Row(str(burst_info["valid_data"][2][0])),
1157
- em.Col(str(burst_info["valid_data"][2][1])),
1158
- ),
1159
- em.Vertex(
1160
- {"index": "4"},
1161
- em.Row(str(burst_info["valid_data"][3][0])),
1162
- em.Col(str(burst_info["valid_data"][3][1])),
1163
- ),
1164
- ),
1190
+ # Collection Info
1191
+ parameter = [
1192
+ ("SLICE", swath_info["parameters"]["SLICE"]),
1193
+ ("BURST", burst_info["parameters"]["BURST"]),
1194
+ ("SWATH", swath_info["parameters"]["SWATH"]),
1195
+ ("ORBIT_SOURCE", swath_info["parameters"]["ORBIT_SOURCE"]),
1196
+ ]
1197
+ if burst_info["parameters"].get("BURST_ID") is not None:
1198
+ parameter.append(("BURST_ID", str(burst_info["parameters"]["BURST_ID"])))
1199
+ parameter.extend(
1200
+ [
1201
+ ("MISSION_DATA_TAKE_ID", swath_info["parameters"]["MISSION_DATA_TAKE_ID"]),
1202
+ ("RELATIVE_ORBIT_NUM", str(base_info["parameters"]["RELATIVE_ORBIT_NUM"])),
1203
+ ]
1165
1204
  )
1205
+ sicd_ew["CollectionInfo"] = {
1206
+ "CollectorName": swath_info["collector_name"],
1207
+ "CoreName": burst_info["core_name"],
1208
+ "CollectType": base_info["collect_type"],
1209
+ "RadarMode": {
1210
+ "ModeType": base_info["mode_type"],
1211
+ "ModeID": swath_info["mode_id"],
1212
+ },
1213
+ "Classification": classification,
1214
+ "Parameter": parameter,
1215
+ }
1216
+ sicd_ew["ImageCreation"] = {
1217
+ "Application": base_info["creation_application"],
1218
+ "DateTime": base_info["creation_date_time"],
1219
+ }
1220
+ sicd_ew["ImageData"] = {
1221
+ "PixelType": swath_info["pixel_type"],
1222
+ "NumRows": swath_info["num_rows"],
1223
+ "NumCols": swath_info["num_cols"],
1224
+ "FirstRow": swath_info["first_row"],
1225
+ "FirstCol": swath_info["first_col"],
1226
+ "FullImage": {
1227
+ "NumRows": swath_info["num_rows"],
1228
+ "NumCols": swath_info["num_cols"],
1229
+ },
1230
+ "SCPPixel": swath_info["scp_pixel"],
1231
+ "ValidData": burst_info["valid_data"],
1232
+ }
1166
1233
 
1167
- def _make_xyz(arr):
1168
- return [em.X(str(arr[0])), em.Y(str(arr[1])), em.Z(str(arr[2]))]
1169
-
1170
- def __make_llh(arr):
1171
- return [em.Lat(str(arr[0])), em.Lon(str(arr[1])), em.HAE(str(arr[2]))]
1172
-
1173
- # Geo Data
1174
- geo_data_node = em.GeoData(
1175
- em.EarthModel("WGS_84"),
1176
- em.SCP(
1177
- em.ECF(*_make_xyz(burst_info["scp_ecf"])),
1178
- em.LLH(*__make_llh(burst_info["scp_llh"])),
1179
- ),
1180
- em.ImageCorners(),
1181
- em.ValidData(),
1182
- )
1234
+ sicd_ew["GeoData"] = {
1235
+ "EarthModel": "WGS_84",
1236
+ "SCP": {
1237
+ "ECF": burst_info["scp_ecf"],
1238
+ "LLH": burst_info["scp_llh"],
1239
+ },
1240
+ }
1183
1241
 
1184
- # Grid
1185
- grid_node = em.Grid(
1186
- em.ImagePlane(swath_info["image_plane"]),
1187
- em.Type(swath_info["grid_type"]),
1188
- em.TimeCOAPoly(),
1189
- em.Row(
1190
- em.UVectECF(*_make_xyz(burst_info["row_uvect_ecf"])),
1191
- em.SS(str(swath_info["row_ss"])),
1192
- em.ImpRespWid(str(swath_info["row_imp_res_wid"])),
1193
- em.Sgn(str(swath_info["row_sgn"])),
1194
- em.ImpRespBW(str(swath_info["row_imp_res_bw"])),
1195
- em.KCtr(str(swath_info["row_kctr"])),
1196
- em.DeltaK1(str(burst_info["row_delta_k1"])),
1197
- em.DeltaK2(str(burst_info["row_delta_k2"])),
1198
- em.DeltaKCOAPoly(),
1199
- em.WgtType(
1200
- em.WindowName(
1201
- str(swath_info["row_window_name"]),
1202
- ),
1203
- *(
1204
- [
1205
- em.Parameter(
1206
- {"name": "COEFFICIENT"},
1207
- str(swath_info["row_params"]),
1208
- )
1209
- ]
1210
- if swath_info["row_params"] is not None
1211
- else []
1212
- ),
1213
- ),
1214
- ),
1215
- em.Col(
1216
- em.UVectECF(*_make_xyz(burst_info["col_uvect_ecf"])),
1217
- em.SS(str(swath_info["col_ss"])),
1218
- em.ImpRespWid(str(swath_info["col_imp_res_wid"])),
1219
- em.Sgn(str(swath_info["col_sgn"])),
1220
- em.ImpRespBW(str(swath_info["col_imp_res_bw"])),
1221
- em.KCtr(str(swath_info["col_kctr"])),
1222
- em.DeltaK1(str(burst_info["col_delta_k1"])),
1223
- em.DeltaK2(str(burst_info["col_delta_k2"])),
1224
- em.DeltaKCOAPoly(),
1225
- em.WgtType(
1226
- em.WindowName(
1227
- str(swath_info["col_window_name"]),
1228
- ),
1229
- *(
1230
- [
1231
- em.Parameter(
1232
- {"name": "COEFFICIENT"},
1233
- str(swath_info["col_params"]),
1234
- )
1235
- ]
1236
- if swath_info["col_params"] is not None
1237
- else []
1238
- ),
1239
- ),
1240
- ),
1241
- )
1242
- sksicd.Poly2dType().set_elem(
1243
- grid_node.find("./{*}TimeCOAPoly"), burst_info["time_coa_poly_coefs"]
1244
- )
1245
- sksicd.Poly2dType().set_elem(
1246
- grid_node.find("./{*}Row/{*}DeltaKCOAPoly"), swath_info["row_deltak_coa_poly"]
1247
- )
1248
- sksicd.Poly2dType().set_elem(
1249
- grid_node.find("./{*}Col/{*}DeltaKCOAPoly"), burst_info["col_deltak_coa_poly"]
1250
- )
1251
- wgtfunc = em.WgtFunct()
1252
- sksicd.TRANSCODERS["Grid/Row/WgtFunct"].set_elem(wgtfunc, swath_info["row_wgts"])
1253
- grid_node.find("./{*}Row").append(wgtfunc)
1254
- wgtfunc = em.WgtFunct()
1255
- sksicd.TRANSCODERS["Grid/Col/WgtFunct"].set_elem(wgtfunc, swath_info["col_wgts"])
1256
- grid_node.find("./{*}Col").append(wgtfunc)
1242
+ sicd_ew["Grid"] = {
1243
+ "ImagePlane": swath_info["image_plane"],
1244
+ "Type": swath_info["grid_type"],
1245
+ "TimeCOAPoly": burst_info["time_coa_poly_coefs"],
1246
+ "Row": {
1247
+ "UVectECF": burst_info["row_uvect_ecf"],
1248
+ "SS": swath_info["row_ss"],
1249
+ "ImpRespWid": swath_info["row_imp_res_wid"],
1250
+ "Sgn": swath_info["row_sgn"],
1251
+ "ImpRespBW": swath_info["row_imp_res_bw"],
1252
+ "KCtr": swath_info["row_kctr"],
1253
+ "DeltaK1": burst_info["row_delta_k1"],
1254
+ "DeltaK2": burst_info["row_delta_k2"],
1255
+ "DeltaKCOAPoly": swath_info["row_deltak_coa_poly"],
1256
+ "WgtType": {
1257
+ "WindowName": swath_info["row_window_name"],
1258
+ },
1259
+ "WgtFunct": swath_info["row_wgts"],
1260
+ },
1261
+ "Col": {
1262
+ "UVectECF": burst_info["col_uvect_ecf"],
1263
+ "SS": swath_info["col_ss"],
1264
+ "ImpRespWid": swath_info["col_imp_res_wid"],
1265
+ "Sgn": swath_info["col_sgn"],
1266
+ "ImpRespBW": swath_info["col_imp_res_bw"],
1267
+ "KCtr": swath_info["col_kctr"],
1268
+ "DeltaK1": burst_info["col_delta_k1"],
1269
+ "DeltaK2": burst_info["col_delta_k2"],
1270
+ "DeltaKCOAPoly": burst_info["col_deltak_coa_poly"],
1271
+ "WgtType": {
1272
+ "WindowName": swath_info["col_window_name"],
1273
+ },
1274
+ "WgtFunct": swath_info["col_wgts"],
1275
+ },
1276
+ }
1277
+ if swath_info["row_params"] is not None:
1278
+ sicd_ew["Grid"]["Row"]["WgtType"]["Parameter"] = [
1279
+ ("COEFFICIENT", str(swath_info["row_params"]))
1280
+ ]
1281
+ if swath_info["col_params"] is not None:
1282
+ sicd_ew["Grid"]["Col"]["WgtType"]["Parameter"] = [
1283
+ ("COEFFICIENT", str(swath_info["col_params"]))
1284
+ ]
1257
1285
 
1258
- # Timeline
1259
- timeline_node = em.Timeline(
1260
- em.CollectStart(burst_info["collect_start"].strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
1261
- em.CollectDuration(str(burst_info["collect_duration"])),
1262
- em.IPP(
1263
- {"size": "1"},
1264
- em.Set(
1265
- {"index": "1"},
1266
- em.TStart(str(swath_info["t_start"])),
1267
- em.TEnd(str(burst_info["ipp_set_tend"])),
1268
- em.IPPStart(str(swath_info["ipp_start"])),
1269
- em.IPPEnd(str(burst_info["ipp_set_ippend"])),
1270
- em.IPPPoly(),
1271
- ),
1272
- ),
1273
- )
1274
- sksicd.PolyType().set_elem(
1275
- timeline_node.find("./{*}IPP/{*}Set/{*}IPPPoly"), swath_info["ipp_poly"]
1276
- )
1286
+ sicd_ew["Timeline"] = {
1287
+ "CollectStart": burst_info["collect_start"],
1288
+ "CollectDuration": burst_info["collect_duration"],
1289
+ "IPP": {
1290
+ "@size": 1,
1291
+ "Set": [
1292
+ {
1293
+ "@index": 1,
1294
+ "TStart": burst_info["ipp_set_tstart"],
1295
+ "TEnd": burst_info["ipp_set_tend"],
1296
+ "IPPStart": burst_info["ipp_set_ippstart"],
1297
+ "IPPEnd": burst_info["ipp_set_ippend"],
1298
+ "IPPPoly": swath_info["ipp_poly"],
1299
+ }
1300
+ ],
1301
+ },
1302
+ }
1277
1303
 
1278
- # Position
1279
- position_node = em.Position(em.ARPPoly())
1280
- sksicd.XyzPolyType().set_elem(
1281
- position_node.find("./{*}ARPPoly"), burst_info["arp_poly_coefs"]
1282
- )
1304
+ sicd_ew["Position"]["ARPPoly"] = burst_info["arp_poly_coefs"]
1283
1305
 
1284
1306
  # Radar Collection
1285
- radar_collection_node = em.RadarCollection(
1286
- em.TxFrequency(
1287
- em.Min(str(swath_info["tx_freq"][0])),
1288
- em.Max(str(swath_info["tx_freq"][1])),
1289
- ),
1290
- em.Waveform(
1291
- {"size": f"{len(swath_info['rcv_window_length'])}"},
1292
- *[
1293
- em.WFParameters(
1294
- {"index": str(i)},
1295
- em.TxPulseLength(str(swath_info["tx_pulse_length"])),
1296
- em.TxRFBandwidth(str(swath_info["tx_rf_bw"])),
1297
- em.TxFreqStart(str(swath_info["tx_freq_start"])),
1298
- em.TxFMRate(str(swath_info["tx_fm_rate"])),
1299
- em.RcvWindowLength(str(swl)),
1300
- em.ADCSampleRate(str(swath_info["adc_sample_rate"])),
1301
- )
1307
+ sicd_ew["RadarCollection"] = {
1308
+ "TxFrequency": {
1309
+ "Min": swath_info["tx_freq"][0],
1310
+ "Max": swath_info["tx_freq"][1],
1311
+ },
1312
+ "Waveform": {
1313
+ "@size": len(swath_info["rcv_window_length"]),
1314
+ "WFParameters": [
1315
+ {
1316
+ "@index": i,
1317
+ "TxPulseLength": swath_info["tx_pulse_length"],
1318
+ "TxRFBandwidth": swath_info["tx_rf_bw"],
1319
+ "TxFreqStart": swath_info["tx_freq_start"],
1320
+ "TxFMRate": swath_info["tx_fm_rate"],
1321
+ "RcvWindowLength": swl,
1322
+ "ADCSampleRate": swath_info["adc_sample_rate"],
1323
+ }
1302
1324
  for i, swl in enumerate(swath_info["rcv_window_length"], start=1)
1303
1325
  ],
1304
- ),
1305
- em.TxPolarization(swath_info["tx_polarization"]),
1306
- em.RcvChannels(
1307
- {"size": f"{len(base_info['tx_rcv_polarization'])}"},
1308
- *[
1309
- em.ChanParameters(
1310
- {"index": str(i)},
1311
- em.TxRcvPolarization(entry),
1312
- )
1326
+ },
1327
+ "TxPolarization": swath_info["tx_polarization"],
1328
+ "RcvChannels": {
1329
+ "@size": len(base_info["tx_rcv_polarization"]),
1330
+ "ChanParameters": [
1331
+ {
1332
+ "@index": i,
1333
+ "TxRcvPolarization": entry,
1334
+ }
1313
1335
  for i, entry in enumerate(base_info["tx_rcv_polarization"], start=1)
1314
1336
  ],
1315
- ),
1316
- )
1337
+ },
1338
+ }
1317
1339
 
1318
- chan_indices = None
1340
+ chan_index = None
1319
1341
  for i, pol in enumerate(base_info["tx_rcv_polarization"], start=1):
1320
1342
  if pol == swath_info["tx_rcv_polarization_proc"]:
1321
- chan_indices = str(i)
1343
+ chan_index = str(i)
1322
1344
 
1323
1345
  # Image Formation
1324
- image_formation_node = em.ImageFormation(
1325
- em.RcvChanProc(
1326
- em.NumChanProc("1"),
1327
- em.PRFScaleFactor("1"),
1328
- em.ChanIndex(chan_indices),
1329
- ),
1330
- em.TxRcvPolarizationProc(swath_info["tx_rcv_polarization_proc"]),
1331
- em.TStartProc(str(swath_info["t_start_proc"])),
1332
- em.TEndProc(str(burst_info["tend_proc"])),
1333
- em.TxFrequencyProc(
1334
- em.MinProc(str(swath_info["tx_freq_proc"][0])),
1335
- em.MaxProc(str(swath_info["tx_freq_proc"][1])),
1336
- ),
1337
- em.ImageFormAlgo(swath_info["image_form_algo"]),
1338
- em.STBeamComp(swath_info["st_beam_comp"]),
1339
- em.ImageBeamComp(swath_info["image_beam_comp"]),
1340
- em.AzAutofocus(swath_info["az_autofocus"]),
1341
- em.RgAutofocus(swath_info["rg_autofocus"]),
1346
+ now = (
1347
+ datetime.datetime.now(datetime.timezone.utc)
1348
+ .isoformat(timespec="microseconds")
1349
+ .replace("+00:00", "Z")
1342
1350
  )
1351
+ sicd_ew["ImageFormation"] = {
1352
+ "RcvChanProc": {
1353
+ "NumChanProc": 1,
1354
+ "PRFScaleFactor": 1,
1355
+ "ChanIndex": [chan_index],
1356
+ },
1357
+ "TxRcvPolarizationProc": swath_info["tx_rcv_polarization_proc"],
1358
+ "TStartProc": burst_info["tstart_proc"],
1359
+ "TEndProc": burst_info["tend_proc"],
1360
+ "TxFrequencyProc": {
1361
+ "MinProc": swath_info["tx_freq_proc"][0],
1362
+ "MaxProc": swath_info["tx_freq_proc"][1],
1363
+ },
1364
+ "ImageFormAlgo": swath_info["image_form_algo"],
1365
+ "STBeamComp": swath_info["st_beam_comp"],
1366
+ "ImageBeamComp": swath_info["image_beam_comp"],
1367
+ "AzAutofocus": swath_info["az_autofocus"],
1368
+ "RgAutofocus": swath_info["rg_autofocus"],
1369
+ "Processing": [
1370
+ {
1371
+ "Type": f"sarkit-convert {__version__} @ {now}",
1372
+ "Applied": True,
1373
+ },
1374
+ ],
1375
+ }
1343
1376
 
1344
- # RMA
1345
- rma_node = em.RMA(
1346
- em.RMAlgoType(swath_info["rm_algo_type"]),
1347
- em.ImageType(swath_info["image_type"]),
1348
- em.INCA(
1349
- em.TimeCAPoly(),
1350
- em.R_CA_SCP(str(swath_info["r_ca_scp"])),
1351
- em.FreqZero(str(swath_info["freq_zero"])),
1352
- em.DRateSFPoly(),
1353
- em.DopCentroidPoly(),
1354
- em.DopCentroidCOA(swath_info["dop_centroid_coa"]),
1355
- ),
1356
- )
1357
- sksicd.PolyType().set_elem(
1358
- rma_node.find("./{*}INCA/{*}TimeCAPoly"), burst_info["time_ca_poly_coefs"]
1359
- )
1360
- sksicd.Poly2dType().set_elem(
1361
- rma_node.find("./{*}INCA/{*}DRateSFPoly"), burst_info["drsf_poly_coefs"]
1362
- )
1363
- sksicd.Poly2dType().set_elem(
1364
- rma_node.find("./{*}INCA/{*}DopCentroidPoly"),
1365
- burst_info["doppler_centroid_poly_coefs"],
1366
- )
1377
+ sicd_ew["RMA"] = {
1378
+ "RMAlgoType": swath_info["rm_algo_type"],
1379
+ "ImageType": swath_info["image_type"],
1380
+ "INCA": {
1381
+ "TimeCAPoly": burst_info["time_ca_poly_coefs"],
1382
+ "R_CA_SCP": swath_info["r_ca_scp"],
1383
+ "FreqZero": swath_info["freq_zero"],
1384
+ "DRateSFPoly": burst_info["drsf_poly_coefs"],
1385
+ "DopCentroidPoly": burst_info["doppler_centroid_poly_coefs"],
1386
+ "DopCentroidCOA": swath_info["dop_centroid_coa"],
1387
+ },
1388
+ }
1367
1389
 
1368
- sicd_xml_obj = em.SICD(
1369
- collection_info_node,
1370
- image_creation_node,
1371
- image_data_node,
1372
- geo_data_node,
1373
- grid_node,
1374
- timeline_node,
1375
- position_node,
1376
- radar_collection_node,
1377
- image_formation_node,
1378
- rma_node,
1379
- )
1380
- breakpoint()
1381
1390
  # Add Radiometric after Sentinel baseline processing calibration update on 25 Nov 2015.
1382
1391
  if "radiometric" in burst_info:
1383
1392
  # Radiometric
1384
- radiometric_node = em.Radiometric(
1385
- em.NoiseLevel(
1386
- em.NoiseLevelType(burst_info["radiometric"]["noise_level_type"]),
1387
- em.NoisePoly(),
1388
- ),
1389
- em.RCSSFPoly(),
1390
- em.SigmaZeroSFPoly(),
1391
- em.BetaZeroSFPoly(),
1392
- em.GammaZeroSFPoly(),
1393
- )
1394
- sksicd.Poly2dType().set_elem(
1395
- radiometric_node.find("./{*}NoiseLevel/{*}NoisePoly"),
1396
- burst_info["radiometric"]["noise_poly_coefs"],
1397
- )
1398
- sksicd.Poly2dType().set_elem(
1399
- radiometric_node.find("./{*}RCSSFPoly"),
1400
- burst_info["radiometric"]["rcs_sf_poly_coefs"],
1401
- )
1402
- sksicd.Poly2dType().set_elem(
1403
- radiometric_node.find("./{*}SigmaZeroSFPoly"),
1404
- burst_info["radiometric"]["sigma_zero_poly_coefs"],
1405
- )
1406
- sksicd.Poly2dType().set_elem(
1407
- radiometric_node.find("./{*}BetaZeroSFPoly"),
1408
- burst_info["radiometric"]["beta_zero_poly_coefs"],
1409
- )
1410
- sksicd.Poly2dType().set_elem(
1411
- radiometric_node.find("./{*}GammaZeroSFPoly"),
1412
- burst_info["radiometric"]["gamma_zero_poly_coefs"],
1413
- )
1414
-
1415
- sicd_xml_obj.find("{*}RMA").addprevious(radiometric_node)
1393
+ sicd_ew["Radiometric"]["NoiseLevel"]["NoiseLevelType"] = burst_info[
1394
+ "radiometric"
1395
+ ]["noise_level_type"]
1396
+ sicd_ew["Radiometric"]["NoiseLevel"]["NoisePoly"] = burst_info["radiometric"][
1397
+ "noise_poly_coefs"
1398
+ ]
1399
+ sicd_ew["Radiometric"]["RCSSFPoly"] = burst_info["radiometric"][
1400
+ "rcs_sf_poly_coefs"
1401
+ ]
1402
+ sicd_ew["Radiometric"]["SigmaZeroSFPoly"] = burst_info["radiometric"][
1403
+ "sigma_zero_poly_coefs"
1404
+ ]
1405
+ sicd_ew["Radiometric"]["BetaZeroSFPoly"] = burst_info["radiometric"][
1406
+ "beta_zero_poly_coefs"
1407
+ ]
1408
+ sicd_ew["Radiometric"]["GammaZeroSFPoly"] = burst_info["radiometric"][
1409
+ "gamma_zero_poly_coefs"
1410
+ ]
1416
1411
 
1417
1412
  return sicd_xml_obj
1418
1413
 
1419
1414
 
1420
- def _update_geo_data(xml_helper):
1415
+ def _update_geo_data(sicd_ew):
1421
1416
  # Update ImageCorners
1422
- num_rows = xml_helper.load("./{*}ImageData/{*}NumRows")
1423
- num_cols = xml_helper.load("./{*}ImageData/{*}NumCols")
1424
- row_ss = xml_helper.load("./{*}Grid/{*}Row/{*}SS")
1425
- col_ss = xml_helper.load("./{*}Grid/{*}Col/{*}SS")
1426
- scp_pixel = xml_helper.load("./{*}ImageData/{*}SCPPixel")
1427
- scp_ecf = xml_helper.load("./{*}GeoData/{*}SCP/{*}ECF")
1417
+ num_rows = sicd_ew["ImageData"]["NumRows"]
1418
+ num_cols = sicd_ew["ImageData"]["NumCols"]
1419
+ row_ss = sicd_ew["Grid"]["Row"]["SS"]
1420
+ col_ss = sicd_ew["Grid"]["Col"]["SS"]
1421
+ scp_pixel = sicd_ew["ImageData"]["SCPPixel"]
1422
+ scp_ecf = sicd_ew["GeoData"]["SCP"]["ECF"]
1428
1423
  image_grid_locations = (
1429
1424
  np.array(
1430
1425
  [
@@ -1438,27 +1433,39 @@ def _update_geo_data(xml_helper):
1438
1433
  ) * [row_ss, col_ss]
1439
1434
 
1440
1435
  icp_ecef, _, _ = sksicd.image_to_ground_plane(
1441
- xml_helper.element_tree,
1436
+ sicd_ew.elem.getroottree(),
1442
1437
  image_grid_locations,
1443
1438
  scp_ecf,
1444
1439
  sarkit.wgs84.up(sarkit.wgs84.cartesian_to_geodetic(scp_ecf)),
1445
1440
  )
1446
1441
  icp_llh = sarkit.wgs84.cartesian_to_geodetic(icp_ecef)
1447
- xml_helper.set("./{*}GeoData/{*}ImageCorners", icp_llh[:, :2])
1448
- xml_helper.set("./{*}GeoData/{*}ValidData", icp_llh[:, :2])
1442
+ sicd_ew["GeoData"]["ImageCorners"] = icp_llh[:, :2]
1443
+ sicd_ew["GeoData"]["ValidData"] = icp_llh[:, :2]
1449
1444
 
1450
1445
 
1451
- def _update_rniirs_info(xml_helper):
1452
- em = lxml.builder.ElementMaker(namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]})
1453
- info_density, predicted_rniirs = utils.get_rniirs_estimate(xml_helper)
1454
- collection_info_node = xml_helper.element_tree.find("./{*}CollectionInfo")
1446
+ def _update_rniirs_info(sicd_ew):
1447
+ info_density, predicted_rniirs = utils.get_rniirs_estimate(sicd_ew)
1448
+ sicd_ew["CollectionInfo"].add(
1449
+ "Parameter", ("INFORMATION_DENSITY", f"{info_density:.2G}")
1450
+ )
1451
+ sicd_ew["CollectionInfo"].add(
1452
+ "Parameter", ("PREDICTED_RNIIRS", f"{predicted_rniirs:.1f}")
1453
+ )
1455
1454
 
1456
- param_node = em.Parameter({"name": "INFORMATION_DENSITY"}, f"{info_density:0.2G}")
1457
- collection_info_node.append(param_node)
1458
- param_node = em.Parameter({"name": "PREDICTED_RNIIRS"}, f"{predicted_rniirs:0.1f}")
1459
- collection_info_node.append(param_node)
1460
1455
 
1461
- return
1456
+ @contextlib.contextmanager
1457
+ def uint_tiff():
1458
+ """Overwrite SAMPLEFORMAT to force TIFFILE not to upcast from complex int16 to complex64
1459
+
1460
+ Per S1-RS-MDA-52-7441 (Sentinel-1 Product Specification) Table 6-13:
1461
+ "Interpretation of pixel format. Set to 5 (complex signed integer, 'int16') for SLC products"
1462
+ """
1463
+ oldformat = tifffile.SAMPLEFORMAT
1464
+ tifffile.SAMPLEFORMAT = lambda _: oldformat.UINT # treat as UINT
1465
+ try:
1466
+ yield
1467
+ finally:
1468
+ tifffile.SAMPLEFORMAT = oldformat
1462
1469
 
1463
1470
 
1464
1471
  def main(args=None):
@@ -1481,83 +1488,80 @@ def main(args=None):
1481
1488
  type=pathlib.Path,
1482
1489
  help="path of the output SICD file. The strings '{swath}', '{burst}', '{pol}' will be replaced as appropriate for multiple images",
1483
1490
  )
1484
- parser.add_argument(
1485
- "--ostaid",
1486
- type=str,
1487
- help="content of the originating station ID (OSTAID) field of the NITF header",
1488
- default="Unknown",
1489
- )
1490
1491
  config = parser.parse_args(args)
1491
1492
 
1492
1493
  manifest_filename = config.safe_product_folder / "manifest.safe"
1493
1494
 
1494
- manifest_root = et.parse(manifest_filename).getroot()
1495
+ manifest_root = lxml.etree.parse(manifest_filename).getroot()
1495
1496
  base_info = _collect_base_info(manifest_root)
1496
1497
  files = _get_file_sets(config.safe_product_folder, manifest_root)
1497
1498
 
1498
1499
  used_filenames = set()
1499
1500
  for entry in files:
1500
- product_root_node = et.parse(entry["product"]).getroot()
1501
+ product_root_node = lxml.etree.parse(entry["product"]).getroot()
1501
1502
  swath_info = _collect_swath_info(product_root_node)
1502
- burst_info_list = _collect_burst_info(product_root_node, swath_info)
1503
- breakpoint()
1503
+ burst_info_list = _collect_burst_info(product_root_node, base_info, swath_info)
1504
1504
  if base_info["creation_date_time"].date() >= np.datetime64("2015-11-25"):
1505
1505
  [burst_info.update({"radiometric": {}}) for burst_info in burst_info_list]
1506
1506
  _calc_radiometric_info(entry["calibration"], swath_info, burst_info_list)
1507
1507
  _calc_noise_level_info(entry["noise"], swath_info, burst_info_list)
1508
1508
 
1509
1509
  # Grab the data and write the files
1510
- with tifffile.TiffFile(entry["data"]) as tif:
1510
+ # Overwrite SAMPLEFORMAT to force TIFFILE not to upcast from complex int16 to complex64
1511
+ # Treat as uint32 and handle the conversion later
1512
+ assert swath_info["pixel_type"] != "RE16I_IM16I", (
1513
+ "pixel handling assumes 'RE16I_IM16I'"
1514
+ )
1515
+ with uint_tiff(), tifffile.TiffFile(entry["data"]) as tif:
1511
1516
  image = tif.asarray().T
1512
1517
  image_width = tif.pages[0].tags.values()[0].value
1513
1518
  begin_col = 0
1514
1519
  for burst_info in burst_info_list:
1515
- sicd = _create_sicd_xml(
1520
+ sicd_xml_obj = _create_sicd_xml(
1516
1521
  base_info, swath_info, burst_info, config.classification.upper()
1517
1522
  )
1523
+ sicd_ew = sksicd.ElementWrapper(sicd_xml_obj)
1518
1524
  # Add SCPCOA node
1519
- scp_coa = sksicd.compute_scp_coa(sicd.getroottree())
1520
- sicd.find("./{*}ImageFormation").addnext(scp_coa)
1521
- xml_helper = sksicd.XmlHelper(et.ElementTree(sicd))
1525
+ sicd_ew["SCPCOA"] = sksicd.compute_scp_coa(sicd_xml_obj.getroottree())
1522
1526
  # Update ImageCorners and ValidData
1523
- _update_geo_data(xml_helper)
1527
+ _update_geo_data(sicd_ew)
1524
1528
 
1525
1529
  # RNIIRS calcs require radiometric info
1526
1530
  if "radiometric" in burst_info:
1527
- _update_rniirs_info(xml_helper)
1531
+ _update_rniirs_info(sicd_ew)
1528
1532
 
1529
1533
  # Check for XML consistency
1530
- sicd_con = sarkit.verification.SicdConsistency(sicd)
1534
+ sicd_con = sarkit.verification.SicdConsistency(sicd_ew.elem)
1531
1535
  sicd_con.check()
1532
1536
  sicd_con.print_result(fail_detail=True)
1533
1537
 
1534
- end_col = begin_col + xml_helper.load("{*}ImageData/{*}NumCols")
1538
+ end_col = begin_col + sicd_ew["ImageData"]["NumCols"]
1535
1539
  subset = (slice(0, image_width, 1), slice(begin_col, end_col, 1))
1536
1540
  begin_col = end_col
1537
- image_subset = image[subset]
1541
+ image_subset = np.ascontiguousarray(image[subset])
1538
1542
  pixel_type = swath_info["pixel_type"]
1539
- view_dtype = sksicd.PIXEL_TYPES[pixel_type]["dtype"]
1540
- complex_data = np.empty(image_subset.shape, dtype=view_dtype)
1541
- complex_data["real"] = image_subset.real.astype(np.int16)
1542
- complex_data["imag"] = image_subset.imag.astype(np.int16)
1543
+ view_dtype = sksicd.PIXEL_TYPES[pixel_type]["dtype"].newbyteorder(
1544
+ image_subset.dtype.byteorder
1545
+ )
1546
+ complex_data = image_subset.view(view_dtype)
1543
1547
 
1544
1548
  metadata = sksicd.NitfMetadata(
1545
- xmltree=sicd.getroottree(),
1549
+ xmltree=sicd_ew.elem.getroottree(),
1546
1550
  file_header_part={
1547
- "ostaid": config.ostaid,
1548
- "ftitle": xml_helper.load("{*}CollectionInfo/{*}CoreName"),
1551
+ "ostaid": "ESA",
1552
+ "ftitle": sicd_ew["CollectionInfo"]["CoreName"],
1549
1553
  "security": {
1550
1554
  "clas": config.classification[0].upper(),
1551
1555
  "clsy": "US",
1552
1556
  },
1553
1557
  },
1554
1558
  im_subheader_part={
1555
- "iid2": xml_helper.load("{*}CollectionInfo/{*}CoreName"),
1559
+ "iid2": sicd_ew["CollectionInfo"]["CoreName"],
1556
1560
  "security": {
1557
1561
  "clas": config.classification[0].upper(),
1558
1562
  "clsy": "US",
1559
1563
  },
1560
- "isorce": xml_helper.load("{*}CollectionInfo/{*}CollectorName"),
1564
+ "isorce": sicd_ew["CollectionInfo"]["CollectorName"],
1561
1565
  },
1562
1566
  de_subheader_part={
1563
1567
  "security": {