PyGeoFetch 2.0.2__py3-none-any.whl → 2.0.4__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.
pygeofetch/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch v2.0.2 — Universal Satellite Data Pipeline.
2
+ PyGeoFetch v2.0.4 — Universal Satellite Data Pipeline.
3
3
 
4
4
  Unified access to 22+ satellite data providers with a consistent
5
5
  Python API and CLI. Supports authentication management, federated search,
@@ -30,7 +30,7 @@ CLI::
30
30
  pygeofetch doctor
31
31
  """
32
32
 
33
- __version__ = "2.0.2"
33
+ __version__ = "2.0.4"
34
34
  __author__ = "PyGeoFetch Contributors"
35
35
  __license__ = "MIT"
36
36
 
pygeofetch/cli/main.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- PyGeoFetch CLI v2.0.2 main entry point.
2
+ PyGeoFetch CLI v2.0.4 main entry point.
3
3
 
4
4
  Full command reference:
5
5
  pygeofetch auth — credential management
@@ -71,7 +71,7 @@ def cli(
71
71
  ) -> None:
72
72
  """
73
73
  \b
74
- PyGeoFetch v2.0.2 — Universal Satellite Data Pipeline
74
+ PyGeoFetch v2.0.4 — Universal Satellite Data Pipeline
75
75
  ===========================================================
76
76
  Unified access to 22+ satellite data providers.
77
77
 
pygeofetch/insar/esd.py CHANGED
@@ -18,6 +18,7 @@ for IW mode).
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from datetime import datetime, timedelta
21
22
  import logging
22
23
  from typing import List, Optional, Tuple
23
24
 
