def eval_dataset(self, spectral_ctx: SpectralContext) -> xr.Dataset: """ Return a dataset that holds the radiative properties of the corresponding atmospheric profile. This method dispatches evaluation to specialised methods depending on the active mode. Parameters ---------- spectral_ctx : :class:`.SpectralContext` A spectral context data structure containing relevant spectral parameters (*e.g.* wavelength in monochromatic mode). Returns ------- Dataset Radiative properties dataset. """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return self.eval_dataset_mono(spectral_ctx.wavelength).squeeze() elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return self.eval_dataset_ckd( spectral_ctx.bindex, bin_set_id=spectral_ctx.bin_set.id).squeeze() else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def eval(self, spectral_ctx: SpectralContext) -> np.ndarray: r""" Evaluate phase function based on a spectral context. This method dispatches evaluation to specialised methods depending on the active mode. Parameters ---------- spectral_ctx : :class:`.SpectralContext` A spectral context data structure containing relevant spectral parameters (*e.g.* wavelength in monochromatic mode, bin and quadrature point index in CKD mode). Returns ------- ndarray Evaluated phase function as a 1D array. Notes ----- The phase function is represented by an array of values mapped to regularly spaced scattering angle cosine values (:math:`\mu \in [-1, 1]`). """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return self.eval_mono(spectral_ctx.wavelength).squeeze() elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return self.eval_ckd(spectral_ctx.bindex).squeeze() else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def test_heterogeneous_single(modes_all_single, components, bin_set, path_to_ussa76_approx_data): """ Unit tests for a HeterogeneousAtmosphere with a single component. """ # Construct succeeds if components == "molecular": if eradiate.mode().has_flags(ModeFlags.ANY_MONO): component = MolecularAtmosphere.ussa1976(absorption_data_sets={ "us76_u86_4": path_to_ussa76_approx_data }, ) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): component = MolecularAtmosphere.afgl_1986() else: pytest.skip(f"unsupported mode '{eradiate.mode().id}'") atmosphere = HeterogeneousAtmosphere(molecular_atmosphere=component) else: component = ParticleLayer() atmosphere = HeterogeneousAtmosphere(particle_layers=[component]) # Produced kernel dict can be loaded ctx = KernelDictContext(spectral_ctx=CKDSpectralContext(bin_set=bin_set)) assert atmosphere.kernel_dict(ctx).load()
def test_add_srf(modes_all_single): if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_cfg = {"wavelengths": [550.0]} elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): spectral_cfg = {"bins": ["550"]} else: pytest.skip(f"Please add test for '{eradiate.mode().id}' mode") exp = OneDimExperiment( atmosphere=None, measures=MultiDistantMeasure.from_viewing_angles( zeniths=[-60, -45, 0, 45, 60], azimuths=0.0, spp=1, spectral_cfg=spectral_cfg, ), ) exp.process() measure = exp.measures[0] # Apply basic post-processing values = Pipeline([ Gather(var="radiance"), AggregateCKDQuad(var="radiance", measure=measure) ]).transform(measure.results) step = AddSpectralResponseFunction(measure=measure) result = step.transform(values) # The spectral response function is added to the dataset as a data variable assert "srf" in result.data_vars # Its only dimension is wavelength assert set(result.srf.dims) == {"srf_w"}
def eval_illumination_spectrum( field_name: str, k_units: pint.Unit ) -> pint.Quantity: # Local helper function to help with illumination spectrum evaluation spectrum: Spectrum = getattr(illumination, field_name) if eradiate.mode().has_flags(ModeFlags.ANY_MONO): # Very important: sort spectral coordinate wavelengths = np.sort(measure.spectral_cfg.wavelengths) assert np.allclose(wavelengths, wavelengths_dataset) return spectrum.eval_mono(wavelengths).m_as(k_units) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): # Collect bins and wavelengths, evaluate spectrum bins = measure.spectral_cfg.bins wavelengths = [bin.wcenter.m_as(ureg.nm) for bin in bins] * ureg.nm # Note: This line assumes that eval_ckd() returns a constant # value over the entire bin result = spectrum.eval_ckd(*(bin.bindexes[0] for bin in bins)).m_as( k_units ) # Reorder data by ascending wavelengths indices = wavelengths.argsort() wavelengths = wavelengths[indices] assert np.allclose(wavelengths, wavelengths_dataset) result = result[indices] return result else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def test_add_illumination(modes_all_single, illumination_type, expected_dims): # Initialise test data if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_cfg = {"wavelengths": [540.0, 550.0, 560.0]} elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): spectral_cfg = {"bins": ["540", "550", "560"]} else: pytest.skip(f"Please add test for '{eradiate.mode().id}' mode") exp = OneDimExperiment( atmosphere=None, illumination={"type": illumination_type}, measures=MultiDistantMeasure(spectral_cfg=spectral_cfg), ) exp.process() measure = exp.measures[0] # Apply basic post-processing values = Pipeline(steps=[ ("gather", Gather(var="radiance")), ("aggregate_ckd_quad", AggregateCKDQuad(var="radiance", measure=measure)), ]).transform(measure.results) step = AddIllumination(illumination=exp.illumination, measure=measure) result = step.transform(values) # Irradiance is here and is indexed in Sun angles and spectral coordinate irradiance = result.data_vars["irradiance"] assert set(irradiance.dims) == {"w"}.union(set(expected_dims))
def test_heterogeneous_multi(modes_all_single, bin_set, path_to_ussa76_approx_data): """ Unit tests for a HeterogeneousAtmosphere with multiple (2+) components. """ # Construct succeeds if eradiate.mode().has_flags(ModeFlags.ANY_MONO): molecular_atmosphere = MolecularAtmosphere.ussa1976( absorption_data_sets={"us76_u86_4": path_to_ussa76_approx_data}, ) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): molecular_atmosphere = MolecularAtmosphere.afgl_1986() else: pytest.skip(f"unsupported mode '{eradiate.mode().id}'") atmosphere = HeterogeneousAtmosphere( molecular_atmosphere=molecular_atmosphere, particle_layers=[ParticleLayer() for _ in range(2)], ) # Radiative property metadata are correct ctx = KernelDictContext(spectral_ctx=CKDSpectralContext(bin_set=bin_set)) # Kernel dict production succeeds assert atmosphere.kernel_phase(ctx) assert atmosphere.kernel_media(ctx) assert atmosphere.kernel_shapes(ctx) # Produced kernel dict can be loaded kernel_dict = atmosphere.kernel_dict(ctx) assert kernel_dict.load()
def test_interpolated_eval(modes_all): if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_ctx = SpectralContext.new(wavelength=550.0) expected = 0.5 elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): bin = BinSet.from_db("10nm").select_bins("550")[0] spectral_ctx = SpectralContext.new(bindex=bin.bindexes[0]) expected = 0.5 else: assert False # Spectrum without quantity performs linear interpolation and yields units # consistent with values spectrum = InterpolatedSpectrum(wavelengths=[500.0, 600.0], values=[0.0, 1.0]) assert spectrum.eval(spectral_ctx) == expected spectrum.values *= ureg("W/m^2/nm") assert spectrum.eval(spectral_ctx) == expected * ureg("W/m^2/nm") # Spectrum with quantity performs linear interpolation and yields units # consistent with quantity spectrum = InterpolatedSpectrum( quantity="irradiance", wavelengths=[500.0, 600.0], values=[0.0, 1.0] ) # Interpolation returns quantity assert spectrum.eval(spectral_ctx) == expected * ucc.get("irradiance")
def eval_sigma_s(self, spectral_ctx: SpectralContext) -> pint.Quantity: """ Evaluate scattering coefficient spectrum based on a spectral context. This method dispatches evaluation to specialised methods depending on the active mode. Parameters ---------- spectral_ctx : :class:`.SpectralContext` A spectral context data structure containing relevant spectral parameters (*e.g.* wavelength in monochromatic mode, bin and quadrature point index in CKD mode). Returns ------- quantity Evaluated spectrum as an array with length equal to the number of layers. """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return self.eval_sigma_s_mono(spectral_ctx.wavelength).squeeze() elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return self.eval_sigma_s_ckd(spectral_ctx.bindex).squeeze() else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def new(**kwargs) -> SpectralContext: """ Create a new instance of one of the :class:`SpectralContext` child classes. *The instantiated class is defined based on the currently active mode.* Keyword arguments are passed to the instantiated class's constructor. Parameters ---------- **kwargs Keyword arguments depending on the currently active mode (see below for a list of actual keyword arguments). wavelength : quantity or float, default: 550 nm **Monochromatic modes** [:class:`.MonoSpectralContext`]. Wavelength. Unit-enabled field (default: ucc[wavelength]). bindex : :class:`.Bindex`, default: test value (1st quadrature point for the "550" bin of the "10nm" bin set) **CKD modes** [:class:`.CKDSpectralContext`]. CKD bindex. See Also -------- :func:`eradiate.mode`, :func:`eradiate.set_mode` """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return MonoSpectralContext(**kwargs) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return CKDSpectralContext(**kwargs) else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def test_onedim_experiment_run_detailed(modes_all): """ Test for correctness of the result dataset generated by OneDimExperiment. """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_cfg = {"wavelengths": 550.0 * ureg.nm} elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): spectral_cfg = {"bin_set": "10nm", "bins": "550"} else: pytest.skip(f"Please add test for '{eradiate.mode().id}' mode") # Create simple scene exp = OneDimExperiment(measures=[ { "type": "hemispherical_distant", "id": "toa_hsphere", "film_resolution": (32, 32), "spp": 1000, "spectral_cfg": spectral_cfg, }, ]) # Run RT simulation exp.run() # Check result dataset structure results = exp.results["toa_hsphere"] # Post-processing creates expected variables ... expected = {"irradiance", "brf", "brdf", "radiance", "spp", "srf"} if eradiate.mode().has_flags(ModeFlags.ANY_CKD): expected |= {"irradiance_srf", "brf_srf", "brdf_srf", "radiance_srf"} assert set(results.data_vars) == expected # ... dimensions assert set( results["radiance"].dims) == {"sza", "saa", "x_index", "y_index", "w"} assert set(results["irradiance"].dims) == {"sza", "saa", "w"} # ... and other coordinates expected_coords = { "sza", "saa", "vza", "vaa", "x", "y", "x_index", "y_index", "w" } if eradiate.mode().has_flags(ModeFlags.ANY_CKD): expected_coords |= {"bin", "bin_wmin", "bin_wmax"} assert set(results["radiance"].coords) == expected_coords expected_coords = {"sza", "saa", "w"} if eradiate.mode().has_flags(ModeFlags.ANY_CKD): expected_coords |= {"bin", "bin_wmin", "bin_wmax"} assert set(results["irradiance"].coords) == expected_coords # We just check that we record something as expected assert np.all(results["radiance"].data > 0.0)
def test_mode_flags(): # Check flags for mono mode eradiate.set_mode("mono") assert eradiate.mode().has_flags(ModeFlags.ERT_MONO) assert not eradiate.mode().has_flags(ModeFlags.MTS_DOUBLE) # Check flags for mono_double mode eradiate.set_mode("mono_double") assert eradiate.mode().has_flags(ModeFlags.ERT_MONO) assert eradiate.mode().has_flags(ModeFlags.MTS_DOUBLE) # Check if conversion of string to flags works as intended eradiate.set_mode("mono_double") assert eradiate.mode().has_flags("any_double")
def test_spectral_context_new(modes_all): """ Unit tests for :meth:`SpectralContext.new`. """ # Empty call to new() should yield the appropriate SpectralContext instance if eradiate.mode().has_flags(ModeFlags.ANY_MONO): assert isinstance(SpectralContext.new(), MonoSpectralContext) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): assert isinstance(SpectralContext.new(), CKDSpectralContext) else: # All modes must have a context: this test fails if the mode has no # associated spectral context assert False
def init_experiment(bottom, top, sigma_a, sigma_s, phase, r, w, spp): assert eradiate.mode().id == "mono_double" return eradiate.experiments.OneDimExperiment( measures=[ eradiate.scenes.measure.MultiDistantMeasure.from_viewing_angles( spectral_cfg=eradiate.scenes.measure.MeasureSpectralConfig.new( wavelengths=w, ), zeniths=np.linspace(-75, 75, 11) * ureg.deg, azimuths=0.0 * ureg.deg, spp=spp, ) ], illumination=eradiate.scenes.illumination.DirectionalIllumination( zenith=30 * ureg.deg, azimuth=0.0 * ureg.deg, irradiance=eradiate.scenes.spectra.SolarIrradianceSpectrum( dataset="blackbody_sun"), ), atmosphere=eradiate.scenes.atmosphere.HomogeneousAtmosphere( bottom=bottom, top=top, sigma_a=sigma_a, sigma_s=sigma_s, phase=phase, ), surface=eradiate.scenes.surface.LambertianSurface(reflectance=r), )
def test_onedim_experiment_run_basic(modes_all): """ OneDimExperiment runs successfully in all modes. """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_cfg = MeasureSpectralConfig.new(wavelengths=550.0 * ureg.nm) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): spectral_cfg = MeasureSpectralConfig.new(bin_set="10nm", bins="550") else: pytest.skip(f"Please add test for '{eradiate.mode().id}' mode") exp = OneDimExperiment() exp.measures[0].spectral_cfg = spectral_cfg exp.run() assert isinstance(exp.results, dict)
def _split_spp_validator(self, attribute, value): if (eradiate.mode().has_flags(ModeFlags.ANY_SINGLE) and self.spp > 1e5 and self.split_spp is None): warnings.warn( "In single-precision modes, setting a sample count ('spp') to " "values greater than 100,000 may result in floating point " "precision issues: using the measure's 'split_spp' parameter is " "recommended.")
def _spectral_dims(): if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return ( ( "w", { "standard_name": "wavelength", "long_name": "wavelength", "units": ucc.get("wavelength"), }, ), ) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return ( ("bin", {"standard_name": "ckd_bin", "long_name": "CKD bin"}), ("index", {"standard_name": "ckd_index", "long_name": "CKD index"}), ) else: raise UnsupportedModeError
def kernel_dict(self, ctx: KernelDictContext) -> KernelDict: if eradiate.mode().has_flags(ModeFlags.ANY_MONO): # TODO: This is a workaround until the hg plugin accepts spectra for # its g parameter g = float(onedict_value(self.g.kernel_dict(ctx=ctx))["value"]) return KernelDict({self.id: { "type": "hg", "g": g, }}) else: raise UnsupportedModeError(supported="monochromatic")
def test_solar_irradiance_eval(modes_all): # Irradiance is correctly interpolated in mono mode s = SolarIrradianceSpectrum(dataset="thuillier_2003") if eradiate.mode().has_flags(ModeFlags.ANY_MONO): spectral_ctx = SpectralContext.new(wavelength=550.0) # Reference value computed manually assert np.allclose(s.eval(spectral_ctx), ureg.Quantity(1.87938, "W/m^2/nm")) elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): bin_set = BinSet.from_db("10nm") bin = bin_set.select_bins("550")[0] bindex = bin.bindexes[0] spectral_ctx = SpectralContext.new(bindex=bindex) # Reference value computed manually assert np.allclose(s.eval(spectral_ctx), ureg.Quantity(1.871527, "W/m^2/nm")) else: assert False
def test_air_scattering_coefficient(modes_all_mono_ckd): ctx = KernelDictContext() # We can instantiate the class s = AirScatteringCoefficientSpectrum() # The spectrum evaluates correctly (reference values computed manually) if eradiate.mode().has_flags(ModeFlags.ANY_MONO): expected = ureg.Quantity(0.0114934, "km^-1") elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): expected = ureg.Quantity(0.0114968, "km^-1") else: assert False value = s.eval(ctx.spectral_ctx) assert np.allclose(value, expected) # The associated kernel dict is correctly formed and can be loaded from mitsuba.core.xml import load_dict assert load_dict(onedict_value(s.kernel_dict(ctx=ctx))) is not None
def eval_sigma_s(self, spectral_ctx: SpectralContext) -> pint.Quantity: """ Evaluate scattering coefficient given a spectral context. Parameters ---------- spectral_ctx : :class:`.SpectralContext` A spectral context data structure containing relevant spectral parameters (*e.g.* wavelength in monochromatic mode, bin and quadrature point index in CKD mode). Returns ------- quantity Particle layer scattering coefficient. """ if eradiate.mode().has_flags(ModeFlags.ANY_MONO): return self.eval_sigma_s_mono(spectral_ctx.wavelength).squeeze() elif eradiate.mode().has_flags(ModeFlags.ANY_CKD): return self.eval_sigma_s_ckd(spectral_ctx.bindex).squeeze() else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def kernel_dict(self, ctx: KernelDictContext) -> KernelDict: if eradiate.mode().has_flags(ModeFlags.ANY_MONO | ModeFlags.ANY_CKD): return KernelDict({ "spectrum": { "type": "uniform", "value": float( self.eval(ctx.spectral_ctx).m_as(uck.get( self.quantity))), } }) else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def new(**kwargs) -> MeasureSpectralConfig: """ Create a new instance of one of the :class:`.SpectralContext` child classes. *The instantiated class is defined based on the currently active mode.* Keyword arguments are passed to the instantiated class's constructor. Parameters ---------- **kwargs : dict, optional Keyword arguments depending on the currently active mode (see below for a list of actual keyword arguments). wavelengths : quantity, default: [550] nm **Monochromatic modes** [:class:`.MonoMeasureSpectralConfig`]. List of wavelengths (automatically converted to a Numpy array). *Unit-enabled field (default: ucc[wavelength]).* bin_set : :class:`.BinSet` or str, default: "10nm" **CKD modes** [:class:`.CKDMeasureSpectralConfig`]. CKD bin set definition. If a string is passed, the data repository is queried for the corresponding identifier using :meth:`.BinSet.from_db`. bins : list of (str or tuple or dict or callable) **CKD modes** [:class:`.CKDSpectralContext`]. List of CKD bins on which to perform the spectral loop. If unset, all the bins defined by the selected bin set will be covered. See Also -------- :func:`eradiate.mode`, :func:`eradiate.set_mode` """ mode = eradiate.mode() if mode is None: raise ModeError( "instantiating MeasureSpectralConfig requires a mode to be selected" ) if mode.has_flags(ModeFlags.ANY_MONO): return MonoMeasureSpectralConfig(**kwargs) elif mode.has_flags(ModeFlags.ANY_CKD): return CKDMeasureSpectralConfig(**kwargs) else: raise UnsupportedModeError(supported=("monochromatic", "ckd"))
def test_apply_spectral_response_function_transform(mode_id, spectral_cfg): """ Unit tests for ApplySpectralResponseFunction.transform(). """ # Prepare basic data eradiate.set_mode(mode_id) exp = OneDimExperiment( atmosphere=None, measures=MultiDistantMeasure.from_viewing_angles( id="measure", zeniths=[-60, -45, 0, 45, 60], azimuths=0.0, spp=1, spectral_cfg=spectral_cfg, ), ) measure = exp.measures[0] exp.process(measure) # Apply first steps of post-processing pipeline = exp.pipeline(measure) values = pipeline.transform(measure.results, stop_after="add_viewing_angles") # Apply tested pipeline step step = ApplySpectralResponseFunction(measure=measure, vars=["radiance"]) if eradiate.mode().has_flags(ModeFlags.ANY_MONO): # In mono modes, the dataset produced by previous steps is missing # bin data required for the step to run successfully with pytest.raises(ValueError): step.transform(values) return # In binned modes, computation goes through result = step.transform(values) # The step adds a SRF-weighted variable assert "radiance_srf" in result.data_vars assert np.all(result.radiance_srf > 0.0)
def test_mode_mono(mode_mono): mode = eradiate.mode() assert mode.kernel_variant == "scalar_mono"
def pipeline( self, *measures: t.Union[Measure, int] ) -> t.Union[Pipeline, t.Tuple[Pipeline, ...]]: result = [] # Convert integer values to measure entries measures = [ self.measures[measure] if isinstance(measure, int) else measure for measure in measures ] for measure in measures: pipeline = pipelines.Pipeline() # Gather pipeline.add( "gather", pipelines.Gather(sensor_dims=measure.sensor_dims, var=measure.var), ) # Aggregate pipeline.add("aggregate_sample_count", pipelines.AggregateSampleCount()) pipeline.add( "aggregate_ckd_quad", pipelines.AggregateCKDQuad(measure=measure, var=measure.var[0]), ) if isinstance(measure, (DistantFluxMeasure, )): pipeline.add( "aggregate_radiosity", pipelines.AggregateRadiosity( sector_radiosity_var=measure.var[0], radiosity_var="radiosity", ), ) # Assemble pipeline.add( "add_illumination", pipelines.AddIllumination( illumination=self.illumination, measure=measure, irradiance_var="irradiance", ), ) if measure.is_distant(): pipeline.add("add_viewing_angles", pipelines.AddViewingAngles(measure=measure)) pipeline.add( "add_srf", pipelines.AddSpectralResponseFunction(measure=measure)) # Compute if isinstance(measure, (MultiDistantMeasure, HemisphericalDistantMeasure)): pipeline.add( "compute_reflectance", pipelines.ComputeReflectance( radiance_var="radiance", irradiance_var="irradiance", brdf_var="brdf", brf_var="brf", ), ) if eradiate.mode().has_flags(ModeFlags.ANY_CKD): pipeline.add( "apply_srf", pipelines.ApplySpectralResponseFunction( measure=measure, vars=["radiance", "irradiance"], ), ) pipeline.add( "compute_reflectance_srf", pipelines.ComputeReflectance( radiance_var="radiance_srf", irradiance_var="irradiance_srf", brdf_var="brdf_srf", brf_var="brf_srf", ), ) elif isinstance(measure, (DistantFluxMeasure, )): pipeline.add( "compute_albedo", pipelines.ComputeAlbedo( radiosity_var="radiosity", irradiance_var="irradiance", albedo_var="albedo", ), ) if eradiate.mode().has_flags(ModeFlags.ANY_CKD): pipeline.add( "apply_srf", pipelines.ApplySpectralResponseFunction( measure=measure, vars=["radiosity", "irradiance"], ), ) pipeline.add( "compute_albedo_srf", pipelines.ComputeAlbedo( radiosity_var="radiosity_srf", irradiance_var="irradiance_srf", albedo_var="albedo_srf", ), ) result.append(pipeline) return result[0] if len(result) == 1 else tuple(result)
def __attrs_pre_init__(self): if not eradiate.mode().has_flags(ModeFlags.ANY_MONO): raise UnsupportedModeError(supported="monochromatic")
def transform(self, x: t.Any) -> t.Any: # If not in CKD mode, this step is a no-op if not eradiate.mode().has_flags(ModeFlags.ANY_CKD): return x # Otherwise, compute quadrature spectrum-indexed variables and turn spp # into a per-bin average # Deduplicate bin list preserving order bins = list(OrderedDict.fromkeys(x.bin.to_index())) n_bins = len(bins) # Collect quadrature data quad = self.measure.spectral_cfg.bin_set.quad # Collect wavelengths associated with each bin wavelength_units = ucc.get("wavelength") wavelengths = [] bin_wmins = [] bin_wmaxs = [] for bin in self.measure.spectral_cfg.bin_set.select_bins(("ids", { "ids": bins })): wavelengths.append(bin.wcenter.m_as(wavelength_units)) bin_wmins.append(bin.wmin.m_as(wavelength_units)) bin_wmaxs.append(bin.wmax.m_as(wavelength_units)) result = x var = self.var # Get dimensions of current variable img = x.data_vars[var] dims = OrderedDict((y, len(img.coords[y])) for y in img.dims) if "bin" not in dims: raise ValueError(f"variable '{var}' is missing dimension 'bin'") if "index" not in dims: raise ValueError(f"variable '{var}' is missing dimension 'index'") # Init storage del dims["bin"] del dims["index"] aggregated = xr.DataArray( np.zeros([n_bins] + list(dims.values())), coords={ "bin": img.bin, **{dim: img.coords[dim] for dim in dims} }, ) # For each bin and each pixel, compute quadrature and store the result for i_bin, bin in enumerate(bins): values_at_nodes = img.sel(bin=bin).values # Rationale: Avoid using xarray's indexing in this loop for # performance reasons (wrong data indexing method will result in # 10x+ speed reduction) for indexes in itertools.product( *[list(range(n)) for n in dims.values()]): aggregated.values[(i_bin, *indexes)] = quad.integrate( values_at_nodes[(slice(None), *indexes)], interval=np.array([0.0, 1.0]), ) result = result.assign({var: aggregated}) result[var].attrs = x[var].attrs # Average the 'spp' variable over the 'index' dimension with xr.set_options(keep_attrs=True): result["spp"] = x.spp.mean(dim="index") # Add spectral coordinates result = result.assign_coords({ "w": ( "bin", wavelengths, { "standard_name": "wavelength", "long_name": "wavelength", "units": symbol(wavelength_units), }, ), "bin_wmin": ( "bin", bin_wmins, { "standard_name": "bin_wmin", "long_name": "spectral bin lower bound", "units": symbol(wavelength_units), }, ), "bin_wmax": ( "bin", bin_wmaxs, { "standard_name": "bin_wmax", "long_name": "spectral bin upper bound", "units": symbol(wavelength_units), }, ), }) # Swap the 'bin' and 'w' dimensions result = result.swap_dims({"bin": "w"}) # Remove the 'index' dimension result = result.drop_dims("index") return result
def __init__(self, supported=None, unsupported=None, msg=None): super(UnsupportedModeError, self).__init__(msg) self.mode = eradiate.mode().id if eradiate.mode() is not None else None self.supported = list(always_iterable(supported)) self.unsupported = list(always_iterable(unsupported))
def transform(self, x: t.Dict) -> xr.Dataset: # Basic preparation prefix = self.prefix sensor_dims = [] sensor_dim_metadata = {} for y in self.sensor_dims: if isinstance(y, str): sensor_dims.append(y) sensor_dim_metadata[y] = {} else: sensor_dims.append(y[0]) sensor_dim_metadata[y[0]] = y[1] spectral_dims = [] spectral_dim_metadata = {} for y in _spectral_dims(): if isinstance(y, str): spectral_dims.append(y) spectral_dim_metadata[y] = {} else: spectral_dims.append(y[0]) spectral_dim_metadata[y[0]] = y[1] sensor_datasets = [] regex = re.compile( r"\_".join( [prefix] + [rf"{sensor_dim}(?P<{sensor_dim}>\d*)" for sensor_dim in sensor_dims] ) ) # Loop on spectral indexes for spectral_index in x.keys(): # Loop on sensors for sensor_id, sensor_data in x[spectral_index]["values"].items(): # Collect data ds = sensor_data.copy(deep=False) spp = x[spectral_index]["spp"][sensor_id] # Set spectral coordinates spectral_coords = { spectral_dim: [spectral_coord] for spectral_dim, spectral_coord in zip( spectral_dims, always_iterable(spectral_index) ) } # Detect sensor coordinates match = regex.match(sensor_id) if match is None: raise RuntimeError( "could not detect requested sensor dimensions in " f"sensor ID '{sensor_id}' using regex '{regex.pattern}'; " "this could be due to incorrect values or order of " "'sensor_dims'" ) sensor_coords = { f"{sensor_dim}_index": [int(match.group(sensor_dim))] for sensor_dim in sensor_dims } # Add spp dimension even though sample count split did not # produce any extra sensor if "spp" not in sensor_dims: sensor_coords["spp_index"] = [0] # Add spectral and sensor dimensions to img array all_coords = {**spectral_coords, **sensor_coords} ds["img"] = ds.img.expand_dims(dim=all_coords) # Package spp in a data array all_dims = list(all_coords.keys()) ds["spp"] = (all_dims, np.reshape(spp, [1 for _ in all_dims])) sensor_datasets.append(ds) # Combine all the data with xr.set_options(keep_attrs=True): result = xr.merge(sensor_datasets) # Drop "channel" dimension when using a mono variant if eradiate.mode().has_flags(ModeFlags.MTS_MONO): result = result.squeeze("channel", drop=True) # Apply metadata to new dimensions for sensor_dim in sensor_dims: result[f"{sensor_dim}_index"].attrs = sensor_dim_metadata[sensor_dim] if "spp" not in sensor_dims: result["spp_index"].attrs = { "standard_name": "spp_index", "long_name": "SPP index", } for spectral_dim in spectral_dims: result[spectral_dim].attrs = spectral_dim_metadata[spectral_dim] # Apply metadata to data variables if isinstance(self.var, str): var = self.var var_metadata = {} else: var = self.var[0] var_metadata = self.var[1] result = result.rename({"img": var}) result[var].attrs.update(var_metadata) result["spp"].attrs = { "standard_name": "sample_count", "long_name": "sample count", } return result