From 8b3c248f456ef37a8e4973f0bffbf247b22d8602 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Mon, 8 Jun 2026 15:28:40 -0700 Subject: [PATCH 01/13] Decouple validation methods in transient notebook Method 1 (GNSS-InSAR comparison) can now fail without blocking Method 2 (noise level validation). Changes: - Wrap Method 1 computation (cells 84-88) in try-except block - Guard Method 1 display/save cells with `if method1_ok:` check - Remove Method 2 dependencies on Method 1 variables: - Use `n_ifgs_method2 = len(ifgs_date)` instead of `len(ddiff_dist_ap1)` - Use `range(n_ifgs_method2)` instead of `displacement.index` iteration - Method 2 creates own `run_date_method2` and `method2_summary` - Appendix plots (cells 150-153) guarded with `if method1_ok:` Result: Run All continues to Method 2 even when Method 1 fails (e.g., insufficient GNSS stations). --- .../Transient_Requirement_Validation.ipynb | 474 ++---------------- 1 file changed, 31 insertions(+), 443 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 1f94a9b..7b4ed9e 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -1437,9 +1437,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "We first pair up all GNSS stations and compare the relative measurement from both GNSS and InSAR. " - ] + "source": "# Initialize Method 1 success flag\nmethod1_ok = False\n\ntry:\n # Empty dictionaries for GNSS and InSAR measurements, etc.\n insar_disp = {}\n gnss_disp = {}\n ddiff_dist = {}\n ddiff_disp = {}\n abs_ddiff_disp = {}\n\n # Define ellipsoid for distance calculation\n geod = pyproj.Geod(ellps=\"WGS84\")\n\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n displacement_i = displacement.loc[ifg_ndx]\n insar_disp_i = []\n gnss_disp_i = []\n ddiff_dist_i = []\n ddiff_disp_i = []\n\n # Loop through site pairs\n for sta1 in displacement_i.index:\n for sta2 in displacement_i.index:\n if sta2 == sta1:\n break\n\n # Compute InSAR and GNSS displacement residuals\n insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n - displacement_i.loc[sta2, 'insar_disp'])\n gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n - displacement_i.loc[sta2, 'gnss_disp'])\n\n # Compute double-difference residual\n ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n\n # Compute distance between sites\n _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n distance = distance / 1000 # convert unit from m to km\n\n # Record double difference\n ddiff_dist_i.append(distance)\n\n # Record all site-to-site values within an IFG\n insar_disp[ifg_ndx] = np.array(insar_disp_i)\n gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))" }, { "cell_type": "code", @@ -1506,55 +1504,21 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "\n", - "## 5.3 Validate Requirement Based on Binned Measurement Residuals" - ] + "source": "## 5.3 Validate Requirement Based on Binned Measurement Residuals" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Set requirement thresholds\n", - "transient_distance_rqmt = (0.1, 50) # distances for evaluation\n", - "transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", - "\n", - "n_bins = 10 # number of distance bins for analysis\n", - "threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", - "\n", - "# Loop through interferograms\n", - "method1_validation_figs = []\n", - "site_loc = sitedata['sites'][site]['calval_location']\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " # Start and end dates as strings\n", - " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", - " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", - "\n", - " # Validation figure and assessment\n", - " _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n", - " site_loc, start_date, end_date,\n", - " requirement=transient_threshold_rqmt,\n", - " distance_rqmt=transient_distance_rqmt,\n", - " n_bins=n_bins,\n", - " threshold=threshold,\n", - " sensor='NISAR',\n", - " validation_type=requirement.lower(),\n", - " validation_data='GNSS')\n", - " method1_validation_figs.append(validation_fig_method1)" - ] + "source": " # Set requirement thresholds\n transient_distance_rqmt = (0.1, 50) # distances for evaluation\n transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n\n n_bins = 10 # number of distance bins for analysis\n threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n\n # Loop through interferograms\n method1_validation_figs = []\n site_loc = sitedata['sites'][site]['calval_location']\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Start and end dates as strings\n start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n\n # Validation figure and assessment\n _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n site_loc, start_date, end_date,\n requirement=transient_threshold_rqmt,\n distance_rqmt=transient_distance_rqmt,\n n_bins=n_bins,\n threshold=threshold,\n sensor='NISAR',\n validation_type=requirement.lower(),\n validation_data='GNSS')\n method1_validation_figs.append(validation_fig_method1)\n \n method1_ok = True\n \nexcept Exception as e:\n print(f\"Method 1 failed: {e}\")\n print(\"Continuing to Method 2...\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Reformat double differences as list\n", - "ddiff_dist_ap1 = list(ddiff_dist.values())\n", - "abs_ddiff_disp_ap1 = list(abs_ddiff_disp.values())" - ] + "source": "if method1_ok:\n # Reformat double differences as list\n ddiff_dist_ap1 = list(ddiff_dist.values())\n abs_ddiff_disp_ap1 = list(abs_ddiff_disp.values())\nelse:\n print(\"Method 1 did not complete successfully. Skipping Method 1 data reformatting.\")" }, { "cell_type": "markdown", @@ -1571,11 +1535,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Define number of interferograms\n", - "n_ifgs = len(ddiff_dist_ap1)\n", - "print(f\"Analyzing {n_ifgs} interferograms\")" - ] + "source": "if method1_ok:\n # Define number of interferograms\n n_ifgs = len(ddiff_dist_ap1)\n print(f\"Analyzing {n_ifgs} interferograms\")\nelse:\n print(\"Method 1 failed. Skipping interferogram count.\")" }, { "cell_type": "markdown", @@ -1589,62 +1549,21 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Define bins over distance requirement\n", - "n_bins = 10\n", - "bins = np.linspace(0.1, 50.0, num=n_bins+1)" - ] + "source": "if method1_ok:\n # Define bins over distance requirement\n n_bins = 10\n bins = np.linspace(0.1, 50.0, num=n_bins+1)\nelse:\n print(\"Method 1 failed. Skipping bin definition.\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Pre-allocate array for number of points for each IFG and bins\n", - "n_all = np.empty([n_ifgs, n_bins+1], dtype=int)\n", - "\n", - "# Pre-allocate array for number of points that pass based on requirement\n", - "n_pass = np.empty([n_ifgs,n_bins+1], dtype=int)\n", - "\n", - "# Loop through interferograms\n", - "for i in range(n_ifgs):\n", - " # Determine bin indices\n", - " inds = np.digitize(ddiff_dist_ap1[i], bins)\n", - "\n", - " # Loop through bins\n", - " for j in range(1,n_bins+1):\n", - " # Evaluate requirement for the i-th IFG and j-th distance bin\n", - " rqmt = 3*(1+np.sqrt(ddiff_dist_ap1[i][inds==j]))\n", - "\n", - " # Relative measurement of i-th IFG and j-th distance bin\n", - " rem = abs_ddiff_disp_ap1[i][inds==j]\n", - " assert len(rqmt) == len(rem)\n", - " n_all[i,j-1] = len(rem)\n", - " n_pass[i,j-1] = np.count_nonzero(rem threshold" - ] + "source": "if method1_ok:\n # Ratio of double-difference residuals that pass requirement\n ratio = n_pass / n_all\n\n # Define threshold of data points in a bin that must pass\n threshold = 0.683\n\n # The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n success_or_fail = ratio > threshold\nelse:\n print(\"Method 1 failed. Skipping ratio calculation.\")" }, { "cell_type": "markdown", @@ -1666,29 +1585,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "def to_str(x:bool):\n", - " if x==True:\n", - " return 'true '\n", - " elif x==False:\n", - " return 'false '\n", - "\n", - "success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n", - "\n", - "columns = []\n", - "for i in range(n_bins):\n", - " columns.append(f'{bins[i]:.2f}-{bins[i+1]:.2f}')\n", - "columns.append('total')\n", - "\n", - "index = []\n", - "for i in range(len(ifgs_date_ap1)):\n", - " index.append(ifgs_date_ap1[i][0].strftime('%Y%m%d')+'-'+ifgs_date_ap1[i][1].strftime('%Y%m%d'))\n", - "\n", - "n_all_pd = pd.DataFrame(n_all,columns=columns,index=index)\n", - "n_pass_pd = pd.DataFrame(n_pass,columns=columns,index=index)\n", - "ratio_pd = pd.DataFrame(ratio,columns=columns,index=index)\n", - "success_or_fail_pd = pd.DataFrame(success_or_fail_str,columns=columns,index=index)" - ] + "source": "if method1_ok:\n def to_str(x:bool):\n if x==True:\n return 'true '\n elif x==False:\n return 'false '\n\n success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n\n columns = []\n for i in range(n_bins):\n columns.append(f'{bins[i]:.2f}-{bins[i+1]:.2f}')\n columns.append('total')\n\n index = []\n for i in range(len(ifgs_date_ap1)):\n index.append(ifgs_date_ap1[i][0].strftime('%Y%m%d')+'-'+ifgs_date_ap1[i][1].strftime('%Y%m%d'))\n\n n_all_pd = pd.DataFrame(n_all,columns=columns,index=index)\n n_pass_pd = pd.DataFrame(n_pass,columns=columns,index=index)\n ratio_pd = pd.DataFrame(ratio,columns=columns,index=index)\n success_or_fail_pd = pd.DataFrame(success_or_fail_str,columns=columns,index=index)\nelse:\n print(\"Method 1 failed. Skipping DataFrame creation.\")" }, { "cell_type": "markdown", @@ -1704,9 +1601,7 @@ "tags": [] }, "outputs": [], - "source": [ - "n_all_pd" - ] + "source": "if method1_ok:\n n_all_pd\nelse:\n print(\"Method 1 failed. No data to display.\")" }, { "cell_type": "markdown", @@ -1722,9 +1617,7 @@ "tags": [] }, "outputs": [], - "source": [ - "n_pass_pd" - ] + "source": "if method1_ok:\n n_pass_pd\nelse:\n print(\"Method 1 failed. No data to display.\")" }, { "cell_type": "markdown", @@ -1740,15 +1633,7 @@ "tags": [] }, "outputs": [], - "source": [ - "# Stylized pandas table\n", - "validation_table_method1 = ratio_pd.style\n", - "validation_table_method1.set_table_styles([ # create internal CSS classes\n", - " {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n", - " {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n", - "], overwrite=False)\n", - "validation_table_method1.set_td_classes(success_or_fail_pd)" - ] + "source": "if method1_ok:\n # Stylized pandas table\n validation_table_method1 = ratio_pd.style\n validation_table_method1.set_table_styles([ # create internal CSS classes\n {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n ], overwrite=False)\n validation_table_method1.set_td_classes(success_or_fail_pd)\nelse:\n print(\"Method 1 failed. No table to display.\")" }, { "cell_type": "markdown", @@ -1763,48 +1648,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "percentage = np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs\n", - "\n", - "method_summary = f\"Percentage of interferograms passes the requirement: {percentage}\"\n", - "\n", - "if percentage >= 0.70:\n", - " method_summary += \"\\nThe interferogram stack passes the requirement.\"\n", - "else:\n", - " method_summary += \"\\nThe interferogram stack fails the requirement.\"\n", - "\n", - "print(method_summary)" - ] + "source": "if method1_ok:\n percentage = np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs\n\n method1_summary = f\"Percentage of interferograms passes the requirement: {percentage}\"\n\n if percentage >= 0.70:\n method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n else:\n method1_summary += \"\\nThe interferogram stack fails the requirement.\"\n\n print(method1_summary)\nelse:\n method1_summary = \"Method 1 failed to complete.\"\n print(method1_summary)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Save Method 1 results to file\n", - "run_date = dt.now().strftime('%Y%m%dT%H%M%S')\n", - "save_fldr = f\"{run_date}-Transient-Method1\"\n", - "save_dir = os.path.join(mintpy_dir, save_fldr)\n", - "\n", - "save_params = {\n", - " 'save_dir': save_dir,\n", - " 'run_date': run_date,\n", - " 'requirement': requirement,\n", - " 'site': site,\n", - " 'method': '1',\n", - " 'sitedata': sitedata['sites'][site],\n", - " 'gnss_insar_figs': gnss_insar_figs,\n", - " 'validation_figs': method1_validation_figs,\n", - " 'validation_table': validation_table_method1,\n", - " 'summary': method_summary\n", - "}\n", - "save_results(**save_params)\n", - "\n", - "# Save the report in the home directory as well\n", - "save_params['save_dir'] = home_dir\n", - "save_results(**save_params)" - ] + "source": "if method1_ok:\n # Save Method 1 results to file\n run_date = dt.now().strftime('%Y%m%dT%H%M%S')\n save_fldr = f\"{run_date}-Transient-Method1\"\n save_dir = os.path.join(mintpy_dir, save_fldr)\n\n save_params = {\n 'save_dir': save_dir,\n 'run_date': run_date,\n 'requirement': requirement,\n 'site': site,\n 'method': '1',\n 'sitedata': sitedata['sites'][site],\n 'gnss_insar_figs': gnss_insar_figs,\n 'validation_figs': method1_validation_figs,\n 'validation_table': validation_table_method1,\n 'summary': method1_summary\n }\n save_results(**save_params)\n\n # Save the report in the home directory as well\n save_params['save_dir'] = home_dir\n save_results(**save_params)\nelse:\n print(\"Method 1 failed. Skipping save.\")" }, { "cell_type": "markdown", @@ -1856,46 +1707,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Construct dataset-layer names as lists\n", - "unwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", - " for date in ifgs_date]\n", - "coherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", - " for date in ifgs_date]\n", - "# Read unwrapped phase from selected interferograms\n", - "ifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n", - "# Coherence is coming from the original ifgramStack.h5\n", - "insar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n", - "\n", - "# Convert phase to displacement in m and switch convention to positive range decrease\n", - "insar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n", - "\n", - "# Convert displacement units from m to mm\n", - "insar_displacement = insar_displacement * 1000.\n", - "if singleStack:\n", - " insar_displacement = insar_displacement[np.newaxis, :, :]\n", - " insar_coherence = insar_coherence[np.newaxis, :, :]\n", - "\n", - "# Read 2D mask array\n", - "msk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n", - "\n", - "# Repeat mask array for each interferogram\n", - "msk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n", - " \n", - "# Set masked pixels to NaN\n", - "insar_displacement[msk == 0] = np.nan\n", - "insar_displacement[insar_displacement==0.0] = np.nan\n", - "\n", - "# Clean up phase-only IFGs to avoid future confusion\n", - "del ifgs_unw\n", - "\n", - "# Define number of interferograms\n", - "n_ifgs = len(ddiff_dist_ap1)\n", - "print(f\"Analyzing {n_ifgs} interferograms\")\n", - "\n", - "# Masking pixels with low coherence\n", - "insar_displacement[insar_coherence <0.3] = np.nan" - ] + "source": "# Construct dataset-layer names as lists\nunwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n for date in ifgs_date]\ncoherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n for date in ifgs_date]\n# Read unwrapped phase from selected interferograms\nifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n# Coherence is coming from the original ifgramStack.h5\ninsar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n\n# Convert phase to displacement in m and switch convention to positive range decrease\ninsar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n\n# Convert displacement units from m to mm\ninsar_displacement = insar_displacement * 1000.\nif singleStack:\n insar_displacement = insar_displacement[np.newaxis, :, :]\n insar_coherence = insar_coherence[np.newaxis, :, :]\n\n# Read 2D mask array\nmsk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n\n# Repeat mask array for each interferogram\nmsk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n \n# Set masked pixels to NaN\ninsar_displacement[msk == 0] = np.nan\ninsar_displacement[insar_displacement==0.0] = np.nan\n\n# Clean up phase-only IFGs to avoid future confusion\ndel ifgs_unw\n\n# Define number of interferograms for Method 2\nn_ifgs_method2 = len(ifgs_date)\nprint(f\"Analyzing {n_ifgs_method2} interferograms\")\n\n# Masking pixels with low coherence\ninsar_displacement[insar_coherence <0.3] = np.nan" }, { "cell_type": "markdown", @@ -1911,17 +1723,7 @@ "tags": [] }, "outputs": [], - "source": [ - "cmap_obj = copy.copy(plt.get_cmap('gray'))\n", - "\n", - "for ifg_ndx in range(n_ifgs):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", - " ax.set_title(f\"Coherence \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " cbar1 = fig.colorbar(img1, ax=ax)\n", - " cbar1.set_label('coherence')" - ] + "source": "cmap_obj = copy.copy(plt.get_cmap('gray'))\n\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n ax.set_title(f\"Coherence \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n cbar1 = fig.colorbar(img1, ax=ax)\n cbar1.set_label('coherence')" }, { "cell_type": "code", @@ -1930,17 +1732,7 @@ "tags": [] }, "outputs": [], - "source": [ - "cmap_obj = copy.copy(plt.get_cmap(cmap))\n", - "\n", - "for ifg_ndx in range(n_ifgs):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", - " ax.set_title(f\"Interferogram \"\n", - " f\"\\n Date range {ifgs_date[i][0].strftime('%Y%m%d')}-{ifgs_date[i][1].strftime('%Y%m%d')}\")\n", - " cbar1 = fig.colorbar(img1, ax=ax)\n", - " cbar1.set_label('LOS displacement [mm]')" - ] + "source": "cmap_obj = copy.copy(plt.get_cmap(cmap))\n\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n ax.set_title(f\"Interferogram \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n cbar1 = fig.colorbar(img1, ax=ax)\n cbar1.set_label('LOS displacement [mm]')" }, { "cell_type": "markdown", @@ -1984,14 +1776,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Determine the distance and phase difference between site pairs\n", - "dist = []; rel_measure = []\n", - "for ifg_ndx in range(n_ifgs):\n", - " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=1000000)\n", - " dist.append(dist_i/1000)\n", - " rel_measure.append(rel_measure_i)" - ] + "source": "# Determine the distance and phase difference between site pairs\ndist = []; rel_measure = []\nfor ifg_ndx in range(n_ifgs_method2):\n dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=1000000)\n dist.append(dist_i/1000)\n rel_measure.append(rel_measure_i)" }, { "cell_type": "markdown", @@ -2007,17 +1792,7 @@ "tags": [] }, "outputs": [], - "source": [ - "# Plot histogram of distances between pixel pairs\n", - "for ifg_ndx in range(n_ifgs):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.hist(dist[ifg_ndx], bins=100)\n", - " ax.set_title(f\"Histogram of distance \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " ax.set_xlabel(r'Distance ($km$)')\n", - " ax.set_ylabel('Frequency')\n", - " ax.set_xlim(0, 50)" - ] + "source": "# Plot histogram of distances between pixel pairs\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.hist(dist[ifg_ndx], bins=100)\n ax.set_title(f\"Histogram of distance \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n ax.set_xlabel(r'Distance ($km$)')\n ax.set_ylabel('Frequency')\n ax.set_xlim(0, 50)" }, { "cell_type": "code", @@ -2026,16 +1801,7 @@ "tags": [] }, "outputs": [], - "source": [ - "# Plot histogram of relative measurements\n", - "for ifg_ndx in range(n_ifgs):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n", - " ax.set_title(f\"Histogram of Relative Measurement \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " ax.set_xlabel(r'Relative Measurement ($mm$)')\n", - " ax.set_ylabel('Frequency')" - ] + "source": "# Plot histogram of relative measurements\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n ax.set_title(f\"Histogram of Relative Measurement \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n ax.set_xlabel(r'Relative Measurement ($mm$)')\n ax.set_ylabel('Frequency')" }, { "cell_type": "markdown", @@ -2060,11 +1826,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Define number of interferograms\n", - "n_ifgs = len(ddiff_dist_ap1)\n", - "print(f\"Analyzing {n_ifgs} interferograms\")" - ] + "source": "# Define number of interferograms for Method 2\nn_ifgs_method2 = len(ifgs_date)\nprint(f\"Analyzing {n_ifgs_method2} interferograms\")" }, { "cell_type": "markdown", @@ -2089,35 +1851,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Number of points for each ifgs and bins\n", - "n_all = np.empty([n_ifgs, n_bins+1], dtype=int)\n", - "\n", - "# Number of points pass\n", - "n_pass = np.empty([n_ifgs,n_bins+1], dtype=int)\n", - "\n", - "# Loop through interferograms\n", - "for i in range(n_ifgs):\n", - " # Determine bin indices\n", - " inds = np.digitize(dist[i], bins)\n", - "\n", - " # Loop through bins\n", - " for j in range(1, n_bins+1):\n", - " # Evaluate requirement for the i-th IFG and j-th distance bin\n", - " rqmt = 3*(1+np.sqrt(dist[i][inds==j])) # mission requirement for i-th ifgs and j-th bins\n", - "\n", - " # Relative measurement of i-th IFG and j-th distance bin\n", - " rem = rel_measure[i][inds==j] # relative measurement\n", - " assert len(rqmt) == len(rem)\n", - " n_all[i,j-1] = len(rem)\n", - " n_pass[i,j-1] = np.count_nonzero(rem thresthod) / n_ifgs" - ] + "source": "percentage = np.count_nonzero(ratio_pd['mean'] > thresthod) / n_ifgs_method2" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "print(f\"Percentage of interferograms passes the requirement (70%): {percentage}.\")\n", - "if percentage >= 0.70:\n", - " print('The interferogram stack passes the requirement.')\n", - "else:\n", - " print('The interferogram stack fails the requirement.')" - ] + "source": "method2_summary = f\"Percentage of interferograms passes the requirement (70%): {percentage}.\"\nif percentage >= 0.70:\n method2_summary += \"\\nThe interferogram stack passes the requirement.\"\nelse:\n method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n\nprint(method2_summary)" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Save Method 2 results to file\n", - "save_fldr = f\"{dt.now().strftime('%Y%m%dT%H%M%S')}-Transient-Method2\"\n", - "save_dir = os.path.join(mintpy_dir, save_fldr)\n", - "\n", - "save_params = {\n", - " 'save_dir': save_dir,\n", - " 'run_date': run_date,\n", - " 'requirement': requirement,\n", - " 'site': site,\n", - " 'method': '2',\n", - " 'sitedata': sitedata['sites'][site],\n", - " 'gnss_insar_figs': gnss_insar_figs,\n", - " 'validation_figs': method2_validation_figs,\n", - " 'validation_table': validation_table_method2,\n", - " 'summary': method_summary\n", - "}\n", - "save_results(**save_params)\n", - "\n", - "# Save the report in the home directory as well\n", - "save_params['save_dir'] = home_dir\n", - "save_results(**save_params)" - ] + "source": "# Save Method 2 results to file\nrun_date_method2 = dt.now().strftime('%Y%m%dT%H%M%S')\nsave_fldr = f\"{run_date_method2}-Transient-Method2\"\nsave_dir = os.path.join(mintpy_dir, save_fldr)\n\nsave_params = {\n 'save_dir': save_dir,\n 'run_date': run_date_method2,\n 'requirement': requirement,\n 'site': site,\n 'method': '2',\n 'sitedata': sitedata['sites'][site],\n 'gnss_insar_figs': gnss_insar_figs if method1_ok else [],\n 'validation_figs': method2_validation_figs,\n 'validation_table': validation_table_method2,\n 'summary': method2_summary\n}\nsave_results(**save_params)\n\n# Save the report in the home directory as well\nsave_params['save_dir'] = home_dir\nsave_results(**save_params)" }, { "cell_type": "markdown", @@ -2372,81 +2048,21 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Loop through interferograms\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " # Initialize figure\n", - " plt.figure(figsize=(11,7))\n", - "\n", - " # Define range of displacement values\n", - " disp_range = (min([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]), max([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]))\n", - "\n", - " # Plot histograms of InSAR and GNSS displacement values\n", - " plt.hist(insar_disp[ifg_ndx], bins=100, range=disp_range, color = \"green\", label='D_InSAR')\n", - " plt.hist(gnss_disp[ifg_ndx], bins=100, range=disp_range, color=\"orange\", label='D_GNSS', alpha=0.5)\n", - "\n", - " # Format figure\n", - " plt.legend(loc='upper right')\n", - " plt.title(f\"Displacements \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n", - " f\"\\n Number of station pairs used: {len(insar_disp[ifg_ndx])}\")\n", - " plt.xlabel('LOS Displacement (mm)')\n", - " plt.ylabel('Number of Station Pairs')\n", - " plt.show()" - ] + "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Define range of displacement values\n disp_range = (min([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]), max([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]))\n\n # Plot histograms of InSAR and GNSS displacement values\n plt.hist(insar_disp[ifg_ndx], bins=100, range=disp_range, color = \"green\", label='D_InSAR')\n plt.hist(gnss_disp[ifg_ndx], bins=100, range=disp_range, color=\"orange\", label='D_GNSS', alpha=0.5)\n\n # Format figure\n plt.legend(loc='upper right')\n plt.title(f\"Displacements \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n f\"\\n Number of station pairs used: {len(insar_disp[ifg_ndx])}\")\n plt.xlabel('LOS Displacement (mm)')\n plt.ylabel('Number of Station Pairs')\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping displacement histogram plots.\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Loop through interferograms\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " # Initialize figure\n", - " plt.figure(figsize=(11,7))\n", - "\n", - " # Plot histogram\n", - " plt.hist(ddiff_disp[ifg_ndx], bins=100, color='darkblue', linewidth=1, label='D_gnss - D_InSAR')\n", - "\n", - " # Format figure\n", - " plt.legend(loc='upper right')\n", - " plt.title(f\"Residuals\"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\"\n", - " f\"\\n Number of stations pairs used: {len(ddiff_disp[i])}\")\n", - " plt.xlabel('Displacement Residual (mm)')\n", - " plt.ylabel('N Stations')\n", - " plt.show()" - ] + "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Plot histogram\n plt.hist(ddiff_disp[ifg_ndx], bins=100, color='darkblue', linewidth=1, label='D_gnss - D_InSAR')\n\n # Format figure\n plt.legend(loc='upper right')\n plt.title(f\"Residuals\"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\"\n f\"\\n Number of stations pairs used: {len(ddiff_disp[ifg_ndx])}\")\n plt.xlabel('Displacement Residual (mm)')\n plt.ylabel('N Stations')\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping residual histogram plots.\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Loop through interferograms\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " # Initialize figure\n", - " plt.figure(figsize=(11,7))\n", - "\n", - " # Draw distance threshold\n", - " dist_th = np.linspace(min(ddiff_dist[ifg_ndx]), max(ddiff_dist[ifg_ndx]),100)\n", - " acpt_error = 3*(1+np.sqrt(dist_th))\n", - "\n", - " # Plot residuals\n", - " plt.scatter(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx], s=1)\n", - " plt.plot(dist_th, acpt_error, 'r')\n", - "\n", - " # Format plot\n", - " plt.xlabel(\"Distance (km)\")\n", - " plt.ylabel(\"Amplitude of Displacement Residuals (mm)\")\n", - " plt.title(f\"Residuals \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n", - " f\"\\n Number of stations pairs used: {len(ddiff_dist[ifg_ndx])}\")\n", - " plt.legend([\"Measurement\", \"Mission Reqiurement\"])\n", - " plt.show()" - ] + "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Draw distance threshold\n dist_th = np.linspace(min(ddiff_dist[ifg_ndx]), max(ddiff_dist[ifg_ndx]),100)\n acpt_error = 3*(1+np.sqrt(dist_th))\n\n # Plot residuals\n plt.scatter(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx], s=1)\n plt.plot(dist_th, acpt_error, 'r')\n\n # Format plot\n plt.xlabel(\"Distance (km)\")\n plt.ylabel(\"Amplitude of Displacement Residuals (mm)\")\n plt.title(f\"Residuals \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n f\"\\n Number of stations pairs used: {len(ddiff_dist[ifg_ndx])}\")\n plt.legend([\"Measurement\", \"Mission Reqiurement\"])\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping residual scatter plots.\")" }, { "cell_type": "code", @@ -2455,35 +2071,7 @@ "tags": [] }, "outputs": [], - "source": [ - "# Loop through interferograms\n", - "for ifg_ndx in range(insar_displacement.shape[0]):\n", - " # Define start and end dates\n", - " start_time_str = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", - " end_time_str = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", - "\n", - " # Loop through stations in IFG\n", - " for site_name in gnss_time_series[ifg_ndx].columns:\n", - " print(f\"Plotting GPS postion from {start_time_str} to {end_time_str} at station: {site_name}\")\n", - "\n", - " # Retrieve GNSS time-series\n", - " series = gnss_time_series[ifg_ndx, site_name]\n", - "\n", - " # Initialize figure\n", - " plt.figure(figsize=(15,5))\n", - "\n", - " # Plot time-series data\n", - " plt.scatter(pd.date_range(start=ifgs_date[ifg_ndx][0], end=ifgs_date[ifg_ndx][1]),series)\n", - "\n", - " # Foramt figure\n", - " plt.title(f\"station name: {site_name}\")\n", - " plt.xlabel('Time')\n", - " plt.ylabel('Relative position in LOS direction (mm)')\n", - "\n", - " # Save figure\n", - " plt.savefig(os.path.join(work_dir, f\"{start_time_str}_{end_time_str}_{site_name}.jpg\"))\n", - " plt.close()" - ] + "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in range(insar_displacement.shape[0]):\n # Define start and end dates\n start_time_str = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n end_time_str = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n\n # Loop through stations in IFG\n for site_name in gnss_time_series[ifg_ndx].columns:\n print(f\"Plotting GPS postion from {start_time_str} to {end_time_str} at station: {site_name}\")\n\n # Retrieve GNSS time-series\n series = gnss_time_series[ifg_ndx, site_name]\n\n # Initialize figure\n plt.figure(figsize=(15,5))\n\n # Plot time-series data\n plt.scatter(pd.date_range(start=ifgs_date[ifg_ndx][0], end=ifgs_date[ifg_ndx][1]),series)\n\n # Foramt figure\n plt.title(f\"station name: {site_name}\")\n plt.xlabel('Time')\n plt.ylabel('Relative position in LOS direction (mm)')\n\n # Save figure\n plt.savefig(os.path.join(work_dir, f\"{start_time_str}_{end_time_str}_{site_name}.jpg\"))\n plt.close()\nelse:\n print(\"Method 1 failed. Skipping GNSS time series plots.\")" }, { "cell_type": "code", @@ -2517,4 +2105,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From 1a216394a65ed0242035c1efc1f1307e882856f3 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Mon, 8 Jun 2026 15:38:34 -0700 Subject: [PATCH 02/13] Fix transient notebook method fallback --- .../Transient_Requirement_Validation.ipynb | 137 ++++++++++++------ 1 file changed, 89 insertions(+), 48 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 7b4ed9e..75a3c58 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -1437,7 +1437,9 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Initialize Method 1 success flag\nmethod1_ok = False\n\ntry:\n # Empty dictionaries for GNSS and InSAR measurements, etc.\n insar_disp = {}\n gnss_disp = {}\n ddiff_dist = {}\n ddiff_disp = {}\n abs_ddiff_disp = {}\n\n # Define ellipsoid for distance calculation\n geod = pyproj.Geod(ellps=\"WGS84\")\n\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n displacement_i = displacement.loc[ifg_ndx]\n insar_disp_i = []\n gnss_disp_i = []\n ddiff_dist_i = []\n ddiff_disp_i = []\n\n # Loop through site pairs\n for sta1 in displacement_i.index:\n for sta2 in displacement_i.index:\n if sta2 == sta1:\n break\n\n # Compute InSAR and GNSS displacement residuals\n insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n - displacement_i.loc[sta2, 'insar_disp'])\n gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n - displacement_i.loc[sta2, 'gnss_disp'])\n\n # Compute double-difference residual\n ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n\n # Compute distance between sites\n _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n distance = distance / 1000 # convert unit from m to km\n\n # Record double difference\n ddiff_dist_i.append(distance)\n\n # Record all site-to-site values within an IFG\n insar_disp[ifg_ndx] = np.array(insar_disp_i)\n gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))" + "source": [ + "Method 1 is wrapped in a guarded code cell so notebook execution can continue to Method 2 if it fails." + ] }, { "cell_type": "code", @@ -1445,53 +1447,92 @@ "metadata": {}, "outputs": [], "source": [ - "# Empty dictionaries for GNSS and InSAR measurements, etc.\n", - "insar_disp = {}\n", - "gnss_disp = {}\n", - "ddiff_dist = {}\n", - "ddiff_disp = {}\n", - "abs_ddiff_disp = {}\n", - "\n", - "# Define ellipsoid for distance calculation\n", - "geod = pyproj.Geod(ellps=\"WGS84\")\n", - "\n", - "# Loop through interferograms\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " displacement_i = displacement.loc[ifg_ndx]\n", - " insar_disp_i = []\n", - " gnss_disp_i = []\n", - " ddiff_dist_i = []\n", - " ddiff_disp_i = []\n", + "# Shared label for Methods 1 and 2\n", + "site_loc = sitedata['sites'][site]['calval_location']\n", "\n", - " # Loop through site pairs\n", - " for sta1 in displacement_i.index:\n", - " for sta2 in displacement_i.index:\n", - " if sta2 == sta1:\n", - " break\n", + "# Initialize Method 1 success flag\n", + "method1_ok = False\n", "\n", - " # Compute InSAR and GNSS displacement residuals\n", - " insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n", - " - displacement_i.loc[sta2, 'insar_disp'])\n", - " gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n", - " - displacement_i.loc[sta2, 'gnss_disp'])\n", - "\n", - " # Compute double-difference residual\n", - " ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n", - "\n", - " # Compute distance between sites\n", - " _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n", - " displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n", - " distance = distance / 1000 # convert unit from m to km\n", - "\n", - " # Record double difference\n", - " ddiff_dist_i.append(distance)\n", - "\n", - " # Record all site-to-site values within an IFG\n", - " insar_disp[ifg_ndx] = np.array(insar_disp_i)\n", - " gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n", - " ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n", - " ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n", - " abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))" + "try:\n", + " # Empty dictionaries for GNSS and InSAR measurements, etc.\n", + " insar_disp = {}\n", + " gnss_disp = {}\n", + " ddiff_dist = {}\n", + " ddiff_disp = {}\n", + " abs_ddiff_disp = {}\n", + "\n", + " # Define ellipsoid for distance calculation\n", + " geod = pyproj.Geod(ellps=\"WGS84\")\n", + "\n", + " # Loop through interferograms\n", + " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + " displacement_i = displacement.loc[ifg_ndx]\n", + " insar_disp_i = []\n", + " gnss_disp_i = []\n", + " ddiff_dist_i = []\n", + " ddiff_disp_i = []\n", + "\n", + " # Loop through site pairs\n", + " for sta1 in displacement_i.index:\n", + " for sta2 in displacement_i.index:\n", + " if sta2 == sta1:\n", + " break\n", + "\n", + " # Compute InSAR and GNSS displacement residuals\n", + " insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n", + " - displacement_i.loc[sta2, 'insar_disp'])\n", + " gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n", + " - displacement_i.loc[sta2, 'gnss_disp'])\n", + "\n", + " # Compute double-difference residual\n", + " ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n", + "\n", + " # Compute distance between sites\n", + " _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n", + " displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n", + " distance = distance / 1000 # convert unit from m to km\n", + "\n", + " # Record double difference\n", + " ddiff_dist_i.append(distance)\n", + "\n", + " # Record all site-to-site values within an IFG\n", + " insar_disp[ifg_ndx] = np.array(insar_disp_i)\n", + " gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n", + " ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n", + " ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n", + " abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))\n", + "\n", + " # Set requirement thresholds\n", + " transient_distance_rqmt = (0.1, 50) # distances for evaluation\n", + " transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", + "\n", + " n_bins = 10 # number of distance bins for analysis\n", + " threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", + "\n", + " # Loop through interferograms\n", + " method1_validation_figs = []\n", + " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + " # Start and end dates as strings\n", + " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", + " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", + "\n", + " # Validation figure and assessment\n", + " _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n", + " site_loc, start_date, end_date,\n", + " requirement=transient_threshold_rqmt,\n", + " distance_rqmt=transient_distance_rqmt,\n", + " n_bins=n_bins,\n", + " threshold=threshold,\n", + " sensor='NISAR',\n", + " validation_type=requirement.lower(),\n", + " validation_data='GNSS')\n", + " method1_validation_figs.append(validation_fig_method1)\n", + "\n", + " method1_ok = True\n", + "\n", + "except Exception as e:\n", + " print(f\"Method 1 failed: {e}\")\n", + " print(\"Continuing to Method 2...\")\n" ] }, { @@ -1511,7 +1552,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": " # Set requirement thresholds\n transient_distance_rqmt = (0.1, 50) # distances for evaluation\n transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n\n n_bins = 10 # number of distance bins for analysis\n threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n\n # Loop through interferograms\n method1_validation_figs = []\n site_loc = sitedata['sites'][site]['calval_location']\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Start and end dates as strings\n start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n\n # Validation figure and assessment\n _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n site_loc, start_date, end_date,\n requirement=transient_threshold_rqmt,\n distance_rqmt=transient_distance_rqmt,\n n_bins=n_bins,\n threshold=threshold,\n sensor='NISAR',\n validation_type=requirement.lower(),\n validation_data='GNSS')\n method1_validation_figs.append(validation_fig_method1)\n \n method1_ok = True\n \nexcept Exception as e:\n print(f\"Method 1 failed: {e}\")\n print(\"Continuing to Method 2...\")" + "source": [] }, { "cell_type": "code", @@ -2105,4 +2146,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From c1711b6e244a3aeb566cf9e8f25f8452d11dd93b Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Mon, 8 Jun 2026 15:59:46 -0700 Subject: [PATCH 03/13] Harden transient validation notebook logic --- .../Transient_Requirement_Validation.ipynb | 181 +++++++++--------- 1 file changed, 94 insertions(+), 87 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 75a3c58..c40cc84 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -420,6 +420,7 @@ " time_interval = (ifgs_date[i][1]-ifgs_date[i][0]).days\n", " if time_interval != 12:\n", " del_row_index.append(i)\n", + "i = 0\n", "while i 0\n", + "if not method1_prereq_ok:\n", + " print(\"Method 1 cannot run: no IFGs remain with at least 3 GNSS stations.\")\n" ] }, { @@ -1453,86 +1457,89 @@ "# Initialize Method 1 success flag\n", "method1_ok = False\n", "\n", - "try:\n", - " # Empty dictionaries for GNSS and InSAR measurements, etc.\n", - " insar_disp = {}\n", - " gnss_disp = {}\n", - " ddiff_dist = {}\n", - " ddiff_disp = {}\n", - " abs_ddiff_disp = {}\n", - "\n", - " # Define ellipsoid for distance calculation\n", - " geod = pyproj.Geod(ellps=\"WGS84\")\n", - "\n", - " # Loop through interferograms\n", - " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " displacement_i = displacement.loc[ifg_ndx]\n", - " insar_disp_i = []\n", - " gnss_disp_i = []\n", - " ddiff_dist_i = []\n", - " ddiff_disp_i = []\n", - "\n", - " # Loop through site pairs\n", - " for sta1 in displacement_i.index:\n", - " for sta2 in displacement_i.index:\n", - " if sta2 == sta1:\n", - " break\n", - "\n", - " # Compute InSAR and GNSS displacement residuals\n", - " insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n", - " - displacement_i.loc[sta2, 'insar_disp'])\n", - " gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n", - " - displacement_i.loc[sta2, 'gnss_disp'])\n", - "\n", - " # Compute double-difference residual\n", - " ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n", - "\n", - " # Compute distance between sites\n", - " _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n", - " displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n", - " distance = distance / 1000 # convert unit from m to km\n", - "\n", - " # Record double difference\n", - " ddiff_dist_i.append(distance)\n", - "\n", - " # Record all site-to-site values within an IFG\n", - " insar_disp[ifg_ndx] = np.array(insar_disp_i)\n", - " gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n", - " ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n", - " ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n", - " abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))\n", - "\n", - " # Set requirement thresholds\n", - " transient_distance_rqmt = (0.1, 50) # distances for evaluation\n", - " transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", - "\n", - " n_bins = 10 # number of distance bins for analysis\n", - " threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", - "\n", - " # Loop through interferograms\n", - " method1_validation_figs = []\n", - " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " # Start and end dates as strings\n", - " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", - " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", - "\n", - " # Validation figure and assessment\n", - " _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n", - " site_loc, start_date, end_date,\n", - " requirement=transient_threshold_rqmt,\n", - " distance_rqmt=transient_distance_rqmt,\n", - " n_bins=n_bins,\n", - " threshold=threshold,\n", - " sensor='NISAR',\n", - " validation_type=requirement.lower(),\n", - " validation_data='GNSS')\n", - " method1_validation_figs.append(validation_fig_method1)\n", - "\n", - " method1_ok = True\n", - "\n", - "except Exception as e:\n", - " print(f\"Method 1 failed: {e}\")\n", - " print(\"Continuing to Method 2...\")\n" + "if not method1_prereq_ok:\n", + " print(\"Method 1 failed at earliest prerequisite. Continuing to Method 2...\")\n", + "else:\n", + " try:\n", + " # Empty dictionaries for GNSS and InSAR measurements, etc.\n", + " insar_disp = {}\n", + " gnss_disp = {}\n", + " ddiff_dist = {}\n", + " ddiff_disp = {}\n", + " abs_ddiff_disp = {}\n", + "\n", + " # Define ellipsoid for distance calculation\n", + " geod = pyproj.Geod(ellps=\"WGS84\")\n", + "\n", + " # Loop through interferograms\n", + " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + " displacement_i = displacement.loc[ifg_ndx]\n", + " insar_disp_i = []\n", + " gnss_disp_i = []\n", + " ddiff_dist_i = []\n", + " ddiff_disp_i = []\n", + "\n", + " # Loop through site pairs\n", + " for sta1 in displacement_i.index:\n", + " for sta2 in displacement_i.index:\n", + " if sta2 == sta1:\n", + " break\n", + "\n", + " # Compute InSAR and GNSS displacement residuals\n", + " insar_disp_i.append(displacement_i.loc[sta1, 'insar_disp'] \\\n", + " - displacement_i.loc[sta2, 'insar_disp'])\n", + " gnss_disp_i.append(displacement_i.loc[sta1, 'gnss_disp'] \\\n", + " - displacement_i.loc[sta2, 'gnss_disp'])\n", + "\n", + " # Compute double-difference residual\n", + " ddiff_disp_i.append(gnss_disp_i[-1] - insar_disp_i[-1])\n", + "\n", + " # Compute distance between sites\n", + " _, _, distance = geod.inv(displacement_i.loc[sta1,'lon'], displacement_i.loc[sta1,'lat'],\n", + " displacement_i.loc[sta2,'lon'], displacement_i.loc[sta2,'lat'])\n", + " distance = distance / 1000 # convert unit from m to km\n", + "\n", + " # Record double difference\n", + " ddiff_dist_i.append(distance)\n", + "\n", + " # Record all site-to-site values within an IFG\n", + " insar_disp[ifg_ndx] = np.array(insar_disp_i)\n", + " gnss_disp[ifg_ndx] = np.array(gnss_disp_i)\n", + " ddiff_dist[ifg_ndx] = np.array(ddiff_dist_i)\n", + " ddiff_disp[ifg_ndx] = np.array(ddiff_disp_i)\n", + " abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))\n", + "\n", + " # Set requirement thresholds\n", + " transient_distance_rqmt = (0.1, 50) # distances for evaluation\n", + " transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", + "\n", + " n_bins = 10 # number of distance bins for analysis\n", + " threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", + "\n", + " # Loop through interferograms\n", + " method1_validation_figs = []\n", + " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + " # Start and end dates as strings\n", + " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", + " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", + "\n", + " # Validation figure and assessment\n", + " _, validation_fig_method1 = display_transient_validation(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx],\n", + " site_loc, start_date, end_date,\n", + " requirement=transient_threshold_rqmt,\n", + " distance_rqmt=transient_distance_rqmt,\n", + " n_bins=n_bins,\n", + " threshold=threshold,\n", + " sensor='NISAR',\n", + " validation_type=requirement.lower(),\n", + " validation_data='GNSS')\n", + " method1_validation_figs.append(validation_fig_method1)\n", + "\n", + " method1_ok = True\n", + "\n", + " except Exception as e:\n", + " print(f\"Method 1 failed: {e}\")\n", + " print(\"Continuing to Method 2...\")\n" ] }, { @@ -1559,7 +1566,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Reformat double differences as list\n ddiff_dist_ap1 = list(ddiff_dist.values())\n abs_ddiff_disp_ap1 = list(abs_ddiff_disp.values())\nelse:\n print(\"Method 1 did not complete successfully. Skipping Method 1 data reformatting.\")" + "source": "if method1_ok:\n # Reformat Method 1 outputs using the same IFG keys for dates and residuals\n ifg_indices_ap1 = list(ddiff_dist.keys())\n ddiff_dist_ap1 = [ddiff_dist[i] for i in ifg_indices_ap1]\n abs_ddiff_disp_ap1 = [abs_ddiff_disp[i] for i in ifg_indices_ap1]\n ifgs_date_ap1 = [ifgs_date[i] for i in ifg_indices_ap1]\nelse:\n print(\"Method 1 did not complete successfully. Skipping Method 1 data reformatting.\")" }, { "cell_type": "markdown", @@ -1597,14 +1604,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Pre-allocate array for number of points for each IFG and bins\n n_all = np.empty([n_ifgs, n_bins+1], dtype=int)\n\n # Pre-allocate array for number of points that pass based on requirement\n n_pass = np.empty([n_ifgs,n_bins+1], dtype=int)\n\n # Loop through interferograms\n for i in range(n_ifgs):\n # Determine bin indices\n inds = np.digitize(ddiff_dist_ap1[i], bins)\n\n # Loop through bins\n for j in range(1,n_bins+1):\n # Evaluate requirement for the i-th IFG and j-th distance bin\n rqmt = 3*(1+np.sqrt(ddiff_dist_ap1[i][inds==j]))\n\n # Relative measurement of i-th IFG and j-th distance bin\n rem = abs_ddiff_disp_ap1[i][inds==j]\n assert len(rqmt) == len(rem)\n n_all[i,j-1] = len(rem)\n n_pass[i,j-1] = np.count_nonzero(rem threshold\nelse:\n print(\"Method 1 failed. Skipping ratio calculation.\")" + "source": "if method1_ok:\n # Ratio of double-difference residuals that pass requirement\n ratio = n_pass / np.where(n_all > 0, n_all, 1)\n\n # Define threshold of data points in a bin that must pass\n threshold = 0.683\n\n # The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n success_or_fail = ratio > threshold\nelse:\n print(\"Method 1 failed. Skipping ratio calculation.\")" }, { "cell_type": "markdown", @@ -1892,7 +1899,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Number of points for each ifgs and bins\nn_all = np.empty([n_ifgs_method2, n_bins+1], dtype=int)\n\n# Number of points pass\nn_pass = np.empty([n_ifgs_method2,n_bins+1], dtype=int)\n\n# Loop through interferograms\nfor i in range(n_ifgs_method2):\n # Determine bin indices\n inds = np.digitize(dist[i], bins)\n\n # Loop through bins\n for j in range(1, n_bins+1):\n # Evaluate requirement for the i-th IFG and j-th distance bin\n rqmt = 3*(1+np.sqrt(dist[i][inds==j])) # mission requirement for i-th ifgs and j-th bins\n\n # Relative measurement of i-th IFG and j-th distance bin\n rem = rel_measure[i][inds==j] # relative measurement\n assert len(rqmt) == len(rem)\n n_all[i,j-1] = len(rem)\n n_pass[i,j-1] = np.count_nonzero(rem 0, n_all, 1)\n", "mean_ratio = np.array([np.mean(ratio[:,:-1],axis=1)])\n", "ratio = np.hstack((ratio,mean_ratio.T))\n", "\n", From d3bf29f0f92da6fd8f2df3aae74abde11a974651 Mon Sep 17 00:00:00 2001 From: ehavazli Date: Mon, 8 Jun 2026 23:57:55 +0000 Subject: [PATCH 04/13] add checks for validation method 1 so failure doesn't break --- .../Transient_Requirement_Validation.ipynb | 572 ++++++++++++++++-- 1 file changed, 512 insertions(+), 60 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index c40cc84..ad200a6 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -139,12 +139,12 @@ "outputs": [], "source": [ "# === Basic Configuration ===\n", - "site = \"CalVal_NISAR_NewZealandD15\" # Cal/Val location ID\n", + "site = \"CalVal_NISAR_LosAngelesA34\" # Cal/Val location ID\n", "requirement = \"Transient\" # Options: 'Secular', 'Coseismic', 'Transient'\n", "dataset = \"NISAR\" # Dataset type: 'NISAR', 'ARIA_S1', 'ARIA_S1_new'\n", "\n", "\n", - "rundate = \"20260602\" # Date of this Cal/Val run\n", + "rundate = \"20260608\" # Date of this Cal/Val run\n", "version = \"1\" # Version of this Cal/Val run\n", "custom_sites = \"/home/jovyan/my_nisar_sites.txt\" # Path to custom site metadata\n", "\n", @@ -1244,7 +1244,13 @@ "gnss_time_series = dict(sorted(gnss_time_series.items()))\n", "gnss_time_series_std = dict(sorted(gnss_time_series_std.items()))\n", "displacement = dict(sorted(displacement.items()))\n", - "bad_stn = dict(sorted(bad_stn.items()))" + "bad_stn = dict(sorted(bad_stn.items()))\n", + "\n", + "# Earliest Method 1 prerequisite based on collected GNSS/InSAR displacement data\n", + "method1_prereq_ok = len(displacement) > 0\n", + "if not method1_prereq_ok:\n", + " message = \"Method 1 cannot run: no valid GNSS/InSAR displacements were collected.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" ] }, { @@ -1267,11 +1273,11 @@ "outputs": [], "source": [ "# Convert displacement dictionaries to pandas dataframes\n", - "displacement = pd.DataFrame.from_dict(displacement, orient='index',\n", + "displacement_df = pd.DataFrame.from_dict(displacement, orient='index',\n", " columns=['lat','lon','gnss_disp','insar_disp'])\n", "\n", "# Organize by IFG index and site name\n", - "displacement.index = pd.MultiIndex.from_tuples(displacement.index, names=['ifg index','station'])" + "displacement_df.index = pd.MultiIndex.from_tuples(displacement_df.index, names=['ifg index','station'])" ] }, { @@ -1291,16 +1297,20 @@ "source": [ "# Drop IFGs with fewer than three stations\n", "drop_index = []\n", - "for i in displacement.index.get_level_values(0).unique():\n", - " if len(displacement.loc[i]) < 3:\n", + "for i in displacement_df.index.get_level_values(0).unique():\n", + " if len(displacement_df.loc[i]) < 3:\n", " drop_index.append(i)\n", - "displacement = displacement.drop(drop_index)\n", + "displacement_df = displacement_df.drop(drop_index)\n", "\n", "# ifgs_date after drop for approach 1\n", "ifgs_date_ap1 = np.delete(ifgs_date, drop_index, axis=0)\n", - "method1_prereq_ok = len(ifgs_date_ap1) > 0\n", - "if not method1_prereq_ok:\n", - " print(\"Method 1 cannot run: no IFGs remain with at least 3 GNSS stations.\")\n" + "if method1_prereq_ok == 1:\n", + " method1_prereq_ok = len(ifgs_date_ap1) > 0\n", + " if not method1_prereq_ok:\n", + " message = \"Method 1 cannot run: no IFGs remain with at least 3 GNSS stations.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{message}\\033[0m\") " ] }, { @@ -1344,19 +1354,19 @@ "print(f\"Using reference site: {gnss_ref_site_name:s}\")\n", "\n", "# Loop through interferograms to re-reference\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + "for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", " # Determine reference site\n", " if gnss_ref_site_name in ['auto', 'random']:\n", " # Choose random GNSS site\n", - " gnss_ref_site_name = random.choice(displacement.loc[ifg_ndx].index.unique()) \n", + " gnss_ref_site_name = random.choice(displacement_df.loc[ifg_ndx].index.unique()) \n", " print(f\"Ifg {ifg_ndx} reference site: {gnss_ref_site_name}\")\n", "\n", " \n", " # Remove reference site values from GNSS and InSAR displacements\n", - " displacement.loc[ifg_ndx, 'gnss_disp'] = displacement.loc[ifg_ndx, 'gnss_disp'].values \\\n", - " - displacement.loc[(ifg_ndx, gnss_ref_site_name), 'gnss_disp']\n", - " displacement.loc[ifg_ndx, 'insar_disp'] = displacement.loc[ifg_ndx, 'insar_disp'].values \\\n", - " - displacement.loc[(ifg_ndx, gnss_ref_site_name), 'insar_disp']\n", + " displacement_df.loc[ifg_ndx, 'gnss_disp'] = displacement_df.loc[ifg_ndx, 'gnss_disp'].values \\\n", + " - displacement_df.loc[(ifg_ndx, gnss_ref_site_name), 'gnss_disp']\n", + " displacement_df.loc[ifg_ndx, 'insar_disp'] = displacement_df.loc[ifg_ndx, 'insar_disp'].values \\\n", + " - displacement_df.loc[(ifg_ndx, gnss_ref_site_name), 'insar_disp']\n", "\n", " # Reference point pixel coordinates\n", " ref_y_value = int(atr['REF_Y'])\n", @@ -1398,7 +1408,7 @@ "\n", "# Loop through interferograms\n", "gnss_insar_figs = []\n", - "for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + "for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", " fig, ax = plt.subplots(figsize = (8,8))\n", " img1 = ax.imshow(insar_displacement[ifg_ndx],\n", " cmap=cmap_str, vmin=vmin_mm, vmax=vmax_mm, interpolation='nearest',\n", @@ -1407,9 +1417,9 @@ " cbar1 = fig.colorbar(img1, ax=ax, orientation='horizontal')\n", " cbar1.set_label('LOS displacement [mm]')\n", "\n", - " for site_name in displacement.loc[ifg_ndx].index:\n", - " lon, lat = displacement.loc[(ifg_ndx, site_name), 'lon'], displacement.loc[(ifg_ndx, site_name), 'lat']\n", - " color = cmap_obj((displacement.loc[(ifg_ndx, site_name), 'gnss_disp']-vmin_mm)/(vmax_mm-vmin_mm))\n", + " for site_name in displacement_df.loc[ifg_ndx].index:\n", + " lon, lat = displacement_df.loc[(ifg_ndx, site_name), 'lon'], displacement_df.loc[(ifg_ndx, site_name), 'lat']\n", + " color = cmap_obj((displacement_df.loc[(ifg_ndx, site_name), 'gnss_disp']-vmin_mm)/(vmax_mm-vmin_mm))\n", " ax.scatter(lon, lat, s=8**2, color=color, edgecolors='k')\n", " ax.annotate(site_name, (lon,lat), color='black')\n", "\n", @@ -1456,9 +1466,11 @@ "\n", "# Initialize Method 1 success flag\n", "method1_ok = False\n", + " \n", "\n", "if not method1_prereq_ok:\n", - " print(\"Method 1 failed at earliest prerequisite. Continuing to Method 2...\")\n", + " message = \"Method 1 failed at earliest prerequisite. Continuing to Method 2...\"\n", + " print(f\"\\033[1m{message}\\033[0m\")\n", "else:\n", " try:\n", " # Empty dictionaries for GNSS and InSAR measurements, etc.\n", @@ -1472,8 +1484,8 @@ " geod = pyproj.Geod(ellps=\"WGS84\")\n", "\n", " # Loop through interferograms\n", - " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", - " displacement_i = displacement.loc[ifg_ndx]\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", + " displacement_i = displacement_df.loc[ifg_ndx]\n", " insar_disp_i = []\n", " gnss_disp_i = []\n", " ddiff_dist_i = []\n", @@ -1518,7 +1530,7 @@ "\n", " # Loop through interferograms\n", " method1_validation_figs = []\n", - " for ifg_ndx in displacement.index.get_level_values(0).unique():\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", " # Start and end dates as strings\n", " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", @@ -1552,21 +1564,26 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## 5.3 Validate Requirement Based on Binned Measurement Residuals" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "## 5.3 Validate Requirement Based on Binned Measurement Residuals" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Reformat Method 1 outputs using the same IFG keys for dates and residuals\n ifg_indices_ap1 = list(ddiff_dist.keys())\n ddiff_dist_ap1 = [ddiff_dist[i] for i in ifg_indices_ap1]\n abs_ddiff_disp_ap1 = [abs_ddiff_disp[i] for i in ifg_indices_ap1]\n ifgs_date_ap1 = [ifgs_date[i] for i in ifg_indices_ap1]\nelse:\n print(\"Method 1 did not complete successfully. Skipping Method 1 data reformatting.\")" + "source": [ + "if method1_ok:\n", + " # Reformat Method 1 outputs using the same IFG keys for dates and residuals\n", + " ifg_indices_ap1 = list(ddiff_dist.keys())\n", + " ddiff_dist_ap1 = [ddiff_dist[i] for i in ifg_indices_ap1]\n", + " abs_ddiff_disp_ap1 = [abs_ddiff_disp[i] for i in ifg_indices_ap1]\n", + " ifgs_date_ap1 = [ifgs_date[i] for i in ifg_indices_ap1]\n", + "else:\n", + " message = \"Method 1 did not complete successfully. Skipping Method 1 data reformatting.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1583,7 +1600,15 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Define number of interferograms\n n_ifgs = len(ddiff_dist_ap1)\n print(f\"Analyzing {n_ifgs} interferograms\")\nelse:\n print(\"Method 1 failed. Skipping interferogram count.\")" + "source": [ + "if method1_ok:\n", + " # Define number of interferograms\n", + " n_ifgs = len(ddiff_dist_ap1)\n", + " print(f\"Analyzing {n_ifgs} interferograms\")\n", + "else:\n", + " message = \"Method 1 failed. Skipping interferogram count.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1597,21 +1622,74 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Define bins over distance requirement\n n_bins = 10\n bins = np.linspace(0.1, 50.0, num=n_bins+1)\nelse:\n print(\"Method 1 failed. Skipping bin definition.\")" + "source": [ + "if method1_ok:\n", + " # Define bins over distance requirement\n", + " n_bins = 10\n", + " bins = np.linspace(0.1, 50.0, num=n_bins+1)\n", + "else:\n", + " message = \"Method 1 failed. Skipping bin definition.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Pre-allocate array for number of points for each IFG and bins\n n_all = np.empty([n_ifgs, n_bins+1], dtype=int)\n\n # Pre-allocate array for number of points that pass based on requirement\n n_pass = np.empty([n_ifgs,n_bins+1], dtype=int)\n\n # Loop through interferograms\n for i in range(n_ifgs):\n # Determine bin indices\n inds = np.digitize(ddiff_dist_ap1[i], bins)\n\n # Loop through bins\n for j in range(1,n_bins+1):\n # Evaluate requirement for the i-th IFG and j-th distance bin\n rqmt = 3*(1+np.sqrt(ddiff_dist_ap1[i][inds==j]))\n\n # Relative measurement of i-th IFG and j-th distance bin\n rem = abs_ddiff_disp_ap1[i][inds==j]\n assert len(rqmt) == len(rem)\n n_all[i,j-1] = len(rem)\n n_pass[i,j-1] = np.count_nonzero(rem 0, n_all, 1)\n\n # Define threshold of data points in a bin that must pass\n threshold = 0.683\n\n # The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n success_or_fail = ratio > threshold\nelse:\n print(\"Method 1 failed. Skipping ratio calculation.\")" + "source": [ + "if method1_ok:\n", + " # Ratio of double-difference residuals that pass requirement\n", + " ratio = n_pass / np.where(n_all > 0, n_all, 1)\n", + "\n", + " # Define threshold of data points in a bin that must pass\n", + " threshold = 0.683\n", + "\n", + " # The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n", + " success_or_fail = ratio > threshold\n", + "else:\n", + " message = \"Method 1 failed. Skipping ratio calculation.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1633,7 +1711,33 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n def to_str(x:bool):\n if x==True:\n return 'true '\n elif x==False:\n return 'false '\n\n success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n\n columns = []\n for i in range(n_bins):\n columns.append(f'{bins[i]:.2f}-{bins[i+1]:.2f}')\n columns.append('total')\n\n index = []\n for i in range(len(ifgs_date_ap1)):\n index.append(ifgs_date_ap1[i][0].strftime('%Y%m%d')+'-'+ifgs_date_ap1[i][1].strftime('%Y%m%d'))\n\n n_all_pd = pd.DataFrame(n_all,columns=columns,index=index)\n n_pass_pd = pd.DataFrame(n_pass,columns=columns,index=index)\n ratio_pd = pd.DataFrame(ratio,columns=columns,index=index)\n success_or_fail_pd = pd.DataFrame(success_or_fail_str,columns=columns,index=index)\nelse:\n print(\"Method 1 failed. Skipping DataFrame creation.\")" + "source": [ + "if method1_ok:\n", + " def to_str(x:bool):\n", + " if x==True:\n", + " return 'true '\n", + " elif x==False:\n", + " return 'false '\n", + "\n", + " success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n", + "\n", + " columns = []\n", + " for i in range(n_bins):\n", + " columns.append(f'{bins[i]:.2f}-{bins[i+1]:.2f}')\n", + " columns.append('total')\n", + "\n", + " index = []\n", + " for i in range(len(ifgs_date_ap1)):\n", + " index.append(ifgs_date_ap1[i][0].strftime('%Y%m%d')+'-'+ifgs_date_ap1[i][1].strftime('%Y%m%d'))\n", + "\n", + " n_all_pd = pd.DataFrame(n_all,columns=columns,index=index)\n", + " n_pass_pd = pd.DataFrame(n_pass,columns=columns,index=index)\n", + " ratio_pd = pd.DataFrame(ratio,columns=columns,index=index)\n", + " success_or_fail_pd = pd.DataFrame(success_or_fail_str,columns=columns,index=index)\n", + "else:\n", + " message = \"Method 1 failed. Skipping DataFrame creation.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1649,7 +1753,13 @@ "tags": [] }, "outputs": [], - "source": "if method1_ok:\n n_all_pd\nelse:\n print(\"Method 1 failed. No data to display.\")" + "source": [ + "if method1_ok:\n", + " n_all_pd\n", + "else:\n", + " message = \"Method 1 failed. No data to display.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1665,7 +1775,13 @@ "tags": [] }, "outputs": [], - "source": "if method1_ok:\n n_pass_pd\nelse:\n print(\"Method 1 failed. No data to display.\")" + "source": [ + "if method1_ok:\n", + " n_pass_pd\n", + "else:\n", + " message = \"Method 1 failed. No data to display.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1681,7 +1797,19 @@ "tags": [] }, "outputs": [], - "source": "if method1_ok:\n # Stylized pandas table\n validation_table_method1 = ratio_pd.style\n validation_table_method1.set_table_styles([ # create internal CSS classes\n {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n ], overwrite=False)\n validation_table_method1.set_td_classes(success_or_fail_pd)\nelse:\n print(\"Method 1 failed. No table to display.\")" + "source": [ + "if method1_ok:\n", + " # Stylized pandas table\n", + " validation_table_method1 = ratio_pd.style\n", + " validation_table_method1.set_table_styles([ # create internal CSS classes\n", + " {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n", + " {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n", + " ], overwrite=False)\n", + " validation_table_method1.set_td_classes(success_or_fail_pd)\n", + "else:\n", + " message = \"Method 1 failed. No table to display.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1696,14 +1824,56 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n percentage = np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs\n\n method1_summary = f\"Percentage of interferograms passes the requirement: {percentage}\"\n\n if percentage >= 0.70:\n method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n else:\n method1_summary += \"\\nThe interferogram stack fails the requirement.\"\n\n print(method1_summary)\nelse:\n method1_summary = \"Method 1 failed to complete.\"\n print(method1_summary)" + "source": [ + "if method1_ok:\n", + " percentage = np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs\n", + "\n", + " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage}\"\n", + "\n", + " if percentage >= 0.70:\n", + " method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n", + " else:\n", + " method1_summary += \"\\nThe interferogram stack fails the requirement.\"\n", + "\n", + " print(method1_summary)\n", + "else:\n", + " method1_summary = \"Method 1 failed to complete.\"\n", + " print(f\"\\033[1m{method1_summary}\\033[0m\")" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Save Method 1 results to file\n run_date = dt.now().strftime('%Y%m%dT%H%M%S')\n save_fldr = f\"{run_date}-Transient-Method1\"\n save_dir = os.path.join(mintpy_dir, save_fldr)\n\n save_params = {\n 'save_dir': save_dir,\n 'run_date': run_date,\n 'requirement': requirement,\n 'site': site,\n 'method': '1',\n 'sitedata': sitedata['sites'][site],\n 'gnss_insar_figs': gnss_insar_figs,\n 'validation_figs': method1_validation_figs,\n 'validation_table': validation_table_method1,\n 'summary': method1_summary\n }\n save_results(**save_params)\n\n # Save the report in the home directory as well\n save_params['save_dir'] = home_dir\n save_results(**save_params)\nelse:\n print(\"Method 1 failed. Skipping save.\")" + "source": [ + "if method1_ok:\n", + " # Save Method 1 results to file\n", + " run_date = dt.now().strftime('%Y%m%dT%H%M%S')\n", + " save_fldr = f\"{run_date}-Transient-Method1\"\n", + " save_dir = os.path.join(mintpy_dir, save_fldr)\n", + "\n", + " save_params = {\n", + " 'save_dir': save_dir,\n", + " 'run_date': run_date,\n", + " 'requirement': requirement,\n", + " 'site': site,\n", + " 'method': '1',\n", + " 'sitedata': sitedata['sites'][site],\n", + " 'gnss_insar_figs': gnss_insar_figs,\n", + " 'validation_figs': method1_validation_figs,\n", + " 'validation_table': validation_table_method1,\n", + " 'summary': method1_summary\n", + " }\n", + " save_results(**save_params)\n", + "\n", + " # Save the report in the home directory as well\n", + " save_params['save_dir'] = home_dir\n", + " save_results(**save_params)\n", + "else:\n", + " message = \"Method 1 failed. Skipping save.\"\n", + " print(f\"\\033[1m{method1_summary}\\033[0m\")" + ] }, { "cell_type": "markdown", @@ -1755,7 +1925,46 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Construct dataset-layer names as lists\nunwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n for date in ifgs_date]\ncoherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n for date in ifgs_date]\n# Read unwrapped phase from selected interferograms\nifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n# Coherence is coming from the original ifgramStack.h5\ninsar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n\n# Convert phase to displacement in m and switch convention to positive range decrease\ninsar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n\n# Convert displacement units from m to mm\ninsar_displacement = insar_displacement * 1000.\nif singleStack:\n insar_displacement = insar_displacement[np.newaxis, :, :]\n insar_coherence = insar_coherence[np.newaxis, :, :]\n\n# Read 2D mask array\nmsk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n\n# Repeat mask array for each interferogram\nmsk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n \n# Set masked pixels to NaN\ninsar_displacement[msk == 0] = np.nan\ninsar_displacement[insar_displacement==0.0] = np.nan\n\n# Clean up phase-only IFGs to avoid future confusion\ndel ifgs_unw\n\n# Define number of interferograms for Method 2\nn_ifgs_method2 = len(ifgs_date)\nprint(f\"Analyzing {n_ifgs_method2} interferograms\")\n\n# Masking pixels with low coherence\ninsar_displacement[insar_coherence <0.3] = np.nan" + "source": [ + "# Construct dataset-layer names as lists\n", + "unwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", + " for date in ifgs_date]\n", + "coherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", + " for date in ifgs_date]\n", + "# Read unwrapped phase from selected interferograms\n", + "ifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n", + "# Coherence is coming from the original ifgramStack.h5\n", + "insar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n", + "\n", + "# Convert phase to displacement in m and switch convention to positive range decrease\n", + "insar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n", + "\n", + "# Convert displacement units from m to mm\n", + "insar_displacement = insar_displacement * 1000.\n", + "if singleStack:\n", + " insar_displacement = insar_displacement[np.newaxis, :, :]\n", + " insar_coherence = insar_coherence[np.newaxis, :, :]\n", + "\n", + "# Read 2D mask array\n", + "msk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n", + "\n", + "# Repeat mask array for each interferogram\n", + "msk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n", + " \n", + "# Set masked pixels to NaN\n", + "insar_displacement[msk == 0] = np.nan\n", + "insar_displacement[insar_displacement==0.0] = np.nan\n", + "\n", + "# Clean up phase-only IFGs to avoid future confusion\n", + "del ifgs_unw\n", + "\n", + "# Define number of interferograms for Method 2\n", + "n_ifgs_method2 = len(ifgs_date)\n", + "print(f\"Analyzing {n_ifgs_method2} interferograms\")\n", + "\n", + "# Masking pixels with low coherence\n", + "insar_displacement[insar_coherence <0.3] = np.nan" + ] }, { "cell_type": "markdown", @@ -1771,7 +1980,17 @@ "tags": [] }, "outputs": [], - "source": "cmap_obj = copy.copy(plt.get_cmap('gray'))\n\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n ax.set_title(f\"Coherence \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n cbar1 = fig.colorbar(img1, ax=ax)\n cbar1.set_label('coherence')" + "source": [ + "cmap_obj = copy.copy(plt.get_cmap('gray'))\n", + "\n", + "for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", + " ax.set_title(f\"Coherence \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " cbar1 = fig.colorbar(img1, ax=ax)\n", + " cbar1.set_label('coherence')" + ] }, { "cell_type": "code", @@ -1780,7 +1999,17 @@ "tags": [] }, "outputs": [], - "source": "cmap_obj = copy.copy(plt.get_cmap(cmap))\n\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n ax.set_title(f\"Interferogram \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n cbar1 = fig.colorbar(img1, ax=ax)\n cbar1.set_label('LOS displacement [mm]')" + "source": [ + "cmap_obj = copy.copy(plt.get_cmap(cmap))\n", + "\n", + "for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", + " ax.set_title(f\"Interferogram \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " cbar1 = fig.colorbar(img1, ax=ax)\n", + " cbar1.set_label('LOS displacement [mm]')" + ] }, { "cell_type": "markdown", @@ -1824,7 +2053,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Determine the distance and phase difference between site pairs\ndist = []; rel_measure = []\nfor ifg_ndx in range(n_ifgs_method2):\n dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=1000000)\n dist.append(dist_i/1000)\n rel_measure.append(rel_measure_i)" + "source": [ + "# Determine the distance and phase difference between site pairs\n", + "dist = []; rel_measure = []\n", + "for ifg_ndx in range(n_ifgs_method2):\n", + " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=1000000)\n", + " dist.append(dist_i/1000)\n", + " rel_measure.append(rel_measure_i)" + ] }, { "cell_type": "markdown", @@ -1840,7 +2076,17 @@ "tags": [] }, "outputs": [], - "source": "# Plot histogram of distances between pixel pairs\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.hist(dist[ifg_ndx], bins=100)\n ax.set_title(f\"Histogram of distance \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n ax.set_xlabel(r'Distance ($km$)')\n ax.set_ylabel('Frequency')\n ax.set_xlim(0, 50)" + "source": [ + "# Plot histogram of distances between pixel pairs\n", + "for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.hist(dist[ifg_ndx], bins=100)\n", + " ax.set_title(f\"Histogram of distance \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " ax.set_xlabel(r'Distance ($km$)')\n", + " ax.set_ylabel('Frequency')\n", + " ax.set_xlim(0, 50)" + ] }, { "cell_type": "code", @@ -1849,7 +2095,16 @@ "tags": [] }, "outputs": [], - "source": "# Plot histogram of relative measurements\nfor ifg_ndx in range(n_ifgs_method2):\n fig, ax = plt.subplots(figsize=[18, 5.5])\n img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n ax.set_title(f\"Histogram of Relative Measurement \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n ax.set_xlabel(r'Relative Measurement ($mm$)')\n ax.set_ylabel('Frequency')" + "source": [ + "# Plot histogram of relative measurements\n", + "for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n", + " ax.set_title(f\"Histogram of Relative Measurement \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " ax.set_xlabel(r'Relative Measurement ($mm$)')\n", + " ax.set_ylabel('Frequency')" + ] }, { "cell_type": "markdown", @@ -1874,7 +2129,11 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Define number of interferograms for Method 2\nn_ifgs_method2 = len(ifgs_date)\nprint(f\"Analyzing {n_ifgs_method2} interferograms\")" + "source": [ + "# Define number of interferograms for Method 2\n", + "n_ifgs_method2 = len(ifgs_date)\n", + "print(f\"Analyzing {n_ifgs_method2} interferograms\")" + ] }, { "cell_type": "markdown", @@ -1899,7 +2158,35 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Number of points for each ifgs and bins\nn_all = np.empty([n_ifgs_method2, n_bins+1], dtype=int)\n\n# Number of points pass\nn_pass = np.empty([n_ifgs_method2,n_bins+1], dtype=int)\n\n# Loop through interferograms\nfor i in range(n_ifgs_method2):\n # Determine bin indices\n inds = np.digitize(dist[i], bins)\n\n # Loop through bins\n for j in range(1, n_bins+1):\n # Evaluate requirement for the i-th IFG and j-th distance bin\n rqmt = 3*(1+np.sqrt(dist[i][inds==j])) # mission requirement for i-th ifgs and j-th bins\n\n # Relative measurement of i-th IFG and j-th distance bin\n rem = rel_measure[i][inds==j] # relative measurement\n assert len(rqmt) == len(rem)\n n_all[i,j-1] = len(rem)\n n_pass[i,j-1] = np.count_nonzero(rem thresthod) / n_ifgs_method2" + "source": [ + "percentage = np.count_nonzero(ratio_pd['mean'] > thresthod) / n_ifgs_method2" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "method2_summary = f\"Percentage of interferograms passes the requirement (70%): {percentage}.\"\nif percentage >= 0.70:\n method2_summary += \"\\nThe interferogram stack passes the requirement.\"\nelse:\n method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n\nprint(method2_summary)" + "source": [ + "method2_summary = f\"Percentage of interferograms passes the requirement (70%): {percentage}.\"\n", + "if percentage >= 0.70:\n", + " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", + "else:\n", + " method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n", + "\n", + "print(method2_summary)" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Save Method 2 results to file\nrun_date_method2 = dt.now().strftime('%Y%m%dT%H%M%S')\nsave_fldr = f\"{run_date_method2}-Transient-Method2\"\nsave_dir = os.path.join(mintpy_dir, save_fldr)\n\nsave_params = {\n 'save_dir': save_dir,\n 'run_date': run_date_method2,\n 'requirement': requirement,\n 'site': site,\n 'method': '2',\n 'sitedata': sitedata['sites'][site],\n 'gnss_insar_figs': gnss_insar_figs if method1_ok else [],\n 'validation_figs': method2_validation_figs,\n 'validation_table': validation_table_method2,\n 'summary': method2_summary\n}\nsave_results(**save_params)\n\n# Save the report in the home directory as well\nsave_params['save_dir'] = home_dir\nsave_results(**save_params)" + "source": [ + "# Save Method 2 results to file\n", + "run_date_method2 = dt.now().strftime('%Y%m%dT%H%M%S')\n", + "save_fldr = f\"{run_date_method2}-Transient-Method2\"\n", + "save_dir = os.path.join(mintpy_dir, save_fldr)\n", + "\n", + "save_params = {\n", + " 'save_dir': save_dir,\n", + " 'run_date': run_date_method2,\n", + " 'requirement': requirement,\n", + " 'site': site,\n", + " 'method': '2',\n", + " 'sitedata': sitedata['sites'][site],\n", + " 'gnss_insar_figs': gnss_insar_figs if method1_ok else [],\n", + " 'validation_figs': method2_validation_figs,\n", + " 'validation_table': validation_table_method2,\n", + " 'summary': method2_summary\n", + "}\n", + "save_results(**save_params)\n", + "\n", + "# Save the report in the home directory as well\n", + "save_params['save_dir'] = home_dir\n", + "save_results(**save_params)" + ] }, { "cell_type": "markdown", @@ -2096,21 +2444,93 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Define range of displacement values\n disp_range = (min([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]), max([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]))\n\n # Plot histograms of InSAR and GNSS displacement values\n plt.hist(insar_disp[ifg_ndx], bins=100, range=disp_range, color = \"green\", label='D_InSAR')\n plt.hist(gnss_disp[ifg_ndx], bins=100, range=disp_range, color=\"orange\", label='D_GNSS', alpha=0.5)\n\n # Format figure\n plt.legend(loc='upper right')\n plt.title(f\"Displacements \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n f\"\\n Number of station pairs used: {len(insar_disp[ifg_ndx])}\")\n plt.xlabel('LOS Displacement (mm)')\n plt.ylabel('Number of Station Pairs')\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping displacement histogram plots.\")" + "source": [ + "if method1_ok:\n", + " # Loop through interferograms\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", + " # Initialize figure\n", + " plt.figure(figsize=(11,7))\n", + "\n", + " # Define range of displacement values\n", + " disp_range = (min([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]), max([*insar_disp[ifg_ndx],*gnss_disp[ifg_ndx]]))\n", + "\n", + " # Plot histograms of InSAR and GNSS displacement values\n", + " plt.hist(insar_disp[ifg_ndx], bins=100, range=disp_range, color = \"green\", label='D_InSAR')\n", + " plt.hist(gnss_disp[ifg_ndx], bins=100, range=disp_range, color=\"orange\", label='D_GNSS', alpha=0.5)\n", + "\n", + " # Format figure\n", + " plt.legend(loc='upper right')\n", + " plt.title(f\"Displacements \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n", + " f\"\\n Number of station pairs used: {len(insar_disp[ifg_ndx])}\")\n", + " plt.xlabel('LOS Displacement (mm)')\n", + " plt.ylabel('Number of Station Pairs')\n", + " plt.show()\n", + "else:\n", + " message = \"Method 1 failed. Skipping displacement histogram plots.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Plot histogram\n plt.hist(ddiff_disp[ifg_ndx], bins=100, color='darkblue', linewidth=1, label='D_gnss - D_InSAR')\n\n # Format figure\n plt.legend(loc='upper right')\n plt.title(f\"Residuals\"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\"\n f\"\\n Number of stations pairs used: {len(ddiff_disp[ifg_ndx])}\")\n plt.xlabel('Displacement Residual (mm)')\n plt.ylabel('N Stations')\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping residual histogram plots.\")" + "source": [ + "if method1_ok:\n", + " # Loop through interferograms\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", + " # Initialize figure\n", + " plt.figure(figsize=(11,7))\n", + "\n", + " # Plot histogram\n", + " plt.hist(ddiff_disp[ifg_ndx], bins=100, color='darkblue', linewidth=1, label='D_gnss - D_InSAR')\n", + "\n", + " # Format figure\n", + " plt.legend(loc='upper right')\n", + " plt.title(f\"Residuals\"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\"\n", + " f\"\\n Number of stations pairs used: {len(ddiff_disp[ifg_ndx])}\")\n", + " plt.xlabel('Displacement Residual (mm)')\n", + " plt.ylabel('N Stations')\n", + " plt.show()\n", + "else:\n", + " message = \"Method 1 failed. Skipping residual histogram plots.\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in displacement.index.get_level_values(0).unique():\n # Initialize figure\n plt.figure(figsize=(11,7))\n\n # Draw distance threshold\n dist_th = np.linspace(min(ddiff_dist[ifg_ndx]), max(ddiff_dist[ifg_ndx]),100)\n acpt_error = 3*(1+np.sqrt(dist_th))\n\n # Plot residuals\n plt.scatter(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx], s=1)\n plt.plot(dist_th, acpt_error, 'r')\n\n # Format plot\n plt.xlabel(\"Distance (km)\")\n plt.ylabel(\"Amplitude of Displacement Residuals (mm)\")\n plt.title(f\"Residuals \"\n f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n f\"\\n Number of stations pairs used: {len(ddiff_dist[ifg_ndx])}\")\n plt.legend([\"Measurement\", \"Mission Reqiurement\"])\n plt.show()\nelse:\n print(\"Method 1 failed. Skipping residual scatter plots.\")" + "source": [ + "if method1_ok:\n", + " # Loop through interferograms\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", + " # Initialize figure\n", + " plt.figure(figsize=(11,7))\n", + "\n", + " # Draw distance threshold\n", + " dist_th = np.linspace(min(ddiff_dist[ifg_ndx]), max(ddiff_dist[ifg_ndx]),100)\n", + " acpt_error = 3*(1+np.sqrt(dist_th))\n", + "\n", + " # Plot residuals\n", + " plt.scatter(ddiff_dist[ifg_ndx], abs_ddiff_disp[ifg_ndx], s=1)\n", + " plt.plot(dist_th, acpt_error, 'r')\n", + "\n", + " # Format plot\n", + " plt.xlabel(\"Distance (km)\")\n", + " plt.ylabel(\"Amplitude of Displacement Residuals (mm)\")\n", + " plt.title(f\"Residuals \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')} \"\n", + " f\"\\n Number of stations pairs used: {len(ddiff_dist[ifg_ndx])}\")\n", + " plt.legend([\"Measurement\", \"Mission Reqiurement\"])\n", + " plt.show()\n", + "else:\n", + " message = \"Method 1 failed. Skipping residual scatter plots.\"\n", + " print(f\"\\033[1m{message}\\033[0m\") " + ] }, { "cell_type": "code", @@ -2119,7 +2539,39 @@ "tags": [] }, "outputs": [], - "source": "if method1_ok:\n # Loop through interferograms\n for ifg_ndx in range(insar_displacement.shape[0]):\n # Define start and end dates\n start_time_str = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n end_time_str = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n\n # Loop through stations in IFG\n for site_name in gnss_time_series[ifg_ndx].columns:\n print(f\"Plotting GPS postion from {start_time_str} to {end_time_str} at station: {site_name}\")\n\n # Retrieve GNSS time-series\n series = gnss_time_series[ifg_ndx, site_name]\n\n # Initialize figure\n plt.figure(figsize=(15,5))\n\n # Plot time-series data\n plt.scatter(pd.date_range(start=ifgs_date[ifg_ndx][0], end=ifgs_date[ifg_ndx][1]),series)\n\n # Foramt figure\n plt.title(f\"station name: {site_name}\")\n plt.xlabel('Time')\n plt.ylabel('Relative position in LOS direction (mm)')\n\n # Save figure\n plt.savefig(os.path.join(work_dir, f\"{start_time_str}_{end_time_str}_{site_name}.jpg\"))\n plt.close()\nelse:\n print(\"Method 1 failed. Skipping GNSS time series plots.\")" + "source": [ + "if method1_ok:\n", + " # Loop through interferograms\n", + " for ifg_ndx in range(insar_displacement.shape[0]):\n", + " # Define start and end dates\n", + " start_time_str = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", + " end_time_str = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", + "\n", + " # Loop through stations in IFG\n", + " for site_name in gnss_time_series[ifg_ndx].columns:\n", + " print(f\"Plotting GPS postion from {start_time_str} to {end_time_str} at station: {site_name}\")\n", + "\n", + " # Retrieve GNSS time-series\n", + " series = gnss_time_series[ifg_ndx, site_name]\n", + "\n", + " # Initialize figure\n", + " plt.figure(figsize=(15,5))\n", + "\n", + " # Plot time-series data\n", + " plt.scatter(pd.date_range(start=ifgs_date[ifg_ndx][0], end=ifgs_date[ifg_ndx][1]),series)\n", + "\n", + " # Foramt figure\n", + " plt.title(f\"station name: {site_name}\")\n", + " plt.xlabel('Time')\n", + " plt.ylabel('Relative position in LOS direction (mm)')\n", + "\n", + " # Save figure\n", + " plt.savefig(os.path.join(work_dir, f\"{start_time_str}_{end_time_str}_{site_name}.jpg\"))\n", + " plt.close()\n", + "else:\n", + " message = \"Method 1 failed. Skipping GNSS time series plots.\"\n", + " print(f\"\\033[1m{message}\\033[0m\") " + ] }, { "cell_type": "code", From d305025a06cf05bbdcc549833633576111b66997 Mon Sep 17 00:00:00 2001 From: ehavazli Date: Tue, 9 Jun 2026 00:07:48 +0000 Subject: [PATCH 05/13] remove double filtering of date pairs --- methods/transient/Transient_Requirement_Validation.ipynb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index ad200a6..d7341ee 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -420,13 +420,6 @@ " time_interval = (ifgs_date[i][1]-ifgs_date[i][0]).days\n", " if time_interval != 12:\n", " del_row_index.append(i)\n", - "i = 0\n", - "while i Date: Wed, 10 Jun 2026 19:51:36 +0000 Subject: [PATCH 06/13] separate validation methods 1 and 2 summaries --- .../Transient_Requirement_Validation.ipynb | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index d7341ee..1c87022 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -120,7 +120,16 @@ "from solid_utils.plotting import display_coseismic_validation as display_transient_validation\n", "from solid_utils.configs import update_reference_point\n", "from solid_utils.corrections import run_cmd, pairwise_stack_from_timeseries\n", - "from solid_utils.saving import save_results" + "from solid_utils.saving import save_results\n", + "\n", + "from IPython.display import display, HTML\n", + "\n", + "def show_result(site, method, percent_pass):\n", + " display(HTML(f\"\"\"\n", + "
\n", + " Method {method} final result for {site}: {percent_pass}% of interferograms pass the requirement.\n", + "
\n", + " \"\"\"))" ] }, { @@ -1819,11 +1828,11 @@ "outputs": [], "source": [ "if method1_ok:\n", - " percentage = np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs\n", + " percentage_m1 = (np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs) * 100\n", "\n", - " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage}\"\n", + " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage_m1}\"\n", "\n", - " if percentage >= 0.70:\n", + " if percentage_m1 >= 70:\n", " method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n", " else:\n", " method1_summary += \"\\nThe interferogram stack fails the requirement.\"\n", @@ -1869,12 +1878,12 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "
\n", - "Approach 1 final result for CentralValleyA144: around 79% of interferograms passes the requirement.\n", - "
" + "show_result(site, \"1\", percentage_m1)" ] }, { @@ -2071,6 +2080,7 @@ "outputs": [], "source": [ "# Plot histogram of distances between pixel pairs\n", + "hist_insar_figs = []\n", "for ifg_ndx in range(n_ifgs_method2):\n", " fig, ax = plt.subplots(figsize=[18, 5.5])\n", " img1 = ax.hist(dist[ifg_ndx], bins=100)\n", @@ -2078,7 +2088,8 @@ " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", " ax.set_xlabel(r'Distance ($km$)')\n", " ax.set_ylabel('Frequency')\n", - " ax.set_xlim(0, 50)" + " ax.set_xlim(0, 50)\n", + " hist_insar_figs.append(fig)" ] }, { @@ -2096,7 +2107,8 @@ " ax.set_title(f\"Histogram of Relative Measurement \"\n", " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", " ax.set_xlabel(r'Relative Measurement ($mm$)')\n", - " ax.set_ylabel('Frequency')" + " ax.set_ylabel('Frequency')\n", + " hist_insar_figs.append(fig)" ] }, { @@ -2193,10 +2205,10 @@ "ratio = np.hstack((ratio,mean_ratio.T))\n", "\n", "# Define threshold of data points in a bin that must pass\n", - "thresthod = 0.683\n", + "threshold = 0.683\n", "\n", "#The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n", - "success_or_fail = ratio > thresthod" + "success_or_fail = ratio > threshold" ] }, { @@ -2360,7 +2372,7 @@ "metadata": {}, "outputs": [], "source": [ - "percentage = np.count_nonzero(ratio_pd['mean'] > thresthod) / n_ifgs_method2" + "percentage_m2 = (np.count_nonzero(ratio_pd['mean'] > threshold) / n_ifgs_method2) * 100" ] }, { @@ -2369,8 +2381,8 @@ "metadata": {}, "outputs": [], "source": [ - "method2_summary = f\"Percentage of interferograms passes the requirement (70%): {percentage}.\"\n", - "if percentage >= 0.70:\n", + "method2_summary = f\"Percentage of interferograms passes the requirement ({threshold}%): {percentage_m2}.\"\n", + "if percentage_m2 >= 70:\n", " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", "else:\n", " method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n", @@ -2396,7 +2408,7 @@ " 'site': site,\n", " 'method': '2',\n", " 'sitedata': sitedata['sites'][site],\n", - " 'gnss_insar_figs': gnss_insar_figs if method1_ok else [],\n", + " 'gnss_insar_figs': hist_insar_figs,\n", " 'validation_figs': method2_validation_figs,\n", " 'validation_table': validation_table_method2,\n", " 'summary': method2_summary\n", @@ -2409,12 +2421,12 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "
\n", - "Approach 2 final result for CentralValleyA144: 100% of interferograms passes the requirement.\n", - "
" + "show_result(site, \"2\", percentage_m2)" ] }, { @@ -2529,6 +2541,7 @@ "cell_type": "code", "execution_count": null, "metadata": { + "scrolled": true, "tags": [] }, "outputs": [], From 641eac1b25d0d8ace3282c13722fdc38e4f31f64 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 10 Jun 2026 14:28:40 -0700 Subject: [PATCH 07/13] Centralize transient validation parameters --- .../Transient_Requirement_Validation.ipynb | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 1c87022..2d724c9 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -252,6 +252,28 @@ "cmap = plt.get_cmap(cmap_str)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# === Validation Parameters ===\n", + "pixel_radius = 3\n", + "max_workers = 8\n", + "max_reference_site_retries = 100\n", + "\n", + "coherence_threshold = 0.3\n", + "pass_threshold = 0.683\n", + "\n", + "n_bins = 10\n", + "distance_min_km = 0.1\n", + "distance_max_km_method1 = 50.0\n", + "distance_max_km_method2 = 100.0\n", + "\n", + "num_pixel_pair_samples = 1000000" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1124,7 +1146,6 @@ "displacement = {}\n", "gnss_time_series = {}\n", "gnss_time_series_std = {}\n", - "pixel_radius = 3\n", "\n", "# coord once\n", "atr = readfile.read_attribute(ifgs_file)\n", @@ -1211,7 +1232,6 @@ " return local_bad, local_disp, local_ts, local_ts_std\n", "\n", "# Run threads\n", - "max_workers = 8 # start here; try 8, 12, 16\n", "with ThreadPoolExecutor(max_workers=max_workers) as ex:\n", " futures = [ex.submit(process_station, s) for s in site_names]\n", " for fut in as_completed(futures):\n", @@ -1405,8 +1425,9 @@ "source": [ "# Set color values\n", "cmap_obj = copy.copy(plt.get_cmap(cmap))\n", - "vmin_mm = vmin * 0.24 / 4 / np.pi * 1000\n", - "vmax_mm = vmax * 0.24 / 4 / np.pi * 1000\n", + "wavelength = float(insar_metadata['WAVELENGTH'])\n", + "vmin_mm = vmin * wavelength / 4 / np.pi * 1000\n", + "vmax_mm = vmax * wavelength / 4 / np.pi * 1000\n", "\n", "# Loop through interferograms\n", "gnss_insar_figs = []\n", @@ -1524,11 +1545,10 @@ " abs_ddiff_disp[ifg_ndx] = abs(np.array(ddiff_disp_i))\n", "\n", " # Set requirement thresholds\n", - " transient_distance_rqmt = (0.1, 50) # distances for evaluation\n", + " transient_distance_rqmt = (distance_min_km, distance_max_km_method1) # distances for evaluation\n", " transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", "\n", - " n_bins = 10 # number of distance bins for analysis\n", - " threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", + " threshold = pass_threshold # fraction of Gaussian normal distribution for pass/fail\n", "\n", " # Loop through interferograms\n", " method1_validation_figs = []\n", @@ -1627,8 +1647,7 @@ "source": [ "if method1_ok:\n", " # Define bins over distance requirement\n", - " n_bins = 10\n", - " bins = np.linspace(0.1, 50.0, num=n_bins+1)\n", + " bins = np.linspace(distance_min_km, distance_max_km_method1, num=n_bins+1)\n", "else:\n", " message = \"Method 1 failed. Skipping bin definition.\"\n", " print(f\"\\033[1m{message}\\033[0m\")" @@ -1684,9 +1703,9 @@ " ratio = n_pass / np.where(n_all > 0, n_all, 1)\n", "\n", " # Define threshold of data points in a bin that must pass\n", - " threshold = 0.683\n", + " threshold = pass_threshold\n", "\n", - " # The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n", + " # The pass threshold represents the one-standard-deviation probability for a Gaussian distribution.\n", " success_or_fail = ratio > threshold\n", "else:\n", " message = \"Method 1 failed. Skipping ratio calculation.\"\n", @@ -1965,7 +1984,7 @@ "print(f\"Analyzing {n_ifgs_method2} interferograms\")\n", "\n", "# Masking pixels with low coherence\n", - "insar_displacement[insar_coherence <0.3] = np.nan" + "insar_displacement[insar_coherence < coherence_threshold] = np.nan" ] }, { @@ -2059,7 +2078,7 @@ "# Determine the distance and phase difference between site pairs\n", "dist = []; rel_measure = []\n", "for ifg_ndx in range(n_ifgs_method2):\n", - " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=1000000)\n", + " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=num_pixel_pair_samples)\n", " dist.append(dist_i/1000)\n", " rel_measure.append(rel_measure_i)" ] @@ -2154,8 +2173,7 @@ "outputs": [], "source": [ "# Define bins over distance requirement\n", - "n_bins = 10\n", - "bins = np.linspace(0.1, 50.0, num=n_bins+1)" + "bins = np.linspace(distance_min_km, distance_max_km_method2, num=n_bins+1)" ] }, { @@ -2205,9 +2223,9 @@ "ratio = np.hstack((ratio,mean_ratio.T))\n", "\n", "# Define threshold of data points in a bin that must pass\n", - "threshold = 0.683\n", + "threshold = pass_threshold\n", "\n", - "#The assumed nature of Gaussian distribution gives a probability of 0.683 of being within one standard deviation.\n", + "# The pass threshold represents the one-standard-deviation probability for a Gaussian distribution.\n", "success_or_fail = ratio > threshold" ] }, @@ -2218,11 +2236,10 @@ "outputs": [], "source": [ "# Set requirement thresholds\n", - "transient_distance_rqmt = (0.1, 100) # distances for evaluation\n", + "transient_distance_rqmt = (distance_min_km, distance_max_km_method2) # distances for evaluation\n", "transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", "\n", - "n_bins = 10 # number of distance bins for analysis\n", - "threshold = 0.683 # fraction of Gaussian normal distribution for pass/fail\n", + "threshold = pass_threshold # fraction of Gaussian normal distribution for pass/fail\n", "\n", "\n", "# Loop through interferograms\n", From 046db6734d29f89904bc185450340a21c41f2790 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 10 Jun 2026 14:29:45 -0700 Subject: [PATCH 08/13] Harden transient correction setup --- .../Transient_Requirement_Validation.ipynb | 101 ++++++++---------- 1 file changed, 44 insertions(+), 57 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 2d724c9..d3d67b1 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -323,20 +323,20 @@ "ifgramStack_file = os.path.join(mintpy_dir, 'inputs/ifgramStack.h5')\n", "\n", "# Modify network - base command\n", - "command = f\"modify_network.py {ifgramStack_file} -t {config_file} \"\n", + "command = [\"modify_network.py\", ifgramStack_file, \"-t\", config_file]\n", "\n", "# Check whether exclusions specified in my_sites file\n", "if site_info.get('ifgExcludePair') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-ifg {site_info.get('ifgExcludePair')}\"\n", + " command.extend([\"--exclude-ifg\", site_info.get('ifgExcludePair')])\n", "\n", "if site_info.get('ifgExcludeDate') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-date {site_info.get('ifgExcludeDate')}\"\n", + " command.extend([\"--exclude-date\", site_info.get('ifgExcludeDate')])\n", "\n", "if site_info.get('ifgExcludeIndex') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-ifg-index {site_info.get('ifgExcludeIndex')} \"\n", + " command.extend([\"--exclude-ifg-index\", str(site_info.get('ifgExcludeIndex'))])\n", "\n", "# Run command\n", - "process = subprocess.run(command, shell=True)" + "process = subprocess.run(command, check=True)" ] }, { @@ -346,34 +346,6 @@ "### Retrieve available date pairs" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# Formulate ifgramStack file\n", - "ifgramStack_file = os.path.join(mintpy_dir, 'inputs/ifgramStack.h5')\n", - "\n", - "# Modify network - base command\n", - "command = f\"modify_network.py {ifgramStack_file} -t {config_file} \"\n", - "\n", - "# Check whether exclusions specified in my_sites file\n", - "if site_info.get('ifgExcludePair') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-ifg {site_info.get('ifgExcludePair')}\"\n", - "\n", - "if site_info.get('ifgExcludeDate') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-date {site_info.get('ifgExcludeDate')}\"\n", - "\n", - "if site_info.get('ifgExcludeIndex') not in [None, 'auto', 'no']:\n", - " command += f\" --exclude-ifg-index {site_info.get('ifgExcludeIndex')} \"\n", - "\n", - "# Run command\n", - "process = subprocess.run(command, shell=True)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -525,9 +497,13 @@ " update_reference_point(config_file, new_lat, new_lon) # updates the reference point in MintPy config file\n", " \n", "# Now reference interferograms to common lat/lon\n", - "command = 'smallbaselineApp.py ' + str(config_file) + ' --dostep reference_point'\n", - "process = subprocess.run(command, shell=True)\n", - "os.system('info.py inputs/ifgramStack.h5 | egrep \"REF_\"');" + "command = ['smallbaselineApp.py', str(config_file), '--dostep', 'reference_point']\n", + "process = subprocess.run(command, check=True)\n", + "\n", + "info = subprocess.run(['info.py', 'inputs/ifgramStack.h5'], check=True, capture_output=True, text=True)\n", + "for line in info.stdout.splitlines():\n", + " if 'REF_' in line:\n", + " print(line)" ] }, { @@ -721,14 +697,17 @@ "metadata": {}, "outputs": [], "source": [ - "print('#'*10, 'Visualize corrected interferogram(s)', '#'*10)\n", - "view_opts = [iono_output_ifgs,\n", - " '-c', cmap_str,\n", - " '-m', msk_file,\n", - " '-v', str(vmin), str(vmax),\n", - " '--noverbose',\n", - " ]\n", - "view.main(view_opts)" + "if site_info.get('do_iono') != \"False\":\n", + " print('#'*10, 'Visualize corrected interferogram(s)', '#'*10)\n", + " view_opts = [iono_output_ifgs,\n", + " '-c', cmap_str,\n", + " '-m', msk_file,\n", + " '-v', str(vmin), str(vmax),\n", + " '--noverbose',\n", + " ]\n", + " view.main(view_opts)\n", + "else:\n", + " print('#'*10, 'Ionosphere Correction set to False', '#'*10)" ] }, { @@ -751,31 +730,38 @@ "outputs": [], "source": [ "if 'do_tropo' in site_info.keys() and site_info.get(\"do_tropo\") != \"False\":\n", - " if site_info.get(\"tropo_model\") == \"ERA5\":\n", + " tropo_model = site_info.get(\"tropo_model\")\n", + "\n", + " if tropo_model == \"ERA5\":\n", " # ERA5-based correction\n", " tropo_source = \"ERA5\"\n", " tropo_cor_file = os.path.join(mintpy_dir, \"inputs\", f\"{tropo_source}.h5\")\n", "\n", - " elif site_info.get(\"tropo_model\") == \"HRRR\":\n", + " elif tropo_model == \"HRRR\":\n", " # HRRR-based correction\n", " tropo_source = \"HRRR_ARIA\"\n", " tropo_cor_file = os.path.join(mintpy_dir, \"inputs\", f\"{tropo_source}.h5\")\n", "\n", - " elif site_info.get(\"tropo_model\") == \"NISAR\":\n", + " elif tropo_model == \"NISAR\":\n", " tropo_source = \"tropoStack\"\n", " tropo_cor_file = os.path.join(mintpy_dir, \"inputs\", f\"{tropo_source}.h5\")\n", "\n", - " print(f\"Troposphere Correction dataset: {tropo_cor_file:s}\")\n", - "\n", - " # Check it tropo correction file exists\n", - " if os.path.exists(tropo_cor_file):\n", - " dirpath, filename = os.path.split(ifgs_file)\n", - " name, ext = os.path.splitext(filename)\n", - " tropo_output_ifgs = os.path.join(dirpath, f\"{name}_{tropo_source}{ext}\")\n", - " print('#'*10, 'Troposphere Correction set to True', '#'*10)\n", " else:\n", + " print(f\"Unsupported tropo_model '{tropo_model}'. Troposphere Correction set to False\")\n", " site_info['do_tropo'] = \"False\"\n", "\n", + " if site_info['do_tropo'] != \"False\":\n", + " print(f\"Troposphere Correction dataset: {tropo_cor_file:s}\")\n", + "\n", + " # Check it tropo correction file exists\n", + " if os.path.exists(tropo_cor_file):\n", + " dirpath, filename = os.path.split(ifgs_file)\n", + " name, ext = os.path.splitext(filename)\n", + " tropo_output_ifgs = os.path.join(dirpath, f\"{name}_{tropo_source}{ext}\")\n", + " print('#'*10, 'Troposphere Correction set to True', '#'*10)\n", + " else:\n", + " site_info['do_tropo'] = \"False\"\n", + "\n", "else:\n", " site_info['do_tropo'] = \"False\"\n", "\n", @@ -804,8 +790,9 @@ " try:\n", " # Attempt to create a pairwise stack of tropo files\n", " tropo_stack_file = pairwise_stack_from_timeseries(ifgs_file, tropo_cor_file)\n", - " except:\n", - " print(\"Tropo stack generation failed. Setting tropo correction to False\")\n", + " except Exception as e:\n", + " print(f\"Tropo stack generation failed: {e}\")\n", + " print(\"Troposphere Correction set to False\")\n", "\n", " # Stack creation failed, set tropo correction to false\n", " site_info['do_tropo'] = \"False\"" From a67e12c4932589ebe55eaea4562c8ec4c15014a6 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 10 Jun 2026 14:30:19 -0700 Subject: [PATCH 09/13] Harden transient Method 1 GNSS flow --- .../Transient_Requirement_Validation.ipynb | 243 ++++++++++++------ 1 file changed, 158 insertions(+), 85 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index d3d67b1..fd6ffba 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -1220,9 +1220,17 @@ "\n", "# Run threads\n", "with ThreadPoolExecutor(max_workers=max_workers) as ex:\n", - " futures = [ex.submit(process_station, s) for s in site_names]\n", + " futures = {ex.submit(process_station, s): s for s in site_names}\n", " for fut in as_completed(futures):\n", - " local_bad, local_disp, local_ts, local_ts_std = fut.result()\n", + " site_name = futures[fut]\n", + "\n", + " try:\n", + " local_bad, local_disp, local_ts, local_ts_std = fut.result()\n", + " except Exception as e:\n", + " print(f\"Skipping GNSS station {site_name}: {e}\")\n", + " for ifg_ndx in range(n_ifg):\n", + " bad_stn[ifg_ndx].append(site_name)\n", + " continue\n", "\n", " for k, v in local_disp.items():\n", " displacement[k] = v\n", @@ -1255,11 +1263,8 @@ "displacement = dict(sorted(displacement.items()))\n", "bad_stn = dict(sorted(bad_stn.items()))\n", "\n", - "# Earliest Method 1 prerequisite based on collected GNSS/InSAR displacement data\n", - "method1_prereq_ok = len(displacement) > 0\n", - "if not method1_prereq_ok:\n", - " message = \"Method 1 cannot run: no valid GNSS/InSAR displacements were collected.\"\n", - " print(f\"\\033[1m{message}\\033[0m\")" + "# Method 1 prerequisites are consolidated after displacement_df is built.\n", + "method1_prereq_failures = []" ] }, { @@ -1282,11 +1287,16 @@ "outputs": [], "source": [ "# Convert displacement dictionaries to pandas dataframes\n", - "displacement_df = pd.DataFrame.from_dict(displacement, orient='index',\n", - " columns=['lat','lon','gnss_disp','insar_disp'])\n", + "if len(displacement) == 0:\n", + " method1_prereq_failures.append(\"No valid GNSS/InSAR displacements were collected.\")\n", + " displacement_df = pd.DataFrame(columns=['lat', 'lon', 'gnss_disp', 'insar_disp'])\n", + " displacement_df.index = pd.MultiIndex.from_tuples([], names=['ifg index', 'station'])\n", + "else:\n", + " displacement_df = pd.DataFrame.from_dict(displacement, orient='index',\n", + " columns=['lat','lon','gnss_disp','insar_disp'])\n", "\n", - "# Organize by IFG index and site name\n", - "displacement_df.index = pd.MultiIndex.from_tuples(displacement_df.index, names=['ifg index','station'])" + " # Organize by IFG index and site name\n", + " displacement_df.index = pd.MultiIndex.from_tuples(displacement_df.index, names=['ifg index','station'])" ] }, { @@ -1309,17 +1319,22 @@ "for i in displacement_df.index.get_level_values(0).unique():\n", " if len(displacement_df.loc[i]) < 3:\n", " drop_index.append(i)\n", - "displacement_df = displacement_df.drop(drop_index)\n", + "if drop_index:\n", + " displacement_df = displacement_df.drop(drop_index)\n", "\n", "# ifgs_date after drop for approach 1\n", - "ifgs_date_ap1 = np.delete(ifgs_date, drop_index, axis=0)\n", - "if method1_prereq_ok == 1:\n", - " method1_prereq_ok = len(ifgs_date_ap1) > 0\n", - " if not method1_prereq_ok:\n", - " message = \"Method 1 cannot run: no IFGs remain with at least 3 GNSS stations.\"\n", - " print(f\"\\033[1m{message}\\033[0m\")\n", - "else:\n", - " print(f\"\\033[1m{message}\\033[0m\") " + "method1_ifg_indices = list(displacement_df.index.get_level_values(0).unique())\n", + "ifgs_date_ap1 = [ifgs_date[i] for i in method1_ifg_indices]\n", + "if len(method1_ifg_indices) == 0:\n", + " method1_prereq_failures.append(\"No IFGs remain with at least 3 GNSS stations.\")\n", + "\n", + "method1_prereq_ok = len(method1_prereq_failures) == 0\n", + "method1_summary = \"Method 1 failed to complete.\"\n", + "\n", + "if not method1_prereq_ok:\n", + " print(\"\\033[1mMethod 1 cannot run:\\033[0m\")\n", + " for failure in method1_prereq_failures:\n", + " print(f\" - {failure}\")" ] }, { @@ -1358,41 +1373,87 @@ "metadata": {}, "outputs": [], "source": [ - "# Read reference site\n", - "gnss_ref_site_name = sitedata['sites'][site]['gps_ref_site_name']\n", - "print(f\"Using reference site: {gnss_ref_site_name:s}\")\n", - "\n", - "# Loop through interferograms to re-reference\n", - "for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", - " # Determine reference site\n", - " if gnss_ref_site_name in ['auto', 'random']:\n", - " # Choose random GNSS site\n", - " gnss_ref_site_name = random.choice(displacement_df.loc[ifg_ndx].index.unique()) \n", - " print(f\"Ifg {ifg_ndx} reference site: {gnss_ref_site_name}\")\n", - "\n", - " \n", - " # Remove reference site values from GNSS and InSAR displacements\n", - " displacement_df.loc[ifg_ndx, 'gnss_disp'] = displacement_df.loc[ifg_ndx, 'gnss_disp'].values \\\n", - " - displacement_df.loc[(ifg_ndx, gnss_ref_site_name), 'gnss_disp']\n", - " displacement_df.loc[ifg_ndx, 'insar_disp'] = displacement_df.loc[ifg_ndx, 'insar_disp'].values \\\n", - " - displacement_df.loc[(ifg_ndx, gnss_ref_site_name), 'insar_disp']\n", - "\n", - " # Reference point pixel coordinates\n", - " ref_y_value = int(atr['REF_Y'])\n", - " ref_x_value = int(atr['REF_X'])\n", - " # ref_x_value = round((displacement.loc[(ifg_ndx, gnss_ref_site_name),'lon'] - W)/lon_step)\n", - " # ref_y_value = round((displacement.loc[(ifg_ndx, gnss_ref_site_name),'lat'] - N)/lat_step)\n", - "\n", - " # InSAR displacement values at site location for re-referencing\n", - " ref_disp_insar = insar_displacement[ifg_ndx,\n", - " ref_y_value-pixel_radius:ref_y_value+1+pixel_radius, \n", - " ref_x_value-pixel_radius:ref_x_value+1+pixel_radius]\n", - "\n", - " # Re-referenced mean value at site location\n", - " ref_disp_insar = np.nanmean(ref_disp_insar)\n", - "\n", - " # Subtract reference value from InSAR displacement\n", - " insar_displacement[ifg_ndx] -= ref_disp_insar" + "if method1_prereq_ok:\n", + " try:\n", + " # Read reference site\n", + " configured_ref_site_name = sitedata['sites'][site]['gps_ref_site_name']\n", + " print(f\"Using reference site: {configured_ref_site_name:s}\")\n", + "\n", + " ifg_indices = list(displacement_df.index.get_level_values(0).unique())\n", + "\n", + " def reference_site_has_all_data(site_name):\n", + " for ifg_ndx in ifg_indices:\n", + " if site_name not in displacement_df.loc[ifg_ndx].index:\n", + " return False\n", + " values = displacement_df.loc[(ifg_ndx, site_name), ['gnss_disp', 'insar_disp']]\n", + " if not np.isfinite(values.to_numpy(dtype=float)).all():\n", + " return False\n", + " return True\n", + "\n", + " if configured_ref_site_name in ['auto', 'random']:\n", + " candidate_sites = list(displacement_df.index.get_level_values('station').unique())\n", + " tried_sites = set()\n", + " ref_site_name = None\n", + " retry_count = 0\n", + "\n", + " while retry_count < max_reference_site_retries:\n", + " remaining_sites = [site_name for site_name in candidate_sites if site_name not in tried_sites]\n", + " if not remaining_sites:\n", + " break\n", + "\n", + " candidate_ref_site = random.choice(remaining_sites)\n", + " tried_sites.add(candidate_ref_site)\n", + " retry_count += 1\n", + "\n", + " if reference_site_has_all_data(candidate_ref_site):\n", + " ref_site_name = candidate_ref_site\n", + " break\n", + "\n", + " if ref_site_name is None:\n", + " raise ValueError(\n", + " f\"No GNSS reference site found with finite GNSS/InSAR data for all \"\n", + " f\"{len(ifg_indices)} Method 1 interferograms after {retry_count} retries.\"\n", + " )\n", + " else:\n", + " ref_site_name = configured_ref_site_name\n", + " if not reference_site_has_all_data(ref_site_name):\n", + " raise ValueError(\n", + " f\"Reference site '{ref_site_name}' does not have finite GNSS/InSAR data \"\n", + " f\"for all {len(ifg_indices)} Method 1 interferograms.\"\n", + " )\n", + "\n", + " print(f\"Method 1 reference site: {ref_site_name}\")\n", + "\n", + " # Loop through interferograms to re-reference\n", + " for ifg_ndx in ifg_indices:\n", + " # Remove reference site values from GNSS and InSAR displacements\n", + " displacement_df.loc[ifg_ndx, 'gnss_disp'] = displacement_df.loc[ifg_ndx, 'gnss_disp'].values \\\n", + " - displacement_df.loc[(ifg_ndx, ref_site_name), 'gnss_disp']\n", + " displacement_df.loc[ifg_ndx, 'insar_disp'] = displacement_df.loc[ifg_ndx, 'insar_disp'].values \\\n", + " - displacement_df.loc[(ifg_ndx, ref_site_name), 'insar_disp']\n", + "\n", + " # Reference point pixel coordinates\n", + " ref_y_value = int(atr['REF_Y'])\n", + " ref_x_value = int(atr['REF_X'])\n", + " # ref_x_value = round((displacement.loc[(ifg_ndx, ref_site_name),'lon'] - W)/lon_step)\n", + " # ref_y_value = round((displacement.loc[(ifg_ndx, ref_site_name),'lat'] - N)/lat_step)\n", + "\n", + " # InSAR displacement values at site location for re-referencing\n", + " ref_disp_insar = insar_displacement[ifg_ndx,\n", + " ref_y_value-pixel_radius:ref_y_value+1+pixel_radius, \n", + " ref_x_value-pixel_radius:ref_x_value+1+pixel_radius]\n", + "\n", + " # Re-referenced mean value at site location\n", + " ref_disp_insar = np.nanmean(ref_disp_insar)\n", + "\n", + " # Subtract reference value from InSAR displacement\n", + " insar_displacement[ifg_ndx] -= ref_disp_insar\n", + " except Exception as e:\n", + " method1_prereq_ok = False\n", + " method1_prereq_failures.append(f\"Failed to re-reference GNSS/InSAR: {e}\")\n", + " print(f\"\\033[1mMethod 1 cannot run: {method1_prereq_failures[-1]}\\033[0m\")\n", + "else:\n", + " print(\"\\033[1mMethod 1 prerequisite failed. Skipping re-reference.\\033[0m\")" ] }, { @@ -1410,31 +1471,38 @@ }, "outputs": [], "source": [ - "# Set color values\n", - "cmap_obj = copy.copy(plt.get_cmap(cmap))\n", - "wavelength = float(insar_metadata['WAVELENGTH'])\n", - "vmin_mm = vmin * wavelength / 4 / np.pi * 1000\n", - "vmax_mm = vmax * wavelength / 4 / np.pi * 1000\n", - "\n", - "# Loop through interferograms\n", "gnss_insar_figs = []\n", - "for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", - " fig, ax = plt.subplots(figsize = (8,8))\n", - " img1 = ax.imshow(insar_displacement[ifg_ndx],\n", - " cmap=cmap_str, vmin=vmin_mm, vmax=vmax_mm, interpolation='nearest',\n", - " extent=(W, E, S, N))\n", - " ax.set_title(f\"{ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " cbar1 = fig.colorbar(img1, ax=ax, orientation='horizontal')\n", - " cbar1.set_label('LOS displacement [mm]')\n", "\n", - " for site_name in displacement_df.loc[ifg_ndx].index:\n", - " lon, lat = displacement_df.loc[(ifg_ndx, site_name), 'lon'], displacement_df.loc[(ifg_ndx, site_name), 'lat']\n", - " color = cmap_obj((displacement_df.loc[(ifg_ndx, site_name), 'gnss_disp']-vmin_mm)/(vmax_mm-vmin_mm))\n", - " ax.scatter(lon, lat, s=8**2, color=color, edgecolors='k')\n", - " ax.annotate(site_name, (lon,lat), color='black')\n", + "if method1_prereq_ok:\n", + " try:\n", + " # Set color values\n", + " cmap_obj = copy.copy(plt.get_cmap(cmap))\n", + " wavelength = float(insar_metadata['WAVELENGTH'])\n", + " vmin_mm = vmin * wavelength / 4 / np.pi * 1000\n", + " vmax_mm = vmax * wavelength / 4 / np.pi * 1000\n", "\n", - " # Append figure to list\n", - " gnss_insar_figs.append(fig)" + " # Loop through interferograms\n", + " for ifg_ndx in displacement_df.index.get_level_values(0).unique():\n", + " fig, ax = plt.subplots(figsize = (8,8))\n", + " img1 = ax.imshow(insar_displacement[ifg_ndx],\n", + " cmap=cmap_str, vmin=vmin_mm, vmax=vmax_mm, interpolation='nearest',\n", + " extent=(W, E, S, N))\n", + " ax.set_title(f\"{ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " cbar1 = fig.colorbar(img1, ax=ax, orientation='horizontal')\n", + " cbar1.set_label('LOS displacement [mm]')\n", + "\n", + " for site_name in displacement_df.loc[ifg_ndx].index:\n", + " lon, lat = displacement_df.loc[(ifg_ndx, site_name), 'lon'], displacement_df.loc[(ifg_ndx, site_name), 'lat']\n", + " color = cmap_obj((displacement_df.loc[(ifg_ndx, site_name), 'gnss_disp']-vmin_mm)/(vmax_mm-vmin_mm))\n", + " ax.scatter(lon, lat, s=8**2, color=color, edgecolors='k')\n", + " ax.annotate(site_name, (lon,lat), color='black')\n", + "\n", + " # Append figure to list\n", + " gnss_insar_figs.append(fig)\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 1 GNSS/InSAR plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(\"\\033[1mMethod 1 prerequisite failed. Skipping GNSS/InSAR station plots.\\033[0m\")" ] }, { @@ -1479,8 +1547,9 @@ " \n", "\n", "if not method1_prereq_ok:\n", - " message = \"Method 1 failed at earliest prerequisite. Continuing to Method 2...\"\n", - " print(f\"\\033[1m{message}\\033[0m\")\n", + " print(\"\\033[1mMethod 1 prerequisite failed. Continuing to Method 2...\\033[0m\")\n", + " for failure in method1_prereq_failures:\n", + " print(f\" - {failure}\")\n", "else:\n", " try:\n", " # Empty dictionaries for GNSS and InSAR measurements, etc.\n", @@ -1763,7 +1832,7 @@ "outputs": [], "source": [ "if method1_ok:\n", - " n_all_pd\n", + " print(n_all_pd)\n", "else:\n", " message = \"Method 1 failed. No data to display.\"\n", " print(f\"\\033[1m{message}\\033[0m\")" @@ -1785,7 +1854,7 @@ "outputs": [], "source": [ "if method1_ok:\n", - " n_pass_pd\n", + " print(n_pass_pd)\n", "else:\n", " message = \"Method 1 failed. No data to display.\"\n", " print(f\"\\033[1m{message}\\033[0m\")" @@ -1889,7 +1958,11 @@ "metadata": {}, "outputs": [], "source": [ - "show_result(site, \"1\", percentage_m1)" + "if method1_ok:\n", + " show_result(site, \"1\", percentage_m1)\n", + "else:\n", + " message = \"Method 1 failed\"\n", + " print(f\"\\033[1m{message}\\033[0m\")" ] }, { @@ -2551,8 +2624,8 @@ "outputs": [], "source": [ "if method1_ok:\n", - " # Loop through interferograms\n", - " for ifg_ndx in range(insar_displacement.shape[0]):\n", + " # Loop through Method 1 interferograms with GNSS time-series data\n", + " for ifg_ndx in gnss_time_series.columns.get_level_values(0).unique():\n", " # Define start and end dates\n", " start_time_str = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", " end_time_str = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", @@ -2562,7 +2635,7 @@ " print(f\"Plotting GPS postion from {start_time_str} to {end_time_str} at station: {site_name}\")\n", "\n", " # Retrieve GNSS time-series\n", - " series = gnss_time_series[ifg_ndx, site_name]\n", + " series = gnss_time_series[(ifg_ndx, site_name)]\n", "\n", " # Initialize figure\n", " plt.figure(figsize=(15,5))\n", From f8f6c4f9565d8b4c8b815ef54eacec1ae9b75d71 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 10 Jun 2026 14:31:25 -0700 Subject: [PATCH 10/13] Harden transient Method 2 validation flow --- .../Transient_Requirement_Validation.ipynb | 519 +++++++++++------- 1 file changed, 319 insertions(+), 200 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index fd6ffba..982e430 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -117,7 +117,7 @@ "from mintpy.cli import view\n", "\n", "from solid_utils.sampling import load_geo, load_geo_utm, samp_pair\n", - "from solid_utils.plotting import display_coseismic_validation as display_transient_validation\n", + "from solid_utils.plotting import display_transient_validation\n", "from solid_utils.configs import update_reference_point\n", "from solid_utils.corrections import run_cmd, pairwise_stack_from_timeseries\n", "from solid_utils.saving import save_results\n", @@ -2007,44 +2007,58 @@ "metadata": {}, "outputs": [], "source": [ - "# Construct dataset-layer names as lists\n", - "unwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", - " for date in ifgs_date]\n", - "coherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", - " for date in ifgs_date]\n", - "# Read unwrapped phase from selected interferograms\n", - "ifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n", - "# Coherence is coming from the original ifgramStack.h5\n", - "insar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n", + "method2_ok = True\n", + "method2_summary = \"Method 2 failed to complete.\"\n", + "hist_insar_figs = []\n", + "method2_validation_figs = []\n", "\n", - "# Convert phase to displacement in m and switch convention to positive range decrease\n", - "insar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n", + "def mark_method2_failed(stage, error):\n", + " global method2_ok, method2_summary\n", + " method2_ok = False\n", + " method2_summary = f\"Method 2 failed during {stage}: {error}\"\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")\n", "\n", - "# Convert displacement units from m to mm\n", - "insar_displacement = insar_displacement * 1000.\n", - "if singleStack:\n", - " insar_displacement = insar_displacement[np.newaxis, :, :]\n", - " insar_coherence = insar_coherence[np.newaxis, :, :]\n", + "try:\n", + " # Construct dataset-layer names as lists\n", + " unwrapPhaseName = [f\"unwrapPhase-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", + " for date in ifgs_date]\n", + " coherenceName = [f\"coherence-{date[0].strftime('%Y%m%d')}_{date[1].strftime('%Y%m%d')}\"\n", + " for date in ifgs_date]\n", + " # Read unwrapped phase from selected interferograms\n", + " ifgs_unw, insar_metadata = readfile.read(ifgs_file, datasetName=unwrapPhaseName)\n", + " # Coherence is coming from the original ifgramStack.h5\n", + " insar_coherence, _ = readfile.read(os.path.join(mintpy_dir, \"inputs\", \"ifgramStack.h5\"), datasetName=coherenceName)\n", "\n", - "# Read 2D mask array\n", - "msk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n", + " # Convert phase to displacement in m and switch convention to positive range decrease\n", + " insar_displacement = -ifgs_unw*float(insar_metadata['WAVELENGTH']) / (4*np.pi)\n", "\n", - "# Repeat mask array for each interferogram\n", - "msk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n", - " \n", - "# Set masked pixels to NaN\n", - "insar_displacement[msk == 0] = np.nan\n", - "insar_displacement[insar_displacement==0.0] = np.nan\n", + " # Convert displacement units from m to mm\n", + " insar_displacement = insar_displacement * 1000.\n", + " if singleStack:\n", + " insar_displacement = insar_displacement[np.newaxis, :, :]\n", + " insar_coherence = insar_coherence[np.newaxis, :, :]\n", + "\n", + " # Read 2D mask array\n", + " msk, _ = readfile.read(msk_file, datasetName=\"waterMask\")\n", "\n", - "# Clean up phase-only IFGs to avoid future confusion\n", - "del ifgs_unw\n", + " # Repeat mask array for each interferogram\n", + " msk = np.stack([msk] * insar_displacement.shape[0], axis=0)\n", + " \n", + " # Set masked pixels to NaN\n", + " insar_displacement[msk == 0] = np.nan\n", + " insar_displacement[insar_displacement==0.0] = np.nan\n", "\n", - "# Define number of interferograms for Method 2\n", - "n_ifgs_method2 = len(ifgs_date)\n", - "print(f\"Analyzing {n_ifgs_method2} interferograms\")\n", + " # Clean up phase-only IFGs to avoid future confusion\n", + " del ifgs_unw\n", "\n", - "# Masking pixels with low coherence\n", - "insar_displacement[insar_coherence < coherence_threshold] = np.nan" + " # Define number of interferograms for Method 2\n", + " n_ifgs_method2 = len(ifgs_date)\n", + " print(f\"Analyzing {n_ifgs_method2} interferograms\")\n", + "\n", + " # Masking pixels with low coherence\n", + " insar_displacement[insar_coherence < coherence_threshold] = np.nan\n", + "except Exception as e:\n", + " mark_method2_failed(\"InSAR array loading\", e)" ] }, { @@ -2062,15 +2076,21 @@ }, "outputs": [], "source": [ - "cmap_obj = copy.copy(plt.get_cmap('gray'))\n", - "\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", - " ax.set_title(f\"Coherence \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " cbar1 = fig.colorbar(img1, ax=ax)\n", - " cbar1.set_label('coherence')" + "if method2_ok:\n", + " try:\n", + " cmap_obj = copy.copy(plt.get_cmap('gray'))\n", + "\n", + " for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.imshow(insar_coherence[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", + " ax.set_title(f\"Coherence \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " cbar1 = fig.colorbar(img1, ax=ax)\n", + " cbar1.set_label('coherence')\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 2 warning: coherence plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2081,15 +2101,21 @@ }, "outputs": [], "source": [ - "cmap_obj = copy.copy(plt.get_cmap(cmap))\n", + "if method2_ok:\n", + " try:\n", + " cmap_obj = copy.copy(plt.get_cmap(cmap))\n", "\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", - " ax.set_title(f\"Interferogram \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " cbar1 = fig.colorbar(img1, ax=ax)\n", - " cbar1.set_label('LOS displacement [mm]')" + " for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.imshow(insar_displacement[ifg_ndx], cmap=cmap_obj, interpolation='nearest', extent=(W, E, S, N))\n", + " ax.set_title(f\"Interferogram \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " cbar1 = fig.colorbar(img1, ax=ax)\n", + " cbar1.set_label('LOS displacement [mm]')\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 2 warning: interferogram plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2115,11 +2141,17 @@ }, "outputs": [], "source": [ - "if 'NISAR' in dataset:\n", - " X0,Y0 = load_geo_utm(insar_metadata)\n", + "if method2_ok:\n", + " try:\n", + " if 'NISAR' in dataset:\n", + " X0,Y0 = load_geo_utm(insar_metadata)\n", + " else:\n", + " X0,Y0 = load_geo(insar_metadata)\n", + " X0_2d, Y0_2d = np.meshgrid(X0, Y0)\n", + " except Exception as e:\n", + " mark_method2_failed(\"coordinate grid creation\", e)\n", "else:\n", - " X0,Y0 = load_geo(insar_metadata)\n", - "X0_2d, Y0_2d = np.meshgrid(X0, Y0)" + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2135,12 +2167,18 @@ "metadata": {}, "outputs": [], "source": [ - "# Determine the distance and phase difference between site pairs\n", - "dist = []; rel_measure = []\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=num_pixel_pair_samples)\n", - " dist.append(dist_i/1000)\n", - " rel_measure.append(rel_measure_i)" + "if method2_ok:\n", + " try:\n", + " # Determine the distance and phase difference between site pairs\n", + " dist = []; rel_measure = []\n", + " for ifg_ndx in range(n_ifgs_method2):\n", + " dist_i, rel_measure_i = samp_pair(X0_2d, Y0_2d, insar_displacement[ifg_ndx], num_samples=num_pixel_pair_samples)\n", + " dist.append(dist_i/1000)\n", + " rel_measure.append(rel_measure_i)\n", + " except Exception as e:\n", + " mark_method2_failed(\"pixel-pair sampling\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2158,17 +2196,23 @@ }, "outputs": [], "source": [ - "# Plot histogram of distances between pixel pairs\n", - "hist_insar_figs = []\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.hist(dist[ifg_ndx], bins=100)\n", - " ax.set_title(f\"Histogram of distance \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " ax.set_xlabel(r'Distance ($km$)')\n", - " ax.set_ylabel('Frequency')\n", - " ax.set_xlim(0, 50)\n", - " hist_insar_figs.append(fig)" + "if method2_ok:\n", + " try:\n", + " # Plot histogram of distances between pixel pairs\n", + " hist_insar_figs = []\n", + " for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.hist(dist[ifg_ndx], bins=100)\n", + " ax.set_title(f\"Histogram of distance \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " ax.set_xlabel(r'Distance ($km$)')\n", + " ax.set_ylabel('Frequency')\n", + " ax.set_xlim(0, 50)\n", + " hist_insar_figs.append(fig)\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 2 warning: distance histogram plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2179,15 +2223,21 @@ }, "outputs": [], "source": [ - "# Plot histogram of relative measurements\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " fig, ax = plt.subplots(figsize=[18, 5.5])\n", - " img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n", - " ax.set_title(f\"Histogram of Relative Measurement \"\n", - " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", - " ax.set_xlabel(r'Relative Measurement ($mm$)')\n", - " ax.set_ylabel('Frequency')\n", - " hist_insar_figs.append(fig)" + "if method2_ok:\n", + " try:\n", + " # Plot histogram of relative measurements\n", + " for ifg_ndx in range(n_ifgs_method2):\n", + " fig, ax = plt.subplots(figsize=[18, 5.5])\n", + " img1 = ax.hist(rel_measure[ifg_ndx], bins=100)\n", + " ax.set_title(f\"Histogram of Relative Measurement \"\n", + " f\"\\n Date range {ifgs_date[ifg_ndx][0].strftime('%Y%m%d')}-{ifgs_date[ifg_ndx][1].strftime('%Y%m%d')}\")\n", + " ax.set_xlabel(r'Relative Measurement ($mm$)')\n", + " ax.set_ylabel('Frequency')\n", + " hist_insar_figs.append(fig)\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 2 warning: relative-measurement histogram plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2214,9 +2264,15 @@ "metadata": {}, "outputs": [], "source": [ - "# Define number of interferograms for Method 2\n", - "n_ifgs_method2 = len(ifgs_date)\n", - "print(f\"Analyzing {n_ifgs_method2} interferograms\")" + "if method2_ok:\n", + " try:\n", + " # Define number of interferograms for Method 2\n", + " n_ifgs_method2 = len(ifgs_date)\n", + " print(f\"Analyzing {n_ifgs_method2} interferograms\")\n", + " except Exception as e:\n", + " mark_method2_failed(\"interferogram counting\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2232,8 +2288,14 @@ "metadata": {}, "outputs": [], "source": [ - "# Define bins over distance requirement\n", - "bins = np.linspace(distance_min_km, distance_max_km_method2, num=n_bins+1)" + "if method2_ok:\n", + " try:\n", + " # Define bins over distance requirement\n", + " bins = np.linspace(distance_min_km, distance_max_km_method2, num=n_bins+1)\n", + " except Exception as e:\n", + " mark_method2_failed(\"bin definition\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2242,33 +2304,39 @@ "metadata": {}, "outputs": [], "source": [ - "# Number of points for each ifgs and bins\n", - "n_all = np.empty([n_ifgs_method2, n_bins+1], dtype=int)\n", - "\n", - "# Number of points pass\n", - "n_pass = np.empty([n_ifgs_method2,n_bins+1], dtype=int)\n", - "\n", - "# Loop through interferograms\n", - "for i in range(n_ifgs_method2):\n", - " # Determine bin indices\n", - " inds = np.digitize(dist[i], bins)\n", - "\n", - " # Loop through bins\n", - " for j in range(1, n_bins+1):\n", - " # Evaluate requirement for the i-th IFG and j-th distance bin\n", - " rqmt = 3*(1+np.sqrt(dist[i][inds==j])) # mission requirement for i-th ifgs and j-th bins\n", - "\n", - " # Relative measurement of i-th IFG and j-th distance bin\n", - " rem = rel_measure[i][inds==j] # relative measurement\n", - " assert len(rqmt) == len(rem)\n", - " n_all[i,j-1] = len(rem)\n", - " n_pass[i,j-1] = np.count_nonzero(rem 0, n_all, 1)\n", - "mean_ratio = np.array([np.mean(ratio[:,:-1],axis=1)])\n", - "ratio = np.hstack((ratio,mean_ratio.T))\n", + "if method2_ok:\n", + " try:\n", + " # Ratio of sample pairs that pass requirement\n", + " ratio = n_pass / np.where(n_all > 0, n_all, 1)\n", + " mean_ratio = np.array([np.mean(ratio[:,:-1],axis=1)])\n", + " ratio = np.hstack((ratio,mean_ratio.T))\n", "\n", - "# Define threshold of data points in a bin that must pass\n", - "threshold = pass_threshold\n", + " # Define threshold of data points in a bin that must pass\n", + " threshold = pass_threshold\n", "\n", - "# The pass threshold represents the one-standard-deviation probability for a Gaussian distribution.\n", - "success_or_fail = ratio > threshold" + " # The pass threshold represents the one-standard-deviation probability for a Gaussian distribution.\n", + " success_or_fail = ratio > threshold\n", + " except Exception as e:\n", + " mark_method2_failed(\"ratio calculation\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2295,32 +2369,37 @@ "metadata": {}, "outputs": [], "source": [ - "# Set requirement thresholds\n", - "transient_distance_rqmt = (distance_min_km, distance_max_km_method2) # distances for evaluation\n", - "transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", - "\n", - "threshold = pass_threshold # fraction of Gaussian normal distribution for pass/fail\n", + "if method2_ok:\n", + " try:\n", + " # Set requirement thresholds\n", + " transient_distance_rqmt = (distance_min_km, distance_max_km_method2) # distances for evaluation\n", + " transient_threshold_rqmt = lambda L: 3 * (1 + np.sqrt(L)) # coseismic threshold in mm\n", "\n", + " threshold = pass_threshold # fraction of Gaussian normal distribution for pass/fail\n", "\n", - "# Loop through interferograms\n", - "method2_validation_figs = []\n", - "for ifg_ndx in range(n_ifgs_method2):\n", - " # Start and end dates as strings\n", - " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", - " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", "\n", - " # Validation figure and assessment\n", - " _, validation_fig_method2 = display_transient_validation(dist[ifg_ndx], rel_measure[ifg_ndx],\n", - " site_loc, start_date, end_date,\n", - " requirement=transient_threshold_rqmt,\n", - " distance_rqmt=transient_distance_rqmt,\n", - " n_bins=n_bins,\n", - " threshold=threshold,\n", - " sensor='NISAR',\n", - " validation_type=requirement.lower(),\n", - " validation_data='INSAR')\n", + " # Loop through interferograms\n", + " for ifg_ndx in range(n_ifgs_method2):\n", + " # Start and end dates as strings\n", + " start_date = ifgs_date[ifg_ndx][0].strftime('%Y%m%d')\n", + " end_date = ifgs_date[ifg_ndx][1].strftime('%Y%m%d')\n", "\n", - " method2_validation_figs.append(validation_fig_method2)" + " # Validation figure and assessment\n", + " _, validation_fig_method2 = display_transient_validation(dist[ifg_ndx], rel_measure[ifg_ndx],\n", + " site_loc, start_date, end_date,\n", + " requirement=transient_threshold_rqmt,\n", + " distance_rqmt=transient_distance_rqmt,\n", + " n_bins=n_bins,\n", + " threshold=threshold,\n", + " sensor='NISAR',\n", + " validation_type=requirement.lower(),\n", + " validation_data='INSAR')\n", + "\n", + " method2_validation_figs.append(validation_fig_method2)\n", + " except Exception as e:\n", + " print(f\"\\033[1mMethod 2 warning: validation plotting failed: {e}\\033[0m\")\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2344,28 +2423,34 @@ "metadata": {}, "outputs": [], "source": [ - "# Format evaluation success/failure as string\n", - "def to_str(x:bool):\n", - " if x==True:\n", - " return 'true '\n", - " elif x==False:\n", - " return 'false '\n", - "\n", - "success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n", - "\n", - "columns = []\n", - "for i in range(n_bins):\n", - " columns.append(f\"{bins[i]:.2f}-{bins[i+1]:.2f}\")\n", - "columns.append('total')\n", - "\n", - "index = []\n", - "for i in range(len(ifgs_date)):\n", - " index.append(f\"{ifgs_date[i][0].strftime('%Y%m%d')}-{ifgs_date[i][1].strftime('%Y%m%d')}\")\n", - "\n", - "n_all_pd = pd.DataFrame(n_all, columns=columns, index=index)\n", - "n_pass_pd = pd.DataFrame(n_pass, columns=columns, index=index)\n", - "ratio_pd = pd.DataFrame(ratio, columns=columns+['mean'], index=index)\n", - "success_or_fail_pd = pd.DataFrame(success_or_fail_str, columns=columns+['mean'], index=index)" + "if method2_ok:\n", + " try:\n", + " # Format evaluation success/failure as string\n", + " def to_str(x:bool):\n", + " if x==True:\n", + " return 'true '\n", + " elif x==False:\n", + " return 'false '\n", + "\n", + " success_or_fail_str = [list(map(to_str, x)) for x in success_or_fail]\n", + "\n", + " columns = []\n", + " for i in range(n_bins):\n", + " columns.append(f\"{bins[i]:.2f}-{bins[i+1]:.2f}\")\n", + " columns.append('total')\n", + "\n", + " index = []\n", + " for i in range(len(ifgs_date)):\n", + " index.append(f\"{ifgs_date[i][0].strftime('%Y%m%d')}-{ifgs_date[i][1].strftime('%Y%m%d')}\")\n", + "\n", + " n_all_pd = pd.DataFrame(n_all, columns=columns, index=index)\n", + " n_pass_pd = pd.DataFrame(n_pass, columns=columns, index=index)\n", + " ratio_pd = pd.DataFrame(ratio, columns=columns+['mean'], index=index)\n", + " success_or_fail_pd = pd.DataFrame(success_or_fail_str, columns=columns+['mean'], index=index)\n", + " except Exception as e:\n", + " mark_method2_failed(\"DataFrame creation\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2383,7 +2468,10 @@ }, "outputs": [], "source": [ - "n_all_pd" + "if method2_ok:\n", + " display(n_all_pd)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2401,7 +2489,10 @@ }, "outputs": [], "source": [ - "n_pass_pd" + "if method2_ok:\n", + " display(n_pass_pd)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2419,13 +2510,20 @@ }, "outputs": [], "source": [ - "# Stylized pandas table\n", - "validation_table_method2 = ratio_pd.style\n", - "validation_table_method2.set_table_styles([ # create internal CSS classes\n", - " {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n", - " {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n", - "], overwrite=False)\n", - "validation_table_method2.set_td_classes(success_or_fail_pd)" + "if method2_ok:\n", + " try:\n", + " # Stylized pandas table\n", + " validation_table_method2 = ratio_pd.style\n", + " validation_table_method2.set_table_styles([ # create internal CSS classes\n", + " {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n", + " {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n", + " ], overwrite=False)\n", + " validation_table_method2.set_td_classes(success_or_fail_pd)\n", + " display(validation_table_method2)\n", + " except Exception as e:\n", + " mark_method2_failed(\"table styling\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2449,7 +2547,13 @@ "metadata": {}, "outputs": [], "source": [ - "percentage_m2 = (np.count_nonzero(ratio_pd['mean'] > threshold) / n_ifgs_method2) * 100" + "if method2_ok:\n", + " try:\n", + " percentage_m2 = (np.count_nonzero(ratio_pd['mean'] > threshold) / n_ifgs_method2) * 100\n", + " except Exception as e:\n", + " mark_method2_failed(\"percentage calculation\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2458,13 +2562,19 @@ "metadata": {}, "outputs": [], "source": [ - "method2_summary = f\"Percentage of interferograms passes the requirement ({threshold}%): {percentage_m2}.\"\n", - "if percentage_m2 >= 70:\n", - " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", - "else:\n", - " method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n", + "if method2_ok:\n", + " try:\n", + " method2_summary = f\"Percentage of interferograms passes the requirement ({threshold}%): {percentage_m2}.\"\n", + " if percentage_m2 >= 70:\n", + " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", + " else:\n", + " method2_summary += \"\\nThe interferogram stack fails the requirement.\"\n", "\n", - "print(method2_summary)" + " print(method2_summary)\n", + " except Exception as e:\n", + " mark_method2_failed(\"summary creation\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2473,28 +2583,34 @@ "metadata": {}, "outputs": [], "source": [ - "# Save Method 2 results to file\n", - "run_date_method2 = dt.now().strftime('%Y%m%dT%H%M%S')\n", - "save_fldr = f\"{run_date_method2}-Transient-Method2\"\n", - "save_dir = os.path.join(mintpy_dir, save_fldr)\n", - "\n", - "save_params = {\n", - " 'save_dir': save_dir,\n", - " 'run_date': run_date_method2,\n", - " 'requirement': requirement,\n", - " 'site': site,\n", - " 'method': '2',\n", - " 'sitedata': sitedata['sites'][site],\n", - " 'gnss_insar_figs': hist_insar_figs,\n", - " 'validation_figs': method2_validation_figs,\n", - " 'validation_table': validation_table_method2,\n", - " 'summary': method2_summary\n", - "}\n", - "save_results(**save_params)\n", - "\n", - "# Save the report in the home directory as well\n", - "save_params['save_dir'] = home_dir\n", - "save_results(**save_params)" + "if method2_ok:\n", + " try:\n", + " # Save Method 2 results to file\n", + " run_date_method2 = dt.now().strftime('%Y%m%dT%H%M%S')\n", + " save_fldr = f\"{run_date_method2}-Transient-Method2\"\n", + " save_dir = os.path.join(mintpy_dir, save_fldr)\n", + "\n", + " save_params = {\n", + " 'save_dir': save_dir,\n", + " 'run_date': run_date_method2,\n", + " 'requirement': requirement,\n", + " 'site': site,\n", + " 'method': '2',\n", + " 'sitedata': sitedata['sites'][site],\n", + " 'gnss_insar_figs': hist_insar_figs,\n", + " 'validation_figs': method2_validation_figs,\n", + " 'validation_table': validation_table_method2,\n", + " 'summary': method2_summary\n", + " }\n", + " save_results(**save_params)\n", + "\n", + " # Save the report in the home directory as well\n", + " save_params['save_dir'] = home_dir\n", + " save_results(**save_params)\n", + " except Exception as e:\n", + " mark_method2_failed(\"result saving\", e)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { @@ -2503,7 +2619,10 @@ "metadata": {}, "outputs": [], "source": [ - "show_result(site, \"2\", percentage_m2)" + "if method2_ok:\n", + " show_result(site, \"2\", percentage_m2)\n", + "else:\n", + " print(f\"\\033[1m{method2_summary}\\033[0m\")" ] }, { From c183d6e3c98b6d1197c2ee9b6d9f741ccba10eeb Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 10 Jun 2026 14:58:55 -0700 Subject: [PATCH 11/13] report effective parameters --- .../Transient_Requirement_Validation.ipynb | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 982e430..fde7b74 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -177,7 +177,37 @@ "except KeyError:\n", " raise ValueError(f\"Site ID '{site}' not found in {custom_sites}\")\n", "\n", - "print(f\"Loaded site: {site}\")" + "print(f\"Loaded site: {site}\")\n", + "\n", + "\n", + "def build_report_site_info(method, effective_params=None):\n", + " \"\"\"Return configured site values plus effective runtime values for reports.\"\"\"\n", + " effective_params = effective_params or {}\n", + " report_site_info = copy.deepcopy(site_info)\n", + " report_site_info[\"report_parameter_note\"] = (\n", + " \"Configured values come from my_sites; effective_* values were used at runtime.\"\n", + " )\n", + " report_site_info[\"configured_site\"] = site\n", + " report_site_info[\"configured_requirement\"] = requirement\n", + " report_site_info[\"configured_dataset\"] = dataset\n", + " report_site_info[\"configured_custom_sites_file\"] = custom_sites\n", + " report_site_info[\"effective_method\"] = method\n", + "\n", + " configured_ref_site_name = site_info.get(\"gps_ref_site_name\", \"not configured\")\n", + " report_site_info[\"configured_gps_ref_site_name\"] = configured_ref_site_name\n", + "\n", + " if \"gps_ref_site_name\" in effective_params:\n", + " report_site_info[\"effective_gps_ref_site_name\"] = effective_params[\"gps_ref_site_name\"]\n", + " elif method == \"1\":\n", + " report_site_info[\"effective_gps_ref_site_name\"] = \"not selected\"\n", + " else:\n", + " report_site_info[\"effective_gps_ref_site_name\"] = \"not used by Method 2\"\n", + "\n", + " for key, value in effective_params.items():\n", + " if key != \"gps_ref_site_name\":\n", + " report_site_info[f\"effective_{key}\"] = value\n", + "\n", + " return report_site_info" ] }, { @@ -1832,7 +1862,7 @@ "outputs": [], "source": [ "if method1_ok:\n", - " print(n_all_pd)\n", + " display(n_all_pd)\n", "else:\n", " message = \"Method 1 failed. No data to display.\"\n", " print(f\"\\033[1m{message}\\033[0m\")" @@ -1854,7 +1884,7 @@ "outputs": [], "source": [ "if method1_ok:\n", - " print(n_pass_pd)\n", + " display(n_pass_pd)\n", "else:\n", " message = \"Method 1 failed. No data to display.\"\n", " print(f\"\\033[1m{message}\\033[0m\")" @@ -1936,7 +1966,7 @@ " 'requirement': requirement,\n", " 'site': site,\n", " 'method': '1',\n", - " 'sitedata': sitedata['sites'][site],\n", + " 'sitedata': build_report_site_info('1', {'gps_ref_site_name': ref_site_name}),\n", " 'gnss_insar_figs': gnss_insar_figs,\n", " 'validation_figs': method1_validation_figs,\n", " 'validation_table': validation_table_method1,\n", @@ -2596,7 +2626,7 @@ " 'requirement': requirement,\n", " 'site': site,\n", " 'method': '2',\n", - " 'sitedata': sitedata['sites'][site],\n", + " 'sitedata': build_report_site_info('2'),\n", " 'gnss_insar_figs': hist_insar_figs,\n", " 'validation_figs': method2_validation_figs,\n", " 'validation_table': validation_table_method2,\n", From 577aaf2804aba4003d5fefeaff8ac53bd217b285 Mon Sep 17 00:00:00 2001 From: ehavazli Date: Wed, 10 Jun 2026 23:09:00 +0000 Subject: [PATCH 12/13] update report parameters with runtime parameters --- .../Transient_Requirement_Validation.ipynb | 110 +++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index fde7b74..476ce3b 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -117,7 +117,7 @@ "from mintpy.cli import view\n", "\n", "from solid_utils.sampling import load_geo, load_geo_utm, samp_pair\n", - "from solid_utils.plotting import display_transient_validation\n", + "from solid_utils.plotting import display_coseismic_validation as display_transient_validation\n", "from solid_utils.configs import update_reference_point\n", "from solid_utils.corrections import run_cmd, pairwise_stack_from_timeseries\n", "from solid_utils.saving import save_results\n", @@ -180,32 +180,26 @@ "print(f\"Loaded site: {site}\")\n", "\n", "\n", - "def build_report_site_info(method, effective_params=None):\n", - " \"\"\"Return configured site values plus effective runtime values for reports.\"\"\"\n", - " effective_params = effective_params or {}\n", + "def build_report_site_info(method, runtime_params=None):\n", + " \"\"\"Return site info with runtime-resolved values for validation report.\n", + "\n", + " Parameters from my_sites are treated as initial configuration.\n", + " Runtime-resolved values overwrite standard parameter names to reflect\n", + " what was actually used during notebook execution.\n", + " \"\"\"\n", + " runtime_params = runtime_params or {}\n", " report_site_info = copy.deepcopy(site_info)\n", - " report_site_info[\"report_parameter_note\"] = (\n", - " \"Configured values come from my_sites; effective_* values were used at runtime.\"\n", - " )\n", - " report_site_info[\"configured_site\"] = site\n", - " report_site_info[\"configured_requirement\"] = requirement\n", - " report_site_info[\"configured_dataset\"] = dataset\n", - " report_site_info[\"configured_custom_sites_file\"] = custom_sites\n", - " report_site_info[\"effective_method\"] = method\n", - "\n", - " configured_ref_site_name = site_info.get(\"gps_ref_site_name\", \"not configured\")\n", - " report_site_info[\"configured_gps_ref_site_name\"] = configured_ref_site_name\n", - "\n", - " if \"gps_ref_site_name\" in effective_params:\n", - " report_site_info[\"effective_gps_ref_site_name\"] = effective_params[\"gps_ref_site_name\"]\n", - " elif method == \"1\":\n", - " report_site_info[\"effective_gps_ref_site_name\"] = \"not selected\"\n", - " else:\n", - " report_site_info[\"effective_gps_ref_site_name\"] = \"not used by Method 2\"\n", "\n", - " for key, value in effective_params.items():\n", - " if key != \"gps_ref_site_name\":\n", - " report_site_info[f\"effective_{key}\"] = value\n", + " # Add top-level run parameters (not in my_sites JSON)\n", + " report_site_info['site'] = site\n", + " report_site_info['requirement'] = requirement\n", + " report_site_info['dataset'] = dataset\n", + " report_site_info['custom_sites_file'] = custom_sites\n", + " report_site_info['validation_method'] = method\n", + "\n", + " # Update with runtime-resolved values under standard parameter names\n", + " for key, value in runtime_params.items():\n", + " report_site_info[key] = value\n", "\n", " return report_site_info" ] @@ -282,28 +276,6 @@ "cmap = plt.get_cmap(cmap_str)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# === Validation Parameters ===\n", - "pixel_radius = 3\n", - "max_workers = 8\n", - "max_reference_site_retries = 100\n", - "\n", - "coherence_threshold = 0.3\n", - "pass_threshold = 0.683\n", - "\n", - "n_bins = 10\n", - "distance_min_km = 0.1\n", - "distance_max_km_method1 = 50.0\n", - "distance_max_km_method2 = 100.0\n", - "\n", - "num_pixel_pair_samples = 1000000" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1028,7 +1000,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 4.2. Not Used " + "### 4.2. Set Validation parameters " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# === Validation Parameters ===\n", + "pixel_radius = 3\n", + "max_workers = 8\n", + "max_reference_site_retries = 100\n", + "\n", + "coherence_threshold = 0.3\n", + "pass_threshold = 0.683\n", + "\n", + "n_bins = 10\n", + "distance_min_km = 0.1\n", + "distance_max_km_method1 = 50.0\n", + "distance_max_km_method2 = 50.0\n", + "\n", + "num_pixel_pair_samples = 1000000" ] }, { @@ -1180,7 +1174,6 @@ "global_start_str = min(ifg_start).strftime(\"%Y%m%d\")\n", "global_end_str = max(ifg_end).strftime(\"%Y%m%d\")\n", "\n", - "# r = pixel_radius\n", "\n", "def process_station(site_name):\n", " \"\"\"Return per-station results so the main thread can merge dicts safely.\"\"\"\n", @@ -1935,7 +1928,7 @@ "if method1_ok:\n", " percentage_m1 = (np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs) * 100\n", "\n", - " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage_m1}\"\n", + " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage_m1}%\"\n", "\n", " if percentage_m1 >= 70:\n", " method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n", @@ -1960,13 +1953,18 @@ " save_fldr = f\"{run_date}-Transient-Method1\"\n", " save_dir = os.path.join(mintpy_dir, save_fldr)\n", "\n", + " # Build runtime parameters for report\n", + " runtime_params = {\n", + " 'gps_ref_site_name': ref_site_name,\n", + " }\n", + "\n", " save_params = {\n", " 'save_dir': save_dir,\n", " 'run_date': run_date,\n", " 'requirement': requirement,\n", " 'site': site,\n", " 'method': '1',\n", - " 'sitedata': build_report_site_info('1', {'gps_ref_site_name': ref_site_name}),\n", + " 'sitedata': build_report_site_info('1', runtime_params),\n", " 'gnss_insar_figs': gnss_insar_figs,\n", " 'validation_figs': method1_validation_figs,\n", " 'validation_table': validation_table_method1,\n", @@ -2594,7 +2592,7 @@ "source": [ "if method2_ok:\n", " try:\n", - " method2_summary = f\"Percentage of interferograms passes the requirement ({threshold}%): {percentage_m2}.\"\n", + " method2_summary = f\"Percentage of interferograms passes the requirement: {percentage_m2}%.\"\n", " if percentage_m2 >= 70:\n", " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", " else:\n", @@ -2620,13 +2618,19 @@ " save_fldr = f\"{run_date_method2}-Transient-Method2\"\n", " save_dir = os.path.join(mintpy_dir, save_fldr)\n", "\n", + " # Build runtime parameters for report\n", + " # Method 2 does not use GPS reference site\n", + " runtime_params = {\n", + " 'gps_ref_site_name': 'not used by Method 2',\n", + " }\n", + "\n", " save_params = {\n", " 'save_dir': save_dir,\n", " 'run_date': run_date_method2,\n", " 'requirement': requirement,\n", " 'site': site,\n", " 'method': '2',\n", - " 'sitedata': build_report_site_info('2'),\n", + " 'sitedata': build_report_site_info('2', runtime_params),\n", " 'gnss_insar_figs': hist_insar_figs,\n", " 'validation_figs': method2_validation_figs,\n", " 'validation_table': validation_table_method2,\n", From 255e4346e388016f441a87e87952fefc4bf883bb Mon Sep 17 00:00:00 2001 From: ehavazli Date: Wed, 10 Jun 2026 23:44:44 +0000 Subject: [PATCH 13/13] fix wording --- methods/transient/Transient_Requirement_Validation.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/methods/transient/Transient_Requirement_Validation.ipynb b/methods/transient/Transient_Requirement_Validation.ipynb index 476ce3b..4aef3ae 100644 --- a/methods/transient/Transient_Requirement_Validation.ipynb +++ b/methods/transient/Transient_Requirement_Validation.ipynb @@ -1928,7 +1928,7 @@ "if method1_ok:\n", " percentage_m1 = (np.count_nonzero(ratio_pd['total'] > threshold) / n_ifgs) * 100\n", "\n", - " method1_summary = f\"Percentage of interferograms passes the requirement: {percentage_m1}%\"\n", + " method1_summary = f\"Percentage of interferograms that pass the requirement: {percentage_m1}%\"\n", "\n", " if percentage_m1 >= 70:\n", " method1_summary += \"\\nThe interferogram stack passes the requirement.\"\n", @@ -2592,7 +2592,7 @@ "source": [ "if method2_ok:\n", " try:\n", - " method2_summary = f\"Percentage of interferograms passes the requirement: {percentage_m2}%.\"\n", + " method2_summary = f\"Percentage of interferograms that pass the requirement: {percentage_m2}%.\"\n", " if percentage_m2 >= 70:\n", " method2_summary += \"\\nThe interferogram stack passes the requirement.\"\n", " else:\n",