diff --git a/README.md b/README.md index 4349c75..c18b1b0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ With an L1b SUVI netCDF file `suvi_nc`, view the exposure time in seconds: ```python from heregoes import load suvi_data = load(suvi_nc) -print(suvi_data["CMD_EXP"][:]) +print(suvi_data["CMD_EXP"][...]) ``` Which returns: diff --git a/heregoes/ancillary.py b/heregoes/ancillary.py index fb93872..142fad0 100755 --- a/heregoes/ancillary.py +++ b/heregoes/ancillary.py @@ -109,7 +109,7 @@ class IREMIS(AncillaryDataset): """ def __init__(self, abi_data, iremis_dir=IREMIS_DIR): - super(IREMIS, self).__init__() + super().__init__() self.abi_data = abi_data month = self.abi_data.time_coverage_start.month @@ -152,15 +152,15 @@ def __init__(self, abi_data, iremis_dir=IREMIS_DIR): self.data["c07_land_emissivity"] = linear_interp( 3.7, 4.3, - iremis["emis1"][:], - iremis["emis2"][:], + iremis["emis1"][...], + iremis["emis2"][...], 3.9, ).astype(np.float32) self.data["c14_land_emissivity"] = linear_interp( 10.8, 12.1, - iremis["emis8"][:], - iremis["emis9"][:], + iremis["emis8"][...], + iremis["emis9"][...], 11.2, ).astype(np.float32) @@ -210,7 +210,7 @@ class WaterMask(AncillaryDataset): """ def __init__(self, abi_data, gshhs_scale="intermediate", rivers=False): - super(WaterMask, self).__init__() + super().__init__() self.abi_data = abi_data self.dataset_name = "gshhs_" + gshhs_scale diff --git a/heregoes/goesr/__init__.py b/heregoes/goesr/__init__.py index 72d5ad3..4553f6c 100644 --- a/heregoes/goesr/__init__.py +++ b/heregoes/goesr/__init__.py @@ -94,12 +94,12 @@ def __init__(self, *args, **kwargs): self.resolution_ifov = 56.0e-6 self.resolution_km = 2.0 - self.band_id_safe = "C" + str(self["band_id"][:].item()).zfill(2) + self.band_id_safe = "C" + str(self["band_id"][...].item()).zfill(2) - self.midpoint_time = self.epoch2timestamp(seconds=float(self["t"][:].item())) + self.midpoint_time = self.epoch2timestamp(seconds=float(self["t"][...].item())) self.instrument_coefficients = coefficients.ABICoeff( - self.platform_ID, self["band_id"][:].item() + self.platform_ID, self["band_id"][...].item() ) @@ -111,11 +111,11 @@ def __init__(self, *args, **kwargs): # Wavelength for SUVI 304 is masked in netCDF self["WAVELNTH"].set_fill_value(0) - self.wavelength_safe = str(int(self["WAVELNTH"][:].item())).zfill(3) + self.wavelength_safe = str(int(self["WAVELNTH"][...].item())).zfill(3) if self.wavelength_safe == "000": self.wavelength_safe = "304" - self["WAVELNTH"][:] = 304 + self["WAVELNTH"][...] = 304 self.instrument_coefficients = coefficients.SUVICoeff( - self.platform_ID, self["WAVELNTH"][:].item() + self.platform_ID, self["WAVELNTH"][...].item() ) diff --git a/heregoes/image.py b/heregoes/image.py index 290a2df..c53e786 100755 --- a/heregoes/image.py +++ b/heregoes/image.py @@ -85,6 +85,7 @@ def __init__( abi_nc, index=slice(None, None), gamma=1.0, + black_space=False, ): """ Creates Cloud Moisture Imagery (CMI) following the CMIP ATBD: https://www.star.nesdis.noaa.gov/goesr/docs/ATBD/Imagery.pdf @@ -102,16 +103,21 @@ def __init__( - `abi_nc`: String or Path object pointing to a GOES-R ABI L1b Radiance netCDF file - `index`: Optionally process an ABI image for a single array index or slice - `gamma`: Optional gamma correction for reflective ABI brightness value. Defaults to no correction + - `black_space`: Optionally overwrites the masked pixels in the final ABI image (nominally the "space" background) to be black. Defaults to no overwriting, or white pixels for reflective imagery and black pixels for emissive imagery. Default `True` """ - super(ABIImage, self).__init__() + super().__init__() self.gamma = gamma + self.black_space = black_space + self._cmi = None self.abi_data = load(abi_nc) + self.rad = self.abi_data["Rad"][index] self.dqf = self.abi_data["DQF"][index] - self.quality = self.abi_data["Rad"].quality + self.mask = self.abi_data["Rad"].mask + self.quality = self.abi_data["Rad"].pct_unmasked self.rad_range = np.array( self.abi_data["Rad"].valid_range * self.abi_data["Rad"].scale_factor @@ -132,20 +138,20 @@ def __init__( @property def cmi(self): if self._cmi is None: - if 1 <= self.abi_data["band_id"][:] <= 6: + if 1 <= self.abi_data["band_id"][...] <= 6: self._cmi = abi.rad2rf( self.rad, - self.abi_data["earth_sun_distance_anomaly_in_AU"][:].item(), - self.abi_data["esun"][:].item(), + self.abi_data["earth_sun_distance_anomaly_in_AU"][...].item(), + self.abi_data["esun"][...].item(), ) - elif 7 <= self.abi_data["band_id"][:] <= 16: + elif 7 <= self.abi_data["band_id"][...] <= 16: self._cmi = abi.rad2bt( self.rad, - self.abi_data["planck_fk1"][:].item(), - self.abi_data["planck_fk2"][:].item(), - self.abi_data["planck_bc1"][:].item(), - self.abi_data["planck_bc2"][:].item(), + self.abi_data["planck_fk1"][...].item(), + self.abi_data["planck_fk2"][...].item(), + self.abi_data["planck_bc1"][...].item(), + self.abi_data["planck_bc2"][...].item(), ) return self._cmi @@ -157,20 +163,23 @@ def cmi(self, value): @property def bv(self): if self._bv is None: - if 1 <= self.abi_data["band_id"][:] <= 6: + if 1 <= self.abi_data["band_id"][...] <= 6: # calculate the range of possible reflectance factors from the provided valid range of radiance, and use it to normalize before the gamma correction self.rf_min, self.rf_max = ( self.rad_range * np.pi - * np.square(self.abi_data["earth_sun_distance_anomaly_in_AU"][:]) - ) / self.abi_data["esun"][:] + * np.square(self.abi_data["earth_sun_distance_anomaly_in_AU"][...]) + ) / self.abi_data["esun"][...] self._bv = abi.rf2bv( self.cmi, min=self.rf_min, max=self.rf_max, gamma=self.gamma ) - elif 7 <= self.abi_data["band_id"][:] <= 16: + elif 7 <= self.abi_data["band_id"][...] <= 16: self._bv = abi.bt2bv(self.cmi) + if self.black_space: + self._bv[self.mask] = 0 + return self._bv @bv.setter @@ -190,6 +199,7 @@ def __init__( upscale=False, upscale_algo=cv2.INTER_CUBIC, gamma=1.0, + black_space=False, ): """ Creates the "natural" color RGB for ABI following https://doi.org/10.1029/2018EA000379 in BGR order @@ -200,13 +210,14 @@ def __init__( - `upscale`: Whether to scale up green and blue images (1 km) to match the red image (500 m) (`True`) or vice versa (`False`, Default) - `upscale_algo`: The OpenCV interpolation algorithm used for upscaling green and blue images. See https://docs.opencv.org/4.6.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121. Default `cv2.INTER_CUBIC` - `gamma`: Optional gamma correction for reflective ABI brightness value. Defaults to no correction + - `black_space`: Optionally overwrites the masked pixels in the final ABI image (nominally the "space" background) to be black. Defaults to no overwriting, or white pixels for reflective imagery and black pixels for emissive imagery. Default `True` """ - super(ABINaturalRGB, self).__init__() + super().__init__() - red_image = ABIImage(red_nc, gamma=gamma) - green_image = ABIImage(green_nc, gamma=gamma) - blue_image = ABIImage(blue_nc, gamma=gamma) + red_image = ABIImage(red_nc, gamma=gamma, black_space=black_space) + green_image = ABIImage(green_nc, gamma=gamma, black_space=black_space) + blue_image = ABIImage(blue_nc, gamma=gamma, black_space=black_space) if upscale: # upscale green and blue to the size of red @@ -216,20 +227,37 @@ def __init__( interpolation=upscale_algo, ) green_image.dqf = cv2.resize( - green_image.bv, + green_image.dqf, (red_image.dqf.shape[1], red_image.dqf.shape[0]), interpolation=cv2.INTER_NEAREST, ) + green_image.mask = ( + cv2.resize( + green_image.mask.astype(np.uint8), + (red_image.mask.shape[1], red_image.mask.shape[0]), + interpolation=cv2.INTER_NEAREST, + ) + == 1 + ) + blue_image.bv = cv2.resize( blue_image.bv, (red_image.bv.shape[1], red_image.bv.shape[0]), interpolation=upscale_algo, ) blue_image.dqf = cv2.resize( - blue_image.bv, + blue_image.dqf, (red_image.dqf.shape[1], red_image.dqf.shape[0]), interpolation=cv2.INTER_NEAREST, ) + blue_image.mask = ( + cv2.resize( + blue_image.mask.astype(np.uint8), + (red_image.mask.shape[1], red_image.mask.shape[0]), + interpolation=cv2.INTER_NEAREST, + ) + == 1 + ) else: # downscale red to the size of green and blue @@ -239,10 +267,18 @@ def __init__( interpolation=cv2.INTER_AREA, ) red_image.dqf = cv2.resize( - red_image.bv, + red_image.dqf, (green_image.dqf.shape[1], green_image.dqf.shape[0]), interpolation=cv2.INTER_NEAREST, ) + red_image.mask = ( + cv2.resize( + red_image.mask.astype(np.uint8), + (green_image.mask.shape[1], green_image.mask.shape[0]), + interpolation=cv2.INTER_NEAREST, + ) + == 1 + ) green_image.bv = ( (red_image.bv * r_coeff) @@ -257,6 +293,9 @@ def __init__( sum([red_image.quality, green_image.quality, blue_image.quality]) / 3 ) self.dqf = np.stack([blue_image.dqf, green_image.dqf, red_image.dqf], axis=2) + self.mask = np.stack( + [blue_image.mask, green_image.mask, red_image.mask], axis=2 + ) if upscale: self.abi_data = red_image.abi_data @@ -264,7 +303,7 @@ def __init__( else: self.abi_data = green_image.abi_data - self.abi_data["band_id"][:] = np.atleast_1d(0) + self.abi_data["band_id"][...] = np.atleast_1d(0) self.abi_data.band_id_safe = "Color" self.abi_data.dataset_name = "RGB from " + ", ".join( @@ -303,20 +342,21 @@ def __init__( - `dqf_correction`: Whether to interpolate over bad pixels marked by DQF. Default `True` """ - super(SUVIImage, self).__init__() + super().__init__() self.suvi_data = load(suvi_nc) - if self.suvi_data["CMD_EXP"][:] != 1.0: + if self.suvi_data["CMD_EXP"][...] != 1.0: logger.warning( - "Short SUVI exposure detected: SUVI exposures shorter than 1 second are not officially supported." + "Short SUVI exposure detected: SUVI exposures shorter than 1 second are not officially supported.", + extra={"caller": f"{__name__}.{self.__class__.__name__}"}, ) - self.rad = self.suvi_data["RAD"][:] - self.dqf = self.suvi_data["DQF"][:] - self.quality = self.suvi_data["RAD"].quality - x_offset = 640 - self.suvi_data["CRPIX1"][:] - y_offset = 640 - self.suvi_data["CRPIX2"][:] + self.rad = self.suvi_data["RAD"][...] + self.dqf = self.suvi_data["DQF"][...] + self.quality = self.suvi_data["RAD"].pct_unmasked + x_offset = 640 - self.suvi_data["CRPIX1"][...] + y_offset = 640 - self.suvi_data["CRPIX2"][...] self.default_filename = "_".join( ( diff --git a/heregoes/navigation.py b/heregoes/navigation.py index 9b41826..9d48111 100755 --- a/heregoes/navigation.py +++ b/heregoes/navigation.py @@ -76,8 +76,8 @@ def __init__( if self.index == slice(None, None): self.x_rad, self.y_rad = np.meshgrid( - self.abi_data["x"][:], - self.abi_data["y"][:], + self.abi_data["x"][...], + self.abi_data["y"][...], ) else: @@ -218,9 +218,9 @@ def area_m(self, value): def _calc_sat(self): self._sat_az, self._sat_za = orbital.get_observer_look( - sat_lon=np.atleast_1d(self.abi_data["nominal_satellite_subpoint_lon"][:]), - sat_lat=np.atleast_1d(self.abi_data["nominal_satellite_subpoint_lat"][:]), - sat_alt=np.atleast_1d(self.abi_data["nominal_satellite_height"][:]), + sat_lon=np.atleast_1d(self.abi_data["nominal_satellite_subpoint_lon"][...]), + sat_lat=np.atleast_1d(self.abi_data["nominal_satellite_subpoint_lat"][...]), + sat_alt=np.atleast_1d(self.abi_data["nominal_satellite_height"][...]), jdays2000=orbital.jdays2000(self.time), lon=self.lon_deg, lat=self.lat_deg, diff --git a/heregoes/projection.py b/heregoes/projection.py index 8ee21dc..9f9e474 100755 --- a/heregoes/projection.py +++ b/heregoes/projection.py @@ -56,10 +56,10 @@ def __init__(self, abi_data): lon_0 = self.abi_data["goes_imager_projection"].longitude_of_projection_origin sweep = self.abi_data["goes_imager_projection"].sweep_angle_axis - ul_x = self.abi_data["x_image_bounds"][0] * h - ul_y = self.abi_data["y_image_bounds"][0] * h - lr_x = self.abi_data["x_image_bounds"][1] * h - lr_y = self.abi_data["y_image_bounds"][1] * h + ul_x = (self.abi_data["x_image_bounds"][0] * h).item() + ul_y = (self.abi_data["y_image_bounds"][0] * h).item() + lr_x = (self.abi_data["x_image_bounds"][1] * h).item() + lr_y = (self.abi_data["y_image_bounds"][1] * h).item() self.abi_bounds = [ul_x, ul_y, lr_x, lr_y] self._intermediate_format = "GTiff" diff --git a/heregoes/util/ncinterface.py b/heregoes/util/ncinterface.py index bf9060b..d1854b9 100644 --- a/heregoes/util/ncinterface.py +++ b/heregoes/util/ncinterface.py @@ -1,197 +1,240 @@ -# Copyright (c) 2023. - -# Author(s): - -# Harry Dove-Robinson -# for Here GOES Radiotelescope (Harry Dove-Robinson & Heidi Neilson) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Partial implementation of a flat netCDF4 Python class interface with lazy-loading netCDF variables""" - -from pathlib import Path - -import netCDF4 -import numpy as np - -from heregoes import exceptions - - -class _NCBase: - # empty class just for interface structure - pass - - -class NCInterface(_NCBase): - def __init__(self, nc_file, lazy_load_size_threshold=500**2): - self._loaded_nc = None - - try: - self._nc_file = Path(nc_file) - - if not ( - self._nc_file.exists() - and (self._nc_file.is_file() or self._nc_file.is_symlink()) - ): - raise exceptions.HereGOESIOError - - except Exception as e: - raise exceptions.HereGOESIOReadException( - caller=f"{__name__}.{self.__class__.__name__}", - filepath=self._nc_file, - exception=e, - ) - - self._loaded_nc = netCDF4.Dataset(self._nc_file, "r") - - # set attributes - self._copy_attrs( - src_obj=self._loaded_nc, dst_obj=self, attr_list=self._loaded_nc.ncattrs() - ) - - # set dimensions - self.dimensions = {} - for dimension_name, dimension_obj in self._loaded_nc.dimensions.items(): - self.dimensions[dimension_name] = _NCDim(dimension_name) - setattr(self.dimensions[dimension_name], "size", dimension_obj.size) - - # set variables - self.variables = _NCBase() - - for var_name in self._loaded_nc.variables.keys(): - var_obj = self._loaded_nc.variables[var_name] - - # set this variable to be lazy-loading only if it's ge lazy_load_size_threshold - if var_obj.size >= lazy_load_size_threshold: - lazy_load = True - - else: - lazy_load = False - - setattr( - self.variables, var_name, _NCVar(self._loaded_nc, var_name, lazy_load) - ) - - var_attrs = var_obj.ncattrs() - var_attrs.extend(["ndim", "shape", "size", "dtype"]) - self._copy_attrs( - src_obj=var_obj, - dst_obj=getattr(self.variables, var_name), - attr_list=var_attrs, - ) - - # add dimension reference to the variable - var_dims = [] - for var_dim in var_obj.dimensions: - var_dims.append(self.dimensions[var_dim]) - setattr(getattr(self.variables, var_name), "dimensions", tuple(var_dims)) - - def _copy_attrs(self, src_obj, dst_obj, attr_list): - for attr_name in attr_list: - if not (attr_name.startswith("_")): - attr_value = getattr(src_obj, attr_name) - if not (callable(attr_value)): - setattr(dst_obj, attr_name, attr_value) - - def __getitem__(self, key): - return getattr(self.variables, key) - - def __del__(self): - if self._loaded_nc is not None: - self._loaded_nc.close() - - -class _NCVar(_NCBase): - def __init__(self, nc_obj, var_name, lazy_load): - self._loaded_nc = nc_obj - - self.var_name = var_name - - self.__value = None - self._override_fill_value = None - - if not lazy_load: - self._loadvar() - - def _loadvar(self): - self.__value = self._loaded_nc.variables[self.var_name][...] - - self.quality = self.__value.count() / self.__value.size - - self.__value = np.atleast_1d(self.__value) - - if self._override_fill_value is not None: - self.__value.set_fill_value(self._override_fill_value) - - self.__value = self.__value.filled() - - @property - def _value(self): - if self.__value is None: - self._loadvar() - - return self.__value - - @_value.setter - def _value(self, val): - self.__value = val - - def set_fill_value(self, val): - self._override_fill_value = val - self._loadvar() - - def __array__(self): - return self.__getitem__(...) - - def __getitem__(self, key): - return self._value[key] - - def __setitem__(self, key, val): - self._value[key] = val - - def __repr__(self): - return self.__str__() - - def __str__(self): - retval = [] - - retval.append(repr(type(self))) - - ncdims = [] - for ncdim in self.dimensions: - ncdims.append(ncdim.name) - - retval.append( - str(self.dtype) + f" {self.var_name}(" + ", ".join(tuple(ncdims)) + ")" - ) - - for attr in self.__dict__.keys(): - if not attr.startswith("_"): - retval.append(f" {attr}: " + str(getattr(self, attr))) - - return "\n".join(retval) - - -class _NCDim(_NCBase): - def __init__(self, name): - self.name = name - - def __repr__(self): - return self.__str__() - - def __str__(self): - return "%r: name = '%s', size = %s" % (type(self), self.name, self.size) - - def __getitem__(self, key): - return getattr(self, key) +# Copyright (c) 2023. + +# Author(s): + +# Harry Dove-Robinson +# for Here GOES Radiotelescope (Harry Dove-Robinson & Heidi Neilson) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Partial implementation of a flat netCDF4 Python class interface with lazy-loading netCDF variables""" + +import logging +from pathlib import Path + +import netCDF4 +import numpy as np + +from heregoes import exceptions + +logger = logging.getLogger("heregoes-logger") + + +class _NCBase: + def __init__(self): + pass + + def _copy_attrs(self, src_obj, dst_obj, attr_list): + for attr_name in attr_list: + attr_value = getattr(src_obj, attr_name) + if not callable(attr_value): + setattr(dst_obj, attr_name, attr_value) + + +class _NCVarBase: + def __init__(self): + pass + + def __getitem__(self, key): + return getattr(self, key) + + +class NCInterface(_NCBase): + def __init__(self, nc_file, lazy_load_size_threshold=500**2): + self._loaded_nc = None + + try: + self._nc_file = Path(nc_file) + + if not ( + self._nc_file.exists() + and (self._nc_file.is_file() or self._nc_file.is_symlink()) + ): + raise exceptions.HereGOESIOError + + except Exception as e: + raise exceptions.HereGOESIOReadException( + caller=f"{__name__}.{self.__class__.__name__}", + filepath=self._nc_file, + exception=e, + ) + + self._loaded_nc = netCDF4.Dataset(self._nc_file, "r") + + # set attributes + self._copy_attrs( + src_obj=self._loaded_nc, dst_obj=self, attr_list=self._loaded_nc.ncattrs() + ) + + # set dimensions + self.dimensions = {} + for dimension_name, dimension_obj in self._loaded_nc.dimensions.items(): + self.dimensions[dimension_name] = _NCDim(dimension_name) + setattr(self.dimensions[dimension_name], "size", dimension_obj.size) + + # set variables + self.variables = _NCVarBase() + + for var_name in self._loaded_nc.variables.keys(): + var_obj = self._loaded_nc.variables[var_name] + + # set this variable to be lazy-loading only if it's ge lazy_load_size_threshold + if var_obj.size >= lazy_load_size_threshold: + lazy_load = True + + else: + lazy_load = False + + setattr( + self.variables, var_name, _NCVar(self._loaded_nc, var_name, lazy_load) + ) + + var_attrs = var_obj.ncattrs() + var_attrs.extend(getattr(getattr(self.variables, var_name), "__npattrs__")) + self._copy_attrs( + src_obj=var_obj, + dst_obj=getattr(self.variables, var_name), + attr_list=var_attrs, + ) + setattr(getattr(self.variables, var_name), "__ncattrs__", var_attrs) + + # add dimension reference to the variable + var_dims = [] + for var_dim in var_obj.dimensions: + var_dims.append(self.dimensions[var_dim]) + setattr(getattr(self.variables, var_name), "dimensions", tuple(var_dims)) + + def __getitem__(self, key): + return getattr(self.variables, key) + + def __del__(self): + if self._loaded_nc is not None: + self._loaded_nc.close() + + +class _NCVar(_NCBase): + def __init__(self, nc_obj, var_name, lazy_load): + self._loaded_nc = nc_obj + + self.name = var_name + + self.__ncattrs__ = [None] + self.__npattrs__ = ["dtype", "ndim", "shape", "size"] + + self.__value__ = None + self._mask = None + self._is_loaded = False + self._slc = np.s_[...] + self._override_fill_value = None + + if not lazy_load: + self._loadvar() + + def _loadvar(self): + logger.debug( + "Loading variable %s with slice %s", + self.name, + str(self._slc), + extra={"caller": f"{__name__}.{self.__class__.__name__}"}, + ) + + self.__value__ = self._loaded_nc.variables[self.name][self._slc] + is_masked = np.ma.isMaskedArray(self.__value__) + + # broadcast the mask in case it is 1D or a scalar + if is_masked: + self._mask = np.broadcast_to(self.__value__.mask, self.__value__.shape) + self.pct_unmasked = self.__value__.count() / self.__value__.size + else: + self.pct_unmasked = 1.0 + + self.__value__ = np.atleast_1d(self.__value__) + + if is_masked: + if self._override_fill_value is not None: + self.__value__.set_fill_value(self._override_fill_value) + + self.__value__ = self.__value__.filled() + + # gather updated attrs of the numpy array + super()._copy_attrs(self.__value__, self, self.__npattrs__) + + self._is_loaded = True + + @property + def mask(self): + if not self._is_loaded: + self._loadvar() + + return self._mask + + def set_fill_value(self, val): + if val != self._FillValue: + self._override_fill_value = val + self._FillValue = val + + # reload with the new fill value + if self._is_loaded: + self._loadvar() + + def __getitem__(self, slc): + slc = np.s_[slc] + if not self._is_loaded or slc != self._slc: + self._slc = slc + self._loadvar() + + return self.__value__ + + def __setitem__(self, slc, val): + slc = np.s_[slc] + if not self._is_loaded or slc != self._slc: + self._slc = slc + self._loadvar() + + self.__value__[...] = val + + def __repr__(self): + return self.__str__() + + def __str__(self): + retval = [] + + retval.append(repr(type(self))) + + ncdims = [] + for ncdim in self.dimensions: + ncdims.append(ncdim.name) + + retval.append( + str(self.dtype) + f" {self.name}(" + ", ".join(tuple(ncdims)) + ")" + ) + + for attr in sorted(self.__ncattrs__): + retval.append(f" {attr}: " + str(getattr(self, attr))) + + return "\n".join(retval) + + +class _NCDim(_NCBase): + def __init__(self, name): + self.name = name + + def __repr__(self): + return self.__str__() + + def __str__(self): + return "%r: name = '%s', size = %s" % (type(self), self.name, self.size) + + def __getitem__(self, key): + return getattr(self, key) diff --git a/test/heregoes_test.py b/test/heregoes_test.py index bfc3184..c62cbf7 100755 --- a/test/heregoes_test.py +++ b/test/heregoes_test.py @@ -18,8 +18,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Test cases for checking imagery and navigation outputs""" - from pathlib import Path import cv2 @@ -170,13 +168,16 @@ def test_abi_image(): abi_mc01_nc, upscale=True, gamma=0.75, + black_space=True, ) assert abi_rgb.bv.dtype == np.uint8 abi_rgb.save(file_path=output_dir, file_ext=".jpeg") - abi_rgb = image.ABINaturalRGB(abi_cc02_nc, abi_cc03_nc, abi_cc01_nc, gamma=0.75) + abi_rgb = image.ABINaturalRGB( + abi_cc02_nc, abi_cc03_nc, abi_cc01_nc, gamma=0.75, black_space=True + ) assert abi_rgb.bv.dtype == np.uint8 @@ -319,28 +320,83 @@ def test_ancillary(): ) +def test_ncinterface(): + abi_data = load(abi_cc07_nc) + + # take a small masked slice to test on + array_slice = abi_data.variables.Rad[10:15, 10:15] + assert (abi_data.variables.Rad.mask == True).all() + assert abi_data.variables.Rad.mask.shape == array_slice.shape == (5, 5) + + # test assignment + array_slice[0, 0] = 1.0 + array_slice[1, 1] = 2.0 + + # test compound assignment + array_slice[2:4, 2:4] /= 0.05 + array_slice[:, -1] *= 10 + + test_arr = np.array( + [ + [1.0000e00, 1.6383e04, 1.6383e04, 1.6383e04, 1.6383e05], + [1.6383e04, 2.0000e00, 1.6383e04, 1.6383e04, 1.6383e05], + [1.6383e04, 1.6383e04, 3.2766e05, 3.2766e05, 1.6383e05], + [1.6383e04, 1.6383e04, 3.2766e05, 3.2766e05, 1.6383e05], + [1.6383e04, 1.6383e04, 1.6383e04, 1.6383e04, 1.6383e05], + ], + dtype=np.float32, + ) + + # test contents of slice against known test_arr + assert (array_slice == test_arr).all() + + # change the index in memory and test the new shape + assert abi_data.variables.Rad[...].shape == (1500, 2500) + + # while we're looking at the full array, test how much of it is masked + assert abi_data.variables.Rad.pct_unmasked == 0.9874234666666667 + + # the contents of the same slice should have changed when the inner index changed + assert not (abi_data.variables.Rad[...][10:15, 10:15] == test_arr).all() + assert not (abi_data.variables.Rad[10:15, 10:15] == test_arr).all() + + # test that the fill value can be changed + test_filled_arr = np.array( + [ + [99.0, 99.0, 99.0, 99.0, 99.0], + [99.0, 99.0, 99.0, 99.0, 99.0], + [99.0, 99.0, 99.0, 99.0, 99.0], + [99.0, 99.0, 99.0, 99.0, 99.0], + [99.0, 99.0, 99.0, 99.0, 99.0], + ], + dtype=np.float32, + ) + abi_data.variables.Rad.set_fill_value(99) + assert (abi_data.variables.Rad[10:15, 10:15] == test_filled_arr).all() + + def test_exceptions(): try: abi_data = load("not_a_real_netcdf.nc") except Exception as e: - assert type(e) == exceptions.HereGOESUnsupportedProductException + assert isinstance(e, exceptions.HereGOESUnsupportedProductException) try: abi_data = load("fake_abi-l1b_netcdf.nc") except Exception as e: - assert type(e) == exceptions.HereGOESIOReadException + assert isinstance(e, exceptions.HereGOESIOReadException) try: suvi_data = load("fake_suvi-l1b_netcdf.nc") except Exception as e: - assert type(e) == exceptions.HereGOESIOReadException + assert isinstance(e, exceptions.HereGOESIOReadException) try: abi_image = image.ABIImage(abi_mc07_nc) abi_image.save(file_path=output_dir, file_ext=".not_a_file_extension") except Exception as e: - assert type(e) == exceptions.HereGOESIOWriteException + assert isinstance(e, exceptions.HereGOESIOWriteException)