Skip completed GF3 scenes on rerun
This commit is contained in:
@@ -42,6 +42,8 @@ The wrapper writes one output subdirectory per scene. On successful runs it save
|
|||||||
|
|
||||||
Batch directory runs continue after per-scene failures. If one archive is truncated, invalid, missing metadata, or fails during SARscape processing, the wrapper prints the failed scene and reason, skips that scene, and continues with the remaining scenes. At the end it prints a batch summary and returns a non-zero exit code when any scene failed.
|
Batch directory runs continue after per-scene failures. If one archive is truncated, invalid, missing metadata, or fails during SARscape processing, the wrapper prints the failed scene and reason, skips that scene, and continues with the remaining scenes. At the end it prints a batch summary and returns a non-zero exit code when any scene failed.
|
||||||
|
|
||||||
|
Before processing each scene, the wrapper checks the scene output directory for completed geocoded products for the requested polarizations. Already completed scenes are skipped, so rerunning the same input/output pair only processes missing or incomplete scenes.
|
||||||
|
|
||||||
More details: [docs/sarscape_go_wrapper.md](docs/sarscape_go_wrapper.md)
|
More details: [docs/sarscape_go_wrapper.md](docs/sarscape_go_wrapper.md)
|
||||||
|
|
||||||
## Python Pipeline
|
## Python Pipeline
|
||||||
|
|||||||
Vendored
+2
@@ -16,4 +16,6 @@ The executable requires ENVI/IDL Runtime and SARscape on the target machine.
|
|||||||
|
|
||||||
In directory mode, failed scenes are skipped and listed in a final batch summary. The wrapper continues with remaining scenes, but exits non-zero if any scene failed.
|
In directory mode, failed scenes are skipped and listed in a final batch summary. The wrapper continues with remaining scenes, but exits non-zero if any scene failed.
|
||||||
|
|
||||||
|
Rerunning the same input/output pair skips scenes with complete geocoded outputs for the requested polarizations and processes only missing or incomplete scenes.
|
||||||
|
|
||||||
After successful runs, `gf3wrapper.json` is created in this directory to store defaults. That file is local runtime state and is ignored by Git.
|
After successful runs, `gf3wrapper.json` is created in this directory to store defaults. That file is local runtime state and is ignored by Git.
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -56,6 +56,20 @@ Failed scenes:
|
|||||||
|
|
||||||
If any scene failed, the process exits with a non-zero status after the summary. This lets scheduled jobs detect that the batch needs attention while still preserving successful outputs from other scenes.
|
If any scene failed, the process exits with a non-zero status after the summary. This lets scheduled jobs detect that the batch needs attention while still preserving successful outputs from other scenes.
|
||||||
|
|
||||||
|
## Resume Behavior
|
||||||
|
|
||||||
|
Before extracting or processing a scene, the wrapper checks the scene output directory for completed geocoded SARscape products for every requested polarization. A product is considered complete when both the final `*_geo.sml` metadata file and its matching data file exist and are non-empty.
|
||||||
|
|
||||||
|
Already completed scenes are skipped:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SKIPPED GF3_...
|
||||||
|
Input D:\GF3\L1A_BATCH\GF3_....tar.gz
|
||||||
|
Output already complete E:\GF3\L2_SARscape\GF3_...
|
||||||
|
```
|
||||||
|
|
||||||
|
This is intended for interrupted or partially failed batch runs. For example, after replacing a truncated archive in the original input directory, rerun the same command with the same output directory; scenes that already have complete HH/HV geocoded outputs are skipped, and only missing or incomplete scenes are processed.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
Each scene is processed into its own output directory:
|
Each scene is processed into its own output directory:
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ If `gf3wrapper.json` exists next to the executable, omitted `-input`, `-output`,
|
|||||||
|
|
||||||
In batch directory mode, a failed scene does not stop the whole run. Invalid or truncated archives, missing metadata, and SARscape failures are reported with the scene input path and reason, then the wrapper continues with the next scene. A final summary lists succeeded and failed scenes, and the process exits non-zero when any scene failed.
|
In batch directory mode, a failed scene does not stop the whole run. Invalid or truncated archives, missing metadata, and SARscape failures are reported with the scene input path and reason, then the wrapper continues with the next scene. A final summary lists succeeded and failed scenes, and the process exits non-zero when any scene failed.
|
||||||
|
|
||||||
|
Reruns with the same output directory skip scenes that already have complete `*_geo.sml` outputs and matching data files for the requested polarizations. This lets a repaired archive be placed back into the original input folder without reprocessing scenes that already finished.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The server still needs ENVI, IDL Runtime, and SARscape installed and licensed.
|
- The server still needs ENVI, IDL Runtime, and SARscape installed and licensed.
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ func (e batchError) Error() string {
|
|||||||
return fmt.Sprintf("batch completed with %d failed scene(s)", len(e.failures))
|
return fmt.Sprintf("batch completed with %d failed scene(s)", len(e.failures))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type completedProduct struct {
|
||||||
|
polarization string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := parseFlags()
|
cfg := parseFlags()
|
||||||
if len(os.Args) == 1 {
|
if len(os.Args) == 1 {
|
||||||
@@ -252,11 +257,6 @@ func run(cfg config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
savPath, err := materializeSAV(cfg.output)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenes, err := discoverScenes(cfg)
|
scenes, err := discoverScenes(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -266,9 +266,16 @@ func run(cfg config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
failures := make([]sceneFailure, 0)
|
failures := make([]sceneFailure, 0)
|
||||||
succeeded := 0
|
processed := 0
|
||||||
|
skipped := 0
|
||||||
|
savPath := ""
|
||||||
for _, sc := range scenes {
|
for _, sc := range scenes {
|
||||||
sceneOut := filepath.Join(cfg.output, sc.name)
|
sceneOut := filepath.Join(cfg.output, sc.name)
|
||||||
|
if products, ok := completedSceneOutputs(sceneOut, sc.name, cfg.polarizations); ok {
|
||||||
|
recordSceneSkip(sc, sceneOut, products)
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err := os.MkdirAll(sceneOut, 0o755); err != nil {
|
if err := os.MkdirAll(sceneOut, 0o755); err != nil {
|
||||||
recordSceneFailure(&failures, sc, err)
|
recordSceneFailure(&failures, sc, err)
|
||||||
continue
|
continue
|
||||||
@@ -292,27 +299,39 @@ func run(cfg config) error {
|
|||||||
sc.metaPath = extractedScenes[0].metaPath
|
sc.metaPath = extractedScenes[0].metaPath
|
||||||
sc.name = sceneName(sc.metaPath)
|
sc.name = sceneName(sc.metaPath)
|
||||||
}
|
}
|
||||||
|
if savPath == "" {
|
||||||
|
var err error
|
||||||
|
savPath, err = materializeSAV(cfg.output)
|
||||||
|
if err != nil {
|
||||||
|
recordSceneFailure(&failures, sc, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := runScene(cfg, savPath, sc, sceneOut); err != nil {
|
if err := runScene(cfg, savPath, sc, sceneOut); err != nil {
|
||||||
recordSceneFailure(&failures, sc, err)
|
recordSceneFailure(&failures, sc, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
succeeded++
|
processed++
|
||||||
}
|
}
|
||||||
printBatchSummary(len(scenes), succeeded, failures)
|
printBatchSummary(len(scenes), processed, skipped, failures)
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
return batchError{failures: failures}
|
return batchError{failures: failures}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordSceneFailure(failures *[]sceneFailure, sc scene, err error) {
|
func sceneInput(sc scene) string {
|
||||||
input := sc.metaPath
|
|
||||||
if sc.archivePath != "" {
|
if sc.archivePath != "" {
|
||||||
input = sc.archivePath
|
return sc.archivePath
|
||||||
}
|
}
|
||||||
if input == "" {
|
if sc.metaPath != "" {
|
||||||
input = sc.name
|
return sc.metaPath
|
||||||
}
|
}
|
||||||
|
return sc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordSceneFailure(failures *[]sceneFailure, sc scene, err error) {
|
||||||
|
input := sceneInput(sc)
|
||||||
|
|
||||||
failure := sceneFailure{name: sc.name, input: input, err: err}
|
failure := sceneFailure{name: sc.name, input: input, err: err}
|
||||||
*failures = append(*failures, failure)
|
*failures = append(*failures, failure)
|
||||||
@@ -323,9 +342,21 @@ func recordSceneFailure(failures *[]sceneFailure, sc scene, err error) {
|
|||||||
fmt.Println("Continuing with next scene.")
|
fmt.Println("Continuing with next scene.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func printBatchSummary(total, succeeded int, failures []sceneFailure) {
|
func recordSceneSkip(sc scene, sceneOut string, products []completedProduct) {
|
||||||
|
parts := make([]string, 0, len(products))
|
||||||
|
for _, product := range products {
|
||||||
|
parts = append(parts, product.polarization+"="+product.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("SKIPPED %s\n", sc.name)
|
||||||
|
fmt.Printf("Input %s\n", sceneInput(sc))
|
||||||
|
fmt.Printf("Output already complete %s\n", sceneOut)
|
||||||
|
fmt.Printf("Products %s\n", strings.Join(parts, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBatchSummary(total, processed, skipped int, failures []sceneFailure) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Batch summary: %d succeeded, %d failed, %d total\n", succeeded, len(failures), total)
|
fmt.Printf("Batch summary: %d processed, %d skipped, %d failed, %d total\n", processed, skipped, len(failures), total)
|
||||||
if len(failures) == 0 {
|
if len(failures) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -536,6 +567,55 @@ func progressStage(sceneOut, sceneNameValue, polarizations string) (string, int,
|
|||||||
return "finishing", done, len(checks)
|
return "finishing", done, len(checks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func completedSceneOutputs(sceneOut, sceneNameValue, polarizations string) ([]completedProduct, bool) {
|
||||||
|
info, err := os.Stat(sceneOut)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
pols := requestedPolarizations(polarizations)
|
||||||
|
products := make([]completedProduct, 0, len(pols))
|
||||||
|
for _, pol := range pols {
|
||||||
|
path, ok := completedGeoProduct(sceneOut, sceneNameValue, pol)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
products = append(products, completedProduct{polarization: pol, path: path})
|
||||||
|
}
|
||||||
|
return products, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func completedGeoProduct(sceneOut, sceneNameValue, polarization string) (string, bool) {
|
||||||
|
lower := strings.ToLower(polarization)
|
||||||
|
exact := filepath.Join(sceneOut, sceneNameValue+"_"+lower+"_geo.sml")
|
||||||
|
if completeGeoSML(exact) {
|
||||||
|
return exact, true
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(filepath.Join(sceneOut, "*_"+lower+"_geo.sml"))
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
sort.Strings(matches)
|
||||||
|
for _, match := range matches {
|
||||||
|
if completeGeoSML(match) {
|
||||||
|
return match, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeGeoSML(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || info.IsDir() || info.Size() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dataPath := strings.TrimSuffix(path, ".sml")
|
||||||
|
dataInfo, err := os.Stat(dataPath)
|
||||||
|
return err == nil && !dataInfo.IsDir() && dataInfo.Size() > 0
|
||||||
|
}
|
||||||
|
|
||||||
func requestedPolarizations(value string) []string {
|
func requestedPolarizations(value string) []string {
|
||||||
parts := strings.Split(value, ",")
|
parts := strings.Split(value, ",")
|
||||||
out := make([]string, 0, len(parts))
|
out := make([]string, 0, len(parts))
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompletedSceneOutputsRequiresGeoSMLAndDataForAllPolarizations(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
sceneName := "GF3_TEST_SCENE"
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hh_geo.sml"), "hh sml")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hh_geo"), "hh data")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hv_geo.sml"), "hv sml")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hv_geo"), "hv data")
|
||||||
|
|
||||||
|
products, ok := completedSceneOutputs(dir, sceneName, "HH,HV")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected completed scene outputs")
|
||||||
|
}
|
||||||
|
if len(products) != 2 {
|
||||||
|
t.Fatalf("expected 2 completed products, got %d", len(products))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletedSceneOutputsRejectsMissingDataFile(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
sceneName := "GF3_TEST_SCENE"
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hh_geo.sml"), "hh sml")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hv_geo.sml"), "hv sml")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hv_geo"), "hv data")
|
||||||
|
|
||||||
|
if _, ok := completedSceneOutputs(dir, sceneName, "HH,HV"); ok {
|
||||||
|
t.Fatal("expected incomplete scene when one geocoded data file is missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletedSceneOutputsRequiresRequestedPolarizations(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
sceneName := "GF3_TEST_SCENE"
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hh_geo.sml"), "hh sml")
|
||||||
|
writeFile(t, filepath.Join(dir, sceneName+"_hh_geo"), "hh data")
|
||||||
|
|
||||||
|
if _, ok := completedSceneOutputs(dir, sceneName, "HH,HV"); ok {
|
||||||
|
t.Fatal("expected incomplete scene when HV output is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := completedSceneOutputs(dir, sceneName, "HH"); !ok {
|
||||||
|
t.Fatal("expected completed scene for HH-only request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(t *testing.T, path, value string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user