From 20eb412fb3cde39a63a558fde8ece955de13422c Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 6 Mar 2019 14:31:02 +1100 Subject: [PATCH 001/376] move get_header_paths function from task to main gamma module --- pyrate/gamma.py | 25 ++++++++++++++++++++++++- pyrate/tasks/gamma.py | 28 +--------------------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/pyrate/gamma.py b/pyrate/gamma.py index 9afecec5c..39c40ce34 100644 --- a/pyrate/gamma.py +++ b/pyrate/gamma.py @@ -18,11 +18,15 @@ """ # coding: utf-8 -#import os +from os.path import join, split +import re +import glob2 from datetime import date, time, timedelta import numpy as np import pyrate.ifgconstants as ifc +PTN = re.compile(r'\d{8}') # match 8 digits for the dates + # constants GAMMA_DATE = 'date' GAMMA_TIME = 'center_time' @@ -217,6 +221,25 @@ def manage_headers(dem_header_file, header_paths): return combined_header +def get_header_paths(input_file, slc_dir=None): + """ + Function that matches input GAMMA file names with GAMMA header file names + + :param str input_file: input GAMMA .unw file. + :param str slc_dir: GAMMA SLC header file directory + :return: list of matching header files + :rtype: list + """ + if slc_dir: + dir_name = slc_dir + _, file_name = split(input_file) + else: # header file must exist in the same dir as that of .unw + dir_name, file_name = split(input_file) + matches = PTN.findall(file_name) + return [glob2.glob(join(dir_name, '**/*%s*slc.par' % m))[0] + for m in matches] + + class GammaException(Exception): """ Gamma generic exception class diff --git a/pyrate/tasks/gamma.py b/pyrate/tasks/gamma.py index 2c6dd4925..acf179ffe 100644 --- a/pyrate/tasks/gamma.py +++ b/pyrate/tasks/gamma.py @@ -17,18 +17,12 @@ This Python module is a Luigi wrapper for converting GAMMA format input data. """ # pylint: disable=attribute-defined-outside-init -import os -from os.path import join -import re -import glob2 import luigi from pyrate import config -from pyrate.gamma import manage_headers +from pyrate.gamma import manage_headers, get_header_paths from pyrate.shared import write_geotiff, output_tiff_filename from pyrate.tasks.utils import IfgListMixin, InputParam -PTN = re.compile(r'\d{8}') # match 8 digits for the dates - class GammaHasRun(luigi.task.ExternalTask): """ @@ -47,26 +41,6 @@ def output(self): return targets -def get_header_paths(input_file, slc_dir=None): - """ - Function that matches input GAMMA file names with GAMMA header file names - - :param str input_file: input GAMMA .unw file. - :param str slc_dir: GAMMA SLC header file directory - - :return: list of matching header files - :rtype: list - """ - if slc_dir: - dir_name = slc_dir - _, file_name = os.path.split(input_file) - else: # header file must exist in the same dir as that of .unw - dir_name, file_name = os.path.split(input_file) - matches = PTN.findall(file_name) - return [glob2.glob(join(dir_name, '**/*%s*slc.par' % m))[0] - for m in matches] - - class ConvertFileToGeotiff(luigi.Task): """ Task responsible for converting a GAMMA file to GeoTiff. From d4b5fad1d4e317c2372633784dd3e4466b9f9bee Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 6 Mar 2019 20:58:11 +1100 Subject: [PATCH 002/376] restructure the run_prepifg script; concentrate gamma/roipac specific commands in one place --- pyrate/scripts/run_prepifg.py | 197 +++++++++++++++++++++------------- 1 file changed, 125 insertions(+), 72 deletions(-) diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index fc7a947b9..6f6e04e86 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -32,8 +32,7 @@ from pyrate import config as cf from pyrate import roipac from pyrate import gamma -from pyrate.shared import write_geotiff, mkdir_p, output_tiff_filename -from pyrate.tasks import gamma as gamma_task +from pyrate import shared import pyrate.ifgconstants as ifc from pyrate import mpiops @@ -83,7 +82,7 @@ def main(params=None): if params[cf.APS_ELEVATION_MAP]: base_ifg_paths.append(params[cf.APS_ELEVATION_MAP]) - mkdir_p(params[cf.OUT_DIR]) # create output dir + shared.mkdir_p(params[cf.OUT_DIR]) # create output dir if use_luigi: log.info("Running prepifg using luigi") @@ -93,111 +92,165 @@ def main(params=None): else: process_base_ifgs_paths = \ np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] - if processor == ROIPAC: - roipac_prepifg(process_base_ifgs_paths, params) - elif processor == GAMMA: - gamma_prepifg(process_base_ifgs_paths, params) - else: - raise prepifg.PreprocessError('Processor must be ROI_PAC (0) or ' - 'GAMMA (1)') + gtiff_paths = do_geotiff(process_base_ifgs_paths, params) + do_prepifg(gtiff_paths, params) +# if processor == ROIPAC: +# roipac_prepifg(process_base_ifgs_paths, params) +# elif processor == GAMMA: +# gamma_prepifg(process_base_ifgs_paths, params) +# else: +# raise prepifg.PreprocessError('Processor must be ROI_PAC (0) or ' +# 'GAMMA (1)') log.info("Finished prepifg") -def roipac_prepifg(base_ifg_paths, params): +#def roipac_prepifg(base_ifg_paths, params): +# """ +# Prepare ROI_PAC interferograms which combines both conversion to geotiff +# and multilooking/cropping operations. +# +# :param list base_ifg_paths: List of unwrapped interferograms +# :param dict params: Parameters dictionary corresponding to config file +# """ +# log.info("Preparing ROI_PAC format interferograms") +# parallel = params[cf.PARALLEL] +# +# if parallel: +# log.info("Parallel prepifg is not implemented for ROI_PAC") +# +# log.info("Running prepifg in serial") +# xlooks, ylooks, crop = cf.transform_params(params) +# rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) +# if rsc_file is not None: +# projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] +# else: +# raise roipac.RoipacException('No DEM resource/header file is ' +# 'provided') +# dest_base_ifgs = [os.path.join(params[cf.OUT_DIR], +# os.path.basename(q).split('.')[0] + '_' + +# os.path.basename(q).split('.')[1] + '.tif') +# for q in base_ifg_paths] +# +# for b, d in zip(base_ifg_paths, dest_base_ifgs): +# header_file = "%s.%s" % (b, ROI_PAC_HEADER_FILE_EXT) +# header = roipac.manage_header(header_file, projection) +# write_geotiff(header, b, d, nodata=params[cf.NO_DATA_VALUE]) +# prepifg.prepare_ifgs( +# dest_base_ifgs, crop_opt=crop, xlooks=xlooks, ylooks=ylooks) + + +def do_geotiff(base_unw_paths, params): """ - Prepare ROI_PAC interferograms which combines both conversion to geotiff - and multilooking/cropping operations. + Convert input interferograms to geotiff format. - :param list base_ifg_paths: List of unwrapped interferograms + :param list base_unw_paths: List of unwrapped interferograms :param dict params: Parameters dictionary corresponding to config file """ - log.info("Preparing ROI_PAC format interferograms") + # pylint: disable=expression-not-assigned + log.info("Converting input interferograms to geotiff") parallel = params[cf.PARALLEL] if parallel: - log.info("Parallel prepifg is not implemented for ROI_PAC") - - log.info("Running prepifg in serial") - xlooks, ylooks, crop = cf.transform_params(params) - rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) - if rsc_file is not None: - projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] + log.info("Running geotiff conversion in parallel with {} " + "processes".format(params[cf.PROCESSES])) + dest_base_ifgs = Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( + delayed(_geotiff_multiprocessing)(p, params) + for p in base_unw_paths) else: - raise roipac.RoipacException('No DEM resource/header file is ' - 'provided') - dest_base_ifgs = [os.path.join(params[cf.OUT_DIR], - os.path.basename(q).split('.')[0] + '_' + - os.path.basename(q).split('.')[1] + '.tif') - for q in base_ifg_paths] + log.info("Running geotiff conversion in serial") + dest_base_ifgs = [_geotiff_multiprocessing(b, params) + for b in base_unw_paths] - for b, d in zip(base_ifg_paths, dest_base_ifgs): - header_file = "%s.%s" % (b, ROI_PAC_HEADER_FILE_EXT) - header = roipac.manage_header(header_file, projection) - write_geotiff(header, b, d, nodata=params[cf.NO_DATA_VALUE]) - prepifg.prepare_ifgs( - dest_base_ifgs, crop_opt=crop, xlooks=xlooks, ylooks=ylooks) + return dest_base_ifgs -def gamma_prepifg(base_unw_paths, params): +def do_prepifg(gtiff_paths, params): """ - Prepare GAMMA interferograms which combines both conversion to geotiff - and multilooking/cropping operations. + Prepare interferograms by applying multilooking/cropping operations. - :param list base_unw_paths: List of unwrapped interferograms + :param list gtiff_paths: List of full-res geotiffs :param dict params: Parameters dictionary corresponding to config file """ # pylint: disable=expression-not-assigned - log.info("Preparing GAMMA format interferograms") + log.info("Preparing interferograms by cropping/multilooking") parallel = params[cf.PARALLEL] - # dest_base_ifgs: location of geo_tif's - if parallel: - log.info("Running prepifg in parallel with {} " - "processes".format(params[cf.PROCESSES])) - dest_base_ifgs = Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( - delayed(_gamma_multiprocessing)(p, params) - for p in base_unw_paths) + if all([os.path.isfile(f) for f in gtiff_paths]): + ifgs = [prepifg.dem_or_ifg(p) for p in gtiff_paths] + xlooks, ylooks, crop = cf.transform_params(params) + user_exts = (params[cf.IFG_XFIRST], params[cf.IFG_YFIRST], + params[cf.IFG_XLAST], params[cf.IFG_YLAST]) + exts = prepifg.get_analysis_extent(crop, ifgs, xlooks, ylooks, + user_exts=user_exts) + thresh = params[cf.NO_DATA_AVERAGING_THRESHOLD] + if parallel: + Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( + delayed(prepifg.prepare_ifg)(p, xlooks, ylooks, exts, thresh, crop) + for p in gtiff_paths) + else: + [prepifg.prepare_ifg(i, xlooks, ylooks, exts, + thresh, crop) for i in gtiff_paths] +# prepifg.prepare_ifgs(gtiff_paths, crop_opt=crop, xlooks=xlooks, +# ylooks=ylooks, thresh=thresh, user_exts=user_exts) else: - log.info("Running prepifg in serial") - dest_base_ifgs = [_gamma_multiprocessing(b, params) - for b in base_unw_paths] + log.info("Full-res geotiffs do not exist") - ifgs = [prepifg.dem_or_ifg(p) for p in dest_base_ifgs] - xlooks, ylooks, crop = cf.transform_params(params) - user_exts = (params[cf.IFG_XFIRST], params[cf.IFG_YFIRST], - params[cf.IFG_XLAST], params[cf.IFG_YLAST]) - exts = prepifg.get_analysis_extent(crop, ifgs, xlooks, ylooks, - user_exts=user_exts) - thresh = params[cf.NO_DATA_AVERAGING_THRESHOLD] - if parallel: - Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( - delayed(prepifg.prepare_ifg)(p, xlooks, ylooks, exts, thresh, crop) - for p in dest_base_ifgs) + +def _geotiff_multiprocessing(unw_path, params): + """ + Multiprocessing wrapper for full-res geotiff conversion + """ + dest = shared.output_tiff_filename(unw_path, params[cf.OUT_DIR]) + processor = params[cf.PROCESSOR] # roipac or gamma + + # Create full-res geotiff if not already on disk + if not os.path.exists(dest): + if processor == GAMMA: + header = _gamma_header(unw_path, params) + elif processor == ROIPAC: + header = _roipac_header(unw_path, params) + else: + raise prepifg.PreprocessError('Processor must be ROI_PAC (0) or ' + 'GAMMA (1)') + shared.write_geotiff(header, unw_path, dest, + nodata=params[cf.NO_DATA_VALUE]) else: - [prepifg.prepare_ifg(i, xlooks, ylooks, exts, - thresh, crop) for i in dest_base_ifgs] + log.info("Full-res geotiff already exists") + return dest -def _gamma_multiprocessing(unw_path, params): + +def _gamma_header(unw_path, params): """ - Multiprocessing wrapper for GAMMA full-res geotiff conversion + Function to obtain combined Gamma headers """ dem_hdr_path = params[cf.DEM_HEADER_FILE] slc_dir = params[cf.SLC_DIR] - header_paths = gamma_task.get_header_paths(unw_path, slc_dir=slc_dir) + header_paths = gamma.get_header_paths(unw_path, slc_dir=slc_dir) combined_headers = gamma.manage_headers(dem_hdr_path, header_paths) - dest = output_tiff_filename(unw_path, params[cf.OUT_DIR]) if os.path.basename(unw_path).split('.')[1] == \ (params[cf.APS_INCIDENCE_EXT] or params[cf.APS_ELEVATION_EXT]): # TODO: implement incidence class here combined_headers['FILE_TYPE'] = 'Incidence' - # Create full-res geotiff if not already on disk - if not os.path.exists(dest): - write_geotiff(combined_headers, unw_path, dest, - nodata=params[cf.NO_DATA_VALUE]) + return combined_headers + + +def _roipac_header(unw_path, params): + """ + Function to obtain a Roipac headers + """ + rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) + if rsc_file is not None: + projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] else: - log.info("Full-res geotiff already exists") + raise roipac.RoipacException('No DEM resource/header file is ' + 'provided') + + header_file = "%s.%s" % (unw_path, ROI_PAC_HEADER_FILE_EXT) + header = roipac.manage_header(header_file, projection) + + return header + - return dest From 46f208c6f9c230ec0171bde568de571618f4a667 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Thu, 7 Mar 2019 15:58:43 +1100 Subject: [PATCH 003/376] split geotiff conversion and multilook/cropping functionality in to separate processing options --- pyrate/scripts/converttogtif.py | 168 +++++++++++++++++++++++++++++--- pyrate/scripts/main.py | 21 +++- pyrate/scripts/run_prepifg.py | 138 +------------------------- 3 files changed, 175 insertions(+), 152 deletions(-) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index dfad919a2..8234f55c0 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -14,30 +14,168 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This Python module uses Luigi to convert interferograms to geotiff format +This Python script converts ROI_PAC or GAMMA format input interferograms +into geotiff format files """ +# -*- coding: utf-8 -*- from __future__ import print_function import sys +import os +import logging import luigi +from joblib import Parallel, delayed +import numpy as np + from pyrate.tasks.utils import pythonify_config from pyrate.tasks import ConvertToGeotiff +from pyrate.prepifg import PreprocessError +from pyrate import config as cf +from pyrate import roipac +from pyrate import gamma +from pyrate import shared +import pyrate.ifgconstants as ifc +from pyrate import mpiops + +log = logging.getLogger(__name__) + +ROI_PAC_HEADER_FILE_EXT = 'rsc' +GAMMA = 1 +ROIPAC = 0 + + +def main(params=None): + """ + Function for converting input interferograms into geotiffs. + + :param dict params: Parameters dictionary read in from the config file + """ + # TODO: looks like base_ifg_paths are ordered according to ifg list + # This probably won't be a problem because input list won't be reordered + # and the original gamma generated list is ordered) this may not affect + # the important pyrate stuff anyway, but might affect gen_thumbs.py. + # Going to assume base_ifg_paths is ordered correcly + # pylint: disable=too-many-branches + + usage = 'Usage: pyrate converttogtif ' + if mpiops.size > 1: # Over-ride input options if this is an MPI job + params[cf.LUIGI] = False + params[cf.PARALLEL] = False + + if params: + base_ifg_paths = cf.original_ifg_paths(params[cf.IFG_FILE_LIST]) + use_luigi = params[cf.LUIGI] # luigi or no luigi + if use_luigi: + raise cf.ConfigException('params can not be provided with luigi') + else: # if params not provided read from config file + if (not params) and (len(sys.argv) < 3): + print(usage) + return + base_ifg_paths, _, params = cf.get_ifg_paths(sys.argv[2]) + use_luigi = params[cf.LUIGI] # luigi or no luigi + raw_config_file = sys.argv[2] + + base_ifg_paths.append(params[cf.DEM_FILE]) + processor = params[cf.PROCESSOR] # roipac or gamma + if processor == GAMMA: # Incidence/elevation only supported for GAMMA + if params[cf.APS_INCIDENCE_MAP]: + base_ifg_paths.append(params[cf.APS_INCIDENCE_MAP]) + if params[cf.APS_ELEVATION_MAP]: + base_ifg_paths.append(params[cf.APS_ELEVATION_MAP]) + + shared.mkdir_p(params[cf.OUT_DIR]) # create output dir + + if use_luigi: + log.info("Running converttogtif using luigi") + luigi.configuration.LuigiConfigParser.add_config_path( + pythonify_config(raw_config_file)) + luigi.build([ConvertToGeotiff()], local_scheduler=True) + else: + process_base_ifgs_paths = \ + np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] + gtiff_paths = do_geotiff(process_base_ifgs_paths, params) + log.info("Finished converttogtif") + + +def do_geotiff(base_unw_paths, params): + """ + Convert input interferograms to geotiff format. + + :param list base_unw_paths: List of unwrapped interferograms + :param dict params: Parameters dictionary corresponding to config file + """ + # pylint: disable=expression-not-assigned + log.info("Converting input interferograms to geotiff") + parallel = params[cf.PARALLEL] + + if parallel: + log.info("Running geotiff conversion in parallel with {} " + "processes".format(params[cf.PROCESSES])) + dest_base_ifgs = Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( + delayed(_geotiff_multiprocessing)(p, params) + for p in base_unw_paths) + else: + log.info("Running geotiff conversion in serial") + dest_base_ifgs = [_geotiff_multiprocessing(b, params) + for b in base_unw_paths] + + return dest_base_ifgs + + +def _geotiff_multiprocessing(unw_path, params): + """ + Multiprocessing wrapper for full-res geotiff conversion + """ + dest = shared.output_tiff_filename(unw_path, params[cf.OUT_DIR]) + processor = params[cf.PROCESSOR] # roipac or gamma + + # Create full-res geotiff if not already on disk + if not os.path.exists(dest): + if processor == GAMMA: + header = _gamma_header(unw_path, params) + elif processor == ROIPAC: + header = _roipac_header(unw_path, params) + else: + raise PreprocessError('Processor must be ROI_PAC (0) or ' + 'GAMMA (1)') + shared.write_geotiff(header, unw_path, dest, + nodata=params[cf.NO_DATA_VALUE]) + else: + log.info("Full-res geotiff already exists") + + return dest -def main(): +def _gamma_header(unw_path, params): """ - Wrapper function to convert unwrapped interferograms into geotiffs - for all data types + Function to obtain combined Gamma headers """ - usage = 'Usage: gamma.py ' - if len(sys.argv) == 1 or sys.argv[1] == '-h' \ - or sys.argv[1] == '--help': # pragma: no cover - print(usage) - return - raw_config_file = sys.argv[1] # this does '~' expansion automatically - luigi.configuration.LuigiConfigParser.add_config_path( - pythonify_config(raw_config_file)) - luigi.build([ConvertToGeotiff()], local_scheduler=True) + dem_hdr_path = params[cf.DEM_HEADER_FILE] + slc_dir = params[cf.SLC_DIR] + header_paths = gamma.get_header_paths(unw_path, slc_dir=slc_dir) + combined_headers = gamma.manage_headers(dem_hdr_path, header_paths) + + if os.path.basename(unw_path).split('.')[1] == \ + (params[cf.APS_INCIDENCE_EXT] or params[cf.APS_ELEVATION_EXT]): + # TODO: implement incidence class here + combined_headers['FILE_TYPE'] = 'Incidence' + + return combined_headers + + +def _roipac_header(unw_path, params): + """ + Function to obtain a Roipac headers + """ + rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) + if rsc_file is not None: + projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] + else: + raise roipac.RoipacException('No DEM resource/header file is ' + 'provided') + + header_file = "%s.%s" % (unw_path, ROI_PAC_HEADER_FILE_EXT) + header = roipac.manage_header(header_file, projection) + + return header -if __name__ == "__main__": - main() diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 5f246deca..b6c3f0e85 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -24,7 +24,7 @@ import click from pyrate import pyratelog as pylog from pyrate import config as cf -from pyrate.scripts import run_prepifg, run_pyrate, postprocessing +from pyrate.scripts import converttogtif, run_prepifg, run_pyrate, postprocessing from pyrate import __version__ log = logging.getLogger(__name__) @@ -50,12 +50,27 @@ def cli(verbosity): pylog.configure(verbosity) +@cli.command() +@click.argument('config_file') +def converttogeotiff(config_file): + """ + Convert input files to geotiff + """ + config_file = abspath(config_file) + params = cf.get_config_params(config_file) + log.info('This job was run with the following parameters:') + log.info(json.dumps(params, indent=4, sort_keys=True)) + if params[cf.LUIGI]: + converttogtif.main() + else: + converttogtif.main(params) + + @cli.command() @click.argument('config_file') def prepifg(config_file): """ - Convert input files to geotiff and perform multilooking - (resampling) and/or cropping + Perform multilooking (resampling) and/or cropping on input files """ config_file = abspath(config_file) params = cf.get_config_params(config_file) diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 6f6e04e86..77a749788 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This Python script converts ROI_PAC or GAMMA format unwrapped -interferograms into geotiffs and applies optional multilooking and cropping. +This Python script applies optional multilooking and cropping to input +interferogram geotiff files. """ # -*- coding: utf-8 -*- from __future__ import print_function @@ -30,17 +30,13 @@ from pyrate.tasks.prepifg import PrepareInterferograms from pyrate import prepifg from pyrate import config as cf -from pyrate import roipac -from pyrate import gamma from pyrate import shared import pyrate.ifgconstants as ifc from pyrate import mpiops log = logging.getLogger(__name__) -ROI_PAC_HEADER_FILE_EXT = 'rsc' GAMMA = 1 -ROIPAC = 0 def main(params=None): @@ -92,78 +88,12 @@ def main(params=None): else: process_base_ifgs_paths = \ np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] - gtiff_paths = do_geotiff(process_base_ifgs_paths, params) + gtiff_paths = [shared.output_tiff_filename(f, \ + params[cf.OUT_DIR]) for f in process_base_ifgs_paths] do_prepifg(gtiff_paths, params) -# if processor == ROIPAC: -# roipac_prepifg(process_base_ifgs_paths, params) -# elif processor == GAMMA: -# gamma_prepifg(process_base_ifgs_paths, params) -# else: -# raise prepifg.PreprocessError('Processor must be ROI_PAC (0) or ' -# 'GAMMA (1)') log.info("Finished prepifg") -#def roipac_prepifg(base_ifg_paths, params): -# """ -# Prepare ROI_PAC interferograms which combines both conversion to geotiff -# and multilooking/cropping operations. -# -# :param list base_ifg_paths: List of unwrapped interferograms -# :param dict params: Parameters dictionary corresponding to config file -# """ -# log.info("Preparing ROI_PAC format interferograms") -# parallel = params[cf.PARALLEL] -# -# if parallel: -# log.info("Parallel prepifg is not implemented for ROI_PAC") -# -# log.info("Running prepifg in serial") -# xlooks, ylooks, crop = cf.transform_params(params) -# rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) -# if rsc_file is not None: -# projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] -# else: -# raise roipac.RoipacException('No DEM resource/header file is ' -# 'provided') -# dest_base_ifgs = [os.path.join(params[cf.OUT_DIR], -# os.path.basename(q).split('.')[0] + '_' + -# os.path.basename(q).split('.')[1] + '.tif') -# for q in base_ifg_paths] -# -# for b, d in zip(base_ifg_paths, dest_base_ifgs): -# header_file = "%s.%s" % (b, ROI_PAC_HEADER_FILE_EXT) -# header = roipac.manage_header(header_file, projection) -# write_geotiff(header, b, d, nodata=params[cf.NO_DATA_VALUE]) -# prepifg.prepare_ifgs( -# dest_base_ifgs, crop_opt=crop, xlooks=xlooks, ylooks=ylooks) - - -def do_geotiff(base_unw_paths, params): - """ - Convert input interferograms to geotiff format. - - :param list base_unw_paths: List of unwrapped interferograms - :param dict params: Parameters dictionary corresponding to config file - """ - # pylint: disable=expression-not-assigned - log.info("Converting input interferograms to geotiff") - parallel = params[cf.PARALLEL] - - if parallel: - log.info("Running geotiff conversion in parallel with {} " - "processes".format(params[cf.PROCESSES])) - dest_base_ifgs = Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( - delayed(_geotiff_multiprocessing)(p, params) - for p in base_unw_paths) - else: - log.info("Running geotiff conversion in serial") - dest_base_ifgs = [_geotiff_multiprocessing(b, params) - for b in base_unw_paths] - - return dest_base_ifgs - - def do_prepifg(gtiff_paths, params): """ Prepare interferograms by applying multilooking/cropping operations. @@ -190,67 +120,7 @@ def do_prepifg(gtiff_paths, params): else: [prepifg.prepare_ifg(i, xlooks, ylooks, exts, thresh, crop) for i in gtiff_paths] -# prepifg.prepare_ifgs(gtiff_paths, crop_opt=crop, xlooks=xlooks, -# ylooks=ylooks, thresh=thresh, user_exts=user_exts) else: log.info("Full-res geotiffs do not exist") -def _geotiff_multiprocessing(unw_path, params): - """ - Multiprocessing wrapper for full-res geotiff conversion - """ - dest = shared.output_tiff_filename(unw_path, params[cf.OUT_DIR]) - processor = params[cf.PROCESSOR] # roipac or gamma - - # Create full-res geotiff if not already on disk - if not os.path.exists(dest): - if processor == GAMMA: - header = _gamma_header(unw_path, params) - elif processor == ROIPAC: - header = _roipac_header(unw_path, params) - else: - raise prepifg.PreprocessError('Processor must be ROI_PAC (0) or ' - 'GAMMA (1)') - shared.write_geotiff(header, unw_path, dest, - nodata=params[cf.NO_DATA_VALUE]) - else: - log.info("Full-res geotiff already exists") - - return dest - - -def _gamma_header(unw_path, params): - """ - Function to obtain combined Gamma headers - """ - dem_hdr_path = params[cf.DEM_HEADER_FILE] - slc_dir = params[cf.SLC_DIR] - header_paths = gamma.get_header_paths(unw_path, slc_dir=slc_dir) - combined_headers = gamma.manage_headers(dem_hdr_path, header_paths) - - if os.path.basename(unw_path).split('.')[1] == \ - (params[cf.APS_INCIDENCE_EXT] or params[cf.APS_ELEVATION_EXT]): - # TODO: implement incidence class here - combined_headers['FILE_TYPE'] = 'Incidence' - - return combined_headers - - -def _roipac_header(unw_path, params): - """ - Function to obtain a Roipac headers - """ - rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) - if rsc_file is not None: - projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] - else: - raise roipac.RoipacException('No DEM resource/header file is ' - 'provided') - - header_file = "%s.%s" % (unw_path, ROI_PAC_HEADER_FILE_EXT) - header = roipac.manage_header(header_file, projection) - - return header - - From 04d36ab697ed587b081dc9794c77986b34693b81 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Tue, 12 Mar 2019 16:06:59 +1100 Subject: [PATCH 004/376] make dem conversion optional. stops crash if demfile not stated in conf file --- pyrate/scripts/converttogtif.py | 4 +++- pyrate/scripts/run_prepifg.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index 8234f55c0..ac72a0e14 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -74,7 +74,9 @@ def main(params=None): use_luigi = params[cf.LUIGI] # luigi or no luigi raw_config_file = sys.argv[2] - base_ifg_paths.append(params[cf.DEM_FILE]) + if params[cf.DEM_FILE] is not None: # optional DEM conversion + base_ifg_paths.append(params[cf.DEM_FILE]) + processor = params[cf.PROCESSOR] # roipac or gamma if processor == GAMMA: # Incidence/elevation only supported for GAMMA if params[cf.APS_INCIDENCE_MAP]: diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 77a749788..08df0ae89 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -70,7 +70,9 @@ def main(params=None): use_luigi = params[cf.LUIGI] # luigi or no luigi raw_config_file = sys.argv[2] - base_ifg_paths.append(params[cf.DEM_FILE]) + if params[cf.DEM_FILE] is not None: # optional DEM conversion + base_ifg_paths.append(params[cf.DEM_FILE]) + processor = params[cf.PROCESSOR] # roipac or gamma if processor == GAMMA: # Incidence/elevation only supported for GAMMA if params[cf.APS_INCIDENCE_MAP]: From 33416f2dda23007c0b79582aeeae9c7e24829b8d Mon Sep 17 00:00:00 2001 From: Josh Sixsmith Date: Wed, 13 Mar 2019 14:36:19 +1100 Subject: [PATCH 005/376] Generalise write geotif (#182) * Initial upload of pyrate/conversion/base.py * Added a docstring notice for the Conversion class; future concept, not to necessarily be developed now. * Initial upload of pyrate/conversion/__init__.py * Fixed imports. --- pyrate/conversion/__init__.py | 0 pyrate/conversion/base.py | 322 ++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 pyrate/conversion/__init__.py create mode 100644 pyrate/conversion/base.py diff --git a/pyrate/conversion/__init__.py b/pyrate/conversion/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyrate/conversion/base.py b/pyrate/conversion/base.py new file mode 100644 index 000000000..4045c397a --- /dev/null +++ b/pyrate/conversion/base.py @@ -0,0 +1,322 @@ +# This Python module is part of the PyRate software package. +# +# Copyright 2017 Geoscience Australia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This Python module contains the base clase that defines +conversion routines for handling metadata, reading of +various formats and output to GeoTIFF (which can be extended to other formats). +""" + +import gdal +import rasterio +from pyrate import ifgconstants as ifc + + +def metadata(header): + """ + Standardise the metadata to enable simpler output for the + write_geotif function. + + :param header: + :type dict: + Dictionary containing interferogram metadata. + + :return: + A dict containing specific flags for either ifg, dem or + incidence. + """ + # similiar to what is written in the Conversion base class' _pass_header + + return + + +def rio_dataset(out_fname, columns, rows, driver="GTiff", bands=1, + dtype="float32", metadata=None, crs=None, geotransform=None, + creation_opts=None): + """ + Initialises a rasterio dataset object for writing image data. + The reason being that the write_function shouldn't need to deal with + reading data. But we shouldn't be required to read the whole file in + order to write data either. + So a generic write_geotif is fine, but works better when you have + read all the data into memory. + """ + kwargs = { + "width": columns, + "height": rows, + "driver": driver, + "count": bands, + "crs": crs, + "dtype": dtype, + "transform": geotransform + } + + # driver creation options + if creation_opts is not None: + for k, v in creation_opts.items(): + kwargs[k] = v + + # create output dataset + outds = rasterio.open(out_fname, 'w', **kwargs) + + # global metadata + if metadata is not None: + outds.update_tags(**metadata) + + return outds + + +def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, + dtype='float32', metadata=None, crs=None, + geotransform=None, creation_opts=None): + """ + Initialises a py-GDAL dataset object for writing image data. + """ + gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 + + # create output dataset + driver = gdal.GetDriverByName(driver) + outds = driver.Create(out_fname, columns, rows, bands, dtype, + options=creation_opts) + + # geospatial info + outds.SetGeoTransform(geotransform) + outds.SetProjection(crs) + + # global metadata + if metadata is not None: + outds.SetMetadataItem(k, str(v)) # just following PyRate's example, but what if value is numeric??? + + return outds + + +def write_geotiff(data, out_fname, dtype='float32', metadata=None, crs=None, + geotransform=None, creation_opts={'compress': 'packbits'}): + """ + A generic routine for writing a NumPy array to a geotiff. + """ + # only support "2 <= dims <= 3" + if data.ndim == 3: + count, height, width = data.shape + elif data.ndim == 2: + height, width = data.shape + else: + msg = "Only support dimensions of '2 <= dims <= 3'." + raise TypeError(msg) + + kwargs = { + "width": width, + "height": height, + "driver": "GTiff", + "count": count, + "crs": crs, + "dtype": dtype, + "transform": geotransform + } + + # driver creation options + if creation_opts is not None: + for k, v in creation_opts.items(): + kwargs[k] = v + + # create output dataset + with rasterio.open(out_fname, 'w', **kwargs) as outds: + + # global metadata + if metadata is not None: + outds.update_tags(**metadata) + + # write data + if data.ndim = 3: + for band_id, blob in enumerate(data): + outds.write(blob, band_id+1) + else: + # single 2D band only (1D or > 3D should have failed earlier) + outds.write(data, 1) + + +class Conversion(object): + + """ + ***************************************************************** + NOTE! + This presents a concept for a future direction when there is a + greater need to support more formats. + ***************************************************************** + + The base conversion class. + Ideally this is subclassed for each format type that requires + conversion. + eg GAMMA, ROIPC, SNAP-HDF5, SNAP-BEAM-DIMAP etc + + And the read methods are unique to each format type. + That way the workflow code just calls {conversion_class}.write_geo_tiff() + + Still need to handle multibands though. + """ + + def __init__(self, filename, header, out_fname): + # we need to specify the native block read size + # eg 1 row by all columns for BSQ raw binary files (gamma raw output file) + # so the _read routine can define an iterator to read native blocks + # and write them out, before reading the next block (save memory) + + self._filename = filename + self._header = header + self._out_fname = out_fname + self._ifg = ifc.PYRATE_WAVELENGTH_METRES in header + self_incidence = 'FILE_TYPE' in header + self._ifg_proc = header[ifc.PYRATE_INSAR_PROCESSOR] + self._ncols = header[ifc.PYRATE_NCOLS] + self._nrows = header[ifc.PYRATE_NROWS] + + self._pass_header() + + def _pass_header(self): + """ + Extract relevant info from the header into dict containing + metadata for direct dumping into gdal file creation. + """ + hdr = {} + if self.ifg: + for k in [ifc.PYRATE_WAVELENGTH_METRES, ifc.PYRATE_TIME_SPAN, + ifc.PYRATE_INSAR_PROCESSOR, + ifc.MASTER_DATE, ifc.SLAVE_DATE, + ifc.DATA_UNITS, ifc.DATA_TYPE]: + # is it always str or could they be numeric? + hdr[k] = str(self._header[k]) + if self.ifg_proc == GAMMA: + for k in [ifc.MASTER_TIME, ifc.SLAVE_TIME, ifc.PYRATE_INCIDENCE_DEGREES]: + # is it always str or could they be numeric? + hdr[k] = str(self._header[k]) + elif self.incidence: + hdr[ifc.DATA_TYPE] = ifc.INCIDENCE + else: # must be dem + hdr[ifc.DATA_TYPE] = ifc.DEM + + self._geotransform = [ + header[ifc.PYRATE_LONG], + header[ifc.PYRATE_X_STEP], + 0, + header[ifc.PYRATE_LAT], + 0, + header[ifc.PYRATE_Y_STEP] + ] + + srs = osr.SpatialReference() + res = srs.SetWellKnownGeogCS(header[ifc.PYRATE_DATUM]) + self._crs = res.ExportToWkt() + + self._metadata = hdr + + def _read_block(self): + # this method needs to be overridded with the specific read routine + # i.e. raw GAMMA, raw ROIPC, tif GAMMA, SNAP HDF5 etc + # this should return a read iterator + # where each call will read a portion + # eg for data, window in {conversion_class}._read_block() + raise NotImplementedError + + def write_geotiff(self): + # output a geotiff according to PyRate's format spec + + # the method will operate as + # create output dataset (via rio_dataset or gdal_dataset) + # read a blob of data + # write that blob of data to the appropriate block in the tif + # read next blob of data + + # eg + # outds = rio_dataset(...) + # for data, window in {conversion_class}._read_block(): + # outds.write(data, 1, window=window) + raise NotImplementedError + + @property + def filename(self): + return self._filename + + @property + def out_fname(self): + return self._out_fname + + @property + def ncols(self): + return self._ncols + + @property + def nrows(self): + return self._nrows + + @property + def ifg(self): + return self._ifg + + @property + def incidence(self): + return self._incidence + + @property + def ifg_proc(self): + return self._ifg_proc + + @property + def metadata(self): + return self._metadata + + @property + def crs(self): + return self._crs + + @property + def geotransform(self): + return self._geotransform + + +class ConvertGamma(Conversion): + + """GAMMA file format converter.""" + + def __init__(self, filename, header, out_fname): + super(ConvertGamma, self).__init__(filename, header, out_fname) + + def write_geotiff(self): + """ + Overwrite the base classes write_geotiff method. + Also ignoring the need for a read iterator, + which could be scrapped altogether. + """ + # these two variables could be properties (unique to raw binary files) + bytes_per_col, fmtstr = _data_format(self.ifg_proc, self.ifg, + self.ncols) + # this could be a property (unique to raw binary files) + row_bytes = self.ncols * bytes_per_col + + # initialise the gdal dataset + outds = gdal_dataset(self.out_fname, self.ncols, self.nrows, + metadata=self.metadata, crs=self.crs, + geotransform=self.geotransform, + creation_opts=['compress=packbits']) + + band = outds.GetRasterBand(1) + + with open(self.filename, 'rb') as src: + for y in range(selfnrows): + data = struct.unpack(fmtstr, src.read(row_bytes)) + band.WriteArray(np.array(data).reshape(1, self.ncols), yoff=y) + + # can't remember if there is a Close() method + band = None + outds = None From b6549cb40c4201ffc208689540503f06b3ae3481 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 13 Mar 2019 16:04:15 +1100 Subject: [PATCH 006/376] split out gdal object creation to separate function in converttogeotiff workflow --- pyrate/conversion/base.py | 44 +++++++++++++++++----------------- pyrate/shared.py | 50 +++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/pyrate/conversion/base.py b/pyrate/conversion/base.py index 4045c397a..00809041e 100644 --- a/pyrate/conversion/base.py +++ b/pyrate/conversion/base.py @@ -78,28 +78,28 @@ def rio_dataset(out_fname, columns, rows, driver="GTiff", bands=1, return outds -def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, - dtype='float32', metadata=None, crs=None, - geotransform=None, creation_opts=None): - """ - Initialises a py-GDAL dataset object for writing image data. - """ - gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 - - # create output dataset - driver = gdal.GetDriverByName(driver) - outds = driver.Create(out_fname, columns, rows, bands, dtype, - options=creation_opts) - - # geospatial info - outds.SetGeoTransform(geotransform) - outds.SetProjection(crs) - - # global metadata - if metadata is not None: - outds.SetMetadataItem(k, str(v)) # just following PyRate's example, but what if value is numeric??? - - return outds +#def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, +# dtype='float32', metadata=None, crs=None, +# geotransform=None, creation_opts=None): +# """ +# Initialises a py-GDAL dataset object for writing image data. +# """ +# gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 +# +# # create output dataset +# driver = gdal.GetDriverByName(driver) +# outds = driver.Create(out_fname, columns, rows, bands, dtype, +# options=creation_opts) +# +# # geospatial info +# outds.SetGeoTransform(geotransform) +# outds.SetProjection(crs) +# +# # global metadata +# if metadata is not None: +# outds.SetMetadataItem(k, str(v)) # just following PyRate's example, but what if value is numeric??? +# +# return outds def write_geotiff(data, out_fname, dtype='float32', metadata=None, crs=None, diff --git a/pyrate/shared.py b/pyrate/shared.py index f6c66126b..65fe79f66 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -713,29 +713,25 @@ def write_geotiff(header, data_path, dest, nodata): _check_pixel_res_mismatch(header) - driver = gdal.GetDriverByName("GTiff") - dtype = gdal.GDT_Float32 if (is_ifg or is_incidence) else gdal.GDT_Int16 - ds = driver.Create(dest, ncols, nrows, 1, dtype, options=['compress=packbits']) - # write pyrate parameters to headers + md = dict() if is_ifg: for k in [ifc.PYRATE_WAVELENGTH_METRES, ifc.PYRATE_TIME_SPAN, ifc.PYRATE_INSAR_PROCESSOR, ifc.MASTER_DATE, ifc.SLAVE_DATE, ifc.DATA_UNITS, ifc.DATA_TYPE]: - ds.SetMetadataItem(k, str(header[k])) + md.update({k: str(header[k])}) if ifg_proc == GAMMA: for k in [ifc.MASTER_TIME, ifc.SLAVE_TIME, ifc.PYRATE_INCIDENCE_DEGREES]: - ds.SetMetadataItem(k, str(header[k])) + md.update({k: str(header[k])}) elif is_incidence: - ds.SetMetadataItem(ifc.DATA_TYPE, ifc.INCIDENCE) + md.update({ifc.DATA_TYPE:ifc.INCIDENCE}) else: # must be dem - ds.SetMetadataItem(ifc.DATA_TYPE, ifc.DEM) + md.update({ifc.DATA_TYPE:ifc.DEM}) # position and projection data - ds.SetGeoTransform([header[ifc.PYRATE_LONG], header[ifc.PYRATE_X_STEP], 0, - header[ifc.PYRATE_LAT], 0, header[ifc.PYRATE_Y_STEP]]) - + gt = [header[ifc.PYRATE_LONG], header[ifc.PYRATE_X_STEP], 0, + header[ifc.PYRATE_LAT], 0, header[ifc.PYRATE_Y_STEP]] srs = osr.SpatialReference() res = srs.SetWellKnownGeogCS(header[ifc.PYRATE_DATUM]) @@ -743,7 +739,12 @@ def write_geotiff(header, data_path, dest, nodata): msg = 'Unrecognised projection: %s' % header[ifc.PYRATE_DATUM] raise GeotiffException(msg) - ds.SetProjection(srs.ExportToWkt()) + crs = srs.ExportToWkt() + dtype = 'float32' if (is_ifg or is_incidence) else 'int16' + + ds = gdal_dataset(dest, ncols, nrows, driver="GTiff", bands=1, + dtype=dtype, metadata=md, crs=crs, + geotransform=gt, creation_opts=['compress=packbits']) # copy data from the binary file band = ds.GetRasterBand(1) @@ -768,6 +769,31 @@ def write_geotiff(header, data_path, dest, nodata): del ds +def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, + dtype='float32', metadata=None, crs=None, + geotransform=None, creation_opts=None): + """ + Initialises a py-GDAL dataset object for writing image data. + """ + gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 + + # create output dataset + driver = gdal.GetDriverByName(driver) + outds = driver.Create(out_fname, columns, rows, bands, gdal_dtype, + options=creation_opts) + + # geospatial info + outds.SetGeoTransform(geotransform) + outds.SetProjection(crs) + + # add metadata + if metadata is not None: + for k, v in metadata.items(): + outds.SetMetadataItem(k, str(v)) + + return outds + + def _data_format(ifg_proc, is_ifg, ncols): """ Convenience function to determine the bytesize and format of input files From 414b297d12d466218a8f07e21188bf4733b9bd38 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 13 Mar 2019 16:47:39 +1100 Subject: [PATCH 007/376] functionise the pyrate metadata collation --- pyrate/conversion/base.py | 32 +++++++++--------- pyrate/shared.py | 70 ++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/pyrate/conversion/base.py b/pyrate/conversion/base.py index 00809041e..70cb9f9ea 100644 --- a/pyrate/conversion/base.py +++ b/pyrate/conversion/base.py @@ -24,22 +24,22 @@ from pyrate import ifgconstants as ifc -def metadata(header): - """ - Standardise the metadata to enable simpler output for the - write_geotif function. - - :param header: - :type dict: - Dictionary containing interferogram metadata. - - :return: - A dict containing specific flags for either ifg, dem or - incidence. - """ - # similiar to what is written in the Conversion base class' _pass_header - - return +#def metadata(header): +# """ +# Standardise the metadata to enable simpler output for the +# write_geotif function. +# +# :param header: +# :type dict: +# Dictionary containing interferogram metadata. +# +# :return: +# A dict containing specific flags for either ifg, dem or +# incidence. +# """ +# # similiar to what is written in the Conversion base class' _pass_header +# +# return def rio_dataset(out_fname, columns, rows, driver="GTiff", bands=1, diff --git a/pyrate/shared.py b/pyrate/shared.py index 65fe79f66..74c307f29 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -684,6 +684,20 @@ def nanmedian(x): return np.median(x[~np.isnan(x)]) +def _is_interferogram(hdr): + """ + Convenience function to determine if file is interferogram + """ + return ifc.PYRATE_WAVELENGTH_METRES in hdr + + +def _is_incidence(hdr): + """ + Convenience function to determine if incidence file + """ + return 'FILE_TYPE' in hdr + + def write_geotiff(header, data_path, dest, nodata): # pylint: disable=too-many-statements """ @@ -699,13 +713,11 @@ def write_geotiff(header, data_path, dest, nodata): """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals - is_ifg = ifc.PYRATE_WAVELENGTH_METRES in header - is_incidence = 'FILE_TYPE' in header ifg_proc = header[ifc.PYRATE_INSAR_PROCESSOR] ncols = header[ifc.PYRATE_NCOLS] nrows = header[ifc.PYRATE_NROWS] - bytes_per_col, fmtstr = _data_format(ifg_proc, is_ifg, ncols) - if is_ifg and ifg_proc == ROIPAC: + bytes_per_col, fmtstr = _data_format(ifg_proc, _is_interferogram(header), ncols) + if _is_interferogram(header) and ifg_proc == ROIPAC: # roipac ifg has 2 bands _check_raw_data(bytes_per_col*2, data_path, ncols, nrows) else: @@ -713,34 +725,20 @@ def write_geotiff(header, data_path, dest, nodata): _check_pixel_res_mismatch(header) - # write pyrate parameters to headers - md = dict() - if is_ifg: - for k in [ifc.PYRATE_WAVELENGTH_METRES, ifc.PYRATE_TIME_SPAN, - ifc.PYRATE_INSAR_PROCESSOR, - ifc.MASTER_DATE, ifc.SLAVE_DATE, - ifc.DATA_UNITS, ifc.DATA_TYPE]: - md.update({k: str(header[k])}) - if ifg_proc == GAMMA: - for k in [ifc.MASTER_TIME, ifc.SLAVE_TIME, ifc.PYRATE_INCIDENCE_DEGREES]: - md.update({k: str(header[k])}) - elif is_incidence: - md.update({ifc.DATA_TYPE:ifc.INCIDENCE}) - else: # must be dem - md.update({ifc.DATA_TYPE:ifc.DEM}) - # position and projection data gt = [header[ifc.PYRATE_LONG], header[ifc.PYRATE_X_STEP], 0, header[ifc.PYRATE_LAT], 0, header[ifc.PYRATE_Y_STEP]] srs = osr.SpatialReference() res = srs.SetWellKnownGeogCS(header[ifc.PYRATE_DATUM]) - if res: msg = 'Unrecognised projection: %s' % header[ifc.PYRATE_DATUM] raise GeotiffException(msg) crs = srs.ExportToWkt() - dtype = 'float32' if (is_ifg or is_incidence) else 'int16' + dtype = 'float32' if (_is_interferogram(header) or _is_incidence(header)) else 'int16' + + # get subset of relevant metadata + md = collate_metadata(header) ds = gdal_dataset(dest, ncols, nrows, driver="GTiff", bands=1, dtype=dtype, metadata=md, crs=crs, @@ -755,7 +753,7 @@ def write_geotiff(header, data_path, dest, nodata): with open(data_path, 'rb') as f: for y in range(nrows): if ifg_proc == ROIPAC: - if is_ifg: + if _is_interferogram(header): f.seek(row_bytes, 1) # skip interleaved band 1 data = struct.unpack(fmtstr, f.read(row_bytes)) @@ -794,6 +792,32 @@ def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, return outds +def collate_metadata(header): + """ + Grab metadata relevant to PyRate from input metadata + + :param dict header: Input file metadata dictionary + + :return: dict of relevant metadata for PyRate + """ + md = dict() + if _is_interferogram(header): + for k in [ifc.PYRATE_WAVELENGTH_METRES, ifc.PYRATE_TIME_SPAN, + ifc.PYRATE_INSAR_PROCESSOR, + ifc.MASTER_DATE, ifc.SLAVE_DATE, + ifc.DATA_UNITS, ifc.DATA_TYPE]: + md.update({k: str(header[k])}) + if header[ifc.PYRATE_INSAR_PROCESSOR] == GAMMA: + for k in [ifc.MASTER_TIME, ifc.SLAVE_TIME, ifc.PYRATE_INCIDENCE_DEGREES]: + md.update({k: str(header[k])}) + elif _is_incidence(header): + md.update({ifc.DATA_TYPE:ifc.INCIDENCE}) + else: # must be dem + md.update({ifc.DATA_TYPE:ifc.DEM}) + + return md + + def _data_format(ifg_proc, is_ifg, ncols): """ Convenience function to determine the bytesize and format of input files From 6240f732c0e2a81c3ee4c60fb8dc4b0a1fa4ade2 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 13 Mar 2019 16:54:58 +1100 Subject: [PATCH 008/376] rename existing function so name can be reused for new generic functionality --- pyrate/scripts/converttogtif.py | 2 +- pyrate/shared.py | 2 +- tests/test_gamma.py | 12 ++++++------ tests/test_roipac.py | 12 ++++++------ tests/test_shared.py | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index ac72a0e14..c4684904d 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -139,7 +139,7 @@ def _geotiff_multiprocessing(unw_path, params): else: raise PreprocessError('Processor must be ROI_PAC (0) or ' 'GAMMA (1)') - shared.write_geotiff(header, unw_path, dest, + shared.write_fullres_geotiff(header, unw_path, dest, nodata=params[cf.NO_DATA_VALUE]) else: log.info("Full-res geotiff already exists") diff --git a/pyrate/shared.py b/pyrate/shared.py index 74c307f29..92c3392ee 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -698,7 +698,7 @@ def _is_incidence(hdr): return 'FILE_TYPE' in hdr -def write_geotiff(header, data_path, dest, nodata): +def write_fullres_geotiff(header, data_path, dest, nodata): # pylint: disable=too-many-statements """ Creates a copy of input image data (interferograms, DEM, incidence maps diff --git a/tests/test_gamma.py b/tests/test_gamma.py index 22d5fae8c..554a97c83 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -53,7 +53,7 @@ APS_ELEVATION_MAP) from pyrate.scripts import run_prepifg from pyrate.scripts.converttogtif import main as gammaMain -from pyrate.shared import write_geotiff, GeotiffException +from pyrate.shared import write_fullres_geotiff, GeotiffException from tests import common from tests.common import GAMMA_TEST_DIR, SML_TEST_GAMMA from tests.common import TEST_CONF_GAMMA, TEMPDIR @@ -140,7 +140,7 @@ def test_to_geotiff_dem(self): data_path = join(GAMMA_TEST_DIR, 'dem16x20raw.dem') self.dest = os.path.join(TEMPDIR, "tmp_gamma_dem.tif") - write_geotiff(hdr, data_path, self.dest, nodata=0) + write_fullres_geotiff(hdr, data_path, self.dest, nodata=0) exp_path = join(GAMMA_TEST_DIR, 'dem16x20_subset_from_gamma.tif') exp_ds = gdal.Open(exp_path) ds = gdal.Open(self.dest) @@ -157,7 +157,7 @@ def test_to_geotiff_ifg(self): self.dest = os.path.join(TEMPDIR, 'tmp_gamma_ifg.tif') data_path = join(GAMMA_TEST_DIR, '16x20_20090713-20090817_VV_4rlks_utm.unw') - write_geotiff(self.COMBINED, data_path, self.dest, nodata=0) + write_fullres_geotiff(self.COMBINED, data_path, self.dest, nodata=0) ds = gdal.Open(self.dest) exp_path = join(GAMMA_TEST_DIR, @@ -184,7 +184,7 @@ def test_to_geotiff_wrong_input_data(self): self.dest = os.path.join(TEMPDIR, 'tmp_gamma_ifg.tif') data_path = join(GAMMA_TEST_DIR, '16x20_20090713-20090817_VV_4rlks_utm.tif') - self.assertRaises(GeotiffException, write_geotiff, + self.assertRaises(GeotiffException, write_fullres_geotiff, self.COMBINED, data_path, self.dest, nodata=0) def test_mismatching_cell_resolution(self): @@ -194,7 +194,7 @@ def test_mismatching_cell_resolution(self): '16x20_20090713-20090817_VV_4rlks_utm.unw') self.dest = os.path.join(TEMPDIR, 'fake') - self.assertRaises(GeotiffException, write_geotiff, hdrs, + self.assertRaises(GeotiffException, write_fullres_geotiff, hdrs, data_path, self.dest, 0) def compare_rasters(self, ds, exp_ds): @@ -216,7 +216,7 @@ def test_bad_projection(self): hdr[ifc.PYRATE_DATUM] = 'nonexistent projection' data_path = join(GAMMA_TEST_DIR, 'dem16x20raw.dem') self.dest = os.path.join(TEMPDIR, 'tmp_gamma_dem2.tif') - self.assertRaises(GeotiffException, write_geotiff, hdr, + self.assertRaises(GeotiffException, write_fullres_geotiff, hdr, data_path, self.dest, nodata=0) diff --git a/tests/test_roipac.py b/tests/test_roipac.py index 61d4f0b9b..23c3ad191 100644 --- a/tests/test_roipac.py +++ b/tests/test_roipac.py @@ -53,7 +53,7 @@ from pyrate.scripts import run_prepifg from pyrate.scripts.converttogtif import main as roipacMain from pyrate.shared import GeotiffException -from pyrate.shared import write_geotiff +from pyrate.shared import write_fullres_geotiff from tests.common import HEADERS_TEST_DIR, PREP_TEST_OBS, PREP_TEST_TIF from tests.common import SML_TEST_DEM_DIR, SML_TEST_OBS, TEMPDIR from tests.common import SML_TEST_DEM_ROIPAC, SML_TEST_DEM_HDR @@ -143,7 +143,7 @@ def test_to_geotiff_dem(self): hdr = roipac.parse_header(SML_TEST_DEM_HDR) self.dest = os.path.join(TEMPDIR, "tmp_roipac_dem.tif") - write_geotiff(hdr, SML_TEST_DEM_ROIPAC, self.dest, nodata=0) + write_fullres_geotiff(hdr, SML_TEST_DEM_ROIPAC, self.dest, nodata=0) exp_path = join(SML_TEST_DEM_DIR, 'roipac_test_trimmed.tif') exp_ds = gdal.Open(exp_path) ds = gdal.Open(self.dest) @@ -161,7 +161,7 @@ def test_to_geotiff_ifg(self): self.dest = os.path.join('tmp_roipac_ifg.tif') data_path = join(PREP_TEST_OBS, 'geo_060619-061002.unw') - write_geotiff(hdrs, data_path, self.dest, nodata=0) + write_fullres_geotiff(hdrs, data_path, self.dest, nodata=0) ds = gdal.Open(self.dest) band = ds.GetRasterBand(1) @@ -192,7 +192,7 @@ def test_to_geotiff_wrong_input_data(self): data_path = join(PREP_TEST_TIF, 'geo_060619-061002.tif') self.assertRaises( GeotiffException, - write_geotiff, + write_fullres_geotiff, self.HDRS, data_path, self.dest, @@ -204,7 +204,7 @@ def test_bad_projection(self): hdrs[ifc.DATA_TYPE] = ifc.ORIG self.dest = os.path.join(TEMPDIR, 'tmp_roipac_ifg2.tif') data_path = join(PREP_TEST_OBS, 'geo_060619-061002.unw') - self.assertRaises(GeotiffException, write_geotiff, hdrs, + self.assertRaises(GeotiffException, write_fullres_geotiff, hdrs, data_path, self.dest, 0) def test_mismatching_cell_resolution(self): @@ -214,7 +214,7 @@ def test_mismatching_cell_resolution(self): data_path = join(PREP_TEST_OBS, 'geo_060619-061002.unw') self.dest = os.path.join(TEMPDIR, 'fake') - self.assertRaises(GeotiffException, write_geotiff, hdrs, + self.assertRaises(GeotiffException, write_fullres_geotiff, hdrs, data_path, self.dest, 0) def compare_rasters(self, ds, exp_ds): diff --git a/tests/test_shared.py b/tests/test_shared.py index b6d473272..1e81d2613 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -392,8 +392,8 @@ def test_unw_contains_same_data_as_numpy_array(self): os.path.join(common.SML_TEST_GAMMA, '20060828_slc.par')) header.update(dem_header) - # insert some dummy data so we are the dem in write_geotiff is not - # not activated and ifg write_geotiff operation works + # insert some dummy data so we are the dem in write_fullres_geotiff is not + # not activated and ifg write_fullres_geotiff operation works header[ifc.PYRATE_TIME_SPAN] = 0 header[ifc.SLAVE_DATE] = 0 header[ifc.DATA_UNITS] = 'degrees' @@ -409,7 +409,7 @@ def test_unw_contains_same_data_as_numpy_array(self): dest_unw=temp_unw, ifg_proc=1) # convert the .unw to geotif - shared.write_geotiff(header=header, data_path=temp_unw, + shared.write_fullres_geotiff(header=header, data_path=temp_unw, dest=temp_tif, nodata=np.nan) # now compare geotiff with original numpy array From 0df4c7fafbaa0e03caaae3219cc10a235768b4e6 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Thu, 14 Mar 2019 13:19:31 +1100 Subject: [PATCH 009/376] add generic routine for writing a NumPy array to a geotiff --- pyrate/shared.py | 57 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/pyrate/shared.py b/pyrate/shared.py index 92c3392ee..dfe4dfd7e 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -734,14 +734,15 @@ def write_fullres_geotiff(header, data_path, dest, nodata): msg = 'Unrecognised projection: %s' % header[ifc.PYRATE_DATUM] raise GeotiffException(msg) - crs = srs.ExportToWkt() + wkt = srs.ExportToWkt() dtype = 'float32' if (_is_interferogram(header) or _is_incidence(header)) else 'int16' # get subset of relevant metadata md = collate_metadata(header) + # create GDAL object ds = gdal_dataset(dest, ncols, nrows, driver="GTiff", bands=1, - dtype=dtype, metadata=md, crs=crs, + dtype=dtype, metadata=md, crs=wkt, geotransform=gt, creation_opts=['compress=packbits']) # copy data from the binary file @@ -757,12 +758,9 @@ def write_fullres_geotiff(header, data_path, dest, nodata): f.seek(row_bytes, 1) # skip interleaved band 1 data = struct.unpack(fmtstr, f.read(row_bytes)) - #else: # GAMMA - # data = struct.unpack(fmtstr, f.read(ncols * 4)) band.WriteArray(np.array(data).reshape(1, ncols), yoff=y) - # Needed? Only in ROIPAC code ds = None # manual close del ds @@ -894,6 +892,7 @@ def write_unw_from_data_or_geotiff(geotif_or_data, dest_unw, ifg_proc): f.write(col_data) +# This function may be able to be deprecated def write_output_geotiff(md, gt, wkt, data, dest, nodata): # pylint: disable=too-many-arguments """ @@ -929,6 +928,54 @@ def write_output_geotiff(md, gt, wkt, data, dest, nodata): band.WriteArray(data, 0, 0) +def write_geotiff(data, outds, nodata): + # pylint: disable=too-many-arguments + """ + A generic routine for writing a NumPy array to a geotiff. + +# :param dict md: Dictionary containing PyRate metadata +# :param list gt: GDAL geotransform for the data +# :param list wkt: GDAL projection information for the data + :param ndarray data: Output data array to save + :param obj outds: GDAL destination object + :param float nodata: No data value of data + + :return None, file saved to disk + """ + # only support "2 <= dims <= 3" + if data.ndim == 3: + count, height, width = data.shape + elif data.ndim == 2: + height, width = data.shape + else: + msg = "Only support dimensions of '2 <= dims <= 3'." + raise GeotiffException(msg) + +# driver = gdal.GetDriverByName("GTiff") +# nrows, ncols = data.shape +# ds = driver.Create(dest, ncols, nrows, 1, gdal.GDT_Float32, options=['compress=packbits']) +# # set spatial reference for geotiff +# ds.SetGeoTransform(gt) +# ds.SetProjection(wkt) +# ds.SetMetadataItem(ifc.EPOCH_DATE, str(md[ifc.EPOCH_DATE])) +# +# # set other metadata +# ds.SetMetadataItem('DATA_TYPE', str(md['DATA_TYPE'])) +# # sequence position for time series products +# if 'SEQUENCE_POSITION' in md: +# ds.SetMetadataItem('SEQUENCE_POSITION', str(md['SEQUENCE_POSITION'])) +# + # write data to geotiff + band = outds.GetRasterBand(1) + band.SetNoDataValue(nodata) + band.WriteArray(data, 0, 0) + + outds = None + band = None + del outds + del band + + class GeotiffException(Exception): """ Geotiff exception class From c058eae2827c72587050b38065d67a187dc6ca7e Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Thu, 14 Mar 2019 13:28:19 +1100 Subject: [PATCH 010/376] remove confusing use of 'gdalwarp' as package alias --- pyrate/prepifg.py | 4 ++-- tests/test_gdal_python.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyrate/prepifg.py b/pyrate/prepifg.py index 26f6b4e85..49a9bfa0d 100644 --- a/pyrate/prepifg.py +++ b/pyrate/prepifg.py @@ -33,7 +33,7 @@ from osgeo import gdal from pyrate import config as cf -from pyrate import gdal_python as gdalwarp +from pyrate.gdal_python import crop_resample_average from pyrate import ifgconstants as ifc from pyrate.shared import Ifg, DEM @@ -289,7 +289,7 @@ def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, # #if params.has_key(REPROJECTION_FLAG): # # reproject() driver_type = 'GTiff' if write_to_disk else 'MEM' - resampled_data, out_ds = gdalwarp.crop_resample_average( + resampled_data, out_ds = crop_resample_average( input_tif=ifg.data_path, extents=extents, new_res=resolution, diff --git a/tests/test_gdal_python.py b/tests/test_gdal_python.py index 6afd3d5c8..012095b59 100644 --- a/tests/test_gdal_python.py +++ b/tests/test_gdal_python.py @@ -31,7 +31,7 @@ import numpy as np from osgeo import gdal, gdalconst -from pyrate import gdal_python as gdalwarp +from pyrate import gdal_python from pyrate.shared import Ifg from pyrate import config as cf from tests import common @@ -52,7 +52,7 @@ def test_small_data_cropping(self): t_cmd = cmd + [s.data_path, temp_tif] subprocess.check_call(t_cmd) clipped_ref = gdal.Open(temp_tif).ReadAsArray() - clipped = gdalwarp.crop(s.data_path, extents)[0] + clipped = gdal_python.crop(s.data_path, extents)[0] np.testing.assert_array_almost_equal(clipped_ref, clipped) os.remove(temp_tif) @@ -89,7 +89,7 @@ def check_same_resampled_output(self, extents, extents_str, res, resampled_temp_tif = tempfile.mktemp(suffix='.tif', prefix='resampled_') - resampled = gdalwarp.resample_nearest_neighbour(s.data_path, + resampled = gdal_python.resample_nearest_neighbour(s.data_path, extents, res, resampled_temp_tif) np.testing.assert_array_almost_equal(resampled_ref, @@ -114,7 +114,7 @@ def test_output_file_written(self): for s in small_test_ifgs: resampled_temp_tif = tempfile.mktemp(suffix='.tif', prefix='resampled_') - gdalwarp.resample_nearest_neighbour(s.data_path, extents, + gdal_python.resample_nearest_neighbour(s.data_path, extents, [res, -res], resampled_temp_tif) self.assertTrue(os.path.exists(resampled_temp_tif)) @@ -125,10 +125,10 @@ def test_small_data_crop_vs_resample(self): # minX, minY, maxX, maxY = extents extents = [150.91, -34.229999976, 150.949166651, -34.17] for s in small_test_ifgs: - clipped = gdalwarp.crop(s.data_path, extents)[0] + clipped = gdal_python.crop(s.data_path, extents)[0] resampled_temp_tif = tempfile.mktemp(suffix='.tif', prefix='resampled_') - resampled = gdalwarp.resample_nearest_neighbour( + resampled = gdal_python.resample_nearest_neighbour( s.data_path, extents, [None, None], resampled_temp_tif) self.assertTrue(os.path.exists(resampled_temp_tif)) np.testing.assert_array_almost_equal(resampled[0, :, :], clipped) @@ -143,7 +143,7 @@ def test_resampled_tif_has_metadata(self): resampled_temp_tif = tempfile.mktemp(suffix='.tif', prefix='resampled_') - gdalwarp.resample_nearest_neighbour( + gdal_python.resample_nearest_neighbour( s.data_path, extents, [None, None], resampled_temp_tif) dst_ds = gdal.Open(resampled_temp_tif) md = dst_ds.GetMetadata() @@ -420,7 +420,7 @@ def test_gdal_python_vs_old_prepifg_prep2(self): self.manipulation(data, self.temp_tif, self.md) # only band 1 is resapled in warp_old - averaged_and_resampled, _ = gdalwarp.crop_resample_average( + averaged_and_resampled, _ = gdal_python.crop_resample_average( self.temp_tif, extents, [res, -res], self.out_tif, thresh, match_pirate=True) ifg = Ifg(self.temp_tif) @@ -464,7 +464,7 @@ def test_gdal_python_vs_old_prepifg_no_match_pirate(self): for looks in range(10): x_looks = y_looks = looks res = orig_res*x_looks - averaged_and_resapled, out_ds = gdalwarp.crop_resample_average( + averaged_and_resapled, out_ds = gdal_python.crop_resample_average( ifg.data_path, extents, new_res=[res, -res], output_file=self.temp_tif, thresh=thresh, match_pirate=False) @@ -501,7 +501,7 @@ def test_gdal_python_vs_old_prepifg(self): for looks in range(1, 10): x_looks = y_looks = looks res = orig_res*x_looks - averaged_and_resampled = gdalwarp.crop_resample_average( + averaged_and_resampled = gdal_python.crop_resample_average( ifg.data_path, extents, new_res=[res, -res], output_file=self.temp_tif, thresh=thresh, match_pirate=True)[0] @@ -532,7 +532,7 @@ def test_no_out_file_when_driver_type_is_mem(self): thresh = 0.5 x_looks = y_looks = 6 res = orig_res*x_looks - averaged_and_resampled, out_ds = gdalwarp.crop_resample_average( + averaged_and_resampled, out_ds = gdal_python.crop_resample_average( ifg.data_path, extents, new_res=[res, -res], output_file=self.temp_tif, thresh=thresh, out_driver_type='MEM', match_pirate=True) From 78c4f1f7b1c5376c98346d8c23fd825bf045dc99 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Thu, 14 Mar 2019 14:09:49 +1100 Subject: [PATCH 011/376] reuse existing write_geotiff function in multilooking process (prepifg) --- pyrate/gdal_python.py | 36 +++++++++++++++++++----------------- pyrate/shared.py | 8 +++++++- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pyrate/gdal_python.py b/pyrate/gdal_python.py index 767e9e75d..7c2ea1983 100644 --- a/pyrate/gdal_python.py +++ b/pyrate/gdal_python.py @@ -21,6 +21,7 @@ from PIL import Image, ImageDraw import numpy as np from pyrate import ifgconstants as ifc +from pyrate import shared gdal.SetCacheMax(2**15) GDAL_WARP_MEMORY_LIMIT = 2**10 @@ -292,38 +293,39 @@ def crop_resample_average( src_dtype = src_ds_mem.GetRasterBand(1).DataType src_gt = src_ds_mem.GetGeoTransform() - # write out to output geotif file - driver = gdal.GetDriverByName(out_driver_type) - # required to match Matlab Pirate output if tmp_ds: _matlab_alignment(input_tif, new_res, resampled_average, src_ds_mem, src_gt, tmp_ds) - # write final pyrate GTiff - out_ds = driver.Create(output_file, dst_ds.RasterXSize, dst_ds.RasterYSize, - 1, src_dtype, options=['compress=packbits']) + # grab metadata from existing geotiff + gt = dst_ds.GetGeoTransform() + wkt = dst_ds.GetProjection() + + # copy and update metadata + md = dst_ds.GetMetadata() + + # TEST HERE IF EXISTING FILE HAS PYRATE METADATA. IF NOT ADD HERE - out_ds.GetRasterBand(1).SetNoDataValue(np.nan) - out_ds.GetRasterBand(1).WriteArray(resampled_average) - out_ds.SetGeoTransform(dst_ds.GetGeoTransform()) - out_ds.SetProjection(dst_ds.GetProjection()) - # copy metadata - for k, v in dst_ds.GetMetadata().items(): + for k, v in md.items(): if k == ifc.DATA_TYPE: # update data type metadata if v == ifc.ORIG: - out_ds.SetMetadataItem(ifc.DATA_TYPE, ifc.MULTILOOKED) + md.update({ifc.DATA_TYPE:ifc.MULTILOOKED}) elif v == ifc.DEM: - out_ds.SetMetadataItem(ifc.DATA_TYPE, ifc.MLOOKED_DEM) + md.update({ifc.DATA_TYPE:ifc.MLOOKED_DEM}) elif v == ifc.INCIDENCE: - out_ds.SetMetadataItem(ifc.DATA_TYPE, ifc.MLOOKED_INC) + md.update({ifc.DATA_TYPE:ifc.MLOOKED_INC}) elif v == ifc.MULTILOOKED: pass else: raise TypeError('Data Type metadata not recognised') - else: - out_ds.SetMetadataItem(k, v) + + out_ds = shared.gdal_dataset(output_file, dst_ds.RasterXSize, dst_ds.RasterYSize, + driver="GTiff", bands=1, dtype=src_dtype, metadata=md, crs=wkt, + geotransform=gt, creation_opts=['compress=packbits']) + + shared.write_geotiff(resampled_average, out_ds, np.nan) return resampled_average, out_ds diff --git a/pyrate/shared.py b/pyrate/shared.py index dfe4dfd7e..cd0ee63b0 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -771,7 +771,13 @@ def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, """ Initialises a py-GDAL dataset object for writing image data. """ - gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 + if dtype == 'float32': + gdal_dtype = gdal.GDT_Float32 + elif dtype == 'int16': + gdal_dtype = gdal.GDT_Int16 + else: + # assume gdal.GDT val is passed to function + gdal_dtype = dtype # create output dataset driver = gdal.GetDriverByName(driver) From 78d8454642d7c41c3d6c681b303995f41487abde Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Thu, 14 Mar 2019 16:01:32 +1100 Subject: [PATCH 012/376] remove commented sections --- pyrate/shared.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pyrate/shared.py b/pyrate/shared.py index cd0ee63b0..d12b8ed44 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -939,9 +939,6 @@ def write_geotiff(data, outds, nodata): """ A generic routine for writing a NumPy array to a geotiff. -# :param dict md: Dictionary containing PyRate metadata -# :param list gt: GDAL geotransform for the data -# :param list wkt: GDAL projection information for the data :param ndarray data: Output data array to save :param obj outds: GDAL destination object :param float nodata: No data value of data @@ -957,20 +954,6 @@ def write_geotiff(data, outds, nodata): msg = "Only support dimensions of '2 <= dims <= 3'." raise GeotiffException(msg) -# driver = gdal.GetDriverByName("GTiff") -# nrows, ncols = data.shape -# ds = driver.Create(dest, ncols, nrows, 1, gdal.GDT_Float32, options=['compress=packbits']) -# # set spatial reference for geotiff -# ds.SetGeoTransform(gt) -# ds.SetProjection(wkt) -# ds.SetMetadataItem(ifc.EPOCH_DATE, str(md[ifc.EPOCH_DATE])) -# -# # set other metadata -# ds.SetMetadataItem('DATA_TYPE', str(md['DATA_TYPE'])) -# # sequence position for time series products -# if 'SEQUENCE_POSITION' in md: -# ds.SetMetadataItem('SEQUENCE_POSITION', str(md['SEQUENCE_POSITION'])) -# # write data to geotiff band = outds.GetRasterBand(1) band.SetNoDataValue(nodata) From 7ff4b3518652fae80d0763fa3b594771df140507 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Fri, 15 Mar 2019 15:28:56 +1100 Subject: [PATCH 013/376] remove metadata collation for fullres geotiff conversion; add check for extension of input filenames - is it already a geotiff? move gamma/roipac header collation to specific modules --- pyrate/gamma.py | 23 ++++++++++++++++-- pyrate/roipac.py | 21 +++++++++++++++-- pyrate/scripts/converttogtif.py | 42 ++++----------------------------- pyrate/shared.py | 11 ++++++--- 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/pyrate/gamma.py b/pyrate/gamma.py index 39c40ce34..a3e9cb91b 100644 --- a/pyrate/gamma.py +++ b/pyrate/gamma.py @@ -20,10 +20,12 @@ from os.path import join, split import re +import os import glob2 from datetime import date, time, timedelta import numpy as np import pyrate.ifgconstants as ifc +from pyrate import config as cf PTN = re.compile(r'\d{8}') # match 8 digits for the dates @@ -225,7 +227,7 @@ def get_header_paths(input_file, slc_dir=None): """ Function that matches input GAMMA file names with GAMMA header file names - :param str input_file: input GAMMA .unw file. + :param str input_file: input GAMMA image file. :param str slc_dir: GAMMA SLC header file directory :return: list of matching header files :rtype: list @@ -233,13 +235,30 @@ def get_header_paths(input_file, slc_dir=None): if slc_dir: dir_name = slc_dir _, file_name = split(input_file) - else: # header file must exist in the same dir as that of .unw + else: # header file must exist in the same dir as that of image file dir_name, file_name = split(input_file) matches = PTN.findall(file_name) return [glob2.glob(join(dir_name, '**/*%s*slc.par' % m))[0] for m in matches] +def gamma_header(file_path, params): + """ + Function to obtain combined Gamma headers for image file + """ + dem_hdr_path = params[cf.DEM_HEADER_FILE] + slc_dir = params[cf.SLC_DIR] + header_paths = get_header_paths(file_path, slc_dir=slc_dir) + combined_headers = manage_headers(dem_hdr_path, header_paths) + + if os.path.basename(file_path).split('.')[1] == \ + (params[cf.APS_INCIDENCE_EXT] or params[cf.APS_ELEVATION_EXT]): + # TODO: implement incidence class here + combined_headers['FILE_TYPE'] = 'Incidence' + + return combined_headers + + class GammaException(Exception): """ Gamma generic exception class diff --git a/pyrate/roipac.py b/pyrate/roipac.py index b7866ea68..deba5f806 100644 --- a/pyrate/roipac.py +++ b/pyrate/roipac.py @@ -16,10 +16,11 @@ """ This Python module contains tools for reading ROI_PAC format input data. """ -#import os +import os import re import datetime import pyrate.ifgconstants as ifc +from pyrate import config as cf # ROIPAC RSC header file constants WIDTH = "WIDTH" @@ -70,7 +71,6 @@ ROIPAC_HEADER_LEFT_JUSTIFY = 18 ROI_PAC_HEADER_FILE_EXT = "rsc" - def parse_date(dstr): """ Parses ROI_PAC 'yymmdd' or 'yymmdd-yymmdd' format string to datetime. @@ -199,6 +199,23 @@ def manage_header(header_file, projection): return header +def roipac_header(file_path, params): + """ + Function to obtain a header for roipac interferogram file + """ + rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) + if rsc_file is not None: + projection = parse_header(rsc_file)[ifc.PYRATE_DATUM] + else: + raise RoipacException('No DEM resource/header file is ' + 'provided') + + header_file = "%s.%s" % (file_path, ROI_PAC_HEADER_FILE_EXT) + header = manage_header(header_file, projection) + + return header + + class RoipacException(Exception): """ Convenience class for throwing exception diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index c4684904d..4eb02afa2 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -38,7 +38,6 @@ log = logging.getLogger(__name__) -ROI_PAC_HEADER_FILE_EXT = 'rsc' GAMMA = 1 ROIPAC = 0 @@ -127,15 +126,16 @@ def _geotiff_multiprocessing(unw_path, params): """ Multiprocessing wrapper for full-res geotiff conversion """ - dest = shared.output_tiff_filename(unw_path, params[cf.OUT_DIR]) + dest = shared.output_tiff_filename(unw_path, params[cf.OBS_DIR]) + print(dest) processor = params[cf.PROCESSOR] # roipac or gamma # Create full-res geotiff if not already on disk if not os.path.exists(dest): if processor == GAMMA: - header = _gamma_header(unw_path, params) + header = gamma.gamma_header(unw_path, params) elif processor == ROIPAC: - header = _roipac_header(unw_path, params) + header = roipac.roipac_header(unw_path, params) else: raise PreprocessError('Processor must be ROI_PAC (0) or ' 'GAMMA (1)') @@ -147,37 +147,3 @@ def _geotiff_multiprocessing(unw_path, params): return dest -def _gamma_header(unw_path, params): - """ - Function to obtain combined Gamma headers - """ - dem_hdr_path = params[cf.DEM_HEADER_FILE] - slc_dir = params[cf.SLC_DIR] - header_paths = gamma.get_header_paths(unw_path, slc_dir=slc_dir) - combined_headers = gamma.manage_headers(dem_hdr_path, header_paths) - - if os.path.basename(unw_path).split('.')[1] == \ - (params[cf.APS_INCIDENCE_EXT] or params[cf.APS_ELEVATION_EXT]): - # TODO: implement incidence class here - combined_headers['FILE_TYPE'] = 'Incidence' - - return combined_headers - - -def _roipac_header(unw_path, params): - """ - Function to obtain a Roipac headers - """ - rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) - if rsc_file is not None: - projection = roipac.parse_header(rsc_file)[ifc.PYRATE_DATUM] - else: - raise roipac.RoipacException('No DEM resource/header file is ' - 'provided') - - header_file = "%s.%s" % (unw_path, ROI_PAC_HEADER_FILE_EXT) - header = roipac.manage_header(header_file, projection) - - return header - - diff --git a/pyrate/shared.py b/pyrate/shared.py index d12b8ed44..48fa15fba 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -738,11 +738,11 @@ def write_fullres_geotiff(header, data_path, dest, nodata): dtype = 'float32' if (_is_interferogram(header) or _is_incidence(header)) else 'int16' # get subset of relevant metadata - md = collate_metadata(header) + #md = collate_metadata(header) # Not needed in full-res geotiff # create GDAL object ds = gdal_dataset(dest, ncols, nrows, driver="GTiff", bands=1, - dtype=dtype, metadata=md, crs=wkt, + dtype=dtype, metadata=None, crs=wkt, geotransform=gt, creation_opts=['compress=packbits']) # copy data from the binary file @@ -1220,7 +1220,12 @@ def output_tiff_filename(inpath, outpath): :rtype: str """ fname, ext = os.path.basename(inpath).split('.') - return os.path.join(outpath, fname + '_' + ext + '.tif') + if ext == 'tif': + name = os.path.join(outpath, fname + '.tif') + else: + name = os.path.join(outpath, fname + '_' + ext + '.tif') + + return name def check_correction_status(preread_ifgs, meta): # pragma: no cover From ba47c51876d3e1861b163f8e2dcfc9143604113b Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Fri, 15 Mar 2019 16:52:55 +1100 Subject: [PATCH 014/376] save multilooked tiffs from prepifg to outdir and fullres tiffs from converttogeotiff to obsdir --- pyrate/prepifg.py | 19 +++++++++++-------- pyrate/scripts/converttogtif.py | 2 -- pyrate/scripts/run_prepifg.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/pyrate/prepifg.py b/pyrate/prepifg.py index 49a9bfa0d..00907c2a7 100644 --- a/pyrate/prepifg.py +++ b/pyrate/prepifg.py @@ -35,7 +35,7 @@ from pyrate import config as cf from pyrate.gdal_python import crop_resample_average from pyrate import ifgconstants as ifc -from pyrate.shared import Ifg, DEM +from pyrate.shared import Ifg, DEM, output_tiff_filename CustomExts = namedtuple('CustExtents', ['xfirst', 'yfirst', 'xlast', 'ylast']) @@ -146,12 +146,12 @@ def _get_extents(ifgs, crop_opt, user_exts=None): def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, - write_to_disk=True): + write_to_disk=True, out_path=None): """ Open, resample, crop and optionally save to disk an interferogram or DEM. Returns are only given if write_to_disk=False - :param str raster_path: Raster file path name + :param str raster_path: Input raster file path name :param int xlooks: Number of multi-looks in x; 5 is 5 times smaller, 1 is no change :param int ylooks: Number of multi-looks in y @@ -160,6 +160,7 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, :param float thresh: see thresh in prepare_ifgs() :param int crop_opt: Crop option :param bool write_to_disk: Write new data to disk + :param str out_path: Path for output file :return: resampled_data: output cropped and resampled image :rtype: ndarray @@ -185,12 +186,12 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, return _dummy_warp(renamed_path) return _warp(raster, xlooks, ylooks, exts, resolution, thresh, - crop_opt, write_to_disk) + crop_opt, write_to_disk, out_path) # TODO: crop options 0 = no cropping? get rid of same size def prepare_ifgs(raster_data_paths, crop_opt, xlooks, ylooks, thresh=0.5, - user_exts=None, write_to_disc=True): + user_exts=None, write_to_disc=True, out_path=None): """ Wrapper function to prepare a sequence of interferogram files for PyRate analysis. See prepifg.prepare_ifg() for full description of @@ -218,7 +219,7 @@ def prepare_ifgs(raster_data_paths, crop_opt, xlooks, ylooks, thresh=0.5, exts = get_analysis_extent(crop_opt, rasters, xlooks, ylooks, user_exts) return [prepare_ifg(d, xlooks, ylooks, exts, thresh, crop_opt, - write_to_disc) + write_to_disc, out_path) for d in raster_data_paths] @@ -269,7 +270,7 @@ def _dummy_warp(renamed_path): def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, - write_to_disk=True): + write_to_disk=True, out_path=None): """ Convenience function for calling GDAL functionality """ @@ -277,7 +278,9 @@ def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, raise ValueError('X and Y looks mismatch') # cut, average, resample the final output layers - looks_path = cf.mlooked_path(ifg.data_path, y_looks, crop_out) + op = output_tiff_filename(ifg.data_path, out_path) + looks_path = cf.mlooked_path(op, y_looks, crop_out) + print(looks_path) # # Add missing/updated metadata to resampled ifg/DEM # new_lyr = type(ifg)(looks_path) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index 4eb02afa2..b128dc918 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -83,8 +83,6 @@ def main(params=None): if params[cf.APS_ELEVATION_MAP]: base_ifg_paths.append(params[cf.APS_ELEVATION_MAP]) - shared.mkdir_p(params[cf.OUT_DIR]) # create output dir - if use_luigi: log.info("Running converttogtif using luigi") luigi.configuration.LuigiConfigParser.add_config_path( diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 08df0ae89..39a86ac5f 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -33,11 +33,14 @@ from pyrate import shared import pyrate.ifgconstants as ifc from pyrate import mpiops +from pyrate.prepifg import PreprocessError +from pyrate import gamma +from pyrate import roipac log = logging.getLogger(__name__) GAMMA = 1 - +ROIPAC = 0 def main(params=None): """ @@ -91,7 +94,7 @@ def main(params=None): process_base_ifgs_paths = \ np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] gtiff_paths = [shared.output_tiff_filename(f, \ - params[cf.OUT_DIR]) for f in process_base_ifgs_paths] + params[cf.OBS_DIR]) for f in process_base_ifgs_paths] do_prepifg(gtiff_paths, params) log.info("Finished prepifg") @@ -117,12 +120,29 @@ def do_prepifg(gtiff_paths, params): thresh = params[cf.NO_DATA_AVERAGING_THRESHOLD] if parallel: Parallel(n_jobs=params[cf.PROCESSES], verbose=50)( - delayed(prepifg.prepare_ifg)(p, xlooks, ylooks, exts, thresh, crop) - for p in gtiff_paths) + delayed(_prepifg_multiprocessing)(p, xlooks, ylooks, exts, thresh, crop, + params) for p in gtiff_paths) else: - [prepifg.prepare_ifg(i, xlooks, ylooks, exts, - thresh, crop) for i in gtiff_paths] + [_prepifg_multiprocessing(p, xlooks, ylooks, exts, thresh, crop, + params) for p in gtiff_paths] else: log.info("Full-res geotiffs do not exist") +def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): + """ + Multiprocessing wrapper for prepifg + """ + processor = params[cf.PROCESSOR] # roipac or gamma + if processor == GAMMA: + header = gamma.gamma_header(path, params) + elif processor == ROIPAC: + header = roipac.roipac_header(path, params) + else: + raise PreprocessError('Processor must be ROI_PAC (0) or ' + 'GAMMA (1)') + + prepifg.prepare_ifg(path, xlooks, ylooks, exts, thresh, crop, + out_path=params[cf.OUT_DIR]) + + From 441c028a04854eff05610a84eb586fc2601cc7c6 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Fri, 22 Mar 2019 16:24:33 +1100 Subject: [PATCH 015/376] add PyRate relevant metadata at prepifg and/or converttogeotiff stages --- pyrate/gdal_python.py | 11 +++++++---- pyrate/prepifg.py | 9 +++++---- pyrate/scripts/run_prepifg.py | 2 +- pyrate/shared.py | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pyrate/gdal_python.py b/pyrate/gdal_python.py index 7c2ea1983..be69cb6ff 100644 --- a/pyrate/gdal_python.py +++ b/pyrate/gdal_python.py @@ -263,7 +263,7 @@ def _gdalwarp_width_and_height(max_x, max_y, min_x, min_y, geo_trans): def crop_resample_average( input_tif, extents, new_res, output_file, thresh, out_driver_type='GTiff', - match_pirate=False): + match_pirate=False, hdr=None): """ Crop, resample, and average a geotiff image. @@ -274,6 +274,7 @@ def crop_resample_average( :param float thresh: NaN fraction threshold :param str out_driver_type: The output driver; `MEM` or `GTiff` (optional) :param bool match_pirate: Match Matlab Pirate output (optional) + :param dict hdr: dictionary of metadata :return: resampled_average: output cropped and resampled image :rtype: ndarray @@ -302,11 +303,13 @@ def crop_resample_average( gt = dst_ds.GetGeoTransform() wkt = dst_ds.GetProjection() - # copy and update metadata - md = dst_ds.GetMetadata() - # TEST HERE IF EXISTING FILE HAS PYRATE METADATA. IF NOT ADD HERE + if not ifc.DATA_TYPE in dst_ds.GetMetadata(): + md = shared.collate_metadata(hdr) + else: + md = dst_ds.GetMetadata() + # update metadata for output for k, v in md.items(): if k == ifc.DATA_TYPE: # update data type metadata diff --git a/pyrate/prepifg.py b/pyrate/prepifg.py index 00907c2a7..b19f4d6f6 100644 --- a/pyrate/prepifg.py +++ b/pyrate/prepifg.py @@ -146,7 +146,7 @@ def _get_extents(ifgs, crop_opt, user_exts=None): def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, - write_to_disk=True, out_path=None): + write_to_disk=True, out_path=None, header=None): """ Open, resample, crop and optionally save to disk an interferogram or DEM. Returns are only given if write_to_disk=False @@ -161,6 +161,7 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, :param int crop_opt: Crop option :param bool write_to_disk: Write new data to disk :param str out_path: Path for output file + :param dict header: dictionary of metadata from header file :return: resampled_data: output cropped and resampled image :rtype: ndarray @@ -186,7 +187,7 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, return _dummy_warp(renamed_path) return _warp(raster, xlooks, ylooks, exts, resolution, thresh, - crop_opt, write_to_disk, out_path) + crop_opt, write_to_disk, out_path, header) # TODO: crop options 0 = no cropping? get rid of same size @@ -270,7 +271,7 @@ def _dummy_warp(renamed_path): def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, - write_to_disk=True, out_path=None): + write_to_disk=True, out_path=None, header=None): """ Convenience function for calling GDAL functionality """ @@ -298,7 +299,7 @@ def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, new_res=resolution, output_file=looks_path, thresh=thresh, - out_driver_type=driver_type) + out_driver_type=driver_type, hdr=header) if not write_to_disk: return resampled_data, out_ds diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 39a86ac5f..25a7edc54 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -143,6 +143,6 @@ def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): 'GAMMA (1)') prepifg.prepare_ifg(path, xlooks, ylooks, exts, thresh, crop, - out_path=params[cf.OUT_DIR]) + out_path=params[cf.OUT_DIR], header=header) diff --git a/pyrate/shared.py b/pyrate/shared.py index 48fa15fba..c96000129 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -737,12 +737,12 @@ def write_fullres_geotiff(header, data_path, dest, nodata): wkt = srs.ExportToWkt() dtype = 'float32' if (_is_interferogram(header) or _is_incidence(header)) else 'int16' - # get subset of relevant metadata - #md = collate_metadata(header) # Not needed in full-res geotiff + # get subset of metadata relevant to PyRate + md = collate_metadata(header) # create GDAL object ds = gdal_dataset(dest, ncols, nrows, driver="GTiff", bands=1, - dtype=dtype, metadata=None, crs=wkt, + dtype=dtype, metadata=md, crs=wkt, geotransform=gt, creation_opts=['compress=packbits']) # copy data from the binary file From a057035a633beb6bbff35fd5f8dde8b66c2e9fe2 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 27 Mar 2019 11:59:54 +1100 Subject: [PATCH 016/376] remove the future 'conversion class' concept for the time being --- pyrate/conversion/__init__.py | 0 pyrate/conversion/base.py | 322 ---------------------------------- 2 files changed, 322 deletions(-) delete mode 100644 pyrate/conversion/__init__.py delete mode 100644 pyrate/conversion/base.py diff --git a/pyrate/conversion/__init__.py b/pyrate/conversion/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pyrate/conversion/base.py b/pyrate/conversion/base.py deleted file mode 100644 index 70cb9f9ea..000000000 --- a/pyrate/conversion/base.py +++ /dev/null @@ -1,322 +0,0 @@ -# This Python module is part of the PyRate software package. -# -# Copyright 2017 Geoscience Australia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -This Python module contains the base clase that defines -conversion routines for handling metadata, reading of -various formats and output to GeoTIFF (which can be extended to other formats). -""" - -import gdal -import rasterio -from pyrate import ifgconstants as ifc - - -#def metadata(header): -# """ -# Standardise the metadata to enable simpler output for the -# write_geotif function. -# -# :param header: -# :type dict: -# Dictionary containing interferogram metadata. -# -# :return: -# A dict containing specific flags for either ifg, dem or -# incidence. -# """ -# # similiar to what is written in the Conversion base class' _pass_header -# -# return - - -def rio_dataset(out_fname, columns, rows, driver="GTiff", bands=1, - dtype="float32", metadata=None, crs=None, geotransform=None, - creation_opts=None): - """ - Initialises a rasterio dataset object for writing image data. - The reason being that the write_function shouldn't need to deal with - reading data. But we shouldn't be required to read the whole file in - order to write data either. - So a generic write_geotif is fine, but works better when you have - read all the data into memory. - """ - kwargs = { - "width": columns, - "height": rows, - "driver": driver, - "count": bands, - "crs": crs, - "dtype": dtype, - "transform": geotransform - } - - # driver creation options - if creation_opts is not None: - for k, v in creation_opts.items(): - kwargs[k] = v - - # create output dataset - outds = rasterio.open(out_fname, 'w', **kwargs) - - # global metadata - if metadata is not None: - outds.update_tags(**metadata) - - return outds - - -#def gdal_dataset(out_fname, columns, rows, driver="GTiff", bands=1, -# dtype='float32', metadata=None, crs=None, -# geotransform=None, creation_opts=None): -# """ -# Initialises a py-GDAL dataset object for writing image data. -# """ -# gdal_dtype = gdal.GDT_Float32 if dtype == 'float32' else gdal.GDT_Int16 -# -# # create output dataset -# driver = gdal.GetDriverByName(driver) -# outds = driver.Create(out_fname, columns, rows, bands, dtype, -# options=creation_opts) -# -# # geospatial info -# outds.SetGeoTransform(geotransform) -# outds.SetProjection(crs) -# -# # global metadata -# if metadata is not None: -# outds.SetMetadataItem(k, str(v)) # just following PyRate's example, but what if value is numeric??? -# -# return outds - - -def write_geotiff(data, out_fname, dtype='float32', metadata=None, crs=None, - geotransform=None, creation_opts={'compress': 'packbits'}): - """ - A generic routine for writing a NumPy array to a geotiff. - """ - # only support "2 <= dims <= 3" - if data.ndim == 3: - count, height, width = data.shape - elif data.ndim == 2: - height, width = data.shape - else: - msg = "Only support dimensions of '2 <= dims <= 3'." - raise TypeError(msg) - - kwargs = { - "width": width, - "height": height, - "driver": "GTiff", - "count": count, - "crs": crs, - "dtype": dtype, - "transform": geotransform - } - - # driver creation options - if creation_opts is not None: - for k, v in creation_opts.items(): - kwargs[k] = v - - # create output dataset - with rasterio.open(out_fname, 'w', **kwargs) as outds: - - # global metadata - if metadata is not None: - outds.update_tags(**metadata) - - # write data - if data.ndim = 3: - for band_id, blob in enumerate(data): - outds.write(blob, band_id+1) - else: - # single 2D band only (1D or > 3D should have failed earlier) - outds.write(data, 1) - - -class Conversion(object): - - """ - ***************************************************************** - NOTE! - This presents a concept for a future direction when there is a - greater need to support more formats. - ***************************************************************** - - The base conversion class. - Ideally this is subclassed for each format type that requires - conversion. - eg GAMMA, ROIPC, SNAP-HDF5, SNAP-BEAM-DIMAP etc - - And the read methods are unique to each format type. - That way the workflow code just calls {conversion_class}.write_geo_tiff() - - Still need to handle multibands though. - """ - - def __init__(self, filename, header, out_fname): - # we need to specify the native block read size - # eg 1 row by all columns for BSQ raw binary files (gamma raw output file) - # so the _read routine can define an iterator to read native blocks - # and write them out, before reading the next block (save memory) - - self._filename = filename - self._header = header - self._out_fname = out_fname - self._ifg = ifc.PYRATE_WAVELENGTH_METRES in header - self_incidence = 'FILE_TYPE' in header - self._ifg_proc = header[ifc.PYRATE_INSAR_PROCESSOR] - self._ncols = header[ifc.PYRATE_NCOLS] - self._nrows = header[ifc.PYRATE_NROWS] - - self._pass_header() - - def _pass_header(self): - """ - Extract relevant info from the header into dict containing - metadata for direct dumping into gdal file creation. - """ - hdr = {} - if self.ifg: - for k in [ifc.PYRATE_WAVELENGTH_METRES, ifc.PYRATE_TIME_SPAN, - ifc.PYRATE_INSAR_PROCESSOR, - ifc.MASTER_DATE, ifc.SLAVE_DATE, - ifc.DATA_UNITS, ifc.DATA_TYPE]: - # is it always str or could they be numeric? - hdr[k] = str(self._header[k]) - if self.ifg_proc == GAMMA: - for k in [ifc.MASTER_TIME, ifc.SLAVE_TIME, ifc.PYRATE_INCIDENCE_DEGREES]: - # is it always str or could they be numeric? - hdr[k] = str(self._header[k]) - elif self.incidence: - hdr[ifc.DATA_TYPE] = ifc.INCIDENCE - else: # must be dem - hdr[ifc.DATA_TYPE] = ifc.DEM - - self._geotransform = [ - header[ifc.PYRATE_LONG], - header[ifc.PYRATE_X_STEP], - 0, - header[ifc.PYRATE_LAT], - 0, - header[ifc.PYRATE_Y_STEP] - ] - - srs = osr.SpatialReference() - res = srs.SetWellKnownGeogCS(header[ifc.PYRATE_DATUM]) - self._crs = res.ExportToWkt() - - self._metadata = hdr - - def _read_block(self): - # this method needs to be overridded with the specific read routine - # i.e. raw GAMMA, raw ROIPC, tif GAMMA, SNAP HDF5 etc - # this should return a read iterator - # where each call will read a portion - # eg for data, window in {conversion_class}._read_block() - raise NotImplementedError - - def write_geotiff(self): - # output a geotiff according to PyRate's format spec - - # the method will operate as - # create output dataset (via rio_dataset or gdal_dataset) - # read a blob of data - # write that blob of data to the appropriate block in the tif - # read next blob of data - - # eg - # outds = rio_dataset(...) - # for data, window in {conversion_class}._read_block(): - # outds.write(data, 1, window=window) - raise NotImplementedError - - @property - def filename(self): - return self._filename - - @property - def out_fname(self): - return self._out_fname - - @property - def ncols(self): - return self._ncols - - @property - def nrows(self): - return self._nrows - - @property - def ifg(self): - return self._ifg - - @property - def incidence(self): - return self._incidence - - @property - def ifg_proc(self): - return self._ifg_proc - - @property - def metadata(self): - return self._metadata - - @property - def crs(self): - return self._crs - - @property - def geotransform(self): - return self._geotransform - - -class ConvertGamma(Conversion): - - """GAMMA file format converter.""" - - def __init__(self, filename, header, out_fname): - super(ConvertGamma, self).__init__(filename, header, out_fname) - - def write_geotiff(self): - """ - Overwrite the base classes write_geotiff method. - Also ignoring the need for a read iterator, - which could be scrapped altogether. - """ - # these two variables could be properties (unique to raw binary files) - bytes_per_col, fmtstr = _data_format(self.ifg_proc, self.ifg, - self.ncols) - # this could be a property (unique to raw binary files) - row_bytes = self.ncols * bytes_per_col - - # initialise the gdal dataset - outds = gdal_dataset(self.out_fname, self.ncols, self.nrows, - metadata=self.metadata, crs=self.crs, - geotransform=self.geotransform, - creation_opts=['compress=packbits']) - - band = outds.GetRasterBand(1) - - with open(self.filename, 'rb') as src: - for y in range(selfnrows): - data = struct.unpack(fmtstr, src.read(row_bytes)) - band.WriteArray(np.array(data).reshape(1, self.ncols), yoff=y) - - # can't remember if there is a Close() method - band = None - outds = None From 2152ade935af24847e63e324dcb4034f1ea35eee Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 01:00:18 +0000 Subject: [PATCH 017/376] Add setuptools and wheel, needed for distribution. --- requirements-dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3231e7756..1888d37a4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,5 @@ recommonmark==0.5.0 matplotlib==3.1.1 IPython==7.6.1 sphinxcontrib-programoutput==0.14 +setuptools==41.0.1 +wheel==0.33.4 From 317f8c59327823ce291f34e8b5ecb1ac8be617ed Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 01:32:03 +0000 Subject: [PATCH 018/376] Add twine to dev reqs and update release make cmd with twine upload. --- Makefile | 7 ++++--- requirements-dev.txt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 5d1eebc15..d35596a7a 100644 --- a/Makefile +++ b/Makefile @@ -75,9 +75,10 @@ docs: ## generate Sphinx HTML documentation, including API docs servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . -release: clean ## package and upload a release - python setup.py sdist upload - python setup.py bdist_wheel upload +release: clean ## builds source and wheel package and uploads to PyPi + python setup.py sdist + python setup.py bdist_wheel + twine upload dist/* dist: clean ## builds source and wheel package python setup.py sdist diff --git a/requirements-dev.txt b/requirements-dev.txt index 1888d37a4..a3b342687 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,4 +6,5 @@ matplotlib==3.1.1 IPython==7.6.1 sphinxcontrib-programoutput==0.14 setuptools==41.0.1 +twine==1.13.0 wheel==0.33.4 From 471229473296e8ef2c19a85ea347921b5c97efc3 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Tue, 30 Jul 2019 13:11:01 +1000 Subject: [PATCH 019/376] rename stage 2 of workflow 'process' --- pyrate/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 1e0bd4ca4..c3fdbc3ab 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -58,7 +58,7 @@ def prepifg(config_file): help='divide ifgs into this many rows') @click.option('-c', '--cols', type=int, default=1, help='divide ifgs into this many columns') -def linrate(config_file, rows, cols): +def process(config_file, rows, cols): """ Main PyRate workflow including time series and linear rate computation """ From 29ca1c36673dd232f0f398448e7044d92bdde04e Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Tue, 30 Jul 2019 13:28:06 +1000 Subject: [PATCH 020/376] add step descriptions --- pyrate/scripts/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index c3fdbc3ab..ec16003a4 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -42,7 +42,7 @@ def cli(verbosity): @click.argument('config_file') def prepifg(config_file): """ - Convert input files to geotiff and perform multilooking + Step 1: Convert input files to geotiff and perform multilooking (resampling) and/or cropping """ config_file = os.path.abspath(config_file) @@ -60,7 +60,7 @@ def prepifg(config_file): help='divide ifgs into this many columns') def process(config_file, rows, cols): """ - Main PyRate workflow including time series and linear rate computation + Step 2: Main PyRate workflow including time series and linear rate computation """ config_file = os.path.abspath(config_file) _, dest_paths, params = cf.get_ifg_paths(config_file) @@ -79,7 +79,7 @@ def process(config_file, rows, cols): 'number of cols used previously in main workflow') def postprocess(config_file, rows, cols): """ - Reassemble PyRate output tiles and save as geotiffs + Step 3: Reassemble PyRate output tiles and save as geotiffs """ config_file = os.path.abspath(config_file) postprocessing.main(config_file, rows, cols) From ba00aeacc2aa1190991de1001d53a01f0680a109 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Tue, 30 Jul 2019 13:28:33 +1000 Subject: [PATCH 021/376] update doco --- docs/installation.rst | 4 ++-- docs/pyrate.scripts.main.rst | 2 +- docs/usage.rst | 23 +++++++++++++---------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 170ea0a92..2700cc29e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -18,7 +18,7 @@ included example config file and data: :: pyrate prepifg input_parameters.conf - pyrate linrate input_parameters.conf + pyrate process input_parameters.conf pyrate postprocess input_parameters.conf On Raijin and other HPC systems, you can utilise MPI to run PyRate in parallel: @@ -27,7 +27,7 @@ On Raijin and other HPC systems, you can utilise MPI to run PyRate in parallel: # Modify 'n' based on the number of processors available. mpirun -n 4 pyrate prepifg input_parameters.conf - mpirun -n 4 pyrate linrate input_parameters.conf -c 2 -r 2 + mpirun -n 4 pyrate process input_parameters.conf -c 2 -r 2 mpirun -n 4 pyrate postprocess input_parameters.conf -c 2 -r 2 If the installation has been successful, this workflow will complete without diff --git a/docs/pyrate.scripts.main.rst b/docs/pyrate.scripts.main.rst index 1eb7d82d8..f4bc8d543 100644 --- a/docs/pyrate.scripts.main.rst +++ b/docs/pyrate.scripts.main.rst @@ -7,5 +7,5 @@ PyRate main Script .. autosummary:: prepifg - linrate + process postprocess diff --git a/docs/usage.rst b/docs/usage.rst index 3c3abe354..2773029a9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -23,21 +23,24 @@ Use ``help`` for the different command line options: >> pyrate --help Usage: pyrate [OPTIONS] COMMAND [ARGS]... + Commandline options and logging setup + Options: + -V, --version Show the version and exit. -v, --verbosity [DEBUG|INFO|WARNING|ERROR] Level of logging --help Show this message and exit. Commands: - linrate - postprocess - prepifg + postprocess Step 3: Reassemble PyRate output tiles and save as geotiffs + prepifg Step 1: Convert input files to geotiff and perform multilooking... + process Step 2: Main PyRate workflow including time series and linear rate... The ``pyrate`` program has three command line options corresponding to different parts of the PyRate workflow: 1. prepifg -2. linrate +2. process 3. postprocess Below we discuss these options. @@ -106,13 +109,13 @@ Two example configuration files are provided in the *configs/* directory, one each for ROI\_PAC and GAMMA prepifg configuration. Either configuration file can be used with ``prepifg``. -linrate: Main workflow and linear rate and time series analysis +process: Main workflow and linear rate and time series analysis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - >> pyrate linrate --help - Usage: pyrate linrate [OPTIONS] CONFIG_FILE + >> pyrate process --help + Usage: pyrate process [OPTIONS] CONFIG_FILE Options: -r, --rows INTEGER divide ifgs into this many rows @@ -120,11 +123,11 @@ linrate: Main workflow and linear rate and time series analysis --help Show this message and exit This is the core of the PyRate processing workflow, handled by the -``linrate`` command: +``process`` command: :: - pyrate linrate path/to/config_file -c 3 -r 4 + pyrate process path/to/config_file -c 3 -r 4 This command will perform the time series and linear rate analysis and has the option to break the interferograms into a number of tiles in @@ -167,7 +170,7 @@ save geotiff files of the final time series and linear rate products. --help Show this message and exit. Make sure to use the same number of rows and columns that was used in -the previous ``linrate`` step: +the previous ``process`` step: :: From 76b19ec1398164458370dedb6403d08b737d9fe0 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 22:48:20 +0000 Subject: [PATCH 022/376] Update PyProj and Pytest to support Python 3.7. Remove deprecated is_latlong checks from pyproj usage. --- pyrate/shared.py | 9 +++++---- requirements-test.txt | 2 +- requirements.txt | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pyrate/shared.py b/pyrate/shared.py index b2e88bbb0..a4189779f 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -1000,11 +1000,12 @@ def cell_size(lat, lon, x_step, y_step): zone = _utm_zone(lon) p0 = pyproj.Proj(proj='latlong', ellps='WGS84') p1 = pyproj.Proj(proj='utm', zone=zone, ellps='WGS84') - assert p0.is_latlong() - assert not p1.is_latlong() - x0, y0 = pyproj.transform(p0, p1, lon, lat) - x1, y1 = pyproj.transform(p0, p1, lon + x_step, lat + y_step) + x0, y0 = pyproj.transform(p0, p1, lon, lat, + errcheck=True) + x1, y1 = pyproj.transform(p0, p1, lon + x_step, lat + y_step, + errcheck=True) + return tuple(abs(e) for e in (x1 - x0, y1 - y0)) diff --git a/requirements-test.txt b/requirements-test.txt index cf04f98e0..58f008b4e 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,4 +2,4 @@ pytest-cov==2.5.1 coverage==4.5.3 codecov==2.0.15 tox==3.13.2 -pytest==3.0.0 +pytest==5.0.1 diff --git a/requirements.txt b/requirements.txt index d089d0da1..e42afafff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ Click==7.0 -numpy==1.16.4 Cython==0.29.11 -mpi4py==3.0.2 -scipy==1.3.0 +GDAL Pillow==6.1.0 -pyproj==1.9.5.1 -networkx==2.3 -joblib==0.13.2 glob2==0.7 -GDAL \ No newline at end of file +joblib==0.13.2 +mpi4py==3.0.2 +networkx==2.3 +numpy==1.16.4 +pyproj==2.2.1 +scipy==1.3.0 From 8432978c2ced8a2bc045fefbbf6953e65ccf7a14 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 22:51:55 +0000 Subject: [PATCH 023/376] Add Python 3.7 to Travis build. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db511f655..305491a0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,9 @@ branches: language: python -python: 3.6 +python: +- "3.6" +- "3.7" os: linux From 909717e74e461e44d991e3616dec9ee5a4d3939c Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:11:45 +0000 Subject: [PATCH 024/376] Add Python 3.7 as supported version. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9e8d169df..7d6bec61e 100644 --- a/setup.py +++ b/setup.py @@ -96,6 +96,7 @@ def run_tests(self): "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", From de84bd855d9ce8a1ac08c90c37ec10de8c82ed2f Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:25:38 +0000 Subject: [PATCH 025/376] Remove Python 3.6 from apt_install. --- utils/apt_install.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/utils/apt_install.sh b/utils/apt_install.sh index 33e26c5c5..62b70de07 100755 --- a/utils/apt_install.sh +++ b/utils/apt_install.sh @@ -1,4 +1,4 @@ -echo "This script will install Python 3.6.8 and additional packages required by PyRate. Continue?" +echo "This script will install packages required by PyRate. Continue?" select yn in "Yes" "No"; do case $yn in Yes ) break;; @@ -12,6 +12,5 @@ sudo apt-get -y install \ gdal-bin \ libgdal-dev \ openmpi-bin \ - libopenmpi-dev \ - python3 + libopenmpi-dev From 742e733fb12ee4c21f780b8d74b3188c088cee6f Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:29:43 +0000 Subject: [PATCH 026/376] Fix typo. --- docs/hpc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hpc.rst b/docs/hpc.rst index 3912ed561..2f7af0fdf 100644 --- a/docs/hpc.rst +++ b/docs/hpc.rst @@ -47,5 +47,5 @@ environment and reload the required modules: :: cd ~ - source PyRateVM/bin/activate + source PyRateVenv/bin/activate source PyRate/utils/load_modules.sh From 5fa55509d02be9c52e2cf5aae78b11f683698aa8 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:39:06 +0000 Subject: [PATCH 027/376] Update readme python badge. --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 764ee874a..af2049d98 100644 --- a/README.rst +++ b/README.rst @@ -9,10 +9,8 @@ Python tool for InSAR Rate and Time-series Estimation :target: https://coveralls.io/github/GeoscienceAustralia/PyRate?branch=master .. image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg :target: https://opensource.org/licenses/Apache-2.0 - :alt: License -.. image:: https://img.shields.io/badge/python-3.6-blue.svg - :target: https://docs.python.org/3/whatsnew/3.6.html - :alt: Python 3.6 support +.. image:: https://img.shields.io/pypi/pyversions/Py-Rate + :target: https://pypi.org/project/Py-Rate/ PyRate is a Python tool for estimating the average rate (velocity) and cumulative displacement time-series of surface movements for every pixel in a stack of geocoded unwrapped interferograms generated by Interferometric Synthetic Aperture Radar (InSAR) processing. PyRate currently supports input data in the GAMMA or ROI_PAC software formats. From 3df44be7ebd3e27fe98d1644261ed96fbd15c5c1 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:39:33 +0000 Subject: [PATCH 028/376] Add note about required Python version. --- docs/ubuntu.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ubuntu.rst b/docs/ubuntu.rst index 3c5bec7a5..8987a982a 100644 --- a/docs/ubuntu.rst +++ b/docs/ubuntu.rst @@ -5,6 +5,8 @@ These instructions have been tested on Ubuntu 18.04. If using another distribution, you will have to install packages equivalent to those listed in `apt_install.sh`. +Ensure you have Python 3.6 or 3.7 installed. + Clone the PyRate repository, install required packages and install PyRate: :: From 5a6f97aba6c12e1874d928844d2c9fc0ef2fdb49 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:54:31 +0000 Subject: [PATCH 029/376] Load Python 3.7 instead of 3.6. --- utils/load_modules.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/load_modules.sh b/utils/load_modules.sh index c62a78a28..6fdad24da 100755 --- a/utils/load_modules.sh +++ b/utils/load_modules.sh @@ -1,7 +1,7 @@ module unload intel-cc module unload intel-fc module unload openmpi -module load python3/3.6.7 +module load python3/3.7.2 module load gdal/2.2.2 module load openmpi/2.1.1 From e31b9934d94876dc81c7c4bfbfa584765346072d Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 30 Jul 2019 23:55:47 +0000 Subject: [PATCH 030/376] Add changes. --- docs/history.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/history.rst b/docs/history.rst index d439e4b58..1979edd4e 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -2,6 +2,16 @@ Release History =============== +Unreleased (2019-xx-xx) +----------------------- +Added ++++++ +- Python 3.7 support + +Changed ++++++++ +- 'linrate' command has been renamed to 'process' + 0.3.0 (2019-07-26) ----------------------- Added From d07563d745d67709c5c08d91e298102dfc6803a8 Mon Sep 17 00:00:00 2001 From: Matt Garthwaite Date: Wed, 31 Jul 2019 13:27:27 +1000 Subject: [PATCH 031/376] add processing strategy info to high level software description --- README.rst | 2 +- docs/index.rst | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index af2049d98..2dc7f29a7 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Python tool for InSAR Rate and Time-series Estimation .. image:: https://img.shields.io/pypi/pyversions/Py-Rate :target: https://pypi.org/project/Py-Rate/ -PyRate is a Python tool for estimating the average rate (velocity) and cumulative displacement time-series of surface movements for every pixel in a stack of geocoded unwrapped interferograms generated by Interferometric Synthetic Aperture Radar (InSAR) processing. PyRate currently supports input data in the GAMMA or ROI_PAC software formats. +PyRate is a Python tool for estimating the average rate (velocity) and cumulative displacement time-series of surface movements for every pixel in a stack of geocoded unwrapped interferograms generated by Interferometric Synthetic Aperture Radar (InSAR) processing. PyRate uses a "Small Baseline Subset" (SBAS) processing strategy and currently supports input data in the GAMMA or ROI_PAC software formats. The PyRate project started in 2012 as a partial Python translation of "Pirate", a Matlab tool developed by the University of Leeds and the Guangdong University of Technology. diff --git a/docs/index.rst b/docs/index.rst index c9421e1ac..b9dc4f0ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,8 +11,9 @@ This is the documentation for the **PyRate** software. **PyRate** is a Python tool for estimating the average rate (velocity) and cumulative displacement time-series of surface movements for every pixel in a stack of geocoded unwrapped interferograms generated by Interferometric -Synthetic Aperture Radar (InSAR) processing. **PyRate** currently supports -input data in the GAMMA or ROI_PAC software formats. +Synthetic Aperture Radar (InSAR) processing. **PyRate** uses a "Small +Baseline Subset" (SBAS) processing strategy and currently supports input +data in the GAMMA or ROI_PAC software formats. The **PyRate** project started in 2012 as a partial Python translation of "Pirate", a Matlab tool developed by the University of Leeds and the Guangdong From 2e46ab229e39240f39732c1d861a970e16f50a66 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 31 Jul 2019 04:16:18 +0000 Subject: [PATCH 032/376] Support finding ROIPAC header files for converted geotiffs. --- pyrate/roipac.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyrate/roipac.py b/pyrate/roipac.py index deba5f806..a51ff6a1c 100644 --- a/pyrate/roipac.py +++ b/pyrate/roipac.py @@ -201,7 +201,8 @@ def manage_header(header_file, projection): def roipac_header(file_path, params): """ - Function to obtain a header for roipac interferogram file + Function to obtain a header for roipac interferogram file or converted + geotiff. """ rsc_file = os.path.join(params[cf.DEM_HEADER_FILE]) if rsc_file is not None: @@ -209,8 +210,14 @@ def roipac_header(file_path, params): else: raise RoipacException('No DEM resource/header file is ' 'provided') + if file_path.endswith('_dem.tif'): + header_file = os.path.join(params[cf.DEM_HEADER_FILE]) + elif file_path.endswith('_unw.tif'): + base_file = file_path[:-8] + header_file = base_file + '.unw.' + ROI_PAC_HEADER_FILE_EXT + else: + header_file = "%s.%s" % (file_path, ROI_PAC_HEADER_FILE_EXT) - header_file = "%s.%s" % (file_path, ROI_PAC_HEADER_FILE_EXT) header = manage_header(header_file, projection) return header From b42d6c162df391443c9c8fc38c40222f819cba63 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 31 Jul 2019 04:16:28 +0000 Subject: [PATCH 033/376] Remove debug print. --- pyrate/scripts/converttogtif.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index d03c68c58..441537491 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -25,8 +25,6 @@ from joblib import Parallel, delayed import numpy as np -from pyrate.tasks.utils import pythonify_config -from pyrate.tasks import ConvertToGeotiff from pyrate.prepifg import PreprocessError from pyrate import config as cf from pyrate import roipac @@ -113,7 +111,6 @@ def _geotiff_multiprocessing(unw_path, params): Multiprocessing wrapper for full-res geotiff conversion """ dest = shared.output_tiff_filename(unw_path, params[cf.OBS_DIR]) - print(dest) processor = params[cf.PROCESSOR] # roipac or gamma # Create full-res geotiff if not already on disk From 2c10ac7aca917eb9ea1c288072ea8b36b5c06395 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 31 Jul 2019 04:16:40 +0000 Subject: [PATCH 034/376] Remove luigi reference. --- pyrate/scripts/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 2d5dbe951..4fa01cff5 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -48,10 +48,7 @@ def converttogeotiff(config_file): params = cf.get_config_params(config_file) log.info('This job was run with the following parameters:') log.info(json.dumps(params, indent=4, sort_keys=True)) - if params[cf.LUIGI]: - converttogtif.main() - else: - converttogtif.main(params) + converttogtif.main(params) @cli.command() From be889d09b4afd97e7a62af380afb83ebc8214b33 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 31 Jul 2019 04:16:53 +0000 Subject: [PATCH 035/376] Remove dead code. --- pyrate/scripts/run_prepifg.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index d005c2029..10f6ee739 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -77,18 +77,12 @@ def main(params=None): base_ifg_paths.append(params[cf.APS_ELEVATION_MAP]) shared.mkdir_p(params[cf.OUT_DIR]) # create output dir - - process_base_ifgs_paths = np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] - if processor == ROIPAC: - roipac_prepifg(process_base_ifgs_paths, params) - elif processor == GAMMA: - gamma_prepifg(process_base_ifgs_paths, params) - else: - process_base_ifgs_paths = \ - np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] - gtiff_paths = [shared.output_tiff_filename(f, \ - params[cf.OBS_DIR]) for f in process_base_ifgs_paths] - do_prepifg(gtiff_paths, params) + + process_base_ifgs_paths = \ + np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] + gtiff_paths = [shared.output_tiff_filename(f, \ + params[cf.OBS_DIR]) for f in process_base_ifgs_paths] + do_prepifg(gtiff_paths, params) log.info("Finished prepifg") From 4e0333b833db1d6c15c52428bd9b6c9ef063d65a Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 31 Jul 2019 04:18:33 +0000 Subject: [PATCH 036/376] Update test to support separation of geotiff conversion from prepifg. --- tests/test_covariance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_covariance.py b/tests/test_covariance.py index 618670fb6..fe55909b7 100644 --- a/tests/test_covariance.py +++ b/tests/test_covariance.py @@ -28,7 +28,7 @@ from pyrate import config as cf from pyrate import ref_phs_est as rpe from pyrate import shared -from pyrate.scripts import run_pyrate, run_prepifg +from pyrate.scripts import run_pyrate, run_prepifg, converttogtif from pyrate.covariance import cvd, get_vcmt, RDist from pyrate import ifgconstants as ifc import pyrate.orbital @@ -191,6 +191,7 @@ def setUpClass(cls): params[cf.TMPDIR] = os.path.join(cls.temp_out_dir, cf.TMPDIR) shared.mkdir_p(params[cf.TMPDIR]) params[cf.REF_EST_METHOD] = 2 + converttogtif.main(params) run_prepifg.main(params) cls.params = params xlks, ylks, crop = cf.transform_params(params) From 472696d167c083ce7186da26bfac088fc2311abe Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 03:12:15 +0000 Subject: [PATCH 037/376] WIP: debugging failure of TestGammaParallelVsSerial following restructure implementaion. --- pyrate/prepifg.py | 7 ++-- pyrate/scripts/converttogtif.py | 2 +- pyrate/scripts/run_prepifg.py | 9 +++-- pyrate/shared.py | 3 -- tests/common.py | 8 ++-- tests/test_gamma.py | 68 +++++++++++++++++++++++---------- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/pyrate/prepifg.py b/pyrate/prepifg.py index b19f4d6f6..6e8af727f 100644 --- a/pyrate/prepifg.py +++ b/pyrate/prepifg.py @@ -168,16 +168,16 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, :return: out_ds: destination gdal dataset object :rtype: gdal.Dataset """ - do_multilook = xlooks > 1 or ylooks > 1 # resolution=None completes faster for non-multilooked layers in gdalwarp resolution = [None, None] raster = dem_or_ifg(raster_path) + # print(f"prepare_ifg: raster = {raster}") - good if not raster.is_open: raster.open() if do_multilook: resolution = [xlooks * raster.x_step, ylooks * raster.y_step] - + # print(f"prepaire_ifg: raster post multilook = {raster}") - good if not do_multilook and crop_opt == ALREADY_SAME_SIZE: renamed_path = \ cf.mlooked_path(raster.data_path, looks=xlooks, crop_out=crop_opt) @@ -235,7 +235,7 @@ def dem_or_ifg(data_path): """ ds = gdal.Open(data_path) md = ds.GetMetadata() - if 'DATE' in md: # ifg + if ifc.MASTER_DATE in md: # ifg return Ifg(data_path) else: return DEM(data_path) @@ -281,7 +281,6 @@ def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, # cut, average, resample the final output layers op = output_tiff_filename(ifg.data_path, out_path) looks_path = cf.mlooked_path(op, y_looks, crop_out) - print(looks_path) # # Add missing/updated metadata to resampled ifg/DEM # new_lyr = type(ifg)(looks_path) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index 441537491..c49d679f3 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -79,6 +79,7 @@ def main(params=None): np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] gtiff_paths = do_geotiff(process_base_ifgs_paths, params) log.info("Finished converttogtif") + return gtiff_paths def do_geotiff(base_unw_paths, params): @@ -102,7 +103,6 @@ def do_geotiff(base_unw_paths, params): log.info("Running geotiff conversion in serial") dest_base_ifgs = [_geotiff_multiprocessing(b, params) for b in base_unw_paths] - return dest_base_ifgs diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 10f6ee739..ae2cf3d62 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -52,13 +52,13 @@ def main(params=None): # the important pyrate stuff anyway, but might affect gen_thumbs.py. # Going to assume base_ifg_paths is ordered correcly # pylint: disable=too-many-branches - usage = 'Usage: pyrate prepifg ' if mpiops.size > 1: # Over-ride input options if this is an MPI job params[cf.PARALLEL] = False if params: - base_ifg_paths = cf.original_ifg_paths(params[cf.IFG_FILE_LIST]) + base_ifg_paths = cf.original_ifg_paths(params[cf.IFG_FILE_LIST]) + # print(f"run_prepifg: first base ifg path = {base_ifg_paths[0]}") else: # if params not provided read from config file if (not params) and (len(sys.argv) < 3): @@ -82,6 +82,7 @@ def main(params=None): np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] gtiff_paths = [shared.output_tiff_filename(f, \ params[cf.OBS_DIR]) for f in process_base_ifgs_paths] + # print(f"run_prepifg: first gtiff path = {gtiff_paths[0]}") do_prepifg(gtiff_paths, params) log.info("Finished prepifg") @@ -99,6 +100,7 @@ def do_prepifg(gtiff_paths, params): if all([os.path.isfile(f) for f in gtiff_paths]): ifgs = [prepifg.dem_or_ifg(p) for p in gtiff_paths] + # print(f"do_prepifg: first ifg = {ifgs[0]}") xlooks, ylooks, crop = cf.transform_params(params) user_exts = (params[cf.IFG_XFIRST], params[cf.IFG_YFIRST], params[cf.IFG_XLAST], params[cf.IFG_YLAST]) @@ -126,9 +128,10 @@ def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): elif processor == ROIPAC: header = roipac.roipac_header(path, params) else: - raise PreprocessError('Processor must be ROI_PAC (0) or ' + raise PreprocessError('Processor must be ROI_PAC (0) or ' 'GAMMA (1)') + # print(f"_prepifg_multiprocessing: path = {path}") prepifg.prepare_ifg(path, xlooks, ylooks, exts, thresh, crop, out_path=params[cf.OUT_DIR], header=header) diff --git a/pyrate/shared.py b/pyrate/shared.py index 92b3bdcc4..b7678d8b4 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -305,7 +305,6 @@ def _to_date(datestr): return date(year, month, day) md = self.dataset.GetMetadata() - datestrs = [md[k] for k in [ifc.MASTER_DATE, ifc.SLAVE_DATE]] if all(datestrs): @@ -1086,8 +1085,6 @@ def cell_size(lat, lon, x_step, y_step): zone = _utm_zone(lon) p0 = pyproj.Proj(proj='latlong', ellps='WGS84') p1 = pyproj.Proj(proj='utm', zone=zone, ellps='WGS84') - assert p0.is_latlong() - assert not p1.is_latlong() x0, y0 = pyproj.transform(p0, p1, lon, lat) x1, y1 = pyproj.transform(p0, p1, lon + x_step, lat + y_step) diff --git a/tests/common.py b/tests/common.py index 785d6d8dd..dbae74de2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -30,10 +30,10 @@ from numpy import isnan, sum as nsum from osgeo import gdal -from pyrate import config as cf, mst, timeseries, matlab_mst, algorithm, \ - ifgconstants as ifc, linrate -from pyrate.shared import Ifg, nan_and_mm_convert, get_geotiff_header_info, \ - write_output_geotiff +from pyrate import (config as cf, mst, timeseries, matlab_mst, algorithm, + ifgconstants as ifc, linrate) +from pyrate.shared import (Ifg, nan_and_mm_convert, get_geotiff_header_info, + write_output_geotiff) from tests.constants import PYRATEPATH diff --git a/tests/test_gamma.py b/tests/test_gamma.py index 4a19b4c27..f742e0f6a 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -49,8 +49,7 @@ DEM_FILE, APS_INCIDENCE_MAP, APS_ELEVATION_MAP) -from pyrate.scripts import run_prepifg -from pyrate.scripts.converttogtif import main as gammaMain +from pyrate.scripts import run_prepifg, converttogtif from pyrate.shared import write_fullres_geotiff, GeotiffException from tests import common from tests.common import GAMMA_TEST_DIR, SML_TEST_GAMMA @@ -300,57 +299,86 @@ def test_fail_bad_date_order(self): class TestGammaParallelVsSerial(unittest.TestCase): - + """ + Test Gamma prepifg produces correct results when run in serial and + parallel and that metadata is correctly set by both methods. These tests + exclude the comparison of DEM files. + """ @classmethod def setUpClass(cls): - + import pprint cls.serial_dir = tempfile.mkdtemp() cls.parallel_dir = tempfile.mkdtemp() - unw_paths = glob.glob(os.path.join(SML_TEST_GAMMA, "*_utm.unw")) # read in the params _, _, params = cf.get_ifg_paths(TEST_CONF_GAMMA) + # SERIAL params[cf.OUT_DIR] = cls.serial_dir params[cf.PARALLEL] = False - shared.mkdir_p(cls.serial_dir) - run_prepifg.gamma_prepifg(unw_paths, params) + gtif_paths = converttogtif.main(params) + # print(f"first serial gtif = {gtif_paths[0]}") + run_prepifg.main(params) + + serial_df = glob.glob(os.path.join(cls.serial_dir, "*utm_uwn_1rlks_1cr.tif")) + cls.serial_ifgs = small_data_setup(datafiles=serial_df) + print("SERIAL IFGS") + pprint.pprint(cls.serial_ifgs) + + # Clean up serial converted tifs so we can test parallel conversion + tifs = glob.glob(os.path.join(SML_TEST_GAMMA, '*.tif')) + for tif in tifs: + os.remove(tif) + + # PARALLEL params[cf.OUT_DIR] = cls.parallel_dir params[cf.PARALLEL] = True shared.mkdir_p(cls.parallel_dir) - run_prepifg.gamma_prepifg(unw_paths, params) + + gtif_paths = converttogtif.main(params) + # print(f"first para gtif = {gtif_paths[0]}") + run_prepifg.main(params) + + para_df = glob.glob(os.path.join(cls.parallel_dir, "*utm_unw_1rlks_1cr.tif")) + cls.para_ifgs = small_data_setup(datafiles=para_df) + print("PARA IFGS") + pprint.pprint(cls.para_ifgs) @classmethod def tearDownClass(cls): shutil.rmtree(cls.parallel_dir) shutil.rmtree(cls.serial_dir) + tifs = glob.glob(os.path.join(SML_TEST_GAMMA, '*.tif')) + for tif in tifs: + os.remove(tif) def test_equality(self): - serial_ifgs = small_data_setup( - datafiles=glob.glob(os.path.join(self.serial_dir, "*_1cr.tif"))) + #Exclude DEM from comparison + #serial_df = glob.glob(os.path.join(self.serial_dir, "*_1cr.tif")) + #serial_df = [df for df in datafiles if 'dem' not in df] + #serial_ifgs = small_data_setup(datafiles=serial_df) - parallel_ifgs = small_data_setup( - datafiles=glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif"))) + #para_df = glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif")) + #datafiles = [df for df in datafiles if 'dem' not in df] + #parallel_ifgs = small_data_setup(datafiles=datafiles) - for s, p in zip(serial_ifgs, parallel_ifgs): + for s, p in zip(self.serial_ifgs, self.para_ifgs): np.testing.assert_array_almost_equal(s.phase_data, p.phase_data) def test_meta_data_exist(self): - serial_ifgs = small_data_setup( - datafiles=glob.glob(os.path.join(self.serial_dir, "*_1cr.tif"))) + #serial_ifgs = small_data_setup( + # datafiles=glob.glob(os.path.join(self.serial_dir, "*_1cr.tif"))) - parallel_ifgs = small_data_setup( - datafiles=glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif"))) - for s, p in zip(serial_ifgs, parallel_ifgs): + #parallel_ifgs = small_data_setup( + # datafiles=glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif"))) + for s, p in zip(self.serial_ifgs, self.para_ifgs): # all metadata equal self.assertDictEqual(s.meta_data, p.meta_data) - # test that DATA_TYPE exists in metadata self.assertIn(ifc.DATA_TYPE, s.meta_data.keys()) - # test that DATA_TYPE is MULTILOOKED self.assertEqual(s.meta_data[ifc.DATA_TYPE], ifc.MULTILOOKED) From 0eaf80b403b1b63167ea85568f8f3e0d2869eaa5 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 03:40:28 +0000 Subject: [PATCH 038/376] Fix typo in test_gamma and remove debug prints. --- pyrate/prepifg.py | 3 --- pyrate/scripts/run_prepifg.py | 4 ---- tests/test_gamma.py | 32 +++++--------------------------- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/pyrate/prepifg.py b/pyrate/prepifg.py index 6e8af727f..28e8d2436 100644 --- a/pyrate/prepifg.py +++ b/pyrate/prepifg.py @@ -172,12 +172,10 @@ def prepare_ifg(raster_path, xlooks, ylooks, exts, thresh, crop_opt, # resolution=None completes faster for non-multilooked layers in gdalwarp resolution = [None, None] raster = dem_or_ifg(raster_path) - # print(f"prepare_ifg: raster = {raster}") - good if not raster.is_open: raster.open() if do_multilook: resolution = [xlooks * raster.x_step, ylooks * raster.y_step] - # print(f"prepaire_ifg: raster post multilook = {raster}") - good if not do_multilook and crop_opt == ALREADY_SAME_SIZE: renamed_path = \ cf.mlooked_path(raster.data_path, looks=xlooks, crop_out=crop_opt) @@ -299,7 +297,6 @@ def _warp(ifg, x_looks, y_looks, extents, resolution, thresh, crop_out, output_file=looks_path, thresh=thresh, out_driver_type=driver_type, hdr=header) - if not write_to_disk: return resampled_data, out_ds diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index ae2cf3d62..fba314248 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -58,7 +58,6 @@ def main(params=None): if params: base_ifg_paths = cf.original_ifg_paths(params[cf.IFG_FILE_LIST]) - # print(f"run_prepifg: first base ifg path = {base_ifg_paths[0]}") else: # if params not provided read from config file if (not params) and (len(sys.argv) < 3): @@ -82,7 +81,6 @@ def main(params=None): np.array_split(base_ifg_paths, mpiops.size)[mpiops.rank] gtiff_paths = [shared.output_tiff_filename(f, \ params[cf.OBS_DIR]) for f in process_base_ifgs_paths] - # print(f"run_prepifg: first gtiff path = {gtiff_paths[0]}") do_prepifg(gtiff_paths, params) log.info("Finished prepifg") @@ -100,7 +98,6 @@ def do_prepifg(gtiff_paths, params): if all([os.path.isfile(f) for f in gtiff_paths]): ifgs = [prepifg.dem_or_ifg(p) for p in gtiff_paths] - # print(f"do_prepifg: first ifg = {ifgs[0]}") xlooks, ylooks, crop = cf.transform_params(params) user_exts = (params[cf.IFG_XFIRST], params[cf.IFG_YFIRST], params[cf.IFG_XLAST], params[cf.IFG_YLAST]) @@ -131,7 +128,6 @@ def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): raise PreprocessError('Processor must be ROI_PAC (0) or ' 'GAMMA (1)') - # print(f"_prepifg_multiprocessing: path = {path}") prepifg.prepare_ifg(path, xlooks, ylooks, exts, thresh, crop, out_path=params[cf.OUT_DIR], header=header) diff --git a/tests/test_gamma.py b/tests/test_gamma.py index f742e0f6a..9e3518275 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -306,26 +306,21 @@ class TestGammaParallelVsSerial(unittest.TestCase): """ @classmethod def setUpClass(cls): - import pprint - cls.serial_dir = tempfile.mkdtemp() - cls.parallel_dir = tempfile.mkdtemp() - # read in the params _, _, params = cf.get_ifg_paths(TEST_CONF_GAMMA) + glob_prefix = "*utm_unw_1rlks_1cr.tif" # SERIAL + cls.serial_dir = tempfile.mkdtemp() params[cf.OUT_DIR] = cls.serial_dir params[cf.PARALLEL] = False shared.mkdir_p(cls.serial_dir) gtif_paths = converttogtif.main(params) - # print(f"first serial gtif = {gtif_paths[0]}") run_prepifg.main(params) - serial_df = glob.glob(os.path.join(cls.serial_dir, "*utm_uwn_1rlks_1cr.tif")) + serial_df = glob.glob(os.path.join(cls.serial_dir, glob_prefix)) cls.serial_ifgs = small_data_setup(datafiles=serial_df) - print("SERIAL IFGS") - pprint.pprint(cls.serial_ifgs) # Clean up serial converted tifs so we can test parallel conversion tifs = glob.glob(os.path.join(SML_TEST_GAMMA, '*.tif')) @@ -333,18 +328,16 @@ def setUpClass(cls): os.remove(tif) # PARALLEL + cls.parallel_dir = tempfile.mkdtemp() params[cf.OUT_DIR] = cls.parallel_dir params[cf.PARALLEL] = True shared.mkdir_p(cls.parallel_dir) gtif_paths = converttogtif.main(params) - # print(f"first para gtif = {gtif_paths[0]}") run_prepifg.main(params) - para_df = glob.glob(os.path.join(cls.parallel_dir, "*utm_unw_1rlks_1cr.tif")) + para_df = glob.glob(os.path.join(cls.parallel_dir, glob_prefix)) cls.para_ifgs = small_data_setup(datafiles=para_df) - print("PARA IFGS") - pprint.pprint(cls.para_ifgs) @classmethod def tearDownClass(cls): @@ -355,25 +348,10 @@ def tearDownClass(cls): os.remove(tif) def test_equality(self): - #Exclude DEM from comparison - #serial_df = glob.glob(os.path.join(self.serial_dir, "*_1cr.tif")) - #serial_df = [df for df in datafiles if 'dem' not in df] - #serial_ifgs = small_data_setup(datafiles=serial_df) - - #para_df = glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif")) - #datafiles = [df for df in datafiles if 'dem' not in df] - #parallel_ifgs = small_data_setup(datafiles=datafiles) - for s, p in zip(self.serial_ifgs, self.para_ifgs): np.testing.assert_array_almost_equal(s.phase_data, p.phase_data) def test_meta_data_exist(self): - #serial_ifgs = small_data_setup( - # datafiles=glob.glob(os.path.join(self.serial_dir, "*_1cr.tif"))) - - #parallel_ifgs = small_data_setup( - # datafiles=glob.glob(os.path.join(self.parallel_dir, "*_1cr.tif"))) - for s, p in zip(self.serial_ifgs, self.para_ifgs): # all metadata equal self.assertDictEqual(s.meta_data, p.meta_data) From 48f8ae161957c46e826b1c320e65a527a0b06c86 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 04:50:09 +0000 Subject: [PATCH 039/376] Update test for converttogeotiff implementation. --- tests/test_gamma_vs_roipac.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/test_gamma_vs_roipac.py b/tests/test_gamma_vs_roipac.py index 2f1673632..fee15c616 100644 --- a/tests/test_gamma_vs_roipac.py +++ b/tests/test_gamma_vs_roipac.py @@ -48,7 +48,7 @@ APS_INCIDENCE_MAP, APS_ELEVATION_MAP ) -from pyrate.scripts import run_prepifg +from pyrate.scripts import run_prepifg, converttogtif from tests.common import SML_TEST_DIR from tests.common import small_data_setup @@ -62,26 +62,33 @@ class TestGammaVsRoipacEquality(unittest.TestCase): @classmethod def setUpClass(cls): cls.gamma_base_dir = tempfile.mkdtemp() - cls.gamma_conffile = os.path.join(cls.gamma_base_dir, 'gamma_test.conf') + cls.gamma_conffile = os.path.join(cls.gamma_base_dir, + 'gamma_test.conf') cls.gamma_ifgListFile = os.path.join(cls.gamma_base_dir, - 'gamma_ifg.list') + 'gamma_ifg.list') cls.roipac_base_dir = tempfile.mkdtemp() cls.roipac_conffile = os.path.join(cls.roipac_base_dir, - 'roipac_test.conf') + 'roipac_test.conf') cls.roipac_ifgListFile = os.path.join(cls.roipac_base_dir, - 'roipac_ifg.list') + 'roipac_ifg.list') @classmethod def tearDownClass(cls): shutil.rmtree(cls.gamma_base_dir) shutil.rmtree(cls.roipac_base_dir) + tifs = glob.glob(os.path.join(cls.SMLNEY_GAMMA_TEST, '*.tif')) + for tif in tifs: + os.remove(tif) + tifs = glob.glob(os.path.join(common.SML_TEST_OBS, '*.tif')) + for tif in tifs: + os.remove(tif) def make_gamma_input_files(self, data): with open(self.conf_file, 'w') as conf: conf.write('[{}]\n'.format(DUMMY_SECTION_NAME)) conf.write('{}: {}\n'.format(NO_DATA_VALUE, '0.0')) - conf.write('{}: {}\n'.format(OBS_DIR, self.base_dir)) + conf.write('{}: {}\n'.format(OBS_DIR, self.SMLNEY_GAMMA_TEST)) conf.write('{}: {}\n'.format(OUT_DIR, self.base_dir)) conf.write('{}: {}\n'.format(IFG_FILE_LIST, self.ifgListFile)) conf.write('{}: {}\n'.format(PROCESSOR, '1')) @@ -115,12 +122,14 @@ def check_gamma(self, conf_file): base_ifg_paths, dest_paths, params = cf.get_ifg_paths(conf_file) dest_base_ifgs = [os.path.join( - params[cf.OUT_DIR], os.path.basename(q).split('.')[0] + '_' + + params[cf.OBS_DIR], os.path.basename(q).split('.')[0] + '_' + os.path.basename(q).split('.')[1] + '.tif') for q in base_ifg_paths] + sys.argv = ['pyrate', 'converttogeotiff', conf_file] + converttogtif.main() sys.argv = ['pyrate', 'prepifg', conf_file] run_prepifg.main() - + for p, q in zip(dest_base_ifgs, dest_paths): self.assertTrue(os.path.exists(p), '{} does not exist'.format(p)) @@ -132,7 +141,7 @@ def make_roipac_input_files(self, data, projection): conf.write('[{}]\n'.format(DUMMY_SECTION_NAME)) conf.write('{}: {}\n'.format(INPUT_IFG_PROJECTION, projection)) conf.write('{}: {}\n'.format(NO_DATA_VALUE, '0.0')) - conf.write('{}: {}\n'.format(OBS_DIR, self.base_dir)) + conf.write('{}: {}\n'.format(OBS_DIR, common.SML_TEST_OBS)) conf.write('{}: {}\n'.format(OUT_DIR, self.base_dir)) conf.write('{}: {}\n'.format(IFG_FILE_LIST, self.ifgListFile)) conf.write('{}: {}\n'.format(PROCESSOR, '0')) @@ -151,7 +160,7 @@ def make_roipac_input_files(self, data, projection): def test_cmd_ifg_no_roipac_files_created_roipac(self): self.dataPaths = common.small_data_roipac_unws() base_exp = common.small_ifg_file_list() - self.expPaths = [os.path.join(self.roipac_base_dir, os.path.basename(i)) + self.expPaths = [os.path.join(common.SML_TEST_OBS, os.path.basename(i)) for i in base_exp] self.confFile = self.roipac_conffile self.ifgListFile = self.roipac_ifgListFile @@ -160,6 +169,8 @@ def test_cmd_ifg_no_roipac_files_created_roipac(self): def check_roipac(self): self.make_roipac_input_files(self.dataPaths, 'WGS84') + sys.argv = ['pyrate', 'converttogeotiff', self.confFile] + converttogtif.main() sys.argv = ['pyrate', 'prepifg', self.confFile] run_prepifg.main() for path in self.expPaths: From 105837c14e70ed0b387ba7114c7168b1e450aa56 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 06:01:02 +0000 Subject: [PATCH 040/376] Fix hardcoded gdal driver in crop_resample_average(). --- pyrate/gdal_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/gdal_python.py b/pyrate/gdal_python.py index 204cc1e18..6c8d5ba05 100644 --- a/pyrate/gdal_python.py +++ b/pyrate/gdal_python.py @@ -310,7 +310,7 @@ def crop_resample_average( raise TypeError('Data Type metadata not recognised') out_ds = shared.gdal_dataset(output_file, dst_ds.RasterXSize, dst_ds.RasterYSize, - driver="GTiff", bands=1, dtype=src_dtype, metadata=md, crs=wkt, + driver=out_driver_type, bands=1, dtype=src_dtype, metadata=md, crs=wkt, geotransform=gt, creation_opts=['compress=packbits']) shared.write_geotiff(resampled_average, out_ds, np.nan) From 31657201469e93c661182fd8ad1c35357a4d11ca Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 06:25:09 +0000 Subject: [PATCH 041/376] Convert tifs before running prepifg --- tests/test_linrate.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_linrate.py b/tests/test_linrate.py index fa66ef78a..1d5700095 100644 --- a/tests/test_linrate.py +++ b/tests/test_linrate.py @@ -22,10 +22,11 @@ import sys import tempfile import unittest -from numpy import eye, array, ones +from numpy import eye, array, ones import numpy as np from numpy.testing import assert_array_almost_equal +import glob import pyrate.orbital import tests.common @@ -34,7 +35,7 @@ from pyrate import shared from pyrate import covariance as vcm_module from pyrate.linrate import linear_rate -from pyrate.scripts import run_pyrate, run_prepifg +from pyrate.scripts import run_pyrate, run_prepifg, converttogtif from tests.common import SML_TEST_DIR, prepare_ifgs_without_phase from tests.common import TEST_CONF_ROIPAC, pre_prepare_ifgs @@ -85,11 +86,11 @@ class MatlabEqualityTest(unittest.TestCase): def setUpClass(cls): params = cf.get_config_params(TEST_CONF_ROIPAC) cls.temp_out_dir = tempfile.mkdtemp() - - sys.argv = ['run_prepifg.py', TEST_CONF_ROIPAC] + params[cf.OUT_DIR] = cls.temp_out_dir params[cf.TMPDIR] = os.path.join(params[cf.OUT_DIR], cf.TMPDIR) shared.mkdir_p(params[cf.TMPDIR]) + converttogtif.main(params) run_prepifg.main(params) params[cf.REF_EST_METHOD] = 2 @@ -98,9 +99,10 @@ def setUpClass(cls): base_ifg_paths = cf.original_ifg_paths( params[cf.IFG_FILE_LIST]) - + dest_paths = cf.get_dest_paths(base_ifg_paths, crop, params, xlks) - + print(f"base_ifg_paths={base_ifg_paths}") + print(f"dest_paths={dest_paths}") # start run_pyrate copy ifgs = pre_prepare_ifgs(dest_paths, params) mst_grid = tests.common.mst_calculation(dest_paths, params) @@ -145,6 +147,10 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + params = cf.get_config_params(TEST_CONF_ROIPAC) + tifs = glob.glob(os.path.join(params[cf.OBS_DIR], '*.tif')) + for tif in tifs: + os.remove(tif) def test_linear_rate_full_parallel(self): """ From 95379ee7a7445e40f5e0306fb7c82b13cf1ac923 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 07:08:17 +0000 Subject: [PATCH 042/376] WIP: debugging orbital error test. --- pyrate/orbital.py | 2 +- pyrate/shared.py | 1 + tests/common.py | 4 ++++ tests/test_mpi.py | 15 +++++++++------ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/pyrate/orbital.py b/pyrate/orbital.py index ea648b8b5..d540b76dc 100644 --- a/pyrate/orbital.py +++ b/pyrate/orbital.py @@ -90,7 +90,7 @@ def remove_orbital_error(ifgs, params, preread_ifgs=None): if isinstance(ifgs[0], Ifg) else ifgs mlooked = None - + print(f"obs dir = {params[cf.OBS_DIR]}") # mlooking is not necessary for independent correction # can use multiple procesing if write_to_disc=True if params[cf.ORBITAL_FIT_METHOD] == NETWORK_METHOD: diff --git a/pyrate/shared.py b/pyrate/shared.py index b7678d8b4..8aa613047 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -1218,6 +1218,7 @@ def output_tiff_filename(inpath, outpath): """ fname, ext = os.path.basename(inpath).split('.') if ext == 'tif': + print(inpath, outpath) name = os.path.join(outpath, fname + '.tif') else: name = os.path.join(outpath, fname + '_' + ext + '.tif') diff --git a/tests/common.py b/tests/common.py index dbae74de2..5f9aa5f54 100644 --- a/tests/common.py +++ b/tests/common.py @@ -106,6 +106,10 @@ log = logging.getLogger(__name__) +def remove_tifs(path): + tifs = glob.glob(os.path.join(path, '*.tif')) + for tif in tifs: + os.remove(tif) def small_data_setup(datafiles=None, is_dir=False): """Returns Ifg objs for the files in the small test dir diff --git a/tests/test_mpi.py b/tests/test_mpi.py index 1ea63e5c4..a2f7030bb 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -34,9 +34,10 @@ from pyrate import ref_phs_est as rpe from pyrate import covariance from pyrate import refpixel -from pyrate.scripts import run_pyrate, run_prepifg, postprocessing -from tests.common import small_data_setup, reconstruct_mst, \ - reconstruct_linrate, SML_TEST_DEM_HDR_GAMMA, pre_prepare_ifgs +from pyrate.scripts import ( + run_pyrate, run_prepifg, postprocessing, converttogtif) +from tests.common import (small_data_setup, reconstruct_mst, + reconstruct_linrate, SML_TEST_DEM_HDR_GAMMA, pre_prepare_ifgs) from tests import common from tests.test_covariance import matlab_maxvar from pyrate import config as cf @@ -164,7 +165,8 @@ def test_vcm_matlab_vs_mpi(mpisync, tempdir, get_config): # run prepifg, create the dest_paths files if mpiops.rank == 0: - run_prepifg.roipac_prepifg(base_unw_paths, params_dict) + converttogtif.main(params_dict) + run_prepifg.main(params_dict) mpiops.comm.barrier() @@ -182,6 +184,7 @@ def test_vcm_matlab_vs_mpi(mpisync, tempdir, get_config): np.testing.assert_array_almost_equal(matlab_vcm, vcmt, decimal=3) if mpiops.rank == 0: shutil.rmtree(outdir) + common.remove_tifs(params_dict[cf.OBS_DIR]) @pytest.fixture(params=[1, 2, 5]) @@ -368,10 +371,10 @@ def test_prepifg_mpi(mpisync, get_config, tempdir, if roipac_or_gamma == 1: base_unw_paths = glob.glob(join(common.SML_TEST_GAMMA, "*_utm.unw")) - run_prepifg.gamma_prepifg(base_unw_paths, params_s) + run_prepifg.main(params) else: base_unw_paths = glob.glob(join(common.SML_TEST_OBS, "*.unw")) - run_prepifg.roipac_prepifg(base_unw_paths, params_s) + run_prepifg.main(params_s) mpi_tifs = glob.glob(join(outdir, "*.tif")) serial_tifs = glob.glob(join(params[cf.OUT_DIR], "*.tif")) From c6ea83076a2504050bede8c98af989c39b915c0d Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Thu, 1 Aug 2019 23:48:02 +0000 Subject: [PATCH 043/376] Handle case that outpath is none. --- pyrate/shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/shared.py b/pyrate/shared.py index 8aa613047..6369ce333 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -1217,8 +1217,8 @@ def output_tiff_filename(inpath, outpath): :rtype: str """ fname, ext = os.path.basename(inpath).split('.') + outpath = inpath if outpath is None else outpath if ext == 'tif': - print(inpath, outpath) name = os.path.join(outpath, fname + '.tif') else: name = os.path.join(outpath, fname + '_' + ext + '.tif') From 5849bc192cbeb7c9ccff1e5bc5bb13ddd4a26ec4 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 01:12:24 +0000 Subject: [PATCH 044/376] Only attempt to collate metadata if header is provided --- pyrate/gdal_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/gdal_python.py b/pyrate/gdal_python.py index 6c8d5ba05..eb66de2f9 100644 --- a/pyrate/gdal_python.py +++ b/pyrate/gdal_python.py @@ -289,7 +289,7 @@ def crop_resample_average( wkt = dst_ds.GetProjection() # TEST HERE IF EXISTING FILE HAS PYRATE METADATA. IF NOT ADD HERE - if not ifc.DATA_TYPE in dst_ds.GetMetadata(): + if not ifc.DATA_TYPE in dst_ds.GetMetadata() and hdr is not None: md = shared.collate_metadata(hdr) else: md = dst_ds.GetMetadata() From d0a3455a6fb311bd7709921321638a3a016f7954 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 01:13:03 +0000 Subject: [PATCH 045/376] Use inpath directory, not full path --- pyrate/shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/shared.py b/pyrate/shared.py index 6369ce333..8d7b603b6 100644 --- a/pyrate/shared.py +++ b/pyrate/shared.py @@ -1217,7 +1217,7 @@ def output_tiff_filename(inpath, outpath): :rtype: str """ fname, ext = os.path.basename(inpath).split('.') - outpath = inpath if outpath is None else outpath + outpath = os.path.dirname(inpath) if outpath is None else outpath if ext == 'tif': name = os.path.join(outpath, fname + '.tif') else: From 9923b6b07ae364e280021b2111b90dcded41a8a9 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 01:13:22 +0000 Subject: [PATCH 046/376] Check OBS_DIR for converted tifs --- tests/test_prepifg.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_prepifg.py b/tests/test_prepifg.py index 84c959724..052b2e001 100644 --- a/tests/test_prepifg.py +++ b/tests/test_prepifg.py @@ -31,7 +31,7 @@ from osgeo import gdal -from pyrate.scripts import run_prepifg +from pyrate.scripts import run_prepifg, converttogtif from pyrate import config as cf from pyrate.config import mlooked_path from pyrate.shared import Ifg, DEM @@ -670,7 +670,9 @@ def setUp(self): self.ifgListFile = os.path.join(common.SML_TEST_GAMMA, 'ifms_17') def tearDown(self): + params = cf.get_config_params(self.conf_file) shutil.rmtree(self.base_dir) + common.remove_tifs(params[cf.OBS_DIR]) def make_input_files(self, inc='', ele=''): with open(self.conf_file, 'w') as conf: @@ -709,16 +711,17 @@ def test_only_ele_file_created(self): def common_check(self, ele, inc): os.path.exists(self.conf_file) params = cf.get_config_params(self.conf_file) + converttogtif.main(params) sys.argv = ['dummy', self.conf_file] run_prepifg.main(params) # test 17 geotiffs created - geotifs = glob.glob(os.path.join(self.base_dir, '*_unw.tif')) + geotifs = glob.glob(os.path.join(params[cf.OBS_DIR], '*_unw.tif')) self.assertEqual(17, len(geotifs)) # test dem geotiff created - demtif = glob.glob(os.path.join(self.base_dir, '*_dem.tif')) + demtif = glob.glob(os.path.join(params[cf.OBS_DIR], '*_dem.tif')) self.assertEqual(1, len(demtif)) # elevation/incidence file - ele = glob.glob(os.path.join(self.base_dir, + ele = glob.glob(os.path.join(params[cf.OBS_DIR], '*utm_{ele}.tif'.format(ele=ele)))[0] self.assertTrue(os.path.exists(ele)) # mlooked tifs From a508922fc38dc0889b1f4d961faec5b99e0bfe0d Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 01:37:19 +0000 Subject: [PATCH 047/376] Convert tif before prepifg. --- tests/test_pyrate.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_pyrate.py b/tests/test_pyrate.py index bf0ad054b..df7732e6f 100644 --- a/tests/test_pyrate.py +++ b/tests/test_pyrate.py @@ -28,7 +28,7 @@ import pyrate.shared from pyrate import config as cf from pyrate import shared, config, prepifg -from pyrate.scripts import run_pyrate, run_prepifg +from pyrate.scripts import run_pyrate, run_prepifg, converttogtif from tests import common # taken from @@ -199,19 +199,21 @@ def setUpClass(cls): params[cf.IFG_FILE_LIST] = os.path.join( common.SML_TEST_GAMMA, 'ifms_17') params[cf.OUT_DIR] = cls.tif_dir - params[cf.PARALLEL] = 0 + params[cf.PARALLEL] = 1 params[cf.APS_CORRECTION] = False params[cf.TMPDIR] = os.path.join(params[cf.OUT_DIR], cf.TMPDIR) xlks, ylks, crop = cf.transform_params(params) - # base_unw_paths need to be geotiffed and multilooked by run_prepifg + # base_unw_paths need to be geotiffed by converttogeotif + # and multilooked by run_prepifg base_unw_paths = cf.original_ifg_paths(params[cf.IFG_FILE_LIST]) # dest_paths are tifs that have been geotif converted and multilooked cls.dest_paths = cf.get_dest_paths( base_unw_paths, crop, params, xlks) - run_prepifg.gamma_prepifg(base_unw_paths, params) + gtif_paths = converttogtif.do_geotiff(base_unw_paths, params) + run_prepifg.do_prepifg(gtif_paths, params) tiles = pyrate.shared.get_tiles(cls.dest_paths[0], 3, 3) ifgs = common.small_data_setup() cls.refpixel_p, cls.maxvar_p, cls.vcmt_p = \ @@ -223,6 +225,8 @@ def setUpClass(cls): ifgs[0].shape, tiles, params[cf.TMPDIR], t) for t in rate_types ] + + common.remove_tifs(params[cf.OBS_DIR]) # now create the non parallel version cls.tif_dir_s = tempfile.mkdtemp() @@ -231,7 +235,8 @@ def setUpClass(cls): params[cf.TMPDIR] = os.path.join(params[cf.OUT_DIR], cf.TMPDIR) cls.dest_paths_s = cf.get_dest_paths( base_unw_paths, crop, params, xlks) - run_prepifg.gamma_prepifg(base_unw_paths, params) + gtif_paths = converttogtif.do_geotiff(base_unw_paths, params) + run_prepifg.do_prepifg(gtif_paths, params) cls.refpixel, cls.maxvar, cls.vcmt = \ run_pyrate.process_ifgs(cls.dest_paths_s, params, 3, 3) @@ -247,6 +252,7 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.tif_dir, ignore_errors=True) shutil.rmtree(cls.tif_dir_s, ignore_errors=True) + common.remove_tifs(cf.get_config_params(cls.test_conf)[cf.OBS_DIR]) def test_orbital_correction(self): key = 'ORBITAL_ERROR' From dfb6d36943571cc2bc671c1429f12385c209ad3b Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 01:37:28 +0000 Subject: [PATCH 048/376] Convert tif before prepifg. --- tests/test_ref_phs_est.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_ref_phs_est.py b/tests/test_ref_phs_est.py index 3e7d6fe22..054bbd533 100644 --- a/tests/test_ref_phs_est.py +++ b/tests/test_ref_phs_est.py @@ -29,8 +29,7 @@ from pyrate import config as cf from pyrate import ifgconstants as ifc from pyrate.ref_phs_est import estimate_ref_phase, ReferencePhaseError -from pyrate.scripts import run_prepifg -from pyrate.scripts import run_pyrate +from pyrate.scripts import run_prepifg, run_pyrate, converttogtif from tests import common matlab_ref_phs_method1 = [-18.2191658020020, @@ -121,6 +120,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir + converttogtif.main(params) run_prepifg.main(params) params[cf.OUT_DIR] = cls.temp_out_dir @@ -159,6 +159,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) def test_estimate_reference_phase(self): np.testing.assert_array_almost_equal(matlab_ref_phs_method1, @@ -208,6 +210,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir + converttogtif.main(params) run_prepifg.main(params) params[cf.OUT_DIR] = cls.temp_out_dir @@ -246,6 +249,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) def test_estimate_reference_phase(self): np.testing.assert_array_almost_equal(matlab_ref_phs_method1, @@ -298,7 +303,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir - + converttogtif.main(params) run_prepifg.main(params) params[cf.OUT_DIR] = cls.temp_out_dir @@ -336,6 +341,9 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) + def test_ifgs_after_ref_phs_est(self): MATLAB_REF_PHASE_DIR = os.path.join(common.SML_TEST_DIR, @@ -388,7 +396,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir - + converttogtif.main(params) run_prepifg.main(params) params[cf.OUT_DIR] = cls.temp_out_dir @@ -424,6 +432,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) def test_ifgs_after_ref_phs_est(self): MATLAB_REF_PHASE_DIR = os.path.join(common.SML_TEST_DIR, From cdf2a770f185fd0f8babf53bd6de6297105e7e1a Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 04:33:08 +0000 Subject: [PATCH 049/376] Remove duplicate test and implement gtif conversion. --- tests/test_shared.py | 60 +++++++++++--------------------------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/tests/test_shared.py b/tests/test_shared.py index 1e81d2613..fbe003a0a 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -38,7 +38,7 @@ from pyrate import ifgconstants as ifc from pyrate import prepifg from pyrate import shared -from pyrate.scripts import run_prepifg +from pyrate.scripts import run_prepifg, converttogtif from pyrate.shared import Ifg, DEM, RasterException from pyrate.shared import cell_size, _utm_zone @@ -364,14 +364,15 @@ def setUpClass(cls): cls.base_unw_paths.append(common.SML_TEST_DEM_GAMMA) xlks, ylks, crop = cf.transform_params(cls.params) - # dest_paths are tifs that have been geotif converted and multilooked - run_prepifg.gamma_prepifg(cls.base_unw_paths, cls.params) + converttogtif.main(cls.params) + run_prepifg.main(cls.params) + # run_prepifg.gamma_prepifg(cls.base_unw_paths, cls.params) cls.base_unw_paths.pop() # removed dem as we don't want it in ifgs - dest_paths = cf.get_dest_paths( + cls.dest_paths = cf.get_dest_paths( cls.base_unw_paths, crop, cls.params, xlks) - cls.ifgs = common.small_data_setup(datafiles=dest_paths) + cls.ifgs = common.small_data_setup(datafiles=cls.dest_paths) @classmethod def tearDownClass(cls): @@ -420,13 +421,11 @@ def test_unw_contains_same_data_as_numpy_array(self): os.remove(temp_tif) os.remove(temp_unw) - def test_equality_of_unw_with_geotiff(self): - geotiffs = [os.path.join( - self.params[cf.OUT_DIR], os.path.basename(b).split('.')[0] + '_' - + os.path.basename(b).split('.')[1] + '.tif') - for b in self.base_unw_paths] + def test_multilooked_tiffs_converted_to_unw_are_same(self): + # Get multilooked geotiffs + geotiffs = self.dest_paths - # create .unws from geotiffs and make sure they read the same + # Convert back to .unw dest_unws = [] for g in geotiffs: dest_unw = os.path.join(self.params[cf.OUT_DIR], @@ -434,41 +433,12 @@ def test_equality_of_unw_with_geotiff(self): shared.write_unw_from_data_or_geotiff( geotif_or_data=g, dest_unw= dest_unw, ifg_proc=1) dest_unws.append(dest_unw) + + # Convert back to tiff + new_geotiffs = converttogtif.do_geotiff(dest_unws, self.params) - new_geotiffs = [run_prepifg._gamma_multiprocessing(b, self.params) - for b in dest_unws] - - for g, u in zip(geotiffs, new_geotiffs): - g_ds = gdal.Open(g) - u_gs = gdal.Open(u) - np.testing.assert_array_almost_equal(u_gs.ReadAsArray(), - g_ds.ReadAsArray()) - u_gs = None - g_ds = None - - def test_unws_created_are_same_as_original(self): - geotiffs = [os.path.join( - self.params[cf.OUT_DIR], os.path.basename(b).split('.')[0] + '_' - + os.path.basename(b).split('.')[1] + '.tif') - for b in self.base_unw_paths] - - new_base_unw_paths = [] - # create .unws from geotiffs and make sure they read the same - for g in geotiffs: - dest_unw = os.path.join(self.params[cf.OUT_DIR], - os.path.splitext(g)[0] + '.unw') - shared.write_unw_from_data_or_geotiff( - geotif_or_data=g, dest_unw=dest_unw, ifg_proc=1) - - # unws created - assert os.path.exists(dest_unw) - new_base_unw_paths.append(dest_unw) - - # make sure we can recovert the unws to gettiffs - new_geotiffs = [run_prepifg._gamma_multiprocessing(b, self.params) - for b in new_base_unw_paths] - - # assert data equal + # Ensure original multilooked geotiffs and + # unw back to geotiff are the same for g, u in zip(geotiffs, new_geotiffs): g_ds = gdal.Open(g) u_gs = gdal.Open(u) From 9c0d5c451d807bd6dff36a71d79ee28e7089c5a3 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 04:53:16 +0000 Subject: [PATCH 050/376] Remove converted test tifs. --- tests/test_shared.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_shared.py b/tests/test_shared.py index fbe003a0a..f6e40794f 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -379,6 +379,7 @@ def tearDownClass(cls): for i in cls.ifgs: i.close() shutil.rmtree(cls.tif_dir) + common.remove_tifs(cls.params[cf.OBS_DIR]) def test_unw_contains_same_data_as_numpy_array(self): from datetime import time From 3ef1d99db46a0ea67a871329f2d74fe1e966ec4b Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 04:53:35 +0000 Subject: [PATCH 051/376] Convert geotiffs before prepifg. --- tests/test_timeseries.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 782e1f906..658c444af 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -34,7 +34,7 @@ from pyrate import ref_phs_est as rpe from pyrate import shared from pyrate import covariance -from pyrate.scripts import run_pyrate, run_prepifg +from pyrate.scripts import run_pyrate, run_prepifg, converttogtif from pyrate.timeseries import time_series @@ -140,6 +140,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir + converttogtif.main(params) run_prepifg.main(params) params[cf.REF_EST_METHOD] = 2 @@ -196,6 +197,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) def test_time_series_equality_parallel_by_rows(self): """ @@ -249,6 +252,7 @@ def setUpClass(cls): cls.temp_out_dir = tempfile.mkdtemp() sys.argv = ['run_prepifg.py', common.TEST_CONF_ROIPAC] params[cf.OUT_DIR] = cls.temp_out_dir + converttogtif.main(params) run_prepifg.main(params) params[cf.REF_EST_METHOD] = 2 @@ -314,6 +318,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) + common.remove_tifs( + cf.get_config_params(common.TEST_CONF_ROIPAC)[cf.OBS_DIR]) def test_time_series_equality_parallel_by_rows(self): From 0bffee05348c4b1665aa8912482297c5d17711e9 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 05:14:42 +0000 Subject: [PATCH 052/376] Error message and early exit when geotiffs don't exist. --- pyrate/scripts/main.py | 2 +- pyrate/scripts/run_prepifg.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 4fa01cff5..89d4b4217 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -44,7 +44,7 @@ def converttogeotiff(config_file): """ Convert input files to geotiff """ - config_file = abspath(config_file) + config_file = os.path.abspath(config_file) params = cf.get_config_params(config_file) log.info('This job was run with the following parameters:') log.info(json.dumps(params, indent=4, sort_keys=True)) diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index fba314248..808f3acfd 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -112,8 +112,9 @@ def do_prepifg(gtiff_paths, params): [_prepifg_multiprocessing(p, xlooks, ylooks, exts, thresh, crop, params) for p in gtiff_paths] else: - log.info("Full-res geotiffs do not exist") - + log.error("Full-res geotiffs do not exist. Ensure you have" + " converted your interferograms to geotiffs.") + sys.exit() def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): """ From 77fac0913ede949d2de7b8ac52fcd42d06b304df Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Fri, 2 Aug 2019 05:22:24 +0000 Subject: [PATCH 053/376] Exit with error. --- pyrate/scripts/run_prepifg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/scripts/run_prepifg.py b/pyrate/scripts/run_prepifg.py index 808f3acfd..3556a1b7d 100644 --- a/pyrate/scripts/run_prepifg.py +++ b/pyrate/scripts/run_prepifg.py @@ -114,7 +114,7 @@ def do_prepifg(gtiff_paths, params): else: log.error("Full-res geotiffs do not exist. Ensure you have" " converted your interferograms to geotiffs.") - sys.exit() + sys.exit(1) def _prepifg_multiprocessing(path, xlooks, ylooks, exts, thresh, crop, params): """ From fb9ee46dea4d070e844573b8623192d953a480b7 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 01:22:01 +0000 Subject: [PATCH 054/376] Geotiff conversion. --- tests/test_mpi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_mpi.py b/tests/test_mpi.py index a2f7030bb..dc1244e2a 100644 --- a/tests/test_mpi.py +++ b/tests/test_mpi.py @@ -357,7 +357,9 @@ def test_prepifg_mpi(mpisync, get_config, tempdir, params[cf.OBS_DIR] = common.SML_TEST_GAMMA params[cf.DEM_FILE] = common.SML_TEST_DEM_GAMMA params[cf.DEM_HEADER_FILE] = common.SML_TEST_DEM_HDR_GAMMA + converttogtif.main(params) run_prepifg.main(params) + common.remove_tifs(params[cf.OBS_DIR]) if mpiops.rank == 0: if roipac_or_gamma == 1: @@ -368,6 +370,7 @@ def test_prepifg_mpi(mpisync, get_config, tempdir, params_s[cf.PARALLEL] = True params_s[cf.IFG_LKSX], params_s[cf.IFG_LKSY] = get_lks, get_lks params_s[cf.IFG_CROP_OPT] = get_crop + converttogtif.main(params) if roipac_or_gamma == 1: base_unw_paths = glob.glob(join(common.SML_TEST_GAMMA, "*_utm.unw")) @@ -387,3 +390,4 @@ def test_prepifg_mpi(mpisync, get_config, tempdir, shutil.rmtree(outdir) shutil.rmtree(params_s[cf.OUT_DIR]) + common.remove_tifs(params[cf.OBS_DIR]) From f19fc432a34acf47a58bf66fbe549f300621439c Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 01:30:46 +0000 Subject: [PATCH 055/376] Clean up test tifs in obs dir. --- tests/test_covariance.py | 6 ++++-- tests/test_gamma.py | 4 +--- tests/test_gamma_vs_roipac.py | 11 +++-------- tests/test_linrate.py | 8 +++----- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/tests/test_covariance.py b/tests/test_covariance.py index fe55909b7..4e7cc0721 100644 --- a/tests/test_covariance.py +++ b/tests/test_covariance.py @@ -33,8 +33,8 @@ from pyrate import ifgconstants as ifc import pyrate.orbital from tests import common -from tests.common import small5_mock_ifgs, small5_ifgs, TEST_CONF_ROIPAC -from tests.common import small_data_setup, prepare_ifgs_without_phase +from tests.common import (small5_mock_ifgs, small5_ifgs, TEST_CONF_ROIPAC, + small_data_setup, prepare_ifgs_without_phase) class CovarianceTests(unittest.TestCase): @@ -213,6 +213,8 @@ def tearDownClass(cls): for i in cls.ifgs: i.close() shutil.rmtree(cls.temp_out_dir) + params = cf.get_config_params(TEST_CONF_ROIPAC) + common.remove_tifs(params[cf.OBS_DIR]) def test_matlab_maxvar_equality_small_test_files(self): np.testing.assert_array_almost_equal(self.maxvar, matlab_maxvar, diff --git a/tests/test_gamma.py b/tests/test_gamma.py index 9e3518275..ba2358039 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -343,9 +343,7 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.parallel_dir) shutil.rmtree(cls.serial_dir) - tifs = glob.glob(os.path.join(SML_TEST_GAMMA, '*.tif')) - for tif in tifs: - os.remove(tif) + remove_tifs(SML_TEST_GAMMA) def test_equality(self): for s, p in zip(self.serial_ifgs, self.para_ifgs): diff --git a/tests/test_gamma_vs_roipac.py b/tests/test_gamma_vs_roipac.py index fee15c616..617ec7eeb 100644 --- a/tests/test_gamma_vs_roipac.py +++ b/tests/test_gamma_vs_roipac.py @@ -49,8 +49,7 @@ APS_ELEVATION_MAP ) from pyrate.scripts import run_prepifg, converttogtif -from tests.common import SML_TEST_DIR -from tests.common import small_data_setup +from tests.common import SML_TEST_DIR, small_data_setup, remove_tifs DUMMY_SECTION_NAME = 'pyrate' @@ -77,12 +76,8 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.gamma_base_dir) shutil.rmtree(cls.roipac_base_dir) - tifs = glob.glob(os.path.join(cls.SMLNEY_GAMMA_TEST, '*.tif')) - for tif in tifs: - os.remove(tif) - tifs = glob.glob(os.path.join(common.SML_TEST_OBS, '*.tif')) - for tif in tifs: - os.remove(tif) + remove_tifs(cls.SMLNEY_GAMMA_TEST) + remove_tifs(common.SML_TEST_OBS) def make_gamma_input_files(self, data): with open(self.conf_file, 'w') as conf: diff --git a/tests/test_linrate.py b/tests/test_linrate.py index 1d5700095..928e7c1da 100644 --- a/tests/test_linrate.py +++ b/tests/test_linrate.py @@ -36,8 +36,8 @@ from pyrate import covariance as vcm_module from pyrate.linrate import linear_rate from pyrate.scripts import run_pyrate, run_prepifg, converttogtif -from tests.common import SML_TEST_DIR, prepare_ifgs_without_phase -from tests.common import TEST_CONF_ROIPAC, pre_prepare_ifgs +from tests.common import (SML_TEST_DIR, prepare_ifgs_without_phase, + TEST_CONF_ROIPAC, pre_prepaire_ifgs, remove_tifs) def default_params(): @@ -148,9 +148,7 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.temp_out_dir) params = cf.get_config_params(TEST_CONF_ROIPAC) - tifs = glob.glob(os.path.join(params[cf.OBS_DIR], '*.tif')) - for tif in tifs: - os.remove(tif) + remove_tifs(params[cf.OBS_DIR]) def test_linear_rate_full_parallel(self): """ From 7364d16ad20f60173d8736bcf7605ea3e64c79fb Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 01:32:23 +0000 Subject: [PATCH 056/376] Fix typo --- tests/test_linrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_linrate.py b/tests/test_linrate.py index 928e7c1da..8d218cd09 100644 --- a/tests/test_linrate.py +++ b/tests/test_linrate.py @@ -37,7 +37,7 @@ from pyrate.linrate import linear_rate from pyrate.scripts import run_pyrate, run_prepifg, converttogtif from tests.common import (SML_TEST_DIR, prepare_ifgs_without_phase, - TEST_CONF_ROIPAC, pre_prepaire_ifgs, remove_tifs) + TEST_CONF_ROIPAC, pre_prepare_ifgs, remove_tifs) def default_params(): From 7015d8687ebf7d49a611bfa4455e1af707c28495 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 01:32:47 +0000 Subject: [PATCH 057/376] Fix typo --- tests/test_gamma.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gamma.py b/tests/test_gamma.py index ba2358039..f2a5690ab 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -343,7 +343,7 @@ def setUpClass(cls): def tearDownClass(cls): shutil.rmtree(cls.parallel_dir) shutil.rmtree(cls.serial_dir) - remove_tifs(SML_TEST_GAMMA) + common.remove_tifs(SML_TEST_GAMMA) def test_equality(self): for s, p in zip(self.serial_ifgs, self.para_ifgs): From 83e5bf01d9e8fa400a9822bdfadd5f3d6c9a0d37 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 03:46:12 +0000 Subject: [PATCH 058/376] Use remove_tifs method. --- tests/test_gamma.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_gamma.py b/tests/test_gamma.py index f2a5690ab..a8b99f5f7 100644 --- a/tests/test_gamma.py +++ b/tests/test_gamma.py @@ -323,9 +323,7 @@ def setUpClass(cls): cls.serial_ifgs = small_data_setup(datafiles=serial_df) # Clean up serial converted tifs so we can test parallel conversion - tifs = glob.glob(os.path.join(SML_TEST_GAMMA, '*.tif')) - for tif in tifs: - os.remove(tif) + common.remove_tifs(SML_TEST_GAMMA) # PARALLEL cls.parallel_dir = tempfile.mkdtemp() From 5473a32c87ed6aa20a060461d33e510fd635d20f Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 03:46:42 +0000 Subject: [PATCH 059/376] converttogtif tests --- pyrate/scripts/converttogtif.py | 5 +- tests/test_converttogtif.py | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/test_converttogtif.py diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index c49d679f3..451ea3271 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -124,9 +124,8 @@ def _geotiff_multiprocessing(unw_path, params): 'GAMMA (1)') shared.write_fullres_geotiff(header, unw_path, dest, nodata=params[cf.NO_DATA_VALUE]) + return dest else: log.info("Full-res geotiff already exists") - - return dest - + return None diff --git a/tests/test_converttogtif.py b/tests/test_converttogtif.py new file mode 100644 index 000000000..fc93507f4 --- /dev/null +++ b/tests/test_converttogtif.py @@ -0,0 +1,83 @@ +import os +import unittest +import pytest +import glob +import copy + +import pyrate.config as cf +from pyrate.scripts import converttogtif, run_prepifg +from tests import common + + +class ConvertToGeotiffTests(unittest.TestCase): + """ + Test basic functionality of converttogeotif - they get placed in the + right directory and there's the right amount. + """ + @classmethod + def setUpClass(cls): + cls.gamma_params = cf.get_config_params(common.TEST_CONF_GAMMA) + cls.roipac_params = cf.get_config_params(common.TEST_CONF_ROIPAC) + + def test_dem_and_incidence_not_converted(self): + gp_copy = copy.deepcopy(self.gamma_params) + gp_copy[cf.DEM_FILE] = None + gp_copy[cf.APS_INCIDENCE_MAP] = None + converttogtif.main(gp_copy) + inc_tif = glob.glob(os.path.join(gp_copy[cf.OBS_DIR], '*inc.tif')) + self.assertEqual(len(inc_tif), 0) + dem_tif = glob.glob(os.path.join(gp_copy[cf.OBS_DIR], '*dem.tif')) + self.assertEqual(len(dem_tif), 0) + + def test_tifs_placed_in_obs_dir(self): + # Test no tifs in obs dir + tifs = glob.glob(os.path.join(self.gamma_params[cf.OBS_DIR], '*.tif')) + self.assertEqual(len(tifs), 0) + # Test tifs in obs dir + converttogtif.main(self.gamma_params) + tifs = glob.glob(os.path.join(self.gamma_params[cf.OBS_DIR], '*.tif')) + self.assertEqual(len(tifs), 19) + + def test_num_gamma_tifs_equals_num_unws(self): + gtifs = converttogtif.main(self.gamma_params) + # 17 unws + incidence + dem + self.assertEqual(len(gtifs), 19) + + def test_num_roipac_tifs_equals_num_unws(self): + gtifs = converttogtif.main(self.roipac_params) + # 17 unws + dem + self.assertEqual(len(gtifs), 18) + + def test_conversion_not_recomputed(self): + """ + If a gtif already exists, converttogtif will return None instead + of path to the gtif. + """ + converttogtif.main(self.gamma_params) + gtifs = converttogtif.main(self.gamma_params) + self.assertTrue(all([gt is None for gt in gtifs])) + + def teardown_method(self, method): + common.remove_tifs(self.gamma_params[cf.OBS_DIR]) + common.remove_tifs(self.roipac_params[cf.OBS_DIR]) + +class PrepifgConversionTests(unittest.TestCase): + """ + Test that prepifg fails if there are no converted IFGs in the observations + directory, and that it succeeds if there are. + """ + @classmethod + def setUpClass(cls): + cls.params = cf.get_config_params(common.TEST_CONF_GAMMA) + + def test_no_tifs_exits(self): + with pytest.raises(SystemExit): + run_prepifg.main(self.params) + + def test_tifs_succeeds(self): + converttogtif.main(self.params) + run_prepifg.main(self.params) + + def teardown_method(self, method): + common.remove_tifs(self.params[cf.OBS_DIR]) + From 7d82d320d4e1cd1e36cbb77b46fd26f9fea91d70 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 03:53:38 +0000 Subject: [PATCH 060/376] Remove debug print. --- pyrate/orbital.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyrate/orbital.py b/pyrate/orbital.py index d540b76dc..c9545ef81 100644 --- a/pyrate/orbital.py +++ b/pyrate/orbital.py @@ -90,7 +90,6 @@ def remove_orbital_error(ifgs, params, preread_ifgs=None): if isinstance(ifgs[0], Ifg) else ifgs mlooked = None - print(f"obs dir = {params[cf.OBS_DIR]}") # mlooking is not necessary for independent correction # can use multiple procesing if write_to_disc=True if params[cf.ORBITAL_FIT_METHOD] == NETWORK_METHOD: From e16ffa72fb916e18f84dcdb5cabcfd258254c9b4 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Mon, 5 Aug 2019 05:00:32 +0000 Subject: [PATCH 061/376] Force numpy version --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index db511f655..bd8e01b35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ install: # command to run tests, e.g. python setup.py test script: + - pip install numpy==1.16.4 - pytest --cov-report term-missing:skip-covered --cov=pyrate tests/ # cache: From 61881979fec83fee89acc61910ce6cdeb39dbc61 Mon Sep 17 00:00:00 2001 From: sheecegardezi Date: Mon, 5 Aug 2019 15:38:03 +1000 Subject: [PATCH 062/376] Travis CI sphinx docs Integrate generation and uploading sphinx doc in Travis pipeline --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0d7cefb98..96e57583c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ branches: - /^release-.*$/ - /.*-travis$/ - master + - sheece-travis-sphinx-integration language: python @@ -37,8 +38,22 @@ install: script: - pip install numpy==1.16.4 - pytest --cov-report term-missing:skip-covered --cov=pyrate tests/ + - cd docs && make html # cache: # - apt # - pip # - $HOME/.cache/pip +deploy: + provider: pages + skip-cleanup: true + keep-history: true + verbose: true + on: + branch: sheece-travis-sphinx-integration + github-token: $GITHUB_TOKEN + local-dir: docs/_build/html + project_name: PyRate + email: insar@ga.gov.au + name: InSAR Team + From c822221d038041f3512602053007514628ca4f30 Mon Sep 17 00:00:00 2001 From: sheecegardezi Date: Mon, 5 Aug 2019 15:59:58 +1000 Subject: [PATCH 063/376] test staging --- .travis.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 96e57583c..e5be2eff8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,16 +44,20 @@ script: # - apt # - pip # - $HOME/.cache/pip -deploy: - provider: pages - skip-cleanup: true - keep-history: true - verbose: true - on: - branch: sheece-travis-sphinx-integration - github-token: $GITHUB_TOKEN - local-dir: docs/_build/html - project_name: PyRate - email: insar@ga.gov.au - name: InSAR Team +jobs: + include: + - stage: deploy + python: 3.6 + deploy: + provider: pages + skip-cleanup: true + keep-history: true + verbose: true + on: + branch: sheece-travis-sphinx-integration + github-token: $GITHUB_TOKEN + local-dir: docs/_build/html + project_name: PyRate + email: insar@ga.gov.au + name: InSAR Team From 9246da8a08b16a8879f82b554c6fb27c3fdd9121 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 6 Aug 2019 01:39:16 +0000 Subject: [PATCH 064/376] Modify comments on CLI options. --- pyrate/scripts/main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 89d4b4217..0927e6a09 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -22,7 +22,8 @@ import click from pyrate import pyratelog as pylog from pyrate import config as cf -from pyrate.scripts import converttogtif, run_prepifg, run_pyrate, postprocessing +from pyrate.scripts import (converttogtif, run_prepifg, run_pyrate, + postprocessing) from pyrate import __version__ log = logging.getLogger(__name__) @@ -42,7 +43,7 @@ def cli(verbosity): @click.argument('config_file') def converttogeotiff(config_file): """ - Convert input files to geotiff + Convert interferograms to geotiff. """ config_file = os.path.abspath(config_file) params = cf.get_config_params(config_file) @@ -55,7 +56,7 @@ def converttogeotiff(config_file): @click.argument('config_file') def prepifg(config_file): """ - Perform multilooking (resampling) and/or cropping on input files + Perform multilooking and cropping on geotiffs. """ config_file = os.path.abspath(config_file) params = cf.get_config_params(config_file) @@ -72,7 +73,7 @@ def prepifg(config_file): help='divide ifgs into this many columns') def process(config_file, rows, cols): """ - Step 2: Main PyRate workflow including time series and linear rate computation + Time series and linear rate computation. """ config_file = os.path.abspath(config_file) _, dest_paths, params = cf.get_ifg_paths(config_file) @@ -91,7 +92,7 @@ def process(config_file, rows, cols): 'number of cols used previously in main workflow') def postprocess(config_file, rows, cols): """ - Step 3: Reassemble PyRate output tiles and save as geotiffs + Reassemble computed tiles and save as geotiffs. """ config_file = os.path.abspath(config_file) postprocessing.main(config_file, rows, cols) From f6fb8c0c2347c0135e823dd80099c15c2e829779 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 6 Aug 2019 01:50:45 +0000 Subject: [PATCH 065/376] Update CLI description --- pyrate/scripts/main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyrate/scripts/main.py b/pyrate/scripts/main.py index 0927e6a09..bf396e5d7 100644 --- a/pyrate/scripts/main.py +++ b/pyrate/scripts/main.py @@ -35,7 +35,15 @@ type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']), default='INFO', help='Level of logging') def cli(verbosity): - """Commandline options and logging setup""" + """ + CLI for carrying out PyRate workflow. Typical workflow:\n + Step 1: converttogeotiff\n + Step 2: prepifg\n + Step 3: process\n + Step 4: postprocess\n + Refer to https://geoscienceaustralia.github.io/PyRate/usage.html for + more details. + """ pylog.configure(verbosity) From db4b7907c4aa5c958e94a1ac5876c29662f456fa Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Tue, 6 Aug 2019 02:28:26 +0000 Subject: [PATCH 066/376] WIP: updating usage docs for converttogeotiff module. --- docs/usage.rst | 104 +++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 2773029a9..d252b34e8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -16,14 +16,24 @@ Workflow After following the steps in `Installation `__, an executable program ``pyrate`` is created. -Use ``help`` for the different command line options: +Use ``--help`` for the different command line options: :: - >> pyrate --help Usage: pyrate [OPTIONS] COMMAND [ARGS]... - Commandline options and logging setup + CLI for carrying out PyRate workflow. Typical workflow: + + Step 1: converttogeotiff + + Step 2: prepifg + + Step 3: process + + Step 4: postprocess + + Refer to https://geoscienceaustralia.github.io/PyRate/usage.html for more + details. Options: -V, --version Show the version and exit. @@ -32,48 +42,28 @@ Use ``help`` for the different command line options: --help Show this message and exit. Commands: - postprocess Step 3: Reassemble PyRate output tiles and save as geotiffs - prepifg Step 1: Convert input files to geotiff and perform multilooking... - process Step 2: Main PyRate workflow including time series and linear rate... + converttogeotiff Convert interferograms to geotiff. + postprocess Reassemble computed tiles and save as geotiffs. + prepifg Perform multilooking and cropping on geotiffs. + process Time series and linear rate computation. -The ``pyrate`` program has three command line options corresponding to +The ``pyrate`` program has four command line options corresponding to different parts of the PyRate workflow: -1. prepifg -2. process -3. postprocess +1. converttogeotiff +2. prepifg +3. process +4. postprocess Below we discuss these options. -prepifg: Preparing input interferograms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The first step of PyRate is to convert the GAMMA or ROI\_PAC format -unwrapped interferograms into geotiff format, followed by applying -multi-looking and cropping operations. These procedures are all -performed by the ``pyrate prepifg`` command: - -:: - - >> pyrate prepifg --help - Usage: pyrate prepifg [OPTIONS] CONFIG_FILE - - Options: - --help Show this message and exit. - -The ``prepifg`` command is used as follows: - -:: - - pyrate prepifg /path/to/config_file +converttogeotiff: Converting input intergerograms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The two major steps during the ``prepifg`` operation are described -below. +Before PyRate can process GAMMA or ROI\_PAC intergerograms, they need to be +converted into geotiff format. -Data formatting: convert to geotiff -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``prepifg`` command will determine the input format from the value +The ``converttogeotiff`` command will determine the input format from the value specified at the *processor:* keyword in the config file (0: ROI\_PAC; 1: GAMMA) @@ -99,15 +89,45 @@ its path and name are specified in the config file under the *demHeaderFile:* keyword. The geographic projection in the parameter *DATUM:* is extracted from the DEM header file. +Upon completion, geotiff formatted copies of the input files will be placed +in the directory the input files are located in. Note that ``converttogeotiff`` +will not perform the conversion if geotiffs for the provided input files +already exist. + +prepifg: Preparing input interferograms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second step of PyRate is applying multi-looking and cropping +operations to the converted interferograms. +These procedures are all performed by the ``pyrate prepifg`` command: + +:: + + >> pyrate prepifg --help + Usage: pyrate prepifg [OPTIONS] CONFIG_FILE + + Options: + --help Show this message and exit. + +The ``prepifg`` command is used as follows: + +:: + + pyrate prepifg /path/to/config_file + Image transformations: multilooking and cropping ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The ``prepifg`` command will also perform multi-looking (image -sub-sampling) and cropping of the input interferograms. +The ``prepifg`` command will perform multi-looking (image +sub-sampling) and cropping of the input interferograms in geotiff format. +The purpose of this is to reduce the resolution of the interferograms to +reduce the computational complexity of performing the time series and +linear rate analysis. + +An example configuration file is provided in the root source directory +as ``input_parameters.conf``. The relevant options for ``prepifg`` +are TODO: EXPLAIN HOW TO CONFIGURE PREPIFG AND WHAT THE OPTIONS DO. -Two example configuration files are provided in the *configs/* -directory, one each for ROI\_PAC and GAMMA prepifg configuration. Either -configuration file can be used with ``prepifg``. process: Main workflow and linear rate and time series analysis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 84d1607d1a739ced5d0d25d4fcf342e08227dd5d Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 7 Aug 2019 01:01:03 +0000 Subject: [PATCH 067/376] Include converttogeotiff in usage documentation. --- docs/usage.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index d252b34e8..74733932a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -61,7 +61,17 @@ converttogeotiff: Converting input intergerograms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before PyRate can process GAMMA or ROI\_PAC intergerograms, they need to be -converted into geotiff format. +converted into geotiff format by the ``converttogeotiff`` command. + +:: + + >> pyrate converttogeotiff --help + Usage: pyrate converttogeotiff [OPTIONS] CONFIG_FILE + + Convert interferograms to geotiff. + + Options: + --help Show this message and exit. The ``converttogeotiff`` command will determine the input format from the value specified at the *processor:* keyword in the config file (0: ROI\_PAC; @@ -97,7 +107,7 @@ already exist. prepifg: Preparing input interferograms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The second step of PyRate is applying multi-looking and cropping +The second step of PyRate is applying multi-looking and cropping operations to the converted interferograms. These procedures are all performed by the ``pyrate prepifg`` command: @@ -125,9 +135,7 @@ reduce the computational complexity of performing the time series and linear rate analysis. An example configuration file is provided in the root source directory -as ``input_parameters.conf``. The relevant options for ``prepifg`` -are TODO: EXPLAIN HOW TO CONFIGURE PREPIFG AND WHAT THE OPTIONS DO. - +as ``input_parameters.conf``. process: Main workflow and linear rate and time series analysis ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 26705ad5dc2b5251bdfa7564a79a4f3ca2cedc9b Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 7 Aug 2019 01:14:57 +0000 Subject: [PATCH 068/376] Update docstring. --- pyrate/scripts/converttogtif.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrate/scripts/converttogtif.py b/pyrate/scripts/converttogtif.py index 451ea3271..a9493ed71 100644 --- a/pyrate/scripts/converttogtif.py +++ b/pyrate/scripts/converttogtif.py @@ -41,7 +41,7 @@ def main(params=None): """ - Function for converting input interferograms into geotiffs. + Parse parameters and prepare files for conversion. :param dict params: Parameters dictionary read in from the config file """ From bd4c498af0b8f143363f8673a0137bf5dd30cb06 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 7 Aug 2019 01:17:07 +0000 Subject: [PATCH 069/376] Add converttogtif module documentation. --- docs/pyrate.scripts.converttogtif.rst | 7 +++++++ docs/scripts.rst | 1 + 2 files changed, 8 insertions(+) create mode 100644 docs/pyrate.scripts.converttogtif.rst diff --git a/docs/pyrate.scripts.converttogtif.rst b/docs/pyrate.scripts.converttogtif.rst new file mode 100644 index 000000000..70641f0ea --- /dev/null +++ b/docs/pyrate.scripts.converttogtif.rst @@ -0,0 +1,7 @@ +PyRate converttogtif Script +========================= + +.. automodule:: pyrate.scripts.converttogtif + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/scripts.rst b/docs/scripts.rst index 29437d509..0c9a6131b 100644 --- a/docs/scripts.rst +++ b/docs/scripts.rst @@ -4,6 +4,7 @@ PyRate Scripts .. toctree:: :maxdepth: 4 + pyrate.scripts.converttogtif pyrate.scripts.run_prepifg pyrate.scripts.run_pyrate pyrate.scripts.postprocessing From 1ff63be0580d19769b243aee8bc8e0aa5a077b92 Mon Sep 17 00:00:00 2001 From: Bren Moushall Date: Wed, 7 Aug 2019 01:31:17 +0000 Subject: [PATCH 070/376] Add geotiff conversion to installation verification. --- docs/installation.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 2700cc29e..bfb563755 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -16,7 +16,7 @@ To verify PyRate has been successfully installed, run the workflow with the included example config file and data: :: - + pyrate converttogeotiff input_parameters.conf pyrate prepifg input_parameters.conf pyrate process input_parameters.conf pyrate postprocess input_parameters.conf @@ -26,6 +26,7 @@ On Raijin and other HPC systems, you can utilise MPI to run PyRate in parallel: :: # Modify 'n' based on the number of processors available. + mpirun -n 4 pyrate converttogeotiff input_parameters.conf mpirun -n 4 pyrate prepifg input_parameters.conf mpirun -n 4 pyrate process input_parameters.conf -c 2 -r 2 mpirun -n 4 pyrate postprocess input_parameters.conf -c 2 -r 2 From 629f80319aa938743fa162f27bdedc8544eda0d1 Mon Sep 17 00:00:00 2001 From: sheecegardezi Date: Thu, 8 Aug 2019 23:38:50 +1000 Subject: [PATCH 071/376] Build docs in Travis limit script to build docs to python: 3.6 build docs in Travis send email on failed builds --- .travis.yml | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index e5be2eff8..243cae1c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,17 +4,17 @@ branches: - /^release-.*$/ - /.*-travis$/ - master - - sheece-travis-sphinx-integration -language: python +os: linux +dist: bionic # Ubuntu 18.04 -python: -- "3.6" -- "3.7" +language: python +cache: pip -os: linux +python: + - "3.6" + - "3.7" -dist: bionic # Ubuntu 18.04 before_install: - sudo apt-get update @@ -25,7 +25,8 @@ before_install: - export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/include/gdal - export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/include/gdal - pip install -U pip - + - pip install codecov + install: - pip install -r requirements-test.txt - pip install -r requirements-dev.txt @@ -38,26 +39,33 @@ install: script: - pip install numpy==1.16.4 - pytest --cov-report term-missing:skip-covered --cov=pyrate tests/ - - cd docs && make html -# cache: -# - apt -# - pip -# - $HOME/.cache/pip +after_success: + - codecov + jobs: include: - stage: deploy python: 3.6 + script: + - pip install numpy==1.16.4 + - cd docs && make html deploy: provider: pages skip-cleanup: true keep-history: true verbose: true on: - branch: sheece-travis-sphinx-integration + branch: master github-token: $GITHUB_TOKEN local-dir: docs/_build/html project_name: PyRate email: insar@ga.gov.au name: InSAR Team +notifications: + email: + recipients: + - $SHEECE_EMAIL + on_success: never + on_failure: always \ No newline at end of file From d5f6944ca92450af0b885946b41d573ebc00808d Mon Sep 17 00:00:00 2001 From: sheecegardezi Date: Fri, 9 Aug 2019 12:59:36 +1000 Subject: [PATCH 072/376] Remove matlab ref in files rename files: remove matlab tag from files and folder in test data --- pyrate/matlab_mst.py | 4 +- tests/common.py | 6 +- tests/test_covariance.py | 4 +- .../{matlab_aps => aps}/preread_ifgs.pk | Bin .../{matlab_linrate => linrate}/coh_sta.csv | 0 .../{matlab_linrate => linrate}/errormap.csv | 0 .../{matlab_linrate => linrate}/stackmap.csv | 0 .../matlab_aps/ifg_spatio_temp_out.mat | Bin 230304 -> 0 bytes .../small_test/matlab_aps/preread_ifgs_py2.pk | 1917 ----------------- .../small_test/matlab_aps/ts_aps.mat | Bin 162632 -> 0 bytes .../matlab_aps/ts_aps_auto_cutoff.mat | Bin 162632 -> 0 bytes .../small_test/matlab_aps/ts_aps_m2.mat | Bin 162632 -> 0 bytes .../test_data/small_test/matlab_aps/ts_hp.mat | Bin 162632 -> 0 bytes .../small_test/matlab_aps/ts_hp_m2.mat | Bin 162632 -> 0 bytes .../small_test/matlab_aps/ts_hp_m3.mat | Bin 162632 -> 0 bytes .../small_test/matlab_aps/tsincr_svd.mat | Bin 162632 -> 0 bytes .../mst_geo_060619-061002.csv} | 0 .../mst_geo_060828-061211.csv} | 0 .../mst_geo_061002-070219.csv} | 0 .../mst_geo_061002-070430.csv} | 0 .../mst_geo_061106-061211.csv} | 0 .../mst_geo_061106-070115.csv} | 0 .../mst_geo_061106-070326.csv} | 0 .../mst_geo_061211-070709.csv} | 0 .../mst_geo_061211-070813.csv} | 0 .../mst_geo_070115-070326.csv} | 0 .../mst_geo_070115-070917.csv} | 0 .../mst_geo_070219-070430.csv} | 0 .../mst_geo_070219-070604.csv} | 0 .../mst_geo_070326-070917.csv} | 0 .../mst_geo_070430-070604.csv} | 0 .../mst_geo_070604-070709.csv} | 0 .../mst_geo_070709-070813.csv} | 0 ...ar_1lks_method1_geo_060619-061002.unw.csv} | 0 ...ar_1lks_method1_geo_060828-061211.unw.csv} | 0 ...ar_1lks_method1_geo_061002-070219.unw.csv} | 0 ...ar_1lks_method1_geo_061002-070430.unw.csv} | 0 ...ar_1lks_method1_geo_061106-061211.unw.csv} | 0 ...ar_1lks_method1_geo_061106-070115.unw.csv} | 0 ...ar_1lks_method1_geo_061106-070326.unw.csv} | 0 ...ar_1lks_method1_geo_061211-070709.unw.csv} | 0 ...ar_1lks_method1_geo_061211-070813.unw.csv} | 0 ...ar_1lks_method1_geo_070115-070326.unw.csv} | 0 ...ar_1lks_method1_geo_070115-070917.unw.csv} | 0 ...ar_1lks_method1_geo_070219-070430.unw.csv} | 0 ...ar_1lks_method1_geo_070219-070604.unw.csv} | 0 ...ar_1lks_method1_geo_070326-070917.unw.csv} | 0 ...ar_1lks_method1_geo_070430-070604.unw.csv} | 0 ...ar_1lks_method1_geo_070604-070709.unw.csv} | 0 ...ar_1lks_method1_geo_070709-070813.unw.csv} | 0 ...ar_1lks_method2_geo_060619-061002.unw.csv} | 0 ...ar_1lks_method2_geo_060828-061211.unw.csv} | 0 ...ar_1lks_method2_geo_061002-070219.unw.csv} | 0 ...ar_1lks_method2_geo_061002-070430.unw.csv} | 0 ...ar_1lks_method2_geo_061106-061211.unw.csv} | 0 ...ar_1lks_method2_geo_061106-070115.unw.csv} | 0 ...ar_1lks_method2_geo_061106-070326.unw.csv} | 0 ...ar_1lks_method2_geo_061211-070709.unw.csv} | 0 ...ar_1lks_method2_geo_061211-070813.unw.csv} | 0 ...ar_1lks_method2_geo_070115-070326.unw.csv} | 0 ...ar_1lks_method2_geo_070115-070917.unw.csv} | 0 ...ar_1lks_method2_geo_070219-070430.unw.csv} | 0 ...ar_1lks_method2_geo_070219-070604.unw.csv} | 0 ...ar_1lks_method2_geo_070326-070917.unw.csv} | 0 ...ar_1lks_method2_geo_070430-070604.unw.csv} | 0 ...ar_1lks_method2_geo_070604-070709.unw.csv} | 0 ...ar_1lks_method2_geo_070709-070813.unw.csv} | 0 .../ifg_mm_geo_060619-061002.unw.csv} | 0 .../ifg_mm_geo_060828-061211.unw.csv} | 0 .../ifg_mm_geo_061002-070219.unw.csv} | 0 .../ifg_mm_geo_061002-070430.unw.csv} | 0 .../ifg_mm_geo_061106-061211.unw.csv} | 0 .../ifg_mm_geo_061106-070115.unw.csv} | 0 .../ifg_mm_geo_061106-070326.unw.csv} | 0 .../ifg_mm_geo_061211-070709.unw.csv} | 0 .../ifg_mm_geo_061211-070813.unw.csv} | 0 .../ifg_mm_geo_070115-070326.unw.csv} | 0 .../ifg_mm_geo_070115-070917.unw.csv} | 0 .../ifg_mm_geo_070219-070430.unw.csv} | 0 .../ifg_mm_geo_070219-070604.unw.csv} | 0 .../ifg_mm_geo_070326-070917.unw.csv} | 0 .../ifg_mm_geo_070430-070604.unw.csv} | 0 .../ifg_mm_geo_070604-070709.unw.csv} | 0 .../ifg_mm_geo_070709-070813.unw.csv} | 0 .../ifg_rad_geo_060619-061002.unw.csv} | 0 .../ifg_rad_geo_060828-061211.unw.csv} | 0 .../ifg_rad_geo_061002-070219.unw.csv} | 0 .../ifg_rad_geo_061002-070430.unw.csv} | 0 .../ifg_rad_geo_061106-061211.unw.csv} | 0 .../ifg_rad_geo_061106-070115.unw.csv} | 0 .../ifg_rad_geo_061106-070326.unw.csv} | 0 .../ifg_rad_geo_061211-070709.unw.csv} | 0 .../ifg_rad_geo_061211-070813.unw.csv} | 0 .../ifg_rad_geo_070115-070326.unw.csv} | 0 .../ifg_rad_geo_070115-070917.unw.csv} | 0 .../ifg_rad_geo_070219-070430.unw.csv} | 0 .../ifg_rad_geo_070219-070604.unw.csv} | 0 .../ifg_rad_geo_070326-070917.unw.csv} | 0 .../ifg_rad_geo_070430-070604.unw.csv} | 0 .../ifg_rad_geo_070604-070709.unw.csv} | 0 .../ifg_rad_geo_070709-070813.unw.csv} | 0 ...orrected_method2geo_060619-061002.unw.csv} | 0 ...orrected_method2geo_060828-061211.unw.csv} | 0 ...orrected_method2geo_061002-070219.unw.csv} | 0 ...orrected_method2geo_061002-070430.unw.csv} | 0 ...orrected_method2geo_061106-061211.unw.csv} | 0 ...orrected_method2geo_061106-070115.unw.csv} | 0 ...orrected_method2geo_061106-070326.unw.csv} | 0 ...orrected_method2geo_061211-070709.unw.csv} | 0 ...orrected_method2geo_061211-070813.unw.csv} | 0 ...orrected_method2geo_070115-070326.unw.csv} | 0 ...orrected_method2geo_070115-070917.unw.csv} | 0 ...orrected_method2geo_070219-070430.unw.csv} | 0 ...orrected_method2geo_070219-070604.unw.csv} | 0 ...orrected_method2geo_070326-070917.unw.csv} | 0 ...orrected_method2geo_070430-070604.unw.csv} | 0 ...orrected_method2geo_070604-070709.unw.csv} | 0 ...orrected_method2geo_070709-070813.unw.csv} | 0 ..._phase_correctedgeo_060619-061002.unw.csv} | 0 ..._phase_correctedgeo_060828-061211.unw.csv} | 0 ..._phase_correctedgeo_061002-070219.unw.csv} | 0 ..._phase_correctedgeo_061002-070430.unw.csv} | 0 ..._phase_correctedgeo_061106-061211.unw.csv} | 0 ..._phase_correctedgeo_061106-070115.unw.csv} | 0 ..._phase_correctedgeo_061106-070326.unw.csv} | 0 ..._phase_correctedgeo_061211-070709.unw.csv} | 0 ..._phase_correctedgeo_061211-070813.unw.csv} | 0 ..._phase_correctedgeo_070115-070326.unw.csv} | 0 ..._phase_correctedgeo_070115-070917.unw.csv} | 0 ..._phase_correctedgeo_070219-070430.unw.csv} | 0 ..._phase_correctedgeo_070219-070604.unw.csv} | 0 ..._phase_correctedgeo_070326-070917.unw.csv} | 0 ..._phase_correctedgeo_070430-070604.unw.csv} | 0 ..._phase_correctedgeo_070604-070709.unw.csv} | 0 ..._phase_correctedgeo_070709-070813.unw.csv} | 0 .../ts_cum_interp0_method1.csv | 0 .../ts_cum_interp0_method2.csv | 0 .../ts_error_interp0_method1.csv | 0 .../ts_incr_interp0_method1.csv | 0 .../ts_incr_interp0_method2.csv | 0 .../matlab_alpha.csv => vcm/alpha.csv} | 0 .../matlab_vcmt.csv => vcm/vcmt.csv} | 0 tests/test_linrate.py | 8 +- tests/test_matlab_mst.py | 2 +- tests/test_mpi.py | 4 +- tests/test_ref_phs_est.py | 8 +- tests/test_timeseries.py | 4 +- utils/docker_install.txt | 23 +- 148 files changed, 35 insertions(+), 1945 deletions(-) rename tests/test_data/small_test/{matlab_aps => aps}/preread_ifgs.pk (100%) rename tests/test_data/small_test/{matlab_linrate => linrate}/coh_sta.csv (100%) rename tests/test_data/small_test/{matlab_linrate => linrate}/errormap.csv (100%) rename tests/test_data/small_test/{matlab_linrate => linrate}/stackmap.csv (100%) delete mode 100644 tests/test_data/small_test/matlab_aps/ifg_spatio_temp_out.mat delete mode 100644 tests/test_data/small_test/matlab_aps/preread_ifgs_py2.pk delete mode 100644 tests/test_data/small_test/matlab_aps/ts_aps.mat delete mode 100644 tests/test_data/small_test/matlab_aps/ts_aps_auto_cutoff.mat delete mode 100644 tests/test_data/small_test/matlab_aps/ts_aps_m2.mat delete mode 100644 tests/test_data/small_test/matlab_aps/ts_hp.mat delete mode 100644 tests/test_data/small_test/matlab_aps/ts_hp_m2.mat delete mode 100644 tests/test_data/small_test/matlab_aps/ts_hp_m3.mat delete mode 100644 tests/test_data/small_test/matlab_aps/tsincr_svd.mat rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_060619-061002.csv => mst/mst_geo_060619-061002.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_060828-061211.csv => mst/mst_geo_060828-061211.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061002-070219.csv => mst/mst_geo_061002-070219.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061002-070430.csv => mst/mst_geo_061002-070430.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061106-061211.csv => mst/mst_geo_061106-061211.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061106-070115.csv => mst/mst_geo_061106-070115.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061106-070326.csv => mst/mst_geo_061106-070326.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061211-070709.csv => mst/mst_geo_061211-070709.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_061211-070813.csv => mst/mst_geo_061211-070813.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070115-070326.csv => mst/mst_geo_070115-070326.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070115-070917.csv => mst/mst_geo_070115-070917.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070219-070430.csv => mst/mst_geo_070219-070430.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070219-070604.csv => mst/mst_geo_070219-070604.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070326-070917.csv => mst/mst_geo_070326-070917.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070430-070604.csv => mst/mst_geo_070430-070604.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070604-070709.csv => mst/mst_geo_070604-070709.csv} (100%) rename tests/test_data/small_test/{matlab_mst/mst_matlab_geo_070709-070813.csv => mst/mst_geo_070709-070813.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_060619-061002.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_060828-061211.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061002-070219.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061002-070430.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061106-061211.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061106-070115.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061106-070326.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061211-070709.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_061211-070813.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070115-070326.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070115-070917.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070219-070430.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070219-070604.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070326-070917.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070430-070604.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070604-070709.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method1_geo_070709-070813.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method1_geo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_060619-061002.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_060828-061211.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061002-070219.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061002-070430.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061106-061211.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061106-070115.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061106-070326.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061211-070709.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_061211-070813.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070115-070326.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070115-070917.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070219-070430.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070219-070604.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070326-070917.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070430-070604.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070604-070709.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_orbital_error_correction/matlab_ifg_orb_planar_1lks_method2_geo_070709-070813.unw.csv => orbital_error_correction/ifg_orb_planar_1lks_method2_geo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_060619-061002.unw.csv => prepifg_output/ifg_mm_geo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_060828-061211.unw.csv => prepifg_output/ifg_mm_geo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061002-070219.unw.csv => prepifg_output/ifg_mm_geo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061002-070430.unw.csv => prepifg_output/ifg_mm_geo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061106-061211.unw.csv => prepifg_output/ifg_mm_geo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061106-070115.unw.csv => prepifg_output/ifg_mm_geo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061106-070326.unw.csv => prepifg_output/ifg_mm_geo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061211-070709.unw.csv => prepifg_output/ifg_mm_geo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_061211-070813.unw.csv => prepifg_output/ifg_mm_geo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070115-070326.unw.csv => prepifg_output/ifg_mm_geo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070115-070917.unw.csv => prepifg_output/ifg_mm_geo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070219-070430.unw.csv => prepifg_output/ifg_mm_geo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070219-070604.unw.csv => prepifg_output/ifg_mm_geo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070326-070917.unw.csv => prepifg_output/ifg_mm_geo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070430-070604.unw.csv => prepifg_output/ifg_mm_geo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070604-070709.unw.csv => prepifg_output/ifg_mm_geo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_mm_geo_070709-070813.unw.csv => prepifg_output/ifg_mm_geo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_060619-061002.unw.csv => prepifg_output/ifg_rad_geo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_060828-061211.unw.csv => prepifg_output/ifg_rad_geo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061002-070219.unw.csv => prepifg_output/ifg_rad_geo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061002-070430.unw.csv => prepifg_output/ifg_rad_geo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061106-061211.unw.csv => prepifg_output/ifg_rad_geo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061106-070115.unw.csv => prepifg_output/ifg_rad_geo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061106-070326.unw.csv => prepifg_output/ifg_rad_geo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061211-070709.unw.csv => prepifg_output/ifg_rad_geo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_061211-070813.unw.csv => prepifg_output/ifg_rad_geo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070115-070326.unw.csv => prepifg_output/ifg_rad_geo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070115-070917.unw.csv => prepifg_output/ifg_rad_geo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070219-070430.unw.csv => prepifg_output/ifg_rad_geo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070219-070604.unw.csv => prepifg_output/ifg_rad_geo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070326-070917.unw.csv => prepifg_output/ifg_rad_geo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070430-070604.unw.csv => prepifg_output/ifg_rad_geo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070604-070709.unw.csv => prepifg_output/ifg_rad_geo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_prepifg_output/matlab_ifg_rad_geo_070709-070813.unw.csv => prepifg_output/ifg_rad_geo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_060619-061002.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_060828-061211.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061002-070219.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061002-070430.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061106-061211.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061106-070115.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061106-070326.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061211-070709.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_061211-070813.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070115-070326.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070115-070917.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070219-070430.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070219-070604.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070326-070917.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070430-070604.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070604-070709.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_corrected_method2geo_070709-070813.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_corrected_method2geo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_060619-061002.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_060619-061002.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_060828-061211.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_060828-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061002-070219.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061002-070219.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061002-070430.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061002-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061106-061211.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061106-061211.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061106-070115.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061106-070115.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061106-070326.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061106-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061211-070709.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061211-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_061211-070813.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_061211-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070115-070326.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070115-070326.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070115-070917.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070115-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070219-070430.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070219-070430.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070219-070604.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070219-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070326-070917.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070326-070917.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070430-070604.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070430-070604.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070604-070709.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070604-070709.unw.csv} (100%) rename tests/test_data/small_test/{matlab_ref_phase_est/matlab_ifg_orb_and_ref_phase_correctedgeo_070709-070813.unw.csv => ref_phase_est/ifg_orb_and_ref_phase_correctedgeo_070709-070813.unw.csv} (100%) rename tests/test_data/small_test/{matlab_time_series => time_series}/ts_cum_interp0_method1.csv (100%) rename tests/test_data/small_test/{matlab_time_series => time_series}/ts_cum_interp0_method2.csv (100%) rename tests/test_data/small_test/{matlab_time_series => time_series}/ts_error_interp0_method1.csv (100%) rename tests/test_data/small_test/{matlab_time_series => time_series}/ts_incr_interp0_method1.csv (100%) rename tests/test_data/small_test/{matlab_time_series => time_series}/ts_incr_interp0_method2.csv (100%) rename tests/test_data/small_test/{matlab_vcm/matlab_alpha.csv => vcm/alpha.csv} (100%) rename tests/test_data/small_test/{matlab_vcm/matlab_vcmt.csv => vcm/vcmt.csv} (100%) diff --git a/pyrate/matlab_mst.py b/pyrate/matlab_mst.py index 25a4547ff..b4920bd44 100644 --- a/pyrate/matlab_mst.py +++ b/pyrate/matlab_mst.py @@ -243,7 +243,7 @@ def _matlab_mst_gen(ifg_instance, p_threshold=1): Construct a pixel-by-pixel matrix containing unique minimum spanning tree networks. This is an implementation of the Matlab Pirate 'make_mstmat.m' function. - This is a generator version of the 'pyrate.matlab_mst.matlab_mst' + This is a generator version of the 'pyrate.mst.mst' function that is more memory efficient. :param ifg_instance: _IfgListPyRate instance @@ -297,7 +297,7 @@ def _matlab_mst_bool(ifg_list_instance, p_threshold=1): :return: result: Minimum Spanning Tree matrix :rtype: ndarray """ - # This should have the same output as matlab_mst. Should be tested. + # This should have the same output as mst. Should be tested. # Please note that the generator version is more memory efficient. # If memory was not a concern we could have found the entire mst matrix in the # previous function and this would have been unnecessary. diff --git a/tests/common.py b/tests/common.py index 5f9aa5f54..de86cde84 100644 --- a/tests/common.py +++ b/tests/common.py @@ -47,10 +47,10 @@ SML_TEST_CONF = join(SML_TEST_DIR, 'conf') SML_TEST_DEM_DIR = join(SML_TEST_DIR, 'dem') -SML_TEST_MATLAB_MST_DIR = join(SML_TEST_DIR, 'matlab_mst') -SML_TEST_MATLAB_PREPIFG_DIR = join(SML_TEST_DIR, 'matlab_prepifg_output') +SML_TEST_MATLAB_MST_DIR = join(SML_TEST_DIR, 'mst') +SML_TEST_MATLAB_PREPIFG_DIR = join(SML_TEST_DIR, 'prepifg_output') SML_TEST_MATLAB_ORBITAL_DIR = join(SML_TEST_DIR, - 'matlab_orbital_error_correction') + 'orbital_error_correction') SML_TEST_DEM_ROIPAC = join(SML_TEST_DEM_DIR, 'roipac_test_trimmed.dem') SML_TEST_DEM_GAMMA = join(SML_TEST_GAMMA, '20060619_utm.dem') SML_TEST_INCIDENCE = join(SML_TEST_GAMMA, '20060619_utm.inc') diff --git a/tests/test_covariance.py b/tests/test_covariance.py index 4e7cc0721..2f302ab36 100644 --- a/tests/test_covariance.py +++ b/tests/test_covariance.py @@ -222,9 +222,9 @@ def test_matlab_maxvar_equality_small_test_files(self): def test_matlab_vcmt_equality_small_test_files(self): from tests.common import SML_TEST_DIR - MATLAB_VCM_DIR = os.path.join(SML_TEST_DIR, 'matlab_vcm') + MATLAB_VCM_DIR = os.path.join(SML_TEST_DIR, 'vcm') matlab_vcm = np.genfromtxt(os.path.join(MATLAB_VCM_DIR, - 'matlab_vcmt.csv'), delimiter=',') + 'vcmt.csv'), delimiter=',') np.testing.assert_array_almost_equal(matlab_vcm, self.vcmt, decimal=3) def test_metadata(self): diff --git a/tests/test_data/small_test/matlab_aps/preread_ifgs.pk b/tests/test_data/small_test/aps/preread_ifgs.pk similarity index 100% rename from tests/test_data/small_test/matlab_aps/preread_ifgs.pk rename to tests/test_data/small_test/aps/preread_ifgs.pk diff --git a/tests/test_data/small_test/matlab_linrate/coh_sta.csv b/tests/test_data/small_test/linrate/coh_sta.csv similarity index 100% rename from tests/test_data/small_test/matlab_linrate/coh_sta.csv rename to tests/test_data/small_test/linrate/coh_sta.csv diff --git a/tests/test_data/small_test/matlab_linrate/errormap.csv b/tests/test_data/small_test/linrate/errormap.csv similarity index 100% rename from tests/test_data/small_test/matlab_linrate/errormap.csv rename to tests/test_data/small_test/linrate/errormap.csv diff --git a/tests/test_data/small_test/matlab_linrate/stackmap.csv b/tests/test_data/small_test/linrate/stackmap.csv similarity index 100% rename from tests/test_data/small_test/matlab_linrate/stackmap.csv rename to tests/test_data/small_test/linrate/stackmap.csv diff --git a/tests/test_data/small_test/matlab_aps/ifg_spatio_temp_out.mat b/tests/test_data/small_test/matlab_aps/ifg_spatio_temp_out.mat deleted file mode 100644 index 0d022ade1e03022151abf498105bc0341f02a4c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 230304 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