diff --git a/cxx/isce3/Headers.cmake b/cxx/isce3/Headers.cmake index 8df7292ff..962351053 100644 --- a/cxx/isce3/Headers.cmake +++ b/cxx/isce3/Headers.cmake @@ -1,6 +1,7 @@ set(HEADERS antenna/detail/WinChirpRgCompPow.h antenna/detail/WinChirpRgCompPow.icc +antenna/edge_method_cost_func.h antenna/forward.h antenna/ElPatternEst.h antenna/geometryfunc.h @@ -18,8 +19,6 @@ core/Baseline.h core/Basis.h core/Common.h core/Constants.h -core/Cube.h -core/Cube.icc core/DateTime.h core/DenseMatrix.h core/detail/BuildOrbit.h @@ -41,7 +40,6 @@ core/LUT1d.h core/LUT1d.icc core/LUT2d.h core/Matrix.h -core/Matrix.icc core/Metadata.h core/Orbit.h core/Peg.h @@ -125,18 +123,12 @@ io/IH5.icc io/Raster.h io/Raster.icc io/Serialization.h -matchtemplate/ampcor/correlators/correlators.h -matchtemplate/ampcor/correlators/kernels.h -matchtemplate/ampcor/correlators/Sequential.h -matchtemplate/ampcor/correlators/Sequential.icc -matchtemplate/ampcor/dom/dom.h -matchtemplate/ampcor/dom/Raster.h -matchtemplate/ampcor/dom/Raster.icc -matchtemplate/ampcor/dom/SLC.h -matchtemplate/ampcor/dom/SLC.icc math/Bessel.h math/ComplexMultiply.h +math/detail/RootFind1dBase.h math/polyfunc.h +math/RootFind1dNewton.h +math/RootFind1dSecant.h math/Sinc.h math/Sinc.icc polsar/symmetrize.h diff --git a/cxx/isce3/Sources.cmake b/cxx/isce3/Sources.cmake index 6c6aafd48..ab92856c5 100644 --- a/cxx/isce3/Sources.cmake +++ b/cxx/isce3/Sources.cmake @@ -1,4 +1,5 @@ set(SRCS +antenna/edge_method_cost_func.cpp antenna/ElPatternEst.cpp antenna/geometryfunc.cpp core/Attitude.cpp @@ -58,21 +59,10 @@ io/gdal/SpatialReference.cpp io/IH5.cpp io/IH5Dataset.cpp io/Raster.cpp -matchtemplate/ampcor/correlators/c2r.cpp -matchtemplate/ampcor/correlators/correlate.cpp -matchtemplate/ampcor/correlators/detect.cpp -matchtemplate/ampcor/correlators/maxcor.cpp -matchtemplate/ampcor/correlators/migrate.cpp -matchtemplate/ampcor/correlators/nudge.cpp -matchtemplate/ampcor/correlators/offsets.cpp -matchtemplate/ampcor/correlators/r2c.cpp -matchtemplate/ampcor/correlators/refStats.cpp -matchtemplate/ampcor/correlators/sat.cpp -matchtemplate/ampcor/correlators/tgtStats.cpp -matchtemplate/ampcor/dom/Raster.cc -matchtemplate/ampcor/dom/SLC.cc math/Bessel.cpp math/polyfunc.cpp +math/RootFind1dNewton.cpp +math/RootFind1dSecant.cpp polsar/symmetrize.cpp product/GeoGridParameters.cpp product/Product.cpp diff --git a/cxx/isce3/antenna/detail/WinChirpRgCompPow.h b/cxx/isce3/antenna/detail/WinChirpRgCompPow.h index 50c12a532..b617e06ad 100644 --- a/cxx/isce3/antenna/detail/WinChirpRgCompPow.h +++ b/cxx/isce3/antenna/detail/WinChirpRgCompPow.h @@ -12,7 +12,6 @@ namespace isce3 { namespace antenna { namespace detail { // Aliases, typedef: -// fuzzy naming convention by isce3::core::EMtarix module!!! using RowMatrixXcf = isce3::core::EMatrix2D>; // Functions: diff --git a/cxx/isce3/antenna/edge_method_cost_func.cpp b/cxx/isce3/antenna/edge_method_cost_func.cpp new file mode 100644 index 000000000..34b368703 --- /dev/null +++ b/cxx/isce3/antenna/edge_method_cost_func.cpp @@ -0,0 +1,150 @@ +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace isce3 { namespace antenna { + +std::tuple rollAngleOffsetFromEdge( + const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + const isce3::core::Linspace& look_ang, + std::optional polyfit_weight) +{ + // check the input arguments + if (polyfit_echo.order != 3 || polyfit_ant.order != 3) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Requires 3rd-order poly-fit object for both " + "Echo and Antenna!"); + constexpr double a_tol {1e-5}; + if (std::abs(polyfit_echo.mean - polyfit_ant.mean) > a_tol || + std::abs(polyfit_echo.norm - polyfit_ant.norm) > a_tol) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Requires same (mean, std) for Echo and Antenna Poly1d obj!"); + + if (!(polyfit_echo.norm > 0.0)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Requires positive std of Echo and Antenna Poly1d obj!"); + + if (polyfit_weight) { + if (polyfit_weight->order < 0) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "The order of polyfit for weights must be " + "at least 0 (constant weights)!"); + if (!(polyfit_weight->norm > 0.0)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Requires positive std of weight Poly1d obj!"); + } + + // create a copy polyfit objects "echo" and "ant" with zero mean and unit + // std + auto pf_echo_cp = polyfit_echo; + pf_echo_cp.mean = 0.0; + pf_echo_cp.norm = 1.0; + auto pf_ant_cp = polyfit_ant; + pf_ant_cp.mean = 0.0; + pf_ant_cp.norm = 1.0; + + // declare and initialize a look angle vector + Eigen::ArrayXd lka_vec(look_ang.size()); + for (int idx = 0; idx < look_ang.size(); ++idx) + lka_vec(idx) = look_ang[idx]; + + // create a weighting vector from look vector and weighting Poly1d + Eigen::ArrayXd wgt_vec; + if (polyfit_weight) { + Eigen::Map wgt_coef( + polyfit_weight->coeffs.data(), polyfit_weight->coeffs.size()); + wgt_vec = isce3::math::polyval( + wgt_coef, lka_vec, polyfit_weight->mean, polyfit_weight->norm); + // normalize power in dB + wgt_vec -= wgt_vec.maxCoeff(); + // convert from dB to linear power scale + wgt_vec = Eigen::pow(10, 0.1 * wgt_vec); + } + // centralized and scaled the look vector based on mean/std of the echo + // Poly1d to be used for both antenna and echo in the cost function. + lka_vec -= polyfit_echo.mean; + const auto std_inv = 1.0 / polyfit_echo.norm; + lka_vec *= std_inv; + + // form some derivatives used in the cost function + auto pf_echo_der = pf_echo_cp.derivative(); + auto pf_ant_der = pf_ant_cp.derivative(); + auto pf_ant_der2 = pf_ant_der.derivative(); + // create a memmap of the coeff for the first and second derivatives + Eigen::Map coef_ant_der( + pf_ant_der.coeffs.data(), pf_ant_der.coeffs.size()); + Eigen::Map coef_ant_der2( + pf_ant_der2.coeffs.data(), pf_ant_der2.coeffs.size()); + Eigen::Map coef_echo_der( + pf_echo_der.coeffs.data(), pf_echo_der.coeffs.size()); + // form some arrays over scaled look angles for diff of first derivatives + // and for second derivative + auto ant_echo_der_dif_vec = + isce3::math::polyval(coef_ant_der - coef_echo_der, lka_vec); + auto ant_der2_vec = isce3::math::polyval(coef_ant_der2, lka_vec); + + // build cost function in the form of Poly1d object (3th order polynimal!) + auto cf_pf = isce3::core::Poly1d(3, 0.0, 1.0); + // fill up the coeff for the derivative of the WMSE cost function: + // cost(ofs) = pf_wgt*(pf_echo_der(el) - pf_ant_der(el + ofs))**2 + // See section 1.1 of the cited reference. + if (polyfit_weight) { + auto tmp1 = wgt_vec * ant_echo_der_dif_vec; + auto tmp2 = wgt_vec * ant_der2_vec; + cf_pf.coeffs[0] = (tmp1 * ant_der2_vec).sum(); + cf_pf.coeffs[1] = (tmp2 * ant_der2_vec).sum() + + 6 * pf_ant_cp.coeffs[3] * tmp1.sum(); + cf_pf.coeffs[2] = 9 * pf_ant_cp.coeffs[3] * tmp2.sum(); + cf_pf.coeffs[3] = + 18 * pf_ant_cp.coeffs[3] * pf_ant_cp.coeffs[3] * wgt_vec.sum(); + } else // no weighting + { + cf_pf.coeffs[0] = (ant_echo_der_dif_vec * ant_der2_vec).sum(); + cf_pf.coeffs[1] = ant_der2_vec.square().sum() + + 6 * pf_ant_cp.coeffs[3] * ant_echo_der_dif_vec.sum(); + cf_pf.coeffs[2] = 9 * pf_ant_cp.coeffs[3] * ant_der2_vec.sum(); + cf_pf.coeffs[3] = 18 * pf_ant_cp.coeffs[3] * pf_ant_cp.coeffs[3] * + look_ang.size(); + } + // form Root finding object + auto rf_obj = + isce3::math::RootFind1dNewton(1e-4, 20, look_ang.spacing() / 10.); + // solve for the root/roll offset via Newton + auto [roll, f_val, flag, n_iter] = rf_obj.root(cf_pf); + // scale back the roll angle by std of original poly1d object + roll *= polyfit_echo.norm; + + return {roll, f_val, flag, n_iter}; +} + +std::tuple rollAngleOffsetFromEdge( + const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + double look_ang_near, double look_ang_far, double look_ang_prec, + std::optional polyfit_weight) +{ + if (!(look_ang_near > 0.0 && look_ang_far > 0.0 && look_ang_prec > 0.0)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "All look angles values must be positive numbers!"); + if (look_ang_near >= (look_ang_far - look_ang_prec)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Near-range look angle shall be smaller than " + "far one by at least one prec!"); + + const auto ang_size = + static_cast((look_ang_far - look_ang_near) / look_ang_prec) + + 1; + auto look_ang = isce3::core::Linspace::from_interval( + look_ang_near, look_ang_far, ang_size); + + return rollAngleOffsetFromEdge( + polyfit_echo, polyfit_ant, look_ang, polyfit_weight); +} + +}} // namespace isce3::antenna diff --git a/cxx/isce3/antenna/edge_method_cost_func.h b/cxx/isce3/antenna/edge_method_cost_func.h new file mode 100644 index 000000000..e1a0fd0f4 --- /dev/null +++ b/cxx/isce3/antenna/edge_method_cost_func.h @@ -0,0 +1,100 @@ +/** @file edge_method_cost_func.h + * Cost functions used in EL antenna pointing estimation via edge method + */ +#pragma once + +#include +#include + +#include +#include + +/** @namespace isce3::antenna */ +namespace isce3 { namespace antenna { + +// Aliases +using poly1d_t = isce3::core::Poly1d; + +/** + * Estimate roll angle offset via edge method from poly-fitted + * power patterns obtained from echo raw data and antenna pattern. + * The cost function is solved via Newton method and final solution + * is the weighted average of individual solution within look + * (off-nadir) angles [near, far] with desired angle precision all + * defined by isce3 Linspace. + * See equations for cost function in section 1.1 of the reference + * @cite EdgeMethodElPointDoc + * The only difference is that the look angles are in (rad) rather than in + * (deg). Note that the respective magnitudes for both echo and antenna can be + * either 2-way or 1-way power patterns. + * @param[in] polyfit_echo isce3 Poly1d object for polyfitted magnitude + * of either range compressed (preferred) or raw echo data. + * It must be third-order polynomial of relative magnitude/power in (dB) + * as a function of look angle in (rad) + * @param[in] polyfit_ant isce3 Poly1d object for antenna EL power pattern. + * It must be third-order polynomial of relative magnitude/power in (dB) + * as a function of look angle in (rad). + * It must have the same mean and std as that of "polyfit_echo"! + * @param[in] look_ang isce3 Linspace object to cover desired range of + * look angles (rad) with desired precision/spacing. + * @param[in] polyfit_weight (optional) isce3 Poly1d object for weightings used + * in final weighted averaged of individual solutions over desired look angle + * coverage. It shall represent relative magnitude/power in (dB) as a function + * of look angle in (rad). + * The order of the polynomial must be at least 0 (constant weights). + * @return roll angle offset (rad) + * Note that the roll offset shall be added to EL angles in antenna frame + * to align EL power pattern from antenna to the one extracted from echo given + * the cost function optimized for offset applied to polyfitted antenna data. + * @return max cost function value among all iterations + * @return overall convergence flag (true or false) + * @return max number of iterations among all iterations + * @exception InvalidArgument + */ +std::tuple rollAngleOffsetFromEdge( + const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + const isce3::core::Linspace& look_ang, + std::optional polyfit_weight = {}); + +/** + * Estimate roll angle offset via edge method from poly-fitted + * power patterns obtained from echo raw data and antenna pattern. + * The cost function is solved via Newton method and final solution + * is the weighted average of individual solution within look + * (off-nadir) angles [near, far] with desired angle precision "look_ang_prec". + * See equations for cost function in section 1.1 of the reference + * @cite EdgeMethodElPointDoc + * The only difference is that the look angles are in (rad) rather than in + * (deg). Note that the respective magnitudes for both echo and antenna can be + * either 2-way or 1-way power patterns. + * @param[in] polyfit_echo isce3 Poly1d object for polyfitted magnitude + * of either range compressed (preferred) or raw echo data. + * It must be third-order polynomial of relative magnitude/power in (dB) + * as a function of look angle in (rad) + * @param[in] polyfit_ant isce3 Poly1d object for antenna EL power pattern. + * It must be third-order polynomial of relative magnitude/power in (dB) + * as a function of look angle in (rad). + * It must have the same mean and std as that of "polyfit_echo"! + * @param[in] look_ang_near look angle for near range in (rad) + * @param[in] look_ang_far look angle for far range in (rad) + * @param[in] look_ang_prec look angle precision/resolution in (rad) + * @param[in] polyfit_weight (optional) isce3 Poly1d object for weightings used + * in final weighted averaged of individual solutions over desired look angle + * coverage. It shall represent relative magnitude/power in (dB) as a function + * of look angle in (rad). + * The order of the polynomial must be at least 0 (constant weights). + * @return roll angle offset (rad) + * Note that the roll offset shall be added to EL angles in antenna frame + * to align EL power pattern from antenna to the one extracted from echo given + * the cost function optimized for offset applied to polyfitted antenna data. + * @return max cost function value among all iterations + * @return overall convergence flag (true or false) + * @return max number of iterations among all iterations + * @exception InvalidArgument + */ +std::tuple rollAngleOffsetFromEdge( + const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + double look_ang_near, double look_ang_far, double look_ang_prec, + std::optional polyfit_weight = {}); + +}} // namespace isce3::antenna diff --git a/cxx/isce3/antenna/geometryfunc.h b/cxx/isce3/antenna/geometryfunc.h index 1a7e67370..8048f6368 100644 --- a/cxx/isce3/antenna/geometryfunc.h +++ b/cxx/isce3/antenna/geometryfunc.h @@ -48,7 +48,6 @@ namespace isce3 { namespace antenna { * @exception InvalidArgument, RuntimeError * @cite ReeTechDesDoc */ - std::tuple ant2rgdop(double el_theta, double az_phi, const isce3::core::Vec3& pos_ecef, const isce3::core::Vec3& vel_ecef, const isce3::core::Quaternion& quat, double wavelength, diff --git a/cxx/isce3/core/Cube.h b/cxx/isce3/core/Cube.h deleted file mode 100644 index 26717386d..000000000 --- a/cxx/isce3/core/Cube.h +++ /dev/null @@ -1,146 +0,0 @@ -//-*- C++ -*- -//-*- coding: utf-8 -*- -// -// Author: Piyush Agram -// Copyright 2017-2019 - -#pragma once - -#include "forward.h" - -#include -#include -#include -#include - -// isce3::core::Cube definition -template -class isce3::core::Cube { - - public: - // Types for interfacing with pyre::grid - using rep_t = std::array; - using index_t = pyre::grid::index_t; - using layout_t = pyre::grid::layout_t; - - // Use a grid with view memory storage - using grid_t = pyre::grid::grid_t>; - - // Dependent types - using view_t = typename grid_t::view_type; - using shape_t = typename layout_t::shape_type; - using slice_t = typename layout_t::slice_type; - using packing_t = typename layout_t::packing_type; - - public: - /** Default constructor */ - inline Cube(); - - /** Constructor with number of slices, rows and columns */ - inline Cube(size_t nslices, size_t nrows, size_t ncols); - - /** Deep copy constructor from another cube - allocates memory and copies values */ - inline Cube(const Cube & m); - - /** Shallow copy constructor from another cube - does not allocate own memory */ - inline Cube(Cube & m); - - /** Shallow copy constructor from raw pointer to data - does not allocate own memory */ - inline Cube(cell_t * data, size_t nslices, size_t nrows, size_t ncols); - - /** Shallow copy constructor from an std::valarray - does not allocate own memory */ - inline Cube(std::valarray & data, size_t nrows, size_t ncols); - - /** Shallow copy constructor from an std::vector - does not allocate own memory */ - inline Cube(std::vector & data, size_t nrows, size_t ncols); - - /** Destructor */ - inline ~Cube(); - - /** Deep assignment operator - allocates memory and copies values */ - inline Cube & operator=(const Cube & m); - - /** Shallow assignment operator - does not allocate own memory */ - inline Cube & operator=(Cube & m); - - /** Resize memory for a given number of slices, rows and columns */ - inline void resize(size_t nslices, size_t nrows, size_t ncols); - - /** Extract copy of sub-cube given starting indices and span */ - inline const view_t subcube(size_t slice, size_t row, size_t col, size_t slicespan, size_t rowspan, size_t colspan); - - /** Access to data buffer */ - inline cell_t * data(); - - /** Read-only access to data buffer */ - inline const cell_t * data() const; - - /** Access to data buffer at a specific slice */ - inline cell_t * sliceptr(size_t slice); - - /** Read-only access to data buffer at a specific slice */ - inline const cell_t * sliceptr(size_t slice) const; - - /** Access matrix value for a given slice, row and column */ - inline cell_t & operator()(size_t slice, size_t row, size_t col); - - /** Read-only access to matrix value for a given slice, row and column */ - inline const cell_t & operator()(size_t slice, size_t row, size_t col) const; - - /** Access matrix value for a flattened index */ - inline cell_t & operator()(size_t index); - - /** Read-only access to matrix value for a flattened index */ - inline const cell_t & operator()(size_t index) const; - - /** Access matrix value for a given grid::index_type */ - inline cell_t & operator[](const index_t & index); - - /** Read-only access to matrix value for a given grid::idnex_type */ - inline const cell_t & operator[](const index_t & index) const; - - /** Fill with zeros */ - inline void zeros(); - - /** Fill with a constant value */ - inline void fill(cell_t value); - - /** Get shape information as grid::shape_type */ - inline shape_t shape() const; - - /** Get cube width */ - inline size_t width() const; - - /** Get cube length */ - inline size_t length() const; - - /** Get cube height */ - inline size_t height() const; - - /** Get byteoffset for row and column for reading flat binary buffer */ - inline shape_t byteoffset() const; - - // Data members - private: - // Shape information - size_t _nslices; - size_t _nrows; - size_t _ncols; - - // Dynamic memory data - cell_t * _buffer; - bool _owner; - - // grid pointer for slicing support - grid_t * _grid; - - // Utility functions - private: - // Reset grid pointer - inline void _resetGrid(); -}; - -// Get inline implementations for Cube -#define ISCE_CORE_CUBE_ICC -#include "Cube.icc" -#undef ISCE_CORE_CUBE_ICC diff --git a/cxx/isce3/core/Cube.icc b/cxx/isce3/core/Cube.icc deleted file mode 100644 index 262487393..000000000 --- a/cxx/isce3/core/Cube.icc +++ /dev/null @@ -1,371 +0,0 @@ -//-*- C++ -*- -//-*- coding: utf-8 -*- -// -// Author: Piyush Agram -// Copyright 2017-2018 - -#if !defined(ISCE_CORE_CUBE_ICC) -#error "Cube.icc is an implementation detail of class Cube" -#endif - -/** Default constructor */ -template -isce3::core::Cube:: -Cube() : _nslices(0), _nrows{0}, _ncols{0}, _owner{false}, _grid{nullptr} {} - -// Constructor with number of slices, rows and columns -/** @param[in] nslices Number of slices - * @param[in] nrows Number of rows - * @param[in] ncols Number of columns */ -template -isce3::core::Cube:: -Cube(size_t nslices, size_t nrows, size_t ncols) : - _nslices{nslices}, - _nrows{nrows}, - _ncols{ncols}, - _buffer{new cell_t[nslices*nrows*ncols]}, - _owner{true}, - _grid{nullptr} {} - -// Deep copy constructor - allocates memory and copies values -/** @param[in] m isce3::core::Cube object to copy */ -template -isce3::core::Cube:: -Cube(const Cube & m) : - _nslices{m.height()}, - _nrows{m.length()}, - _ncols{m.width()}, - _buffer{new cell_t[_nslices*_nrows*_ncols]}, - _owner{true}, - _grid{nullptr} { - std::copy(m.data(), m.data() + _nslices*_nrows*_ncols, _buffer); -} - -// Shallow copy constructor - does not allocate its own memory -/** @param[in] m isce3::core::Cube object to copy */ -template -isce3::core::Cube:: -Cube(Cube & m) : - _nslices{m.height()}, - _nrows{m.length()}, - _ncols{m.width()}, - _buffer{m.data()}, - _owner{false}, - _grid{nullptr} {} - -// Shallow copy constructor from a raw pointer - does not allocate own memory -/** @param[in] data raw pointer to buffer containing data - * @param[in] nslices Number of slices for data - * @param[in] nrows Number of rows for data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Cube:: -Cube(cell_t * data, size_t nslices, size_t nrows, size_t ncols) : - _nslices{nslices}, - _nrows{nrows}, - _ncols{ncols}, - _buffer{data}, - _owner{false}, - _grid{nullptr} {} - -// Shallow copy constructor from an std::valarray - does not allocate own memory -/** @param[in] data Valarray containing data - * @param[in] nrows Number of rows for data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Cube:: -Cube(std::valarray & data, size_t nrows, size_t ncols) : - _nslices{data.size()/(nrows*ncols)}, - _nrows{nrows}, - _ncols{ncols}, - _buffer{&data[0]}, - _owner{false}, - _grid{nullptr} {} - -// Shallow copy constructor from an std::vector - does not allocate own memory -/** @param[in] data Vector containing data - * @param[in] nrows Number of rows for data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Cube:: -Cube(std::vector & data, size_t nrows, size_t ncols) : - _nslices{data.size()/(nrows*ncols)}, - _nrows{nrows}, - _ncols{ncols}, - _buffer{data.data()}, - _owner{false}, - _grid{nullptr} {} - -/** Destructor */ -template -isce3::core::Cube:: -~Cube() { - // If I allocated memory myself, delete it - if (_owner) { - delete [] _buffer; - } - // If I allocated a grid pointer, delete it - if (_grid) { - delete _grid; - } -} - -// Deep assignment operator - allocates memory and copies values -/** @param[in] m isce3::core::Cube object to copy */ -template -isce3::core::Cube & -isce3::core::Cube:: -operator=(const Cube & m) { - // Resize my storage - resize(m.height(), m.length(), m.width()); - // Copy values - std::copy(m.data(), m.data() + _nslices*_nrows*_ncols, _buffer); - // Reset grid pointer - _resetGrid(); - return *this; -} - -// Shallow assignment operator - does not allocate its own memory -/** @param[in] m isce3::core::Cube object to copy */ -template -isce3::core::Cube & -isce3::core::Cube:: -operator=(Cube & m) { - _nslices = m.height(); - _nrows = m.length(); - _ncols = m.width(); - _buffer = m.data(); - _owner = false; - _resetGrid(); - return *this; -} - -// Resize memory for a given number of slices, rows and columns (no value initialization) -/** @param[in] nslices Number of slices - * @param[in] nrows Number of rows - * @param[in] ncols Number of columns */ -template -void -isce3::core::Cube:: -resize(size_t nslices, size_t nrows, size_t ncols) { - - // If I have already allocated memory, delete it first - if (_owner) { - delete [] _buffer; - } - - // Allocate new memory and save shape - _nslices = nslices; - _nrows = nrows; - _ncols = ncols; - _buffer = new cell_t[_nslices * _nrows * _ncols]; - _owner = true; - - // Reset grid pointer - _resetGrid(); -} - -// Extract copy of sub-cube given starting indices and span -/** @param[in] slice Starting slice of subcube - * @param[in] row Starting row of subcube - * @param[in] col Starting column of subcube - * @param[in] slicespan Number of slices of subcube - * @param[in] rowspan Number of rows of subcube - * @param[in] colspan Number of columns of subcube */ -template -const typename isce3::core::Cube::view_t -isce3::core::Cube:: -subcube(size_t slice, size_t row, size_t col, size_t slicespan, size_t rowspan, size_t colspan) { - - // Allocate grid pointer for my data - _resetGrid(); - const shape_t shape{_nslices, _nrows, _ncols}; - _grid = new grid_t{{shape}, &_buffer[0]}; - - // Create slice - const index_t low = {slice, row, col}; - const index_t high = {slice + slicespan, row + rowspan, col + colspan}; - const packing_t packing{2ul, 1ul, 0ul}; - const slice_t slice3d = {low, high, packing}; - - // Return a view - return _grid->view(slice3d); -} - -/** Access to data buffer */ -template -cell_t * -isce3::core::Cube:: -data() { - return _buffer; -} - -/** Read-only access to data buffer */ -template -const cell_t * -isce3::core::Cube:: -data() const { - return _buffer; -} - -/** Access to data buffer at specific slice */ -template -cell_t * -isce3::core::Cube:: -sliceptr(size_t slice) { - // Make a view pointing to row - auto view = subcube(slice, 0, 0, 1, 1, 1); - // Return pointer - return &(*view.begin()); -} - -/** Read-only access to data buffer at specific slice */ -template -const cell_t * -isce3::core::Cube:: -sliceptr(size_t slicenum) const { - // Make a view pointing to row - auto view = subcube(slicenum, 0, 0, 1, 1, 1); - // Return pointer - return &(*view.begin()); -} - -// Access matrix value for a given slice, row and column -/** @param[in] slice Slice coordinate to access - * @param[in] row Row coordinate to access - * @param[in] col Column coordinate to access */ -template -cell_t & -isce3::core::Cube:: -operator()(size_t slice, size_t row, size_t col) { - return _buffer[(slice*_nrows + row)*_ncols + col]; -} - -// Read-only access to cube value for a given slice, row and column -/** @param[in] slice Slice coordinate to access - * @param[in] row Row coordinate to access - * @param[in] col Column coordinate to access */ -template -const cell_t & -isce3::core::Cube:: -operator()(size_t slice, size_t row, size_t col) const { - return _buffer[(slice*_nrows + row)*_ncols + col]; -} - -// Access matrix value for a flattened index -/** @param[in] index Flattened index to access */ -template -cell_t & -isce3::core::Cube:: -operator()(size_t index) { - return _buffer[index]; -} - -// Read-only access to cube value for a flattened index -/** @param[in] index Flattened index to access */ -template -const cell_t & -isce3::core::Cube:: -operator()(size_t index) const { - return _buffer[index]; -} - -// Access cube value for a given grid::index_type -/** @param[in] index pyre::grid_t::index_type for coordinate */ -template -cell_t & -isce3::core::Cube:: -operator[](const index_t & index) { - // Create grid for my data - shape_t shape{_nslices, _nrows, _ncols}; - grid_t grid{{shape}, &_buffer[0]}; - // Pass it an index - return grid[index]; -} - -// Read-only access to matrix value for a given grid::index_type -/** @param[in] index pyre::grid_t::index_type for coordinate */ -template -const cell_t & -isce3::core::Cube:: -operator[](const index_t & index) const { - // Create grid for my data - shape_t shape{_nslices, _nrows, _ncols}; - grid_t grid{{shape}, &_buffer[0]}; - // Pass it an index - return grid[index]; -} - -// Fill with zeros -template -void -isce3::core::Cube:: -zeros() { - for (size_t i = 0; i < _nslices *_nrows * _ncols; ++i) { - _buffer[i] = 0.0; - } -} - -// Fill with a constant value -template -void -isce3::core::Cube:: -fill(cell_t value) { - for (size_t i = 0; i < _nslices * _nrows * _ncols; ++i) { - _buffer[i] = value; - } -} - -/** Get shape information as grid::shape_type */ -template -typename isce3::core::Cube::shape_t -isce3::core::Cube:: -shape() const { - return {_nslices, _nrows, _ncols}; -} - -/** Get cube width */ -template -size_t -isce3::core::Cube:: -width() const { - return _ncols; -} - -/** Get cube length */ -template -size_t -isce3::core::Cube:: -length() const { - return _nrows; -} - -/** Get cube height */ -template -size_t -isce3::core::Cube:: -height() const { - return _nslices; -} - -/** Get matrix byte offset for slice, row and column for raw binary buffer */ -template -typename isce3::core::Cube::shape_t -isce3::core::Cube:: -byteoffset() const { - size_t unitsize = sizeof(cell_t); - return { _nrows * _ncols * unitsize, _ncols * unitsize, unitsize}; -} - -// Reset grid pointer for matrix views -template -void -isce3::core::Cube:: -_resetGrid() { - if (_grid) { - delete _grid; - } - _grid = nullptr; -} - -// end of file diff --git a/cxx/isce3/core/DenseMatrix.h b/cxx/isce3/core/DenseMatrix.h index 3556692a8..5b7956290 100644 --- a/cxx/isce3/core/DenseMatrix.h +++ b/cxx/isce3/core/DenseMatrix.h @@ -33,7 +33,9 @@ class DenseMatrix : public Eigen::Matrix { return *this * other; } - CUDA_HOSTDEV constexpr DenseMatrix( +// Backport Eigen 3.4.0's initializer_list constructor +#if !EIGEN_VERSION_AT_LEAST(3, 4, 0) + CUDA_HOSTDEV explicit constexpr DenseMatrix( std::initializer_list> lst) { int i = 0, j = 0; for (const auto& l : lst) { @@ -43,6 +45,7 @@ class DenseMatrix : public Eigen::Matrix { i++, j = 0; } } +#endif /** Matrix transposition */ CUDA_HOSTDEV constexpr DenseMatrix transpose() const { diff --git a/cxx/isce3/core/LUT2d.cpp b/cxx/isce3/core/LUT2d.cpp index 2acd625db..440e51bb8 100644 --- a/cxx/isce3/core/LUT2d.cpp +++ b/cxx/isce3/core/LUT2d.cpp @@ -7,6 +7,7 @@ #include "LUT2d.h" #include +#include #include "Interpolator.h" diff --git a/cxx/isce3/core/Matrix.h b/cxx/isce3/core/Matrix.h index f2ce82bdb..b76d0c548 100644 --- a/cxx/isce3/core/Matrix.h +++ b/cxx/isce3/core/Matrix.h @@ -8,151 +8,80 @@ #include "forward.h" +#include #include +#include #include #include -#include - #include "EMatrix.h" /** Data structure for a 2D row-major matrix*/ -template -class isce3::core::Matrix { - - public: - // Types for interfacing with pyre::grid - using rep_t = std::array; - using index_t = pyre::grid::index_t; - using layout_t = pyre::grid::layout_t; +template +class isce3::core::Matrix : + public Eigen::Array +{ - // Use a grid with view memory storage - using grid_t = pyre::grid::grid_t>; + using super_t = Eigen::Array; - // Dependent types - using view_t = typename grid_t::view_type; - using shape_t = index_t; - using packing_t = pyre::grid::packing_t<2>; - using slice_t = pyre::grid::slice_t; + public: + using index_t = typename super_t::Index; public: /** Default constructor */ - inline Matrix(); - - /** Constructor with number of rows and number of columns */ - inline Matrix(size_t nrows, size_t ncols); - - /** Deep copy constructor from another matrix - allocates memory and copies values */ - inline Matrix(const Matrix & m); - - /** Shallow copy constructor from another matrix - does not allocate own memory */ - inline Matrix(Matrix & m); - - /** Copy constructor from a grid view (copy values) */ - inline Matrix(const view_t & view); - - /** Shallow copy constructor from raw pointer to data - does not allocate own memory */ - inline Matrix(cell_t * data, size_t nrows, size_t ncols); + Matrix() : super_t(0, 0) {} - /** Shallow copy constructor from an std::valarray - does not allocate own memory */ - inline Matrix(std::valarray & data, size_t ncols); + Matrix(index_t rows, index_t cols) : super_t(rows, cols) {} - /** Shallow copy constructor from an std::vector - does not allocate own memory */ - inline Matrix(std::vector & data, size_t ncols); + template + Matrix(const Eigen::Block& b) : super_t(b) {} - /** Destructor */ - inline ~Matrix(); - - /** Deep assignment operator - allocates memory and copies values */ - inline Matrix & operator=(const Matrix & m); + /** Copy constructor from raw pointer to data */ + Matrix(T * data, size_t nrows, size_t ncols) : + super_t(Eigen::Map(data, nrows, ncols)) + { + assert(ncols <= std::numeric_limits::max()); + assert(nrows <= std::numeric_limits::max()); + } - /** Shallow assignment operator - does not allocate own memory */ - inline Matrix & operator=(Matrix & m); - - /** Assignment operator from a grid view (copy values) */ - inline Matrix & operator=(const view_t & view); + /** Copy constructor from an std::valarray */ + Matrix(std::valarray & data, size_t ncols) : + super_t(Eigen::Map(data.data(), data.size() / ncols, ncols)) + { + assert(ncols <= std::numeric_limits::max()); + } - /** Resize memory for a given number of rows and columns */ - inline void resize(size_t nrows, size_t ncols); + /** Copy constructor from an std::vector */ + Matrix(std::vector & data, size_t ncols) : + super_t(Eigen::Map(data.data(), data.size() / ncols, ncols)) + { + assert(ncols <= std::numeric_limits::max()); + } /** Extract copy of sub-matrix given starting indices and span */ - inline const view_t submat(size_t row, size_t col, size_t rowspan, size_t colspan); - - /** Access to data buffer */ - inline cell_t * data(); - - /** Read-only access to data buffer */ - inline const cell_t * data() const; - - /** Access to data buffer at a specific row */ - inline cell_t * rowptr(size_t row); - - /** Read-only access to data buffer at a specific row */ - inline const cell_t * rowptr(size_t row) const; - - /** Access matrix value for a given row and column */ - inline cell_t & operator()(size_t row, size_t col); - - /** Read-only access to matrix value for a given row and column */ - inline const cell_t & operator()(size_t row, size_t col) const; - - /** Access matrix value for a flattened index */ - inline cell_t & operator()(size_t index); - - /** Read-only access to matrix value for a flattened index */ - inline const cell_t & operator()(size_t index) const; - - /** Access matrix value for a given grid::index_type */ - inline cell_t & operator[](const index_t & index); - - /** Read-only access to matrix value for a given grid::idnex_type */ - inline const cell_t & operator[](const index_t & index) const; + auto submat(size_t row, size_t col, size_t rowspan, size_t colspan) { + assert(col <= std::numeric_limits::max()); + assert(row <= std::numeric_limits::max()); + assert(colspan <= std::numeric_limits::max()); + assert(rowspan <= std::numeric_limits::max()); + return this->block(row, col, rowspan, colspan); + } /** Fill with zeros */ - inline void zeros(); - - /** Fill with a constant value */ - inline void fill(cell_t value); - - /** Get shape information as grid::shape_type */ - inline shape_t shape() const; + void zeros() { this->fill(0); } /** Get matrix width */ - inline size_t width() const; + size_t width() const { return this->cols(); } /** Get matrix length */ - inline size_t length() const; - - /** Get byteoffset for row and column for reading flat binary buffer */ - inline shape_t byteoffset() const; - - auto map() const - { - return Eigen::Map> { - _buffer, static_cast(_nrows), - static_cast(_ncols)}; + size_t length() const { return this->rows(); } + + auto map() const { + return Eigen::Map { + this->data(), + this->rows(), + this->cols(), + }; } - // Data members - private: - // Shape information - size_t _nrows; - size_t _ncols; - - // Dynamic memory data - cell_t* _buffer = nullptr; - bool _owner; - - // grid pointer for slicing support - grid_t * _grid; - - // Utility functions - private: - // Reset grid pointer - inline void _resetGrid(); }; - -// Get inline implementations for Matrix -#define ISCE_CORE_MATRIX_ICC -#include "Matrix.icc" -#undef ISCE_CORE_MATRIX_ICC diff --git a/cxx/isce3/core/Matrix.icc b/cxx/isce3/core/Matrix.icc deleted file mode 100644 index f174cf5dc..000000000 --- a/cxx/isce3/core/Matrix.icc +++ /dev/null @@ -1,381 +0,0 @@ -//-*- C++ -*- -//-*- coding: utf-8 -*- -// -// Author: Bryan V. Riel -// Copyright 2017-2018 - -#if !defined(ISCE_CORE_MATRIX_ICC) -#error "Matrix.icc is an implementation detail of class Matrix" -#endif - -/** Default constructor */ -template -isce3::core::Matrix:: -Matrix() : _nrows{0}, _ncols{0}, _owner{false}, _grid{nullptr} {} - -// Constructor with number of rows and number of columns -/** @param[in] nrows Number of rows - * @param[in] ncols Number of columns */ -template -isce3::core::Matrix:: -Matrix(size_t nrows, size_t ncols) : - _nrows{nrows}, - _ncols{ncols}, - _buffer{new cell_t[nrows*ncols]}, - _owner{true}, - _grid{nullptr} {} - -// Deep copy constructor - allocates memory and copies values -/** @param[in] m isce3::core::Matrix object to copy */ -template -isce3::core::Matrix:: -Matrix(const Matrix & m) : - _nrows{m.length()}, - _ncols{m.width()}, - _buffer{new cell_t[_nrows*_ncols]}, - _owner{true}, - _grid{nullptr} { - std::copy(m.data(), m.data() + _nrows*_ncols, _buffer); -} - -// Shallow copy constructor - does not allocate its own memory -/** @param[in] m isce3::core::Matrix object to copy */ -template -isce3::core::Matrix:: -Matrix(Matrix & m) : - _nrows{m.length()}, - _ncols{m.width()}, - _buffer{m.data()}, - _owner{false}, - _grid{nullptr} {} - -// Copy constructor from a grid view (copy values) -/** @param[in] view pyre::grid_t::view_type to copy from */ -template -isce3::core::Matrix:: -Matrix(const view_t & view) : _grid{nullptr} { - // Set the shape from the view - auto shape = view.layout().shape(); - _nrows = shape[0]; - _ncols = shape[1]; - // Allocate memory - _owner = true; - _buffer = new cell_t[_nrows*_ncols]; - // Copy values - std::copy(view.begin(), view.end(), _buffer); -} - -// Shallow copy constructor from a raw pointer - does not allocate own memory -/** @param[in] data Valarray containing data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Matrix:: -Matrix(cell_t * data, size_t nrows, size_t ncols) : - _nrows{nrows}, - _ncols{ncols}, - _buffer{data}, - _owner{false}, - _grid{nullptr} {} - -// Shallow copy constructor from an std::valarray - does not allocate own memory -/** @param[in] data Valarray containing data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Matrix:: -Matrix(std::valarray & data, size_t ncols) : - _nrows{data.size() / ncols}, - _ncols{ncols}, - _buffer{&data[0]}, - _owner{false}, - _grid{nullptr} {} - -// Shallow copy constructor from an std::vector - does not allocate own memory -/** @param[in] data Vector containing data - * @param[in] ncols Number of columns for data (assume row major packing) */ -template -isce3::core::Matrix:: -Matrix(std::vector & data, size_t ncols) : - _nrows{data.size() / ncols}, - _ncols{ncols}, - _buffer{data.data()}, - _owner{false}, - _grid{nullptr} {} - -/** Destructor */ -template -isce3::core::Matrix:: -~Matrix() { - // If I allocated memory myself, delete it - if (_owner) { - delete [] _buffer; - } - // If I allocated a grid pointer, delete it - if (_grid) { - delete _grid; - } -} - -// Deep assignment operator - allocates memory and copies values -/** @param[in] m isce3::core::Matrix object to copy */ -template -isce3::core::Matrix & -isce3::core::Matrix:: -operator=(const Matrix & m) { - // Resize my storage - resize(m.length(), m.width()); - // Copy values - std::copy(m.data(), m.data() + _nrows*_ncols, _buffer); - // Reset grid pointer - _resetGrid(); - return *this; -} - -// Shallow assignment operator - does not allocate its own memory -/** @param[in] m isce3::core::Matrix object to copy */ -template -isce3::core::Matrix & -isce3::core::Matrix:: -operator=(Matrix & m) { - _nrows = m.length(); - _ncols = m.width(); - _buffer = m.data(); - _owner = false; - _resetGrid(); - return *this; -} - -// Assignment operator from a grid view (copy values) -/** @param[in] view pyre::grid_t::view_type to copy from */ -template -isce3::core::Matrix & -isce3::core::Matrix:: -operator=(const view_t & view) { - // Set the shape from the view - auto shape = view.layout().shape(); - _nrows = shape[0]; - _ncols = shape[1]; - // Allocate memory - _owner = true; - _buffer = new cell_t[_nrows*_ncols]; - // Copy values - std::copy(view.begin(), view.end(), _buffer); - // Reset grid pointer - _resetGrid(); - return *this; -} - -// Resize memory for a given number of rows and columns (no value initialization) -/** @param[in] nrows Number of rows - * @param[in] ncols Number of columns */ -template -void -isce3::core::Matrix:: -resize(size_t nrows, size_t ncols) { - - // If I have already allocated memory, delete it first - if (_owner) { - delete [] _buffer; - } - - // Allocate new memory and save shape - _nrows = nrows; - _ncols = ncols; - _buffer = new cell_t[_nrows * _ncols]; - _owner = true; - - // Reset grid pointer - _resetGrid(); -} - -// Extract copy of sub-matrix given starting indices and span -/** @param[in] row Starting row of submatrix - * @param[in] col Starting column of submatrix - * @param[in] rowspan Number of rows of submatrix - * @param[in] colspan Number of columns of submatrix */ -template -const typename isce3::core::Matrix::view_t -isce3::core::Matrix:: -submat(size_t row, size_t col, size_t rowspan, size_t colspan) { - - // Allocate grid pointer for my data - _resetGrid(); - const shape_t shape{_nrows, _ncols}; - _grid = new grid_t{{shape}, &_buffer[0]}; - - // Create slice - const index_t low = {row, col}; - const index_t high = {row + rowspan, col + colspan}; - const packing_t packing{1ul, 0ul}; - const slice_t slice = {low, high, packing}; - - // Return a view - return _grid->view(slice); -} - -/** Access to data buffer */ -template -cell_t * -isce3::core::Matrix:: -data() { - return _buffer; -} - -/** Read-only access to data buffer */ -template -const cell_t * -isce3::core::Matrix:: -data() const { - return _buffer; -} - -/** Access to data buffer at specific row */ -template -cell_t * -isce3::core::Matrix:: -rowptr(size_t row) { - // Make a view pointing to row - auto view = submat(row, 0, 1, 1); - // Return pointer - return &(*view.begin()); -} - -/** Read-only access to data buffer at specific row */ -template -const cell_t * -isce3::core::Matrix:: -rowptr(size_t row) const { - // Make a view pointing to row - auto view = submat(row, 0, 1, 1); - // Return pointer - return &(*view.begin()); -} - -// Access matrix value for a given row and column -/** @param[in] row Row coordinate to access - * @param[in] col Column coordinate to access */ -template -cell_t & -isce3::core::Matrix:: -operator()(size_t row, size_t col) { - return _buffer[row*_ncols + col]; -} - -// Read-only access to matrix value for a given row and column -/** @param[in] row Row coordinate to access - * @param[in] col Column coordinate to access */ -template -const cell_t & -isce3::core::Matrix:: -operator()(size_t row, size_t col) const { - return _buffer[row*_ncols + col]; -} - -// Access matrix value for a flattened index -/** @param[in] index Flattened index to access */ -template -cell_t & -isce3::core::Matrix:: -operator()(size_t index) { - return _buffer[index]; -} - -// Read-only access to matrix value for a flattened index -/** @param[in] index Flattened index to access */ -template -const cell_t & -isce3::core::Matrix:: -operator()(size_t index) const { - return _buffer[index]; -} - -// Access matrix value for a given grid::index_type -/** @param[in] index pyre::grid_t::index_type for coordinate */ -template -cell_t & -isce3::core::Matrix:: -operator[](const index_t & index) { - // Create grid for my data - shape_t shape{_nrows, _ncols}; - grid_t grid{{shape}, &_buffer[0]}; - // Pass it an index - return grid[index]; -} - -// Read-only access to matrix value for a given grid::idnex_type -/** @param[in] index pyre::grid_t::index_type for coordinate */ -template -const cell_t & -isce3::core::Matrix:: -operator[](const index_t & index) const { - // Create grid for my data - shape_t shape{_nrows, _ncols}; - grid_t grid{{shape}, &_buffer[0]}; - // Pass it an index - return grid[index]; -} - -// Fill with zeros -template -void -isce3::core::Matrix:: -zeros() { - for (size_t i = 0; i < _nrows * _ncols; ++i) { - _buffer[i] = 0.0; - } -} - -// Fill with a constant value -template -void -isce3::core::Matrix:: -fill(cell_t value) { - for (size_t i = 0; i < _nrows * _ncols; ++i) { - _buffer[i] = value; - } -} - -/** Get shape information as grid::shape_type */ -template -typename isce3::core::Matrix::shape_t -isce3::core::Matrix:: -shape() const { - return {_nrows, _ncols}; -} - -/** Get matrix width */ -template -size_t -isce3::core::Matrix:: -width() const { - return _ncols; -} - -/** Get matrix length */ -template -size_t -isce3::core::Matrix:: -length() const { - return _nrows; -} - -/** Get matrix byte offset for row and column for raw binary buffer */ -template -typename isce3::core::Matrix::shape_t -isce3::core::Matrix:: -byteoffset() const { - size_t unitsize = sizeof(cell_t); - return { _ncols * unitsize, unitsize}; -} - -// Reset grid pointer for matrix views -template -void -isce3::core::Matrix:: -_resetGrid() { - if (_grid) { - delete _grid; - } - _grid = nullptr; -} - -// end of file diff --git a/cxx/isce3/core/Pegtrans.cpp b/cxx/isce3/core/Pegtrans.cpp index 244177077..d05525b87 100644 --- a/cxx/isce3/core/Pegtrans.cpp +++ b/cxx/isce3/core/Pegtrans.cpp @@ -99,7 +99,7 @@ void Pegtrans::SCHbasis(const cartesian_t &sch, cartmat_t &, /* * Computes the transformation matrix from xyz to a local sch frame */ - cartmat_t matschxyzp = {{{-sin(sch[0]/radcur), + cartmat_t matschxyzp {{{-sin(sch[0]/radcur), -(sin(sch[1]/radcur) * cos(sch[0]/radcur)), cos(sch[0]/radcur) * cos(sch[1]/radcur)}, {cos(sch[0]/radcur), diff --git a/cxx/isce3/core/forward.h b/cxx/isce3/core/forward.h index f2ae97f46..29f2b6889 100644 --- a/cxx/isce3/core/forward.h +++ b/cxx/isce3/core/forward.h @@ -24,7 +24,6 @@ namespace isce3 { template class DenseMatrix; template class Vector; - template class Cube; template class Linspace; template class LUT1d; template class LUT2d; diff --git a/cxx/isce3/cuda/Headers.cmake b/cxx/isce3/cuda/Headers.cmake index 89e09de69..4a1363caa 100644 --- a/cxx/isce3/cuda/Headers.cmake +++ b/cxx/isce3/cuda/Headers.cmake @@ -44,11 +44,6 @@ image/gpuResampSlc.h image/ResampSlc.h io/DataStream.h io/DataStream.icc -matchtemplate/ampcor/correlators.h -matchtemplate/ampcor/kernels.h -matchtemplate/ampcor/public.h -matchtemplate/ampcor/Sequential.h -matchtemplate/ampcor/Sequential.icc math/ComplexMultiply.h signal/forward.h signal/gpuCrossMul.h diff --git a/cxx/isce3/cuda/Sources.cmake b/cxx/isce3/cuda/Sources.cmake index 979c7b2db..c8eb1908e 100644 --- a/cxx/isce3/cuda/Sources.cmake +++ b/cxx/isce3/cuda/Sources.cmake @@ -30,17 +30,6 @@ geometry/Topo.cpp geometry/utilities.cu image/gpuResampSlc.cu image/ResampSlc.cpp -matchtemplate/ampcor/c2r.cu -matchtemplate/ampcor/correlate.cu -matchtemplate/ampcor/detect.cu -matchtemplate/ampcor/maxcor.cu -matchtemplate/ampcor/migrate.cu -matchtemplate/ampcor/nudge.cu -matchtemplate/ampcor/offsets.cu -matchtemplate/ampcor/r2c.cu -matchtemplate/ampcor/refStats.cu -matchtemplate/ampcor/sat.cu -matchtemplate/ampcor/tgtStats.cu matchtemplate/pycuampcor/cuAmpcorChunk.cu matchtemplate/pycuampcor/cuAmpcorController.cu matchtemplate/pycuampcor/cuAmpcorParameter.cu diff --git a/cxx/isce3/cuda/core/gpuSinc2dInterpolator.cu b/cxx/isce3/cuda/core/gpuSinc2dInterpolator.cu index 45ce0c002..08e863ac3 100644 --- a/cxx/isce3/cuda/core/gpuSinc2dInterpolator.cu +++ b/cxx/isce3/cuda/core/gpuSinc2dInterpolator.cu @@ -50,7 +50,7 @@ __host__ gpuSinc2dInterpolator::gpuSinc2dInterpolator( // Copy Orbit data to device-side memory and keep device pointer in gpuOrbit // object. Device-side copy constructor simply shallow-copies the device // pointers when called - checkCudaErrors(cudaMemcpy(kernel, &(h_kernel[0]), + checkCudaErrors(cudaMemcpy(kernel, h_kernel.data(), filter.size() * sizeof(double), cudaMemcpyHostToDevice)); } diff --git a/cxx/isce3/cuda/geometry/gpuTopo.cu b/cxx/isce3/cuda/geometry/gpuTopo.cu index 5b4d15898..2feb64fdc 100644 --- a/cxx/isce3/cuda/geometry/gpuTopo.cu +++ b/cxx/isce3/cuda/geometry/gpuTopo.cu @@ -114,16 +114,41 @@ void setOutputTopoLayers(const Vec3& targetLLH, } layers.hdg(index, heading); + // Project output coordinates to DEM coordinates + Vec3 input_coords_llh; + (*projOutput)->inverse({x, y, targetLLH[2]}, input_coords_llh); + Vec3 dem_vect; + (*(demInterp.proj()))->forward(input_coords_llh, dem_vect); + // East-west slope using central difference - double aa = demInterp.interpolateXY(x - demInterp.deltaX(), y); - double bb = demInterp.interpolateXY(x + demInterp.deltaX(), y); - double gamma = targetLLH[1]; - double alpha = ((bb - aa) * degrees) / (2.0 * ellipsoid.rEast(gamma) * demInterp.deltaX()); + double aa = demInterp.interpolateXY(dem_vect[0] - demInterp.deltaX(), dem_vect[1]); + double bb = demInterp.interpolateXY(dem_vect[0] + demInterp.deltaX(), dem_vect[1]); + + Vec3 dem_vect_p_dx = {dem_vect[0] + demInterp.deltaX(), dem_vect[1], dem_vect[2]}; + Vec3 dem_vect_m_dx = {dem_vect[0] - demInterp.deltaX(), dem_vect[1], dem_vect[2]}; + Vec3 input_coords_llh_p_dx, input_coords_llh_m_dx; + (*(demInterp.proj()))->inverse(dem_vect_p_dx, input_coords_llh_p_dx); + (*(demInterp.proj()))->inverse(dem_vect_m_dx, input_coords_llh_m_dx); + const Vec3 input_coords_xyz_p_dx = ellipsoid.lonLatToXyz(input_coords_llh_p_dx); + const Vec3 input_coords_xyz_m_dx = ellipsoid.lonLatToXyz(input_coords_llh_m_dx); + double dx = (input_coords_xyz_p_dx - input_coords_xyz_m_dx).norm(); + + double alpha = (bb - aa) / dx; // North-south slope using central difference - aa = demInterp.interpolateXY(x, y - demInterp.deltaY()); - bb = demInterp.interpolateXY(x, y + demInterp.deltaY()); - double beta = ((bb - aa) * degrees) / (2.0 * ellipsoid.rNorth(gamma) * demInterp.deltaY()); + aa = demInterp.interpolateXY(dem_vect[0], dem_vect[1] - demInterp.deltaY()); + bb = demInterp.interpolateXY(dem_vect[0], dem_vect[1] + demInterp.deltaY()); + + Vec3 dem_vect_p_dy = {dem_vect[0], dem_vect[1] + demInterp.deltaY(), dem_vect[2]}; + Vec3 dem_vect_m_dy = {dem_vect[0], dem_vect[1] - demInterp.deltaY(), dem_vect[2]}; + Vec3 input_coords_llh_p_dy, input_coords_llh_m_dy; + (*(demInterp.proj()))->inverse(dem_vect_p_dy, input_coords_llh_p_dy); + (*(demInterp.proj()))->inverse(dem_vect_m_dy, input_coords_llh_m_dy); + const Vec3 input_coords_xyz_p_dy = ellipsoid.lonLatToXyz(input_coords_llh_p_dy); + const Vec3 input_coords_xyz_m_dy = ellipsoid.lonLatToXyz(input_coords_llh_m_dy); + double dy = (input_coords_xyz_p_dy - input_coords_xyz_m_dy).norm(); + + double beta = (bb - aa) / dy; // Compute local incidence angle enu /= enu.norm(); diff --git a/cxx/isce3/cuda/signal/gpuCrossMul.h b/cxx/isce3/cuda/signal/gpuCrossMul.h index 7da3bb296..c4e0572c8 100644 --- a/cxx/isce3/cuda/signal/gpuCrossMul.h +++ b/cxx/isce3/cuda/signal/gpuCrossMul.h @@ -165,7 +165,7 @@ class isce3::cuda::signal::gpuCrossmul { bool _doCommonRangeBandFilter = false; // number of lines per block - size_t _rowsPerBlock = 8192; + size_t _rowsPerBlock = 2048; // upsampling factor size_t _oversample = 1; diff --git a/cxx/isce3/geocode/GeocodeCov.cpp b/cxx/isce3/geocode/GeocodeCov.cpp index 218e2d02c..c7c3d7d74 100644 --- a/cxx/isce3/geocode/GeocodeCov.cpp +++ b/cxx/isce3/geocode/GeocodeCov.cpp @@ -769,7 +769,7 @@ inline void Geocode::_interpolate( if (flag_apply_rtc) { float rtc_value = - rtc_area(rdrY + azimuthFirstLine, rdrX + rangeFirstPixel); + rtc_area(int(rdrY + azimuthFirstLine), int(rdrX + rangeFirstPixel)); val /= std::sqrt(rtc_value); if (out_geo_rtc != nullptr) { #pragma omp atomic write @@ -818,7 +818,7 @@ inline void Geocode::_interpolate( if (phase_screen_raster != nullptr) { phase -= phase_screen_array( - rdrY + azimuthFirstLine, rdrX + rangeFirstPixel); + int(rdrY + azimuthFirstLine), int(rdrX + rangeFirstPixel)); } T_out cpxPhase; @@ -2109,11 +2109,9 @@ void Geocode::_runBlock( getDemCoords = isce3::geometry::getDemCoordsDiffEpsg; } - // Load DEM using the block geogrid with a margin of 100 pixels - const double margin_x = std::abs(_geoGridSpacingX) * 100; - const double margin_y = std::abs(_geoGridSpacingY) * 100; + // Load DEM using the block geogrid extents auto error_code = loadDemFromProj(dem_raster, minX, maxX, minY, maxY, - &dem_interp_block, proj, margin_x, margin_y); + &dem_interp_block, proj); if (error_code != isce3::error::ErrorCode::Success) { _saveOptionalFiles(block_x, block_size_x, block_y, block_size_y, diff --git a/cxx/isce3/geometry/Geo2rdr.h b/cxx/isce3/geometry/Geo2rdr.h index 2b1396dd2..71ae585dc 100644 --- a/cxx/isce3/geometry/Geo2rdr.h +++ b/cxx/isce3/geometry/Geo2rdr.h @@ -94,6 +94,13 @@ class isce3::geometry::Geo2rdr { */ void numiter(int n) { _numiter = n; } + /** + * Set lines to be processed per block + * + * @param[in] linesPerBlock Lines to be processed per block + */ + void linesPerBlock(size_t linesPerBlock) { _linesPerBlock = linesPerBlock; } + /** * Run geo2rdr with offsets and externally created offset rasters * @@ -151,6 +158,9 @@ class isce3::geometry::Geo2rdr { /** Return number of Newton-Raphson iterations used for processing */ int numiter() const { return _numiter; } + /** Get linesPerBlock */ + size_t linesPerBlock() const { return _linesPerBlock; } + private: /** Print information for debugging */ diff --git a/cxx/isce3/geometry/RTC.cpp b/cxx/isce3/geometry/RTC.cpp index 43e4a3c4b..40b19d9ad 100644 --- a/cxx/isce3/geometry/RTC.cpp +++ b/cxx/isce3/geometry/RTC.cpp @@ -1265,11 +1265,8 @@ void _RunBlock(const int jmax, const int block_size, getDemCoords = getDemCoordsDiffEpsg; } - const int dem_margin_in_pixels = 100; - auto error_code = loadDemFromProj(dem_raster, minX, maxX, minY, maxY, - &dem_interp_block, proj, dem_margin_in_pixels, - dem_margin_in_pixels); + &dem_interp_block, proj); if (error_code != isce3::error::ErrorCode::Success) { return; diff --git a/cxx/isce3/geometry/RTC.h b/cxx/isce3/geometry/RTC.h index 47a5bc756..87823cf9c 100644 --- a/cxx/isce3/geometry/RTC.h +++ b/cxx/isce3/geometry/RTC.h @@ -329,10 +329,34 @@ void computeRtcAreaProj(isce3::io::Raster& dem, isce3::core::dataInterpMethod::BIQUINTIC_METHOD, double threshold = 1e-8, int num_iter = 100, double delta_range = 1e-8); +/* + Interpolate DEM at position (x, y) considering that input_proj and + dem_interp have same coordinate systems. The function is written to + have the same interface of getDemCoordsDiffEpsg() + * @param[in] x X-coordinate in input coordinates + * @param[in] y Y-coordinate in input coordinates + * @param[in] dem_interp DEM interpolation object + * @param[in] input_proj Input projection object + * @returns 3-elements vector containing the x and + * y coordinates over DEM projection coordinates, and interpolated + * DEM value at that position: {x_dem, y_dem, z_dem} + */ inline isce3::core::Vec3 getDemCoordsSameEpsg(double x, double y, const DEMInterpolator& dem_interp, isce3::core::ProjectionBase* input_proj); +/* + Convert x and y coordinates to from input_proj coordinates to + DEM (dem_interp) coordinates and interpolate DEM at that position. + 3-elements vector containing the + * @param[in] x X-coordinate in input coordinates + * @param[in] y Y-coordinate in input coordinates + * @param[in] dem_interp DEM interpolation object + * @param[in] input_proj Input projection object + * @returns 3-elements vector containing the x and + * y coordinates over DEM projection coordinates, and interpolated + * DEM value at that position: {x_dem, y_dem, z_dem} + */ inline isce3::core::Vec3 getDemCoordsDiffEpsg(double x, double y, const DEMInterpolator& dem_interp, isce3::core::ProjectionBase* input_proj); @@ -355,8 +379,8 @@ isce3::error::ErrorCode loadDemFromProj(isce3::io::Raster& dem_raster, const double minX, const double maxX, const double minY, const double maxY, DEMInterpolator* dem_interp_block, isce3::core::ProjectionBase* proj = nullptr, - const int dem_margin_x_in_pixels = 50, - const int dem_margin_y_in_pixels = 50); + const int dem_margin_x_in_pixels = 100, + const int dem_margin_y_in_pixels = 100); void areaProjIntegrateSegment(double y1, double y2, double x1, double x2, int length, int width, diff --git a/cxx/isce3/geometry/Topo.cpp b/cxx/isce3/geometry/Topo.cpp index 421eed683..09be882f0 100644 --- a/cxx/isce3/geometry/Topo.cpp +++ b/cxx/isce3/geometry/Topo.cpp @@ -20,6 +20,7 @@ #include // isce3::geometry +#include #include "DEMInterpolator.h" #include "TopoLayers.h" @@ -143,6 +144,9 @@ topo(Raster & demRaster, TopoLayers & layers) const double endingRange = _radarGrid.endingRange(); const double midRange = _radarGrid.midRange(); + info << "DEM EPSG: " << demRaster.getEPSG() << pyre::journal::newline; + info << "Output EPSG: " << _epsgOut << pyre::journal::endl; + // Loop over blocks size_t totalconv = 0; for (size_t block = 0; block < nBlocks; ++block) { @@ -619,16 +623,39 @@ _setOutputTopoLayers(Vec3 & targetLLH, TopoLayers & layers, size_t line, } layers.hdg(line, bin, heading); + // Project output coordinates to DEM coordinates + auto input_coords_llh = _proj->inverse({x, y, targetLLH[2]}); + Vec3 dem_vect = demInterp.proj()->forward(input_coords_llh); + // East-west slope using central difference - double aa = demInterp.interpolateXY(x - demInterp.deltaX(), y); - double bb = demInterp.interpolateXY(x + demInterp.deltaX(), y); - double gamma = targetLLH[1]; - double alpha = ((bb - aa) * degrees) / (2.0 * _ellipsoid.rEast(gamma) * demInterp.deltaX()); + double aa = demInterp.interpolateXY(dem_vect[0] - demInterp.deltaX(), dem_vect[1]); + double bb = demInterp.interpolateXY(dem_vect[0] + demInterp.deltaX(), dem_vect[1]); + + Vec3 dem_vect_p_dx = {dem_vect[0] + demInterp.deltaX(), dem_vect[1], dem_vect[2]}; + Vec3 dem_vect_m_dx = {dem_vect[0] - demInterp.deltaX(), dem_vect[1], dem_vect[2]}; + Vec3 input_coords_llh_p_dx, input_coords_llh_m_dx; + demInterp.proj()->inverse(dem_vect_p_dx, input_coords_llh_p_dx); + demInterp.proj()->inverse(dem_vect_m_dx, input_coords_llh_m_dx); + const Vec3 input_coords_xyz_p_dx = _ellipsoid.lonLatToXyz(input_coords_llh_p_dx); + const Vec3 input_coords_xyz_m_dx = _ellipsoid.lonLatToXyz(input_coords_llh_m_dx); + double dx = (input_coords_xyz_p_dx - input_coords_xyz_m_dx).norm(); + + double alpha = (bb - aa) / dx; // North-south slope using central difference - aa = demInterp.interpolateXY(x, y - demInterp.deltaY()); - bb = demInterp.interpolateXY(x, y + demInterp.deltaY()); - double beta = ((bb - aa) * degrees) / (2.0 * _ellipsoid.rNorth(gamma) * demInterp.deltaY()); + aa = demInterp.interpolateXY(dem_vect[0], dem_vect[1] - demInterp.deltaY()); + bb = demInterp.interpolateXY(dem_vect[0], dem_vect[1] + demInterp.deltaY()); + + Vec3 dem_vect_p_dy = {dem_vect[0], dem_vect[1] + demInterp.deltaY(), dem_vect[2]}; + Vec3 dem_vect_m_dy = {dem_vect[0], dem_vect[1] - demInterp.deltaY(), dem_vect[2]}; + Vec3 input_coords_llh_p_dy, input_coords_llh_m_dy; + demInterp.proj()->inverse(dem_vect_p_dy, input_coords_llh_p_dy); + demInterp.proj()->inverse(dem_vect_m_dy, input_coords_llh_m_dy); + const Vec3 input_coords_xyz_p_dy = _ellipsoid.lonLatToXyz(input_coords_llh_p_dy); + const Vec3 input_coords_xyz_m_dy = _ellipsoid.lonLatToXyz(input_coords_llh_m_dy); + double dy = (input_coords_xyz_p_dy - input_coords_xyz_m_dy).norm(); + + double beta = (bb - aa) / dy; // Compute local incidence angle const Vec3 enunorm = enu.normalized(); @@ -675,6 +702,17 @@ setLayoverShadow(TopoLayers& layers, DEMInterpolator& demInterp, // Initialize mask to zero for this block layers.mask() = 0; + // Prepare function getDemCoords() to interpolate DEM + std::function getDemCoords; + + if (_epsgOut == demInterp.epsgCode()) { + getDemCoords = isce3::geometry::getDemCoordsSameEpsg; + } else { + getDemCoords = isce3::geometry::getDemCoordsDiffEpsg; + } + // Loop over lines in block #pragma omp parallel for firstprivate(x, y, ctrack, ctrackGrid, \ slantRangeGrid, maskGrid) @@ -720,12 +758,11 @@ setLayoverShadow(TopoLayers& layers, DEMInterpolator& demInterp, const double y_grid = y[k] * frac1 + y[k+1] * frac2; // Interpolate DEM at x/y - const float z_grid = demInterp.interpolateXY(x_grid, y_grid); + Vec3 demXYZ = getDemCoords(x_grid, y_grid, demInterp, _proj); // Convert DEM XYZ to ECEF XYZ Vec3 llh, xyz, satToGround; - Vec3 demXYZ{x_grid, y_grid, z_grid}; - _proj->inverse(demXYZ, llh); + demInterp.proj()->inverse(demXYZ, llh); _ellipsoid.lonLatToXyz(llh, xyz); // Compute and save slant range diff --git a/cxx/isce3/geometry/Topo.h b/cxx/isce3/geometry/Topo.h index 2561c3e9c..68ed62efa 100644 --- a/cxx/isce3/geometry/Topo.h +++ b/cxx/isce3/geometry/Topo.h @@ -142,6 +142,13 @@ class isce3::geometry::Topo { */ void decimaldegMargin(double deg) { _margin = deg; } + /** + * Set lines to be processed per block + * + * @param[in] linesPerBlock Lines to be processed per block + */ + void linesPerBlock(size_t linesPerBlock) { _linesPerBlock = linesPerBlock; } + // Get topo processing options /** Get distance convergence threshold used for processing */ @@ -171,6 +178,9 @@ class isce3::geometry::Topo { /** Get margin in decimal degrees */ double decimaldegMargin() const { return _margin; } + /** Get linesPerBlock */ + size_t linesPerBlock() const { return _linesPerBlock; } + /** Get read-only reference to RadarGridParameters */ const isce3::product::RadarGridParameters & radarGridParameters() const { return _radarGrid; } diff --git a/cxx/isce3/geometry/metadataCubes.cpp b/cxx/isce3/geometry/metadataCubes.cpp index 3d87718c2..192ec9bbe 100644 --- a/cxx/isce3/geometry/metadataCubes.cpp +++ b/cxx/isce3/geometry/metadataCubes.cpp @@ -59,7 +59,7 @@ static void writeArray(isce3::io::Raster* raster, } } -static inline void writeVectorDerivedCubes(const int array_pos_i, +inline void writeVectorDerivedCubes(const int array_pos_i, const int array_pos_j, const double native_azimuth_time, const isce3::core::Vec3& target_llh, const isce3::core::Vec3& target_proj, const isce3::core::Orbit& orbit, @@ -76,7 +76,9 @@ static inline void writeVectorDerivedCubes(const int array_pos_i, isce3::io::Raster* along_track_unit_vector_y_raster, isce3::core::Matrix& along_track_unit_vector_y_array, isce3::io::Raster* elevation_angle_raster, - isce3::core::Matrix& elevation_angle_array) + isce3::core::Matrix& elevation_angle_array, + isce3::io::Raster* ground_track_velocity_raster, + isce3::core::Matrix& ground_track_velocity_array) { const int i = array_pos_i; @@ -99,6 +101,13 @@ static inline void writeVectorDerivedCubes(const int array_pos_i, // Get target position in ECEF (target_xyz) const isce3::core::Vec3 target_xyz = ellipsoid.lonLatToXyz(target_llh); + // Ground-track velocity + if (ground_track_velocity_raster != nullptr) { + const double ground_velocity = + target_xyz.norm() * vel_xyz.norm() / sat_xyz.norm(); + ground_track_velocity_array(i, j) = ground_velocity; + } + // Create target-to-sat vector in ECEF const isce3::core::Vec3 look_vector_xyz = (sat_xyz - target_xyz).normalized(); @@ -270,6 +279,7 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, isce3::io::Raster* along_track_unit_vector_x_raster, isce3::io::Raster* along_track_unit_vector_y_raster, isce3::io::Raster* elevation_angle_raster, + isce3::io::Raster* ground_track_velocity_raster, const double threshold_geo2rdr, const int numiter_geo2rdr, const double delta_range) { @@ -311,6 +321,8 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, getNanArray(along_track_unit_vector_y_raster, geogrid); auto elevation_angle_array = getNanArray(elevation_angle_raster, geogrid); + auto ground_track_velocity_array = + getNanArray(ground_track_velocity_raster, geogrid); double azimuth_time = radar_grid.sensingMid(); double native_azimuth_time = radar_grid.sensingMid(); @@ -360,7 +372,8 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, los_unit_vector_y_raster == nullptr && along_track_unit_vector_x_raster == nullptr && along_track_unit_vector_y_raster == nullptr && - elevation_angle_raster == nullptr) { + elevation_angle_raster == nullptr && + ground_track_velocity_raster == nullptr) { continue; } @@ -390,8 +403,11 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, along_track_unit_vector_x_raster, along_track_unit_vector_x_array, along_track_unit_vector_y_raster, - along_track_unit_vector_y_array, elevation_angle_raster, - elevation_angle_array); + along_track_unit_vector_y_array, + elevation_angle_raster, + elevation_angle_array, + ground_track_velocity_raster, + ground_track_velocity_array); } } @@ -407,6 +423,8 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, writeArray(along_track_unit_vector_y_raster, along_track_unit_vector_y_array, height_count); writeArray(elevation_angle_raster, elevation_angle_array, height_count); + writeArray(ground_track_velocity_raster, ground_track_velocity_array, + height_count); } double geotransform[] = { @@ -445,6 +463,10 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, elevation_angle_raster->setGeoTransform(geotransform); elevation_angle_raster->setEPSG(geogrid.epsg()); } + if (ground_track_velocity_raster != nullptr) { + ground_track_velocity_raster->setGeoTransform(geotransform); + ground_track_velocity_raster->setEPSG(geogrid.epsg()); + } } void makeGeolocationGridCubes( @@ -461,6 +483,7 @@ void makeGeolocationGridCubes( isce3::io::Raster* along_track_unit_vector_x_raster, isce3::io::Raster* along_track_unit_vector_y_raster, isce3::io::Raster* elevation_angle_raster, + isce3::io::Raster* ground_track_velocity_raster, const double threshold_geo2rdr, const int numiter_geo2rdr, const double delta_range) { @@ -501,6 +524,8 @@ void makeGeolocationGridCubes( getNanArrayRadarGrid(along_track_unit_vector_y_raster, radar_grid); auto elevation_angle_array = getNanArrayRadarGrid(elevation_angle_raster, radar_grid); + auto ground_track_velocity_array = + getNanArrayRadarGrid(ground_track_velocity_raster, radar_grid); auto height = heights[height_count]; double native_azimuth_time = radar_grid.sensingMid(); @@ -550,7 +575,8 @@ void makeGeolocationGridCubes( los_unit_vector_y_raster == nullptr && along_track_unit_vector_x_raster == nullptr && along_track_unit_vector_y_raster == nullptr && - elevation_angle_raster == nullptr) { + elevation_angle_raster == nullptr && + ground_track_velocity_raster == nullptr) { continue; } @@ -579,8 +605,11 @@ void makeGeolocationGridCubes( along_track_unit_vector_x_raster, along_track_unit_vector_x_array, along_track_unit_vector_y_raster, - along_track_unit_vector_y_array, elevation_angle_raster, - elevation_angle_array); + along_track_unit_vector_y_array, + elevation_angle_raster, + elevation_angle_array, + ground_track_velocity_raster, + ground_track_velocity_array); } } writeArray(coordinate_x_raster, coordinate_x_array, height_count); @@ -595,6 +624,8 @@ void makeGeolocationGridCubes( writeArray(along_track_unit_vector_y_raster, along_track_unit_vector_y_array, height_count); writeArray(elevation_angle_raster, elevation_angle_array, height_count); + writeArray(ground_track_velocity_raster, ground_track_velocity_array, + height_count); } } } diff --git a/cxx/isce3/geometry/metadataCubes.h b/cxx/isce3/geometry/metadataCubes.h index aad104f93..68e8582e6 100644 --- a/cxx/isce3/geometry/metadataCubes.h +++ b/cxx/isce3/geometry/metadataCubes.h @@ -37,8 +37,10 @@ namespace isce3 { namespace geometry { * @param[out] along_track_unit_vector_y_array Along-track unit vector Y array * @param[out] elevation_angle_raster Elevation cube raster * @param[out] elevation_angle_array Elevation cube array + * @param[out] ground_track_velocity_raster Ground-track velocity raster + * @param[out] ground_track_velocity_array Ground-track velocity array */ -static inline void writeVectorDerivedCubes(const int array_pos_i, +inline void writeVectorDerivedCubes(const int array_pos_i, const int array_pos_j, const double native_azimuth_time, const isce3::core::Vec3& target_llh, const isce3::core::Vec3& target_proj, const isce3::core::Orbit& orbit, @@ -55,7 +57,9 @@ static inline void writeVectorDerivedCubes(const int array_pos_i, isce3::io::Raster* along_track_unit_vector_y_raster, isce3::core::Matrix& along_track_unit_vector_y_array, isce3::io::Raster* elevation_angle_raster, - isce3::core::Matrix& elevation_angle_array); + isce3::core::Matrix& elevation_angle_array, + isce3::io::Raster* ground_track_velocity_raster, + isce3::core::Matrix& ground_track_velocity_array); /** Make metadata radar grid cubes * @@ -107,6 +111,7 @@ static inline void writeVectorDerivedCubes(const int array_pos_i, * cube raster * @param[out] elevation_angle_raster Elevation angle (in degrees wrt * geodedic nadir) cube raster + * @param[out] ground_track_velocity_raster Ground-track velocity raster * @param[in] threshold_geo2rdr Range threshold for geo2rdr * @param[in] numiter_geo2rdr Geo2rdr maximum number of iterations * @param[in] delta_range Step size used for computing @@ -126,6 +131,7 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, isce3::io::Raster* along_track_unit_vector_x_raster = nullptr, isce3::io::Raster* along_track_unit_vector_y_raster = nullptr, isce3::io::Raster* elevation_angle_raster = nullptr, + isce3::io::Raster* ground_track_velocity_raster = nullptr, const double threshold_geo2rdr = 1e-8, const int numiter_geo2rdr = 100, const double delta_range = 1e-8); @@ -176,6 +182,7 @@ void makeRadarGridCubes(const isce3::product::RadarGridParameters& radar_grid, * cube raster * @param[out] elevation_angle_raster Elevation angle (in degrees wrt * geodedic nadir) cube raster + * @param[out] ground_track_velocity_raster Ground-track velocity raster * @param[in] threshold_geo2rdr Range threshold for geo2rdr * @param[in] numiter_geo2rdr Geo2rdr maximum number of iterations * @param[in] delta_range Step size used for computing @@ -190,11 +197,12 @@ void makeGeolocationGridCubes( isce3::io::Raster* coordinate_x_raster = nullptr, isce3::io::Raster* coordinate_y_raster = nullptr, isce3::io::Raster* incidence_angle_raster = nullptr, - isce3::io::Raster* losUnitVectorX_raster = nullptr, - isce3::io::Raster* losUnitVectorY_raster = nullptr, - isce3::io::Raster* alongTrackUnitVectorX_raster = nullptr, - isce3::io::Raster* alongTrackUnitVectorY_raster = nullptr, - isce3::io::Raster* elevationAngle_raster = nullptr, + isce3::io::Raster* los_unit_vector_x_raster = nullptr, + isce3::io::Raster* los_unit_vector_y_raster = nullptr, + isce3::io::Raster* along_track_unit_vector_x_raster = nullptr, + isce3::io::Raster* along_track_unit_vector_y_raster = nullptr, + isce3::io::Raster* elevation_angle_raster = nullptr, + isce3::io::Raster* ground_track_velocity_raster = nullptr, const double threshold_geo2rdr = 1e-8, const int numiter_geo2rdr = 100, const double delta_range = 1e-8); diff --git a/cxx/isce3/image/ResampSlc.h b/cxx/isce3/image/ResampSlc.h index e5d601207..4890857f1 100644 --- a/cxx/isce3/image/ResampSlc.h +++ b/cxx/isce3/image/ResampSlc.h @@ -8,6 +8,8 @@ #include #include +#include + #include #include #include diff --git a/cxx/isce3/io/Raster.cpp b/cxx/isce3/io/Raster.cpp index f78281649..b908ec3cd 100644 --- a/cxx/isce3/io/Raster.cpp +++ b/cxx/isce3/io/Raster.cpp @@ -233,7 +233,7 @@ int isce3::io::Raster::setEPSG(int epsgcode) // Destructor. When GDALOpenShared() is used the dataset is dereferenced // and closed only if the referenced count is less than 1. isce3::io::Raster::~Raster() { - if (_owner) { + if (_owner and _dataset != nullptr) { GDALClose( _dataset ); } } diff --git a/cxx/isce3/io/Raster.h b/cxx/isce3/io/Raster.h index 746fc4dfc..9bd6853d5 100644 --- a/cxx/isce3/io/Raster.h +++ b/cxx/isce3/io/Raster.h @@ -24,8 +24,6 @@ #include -//#include - /** Data structure meant to handle Raster I/O operations. * * This is currently a thin wrapper over GDAL's Dataset class with some simpler @@ -48,11 +46,43 @@ class isce3::io::Raster { /** Constructor to create a 1 band dataset with default Driver */ Raster(const std::string& fname, size_t width, size_t length, GDALDataType dtype = isce3::io::defaultGDALDataType); - /** Constructor for a 1 band dataset from isce3::core::Matrix */ - template Raster(isce3::core::Matrix &matrix); - // Constructor for a 1 band dataset from isce3::core::Matrix::view_type - template Raster(pyre::grid::View &view); + template Raster(Eigen::PlainObjectBase &view); + + // Constructor for a 1 band dataset from isce3::core::Matrix view type + template + Raster(Eigen::Block& view) + { + using Scalar = typename Eigen::PlainObjectBase::value_type; + + //Get the packing. Update with pyre error logging. + if (!view.IsRowMajor) + { + throw std::runtime_error("Input view is not packed in row major order"); + } + + //Size of each element + size_t bytesperunit = sizeof(Scalar); + + //Pointer and offset math + size_t pixeloffset = (const char*) &view(0, 1) - (const char*) &view(0, 0); + size_t lineoffset = (const char*) &view(1, 0) - (const char*) &view(0, 0); + + //Update with pyre error logging + if ((pixeloffset < bytesperunit) || (lineoffset < bytesperunit)) + { + throw std::runtime_error("Invalid pixel/line offset"); + } + + initFromPointer(view.data(), + asGDT, + view.cols(), + view.rows(), + pixeloffset, + lineoffset); + } + + /** Create new raster object like another */ Raster(const std::string& fname, const Raster& rast); @@ -102,6 +132,9 @@ class isce3::io::Raster { * @param[in] ds GDALDataset pointer*/ inline void dataset(GDALDataset* ds) { _dataset=ds; } + /** GDALDataset owner getter*/ + inline bool dataset_owner() const { return _owner; } + /** Return GDALDatatype of specified band * * @param[in] band Band number in 1-index*/ @@ -160,10 +193,6 @@ class isce3::io::Raster { template void getBlock(isce3::core::Matrix& mat, size_t xidx, size_t yidx, size_t band = 1); template void setBlock(isce3::core::Matrix& mat, size_t xidx, size_t yidx, size_t band = 1); //2D block read/write for Matrix, optional band index - /** Read/Write block of data from given band to/from Matrix::view_type */ - template void getSetBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band, GDALRWFlag iodir); - template void getBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band = 1); - template void setBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band = 1); /** Read/write block of data from given band to/from EArray2D */ template void getBlock(isce3::core::EArray2D& mat, size_t xidx, size_t yidx, size_t band = 1); @@ -173,6 +202,50 @@ class isce3::io::Raster { template void getBlock(isce3::core::EMatrix2D& mat, size_t xidx, size_t yidx, size_t band = 1); template void setBlock(isce3::core::EMatrix2D& mat, size_t xidx, size_t yidx, size_t band = 1); + template + void setBlock(const Eigen::Block& block, int xoff, int yoff, int band = 1) { + + using T = typename Eigen::Block::value_type; + + const int nxsize = block.cols(); + const int nysize = block.rows(); + + const size_t line_spacing = (char*) &block(1, 0) - (char*) &block(0, 0); + + auto iodir = GF_Write; + auto iostat = _dataset->GetRasterBand(band)->RasterIO( + iodir, xoff, yoff, nxsize, nysize, + (void*) &block(0, 0), nxsize, nysize, asGDT, + sizeof(T), line_spacing); + + if (iostat != CPLE_None) { // RasterIO returned errors + throw std::runtime_error( + "Raster::getBlock(Eigen::Block): error in RasterIO"); + } + } + + template + void getBlock(Eigen::Block& block, int xoff, int yoff, int band = 1) const { + + using T = typename Eigen::Block::value_type; + + const int nxsize = block.cols(); + const int nysize = block.rows(); + + const size_t line_spacing = (char*) &block(1, 0) - (char*) &block(0, 0); + + auto iodir = GF_Read; + auto iostat = _dataset->GetRasterBand(band)->RasterIO( + iodir, xoff, yoff, nxsize, nysize, + (void*) &block(0, 0), nxsize, nysize, asGDT, + sizeof(T), line_spacing); + + if (iostat != CPLE_None) { // RasterIO returned errors + throw std::runtime_error( + "Raster::getBlock(Eigen::Block): error in RasterIO"); + } + } + //Functions to deal with projections and geotransform information /** Return EPSG code corresponding to raster*/ int getEPSG() const; diff --git a/cxx/isce3/io/Raster.icc b/cxx/isce3/io/Raster.icc index 471967de0..2d3b9326c 100644 --- a/cxx/isce3/io/Raster.icc +++ b/cxx/isce3/io/Raster.icc @@ -54,76 +54,35 @@ inline void isce3::io::Raster::initFromPointer(void* ptr, //Raw pointer } -// Constructor for a 1 band dataset from isce3::core::Matrix -template -isce3::io::Raster::Raster(isce3::core::Matrix &matrix) +// Constructor for a 1 band dataset from isce3::core::Matrix view type +template +isce3::io::Raster::Raster(Eigen::PlainObjectBase& view) { - auto byteoffset = matrix.byteoffset(); - initFromPointer(matrix.data(), - asGDT, - matrix.width(), - matrix.length(), - byteoffset[1], - byteoffset[0]); -} - - -// Constructor for a 1 band dataset from isce3::core::Matrix::grid_type::view_type -template -isce3::io::Raster::Raster(pyre::grid::View& view) -{ - //If we add a similar byteoffset method to pyre::grid::View, we could use that - //For now, we need to do the math ourselves - //Get the layout. Update with pyre error logging. - auto layout = view.layout(); - if (layout.dim() != 2) - { - std::cout << "Input is not a 2D view \n"; - return; - } + using Scalar = typename Eigen::PlainObjectBase::value_type; //Get the packing. Update with pyre error logging. - auto packing = view.slice().packing(); - if ((packing[0] != 1u) || (packing[1] != 0u)) + if (!view.IsRowMajor) { - std::cout << "Input view is not packed in row major order\n"; - return; + throw std::runtime_error("Input view is not packed in row major order"); } //Size of each element - size_t bytesperunit = sizeof(typename T::cell_type); - //std::cout << "Element size in bytes: " << bytesperunit << "\n"; - - //Get shape - auto shape = view.slice().shape(); - //std::cout << "Shape of view: " << shape[0] << " x " << shape[1] << "\n"; - - //Address to the first element of the view - auto iterator = view.begin(); - auto startoffset = &(*iterator); - - //Address to the next element in fast direction (assuming Row Major) - auto incrementoffset = &(*(++iterator)); - - //Address to the last element of the view - auto endoffset = &(*view.end()); + size_t bytesperunit = sizeof(Scalar); //Pointer and offset math - size_t pixeloffset = (incrementoffset - startoffset) * bytesperunit; - size_t totalbyteoffset = (endoffset - startoffset) * bytesperunit; - size_t lineoffset = (totalbyteoffset - shape[1] * pixeloffset) / shape[0]; + size_t pixeloffset = (char*) &view(0, 1) - (char*) &view(0, 0); + size_t lineoffset = (char*) &view(1, 0) - (char*) &view(0, 0); //Update with pyre error logging if ((pixeloffset < bytesperunit) || (lineoffset < bytesperunit)) { - std::cout << "Something looks fishy in the offset estimation \n"; - return; + throw std::runtime_error("Something looks fishy in the offset estimation"); } - initFromPointer(startoffset, - asGDT, - shape[1], - shape[0], + initFromPointer(view.data(), + asGDT, + view.cols(), + view.rows(), pixeloffset, lineoffset); } @@ -491,89 +450,6 @@ void isce3::io::Raster::setBlock(isce3::core::EMatrix2D& mat, size_t xidx, si setBlock(mat.data(), xidx, yidx, mat.cols(), mat.rows(), band); } - -// Get/Set a block of data for given x/y position and band from Matrix::view_type -template -void isce3::io::Raster::getSetBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band, GDALRWFlag iodir) -{ - //Ensure that the view is 2D and Row major - //Unclear if view currently supports numpy-like squeeze - //functionality for higher dimension grids - //Might need updates if view supports such features - //and we wish to support 2D subset views - - //Get the layout. Update with pyre error logging. - auto layout = view.layout(); - if (layout.dim() != 2) - { - std::cout << "Input is not a 2D view \n"; - return; - } - - //Get the packing. Update with pyre error logging. - auto packing = view.slice().packing(); - if ((packing[0] != 1u) || (packing[1] != 0u)) - { - std::cout << "Input view is not packed in row major order\n"; - return; - } - - //Size of each element - size_t bytesperunit = sizeof(typename T::cell_type); - //std::cout << "Element size in bytes: " << bytesperunit << "\n"; - - //Get shape - auto shape = view.slice().shape(); - //std::cout << "Shape of view: " << shape[0] << " x " << shape[1] << "\n"; - - //Address to the first element of the view - auto iterator = view.begin(); - auto startoffset = &(*iterator); - - //Address to the next element in fast direction (assuming Row Major) - auto incrementoffset = &(*(++iterator)); - - //Address to the last element of the view - auto endoffset = &(*view.end()); - - //Pointer and offset math - size_t pixeloffset = (incrementoffset - startoffset) * bytesperunit; - size_t totalbyteoffset = (endoffset - startoffset) * bytesperunit; - size_t lineoffset = (totalbyteoffset - shape[1] * pixeloffset) / shape[0]; - - //Update with pyre error logging - if ((pixeloffset < bytesperunit) || (lineoffset < bytesperunit)) - { - std::cout << "Something looks fishy in the offset estimation \n"; - return; - } - - //std::cout << "Start: " << startoffset << "\n" - // << "Pixel offset: " << pixeloffset << "\n" - // << "Line offset: " << lineoffset << "\n"; - - auto iostat = _dataset->GetRasterBand(band)->RasterIO(iodir, xidx, yidx, - shape[1], shape[0], startoffset, shape[1], shape[0], - asGDT, pixeloffset, lineoffset); - - if (iostat != CPLE_None) // RasterIO returned errors - std::cout << "In isce3::io::Raster::get/setValue() - error in RasterIO." << std::endl; -} - -//Get block of data from given band -template -void isce3::io::Raster::getBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band) -{ - this->getSetBlock(view, xidx, yidx, band, GF_Read); -} - -//Set block of data from given band -template -void isce3::io::Raster::setBlock(pyre::grid::View& view, size_t xidx, size_t yidx, size_t band) -{ - this->getSetBlock(view, xidx, yidx, band, GF_Write); -} - /** * @param[in] arr Array of 6 double precision numbers * diff --git a/cxx/isce3/math/RootFind1dNewton.cpp b/cxx/isce3/math/RootFind1dNewton.cpp new file mode 100644 index 000000000..963052574 --- /dev/null +++ b/cxx/isce3/math/RootFind1dNewton.cpp @@ -0,0 +1,74 @@ +#include "RootFind1dNewton.h" + +#include + +#include + +namespace isce3 { namespace math { + +// regular methods +typename RootFind1dNewton::tuple4_t RootFind1dNewton::root( + const poly1d_t& f, double x0) const +{ + // check the order/degree of the polynomial + if (f.order <= 1) + throw isce3::except::InvalidArgument( + ISCE_SRCINFO(), "Polynomial degree must be at least 1!"); + return root(poly2func(f), poly2func(f.derivative()), x0); +} + +typename RootFind1dNewton::tuple4_t RootFind1dNewton::root( + const func2_t& f, double x0) const +{ + // if optional x_tol exists run stricter Newton approach + if (x_tol) + return _newton_strict(f, x0); + // otherwise run regular Newton + return _newton(f, x0); +} + +typename RootFind1dNewton::tuple4_t RootFind1dNewton::root( + const func_t& f, const func_t& f_der, double x0) const +{ + // create a single function from two functions (f, f_der) + func2_t f2 = [=](double x) { return std::make_tuple(f(x), f_der(x)); }; + // call the other overloaded root method + return root(f2, x0); +} + +// helper functions +typename RootFind1dNewton::tuple4_t RootFind1dNewton::_newton( + const func2_t& f, double x0) const +{ + // setting initial values and iterations + auto x1 = x0; + int n_itr {0}; + auto [f_val, fp_eval] = f(x1); + do { + if (std::abs(fp_eval) > 0.0) + x1 -= f_val / fp_eval; + std::tie(f_val, fp_eval) = f(x1); + ++n_itr; + } while (std::abs(f_val) > f_tol && n_itr < max_iter); + bool flag {(std::abs(f_val) <= f_tol && n_itr <= max_iter)}; + return {x1, f_val, flag, n_itr}; +} + +typename RootFind1dNewton::tuple4_t RootFind1dNewton::_newton_strict( + const func2_t& f, double x0) const +{ + int n_itr {0}; + auto x = x0; + auto [f_val, fp_eval] = f(x); + double dx; + do { + dx = (std::abs(fp_eval) > 0.0) ? -f_val / fp_eval : 0.0; + x += dx; + std::tie(f_val, fp_eval) = f(x); + ++n_itr; + } while ((std::abs(f_val) > f_tol || dx > *x_tol) && n_itr < max_iter); + bool flag {(std::abs(f_val) <= f_tol && dx <= *x_tol && n_itr <= max_iter)}; + return {x, f_val, flag, n_itr}; +} + +}} // namespace isce3::math diff --git a/cxx/isce3/math/RootFind1dNewton.h b/cxx/isce3/math/RootFind1dNewton.h new file mode 100644 index 000000000..da107662b --- /dev/null +++ b/cxx/isce3/math/RootFind1dNewton.h @@ -0,0 +1,112 @@ +// 1-D Newton Root finding class +#pragma once + +#include +#include + +#include + +#include "detail/RootFind1dBase.h" + +namespace isce3 { namespace math { + +/** + * A class with overloaded method to solve 1-D (single variable) + * equation f(x)=0 with at least one real-value root/solution via + * Newton approach See Newton method. + */ +class RootFind1dNewton : public detail::RootFind1dBase { + // aliases +protected: + using func_t = std::function; + using tuple4_t = std::tuple; + using poly1d_t = isce3::core::Poly1d; + using func2_t = std::function(double)>; + +public: + // inherit all constructors from base class + using detail::RootFind1dBase::RootFind1dBase; + + // methods + /** + * Find a root of the function "f(x)" closest to its initial value via + * Newton approach. + * @param[in] f isce3.core.Poly1d object + * expressing 1-D function "f(x)" as polynomial. + * @param[in] x0 (optional) initial guess of the "x". Default is 0.0. + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + * @exception InvalidArgument + */ + std::tuple root( + const isce3::core::Poly1d& f, double x0 = 0) const; + + /** + * Find a root of the function "f(x)" closest to its initial value via + * Newton approach. + * @param[in] f single-variable function object to represent "f(x)". + * @param[in] f_der single-variable function object to represent derivative + * of "f(x)". + * @param[in] x0 (optional) initial guess of the "x". Default is 0.0. + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + */ + std::tuple root( + const std::function& f, + const std::function& f_der, double x0 = 0) const; + + /** + * Find a root of the function "f(x)" closest to its initial value via + * Newton approach. + * @param[in] f single-variable function object to return a tuple of "f(x)" + * and "f_der(x)", that is both function value and its first derivative for + * "x". + * @param[in] x0 (optional) initial guess of the "x". Default is 0.0. + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + */ + std::tuple root( + const std::function(double)>& f, + double x0 = 0) const; + +private: + /** + * @internal + * Overloaded helper function for Newton approach. + * @param[in] f single-variable function object to return "f(x)" and + * "f_der(x)", that is a pair of values of function and its first + * derivative. + * @param[in] x0 initial guess of the "x". + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + * @see _newton_strict() + */ + tuple4_t _newton(const func2_t& f, double x0) const; + + /** + * @internal + * Overloaded helper function for Newton approach with extra tolerance + * "x_tol" for a more strict convergence criteria. + * @param[in] f single-variable function object to return "f(x)" and + * "f_der(x)", that is a pair of values of function and its first + * derivative. + * @param[in] x0 initial guess of the "x". + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + * @see _newton() + */ + tuple4_t _newton_strict(const func2_t& f, double x0) const; +}; + +}} // namespace isce3::math diff --git a/cxx/isce3/math/RootFind1dSecant.cpp b/cxx/isce3/math/RootFind1dSecant.cpp new file mode 100644 index 000000000..2e80472b3 --- /dev/null +++ b/cxx/isce3/math/RootFind1dSecant.cpp @@ -0,0 +1,71 @@ +#include "RootFind1dSecant.h" + +#include + +namespace isce3 { namespace math { + +// regular methods +typename RootFind1dSecant::tuple4_t RootFind1dSecant::root( + const func_t& f, double x0, double x1) const +{ + // if optional x_tol exists run stricter Secant approach + if (x_tol) + return _secant_strict(f, x0, x1); + // otherwise run regular Secant + return _secant(f, x0, x1); +} + +// helper functions +typename RootFind1dSecant::tuple4_t RootFind1dSecant::_secant( + const func_t& f, double x0, double x1) const +{ + // setting initial values and iterations + double x {0.0}; + int n_itr {0}; + auto f0 = f(x0); + auto f1 = f(x1); + do { + auto df = f1 - f0; + if (std::abs(df) > 0.0) + x = (x0 * f1 - x1 * f0) / df; + else + x = x1; + // update two initial guesses + x0 = x1; + x1 = x; + f0 = f1; + f1 = f(x1); + ++n_itr; + } while (std::abs(f1) > f_tol && n_itr < max_iter); + bool flag {(std::abs(f1) <= f_tol && n_itr <= max_iter)}; + return {x1, f1, flag, n_itr}; +} + +typename RootFind1dSecant::tuple4_t RootFind1dSecant::_secant_strict( + const func_t& f, double x0, double x1) const +{ + // setting initial values and iterations + double x {0.0}; + int n_itr {0}; + double dx {}; + auto f0 = f(x0); + auto f1 = f(x1); + do { + auto df = f1 - f0; + if (std::abs(df) > 0.0) + x = (x0 * f1 - x1 * f0) / df; + else + x = x1; + dx = std::abs(x - x1); + // update two initial guesses + x0 = x1; + x1 = x; + f0 = f1; + f1 = f(x1); + ++n_itr; + } while ((std::abs(f1) > f_tol || dx > *x_tol) && n_itr < max_iter); + bool flag {(std::abs(f1) <= f_tol && dx <= *x_tol && n_itr <= max_iter)}; + return {x1, f1, flag, n_itr}; +} + +}} // namespace isce3::math diff --git a/cxx/isce3/math/RootFind1dSecant.h b/cxx/isce3/math/RootFind1dSecant.h new file mode 100644 index 000000000..89cadbf15 --- /dev/null +++ b/cxx/isce3/math/RootFind1dSecant.h @@ -0,0 +1,74 @@ +// 1-D Secant Root finding class +#pragma once + +#include +#include + +#include "detail/RootFind1dBase.h" + +namespace isce3 { namespace math { + +/** + * A class with root method to solve 1-D (single variable) + * equation f(x)=0 with at least one real-value root/solution via + * Secant approach See Secant method + */ +class RootFind1dSecant : public detail::RootFind1dBase { + // aliases +protected: + using func_t = std::function; + using tuple4_t = std::tuple; + +public: + // inherit all constructors from base class + using detail::RootFind1dBase::RootFind1dBase; + + /** + * Find a root of the function "f(x)" closest to its initial values via + * Secant approach. + * @param[in] f single-variable function object to represent "f(x)". + * @param[in] x0 first initial guess of the "x". + * @param[in] x1 second guess of the "x". + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + */ + std::tuple root( + const std::function& f, double x0, double x1) const; + +private: + /** + * @internal + * Secant approach with two initial values when simply f(x) is known and + * its derivative is not available. + * @param[in] f single-variable function object to represent "f(x)". + * @param[in] x0 first initial guess of the "x". + * @param[in] x1 second guess of the "x". + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + * @see _secant_strict() + */ + tuple4_t _secant(const func_t& f, double x0, double x1) const; + + /** + * @internal + * A more strict Secant approach with two initial values when simply f(x) is + * known and its derivative is not available. The second tolerance, "x_tol" + * will be used on top of "f_tol". + * @param[in] f single-variable function object to represent "f(x)". + * @param[in] x0 first initial guess of the "x". + * @param[in] x1 second guess of the "x". + * @return solution "x" + * @return function eval "f(x)" + * @return convergence flag (true or false) + * @return number of iterations + * @see _secant() + */ + tuple4_t _secant_strict(const func_t& f, double x0, double x1) const; +}; + +}} // namespace isce3::math diff --git a/cxx/isce3/math/detail/RootFind1dBase.h b/cxx/isce3/math/detail/RootFind1dBase.h new file mode 100644 index 000000000..2f22ed32f --- /dev/null +++ b/cxx/isce3/math/detail/RootFind1dBase.h @@ -0,0 +1,105 @@ +// Base class for all 1-D Root finding classes +#pragma once + +#include +#include + +#include +#include + +namespace isce3 { namespace math { namespace detail { + +/** + * A base class used in other derived classes to solve 1-D (single variable) + * equation f(x)=0 with at least one real-value root/solution. + * Each derived class must represent a unique solver/method. + * All derived class must have a method called "root" returning + * a tuple of four scalars: + * {solution, function value, convergence flag, number of iterations}. + */ +class RootFind1dBase { + +public: + // constructors + /** + * A default constructor with absolute tolerances for "x" and "f(x)" + * plus max number of iterations. + * @param[in] f_tol (optional) absolute tolerance for function eval + * "f(x)". Default is 1e-5. + * @param[in] max_iter (optional) max number of iterations. + * Default is 20. + * @param[in] x_tol (optional) absolute tolerance for function variable "x". + * If not specified or set to {} or std::nullopt, it will be ignored, + * otherwise it will be an extra layer of tolerance checking on top of + * "f_val"! + * @exception InvalidArgument + */ + RootFind1dBase(double f_tol = 1e-5, int max_iter = 20, + std::optional x_tol = {}) + : f_tol {f_tol}, max_iter {max_iter}, x_tol {x_tol} + { + if (max_iter < 1) + throw isce3::except::InvalidArgument( + ISCE_SRCINFO(), "Max number of iterations must be >=1!"); + if (!(f_tol > 0.0)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Tolerance for function value must be positive!"); + if (x_tol) + if (!(*x_tol > 0.0)) + throw isce3::except::InvalidArgument(ISCE_SRCINFO(), + "Tolerance for function variable must be positive!"); + } + + /** + * A constructor with max number of iterations. + * @param[in] max_iter max number of iterations. + * Note that the absolute tolerance for the function value is 1e-5. + * @exception InvalidArgument + */ + RootFind1dBase(int max_iter) : RootFind1dBase(1e-5, max_iter, {}) {} + + // Abstract virtual method needed by all derived class + // virtual std::tuple root(arg, ...) const = 0; + + // non-virtual public methods + + /** + * Get the absolute tolerance for function value + * @return tolerance + */ + double func_tol() const { return f_tol; } + + /** + * Get max number of iteration being set + * @return number of iterations + */ + int max_num_iter() const { return max_iter; } + + /** + * Get the absolute tolerance for function variable "x" if set + * It returns std::nullopt if "x_tol" is not specified at object creation. + * @return optional double precision variable tolerance. + */ + std::optional var_tol() const + { + return x_tol ? *x_tol : std::optional {}; + } + + /** + * Convert isce3 Poly1d object into a single-variavle function object "f(x)" + * @param[in] f isce3 Poly1d object + * @return single-variable function object + */ + static std::function poly2func(const isce3::core::Poly1d& f) + { + return [=](double x) { return f.eval(x); }; + } + + // members +protected: + double f_tol; + int max_iter; + std::optional x_tol; +}; + +}}} // namespace isce3::math::detail diff --git a/cxx/isce3/math/polyfunc.h b/cxx/isce3/math/polyfunc.h index a4e02e940..6bc7808b3 100644 --- a/cxx/isce3/math/polyfunc.h +++ b/cxx/isce3/math/polyfunc.h @@ -10,7 +10,7 @@ #include #include - +/** @namespace isce3::math */ namespace isce3 { namespace math { /** diff --git a/doc/doxygen/references.bib b/doc/doxygen/references.bib index 235933fa0..0e784b873 100644 --- a/doc/doxygen/references.bib +++ b/doc/doxygen/references.bib @@ -176,9 +176,19 @@ @ARTICLE{villano2013 @manual{ReeTechDesDoc, author={H. {Ghaemi}}, title={REE Technical Description}, - organization={JPL}, + institution={JPL}, year={2019}, number={D-100710}, url={https://github.jpl.nasa.gov/SALSA-REE/REE_DOC/blob/master/REE_TECHNICAL_DESCRIPTION.pdf}, email={hirad.ghaemi@jpl.nasa.gov} } + +@techreport{EdgeMethodElPointDoc, + author={H. {Ghaemi}}, + title={Formulation of Rising-Edge Method in EL Pointing Estimation & its Application to ALOS PALSAR DATA}, + institution={JPL}, + year={2020}, + type={Report}, + url={https://github.jpl.nasa.gov/NISAR-POINTING/DOC/blob/master/El_Pointing_Est_Rising_Edge.pdf}, + email={hirad.ghaemi@jpl.nasa.gov} +} \ No newline at end of file diff --git a/python/extensions/pybind_isce3/Sources.cmake b/python/extensions/pybind_isce3/Sources.cmake index 36eadeaf5..19b1b9278 100644 --- a/python/extensions/pybind_isce3/Sources.cmake +++ b/python/extensions/pybind_isce3/Sources.cmake @@ -1,5 +1,6 @@ set(SRCS antenna/antenna.cpp +antenna/edge_method_cost_func.cpp antenna/ElPatternEst.cpp antenna/Frame.cpp antenna/geometryfunc.cpp diff --git a/python/extensions/pybind_isce3/antenna/antenna.cpp b/python/extensions/pybind_isce3/antenna/antenna.cpp index 126734bac..d7b03226d 100644 --- a/python/extensions/pybind_isce3/antenna/antenna.cpp +++ b/python/extensions/pybind_isce3/antenna/antenna.cpp @@ -4,6 +4,7 @@ #include "SphGridType.h" #include "geometryfunc.h" #include "ElPatternEst.h" +#include "edge_method_cost_func.h" namespace py = pybind11; @@ -28,4 +29,5 @@ void addsubmodule_antenna(py::module & m) // for modules with pure functions addbinding_geometryfunc(m_antenna); + addbinding_edge_method_cost_func(m_antenna); } diff --git a/python/extensions/pybind_isce3/antenna/edge_method_cost_func.cpp b/python/extensions/pybind_isce3/antenna/edge_method_cost_func.cpp new file mode 100644 index 000000000..999f4ae81 --- /dev/null +++ b/python/extensions/pybind_isce3/antenna/edge_method_cost_func.cpp @@ -0,0 +1,166 @@ +#include "edge_method_cost_func.h" + +#include +#include +#include +#include + +#include +#include +#include + +// Aliases +namespace py = pybind11; +namespace ant = isce3::antenna; +using poly1d_t = isce3::core::Poly1d; +using linspace_t = isce3::core::Linspace; + +// Functions binding +void addbinding_edge_method_cost_func(py::module& m) +{ + m.def( + "roll_angle_offset_from_edge", + [](const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + const linspace_t& look_ang, + std::optional polyfit_weight = {}) { + return ant::rollAngleOffsetFromEdge( + polyfit_echo, polyfit_ant, look_ang, polyfit_weight); + }, + py::arg("polyfit_echo"), py::arg("polyfit_ant"), + py::arg("look_ang"), py::arg("polyfit_weight") = std::nullopt, + R"( +Estimate roll angle offset via edge method from poly-fitted +power patterns obtained from echo raw data and antenna pattern. +The cost function is solved via Newton method and final solution +is the weighted average of individual solution within look +(off-nadir) angles [near, far] with desired angle precision all +defined by isce3 Linspace. +See equations for cost function in section 1.1 of the reference [1]_. +The only difference is that the look angles are in (rad) rather than in (deg). +Note that the respective magnitudes for both echo and antenna can be +either 2-way or 1-way power patterns. + +Parameters +---------- +polyfit_echo : isce3.core.Poly1d + to represent polyfitted magnitude of echo (range compressed or raw) data. + It must be third-order polynomial of relative magnitude/power in (dB) + as a function of look angle in (rad). +polyfit_ant : isce3.core.Poly1d + to represent polyfitted magnitude of antenna EL power pattern. + It must be third-order polynomial of relative magnitude/power in (dB) + as a function of look angle in (rad). + It must have the same mean and std as that of polyfit_echo. +look_ang : isce3.core.Linspace + to cover desired range of look angles with desired precision/spacing. +polyfit_weight : isce3.core.Poly1d , optional + to represent weightings used in final weighted averaged of individual + solutions over desired look angle coverage. + It shall represent relative magnitude/power in (dB) as a function + of look angle in (rad). The order of polynom must be at least 0 (constant weights). + +Returns +------- +float + roll angle offset (rad) + Note that the roll offset shall be added to EL angles in antenna frame + to align EL power pattern from antenna to the one extracted from echo given + the cost function optimized for offset applied to polyfitted antenna data. +float + max cost function value among all iterations +bool + overall convergence flag (true or false) +int + max number of iterations among all iterations + +Raises +------ +ValueError + for bad input arguments + +Notes +----- +See section 1.1 of references [1]_ for cost function equation. + +References +---------- +..[1] https://github.jpl.nasa.gov/NISAR-POINTING/DOC/blob/master/El_Pointing_Est_Rising_Edge.pdf + +)"); + + m.def( + "roll_angle_offset_from_edge", + [](const poly1d_t& polyfit_echo, const poly1d_t& polyfit_ant, + double look_ang_near, double look_ang_far, + double look_ang_prec, + std::optional polyfit_weight = {}) { + return ant::rollAngleOffsetFromEdge(polyfit_echo, polyfit_ant, + look_ang_near, look_ang_far, look_ang_prec, + polyfit_weight); + }, + py::arg("polyfit_echo"), py::arg("polyfit_ant"), + py::arg("look_ang_near"), py::arg("look_ang_far"), + py::arg("look_ang_prec"), py::arg("polyfit_weight") = std::nullopt, + R"( +Estimate roll angle offset via edge method from poly-fitted +power patterns obtained from echo raw data and antenna pattern. +The cost function is solved via Newton method and final solution +is the weighted average of individual solution within look +(off-nadir) angles [near, far] with desired angle precision . +See equations for cost function in section 1.1 of the reference [1]_. +The only difference is that the look angles are in (rad) rather than in (deg). +Note that the respective magnitudes for both echo and antenna can be +either 2-way or 1-way power patterns. + +Parameters +---------- +polyfit_echo : isce3.core.Poly1d + to represent polyfitted magnitude of echo (range compressed or raw) data. + It must be third-order polynomial of relative magnitude/power in (dB) + as a function of look angle in (rad). +polyfit_ant : isce3.core.Poly1d + to represent polyfitted magnitude of antenna EL power pattern. + It must be third-order polynomial of relative magnitude/power in (dB) + as a function of look angle in (rad). + It must have the same mean and std as that of polyfit_echo. +look_ang_near : float + look angle for near range in (rad) +look_ang_far : float + look angle for far range in (rad) +look_ang_prec : float + look angle precision/resolution in (rad) +polyfit_weight : isce3.core.Poly1d , optional + to represent weightings used in final weighted averaged of individual + solutions over desired look angle coverage. + It shall represent relative magnitude/power in (dB) as a function + of look angle in (rad). The order of polynom must be at least 0 (constant weights). + +Returns +------- +float + roll angle offset (rad) + Note that the roll offset shall be added to EL angles in antenna frame + to align EL power pattern from antenna to the one extracted from echo given + the cost function optimized for offset applied to polyfitted antenna data. +float + max cost function value among all iterations +bool + overall convergence flag (true or false) +int + max number of iterations among all iterations + +Raises +------ +ValueError + for bad input arguments + +Notes +----- +See section 1.1 of references [1]_ for cost function equation. + +References +---------- +..[1] https://github.jpl.nasa.gov/NISAR-POINTING/DOC/blob/master/El_Pointing_Est_Rising_Edge.pdf + +)"); +} diff --git a/python/extensions/pybind_isce3/antenna/edge_method_cost_func.h b/python/extensions/pybind_isce3/antenna/edge_method_cost_func.h new file mode 100644 index 000000000..8f9e805ca --- /dev/null +++ b/python/extensions/pybind_isce3/antenna/edge_method_cost_func.h @@ -0,0 +1,6 @@ +// cost functions for edge method used in EL pointing +#pragma once + +#include + +void addbinding_edge_method_cost_func(pybind11::module& m); diff --git a/python/extensions/pybind_isce3/cuda/geometry/geo2rdr.cpp b/python/extensions/pybind_isce3/cuda/geometry/geo2rdr.cpp index 371c2a2cd..18897676c 100644 --- a/python/extensions/pybind_isce3/cuda/geometry/geo2rdr.cpp +++ b/python/extensions/pybind_isce3/cuda/geometry/geo2rdr.cpp @@ -22,11 +22,13 @@ void addbinding(py::class_ & pyGeo2Rdr) const isce3::core::Ellipsoid & ellipsoid, const isce3::core::LUT2d & doppler, const double threshold, - const int numiter) + const int numiter, + const int lines_per_block) { auto geo2rdr_obj = Geo2rdr(radar_grid, orbit, ellipsoid, doppler); geo2rdr_obj.threshold(threshold); geo2rdr_obj.numiter(numiter); + geo2rdr_obj.linesPerBlock(lines_per_block); return geo2rdr_obj; }), py::arg("radar_grid"), @@ -34,7 +36,8 @@ void addbinding(py::class_ & pyGeo2Rdr) py::arg("ellipsoid"), py::arg("doppler") = isce3::core::LUT2d(), py::arg("threshold") = 0.05, - py::arg("numiter") = 25) + py::arg("numiter") = 25, + py::arg("lines_per_block") = 1000) .def("geo2rdr", py::overload_cast (&Geo2rdr::geo2rdr), @@ -52,5 +55,8 @@ void addbinding(py::class_ & pyGeo2Rdr) .def_property("numiter", py::overload_cast<>(&Geo2rdr::numiter, py::const_), py::overload_cast(&Geo2rdr::numiter)) + .def_property("lines_per_block", + py::overload_cast<>(&Geo2rdr::linesPerBlock, py::const_), + py::overload_cast(&Geo2rdr::linesPerBlock)) ; } diff --git a/python/extensions/pybind_isce3/cuda/geometry/rdr2geo.cpp b/python/extensions/pybind_isce3/cuda/geometry/rdr2geo.cpp index 87ac28032..1c55ddb0e 100644 --- a/python/extensions/pybind_isce3/cuda/geometry/rdr2geo.cpp +++ b/python/extensions/pybind_isce3/cuda/geometry/rdr2geo.cpp @@ -25,7 +25,8 @@ void addbinding(py::class_ & pyRdr2Geo) const double threshold, const int numiter, const int extraiter, const dataInterpMethod dem_interp_method, - const int epsg_out, const bool compute_mask) { + const int epsg_out, const bool compute_mask, + const int lines_per_block) { auto rdr2geo_obj = Topo(radar_grid, orbit, ellipsoid, doppler); rdr2geo_obj.threshold(threshold); rdr2geo_obj.numiter(numiter); @@ -33,6 +34,7 @@ void addbinding(py::class_ & pyRdr2Geo) rdr2geo_obj.demMethod(dem_interp_method); rdr2geo_obj.epsgOut(epsg_out); rdr2geo_obj.computeMask(compute_mask); + rdr2geo_obj.linesPerBlock(lines_per_block); return rdr2geo_obj; }), py::arg("radar_grid"), py::arg("orbit"), @@ -42,7 +44,8 @@ void addbinding(py::class_ & pyRdr2Geo) py::arg("extraiter") = 10, py::arg("dem_interp_method") = isce3::core::BIQUINTIC_METHOD, - py::arg("epsg_out") = 4326, py::arg("compute_mask") = true) + py::arg("epsg_out") = 4326, py::arg("compute_mask") = true, + py::arg("lines_per_block") = 1000) .def("topo", py::overload_cast( &Topo::topo), @@ -96,5 +99,9 @@ void addbinding(py::class_ & pyRdr2Geo) py::overload_cast(&Topo::epsgOut)) .def_property("compute_mask", py::overload_cast<>(&Topo::computeMask, py::const_), - py::overload_cast(&Topo::computeMask)); + py::overload_cast(&Topo::computeMask)) + .def_property("lines_per_block", + py::overload_cast<>(&Topo::linesPerBlock, py::const_), + py::overload_cast(&Topo::linesPerBlock)) + ; } diff --git a/python/extensions/pybind_isce3/geometry/geo2rdr.cpp b/python/extensions/pybind_isce3/geometry/geo2rdr.cpp index 1f1c5dae6..2c1ca1a6d 100644 --- a/python/extensions/pybind_isce3/geometry/geo2rdr.cpp +++ b/python/extensions/pybind_isce3/geometry/geo2rdr.cpp @@ -31,11 +31,13 @@ void addbinding(py::class_ & pyGeo2Rdr) const isce3::core::Ellipsoid & ellipsoid, const isce3::core::LUT2d & doppler, const double threshold, - const int numiter) + const int numiter, + const int lines_per_block) { auto geo2rdr_obj = Geo2rdr(radar_grid, orbit, ellipsoid, doppler); geo2rdr_obj.threshold(threshold); geo2rdr_obj.numiter(numiter); + geo2rdr_obj.linesPerBlock(lines_per_block); return geo2rdr_obj; }), py::arg("radar_grid"), @@ -43,7 +45,8 @@ void addbinding(py::class_ & pyGeo2Rdr) py::arg("ellipsoid"), py::arg("doppler") = isce3::core::LUT2d(), py::arg("threshold") = 0.05, - py::arg("numiter") = 25) + py::arg("numiter") = 25, + py::arg("lines_per_block") = 1000) .def("geo2rdr", py::overload_cast (&Geo2rdr::geo2rdr), @@ -61,6 +64,9 @@ void addbinding(py::class_ & pyGeo2Rdr) .def_property("numiter", py::overload_cast<>(&Geo2rdr::numiter, py::const_), py::overload_cast(&Geo2rdr::numiter)) + .def_property("lines_per_block", + py::overload_cast<>(&Geo2rdr::linesPerBlock, py::const_), + py::overload_cast(&Geo2rdr::linesPerBlock)) ; } diff --git a/python/extensions/pybind_isce3/geometry/metadataCubes.cpp b/python/extensions/pybind_isce3/geometry/metadataCubes.cpp index 3924a0b6e..2ea1ef089 100644 --- a/python/extensions/pybind_isce3/geometry/metadataCubes.cpp +++ b/python/extensions/pybind_isce3/geometry/metadataCubes.cpp @@ -24,6 +24,7 @@ void addbinding_metadata_cubes(py::module & m) py::arg("along_track_unit_vector_x_raster") = nullptr, py::arg("along_track_unit_vector_y_raster") = nullptr, py::arg("elevation_angle_raster") = nullptr, + py::arg("ground_track_velocity_raster") = nullptr, py::arg("threshold_geo2rdr") = 1.0e-8, py::arg("numiter_geo2rdr") = 100, py::arg("delta_range") = 1.0e-8, R"(Make metadata radar grid cubes @@ -85,6 +86,8 @@ void addbinding_metadata_cubes(py::module & m) Along-track unit vector Y raster elevation_angle_raster : isce3.io.Raster, optional Elevation angle (in degrees wrt geodedic nadir) cube raster + ground_track_velocity_raster : isce3.io.Raster, optional + Ground-track velocity cube raster threshold_geo2rdr : double, optional Range threshold for geo2rdr numiter_geo2rdr : int, optional @@ -106,6 +109,7 @@ void addbinding_metadata_cubes(py::module & m) py::arg("along_track_unit_vector_x_raster") = nullptr, py::arg("along_track_unit_vector_y_raster") = nullptr, py::arg("elevation_angle_raster") = nullptr, + py::arg("ground_track_velocity_raster") = nullptr, py::arg("threshold_geo2rdr") = 1.0e-8, py::arg("numiter_geo2rdr") = 100, py::arg("delta_range") = 1.0e-6, R"(Make metadata geolocation grid cubes @@ -167,6 +171,8 @@ void addbinding_metadata_cubes(py::module & m) Along-track unit vector Y raster elevation_angle_raster : isce3.io.Raster, optional Elevation angle (in degrees wrt geodedic nadir) cube raster + ground_track_velocity_raster : isce3.io.Raster, optional + Ground-track velocity cube raster threshold_geo2rdr : double, optional Range threshold for geo2rdr numiter_geo2rdr : int, optional diff --git a/python/extensions/pybind_isce3/geometry/rdr2geo.cpp b/python/extensions/pybind_isce3/geometry/rdr2geo.cpp index 7add82eaf..5d62db1a6 100644 --- a/python/extensions/pybind_isce3/geometry/rdr2geo.cpp +++ b/python/extensions/pybind_isce3/geometry/rdr2geo.cpp @@ -157,7 +157,8 @@ void addbinding(py::class_ & pyRdr2Geo) const double threshold, const int numiter, const int extraiter, const dataInterpMethod dem_interp_method, - const int epsg_out, const bool compute_mask) { + const int epsg_out, const bool compute_mask, + const int lines_per_block) { auto rdr2geo_obj = Topo(radar_grid, orbit, ellipsoid, doppler); rdr2geo_obj.threshold(threshold); rdr2geo_obj.numiter(numiter); @@ -165,6 +166,7 @@ void addbinding(py::class_ & pyRdr2Geo) rdr2geo_obj.demMethod(dem_interp_method); rdr2geo_obj.epsgOut(epsg_out); rdr2geo_obj.computeMask(compute_mask); + rdr2geo_obj.linesPerBlock(lines_per_block); return rdr2geo_obj; }), py::arg("radar_grid"), py::arg("orbit"), @@ -174,7 +176,8 @@ void addbinding(py::class_ & pyRdr2Geo) py::arg("extraiter") = 10, py::arg("dem_interp_method") = isce3::core::BIQUINTIC_METHOD, - py::arg("epsg_out") = 4326, py::arg("compute_mask") = true) + py::arg("epsg_out") = 4326, py::arg("compute_mask") = true, + py::arg("lines_per_block") = 1000) .def("topo", py::overload_cast( &Topo::topo), @@ -228,5 +231,9 @@ void addbinding(py::class_ & pyRdr2Geo) py::overload_cast(&Topo::epsgOut)) .def_property("compute_mask", py::overload_cast<>(&Topo::computeMask, py::const_), - py::overload_cast(&Topo::computeMask)); + py::overload_cast(&Topo::computeMask)) + .def_property("lines_per_block", + py::overload_cast<>(&Topo::linesPerBlock, py::const_), + py::overload_cast(&Topo::linesPerBlock)) + ; } diff --git a/python/extensions/pybind_isce3/io/Raster.cpp b/python/extensions/pybind_isce3/io/Raster.cpp index 5786fc056..fc57bc216 100644 --- a/python/extensions/pybind_isce3/io/Raster.cpp +++ b/python/extensions/pybind_isce3/io/Raster.cpp @@ -93,6 +93,24 @@ void addbinding(py::class_ & pyRaster) "Create a VRT raster dataset from list of rasters", py::arg("path"), py::arg("raster_list")) + .def("close_dataset", [](Raster & self) + { + if (self.dataset_owner()) { + auto gdal_ds = self.dataset(); + delete gdal_ds; + } + self.dataset(nullptr); + }, + R"( + Close the dataset. + + Decrements the reference count of the underlying `GDALDataset`, which, + if this was the last open instance, causes the dataset to be closed + and any cached changes to be flushed to disk. + + This invalidates the `Raster` instance -- it cannot be used after closing + the underlying dataset. + )") .def(py::init([](std::uintptr_t py_ds_ptr) { auto gdal_ds = reinterpret_cast(py_ds_ptr); diff --git a/python/packages/nisar/products/writers/SLC.py b/python/packages/nisar/products/writers/SLC.py index e3a5f68f1..b4f432a16 100644 --- a/python/packages/nisar/products/writers/SLC.py +++ b/python/packages/nisar/products/writers/SLC.py @@ -1,6 +1,9 @@ import h5py import logging import numpy as np +from numpy.testing import assert_allclose +import os +import isce3 from pybind_isce3.core import LUT2d, DateTime, Orbit, Attitude, EulerAngles from pybind_isce3.product import RadarGridParameters from pybind_isce3.geometry import DEMInterpolator @@ -21,6 +24,61 @@ def time_units(epoch: DateTime) -> str: return "seconds since " + date + " " + time +def assert_same_lut2d_grid(x: np.ndarray, y: np.ndarray, lut: LUT2d): + assert_allclose(x[0], lut.x_start) + assert_allclose(y[0], lut.y_start) + assert (len(x) > 1) and (len(y) > 1) + assert_allclose(x[1] - x[0], lut.x_spacing) + assert_allclose(y[1] - y[0], lut.y_spacing) + assert lut.width == len(x) + assert lut.length == len(y) + + +def h5_require_dirname(group: h5py.Group, name: str): + """Make sure any intermediate paths in `name` exist in `group`. + """ + assert os.sep == '/', "Need to fix HDF5 path manipulation on Windows" + d = os.path.dirname(name) + group.require_group(d) + + +def add_cal_layer(group: h5py.Group, lut: LUT2d, name: str, + epoch: DateTime, units: str): + """Add calibration LUT to HDF5 group, making sure that its domain matches + any existing cal LUTs. + + Parameters + ---------- + group : h5py.Group + Group where LUT and its axes will be stored. + lut : isce3.core.LUT2d + Look up table. Axes are assumed to be y=zeroDopplerTime, x=slantRange. + name : str + Name of dataset to store LUT data (can be path relative to `group`). + epoch: isce3.core.DateTime + Reference time associated with y-axis. + units : str + Units of lut.data. Will be stored in `units` attribute of dataset. + """ + # If we've already saved one cal layer then we've already saved its + # x and y axes. Make sure they're the same and just save the new z data. + xname, yname = "slantRange", "zeroDopplerTime" + if (xname in group) and (yname in group): + x, y = group[xname], group[yname] + assert_same_lut2d_grid(x, y, lut) + extant_epoch = isce3.io.get_ref_epoch(y.parent, y.name) + assert extant_epoch == epoch + data = lut.data + z = group.require_dataset(name, data.shape, data.dtype) + z[...] = data + elif (xname not in group) and (yname not in group): + h5_require_dirname(group, name) + lut.save_to_h5(group, name, epoch, units) + else: + raise IOError(f"Found only one of {xname} or {yname}." + " Need both or none.") + + class SLC(h5py.File): def __init__(self, *args, band="LSAR", product="RSLC", **kw): super().__init__(*args, **kw) @@ -49,7 +107,7 @@ def set_parameters(self, dop: LUT2d, epoch: DateTime, frequency='A'): # Actual LUT goes into a subdirectory, not created by serialization. name = f"frequency{frequency}" fg = g.require_group(name) - dop.save_to_h5(g, f"{name}/dopplerCentroid", epoch, "Hz") + add_cal_layer(g, dop, f"{name}/dopplerCentroid", epoch, "Hz") # TODO veff, fmrate not used anywhere afaict except product io. v = np.zeros_like(dop.data) g.require_dataset("effectiveVelocity", v.shape, v.dtype, data=v) @@ -108,15 +166,14 @@ def swath(self, frequency="A") -> h5py.Group: def add_polarization(self, frequency="A", pol="HH"): assert len(pol) == 2 and pol[0] in "HVLR" and pol[1] in "HV" + pols = np.string_([pol]) # careful not to promote to unicode below g = self.swath(frequency) name = "listOfPolarizations" if name in g: - pols = np.array(g[name]) - assert(pol not in pols) - pols = np.append(pols, [pol]) + old_pols = np.array(g[name]) + assert pols[0] not in old_pols + pols = np.append(old_pols, pols) del g[name] - else: - pols = np.array([pol], dtype="S2") dset = g.create_dataset(name, data=pols) desc = f"List of polarization layers with frequency{frequency}" dset.attrs["description"] = np.string_(desc) diff --git a/python/packages/nisar/workflows/filter_data.py b/python/packages/nisar/workflows/filter_data.py new file mode 100644 index 000000000..1b8a0a66b --- /dev/null +++ b/python/packages/nisar/workflows/filter_data.py @@ -0,0 +1,301 @@ +import os +import isce3 +import h5py +import numpy as np +from osgeo import gdal +from dataclasses import dataclass + +@dataclass +class BlockParam: + ''' + Class for block specific parameters + Facilitate block parameters exchange between functions + ''' + # Length of current block to filter; padding not included + block_length: int + + # First line to write to for current block + write_start_line: int + + # First line to read from dataset for current block + read_start_line: int + + # Number of lines to read from dataset for current block + read_length: int + + # Padding to be applied to read in current block. First tuple is padding to + # be applied to top/bottom (along length). Second tuple is padding to be + # applied to left/right (along width). Values in second tuple do not change; + # included in class so one less value is passed between functions. + block_pad: tuple + + # Width of current block. Value does not change per block; included to + # in class so one less value is to be passed between functions. + data_width: int + +def np2gdal_dtype(np_dtype): + dict_np2gdal = { + np.byte: gdal.GDT_Byte, + np.ushort: gdal.GDT_UInt16, + np.short: gdal.GDT_Int16, + np.uintc: gdal.GDT_UInt32, + np.intc: gdal.GDT_Int32, + np.float32: gdal.GDT_Float32, + np.float64: gdal.GDT_Float64, + np.complex64: gdal.GDT_CFloat32, + np.complex128: gdal.GDT_CFloat64} + if np_dtype not in dict_np2gdal: + # throw unsupported error + pass + else: + return dict_np2gdal[int_dtype] + +def block_param_generator(lines_per_block, data_shape, pad_shape): + ''' Generator for block specific parameter class. + + Parameters + ---------- + lines_per_block: int + Lines to be processed per block (in batch). + data_shape: tuple(int, int) + Length and width of input raster. + pad_shape: tuple(int, int) + Padding for the length and width of block to be filtered. + + Returns + ------- + _: BlockParam + BlockParam object for current block + ''' + data_length, data_width = data_shape + pad_length, pad_width = pad_shape + + # Calculate number of blocks to break raster into + num_blocks = int(np.ceil(data_length / lines_per_block)) + + for block in range(num_blocks): + start_line = block * lines_per_block + + # Discriminate between first, last, and middle blocks + first_block = block == 0 + last_block = block == num_blocks - 1 or num_blocks == 1 + middle_block = not first_block and not last_block + + # Determine block size; Last block uses leftover lines + block_length = data_length - start_line if last_block else lines_per_block + + # Determine padding along length. Full padding for middle blocks + # Half padding for start and end blocks + read_length_pad = pad_length if middle_block else pad_length // 2 + + # Determine 1st line of output + write_start_line = block * lines_per_block + + # Determine 1st dataset line to read. Subtract half padding length + # to account for additional lines to be read. + read_start_line = block * lines_per_block - pad_length // 2 + + # If applicable, save negative start line as deficit to account for later + read_start_line, start_line_deficit = ( + 0, read_start_line) if read_start_line < 0 else ( + read_start_line, 0) + + # Initial guess at number lines to read; accounting for negative start at the end + read_length = block_length + read_length_pad + if not first_block: + read_length -= abs(start_line_deficit) + + # Check for over-reading and adjust lines read as needed + end_line_deficit = min( + data_length - read_start_line - read_length, 0) + read_length -= abs(end_line_deficit) + + # Determine block padding in length + if first_block: + # Only the top part of the block should be padded. If end_deficit_line=0 + # we have a sufficient number of lines to be read in the subsequent block + top_pad = pad_length // 2 + bottom_pad = abs(end_line_deficit) + elif last_block: + # Only the bottom part of the block should be padded + top_pad = abs( + start_line_deficit) if start_line_deficit < 0 else 0 + bottom_pad = pad_length // 2 + else: + # Top and bottom should be added taking into account line deficit + top_pad = abs( + start_line_deficit) if start_line_deficit < 0 else 0 + bottom_pad = abs(end_line_deficit) + + block_pad = ((top_pad, bottom_pad), + (pad_width // 2, pad_width // 2)) + + yield BlockParam(block_length, write_start_line, read_start_line, read_length, block_pad, data_width) + + return + +def get_raster_info(raster): + ''' Determine raster shape based on raster + type (h5py.Dataset or GDAL-friendly raster). + + Parameters + ---------- + raster: h5py.Dataset or str + Raster whose size is to be determined. String value represents + filepath for GDAL rasters. + + Returns + ------- + data_width: int + Width of raster. + data_length: int + Length of raster. + ''' + if isinstance(raster, h5py.Dataset): + return raster.shape, raster.dtype + else: + # Open input data using GDAL to get raster length + ds = gdal.Open(raster, gdal.GA_ReadOnly) + data_length = ds.RasterYSize + data_width = ds.RasterXSize + data_type = ds.GetRasterBand(1).DataType + return (data_length, data_width), data_type + +def get_raster_block(raster, block_param): + ''' Get a block of data from raster. + Raster can be a HDF5 file or a GDAL-friendly raster + + Parameters + ---------- + raster: h5py.Dataset or str + Raster where a block is to be read from. String value represents a + filepath for GDAL rasters. + block_param: BlockParam + Object specifying size of block and where to read from raster, + and amount of padding for the read array + + Returns + ------- + data_block: np.ndarray + Block read from raster with shape specified in block_param. + ''' + if isinstance(raster, h5py.Dataset): + data_block = np.empty((block_param.read_length, block_param.data_width), + dtype=raster.dtype) + raster.read_direct(data_block, np.s_[block_param.read_start_line: + block_param.read_start_line + block_param.read_length, :]) + else: + # Open input data using GDAL to get raster length + ds_data = gdal.Open(raster, gdal.GA_Update) + data_block = ds_data.GetRasterBand(1).ReadAsArray(0, + block_param.read_start_line, + block_param.data_width, + block_param.read_length) + + # Pad igram_block with zeros according to pad_length/pad_width + data_block = np.pad(data_block, block_param.block_pad, + mode='constant', constant_values=0) + + return data_block + +def write_raster_block(out_raster, data, block_param): + ''' Write processed block to out_raster. + + Parameters + ---------- + out_raster: h5py.Dataset or str + Raster where data (i.e., filtered data) needs to be written. + String value represents filepath for GDAL rasters. + data: np.ndarray + Filtered data to write to out_raster. + block_param: BlockParam + Object specifying where and how much to write to out_raster. + ''' + if isinstance(out_raster, h5py.Dataset): + out_raster.write_direct(data, + dest_sel=np.s_[ + block_param.write_start_line:block_param.write_start_line + block_param.block_length, + :]) + else: + ds_data = gdal.Open(out_raster, gdal.GA_Update) + ds_data.GetRasterBand(1).WriteArray(data, xoff=0, yoff=block_param.write_start_line) + + +def filter_data(input_data, lines_per_block, + kernel_rows, kernel_cols, output_data=None, mask_path=None): + ''' Filter data using two separable 1D kernels. + + Parameters + ---------- + input_data: str + File path to input data raster (GDAL-friendly) + lines_per_block: int + Number of lines to process in batch + kernel_rows: float array + 1D kernel along rows direction + kernel_cols: float array + 1D kernel along columns direction + output_data: h5py.Dataset or str + Raster where a block needs to be written to. String value represents + file path for GDAL rasters. If not provided, input_data is overwritten + with the output filtered data + mask_path: str + Filepath to the mask to use during filtering + + Returns + ------- + ''' + + data_shape, data_type = get_raster_info(input_data) + data_length, data_width = data_shape + + # Determine the amount of padding + pad_length = 2 * (len(kernel_rows) // 2) + pad_width = 2 * (kernel_cols.shape[1] // 2) + pad_shape = (pad_length, pad_width) + + # Determine number of blocks to process + lines_per_block = min(data_length, + lines_per_block) + + # Start block processing + block_params = block_param_generator(lines_per_block, data_shape, pad_shape) + for block_param in block_params: + # Read a block of data. If hdf5_dset is set, read a block of data + # directly from the hdf5 file. Otherwise, use gdal to read block of data + data_block = get_raster_block(input_data, block_param) + + # Get if filtering needs to be performed with or without a mask + if mask_path is not None: + # Use gdal to extract a mask block, pad the mask (mask need to be same shape as input) + ds_mask = gdal.Open(mask_path, + gdal.GA_ReadOnly) + mask_block = ds_mask.GetRasterBand(1).ReadAsArray(0, + block_param.read_start_line, + block_param.data_width, + block_param.read_length) + mask_block = np.pad(mask_block, block_param.block_pad, + mode='constant', constant_values=0) + filt_data_block = isce3.signal.convolve2D(data_block, + mask_block, + kernel_cols, + kernel_rows, + False) + else: + filt_data_block = isce3.signal.convolve2D(data_block, + kernel_cols, + kernel_rows, + False) + # If no value provided for output_data, then overwrite existing + # input with filtered output + # Otherwise write filtered output to output_data + out_raster = input_data if output_data is None else output_data + + # If writing to GDAL raster, prepare file + if not isinstance(out_raster, h5py.Dataset) and not os.path.isfile(out_raster): + raster = isce3.io.Raster(path=out_raster, width=data_width, + length=data_length, num_bands=1, + dtype=data_type, driver_name='GTiff') + del raster + + write_raster_block(out_raster, filt_data_block, block_param) diff --git a/python/packages/nisar/workflows/filter_interferogram.py b/python/packages/nisar/workflows/filter_interferogram.py new file mode 100644 index 000000000..01636ebd3 --- /dev/null +++ b/python/packages/nisar/workflows/filter_interferogram.py @@ -0,0 +1,149 @@ +''' +Wrapper for interferogram filtering +''' + +import pathlib +import time +import h5py + + +import journal +import numpy as np +from nisar.workflows.filter_interferogram_runconfig import \ + FilterInterferogramRunConfig +from nisar.workflows.filter_data import filter_data +from nisar.workflows.yaml_argparse import YamlArgparse + + +def run(cfg: dict, input_hdf5: str): + ''' + Run interferogram filtering + ''' + # Pull parameters from runconfig dictionary + freq_pols = cfg['processing']['input_subset']['list_of_frequencies'] + filter_args = cfg['processing']['filter_interferogram'] + + # Create error and info channels + error_channel = journal.error('filter_interferogram.run') + info_channel = journal.info('filter_interferogram.run') + info_channel.log("Start interferogram filtering") + + # Check interferogram path, if not file, raise exception + interferogram_path = filter_args['interferogram_path'] + if interferogram_path is None: + interferogram_path = input_hdf5 + interferogram_path = pathlib.Path(interferogram_path) + + if not interferogram_path.is_file(): + err_str = f"{interferogram_path} is invalid; needs to be a file" + error_channel.log(err_str) + raise ValueError(err_str) + + # Record processing start time + t_all = time.time() + + # Prepare filter kernels according to user-preference + filter_type = filter_args['filter_type'] + if filter_type == 'no_filter': + # No filter is required, record processing time and return + info_channel.log('No intereferogram filtering requested') + t_all_elapsed = time.time() - t_all + info_channel.log(f"Ran insar filtering in {t_all_elapsed:.3f} seconds") + return + elif filter_type == 'boxcar': + # Create 1D boxcar kernels in slant range/azimuth + kernel_width = filter_args['boxcar']['filter_size_range'] + kernel_length = filter_args['boxcar']['filter_size_azimuth'] + kernel_rows = np.ones((kernel_length, 1), + dtype=np.float64) / kernel_length + kernel_cols = np.ones((1, kernel_width), + dtype=np.float64) / kernel_width + else: + # Create 1D gaussian kernels centered around 0 in range/azimuth + kernel_width = filter_args['gaussian']['filter_size_range'] + kernel_length = filter_args['gaussian']['filter_size_azimuth'] + kernel_rows = create_gaussian_kernel(kernel_length, + filter_args['gaussian'][ + 'sigma_azimuth']) + kernel_rows = np.reshape(kernel_rows, (kernel_length, 1)) + kernel_cols = create_gaussian_kernel(kernel_width, + filter_args['gaussian'][ + 'sigma_range']) + kernel_cols = np.reshape(kernel_cols, (1, kernel_width)) + + # When using isce3.signal.convolve2D, it is necessary to pad the input block + # to have an output with the same shape as the input. + + with h5py.File(input_hdf5, 'a', libver='latest', swmr=True) as dst_h5: + for freq, pol_list in freq_pols.items(): + freq_group_path = f'/science/LSAR/RIFG/swaths/frequency{freq}' + for pol in pol_list: + + # Get mask for that frequency/pol or general mask to be applied + mask = get_mask(filter_args['mask'], freq, pol) + + # Get h5py igram dataset to be filtered + pol_group_path = f'{freq_group_path}/interferogram/{pol}' + igram_dset = dst_h5[f'{pol_group_path}/wrappedInterferogram'] + + # Filter dataset + filter_data(igram_dset, filter_args['lines_per_block'], + kernel_rows, kernel_cols, mask_path=mask) + + + t_all_elapsed = time.time() - t_all + info_channel.log(f"successfully ran filter_interferogram in {t_all_elapsed:.3f} seconds") + +def get_mask(mask_args, freq, pol): + ''' + + Parameters + ---------- + mask_args: dict + Dictionary containing mask filtering options + freq: str + String indicating the frequency for which extract the mask + pol: str + String indicating the polarization for which extract the mask + + Returns + ------- + mask: str + Filepath to general mask or to a mask for selected freq/pol + ''' + mask = None + + # Check if freq and pol are in mask_args, if yes, extract mask + if freq in mask_args: + if pol in mask_args[freq]: + mask = mask_args[freq][pol] + elif 'general' in mask_args: + mask = mask_args['general'] + return mask + + +def create_gaussian_kernel(size, sigma): + ''' + Create 1D gaussian kernel given kernel size + and standard deviation + ''' + array = np.arange(-int(size / 2), int(size / 2) + 1) + return np.asarray([1 / (sigma * np.sqrt(2 * np.pi)) * np.exp( + -float(x) ** 2 / (2 * sigma ** 2)) + for x in array]) + + +if __name__ == "__main__": + ''' + Run interferogram filtering from command line + ''' + # Load command line args + filter_parser = YamlArgparse() + args = filter_parser.parse() + # Get a runconfig dict from command line args + filter_runconfig = FilterInterferogramRunConfig(args) + # Use RIFG from interferogram path + rifg_h5 = filter_runconfig.cfg['processing']['filter_interferogram'][ + 'interferogram_path'] + # run insar filtering + run(filter_runconfig.cfg, rifg_h5) diff --git a/python/packages/nisar/workflows/filter_interferogram_runconfig.py b/python/packages/nisar/workflows/filter_interferogram_runconfig.py new file mode 100644 index 000000000..aba14fc9f --- /dev/null +++ b/python/packages/nisar/workflows/filter_interferogram_runconfig.py @@ -0,0 +1,88 @@ +import os + +import journal +import nisar.workflows.helpers as helpers +from nisar.workflows.runconfig import RunConfig + + +class FilterInterferogramRunConfig(RunConfig): + def __init__(self, args): + # All InSAR workflow steps share a common InSAR schema + super().__init__(args, 'insar') + + if self.args.run_config_path is not None: + self.load_geocode_yaml_to_dict() + self.geocode_common_arg_load() + self.yaml_check() + + def yaml_check(self): + ''' + Check filter_interferogram inputs from YAML file + ''' + error_channel = journal.error('FilterInterferogramRunconfig.yaml_check') + + # Extract frequency and polarizations to process + freq_pols = self.cfg['processing']['input_subset'][ + 'list_of_frequencies'] + + # interferogram_path is required for stand-alone usage of filter_interferogram.py + interferogram_path = self.cfg['processing']['filter_interferogram'][ + 'interferogram_path'] + if interferogram_path is None: + err_str = f'{interferogram_path} in filter_interferogram required for stand-alone usage of filter_interferogram.py' + error_channel.log(err_str) + raise ValueError(err_str) + + # Check if interferogram_path is a file + if not os.path.isfile(interferogram_path): + err_str = f"{interferogram_path} is invalid; needs to be a file" + error_channel.log(err_str) + raise ValueError(err_str) + else: + # Check that required polarization/frequencies are in interferogram_path + helpers.check_hdf5_freq_pols(interferogram_path, freq_pols) + + # If mask/weight is assigned check if it exists + mask_options = self.cfg['processing']['filter_interferogram']['mask'] + if 'general' in mask_options and mask_options['general'] is not None: + if not os.path.isfile(mask_options['general']): + err_str = f"The mask file {mask_options['general']} is not a file" + error_channel.log(err_str) + raise ValueError(err_str) + else: + # Otherwise check that mask for individual freq/pols are correctly assigned + for freq, pol_list in freq_pols.items(): + if freq in mask_options: + for pol in pol_list: + if pol in mask_options[freq]: + mask_file = mask_options[freq][pol] + if mask_file is not None and not os.path.isfile( + mask_file): + err_str = f"{mask_file} is invalid; needs to be a file" + error_channel.log(err_str) + raise ValueError(err_str) + + # Check filter_type and if not allocated, create a default cfg dictionary + # filter_type will be present at runtime because is allocated in share/nisar/defaults + filter_type = self.cfg['processing']['filter_interferogram']['filter_type'] + if filter_type != 'no_filter' and filter_type not in \ + self.cfg['processing']['filter_interferogram']: + self.cfg['processing']['filter_interferogram'][filter_type] = {} + + # Based on filter_type, check if related dictionary and/or parameters + # are assigned. Note, if filter_type='boxcar', the filter dictionary + # is filled by share/nisar/defaults + if filter_type == 'gaussian': + if 'gaussian' not in self.cfg['processing']['filter_interferogram']: + self.cfg['processing']['filter_interferogram'][ + 'gaussian'] = {} + gaussian_options = self.cfg['processing']['filter_interferogram'][ + 'gaussian'] + if 'sigma_range' not in gaussian_options: + gaussian_options['sigma_range'] = 1 + if 'sigma_azimuth' not in gaussian_options: + gaussian_options['sigma_azimuth'] = 1 + if 'filter_size_range' not in gaussian_options: + gaussian_options['filter_size_range'] = 9 + if 'filter_size_azimuth' not in gaussian_options: + gaussian_options['filter_size_azimuth'] = 9 diff --git a/python/packages/nisar/workflows/gcov.py b/python/packages/nisar/workflows/gcov.py index 8be197a16..8d297597f 100644 --- a/python/packages/nisar/workflows/gcov.py +++ b/python/packages/nisar/workflows/gcov.py @@ -65,6 +65,7 @@ def run(cfg): rtc_algorithm = rtc_dict['algorithm_type'] input_terrain_radiometry = rtc_dict['input_terrain_radiometry'] rtc_min_value_db = rtc_dict['rtc_min_value_db'] + rtc_upsampling = rtc_dict['dem_upsampling'] # unpack geo2rdr parameters geo2rdr_dict = cfg['processing']['geo2rdr'] @@ -291,6 +292,7 @@ def run(cfg): output_terrain_radiometry=output_terrain_radiometry, exponent=exponent, rtc_min_value_db=rtc_min_value_db, + rtc_upsampling=rtc_upsampling, rtc_algorithm=rtc_algorithm, abs_cal_factor=abs_cal_factor, flag_upsample_radar_grid=flag_upsample_radar_grid, @@ -344,6 +346,7 @@ def run(cfg): yds = hdf5_obj[os.path.join(root_ds, 'yCoordinates')] cov_elements_list = [p.upper()+p.upper() for p in pol_list] + # save GCOV imagery _save_hdf5_dataset(temp_output.name, hdf5_obj, root_ds, yds, xds, cov_elements_list, long_name=output_radiometry_str, @@ -351,12 +354,18 @@ def run(cfg): valid_min=clip_min, valid_max=clip_max) + # save listOfCovarianceTerms + freq_group = hdf5_obj[root_ds] + if not flag_fullcovariance: + _save_list_cov_terms(cov_elements_list, freq_group) + # save nlooks - _save_hdf5_dataset(temp_nlooks.name, hdf5_obj, root_ds, - yds, xds, 'numberOfLooks', - long_name = 'number of looks', - units = '', - valid_min = 0) + if flag_save_nlooks: + _save_hdf5_dataset(temp_nlooks.name, hdf5_obj, root_ds, + yds, xds, 'numberOfLooks', + long_name = 'number of looks', + units = '', + valid_min = 0) # save rtc if flag_save_rtc: @@ -403,7 +412,8 @@ def run(cfg): if (b2 <= b1): continue off_diag_terms_list.append(p1.upper()+p2.upper()) - + _save_list_cov_terms(cov_elements_list + off_diag_terms_list, + freq_group) _save_hdf5_dataset(temp_off_diag.name, hdf5_obj, root_ds, yds, xds, off_diag_terms_list, long_name = output_radiometry_str, @@ -441,6 +451,17 @@ def run(cfg): t_all_elapsed = time.time() - t_all info_channel.log(f"successfully ran geocode COV in {t_all_elapsed:.3f} seconds") + +def _save_list_cov_terms(cov_elements_list, dataset_group): + + name = "listOfCovarianceTerms" + cov_elements_list.sort() + cov_elements_array = np.array(cov_elements_list, dtype="S4") + dset = dataset_group.create_dataset(name, data=cov_elements_array) + desc = f"List of processed covariance terms" + dset.attrs["description"] = np.string_(desc) + + def _save_hdf5_dataset(ds_filename, h5py_obj, root_path, yds, xds, ds_name, standard_name=None, long_name=None, units=None, fill_value=None, diff --git a/python/packages/nisar/workflows/gcov_runconfig.py b/python/packages/nisar/workflows/gcov_runconfig.py index c8c2c896c..a79c33709 100644 --- a/python/packages/nisar/workflows/gcov_runconfig.py +++ b/python/packages/nisar/workflows/gcov_runconfig.py @@ -26,10 +26,10 @@ def load(self): geocode_dict['abs_rad_cal'] = 1.0 if geocode_dict['clip_max'] is None: - geocode_dict['clip_max'] = 5.0 + geocode_dict['clip_max'] = np.nan if geocode_dict['clip_min'] is None: - geocode_dict['clip_min'] = 0.0 + geocode_dict['clip_min'] = np.nan if geocode_dict['geogrid_upsampling'] is None: geocode_dict['geogrid_upsampling'] = 1.0 diff --git a/python/packages/nisar/workflows/geo2rdr.py b/python/packages/nisar/workflows/geo2rdr.py index e1c9e852a..3ec567323 100644 --- a/python/packages/nisar/workflows/geo2rdr.py +++ b/python/packages/nisar/workflows/geo2rdr.py @@ -25,9 +25,9 @@ def run(cfg): dem_file = cfg['DynamicAncillaryFileGroup']['DEMFile'] scratch_path = pathlib.Path(cfg['ProductPathGroup']['ScratchPath']) freq_pols = cfg['processing']['input_subset']['list_of_frequencies'] - - # Get geo2rdr params - geo2rdr_in = cfg['processing']['geo2rdr'] + threshold = cfg['processing']['geo2rdr']['threshold'] + numiter = cfg['processing']['geo2rdr']['maxiter'] + lines_per_block = cfg['processing']['geo2rdr']['lines_per_block'] # Get parameters from SLC slc = SLC(hdf5file=sec_hdf5) @@ -72,8 +72,8 @@ def run(cfg): Geo2Rdr = isce3.geometry.Geo2Rdr geo2rdr_obj = Geo2Rdr(radar_grid, orbit, ellipsoid, doppler_grid, - threshold=geo2rdr_in['threshold'], - numiter=geo2rdr_in['maxiter']) + threshold, numiter, lines_per_block) + # Opem Topo Raster topo_path = pathlib.Path(cfg['processing']['geo2rdr']['topo_path']) rdr2geo_topo_path = topo_path / 'rdr2geo' / f'freq{freq}' / 'topo.vrt' diff --git a/python/packages/nisar/workflows/geocode_insar.py b/python/packages/nisar/workflows/geocode_insar.py index 260e75e05..f546c0b3f 100644 --- a/python/packages/nisar/workflows/geocode_insar.py +++ b/python/packages/nisar/workflows/geocode_insar.py @@ -233,7 +233,7 @@ def cpu_run(cfg, runw_hdf5, output_hdf5): dem_file = cfg["DynamicAncillaryFileGroup"]["DEMFile"] threshold_geo2rdr = cfg["processing"]["geo2rdr"]["threshold"] iteration_geo2rdr = cfg["processing"]["geo2rdr"]["maxiter"] - lines_per_block = cfg["processing"]["blocksize"]["y"] + lines_per_block = cfg["processing"]["geocode"]["lines_per_block"] dem_block_margin = cfg["processing"]["dem_margin"] az_looks = cfg["processing"]["crossmul"]["azimuth_looks"] rg_looks = cfg["processing"]["crossmul"]["range_looks"] @@ -379,7 +379,7 @@ def gpu_run(cfg, runw_hdf5, output_hdf5): dem_file = cfg["DynamicAncillaryFileGroup"]["DEMFile"] freq_pols = cfg["processing"]["input_subset"]["list_of_frequencies"] geogrids = cfg["processing"]["geocode"]["geogrids"] - lines_per_block = cfg["processing"]["blocksize"]["y"] + lines_per_block = cfg["processing"]["geocode"]["lines_per_block"] interp_method = cfg["processing"]["geocode"]["interp_method"] gunw_datasets = cfg["processing"]["geocode"]["datasets"] az_looks = cfg["processing"]["crossmul"]["azimuth_looks"] diff --git a/python/packages/nisar/workflows/h5_prep.py b/python/packages/nisar/workflows/h5_prep.py index 9db762392..2120281d0 100644 --- a/python/packages/nisar/workflows/h5_prep.py +++ b/python/packages/nisar/workflows/h5_prep.py @@ -1176,6 +1176,12 @@ def add_radar_grid_cubes_to_hdf5(hdf5_obj, cube_group_name, geogrid, long_name='Elevation angle', descr='Elevation angle is defined as angle between LOS vector and norm at the sensor', units='degrees') + ground_track_velocity_raster = _get_raster_from_hdf5_ds( + cube_group, 'groundTrackVelocity', np.float64, cube_shape, + zds=zds, yds=yds, xds=xds, + long_name='Ground-track velocity', + descr='Ground track velocity needed to convert azimuth offsets in pixels to meters', + units='m/s') isce3.geometry.make_radar_grid_cubes(radar_grid, geogrid, @@ -1192,6 +1198,7 @@ def add_radar_grid_cubes_to_hdf5(hdf5_obj, cube_group_name, geogrid, along_track_unit_vector_x_raster, along_track_unit_vector_y_raster, elevation_angle_raster, + ground_track_velocity_raster, threshold_geo2rdr, numiter_geo2rdr, delta_range) @@ -1317,6 +1324,12 @@ def add_geolocation_grid_cubes_to_hdf5(hdf5_obj, cube_group_name, radar_grid, long_name='Elevation angle', descr='Elevation angle is defined as angle between LOS vector and norm at the sensor', units='degrees') + ground_track_velocity_raster = _get_raster_from_hdf5_ds( + cube_group, 'groundTrackVelocity', np.float64, cube_shape, + zds=zds, yds=yds, xds=xds, + long_name='Ground-track velocity', + descr='Ground track velocity needed to convert azimuth offsets in pixels to meters', + units='m/s') isce3.geometry.make_geolocation_cubes(radar_grid, heights, @@ -1333,6 +1346,7 @@ def add_geolocation_grid_cubes_to_hdf5(hdf5_obj, cube_group_name, radar_grid, along_track_unit_vector_x_raster, along_track_unit_vector_y_raster, elevation_angle_raster, + ground_track_velocity_raster, threshold_geo2rdr, numiter_geo2rdr, delta_range) diff --git a/python/packages/nisar/workflows/insar.py b/python/packages/nisar/workflows/insar.py index 8971b184d..a950732c7 100644 --- a/python/packages/nisar/workflows/insar.py +++ b/python/packages/nisar/workflows/insar.py @@ -3,13 +3,14 @@ import journal from nisar.workflows import (crossmul, dense_offsets, geo2rdr, - geocode_insar, h5_prep, rdr2geo, - resample_slc, rubbersheet, unwrap) + geocode_insar, h5_prep, filter_interferogram, + rdr2geo, resample_slc, rubbersheet, unwrap) from nisar.workflows.insar_runconfig import InsarRunConfig from nisar.workflows.persistence import Persistence from nisar.workflows.yaml_argparse import YamlArgparse + def run(cfg: dict, out_paths: dict, run_steps: dict): ''' Run INSAR workflow with parameters in cfg dictionary @@ -49,6 +50,11 @@ def run(cfg: dict, out_paths: dict, run_steps: dict): else: crossmul.run(cfg, out_paths['RIFG'], 'coarse') + # Run insar_filter only + if run_steps['filter_interferogram'] and \ + cfg['processing']['filter_interferogram']['filter_type'] != 'no_filter': + filter_interferogram.run(cfg, out_paths['RIFG']) + if run_steps['unwrap'] and 'RUNW' in out_paths: unwrap.run(cfg, out_paths['RIFG'], out_paths['RUNW']) diff --git a/python/packages/nisar/workflows/insar_runconfig.py b/python/packages/nisar/workflows/insar_runconfig.py index 414a13dd0..e1af7d5dd 100644 --- a/python/packages/nisar/workflows/insar_runconfig.py +++ b/python/packages/nisar/workflows/insar_runconfig.py @@ -1,7 +1,6 @@ -import journal - from nisar.workflows.geo2rdr_runconfig import Geo2rdrRunConfig - +import journal +import os class InsarRunConfig(Geo2rdrRunConfig): def __init__(self, args): @@ -14,9 +13,14 @@ def yaml_check(self): ''' Check submodule paths from YAML ''' + scratch_path = self.cfg['ProductPathGroup']['ScratchPath'] error_channel = journal.error('InsarRunConfig.yaml_check') + # Extract frequencies and polarizations to process + freq_pols = self.cfg['processing']['input_subset'][ + 'list_of_frequencies'] + # If dense_offsets is disabled and rubbersheet is enabled # throw an exception and do not run the workflow if not self.cfg['processing']['dense_offsets']['enabled'] and \ @@ -69,11 +73,67 @@ def yaml_check(self): else: self.cfg['processing']['crossmul']['flatten'] = None + # Check dictionary for interferogram filtering + mask_options = self.cfg['processing']['filter_interferogram']['mask'] + + # If general mask is provided, check its existence + if 'general' in mask_options and mask_options['general'] is not None: + if not os.path.isfile(mask_options['general']): + err_str = f"The mask file {mask_options['general']} is not a file" + error_channel.log(err_str) + raise ValueError(err_str) + else: + # Otherwise check that mask for individual freq/pols are correctly assigned + for freq, pol_list in freq_pols.items(): + if freq in mask_options: + for pol in pol_list: + if pol in mask_options[freq]: + mask_file = mask_options[freq][pol] + if mask_file is not None and not os.path.isfile(mask_file): + err_str = f"{mask_file} is invalid; needs to be a file" + error_channel.log(err_str) + raise ValueError(err_str) + + # Check filter_type and if not allocated, create a default cfg dictionary + # filter_type will be present at runtime because is allocated in share/nisar/defaults + filter_type = self.cfg['processing']['filter_interferogram']['filter_type'] + if filter_type != 'no_filter' and filter_type not in \ + self.cfg['processing']['filter_interferogram']: + self.cfg['processing']['filter_interferogram'][filter_type] = {} + + # Based on filter_type, check if related dictionary and/or parameters + # are assigned. Note, if filter_type='boxcar', the filter dictionary + # is filled by share/nisar/defaults + if filter_type == 'gaussian': + if 'gaussian' not in self.cfg['processing']['filter_interferogram']: + self.cfg['processing']['filter_interferogram'][ + 'gaussian'] = {} + gaussian_options = self.cfg['processing']['filter_interferogram'][ + 'gaussian'] + if 'sigma_range' not in gaussian_options: + gaussian_options['sigma_range'] = 1 + if 'sigma_azimuth' not in gaussian_options: + gaussian_options['sigma_azimuth'] = 1 + if 'filter_size_range' not in gaussian_options: + gaussian_options['filter_size_range'] = 9 + if 'filter_size_azimuth' not in gaussian_options: + gaussian_options['filter_size_azimuth'] = 9 + + # set to empty dict and default unwrap values will be used + # if phase_unwrap fields not in yaml + if 'phase_unwrap' not in self.cfg['processing']: + self.cfg['processing']['phase_unwrap'] = {} + + # if phase_unwrap fields not in yaml + if self.cfg['processing']['phase_unwrap'] is None: + self.cfg['processing']['phase_unwrap'] = {} + # Create default unwrap cfg dict depending on unwrapping algorithm algorithm = self.cfg['processing']['phase_unwrap']['algorithm'] if algorithm not in self.cfg['processing']['phase_unwrap']: self.cfg['processing']['phase_unwrap'][algorithm]={} + if 'interp_method' not in self.cfg['processing']['geocode']: self.cfg['processing']['geocode']['interp_method'] = 'BILINEAR' diff --git a/python/packages/nisar/workflows/persistence.py b/python/packages/nisar/workflows/persistence.py index 1007d95b6..e93ff015c 100644 --- a/python/packages/nisar/workflows/persistence.py +++ b/python/packages/nisar/workflows/persistence.py @@ -7,7 +7,7 @@ class Persistence(): basic class that determines InSAR persistence ''' # init InSAR steps in reverse chronological run order - insar_steps = ['geocode', 'unwrap', 'crossmul', 'fine_resample', 'rubbersheet', + insar_steps = ['geocode', 'unwrap', 'filter_interferogram','crossmul', 'fine_resample', 'rubbersheet', 'dense_offsets', 'coarse_resample', 'geo2rdr', 'rdr2geo', 'h5_prep'] def __init__(self, restart=False): diff --git a/python/packages/nisar/workflows/rdr2geo.py b/python/packages/nisar/workflows/rdr2geo.py index 85eba8688..d8043210a 100644 --- a/python/packages/nisar/workflows/rdr2geo.py +++ b/python/packages/nisar/workflows/rdr2geo.py @@ -24,6 +24,10 @@ def run(cfg): dem_file = cfg['DynamicAncillaryFileGroup']['DEMFile'] scratch_path = pathlib.Path(cfg['ProductPathGroup']['ScratchPath']) freq_pols = cfg['processing']['input_subset']['list_of_frequencies'] + threshold = cfg['processing']['rdr2geo']['threshold'] + numiter = cfg['processing']['rdr2geo']['numiter'] + extraiter = cfg['processing']['rdr2geo']['extraiter'] + lines_per_block = cfg['processing']['rdr2geo']['lines_per_block'] # get params from SLC slc = SLC(hdf5file=input_hdf5) @@ -64,7 +68,10 @@ def run(cfg): else: Rdr2Geo = isce3.geometry.Rdr2Geo - rdr2geo_obj = Rdr2Geo(radargrid, orbit, ellipsoid, grid_doppler) + rdr2geo_obj = Rdr2Geo(radargrid, orbit, ellipsoid, grid_doppler, + threshold=threshold, numiter=numiter, + extraiter=extraiter, + lines_per_block=lines_per_block) # run rdr2geo_obj.topo(dem_raster, str(rdr2geo_scratch_path)) diff --git a/python/packages/nisar/workflows/rubbersheet.py b/python/packages/nisar/workflows/rubbersheet.py index 5777d95f6..8eb42a972 100644 --- a/python/packages/nisar/workflows/rubbersheet.py +++ b/python/packages/nisar/workflows/rubbersheet.py @@ -281,8 +281,10 @@ def fill_outliers_holes(offset, output_path, rubbersheet_params): offset_smoothed = ndimage.median_filter(offset_filled, [window_az, window_rg]) elif filter_type == 'gaussian': - sigma = rubbersheet_params['gaussian']['sigma'] - offset_smoothed = ndimage.gaussian_filter(offset_filled, sigma) + sigma_range = rubbersheet_params['gaussian']['sigma_range'] + sigma_azimuth = rubbersheet_params['gaussian']['sigma_azimuth'] + offset_smoothed = ndimage.gaussian_filter(offset_filled, [sigma_azimuth, + sigma_range]) else: err_str = "Not a valid filter option to filter rubbersheeted offsets" error_channel.log(err_str) diff --git a/python/packages/nisar/workflows/rubbersheet_runconfig.py b/python/packages/nisar/workflows/rubbersheet_runconfig.py index 53c3fe6dc..588240b69 100644 --- a/python/packages/nisar/workflows/rubbersheet_runconfig.py +++ b/python/packages/nisar/workflows/rubbersheet_runconfig.py @@ -45,8 +45,10 @@ def yaml_check(self): self.cfg['processing']['rubbersheet']['median'][ 'filter_size_azimuth'] = 5 elif filter_type == 'gaussian': - if 'sigma' not in self.cfg['processing']['rubbersheet']['gaussian']: - self.cfg['processing']['rubbersheet']['gaussian']['sigma'] = 1 + if 'sigma_range' not in self.cfg['processing']['rubbersheet']['gaussian']: + self.cfg['processing']['rubbersheet']['gaussian']['sigma_range'] = 1 + if 'sigma_azimuth' not in self.cfg['processing']['rubbersheet']['gaussian']: + self.cfg['processing']['rubbersheet']['gaussian']['sigma_azimuth'] = 1 # If dense_offsets_path is None, assume that we run rubbersheet # as part of insar.py. In this case, dense_offsets_path comes from diff --git a/share/nisar/defaults/gcov.yaml b/share/nisar/defaults/gcov.yaml index fd1f307a2..a0850b6c8 100644 --- a/share/nisar/defaults/gcov.yaml +++ b/share/nisar/defaults/gcov.yaml @@ -101,7 +101,10 @@ runconfig: input_terrain_radiometry: beta0 # OPTIONAL - Minimum RTC area factor in dB - rtc_min_value_db: -30 + rtc_min_value_db: + + # RTC DEM upsampling + dem_upsampling: 1 # OPTIONAL - Mechanism to specify output posting and DEM geocode: @@ -131,10 +134,10 @@ runconfig: abs_rad_cal: 1 # OPTIONAL - Clip values above threshold - clip_max: 5.0 + clip_max: # OPTIONAL - Clip values below threshold - clip_min: 0.0 + clip_min: # OPTIONAL - Double sampling of the radar-grid # input sampling in the range direction diff --git a/share/nisar/defaults/insar.yaml b/share/nisar/defaults/insar.yaml index a0ebf7fc6..0b1638546 100644 --- a/share/nisar/defaults/insar.yaml +++ b/share/nisar/defaults/insar.yaml @@ -100,10 +100,13 @@ runconfig: y_abs: x_abs: - radar_grid_cubes: - + # OPTIONAL - Set lines to be processed per block + lines_per_block: 1000 + + radar_grid_cubes: + # List of heights in meters - heights: + heights: # OPTIONAL - Same as the geocode group outputEPSG if not provided outputEPSG: @@ -114,8 +117,8 @@ runconfig: # All postings/spacings must be > 0. # ISCE3 output rasters always have North-up West-left orientation output_posting: - x_posting: - y_posting: + x_posting: + y_posting: # OPTIONAL - To control output grid in same units as output EPSG y_snap: @@ -124,7 +127,7 @@ runconfig: x_snap: # OPTIONAL - Can control with absolute values or with snap values - top_left: + top_left: # OPTIONAL - Set top-left y in same units as output EPSG y_abs: # OPTIONAL - Set top-left x in same units as output EPSG @@ -133,15 +136,20 @@ runconfig: # OPTIONAL - Can control with absolute values or with snap values bottom_right: y_abs: - x_abs: + x_abs: geo2rdr: # No topo_path provided. Default to ScratchPath. threshold: 1.0e-8 maxiter: 25 + lines_per_block: 1000 - blocksize: - y: 1000 + rdr2geo: + # No topo_path provided. Default to ScratchPath. + threshold: 1.0e-7 + numiter: 25 + extraiter: 10 + lines_per_block: 1000 dem_margin: @@ -280,6 +288,17 @@ runconfig: oversample: 2 rows_per_block: 8192 + + filter_interferogram: + interferogram_path: + mask: + general: + lines_per_block: 100 + filter_type: boxcar + boxcar: + filter_size_range: 9 + filter_size_azimuth: 9 + phase_unwrap: # Path to HDF5 file or directory containing the input interferogram # and coherence (normalized magnitude of complex correlation) rasters @@ -291,3 +310,4 @@ runconfig: correlation_threshold_increments: 0.1 # Unwrapping algorithm to use algorithm: icu + diff --git a/share/nisar/schemas/focus.yaml b/share/nisar/schemas/focus.yaml index 8ecad1d63..0e09b8b23 100644 --- a/share/nisar/schemas/focus.yaml +++ b/share/nisar/schemas/focus.yaml @@ -56,7 +56,6 @@ runconfig: ProcessingType: enum('PR', 'UR') # production, urgent response MissionId: enum('NISAR') ProductAccuracy: enum('P', 'M', 'N', 'F', 'T') - CoverageIndicator: enum('F', 'P') DebugLevelGroup: DebugSwitch: bool() Geometry: @@ -64,6 +63,8 @@ runconfig: RelativeOrbitNumber: int(min=1, max=173) FrameNumber: int(min=1, max=176) OrbitDirection: enum('Descending', 'Ascending') + TrackFramePolygon: str(required=False) + FullCoverageThresholdPercent: num(min=0.0, required=False) worker: # Whether or not to use GPU, optional. Defaults to True if available. gpu_enabled: bool(required=False) diff --git a/share/nisar/schemas/gcov.yaml b/share/nisar/schemas/gcov.yaml index 29ae97734..fc93d840f 100644 --- a/share/nisar/schemas/gcov.yaml +++ b/share/nisar/schemas/gcov.yaml @@ -16,6 +16,7 @@ runconfig: ProductPathGroup: # Directory where PGE will place results ProductPath: str() + ProductCounter: int(min=1, max=999, required=False) # Directory where SAS can write temporary data ScratchPath: str() @@ -27,6 +28,9 @@ runconfig: PrimaryExecutable: ProductType: enum('GCOV') + CompositeReleaseID: regex(r'\w\d\d\d\d\d', name='CRID', required=False) + ProcessingType: enum('PR', 'UR', required=False) + ProductAccuracy: enum('P', 'M', 'N', 'F', 'T', required=False) DebugLevelGroup: DebugSwitch: bool() @@ -172,6 +176,9 @@ rtc_options: # Minimum RTC area factor in dB rtc_min_value_db: num(required=False) + # RTC DEM upsampling + dem_upsampling: int(min=1, required=False) + geocode_options: algorithm_type: enum('area_projection', 'sinc', 'bilinear', 'bicubic', 'nearest', 'biquintic', required=False) diff --git a/share/nisar/schemas/gslc.yaml b/share/nisar/schemas/gslc.yaml index 163333e0d..915975f6c 100644 --- a/share/nisar/schemas/gslc.yaml +++ b/share/nisar/schemas/gslc.yaml @@ -16,6 +16,8 @@ runconfig: ProductPathGroup: # Directory where PGE will place results ProductPath: str() + # Product Counter + ProductCounter: int(min=1, max=999, required=False) # Directory where SAS can write temporary data ScratchPath: str() @@ -27,6 +29,9 @@ runconfig: PrimaryExecutable: ProductType: enum('GSLC') + CompositeReleaseID: regex(r'\w\d\d\d\d\d', name='CRID', required=False) + ProcessingType: enum('PR', 'UR', required=False) + ProductAccuracy: enum('P', 'M', 'N', 'F', 'T', required=False) DebugLevelGroup: DebugSwitch: bool() diff --git a/share/nisar/schemas/insar.yaml b/share/nisar/schemas/insar.yaml index b37607997..8b3640272 100644 --- a/share/nisar/schemas/insar.yaml +++ b/share/nisar/schemas/insar.yaml @@ -16,6 +16,7 @@ runconfig: ProductPathGroup: # Directory where PGE will place results ProductPath: str() + ProductCounter: int(min=1, max=999, required=False) # Directory where SAS can write temporary data ScratchPath: str() @@ -29,6 +30,9 @@ runconfig: PrimaryExecutable: # RIFG_RUNW_GUNW will produce all three InSAR products ProductType: enum('GUNW', 'RIFG', 'RUNW', 'RIFG_RUNW_GUNW', 'RUNW_STANDALONE', 'GUNW_STANDALONE') + CompositeReleaseID: regex(r'\w\d\d\d\d\d', name='CRID', required=False) + ProcessingType: enum('PR', 'UR', required=False) + ProductAccuracy: enum('P','M','N','F','T', required=False) DebugLevelGroup: DebugSwitch: bool() @@ -54,10 +58,9 @@ runconfig: radar_grid_cubes: include('radar_grid_cubes_options', required=False) - geo2rdr: include('geo2rdr_options', required=False) + rdr2geo: include('rdr2geo_options', required=False) - blocksize: - y: int(min=100, max=10000) + geo2rdr: include('geo2rdr_options', required=False) dem_margin: num(required=False) @@ -76,9 +79,12 @@ runconfig: # Interferogram/coherence options (e.g. range looks) crossmul: include('crossmul_options', required=False) + filter_interferogram: include('filter_interferogram_options', required=False) + # Phase unwrapping options (e.g. unwrapping algorithm) phase_unwrap: include('phase_unwrap_options', required=False) + # To setup type of worker worker: include('worker_options', required=False) @@ -357,8 +363,14 @@ offset_filter_options: filter_size_azimuth: int(min=3, required=False) gaussian_filter_options: - # Gaussian filter standard deviation - sigma: int(min=0, required=False) + # Gaussian filter standard deviation along slant range + sigma_range: num(min=0, required=False) + # Gaussian filter standard deviation along azimuth + sigma_azimuth: num(min=0, required=False) + # Number of rows for 1D gaussian kernel + filter_size_azimuth: int(min=1, required=False) + # Number of columns for 1D gaussian kernel + filter_size_range: int(min=1, required=False) crossmul_options: # Path to HDF5 file or directory with coregistered SLCs @@ -394,6 +406,45 @@ crossmul_options: # Number of lines per block to process in batch rows_per_block: int(min=1, required=False) +filter_interferogram_options: + # Path to HDF5 or directory containing complex interferogram + # Not required as InSAR workflow allows using intermediate outputs + # from previous steps (not user-specified) + interferogram_path: str(required=False) + + # Options to set frequency/polarization dependent masks during filtering + mask: include('filter_mask_options', required=False) + + # Blocksize, for block-processing usage on insar_filtering.py + lines_per_block: int(min=1, required=False) + + # Filter type to be used in interferogram filtering. Available options: + # no filter, boxcar, gaussian + filter_type: enum('no_filter', 'boxcar', 'gaussian', required=False) + + # Boxcar filter options: kernel sizes in slant range and azimuth + boxcar: include('offset_filter_options', required=False) + + # Gaussian filter options: standard deviation in slant range and azimuth + gaussian: include('gaussian_filter_options', required=False) + +filter_mask_options: + # Filepath to one mask to be applied for each frequency and polarization + general: str(required=False) + + # Filepath to the masks applied to filter frequency A rasters + A: include('freq_pol_masks', required=False) + + # Filepath to the masks applied to filter frequency B rasters + B: include('freq_pol_masks', required=False) + +freq_pol_masks: + # Filepath to masks for filtering HH, HV, VH, and VV raster layers + HH: str(required=False) + HV: str(required=False) + VH: str(required=False) + VV: str(required=False) + radar_grid_cubes_options: # List of heights in meters @@ -430,15 +481,31 @@ radar_grid_cubes_options: geo2rdr_options: # Convergence threshold for geo2rdr algorithm - threshold: num(min=1.0e-9, max=1.0e-3, required=False) + threshold: num(min=0, required=False) # Maximum number of iterations - maxiter: int(min=10, max=50, required=False) + maxiter: int(min=1, required=False) # Path to rdr2geo (topo) outputs. Required only for # stand-alone usage of geo2rdr.py topo_path: str(required=False) + # Line per block to + lines_per_block: int(min=1, required=False) + +rdr2geo_options: + # Convergence threshold for rdr2geo algorithm + threshold: num(min=0, required=False) + + # Maximum number of iterations + numiter: int(min=1, required=False) + + # Extra/secondary number of iterations + extraiter: int(min=1, required=False) + + # Line per block to + lines_per_block: int(min=1, required=False) + log_nfo: # Path to log file @@ -513,6 +580,9 @@ geocode_options: # Set bottom-right x in same units as output EPSG x_abs: num(required=False) + # Set lines to be processed per block + lines_per_block: int(min=100, max=10000, required=False) + gunw_datasets: connectedComponents: bool(required=False) coherenceMagnitude: bool(required=False) diff --git a/tests/cxx/isce3/Sources.cmake b/tests/cxx/isce3/Sources.cmake index 5eac1213e..09390b869 100644 --- a/tests/cxx/isce3/Sources.cmake +++ b/tests/cxx/isce3/Sources.cmake @@ -1,10 +1,10 @@ set(TESTFILES +antenna/edge_method_cost_func.cpp antenna/frame.cpp container/rsd.cpp core/attitude/quaternion_euler.cpp core/attitude/attitude.cpp core/attitude/representations.cpp -core/cube/cube.cpp core/datetime/datetime.cpp core/ellipsoid/ellipsoid.cpp core/interp1d.cpp @@ -57,10 +57,10 @@ io/raster/raster.cpp io/raster/rasterepsg.cpp io/raster/rastermatrix.cpp io/raster/rasterview.cpp -matchtemplate/ampcor/ampcor.cpp math/bessel/bessel53.cpp math/sinc.cpp math/polyfunc.cpp +math/root_find1d.cpp polsar/symmetrize.cpp product/serialization/serializeProduct.cpp product/serialization/serializeProductMetadata.cpp diff --git a/tests/cxx/isce3/antenna/edge_method_cost_func.cpp b/tests/cxx/isce3/antenna/edge_method_cost_func.cpp new file mode 100644 index 000000000..e6ba40952 --- /dev/null +++ b/tests/cxx/isce3/antenna/edge_method_cost_func.cpp @@ -0,0 +1,198 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +using namespace isce3::antenna; + +// a helper function to evaluate a Poly1d object over Eigen array of inputs +Eigen::ArrayXd polyvals(const isce3::core::Poly1d& pf, Eigen::ArrayXd x) +{ + Eigen::Map pf_coef_map( + pf.coeffs.data(), pf.coeffs.size()); + return isce3::math::polyval(pf_coef_map, x, pf.mean, pf.norm); +} + +struct EdgeMethodCostFuncTest : public ::testing::Test { + + void SetUp() override + { + // form gain and look angle of reference antenna pattern + // perform polyfiting to build Poly1d object version of reference + // antenna pattern + Eigen::Map lka_deg_map( + lka_deg.data(), lka_deg.size()); + Eigen::Map gain_map(gain.data(), gain.size()); + pf_ref = isce3::math::polyfitObj(lka_deg_map * d2r, gain_map, 6, false); + + // uniformly-spaced look angles around rising edge used for both antenna + // and echo objects + Eigen::ArrayXd lka_edge_rad; + lka_edge_rad = Eigen::ArrayXd::LinSpaced( + num_lka_edge, d2r * min_lka_edge_deg, d2r * max_lka_edge_deg); + + // form ANT 3rd-order poly object with roll offset applied to edge look + // angles + pf_ant_vec.reserve(roll_ofs_ant_mdeg.size()); + for (const auto& roll : roll_ofs_ant_mdeg) { + // add roll offset (perturbed) + auto lka_ant_rad = lka_edge_rad + roll * md2r; + // add a gain offset + auto gain_ant = polyvals(pf_ref, lka_ant_rad) + gain_ofs; + pf_ant_vec.push_back( + isce3::math::polyfitObj(lka_edge_rad, gain_ant, 3, false)); + } + + // form echo 3rd-order poly object with roll offset applied to edge look + // angles + auto gain_echo = polyvals(pf_ref, lka_edge_rad); + pf_echo = isce3::math::polyfitObj(lka_edge_rad, gain_echo, 3, false); + + // here we use constant but non-normalized weights (order 0) + pf_wgt = isce3::core::Poly1d(0, 0.0, 1.0); + pf_wgt.coeffs = std::vector {10}; + } + // List of methods + void validate_estimation(const std::tuple& est, + double roll_true_mdeg, const std::string& err_msg = {}) + { + auto [roll_est, f_val, flag, n_iter] = est; + // Absolute error (residual after compensating for estimated offset) in + // (mdeg) + auto abs_err = std::abs(roll_est * r2md + roll_true_mdeg); + // check individual values + std::string err_msg1 {"@ true roll offset " + + std::to_string(roll_true_mdeg) + " (mdeg) " + + err_msg}; + EXPECT_LE(n_iter, max_iter) + << "Exceed max number of iteration " + err_msg1; + EXPECT_TRUE(flag) << "Wrong convergence flag " + err_msg1; + EXPECT_NEAR(abs_err, 0.0, max_abs_err_mdeg) + << "Too large residual roll offset " + err_msg1; + EXPECT_NEAR(f_val, 0.0, abs_tol) + << "Wrong cost function value " + err_msg1; + } + + // List of public members + + // conversion from (deg/mdeg) to (rad) and vice versa + const double d2r {M_PI / 180.}; + const double md2r {d2r * 1e-3}; + const double r2md {1.0 / md2r}; + + // max absolute pointing error (mdeg) of the estimation over wide range of + // Roll angle offset. This is used to evaluate the residual error after + // compensating for estimated offset. e.g. within [-200, +200] (mdeg, mdeg) + // used here, it is set to around 1% margin of total 400 mdeg. This is way + // finer than the requirement (>=15 mdeg)! + const double max_abs_err_mdeg {5.0}; + + // Absolute function value tolerance in root of cost function used in + // "RollAngleOffsetFromEdge" + const double abs_tol {1e-4}; + // Max expected interation of cost function used in + // "RollAngleOffsetFromEdge" + const int max_iter {20}; + + // look angle (off-nadir angle) inputs + const double min_lka_edge_deg {32.8}; + const double max_lka_edge_deg {34.0}; + const double prec_lka_edge_deg {1e-3}; + const int num_lka_edge { + static_cast( + (max_lka_edge_deg - min_lka_edge_deg) / prec_lka_edge_deg) + + 1}; + + // gain offset in (dB) between relative EL power patterns extracted from + // antenna and echo. the roll offset estimation is insensitive to this gain + // offset! + const double gain_ofs {0.5}; + + // desired roll angle offset in (mdeg) , ground truth values used for + // validation. These value are also used to perturb EL power pattern from + // antenna given the cost function tries to find a roll offset to be added + // to antenna EL to align its power pattern with that of echo data. Thus, + // the sign of estimated roll offset will be the opposite of these angles + // with some tiny deviation due to poly fitting. + std::vector roll_ofs_ant_mdeg {-198.0, -42.5, 0.0, 67.0, 157.3}; + + // Build a 6-order polyminals of a relative antenna gain from gain (dB) + // versus look angles (rad) to be used as a reference for building both + // antenna and echo data + // These points are extracted from a realitic EL power pattern of ALOS1 beam + // #7. + std::vector gain { + -2.2, -1.2, -0.55, -0.2, 0.0, -0.2, -0.5, -1.0, -2.0}; + std::vector lka_deg { + 32.0, 32.5, 33.0, 33.5, 34.1, 34.5, 35., 35.5, 36.}; + isce3::core::Poly1d pf_ref; + + // a vector of 3rd-order polyfit objects for Antenna , one per each roll + // angle offset (perturbed) + std::vector pf_ant_vec; + + // 3rd-order polyfit object for Echo common for all offset (unperturbed) + isce3::core::Poly1d pf_echo; + + // Polyfit version of weights used in weighting cost function over look + // angles (optional) + isce3::core::Poly1d pf_wgt; +}; + +TEST_F(EdgeMethodCostFuncTest, RollAngleOffsetFromEdge_LookAngNearFar) +{ + // loop over roll offset (and antenna polyfit objects) + for (std::size_t idx = 0; idx < roll_ofs_ant_mdeg.size(); ++idx) { + // estimate roll offset w/o weighting + auto est_tuple = rollAngleOffsetFromEdge(pf_echo, pf_ant_vec[idx], + min_lka_edge_deg * d2r, max_lka_edge_deg * d2r, + prec_lka_edge_deg * d2r); + // validate results w/o weighting + validate_estimation(est_tuple, roll_ofs_ant_mdeg[idx], + std::string("w/o weighting!")); + + // estimate roll offset w/ weighting + auto est_wgt_tuple = rollAngleOffsetFromEdge(pf_echo, pf_ant_vec[idx], + min_lka_edge_deg * d2r, max_lka_edge_deg * d2r, + prec_lka_edge_deg * d2r, pf_wgt); + // validate results w/ weighting + validate_estimation(est_wgt_tuple, roll_ofs_ant_mdeg[idx], + std::string("w/ weighting!")); + } +} + +TEST_F(EdgeMethodCostFuncTest, RollAngleOffsetFromEdge_LookAngLinspace) +{ + // form isce3 Linspace object for uniformly sampled look angles within [min + // ,max] + auto lka_lsp = isce3::core::Linspace::from_interval( + min_lka_edge_deg * d2r, max_lka_edge_deg * d2r, num_lka_edge); + // loop over roll offset (and antenna polyfit objects) + for (std::size_t idx = 0; idx < roll_ofs_ant_mdeg.size(); ++idx) { + // estimate roll offset w/o weighting + auto est_tuple = + rollAngleOffsetFromEdge(pf_echo, pf_ant_vec[idx], lka_lsp); + // validate results + validate_estimation(est_tuple, roll_ofs_ant_mdeg[idx], + std::string("w/o weighting!")); + + // estimate roll offset w/ weighting + auto est_wgt_tuple = rollAngleOffsetFromEdge( + pf_echo, pf_ant_vec[idx], lka_lsp, pf_wgt); + // validate results w/ weighting + validate_estimation(est_wgt_tuple, roll_ofs_ant_mdeg[idx], + std::string("w/ weighting!")); + } +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/cxx/isce3/core/attitude/attitude.cpp b/tests/cxx/isce3/core/attitude/attitude.cpp index b4681245d..21b8adb9c 100644 --- a/tests/cxx/isce3/core/attitude/attitude.cpp +++ b/tests/cxx/isce3/core/attitude/attitude.cpp @@ -38,9 +38,9 @@ struct AttitudeTest : public ::testing::Test { attitude = Attitude(time, quaternions, epoch); // Define the reference rotation matrix (YPR) - R_ypr_ref = {{{0.993760669166, -0.104299329454, 0.039514330251}, - {0.099708650872, 0.989535160981, 0.104299329454}, - {-0.049979169271, -0.099708650872, 0.993760669166}}}; + R_ypr_ref = cartmat_t{{{0.993760669166, -0.104299329454, 0.039514330251}, + {0.099708650872, 0.989535160981, 0.104299329454}, + {-0.049979169271, -0.099708650872, 0.993760669166}}}; // Set tolerance tol = 1.0e-10; diff --git a/tests/cxx/isce3/core/cube/cube.cpp b/tests/cxx/isce3/core/cube/cube.cpp deleted file mode 100644 index fc37e2a9d..000000000 --- a/tests/cxx/isce3/core/cube/cube.cpp +++ /dev/null @@ -1,180 +0,0 @@ -//-*- C++ -*- -//-*- coding: utf-8 -*- -// -// Author: Bryan V. Riel -// Copyright 2017-2018 -// - -#include -#include -#include -#include -#include -#include "gtest/gtest.h" - -// isce3::core -#include "isce3/core/Constants.h" -#include "isce3/core/Utilities.h" -#include "isce3/core/Cube.h" - -TEST(CubeTest, SimpleConstructor) { - // Make a cube with a fixed shape - isce3::core::Cube M(3, 4, 5); - ASSERT_EQ(M.height(), 3); - ASSERT_EQ(M.length(), 4); - ASSERT_EQ(M.width(), 5); -} - -TEST(CubeTest, Resize) { - // Make a cube with a fixed shape - isce3::core::Cube M(3, 4, 5); - // Resize it - M.resize(2, 3, 4); - // Check shape - ASSERT_EQ(M.height(), 2); - ASSERT_EQ(M.length(), 3); - ASSERT_EQ(M.width(), 4); -} - -TEST(CubeTest, FixedValues) { - // Make a cube with a fixed shape - isce3::core::Cube M(3, 4, 5); - - // Fill it with zeros and check values - M.zeros(); - for (size_t count = 0; count < (M.height() * M.width() * M.length()); ++count) { - ASSERT_NEAR(M(count), 0.0, 1.0e-12); - } - - // Fill it with a constant value and check - M.fill(10.0); - for (size_t count = 0; count < (M.height() * M.width() * M.length()); ++count) { - ASSERT_NEAR(M(count), 10.0, 1.0e-12); - } -} - -TEST(CubeTest, VectorConstructor) { - // Make a vector of values - std::vector values = isce3::core::arange(0.0, 60.0, 1.0); - // Make a cube from the vector - isce3::core::Cube M(values, 4, 5); - - // Check the shape - ASSERT_EQ(M.height(), 3); - ASSERT_EQ(M.length(), 4); - ASSERT_EQ(M.width(), 5); - - // Check the values with flattened indices - for (size_t i = 0; i < values.size(); ++i) { - // Get the matrix value - const double mat_val = M(i); - // Get the vector value - const double vec_val = values[i]; - // Check - ASSERT_NEAR(mat_val, vec_val, 1.0e-12); - } -} - -TEST(CubeTest, CopyConstructor) { - // Make a vector of values - std::vector values = isce3::core::arange(0.0, 60.0, 1.0); - // Make a cube from the vector - isce3::core::Cube M(values, 4, 5); - // Make a shallow copy - isce3::core::Cube N(M); - - // Check shapes are equal - ASSERT_EQ(M.height(), N.height()); - ASSERT_EQ(M.width(), N.width()); - ASSERT_EQ(M.length(), N.length()); - - // Check the values - for (size_t h=0; h < M.height(); ++h) { - for (size_t i = 0; i < M.length(); ++i) { - for (size_t j = 0; j < M.width(); ++j) { - ASSERT_NEAR(M(h,i,j), N(h,i,j), 1.0e-12); - } - } - } - - // Change value of middle element of original matrix - M(1, 1, 1) = 20.0; - // Check corresponding value in copied matrix has been udpated - ASSERT_NEAR(N(1, 1, 1), 20.0, 1.0e-12); - - // Change value of last element in copied matrix - N(2, 3, 4) = 50.0; - // Check corresponding value in original matrix has been udpated - ASSERT_NEAR(M(2, 3, 4), 50.0, 1.0e-12); -} - -TEST(CubeTest, DeepCopyConstructor) { - // Make a vector of values - std::vector values = isce3::core::arange(0.0, 60.0, 1.0); - // Make a const cube from the vector - const isce3::core::Cube M(values, 4, 5); - - //Copy original value in 1,1,1 - double origval = M(1,1,1); - - // Make a deep copy (by passing in const matrix) - isce3::core::Cube N(M); - - // Change value of middle element of copied matrix - N(1, 1, 1) = 20.0; - - // Check corresponding value in original matrix has NOT been udpated - ASSERT_NEAR(M(1, 1, 1), origval, 1.0e-12); -} - -TEST(CubeTest, CubeView) { - // Make a vector of values - std::vector values = isce3::core::arange(0.0, 60.0, 1.0); - // Make a cube from the vector - isce3::core::Cube M(values, 4, 5); - // Get a view of a subset of the cube - const isce3::core::Cube::view_t view = M.subcube(1, 1, 1, 2, 2, 2); - - // Vector of expected values - std::vector expected{26.0, 27.0, 31.0, 32.0, 46.0, 47.0, 51.0, 52.0}; - - // Compare values - size_t count = 0; - for (auto it = view.begin(); it != view.end(); ++it) { - double view_val = *it; - ASSERT_NEAR(view_val, expected[count], 1.0e-12); - ++count; - } -} - -TEST(CubeTest, squareBracket) { - //Make a vector of values - std::vector values = isce3::core::arange(0.0, 60.0, 1.0); - //Make a cube from the vector - isce3::core::Cube M(values, 4, 5); - //Test flat indices - for (size_t ii=0; ii<60; ii++) - { - decltype(M)::index_t ind = { ii/20, (ii%20)/5, ii%5}; - ASSERT_NEAR( M[ind], 1.0*ii, 1.0e-12); - } - //Negate all values - for (size_t ii=0; ii<60; ii++) - { - decltype(M)::index_t ind = { ii/20, (ii%20)/5, ii%5}; - M[ind] = -M[ind]; - } - //Check for negative values - for (size_t ii=0; ii<60; ii++) - { - decltype(M)::index_t ind = { ii/20, (ii%20)/5, ii%5}; - ASSERT_NEAR( M[ind], -1.0*ii, 1.0e-12); - } -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} - -// end of file diff --git a/tests/cxx/isce3/core/matrix/matrix.cpp b/tests/cxx/isce3/core/matrix/matrix.cpp index 8af40c810..c32ec6841 100644 --- a/tests/cxx/isce3/core/matrix/matrix.cpp +++ b/tests/cxx/isce3/core/matrix/matrix.cpp @@ -73,36 +73,6 @@ TEST(MatrixTest, VectorConstructor) { } TEST(MatrixTest, CopyConstructor) { - // Make a vector of values - std::vector values = isce3::core::arange(0.0, 9.0, 1.0); - // Make a matrix from the vector - isce3::core::Matrix M(values, 3); - // Make a shallow copy - isce3::core::Matrix N(M); - - // Check shapes are equal - ASSERT_EQ(M.width(), N.width()); - ASSERT_EQ(M.length(), N.length()); - - // Check the values - for (size_t i = 0; i < M.length(); ++i) { - for (size_t j = 0; j < M.width(); ++j) { - ASSERT_NEAR(M(i,j), N(i,j), 1.0e-12); - } - } - - // Change value of middle element of original matrix - M(1, 1) = 20.0; - // Check corresponding value in copied matrix has been udpated - ASSERT_NEAR(N(1, 1), 20.0, 1.0e-12); - - // Change value of last element in copied matrix - N(2, 2) = 50.0; - // Check corresponding value in original matrix has been udpated - ASSERT_NEAR(M(2, 2), 50.0, 1.0e-12); -} - -TEST(MatrixTest, DeepCopyConstructor) { // Make a vector of values std::vector values = isce3::core::arange(0.0, 9.0, 1.0); // Make a const matrix from the vector @@ -122,17 +92,19 @@ TEST(MatrixTest, MatrixView) { // Make a matrix from the vector isce3::core::Matrix M(values, 3); // Get a view of a subset of the matrix - const isce3::core::Matrix::view_t view = M.submat(1, 1, 2, 2); + auto view = M.submat(1, 1, 2, 2); // Vector of expected values std::vector expected{4.0, 5.0, 7.0, 8.0}; // Compare values size_t count = 0; - for (auto it = view.begin(); it != view.end(); ++it) { - double view_val = *it; - ASSERT_NEAR(view_val, expected[count], 1.0e-12); - ++count; + for (int row = 0; row < view.rows(); row++) { + for (int col = 0; col < view.cols(); col++) { + double view_val = view(row, col); + ASSERT_NEAR(view_val, expected[count], 1.0e-12); + ++count; + } } } @@ -168,7 +140,7 @@ TEST(MatrixTest, MatrixViewSet) { N.zeros(); // Set column of matrix with row from original matrix - N.submat(0, 1, 3, 1) = M.submat(1, 0, 1, 3); + N.submat(0, 1, 3, 1) = M.submat(1, 0, 1, 3).transpose(); // Vector of expected values std::vector expected{0.0, 3.0, 0.0, @@ -176,8 +148,10 @@ TEST(MatrixTest, MatrixViewSet) { 0.0, 5.0, 0.0}; // Compare values - for (size_t count = 0; count < (N.width() * N.length()); ++count) { - ASSERT_NEAR(N(count), expected[count], 1.0e-12); + for (int row = 0; row < N.rows(); row++) { + for (int col = 0; col < N.cols(); col++) { + ASSERT_NEAR(N(row, col), expected[row * N.cols() + col], 1.0e-12); + } } } diff --git a/tests/cxx/isce3/cuda/Sources.cmake b/tests/cxx/isce3/cuda/Sources.cmake index 03795eeb6..f6cded312 100644 --- a/tests/cxx/isce3/cuda/Sources.cmake +++ b/tests/cxx/isce3/cuda/Sources.cmake @@ -25,19 +25,6 @@ geometry/geometry/gpuGeometry.cpp geometry/topo/gpuTopo.cpp image/resampslc/gpuResampSlc.cpp io/datastream/datastream.cu -matchtemplate/ampcor/addTiles.cpp -matchtemplate/ampcor/correlate.cpp -matchtemplate/ampcor/detect.cpp -matchtemplate/ampcor/maxcor.cpp -matchtemplate/ampcor/migrate.cpp -matchtemplate/ampcor/nudge.cpp -matchtemplate/ampcor/pushTiles.cpp -matchtemplate/ampcor/refine.cpp -matchtemplate/ampcor/refStats.cpp -matchtemplate/ampcor/sanity.cpp -matchtemplate/ampcor/sat.cpp -matchtemplate/ampcor/tgtStats.cpp -matchtemplate/ampcor/zoomcor.cpp signal/gpuCrossMul.cpp signal/gpuFilter.cpp signal/gpuLooks.cpp diff --git a/tests/cxx/isce3/geometry/metadata_cubes/metadata_cubes.cpp b/tests/cxx/isce3/geometry/metadata_cubes/metadata_cubes.cpp index cdf7114a8..27f4e723d 100644 --- a/tests/cxx/isce3/geometry/metadata_cubes/metadata_cubes.cpp +++ b/tests/cxx/isce3/geometry/metadata_cubes/metadata_cubes.cpp @@ -35,7 +35,8 @@ void _check_vectors(const isce3::product::GeoGridParameters& geogrid, isce3::io::Raster& los_unit_vector_y_raster, isce3::io::Raster& along_track_unit_vector_x_raster, isce3::io::Raster& along_track_unit_vector_y_raster, - isce3::io::Raster& elevation_angle_raster) + isce3::io::Raster& elevation_angle_raster, + isce3::io::Raster& ground_track_velocity_raster) { auto proj = isce3::core::makeProjection(geogrid.epsg()); @@ -59,26 +60,15 @@ void _check_vectors(const isce3::product::GeoGridParameters& geogrid, geogrid.length(), geogrid.width(), band); auto elevation_angle_array = _getCubeArray( elevation_angle_raster, geogrid.length(), geogrid.width(), band); + auto ground_track_velocity_array = _getCubeArray( + ground_track_velocity_raster, geogrid.length(), geogrid.width(), + band); - double slant_range_error_threshold; - double vel_unit_error_threshold; - double sat_position_error_threshold; - double incidence_angle_error_threshold; - /* - Error thresholds in UTM are larger because of the higher number of - operations whose errors add up. - */ - if (geogrid.epsg() == 4326) { - slant_range_error_threshold = 1e-16; - incidence_angle_error_threshold = 1e-5; - sat_position_error_threshold = 1e-3; - vel_unit_error_threshold = 1e-3; - } else { - slant_range_error_threshold = 1e-16; - incidence_angle_error_threshold = 1e-5; - sat_position_error_threshold = 1e-3; - vel_unit_error_threshold = 1e-3; - } + const double slant_range_error_threshold = 1e-16; + const double vel_unit_error_threshold = 1e-3; + const double sat_position_error_threshold = 1e-3; + const double incidence_angle_error_threshold = 1e-5; + const double ground_track_velocity_error_threshold = 1e-15; for (int i = 0; i < geogrid.length(); ++i) { double pos_y = geogrid.startY() + (0.5 + i) * geogrid.spacingY(); @@ -210,13 +200,21 @@ void _check_vectors(const isce3::product::GeoGridParameters& geogrid, along_track_unit_vector_xyz_test = (sat_next_xyz - sat_xyz).normalized(); } - // 4. Check velocity unit vector + // 4. Check along-track unit vector ASSERT_NEAR(along_track_unit_vector_xyz_test[0], vel_unit_xyz[0], vel_unit_error_threshold); ASSERT_NEAR(along_track_unit_vector_xyz_test[1], vel_unit_xyz[1], vel_unit_error_threshold); ASSERT_NEAR(along_track_unit_vector_xyz_test[2], vel_unit_xyz[2], vel_unit_error_threshold); + + // 5. Check ground-track velocity vector + double ground_track_velocity_test = ground_track_velocity_array(i, j); + const double ground_track_velocity_ref = + target_xyz.norm() * vel_xyz.norm() / sat_xyz.norm(); + ASSERT_NEAR(ground_track_velocity_test, ground_track_velocity_ref, + ground_track_velocity_error_threshold); + } } } @@ -449,7 +447,10 @@ TEST(radarGridCubeTest, testRadarGridCube) GDT_Float32, "ENVI"); isce3::io::Raster elevation_angle_raster("elevationAngle.rdr", width, length, heights.size(), - GDT_Float64, "ENVI"); + GDT_Float32, "ENVI"); + isce3::io::Raster ground_track_velocity_raster( + "groundTrackVelocity.rdr", width, length, heights.size(), + GDT_Float64, "ENVI"); isce3::product::GeoGridParameters geogrid(x0, y0, dx, dy, width, length, epsg); @@ -465,6 +466,7 @@ TEST(radarGridCubeTest, testRadarGridCube) &incidence_angle_raster, &los_unit_vector_x_raster, &los_unit_vector_y_raster, &along_track_unit_vector_x_raster, &along_track_unit_vector_y_raster, &elevation_angle_raster, + &ground_track_velocity_raster, threshold_geo2rdr, numiter_geo2rdr, delta_range); // 1. Check geotransform and EPSG @@ -476,7 +478,8 @@ TEST(radarGridCubeTest, testRadarGridCube) los_unit_vector_y_raster, along_track_unit_vector_x_raster, along_track_unit_vector_y_raster, - elevation_angle_raster}; + elevation_angle_raster, + ground_track_velocity_raster}; for (auto cube_raster : raster_vector) { ASSERT_TRUE(cube_raster.getEPSG() == epsg); @@ -503,7 +506,8 @@ TEST(radarGridCubeTest, testRadarGridCube) slant_range_raster, azimuth_time_raster, incidence_angle_raster, los_unit_vector_x_raster, los_unit_vector_y_raster, along_track_unit_vector_x_raster, - along_track_unit_vector_y_raster, elevation_angle_raster); + along_track_unit_vector_y_raster, elevation_angle_raster, + ground_track_velocity_raster); } // 3. Compare results with topo @@ -730,7 +734,10 @@ TEST(metadataCubesTest, testMetadataCubes) { GDT_Float32, "ENVI"); isce3::io::Raster elevation_angle_raster("elevationAngle.bin", width, length, heights.size(), - GDT_Float64, "ENVI"); + GDT_Float32, "ENVI"); + isce3::io::Raster ground_track_velocity_raster( + "groundTrackVelocity.bin", width, length, heights.size(), + GDT_Float32, "ENVI"); // Make cubes isce3::geometry::makeGeolocationGridCubes(radar_grid, heights, orbit, @@ -739,7 +746,8 @@ TEST(metadataCubesTest, testMetadataCubes) { &los_unit_vector_x_raster, &los_unit_vector_y_raster, &along_track_unit_vector_x_raster, &along_track_unit_vector_y_raster, &elevation_angle_raster, - threshold_geo2rdr, numiter_geo2rdr, delta_range); + &ground_track_velocity_raster, threshold_geo2rdr, numiter_geo2rdr, + delta_range); auto proj = isce3::core::makeProjection(epsg); diff --git a/tests/cxx/isce3/geometry/topo/topo.cpp b/tests/cxx/isce3/geometry/topo/topo.cpp index 88636b8b7..9e39addd7 100644 --- a/tests/cxx/isce3/geometry/topo/topo.cpp +++ b/tests/cxx/isce3/geometry/topo/topo.cpp @@ -58,10 +58,13 @@ TEST(TopoTest, RunTopo) { TEST(TopoTest, CheckResults) { // Open generated topo raster + std::cout << "test file: ./topo.vrt" << std::endl; isce3::io::Raster testRaster("topo.vrt"); // Open reference topo raster - isce3::io::Raster refRaster(TESTDATA_DIR "topo/topo.vrt"); + std::string ref_filename = TESTDATA_DIR "topo/topo.vrt"; + std::cout << "reference file:" << ref_filename << std::endl; + isce3::io::Raster refRaster(ref_filename); // The associated tolerances std::vector tols{1.0e-5, 1.0e-5, 0.15, 1.0e-4, 1.0e-4, 0.02, 0.02}; @@ -75,6 +78,9 @@ TEST(TopoTest, CheckResults) { // Loop over topo bands for (size_t k = 0; k < refRaster.numBands(); ++k) { + + std::cout << "comparing band: " << k + 1 << std::endl; + // Compute sum of absolute error double error = 0.0; size_t count = 0; diff --git a/tests/cxx/isce3/io/raster/rastermatrix.cpp b/tests/cxx/isce3/io/raster/rastermatrix.cpp index e7a2b178e..5bf67cb65 100644 --- a/tests/cxx/isce3/io/raster/rastermatrix.cpp +++ b/tests/cxx/isce3/io/raster/rastermatrix.cpp @@ -28,16 +28,15 @@ class RasterTest : public ::testing::Test { std::remove( filename.c_str()); isce3::io::Raster inc = isce3::io::Raster( filename, nc, nl, 1, GDT_Float32, "GTiff"); - std::valarray block( nbx*nby ); // 1d valarray for uint nXBlocks = floor( (float) inc.width() / (float) nbx ); // number of blocks uint nYBlocks = floor( (float) inc.length() / (float) nby ); // number of blocks //Wrap Matrix around valarray - isce3::core::Matrix mat( &(block[0]), nby, nbx); + isce3::core::Matrix mat(nby, nbx); for ( uint y=0; y blockmat(nby, nbx); - //Create raster object from matrix - isce3::io::Raster raster(blockmat); - //Fill the matrix for(uint ii=0; ii < nby; ii++) for(uint jj=0; jj < nbx; jj++) blockmat(ii,jj) = ii * nbx + jj; + //Create raster object from matrix + isce3::io::Raster raster(blockmat); + //Scalar for querying raster float a; raster.getValue(a,0,0,1); @@ -134,16 +133,18 @@ TEST_F(RasterTest, setMatrixRaster) { //Set 0,0 and check a = 10.0; raster.setValue(a, 0, 0, 1); + raster.getBlock(blockmat, 0, 0, 1); ASSERT_EQ(blockmat(0,0), 10); //Set last pixel a = -4.0; raster.setValue(a, nbx-1, nby-1, 1); + raster.getBlock(blockmat, 0, 0, 1); ASSERT_EQ( blockmat(nby-1,nbx-1), -4); } -// +// TEST_F(RasterTest, setGetBlockEMatrix2D) { //Setup geotiff std::string filename = "matrix_getset.tif"; diff --git a/tests/cxx/isce3/math/root_find1d.cpp b/tests/cxx/isce3/math/root_find1d.cpp new file mode 100644 index 000000000..ca3ae4a11 --- /dev/null +++ b/tests/cxx/isce3/math/root_find1d.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace isce3::math; + +struct RootFind1dTest : public ::testing::Test { + + void SetUp() override + { + // Define 1-D poly-fit object + // (x - 1)^3 -> solution = 1.0 + pf_obj = isce3::core::Poly1d(3, 0.0, 1.0); + pf_obj.coeffs = std::vector {-1, 3, -3, 1}; + solution = 1; + } + // list of methods + void validate_constrcutor(const detail::RootFind1dBase& rf_obj, + double f_tol, int iter, std::optional x_tol, + const std::string& err_msg = {}) + { + ASSERT_EQ(rf_obj.max_num_iter(), iter) + << "Wrong max iteration " + err_msg; + ASSERT_NEAR(rf_obj.func_tol(), f_tol, abs_tol) + << "Wrong function tolerance " + err_msg; + if (x_tol) { + ASSERT_NEAR(*rf_obj.var_tol(), *x_tol, abs_tol) + << "Wrong variable tolerance " + err_msg; + } + } + void validate_root(const std::tuple& rf_result, + const std::string& err_msg = {}) + { + auto [x_val, f_val, flag, n_iter] = rf_result; + EXPECT_LE(n_iter, max_iter) + << "Exceed max number of iteration " + err_msg; + EXPECT_TRUE(flag) << "Wrong convergence flag " + err_msg; + EXPECT_NEAR(x_val, solution, solution * rel_tol) + << "Wrong root " + err_msg; + EXPECT_NEAR(f_val, 0.0, f_tol) << "Wrong function value " + err_msg; + } + // list of members + const double f_tol {1e-6}; + const double x_tol {1e-4}; + const int max_iter {30}; + const double rel_tol {1e-3}; + const double abs_tol {1e-8}; + double solution; + isce3::core::Poly1d pf_obj; +}; + +TEST_F(RootFind1dTest, NewtonConstructors) +{ + // Validate constructors with correct inputs + validate_constrcutor(RootFind1dNewton(), 1e-5, 20, {}, + std::string("for default constructor")); + + auto rf_obj = RootFind1dNewton(f_tol, max_iter, x_tol); + validate_constrcutor(rf_obj, f_tol, max_iter, x_tol, + std::string("for three-value constructor")); + + rf_obj = RootFind1dNewton(max_iter); + validate_constrcutor(rf_obj, 1e-5, max_iter, {}, + std::string("for single-value max-iter constructor")); + + // Check exceptions thrown by the constructors with bad input + EXPECT_THROW(RootFind1dNewton(0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad max_iter"; + + EXPECT_THROW(RootFind1dNewton(0.0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad func_tol"; + + EXPECT_THROW( + RootFind1dNewton(1e-2, 10, 0.0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad var_tol"; +} + +TEST_F(RootFind1dTest, SecantConstructors) +{ + // Validate constructors with correct inputs + validate_constrcutor(RootFind1dSecant(), 1e-5, 20, {}, + std::string("for default constructor")); + + auto rf_obj = RootFind1dSecant(f_tol, max_iter, x_tol); + validate_constrcutor(rf_obj, f_tol, max_iter, x_tol, + std::string("for three-value constructor")); + + rf_obj = RootFind1dSecant(max_iter); + validate_constrcutor(rf_obj, 1e-5, max_iter, {}, + std::string("for single-value max-iter constructor")); + + // Check exceptions thrown by the constructors with bad input + EXPECT_THROW(RootFind1dSecant(0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad max_iter"; + + EXPECT_THROW(RootFind1dSecant(0.0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad func_tol"; + + EXPECT_THROW( + RootFind1dSecant(1e-2, 10, 0.0), isce3::except::InvalidArgument) + << "Must throw ISCE3 InvalidArgument for bad var_tol"; +} + +TEST_F(RootFind1dTest, Poly2funcMethod) +{ + auto func = detail::RootFind1dBase::poly2func(pf_obj); + // Min four distinct values needed to confirm 3rd-order Polynomial + std::vector x_vals {-2, -1, 0, 1, 2}; + for (const auto& x : x_vals) + EXPECT_NEAR(func(x), pf_obj.eval(x), abs_tol) + << "Wrong function value for x=" << x; +} + +TEST_F(RootFind1dTest, NewtonRootMethod) +{ + // form RootFind1d obj used in all "root" cases + auto rf_obj = RootFind1dNewton(f_tol, max_iter, x_tol); + + // build function and its derivative + auto func = rf_obj.poly2func(pf_obj); + auto func_der = rf_obj.poly2func(pf_obj.derivative()); + + // Root via Newton approach with Poly1d obj + validate_root(rf_obj.root(pf_obj, 0.0), + std::string("for Root method with Poly1d as input")); + + // Root via Newton approach with callback function and its derivative + validate_root(rf_obj.root(func, func_der, 0.0), + std::string( + "for Root method with two callback functions as input")); +} + +TEST_F(RootFind1dTest, SecantRootMethod) +{ + // form RootFind1d obj used in all "root" cases + auto rf_obj = RootFind1dSecant(f_tol, max_iter, x_tol); + + // build function + auto func = rf_obj.poly2func(pf_obj); + + // Root via Secant approach with callback function (no derivative) and two + // initial values + validate_root(rf_obj.root(func, 0.0, -1.0), + std::string("for Root method with one callback function and two " + "initial values")); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/data/insar_test.yaml b/tests/data/insar_test.yaml index 7c7c76210..4afb53e84 100644 --- a/tests/data/insar_test.yaml +++ b/tests/data/insar_test.yaml @@ -103,9 +103,6 @@ runconfig: threshold: 1.0e-9 maxiter: 25 - blocksize: - y: 1000 - dem_margin: 0.1 coarse_resample: @@ -136,5 +133,10 @@ runconfig: flatten: False oversample: 2 + filter_interferogram: + interferogram_path: + lines_per_block: 22 + filter_type: boxcar + phase_unwrap: crossmul_path: @ISCETEST@/winnipeg.h5 diff --git a/tests/data/topo/localInc.rdr b/tests/data/topo/localInc.rdr index 1995a3583..3408e747a 100644 Binary files a/tests/data/topo/localInc.rdr and b/tests/data/topo/localInc.rdr differ diff --git a/tests/data/topo/localPsi.rdr b/tests/data/topo/localPsi.rdr index 08e6bb884..ee486840a 100644 Binary files a/tests/data/topo/localPsi.rdr and b/tests/data/topo/localPsi.rdr differ diff --git a/tests/data/topo_winnipeg/localInc.rdr b/tests/data/topo_winnipeg/localInc.rdr index 595b7472c..3c926696b 100644 Binary files a/tests/data/topo_winnipeg/localInc.rdr and b/tests/data/topo_winnipeg/localInc.rdr differ diff --git a/tests/data/topo_winnipeg/localPsi.rdr b/tests/data/topo_winnipeg/localPsi.rdr index cdf37e257..652ac436d 100644 Binary files a/tests/data/topo_winnipeg/localPsi.rdr and b/tests/data/topo_winnipeg/localPsi.rdr differ diff --git a/tests/python/extensions/pybind/CMakeLists.txt b/tests/python/extensions/pybind/CMakeLists.txt index ff0513829..96e76bfe8 100644 --- a/tests/python/extensions/pybind/CMakeLists.txt +++ b/tests/python/extensions/pybind/CMakeLists.txt @@ -50,6 +50,7 @@ geometry/look_inc_from_sr.py antenna/frame.py antenna/geometryfunc.py antenna/el_pattern_est.py +antenna/edge_method_cost_func.py ) if(WITH_CUDA) diff --git a/tests/python/extensions/pybind/antenna/edge_method_cost_func.py b/tests/python/extensions/pybind/antenna/edge_method_cost_func.py new file mode 100644 index 000000000..77d3ae3e0 --- /dev/null +++ b/tests/python/extensions/pybind/antenna/edge_method_cost_func.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +import numpy as np +import numpy.testing as npt + +from pybind_isce3 import antenna as ant +from pybind_isce3.core import Poly1d +from pybind_isce3.core import Linspace + +# functions for conversion from (rad) to (mdeg) and vice versa + + +def r2md(ang_rad): return 1000 * np.rad2deg(ang_rad) +def md2r(ang_mdeg): return 1e-3 * np.deg2rad(ang_mdeg) + + +class TestEdgeMethodCostFunc: + # List of input parameters: + # max absolute pointing error (mdeg) + max_abs_err_mdeg = 5.0 + + # Absolute function value tolerance and max itereration + # in root of cost function + abs_tol = 1e-4 + max_iter = 20 + + # look angle (off-nadir angle) inputs + min_lka_edge_deg = 32.8 + max_lka_edge_deg = 34.0 + prec_lka_edge_deg = 1e-3 + + # gain offset in (dB) between relative EL power patterns extracted from + # antenna and echo. the roll offset estimation is insensitive to this gain + # offset! + gain_ofs = 0.5 + + # desired roll angle offset in (mdeg) , ground truth values used for V&V + roll_ofs_ant_mdeg = [-198.0, -42.5, 0.0, 67.0, 157.3] + + # Build a 6-order polyminals of a relative antenna gain from gain (dB) + # versus look angles (rad) to be used as a reference for building both + # antenna and echo data These points are extracted from a realitic + # EL power pattern of ALOS1 beam 7. + gain = [-2.2, -1.2, -0.55, -0.2, 0.0, -0.2, -0.5, -1.0, -2.0] + lka_deg = [32.0, 32.5, 33.0, 33.5, 34.1, 34.5, 35., 35.5, 36.] + + # Calculated parameters from input parameters: + num_lka_edge = int((max_lka_edge_deg - min_lka_edge_deg) / + prec_lka_edge_deg) + 1 + + # form gain and look angle of reference antenna pattern, perform + # polyfiting to build Poly1d object version of reference antenna pattern + pf_ref = Poly1d(np.polyfit(np.deg2rad(lka_deg), gain, 6)[::-1]) + + # uniformly-spaced look angles around rising edge used for both antenna + # and echo objects + lka_edge_rad = np.linspace(np.deg2rad(min_lka_edge_deg), + np.deg2rad(max_lka_edge_deg), + num_lka_edge) + + # form ANT 3rd-order poly object with roll offset applied to edge + # look angles + pf_ant_vec = [] + for roll in roll_ofs_ant_mdeg: + lka_ant_rad = lka_edge_rad + md2r(roll) + gain_ant = np.polyval(pf_ref[::-1], lka_ant_rad) + gain_ofs + pf_ant_vec.append(Poly1d(np.polyfit(lka_edge_rad, gain_ant, 3)[::-1])) + + # form echo 3rd-order poly object with roll offset applied to edge look + # angles + gain_echo = np.polyval(pf_ref[::-1], lka_edge_rad) + pf_echo = Poly1d(np.polyfit(lka_edge_rad, gain_echo, 3)[::-1]) + + # here we use constant but non-normalized weights (order 0) + pf_wgt = Poly1d([10], 0.0, 1.0) + + def _validate_estimation(self, est: tuple, roll_true_mdeg: float, + err_msg: str = ""): + roll_est, f_val, flag, n_iter = est + # abs error in (mdeg) + abs_err = np.abs(r2md(roll_est) + roll_true_mdeg) + # error msg + err_msg1 = f'@true roll offset {roll_true_mdeg:.6f} (mdeg) {err_msg}' + # validate + npt.assert_(n_iter <= self.max_iter, + msg="Exceed max number of iteration " + err_msg1) + npt.assert_(flag, msg="Wrong convergence flag " + err_msg1) + npt.assert_allclose(abs_err, 0.0, atol=self.max_abs_err_mdeg, + err_msg=("Too large residual roll offset " + + err_msg1)) + npt.assert_allclose(f_val, 0.0, atol=self.abs_tol, + err_msg="Wrong cost function value " + err_msg1) + + def test_roll_angle_offset_from_edge_scalars(self): + # loop over roll offset (and antenna polyfit objects) + for pf_ant, roll_true_mdeg in zip(self.pf_ant_vec, + self.roll_ofs_ant_mdeg): + + # estimate roll angle offset w/o weighting + est_tuple = ant.roll_angle_offset_from_edge( + self.pf_echo, pf_ant, + np.deg2rad(self.min_lka_edge_deg), + np.deg2rad(self.max_lka_edge_deg), + np.deg2rad(self.prec_lka_edge_deg)) + # validate + self._validate_estimation(est_tuple, roll_true_mdeg, + err_msg="w/o weighting") + + # estimate roll angle offset w/ weighting + est_wgt_tuple = ant.roll_angle_offset_from_edge( + self.pf_echo, pf_ant, + np.deg2rad(self.min_lka_edge_deg), + np.deg2rad(self.max_lka_edge_deg), + np.deg2rad(self.prec_lka_edge_deg), + self.pf_wgt) + + # validate + self._validate_estimation(est_wgt_tuple, roll_true_mdeg, + err_msg="w/ weighting") + + def test_roll_angle_offset_from_edge_linspace(self): + lka_lsp = Linspace(np.deg2rad(self.min_lka_edge_deg), + np.deg2rad(self.prec_lka_edge_deg), + self.num_lka_edge) + + for pf_ant, roll_true_mdeg in zip(self.pf_ant_vec, + self.roll_ofs_ant_mdeg): + # estimate roll angle offset w/o weighting + est_tuple = ant.roll_angle_offset_from_edge( + self.pf_echo, pf_ant, lka_lsp) + # validate + self._validate_estimation(est_tuple, roll_true_mdeg, + err_msg="w/o weighting") + + # estimate roll angle offset w/ weighting + est_wgt_tuple = ant.roll_angle_offset_from_edge( + self.pf_echo, pf_ant, lka_lsp, self.pf_wgt) + # validate + self._validate_estimation(est_wgt_tuple, roll_true_mdeg, + err_msg="w/ weighting") diff --git a/tests/python/packages/CMakeLists.txt b/tests/python/packages/CMakeLists.txt index 6b4165afe..e6fdd61d8 100644 --- a/tests/python/packages/CMakeLists.txt +++ b/tests/python/packages/CMakeLists.txt @@ -11,6 +11,7 @@ nisar/workflows/geocode_insar.py nisar/workflows/gslc.py nisar/workflows/gpu_check.py nisar/workflows/insar.py +nisar/workflows/filter_interferogram.py nisar/workflows/process_args.py nisar/workflows/point_target_info.py nisar/workflows/rdr2geo.py @@ -40,12 +41,16 @@ foreach(TESTFILE ${TESTFILES}) set_property(TEST ${TESTNAME} PROPERTY WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${DIR}) endforeach() + set_tests_properties(test.python.pkg.nisar.workflows.geo2rdr PROPERTIES DEPENDS test.python.pkg.nisar.workflows.rdr2geo) set_tests_properties(test.python.pkg.nisar.workflows.resample_slc PROPERTIES DEPENDS test.python.pkg.nisar.workflows.geo2rdr) set_tests_properties(test.python.pkg.nisar.workflows.crossmul PROPERTIES DEPENDS test.python.pkg.nisar.workflows.resample_slc) +set_tests_properties(test.python.pkg.nisar.workflows.filter_interferogram PROPERTIES + DEPENDS test.python.pkg.nisar.workflows.crossmul) + # using rdr2geo outputs as RUNW rasters to confirm geocode run # using RUNW HDF5 needed as a verifiable dummy RUNW input set_tests_properties(test.python.pkg.nisar.workflows.geocode_insar PROPERTIES diff --git a/tests/python/packages/nisar/workflows/filter_interferogram.py b/tests/python/packages/nisar/workflows/filter_interferogram.py new file mode 100644 index 000000000..52e979fa4 --- /dev/null +++ b/tests/python/packages/nisar/workflows/filter_interferogram.py @@ -0,0 +1,187 @@ +import argparse +import os +from osgeo import gdal + +import h5py +import iscetest +import numpy as np +from nisar.workflows import filter_interferogram, h5_prep +from nisar.workflows.filter_data import filter_data +from nisar.workflows.filter_interferogram_runconfig import \ + FilterInterferogramRunConfig +from scipy.signal import convolve2d + + +def test_filter_interferogram_run(): + ''' + Check if insar_filtering runs without crashing + ''' + + # Load yaml file + test_yaml = os.path.join(iscetest.data, 'insar_test.yaml') + with open(test_yaml) as fh_test_yaml: + test_yaml = fh_test_yaml.read().replace('@ISCETEST@', iscetest.data). \ + replace('@TEST_OUTPUT@', 'RIFG.h5'). \ + replace('@TEST_PRODUCT_TYPES@', 'RIFG'). \ + replace('interferogram_path:', 'interferogram_path: RIFG.h5') + + # Create CLI input namespace with yaml text instead of filepath + args = argparse.Namespace(run_config_path=test_yaml, log_file=False) + + # Initialize runconfig object + runconfig = FilterInterferogramRunConfig(args) + runconfig.geocode_common_arg_load() + + out_paths = h5_prep.run(runconfig.cfg) + + # Modify the interferogram data to have something meaningful + product_path = '/science/LSAR/RIFG/swaths/frequencyA/interferogram/HH' + with h5py.File(out_paths['RIFG'], "a", libver='latest', + swmr=True) as h_rifg: + ds = h_rifg[os.path.join(product_path, 'wrappedInterferogram')] + nrows, ncols = ds.shape + + # Generate a ramp and add some noise + x, y = np.meshgrid(np.arange(ncols), np.arange(nrows)) + igram = np.exp(1j * (x + y)) + h_rifg[os.path.join(product_path, 'wrappedInterferogram')][:, :] = igram + h_rifg.close() + + # Set interferogram_path for stand-alone usage of insar_filtering + filter_interferogram.run(runconfig.cfg, out_paths['RIFG']) + + +def test_filter_interferogram_validate(): + ''' + Validate the filtered interferogram generated by insar_filtering + ''' + + scratch_path = '.' + product_path = '/science/LSAR/RIFG/swaths/frequencyA/interferogram/HH' + + with h5py.File(os.path.join(scratch_path, 'RIFG.h5')) as h_rifg: + # Check that the filtered interferogram still have 0 phase + igram = h_rifg[os.path.join(product_path, 'wrappedInterferogram')][()] + + # Re-create reference interferogram (prior to filtering) + nrows, ncols = igram.shape + x, y = np.meshgrid(np.arange(ncols), np.arange(nrows)) + ref_igram = np.exp(1j * (x + y)) + + # Filter with boxcar 9x9 (see defaults/insar.yaml) + filter_size_range = 9 + filter_size_azimuth = 9 + + kernel = np.ones((filter_size_azimuth, filter_size_azimuth), + dtype=np.float32) / ( + filter_size_azimuth * filter_size_range + ) + + filt_igram = convolve2d(ref_igram, kernel, mode='same') + + difference_abs = np.abs(igram - filt_igram) + difference_phase = np.angle(igram * np.conj(filt_igram)) + + assert difference_abs.max() < 1e-5 + assert difference_phase.max() < 1e-5 + + +def test_filter_data_gdal(): + ''' + Test stand-alone usage of filter_data.py on warped_envisat.slc.vrt + ''' + + # Path to data to filter + input_path = os.path.join(iscetest.data, 'warped_winnipeg.slc.vrt') + + # kernel size along length and width + kernel_length = 9 + kernel_width = 9 + + # Extract HH SLCs and save it in array + ds = gdal.Open(input_path, gdal.GA_ReadOnly) + input_data = ds.GetRasterBand(1).ReadAsArray() + ds = None + + # Filter using scipy.signal.convolve2d. Create 2D kernel + kernel2d = np.ones((kernel_length, kernel_width), + dtype=np.float64) / (kernel_width * kernel_length) + output_scipy = convolve2d(input_data, kernel2d, mode='same') + + # filter data with filter_data.py. Create 1d kernels + kernel_row = np.ones([kernel_length, 1], dtype=np.float64) / kernel_length + kernel_col = kernel_row.transpose() + lines_per_block = 40 + + # Filter + output_path = 'filtered_output.tiff' + + # Translate input file to Tiff using gdal + ds = gdal.Open(input_path) + gdal.Translate('warped_winnipeg_slc.tiff', ds, format='GTiff') + ds = None + + filter_data('warped_winnipeg_slc.tiff', lines_per_block, kernel_row, kernel_col, output_path) + ds = gdal.Open(output_path, gdal.GA_ReadOnly) + output_isce3 = ds.GetRasterBand(1).ReadAsArray() + ds = None + + # Perform assertion on amplitude and phase difference + diff_amp = np.abs(output_scipy) - np.abs(output_isce3) + diff_pha = np.angle(output_scipy * np.conj(output_isce3)) + + assert diff_amp.max() < 1e-5 + assert diff_pha.max() < 1e-5 + + +def test_filter_data_h5(): + ''' + Test stand-alone usage of filter_data.py on winnipeg.h5 + ''' + + # Path to data to filter + data_path = os.path.join(iscetest.data, 'winnipeg.h5') + + # kernel size along length and width + kernel_length = 9 + kernel_width = 9 + + # Extract HH SLCs and save it in array + ds_path = 'science/LSAR/SLC/swaths/frequencyA/HH' + with h5py.File(data_path, 'r') as h: + input_data = h[ds_path][()] + + # Filter using scipy.signal.convolve2d. Create 2D kernel + kernel2d = np.ones((kernel_length, kernel_width), + dtype=np.float64) / (kernel_width * kernel_length) + output_scipy = convolve2d(input_data, kernel2d, mode='same') + + # filter data with filter_data.py. Create 1d kernels + kernel_row = np.ones([kernel_length, 1], dtype=np.float64) / kernel_length + kernel_col = kernel_row.transpose() + lines_per_block = 40 + + # Filter + with h5py.File(data_path, 'r') as h_in, h5py.File('filtered_slc.h5', + 'a') as h_out: + ds_in = h_in[ds_path] + if 'filtered_slc' not in h_out.keys(): + ds_out = h_out.create_dataset('filtered_slc', data=ds_in[()]) + else: + ds_out = h_out['filtered_slc'] + filter_data(ds_in, lines_per_block, kernel_row, kernel_col, ds_out) + output_isce3 = h_out['filtered_slc'][()] + + # Perform assertion on amplitude and phase difference + diff_amp = np.abs(output_scipy) - np.abs(output_isce3) + diff_pha = np.angle(output_scipy * np.conj(output_isce3)) + + assert diff_amp.max() < 1e-5 + assert diff_pha.max() < 1e-5 + + +if __name__ == '__main__': + test_filter_interferogram_run() + test_filter_interferogram_validate() + test_filter_data_gdal() + test_filter_data_h5()