@@ -205,157 +206,342 @@ def _estimate_shift_from_overlap(
205
206
  return float(shift_seconds), median_coh
206
207
 
207
208
 
208
- def estimate_esd_shift_per_burst_overlap(
209
- ref_complex,
210
- sec_complex,
211
- swath_timing: SwathTiming,
212
- azimuth_time_interval_s: float,
213
- row_offset: int = 0,
214
- delta_f_ovl_hz: float = SENTINEL1_IW_DELTA_F_OVL_HZ,
215
- coherence_threshold: float = 0.3,
216
- ) -> Tuple[Optional[float], List[Optional[float]]]:
209
+ from typing import Dict, List, Optional, Tuple
210
+
211
+ def overlap_time_windows(
212
+ swath_timing: SwathTiming, azimuth_time_interval_s: float
213
+ ) -> List[Tuple[int, "datetime", "datetime"]]:
217
214
  """
218
- Real per-burst-overlap ESD: estimate azimuth misregistration from
219
- the actual burst overlap regions, replacing the previous whole-image
220
- single-shift approximation.
215
+ Absolute azimuth-time window of every adjacent-burst overlap -- the
216
+ ground area imaged by BOTH burst i (its trailing lines) and burst
217
+ i+1 (its leading lines) in THIS scene.
221
218
 
222
- Uses WINDOWED coherence estimation (7x7 window) and MEDIAN coherence
223
- for the quality check, matching SNAP's approach. This is critical for
224
- full-scene processing where the overlap spans the entire range width
225
- (~23,000 pixels) including ocean, vegetation, and other low-coherence
226
- terrain that would drag a global-mean coherence below threshold.
219
+ Returns:
220
+ List of (overlap_index, window_start, window_end) as absolute
221
+ azimuth datetimes.
222
+ """
223
+ bursts = swath_timing.bursts
224
+ lines_per_burst = swath_timing.lines_per_burst
225
+ out = []
226
+ for i in range(len(bursts) - 1):
227
+ start = bursts[i + 1].azimuth_time
228
+ end = bursts[i].azimuth_time + timedelta(
229
+ seconds=(lines_per_burst - 1) * azimuth_time_interval_s
230
+ )
231
+ out.append((i, start, end))
232
+ return out
227
233
 
228
- Memory optimization: subsamples in range to max 4096 columns per
229
- overlap, reducing peak memory from ~600 MB to ~100 MB per overlap
230
- while maintaining statistical robustness.
231
234
 
232
- Args:
233
- ref_complex, sec_complex: Real, coregistered (at least coarsely)
234
- complex SLC arrays, same shape -- full burst stack, or an
235
- already-cropped extract (see row_offset).
236
- swath_timing: Real burst metadata from annotation.parse_burst_info().
237
- azimuth_time_interval_s: Real per-line time spacing.
238
- row_offset: Real full-scene row that ref_complex/sec_complex's
239
- row 0 corresponds to. Default 0 assumes the arrays start at
240
- the full-scene row 0.
241
- delta_f_ovl_hz: Doppler centroid difference in the overlap.
242
- coherence_threshold: Minimum MEDIAN windowed coherence for an
243
- overlap to be considered usable.
244
235
 
245
- Returns:
246
- (shift_seconds, per_overlap_shifts): shift_seconds is the real,
247
- median azimuth timing shift (seconds) across all valid burst
248
- overlaps (None if no overlap gave a usable estimate);
249
- per_overlap_shifts is one entry per adjacent burst pair (None
250
- where that overlap's coherence was too low or fell entirely
251
- outside the given arrays).
236
+ def compute_common_ground_overlaps(
237
+ ref_timing: SwathTiming,
238
+ sec_timing: SwathTiming,
239
+ azimuth_time_interval_s: float,
240
+ ) -> List[Dict]:
252
241
  """
253
- import numpy as np
242
+ Per-reference-overlap diagnostic of how much COMMON double-covered
243
+ ground (in azimuth lines) this date pair actually shares.
254
244
 
255
- if not isinstance(swath_timing, SwathTiming):
256
- raise TypeError(
257
- f"estimate_esd_shift_per_burst_overlap expects a SwathTiming "
258
- f"object, got {type(swath_timing).__name__}. Pass the result of "
259
- f"parse_burst_info(), not a raw bursts list."
260
- )
245
+ For each reference overlap window, intersect it with every secondary
246
+ overlap window and keep the largest intersection. That intersection
247
+ length is the only ground on which the ESD double-difference is
248
+ physically meaningful for this pair.
261
249
 
262
- overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
263
- per_overlap_shifts: List[Optional[float]] = []
250
+ This is the number that cleanly separates the two regimes:
251
+ - same-family pairs (identical burst timing): common lines ~= the
252
+ full overlap length (tens of lines per overlap);
253
+ - cross-family pairs (8 vs 9 bursts, shifted timing): common lines
254
+ shrink toward 0, which is exactly why ESD finds few/no usable
255
+ overlaps for them -- a physical limitation, not a bug.
264
256
 
265
- n_outside = 0
266
- n_below_coh = 0
267
- n_usable = 0
257
+ Returns:
258
+ List of dicts, one per reference overlap, each with:
259
+ ref_overlap_index, ref_window, best_sec_overlap_index,
260
+ sec_window, common_lines, common_window
261
+ """
262
+ dt = azimuth_time_interval_s
263
+ ref_wins = overlap_time_windows(ref_timing, dt)
264
+ sec_wins = overlap_time_windows(sec_timing, dt)
265
+
266
+ report = []
267
+ for i, r_start, r_end in ref_wins:
268
+ best_j, best_lines, best_win = None, 0, None
269
+ for j, s_start, s_end in sec_wins:
270
+ t0, t1 = max(r_start, s_start), min(r_end, s_end)
271
+ if t1 <= t0:
272
+ continue # no intersection for this secondary overlap
273
+ n_lines = int(round((t1 - t0).total_seconds() / dt)) + 1
274
+ if n_lines > best_lines:
275
+ best_j, best_lines, best_win = j, n_lines, (t0, t1)
276
+ report.append({
277
+ "ref_overlap_index": i,
278
+ "ref_window": (r_start, r_end),
279
+ "best_sec_overlap_index": best_j,
280
+ "sec_window": best_win,
281
+ "common_lines": best_lines,
282
+ "common_window": best_win,
283
+ })
284
+ return report
268
285
 
269
- h, w = ref_complex.shape
286
+ def estimate_esd_shift_per_burst_overlap(
287
+ ref_complex, sec_complex,
288
+ swath_timing, azimuth_time_interval_s,
289
+ row_offset: int = 0,
290
+ sec_swath_timing=None, # NEW: secondary burst timing
291
+ delta_f_ovl_hz=SENTINEL1_IW_DELTA_F_OVL_HZ,
292
+ coherence_threshold: float = 0.2,
293
+ min_common_lines: int = 3,
294
+ ):
295
+ import numpy as np
296
+ from scipy.ndimage import uniform_filter
270
297
 
271
- for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
272
- # Map full-scene rows to local array rows
273
- local_bw_start = bw_start - row_offset
274
- local_bw_end = bw_end - row_offset
275
- local_fw_start = fw_start - row_offset
276
- local_fw_end = fw_end - row_offset
298
+ dt = azimuth_time_interval_s
299
+ L = swath_timing.lines_per_burst
300
+ ref_wins = overlap_time_windows(swath_timing, dt)
301
+ # If no secondary timing given, fall back to ref-only (old behaviour)
302
+ sec_wins = (overlap_time_windows(sec_swath_timing, dt)
303
+ if sec_swath_timing is not None else ref_wins)
304
+
305
+ per_overlap_shifts = []
306
+ skip_reasons = []
307
+
308
+ for (i, r_start, r_end) in ref_wins:
309
+ # Find the secondary overlap with the largest time intersection
310
+ best = None
311
+ for (j, s_start, s_end) in sec_wins:
312
+ t0, t1 = max(r_start, s_start), min(r_end, s_end)
313
+ n_lines = int((t1 - t0).total_seconds() / dt) + 1
314
+ if best is None or n_lines > best[0]:
315
+ best = (n_lines, t0, t1)
316
+ n_lines, t0, t1 = best
317
+
318
+ if n_lines < min_common_lines:
319
+ per_overlap_shifts.append(None)
320
+ skip_reasons.append("no_common_ground")
321
+ continue
277
322
 
278
- # Check bounds
279
- if (local_bw_start < 0 or local_bw_end >= h
280
- or local_fw_start < 0 or local_fw_end >= h):
281
- n_outside += 1
323
+ # Map the common time window to reference rows (bw=burst i tail,
324
+ # fw=burst i+1 head). Secondary is coregistered to the ref grid,
325
+ # so it is indexed at the same rows.
326
+ bw_rows, fw_rows = [], []
327
+ for k in range(n_lines):
328
+ tt = t0 + timedelta(seconds=k * dt)
329
+ bw_rows.append(i * L + round((tt - swath_timing.bursts[i].azimuth_time).total_seconds() / dt) - row_offset)
330
+ fw_rows.append((i + 1) * L + round((tt - swath_timing.bursts[i + 1].azimuth_time).total_seconds() / dt) - row_offset)
331
+
332
+ bw = np.array([r for r in bw_rows if 0 <= r < ref_complex.shape[0]])
333
+ fw = np.array([r for r in fw_rows if 0 <= r < ref_complex.shape[0]])
334
+ if len(bw) < min_common_lines or len(fw) < min_common_lines:
282
335
  per_overlap_shifts.append(None)
283
- logger.debug(
284
- "Overlap %d<->%d: outside array bounds "
285
- "(local rows bw=[%d,%d], fw=[%d,%d], array height=%d)",
286
- i, i + 1, local_bw_start, local_bw_end,
287
- local_fw_start, local_fw_end, h,
288
- )
336
+ skip_reasons.append("outside_array_bounds")
289
337
  continue
290
338
 
291
- # Extract overlap regions
292
- ref_bw = ref_complex[local_bw_start:local_bw_end + 1, :]
293
- sec_bw = sec_complex[local_bw_start:local_bw_end + 1, :]
294
- ref_fw = ref_complex[local_fw_start:local_fw_end + 1, :]
295
- sec_fw = sec_complex[local_fw_start:local_fw_end + 1, :]
296
-
297
- # Estimate shift with windowed coherence
298
- shift_s, median_coh = _estimate_shift_from_overlap(
299
- ref_bw, sec_bw, ref_fw, sec_fw,
300
- delta_f_ovl_hz=delta_f_ovl_hz,
301
- coherence_threshold=coherence_threshold,
302
- )
339
+ ref_bw, sec_bw = ref_complex[bw], sec_complex[bw]
340
+ ref_fw, sec_fw = ref_complex[fw], sec_complex[fw]
341
+ igram_bw = ref_bw * np.conj(sec_bw)
342
+ igram_fw = ref_fw * np.conj(sec_fw)
343
+
344
+ # Windowed coherence gate (same as before)
345
+ n_common = len(bw)
346
+ coh_window = min(5, n_common) if n_common >= 3 else 1
347
+ if coh_window >= 3:
348
+ num = np.abs(uniform_filter(igram_bw.real, size=coh_window)
349
+ + 1j * uniform_filter(igram_bw.imag, size=coh_window))
350
+ denom = np.sqrt(uniform_filter(np.abs(ref_bw)**2, size=coh_window)
351
+ * uniform_filter(np.abs(sec_bw)**2, size=coh_window) + 1e-10)
352
+ valid = np.isfinite(num / denom) & ((num / denom) >= coherence_threshold)
353
+ else:
354
+ valid = np.ones(igram_bw.shape, dtype=bool)
303
355
 
304
- if shift_s is None:
305
- n_below_coh += 1
356
+ if valid.mean() < 0.5:
306
357
  per_overlap_shifts.append(None)
307
- logger.debug(
308
- "Overlap %d<->%d: median windowed coherence %.3f < threshold %.2f",
309
- i, i + 1, median_coh, coherence_threshold,
310
- )
311
- else:
312
- n_usable += 1
313
- per_overlap_shifts.append(shift_s)
314
- logger.debug(
315
- "Overlap %d<->%d: shift=%.6f ms, median_coh=%.3f",
316
- i, i + 1, shift_s * 1000, median_coh,
317
- )
358
+ skip_reasons.append(f"low_coherence({100*valid.mean():.0f}%)")
359
+ continue
318
360
 
319
- # Report diagnostics
320
- if n_usable == 0:
321
- if n_outside > 0 and n_below_coh > 0:
322
- logger.warning(
323
- "Real per-burst-overlap ESD found no usable burst overlaps "
324
- "out of %d total: %d fell outside the given (possibly "
325
- "cropped) arrays, %d were below coherence_threshold=%.2f. "
326
- "A mix of both -- see the per-overlap detail at DEBUG level.",
327
- len(overlaps), n_outside, n_below_coh, coherence_threshold,
328
- )
329
- elif n_outside > 0:
330
- logger.warning(
331
- "Real per-burst-overlap ESD found no usable burst overlaps "
332
- "out of %d total: all %d fell outside the given (possibly "
333
- "cropped) arrays.",
334
- len(overlaps), n_outside,
335
- )
336
- else:
337
- logger.warning(
338
- "Real per-burst-overlap ESD found no usable burst overlaps "
339
- "out of %d total: 0 fell outside the given (possibly "
340
- "cropped) arrays, %d were below coherence_threshold=%.2f. "
341
- "Every real, in-bounds overlap was tested and found "
342
- "genuinely below the coherence threshold -- this reflects "
343
- "real decorrelation in the data, not something a larger "
344
- "AOI would fix.",
345
- len(overlaps), n_below_coh, coherence_threshold,
346
- )
347
- return None, per_overlap_shifts
361
+ double_diff = igram_fw * np.conj(igram_bw)
362
+ mean_phase = np.angle(np.mean(double_diff[valid]))
363
+ per_overlap_shifts.append(float(mean_phase / (2 * np.pi * delta_f_ovl_hz)))
364
+ skip_reasons.append("used")
348
365
 
349
- # Combine valid shifts via median (robust to outliers)
350
366
  valid_shifts = [s for s in per_overlap_shifts if s is not None]
351
- combined_shift = float(np.median(valid_shifts))
352
-
353
- logger.info(
354
- "Real per-burst-overlap ESD: %d/%d overlaps usable, "
355
- "combined shift=%.6f ms (%.4f px)",
356
- n_usable, len(overlaps),
357
- combined_shift * 1000,
358
- combined_shift / azimuth_time_interval_s,
359
- )
367
+ if not valid_shifts:
368
+ n_outside = skip_reasons.count("outside_array_bounds")
369
+ n_nocommon = skip_reasons.count("no_common_ground")
370
+ n_low = sum(1 for r in skip_reasons if r.startswith("low_coherence"))
371
+ logger.warning(
372
+ "ESD: no usable overlaps of %d (%d no common ground, %d outside, %d low coh)",
373
+ len(ref_wins), n_nocommon, n_outside, n_low)
374
+ return None, per_overlap_shifts
360
375
 
361
- return combined_shift, per_overlap_shifts
376
+ combined = float(np.median(valid_shifts))
377
+ logger.info("ESD common-ground: %d/%d usable, shift=%.6f ms (%.4f px)",
378
+ len(valid_shifts), len(ref_wins), combined*1000, combined/dt)
379
+ return combined, per_overlap_shifts
380
+
381
+
382
+ # def estimate_esd_shift_per_burst_overlap(
383
+ # ref_complex,
384
+ # sec_complex,
385
+ # swath_timing: SwathTiming,
386
+ # azimuth_time_interval_s: float,
387
+ # row_offset: int = 0,
388
+ # delta_f_ovl_hz: float = SENTINEL1_IW_DELTA_F_OVL_HZ,
389
+ # coherence_threshold: float = 0.3,
390
+ # ) -> Tuple[Optional[float], List[Optional[float]]]:
391
+ # """
392
+ # Real per-burst-overlap ESD: estimate azimuth misregistration from
393
+ # the actual burst overlap regions, replacing the previous whole-image
394
+ # single-shift approximation.
395
+
396
+ # Uses WINDOWED coherence estimation (7x7 window) and MEDIAN coherence
397
+ # for the quality check, matching SNAP's approach. This is critical for
398
+ # full-scene processing where the overlap spans the entire range width
399
+ # (~23,000 pixels) including ocean, vegetation, and other low-coherence
400
+ # terrain that would drag a global-mean coherence below threshold.
401
+
402
+ # Memory optimization: subsamples in range to max 4096 columns per
403
+ # overlap, reducing peak memory from ~600 MB to ~100 MB per overlap
404
+ # while maintaining statistical robustness.
405
+
406
+ # Args:
407
+ # ref_complex, sec_complex: Real, coregistered (at least coarsely)
408
+ # complex SLC arrays, same shape -- full burst stack, or an
409
+ # already-cropped extract (see row_offset).
410
+ # swath_timing: Real burst metadata from annotation.parse_burst_info().
411
+ # azimuth_time_interval_s: Real per-line time spacing.
412
+ # row_offset: Real full-scene row that ref_complex/sec_complex's
413
+ # row 0 corresponds to. Default 0 assumes the arrays start at
414
+ # the full-scene row 0.
415
+ # delta_f_ovl_hz: Doppler centroid difference in the overlap.
416
+ # coherence_threshold: Minimum MEDIAN windowed coherence for an
417
+ # overlap to be considered usable.
418
+
419
+ # Returns:
420
+ # (shift_seconds, per_overlap_shifts): shift_seconds is the real,
421
+ # median azimuth timing shift (seconds) across all valid burst
422
+ # overlaps (None if no overlap gave a usable estimate);
423
+ # per_overlap_shifts is one entry per adjacent burst pair (None
424
+ # where that overlap's coherence was too low or fell entirely
425
+ # outside the given arrays).
426
+ # """
427
+ # import numpy as np
428
+
429
+ # if not isinstance(swath_timing, SwathTiming):
430
+ # raise TypeError(
431
+ # f"estimate_esd_shift_per_burst_overlap expects a SwathTiming "
432
+ # f"object, got {type(swath_timing).__name__}. Pass the result of "
433
+ # f"parse_burst_info(), not a raw bursts list."
434
+ # )
435
+
436
+ # overlaps = compute_overlap_row_ranges(swath_timing, azimuth_time_interval_s)
437
+ # per_overlap_shifts: List[Optional[float]] = []
438
+
439
+ # n_outside = 0
440
+ # n_below_coh = 0
441
+ # n_usable = 0
442
+
443
+ # h, w = ref_complex.shape
444
+
445
+ # for i, ((bw_start, bw_end), (fw_start, fw_end)) in enumerate(overlaps):
446
+ # # Map full-scene rows to local array rows
447
+ # local_bw_start = bw_start - row_offset
448
+ # local_bw_end = bw_end - row_offset
449
+ # local_fw_start = fw_start - row_offset
450
+ # local_fw_end = fw_end - row_offset
451
+
452
+ # # Check bounds
453
+ # if (local_bw_start < 0 or local_bw_end >= h
454
+ # or local_fw_start < 0 or local_fw_end >= h):
455
+ # n_outside += 1
456
+ # per_overlap_shifts.append(None)
457
+ # logger.debug(
458
+ # "Overlap %d<->%d: outside array bounds "
459
+ # "(local rows bw=[%d,%d], fw=[%d,%d], array height=%d)",
460
+ # i, i + 1, local_bw_start, local_bw_end,
461
+ # local_fw_start, local_fw_end, h,
462
+ # )
463
+ # continue
464
+
465
+ # # Extract overlap regions
466
+ # ref_bw = ref_complex[local_bw_start:local_bw_end + 1, :]
467
+ # sec_bw = sec_complex[local_bw_start:local_bw_end + 1, :]
468
+ # ref_fw = ref_complex[local_fw_start:local_fw_end + 1, :]
469
+ # sec_fw = sec_complex[local_fw_start:local_fw_end + 1, :]
470
+
471
+ # # Estimate shift with windowed coherence
472
+ # shift_s, median_coh = _estimate_shift_from_overlap(
473
+ # ref_bw, sec_bw, ref_fw, sec_fw,
474
+ # delta_f_ovl_hz=delta_f_ovl_hz,
475
+ # coherence_threshold=coherence_threshold,
476
+ # )
477
+
478
+ # if shift_s is None:
479
+ # n_below_coh += 1
480
+ # per_overlap_shifts.append(None)
481
+ # logger.debug(
482
+ # "Overlap %d<->%d: median windowed coherence %.3f < threshold %.2f",
483
+ # i, i + 1, median_coh, coherence_threshold,
484
+ # )
485
+ # else:
486
+ # n_usable += 1
487
+ # per_overlap_shifts.append(shift_s)
488
+ # logger.debug(
489
+ # "Overlap %d<->%d: shift=%.6f ms, median_coh=%.3f",
490
+ # i, i + 1, shift_s * 1000, median_coh,
491
+ # )
492
+
493
+ # # Report diagnostics
494
+ # if n_usable == 0:
495
+ # if n_outside > 0 and n_below_coh > 0:
496
+ # logger.warning(
497
+ # "Real per-burst-overlap ESD found no usable burst overlaps "
498
+ # "out of %d total: %d fell outside the given (possibly "
499
+ # "cropped) arrays, %d were below coherence_threshold=%.2f. "
500
+ # "A mix of both -- see the per-overlap detail at DEBUG level.",
501
+ # len(overlaps), n_outside, n_below_coh, coherence_threshold,
502
+ # )
503
+ # elif n_outside > 0:
504
+ # logger.warning(
505
+ # "Real per-burst-overlap ESD found no usable burst overlaps "
506
+ # "out of %d total: all %d fell outside the given (possibly "
507
+ # "cropped) arrays.",
508
+ # len(overlaps), n_outside,
509
+ # )
510
+ # else:
511
+ # logger.warning(
512
+ # "Real per-burst-overlap ESD found no usable burst overlaps "
513
+ # "out of %d total: 0 fell outside the given (possibly "
514
+ # "cropped) arrays, %d were below coherence_threshold=%.2f. "
515
+ # "Every real, in-bounds overlap was tested and found "
516
+ # "genuinely below the coherence threshold -- this reflects "
517
+ # "real decorrelation in the data, not something a larger "
518
+ # "AOI would fix.",
519
+ # len(overlaps), n_below_coh, coherence_threshold,
520
+ # )
521
+ # return None, per_overlap_shifts
522
+
523
+ # # Combine valid shifts via median (robust to outliers)
524
+ # valid_shifts = [s for s in per_overlap_shifts if s is not None]
525
+ # combined_shift = float(np.median(valid_shifts))
526
+
527
+ # logger.info(
528
+ # "Real per-burst-overlap ESD: %d/%d overlaps usable, "
529
+ # "combined shift=%.6f ms (%.4f px)",
530
+ # n_usable, len(overlaps),
531
+ # combined_shift * 1000,
532
+ # combined_shift / azimuth_time_interval_s,
533
+ # )
534
+
535
+ # return combined_shift, per_overlap_shifts
536
+
537
+
538
+ # def overlap_time_windows(swath_timing, dt):
539
+ # """Absolute azimuth-time windows for each adjacent-burst overlap."""
540
+ # bursts = swath_timing.bursts
541
+ # L = swath_timing.lines_per_burst
542
+ # out = []
543
+ # for i in range(len(bursts) - 1):
544
+ # start = bursts[i + 1].azimuth_time
545
+ # end = bursts[i].azimuth_time + timedelta(seconds=(L - 1) * dt)
546
+ # out.append((i, start, end))
547
+ # return out