def test_directional(mode_mono): from mitsuba.core.xml import load_dict # We need a default spectral config ctx = KernelDictContext() # Constructor d = DirectionalIllumination() assert load_dict(onedict_value(d.kernel_dict(ctx))) is not None # Check if a more detailed spec is valid d = DirectionalIllumination(irradiance={"type": "uniform", "value": 1.0}) assert load_dict(onedict_value(d.kernel_dict(ctx))) is not None # Check if solar irradiance spectrum can be used d = DirectionalIllumination(irradiance={"type": "solar_irradiance"}) assert load_dict(onedict_value(d.kernel_dict(ctx))) is not None # Check if specification from a float works d = DirectionalIllumination(irradiance=1.0) assert load_dict(onedict_value(d.kernel_dict(ctx))) is not None # Check if specification from a constant works d = DirectionalIllumination(irradiance=ureg.Quantity(1.0, "W/m^2/nm")) assert load_dict(onedict_value(d.kernel_dict(ctx))) is not None with pytest.raises(pinttr.exceptions.UnitsError): # Wrong units DirectionalIllumination(irradiance=ureg.Quantity(1.0, "W/m^2/sr/nm"))
def test_rpv(mode_mono): ctx = KernelDictContext() # Default constructor surface = RPVSurface() # Check if produced scene can be instantiated kernel_dict = KernelDict.from_elements(surface, ctx=ctx) assert kernel_dict.load() is not None # Construct from floats surface = RPVSurface(width=ureg.Quantity(1000.0, "km"), rho_0=0.3, k=1.4, g=-0.23) assert np.allclose(surface.width, ureg.Quantity(1e6, ureg.m)) # Construct from mixed spectrum types surface = RPVSurface( width=ureg.Quantity(1000.0, "km"), rho_0=0.3, k={ "type": "uniform", "value": 0.3 }, g={ "type": "interpolated", "wavelengths": [300.0, 800.0], "values": [-0.23, 0.23], }, ) # Check if produced scene can be instantiated assert KernelDict.from_elements(surface, ctx=ctx).load() is not None
def test_compute_sigma_s_air_multidim(): """ Supports multiple wavelength and number density values. """ w = ureg.Quantity(np.linspace(280, 2400), "nm") n = ureg.Quantity(np.array([_LOSCHMIDT.m_as("m^-3")] * 8), "m^-3") result = compute_sigma_s_air(wavelength=w, number_density=n) assert result.shape == (len(w), len(n))
def test_array_rad_props_profile_eval_dataset(mode_mono): """ Returns a data set. """ p = ArrayRadProfile( levels=ureg.Quantity(np.linspace(0, 100, 12), "km"), albedo_values=ureg.Quantity(np.linspace(0.0, 1.0, 11), ureg.dimensionless), sigma_t_values=ureg.Quantity(np.linspace(0.0, 1e-5, 11), "m^-1"), ) spectral_ctx = SpectralContext.new() assert isinstance(p.eval_dataset(spectral_ctx), xr.Dataset)
def test_spherical_to_cartesian(): r = 2.0 theta = np.deg2rad(30) phi = np.deg2rad(0) d = spherical_to_cartesian(r, theta, phi) assert np.allclose(d, [1, 0, np.sqrt(3)]) r = ureg.Quantity(2.0, "km") theta = np.deg2rad(60) phi = np.deg2rad(30) d = spherical_to_cartesian(r, theta, phi) assert np.allclose(d, ureg.Quantity( [3.0 / 2.0, np.sqrt(3) / 2.0, 1.0], "km"))
def test_uniform(modes_all): from mitsuba.core.xml import load_dict # Instantiate with value only s = UniformSpectrum(value=1.0) assert s.quantity is PhysicalQuantity.DIMENSIONLESS assert s.value == 1.0 * ureg.dimensionless # Instantiate with value and quantity s = UniformSpectrum(value=1.0, quantity=PhysicalQuantity.COLLISION_COEFFICIENT) assert s.value == 1.0 * ureg.m**-1 UniformSpectrum(value=1.0, quantity="collision_coefficient") assert s.quantity == PhysicalQuantity.COLLISION_COEFFICIENT assert s.value == 1.0 * ureg.m**-1 # Instantiate with unsupported quantity with pytest.raises(ValueError): UniformSpectrum(value=1.0, quantity="speed") # Instantiate with all arguments s = UniformSpectrum(quantity="collision_coefficient", value=1.0) assert s.value == ureg.Quantity(1.0, "m^-1") # Raise if units and quantity are inconsistent with pytest.raises(pinttr.exceptions.UnitsError): UniformSpectrum(quantity="collision_coefficient", value=ureg.Quantity(1.0, "")) # Instantiate from factory using dict s = spectrum_factory.convert({ "type": "uniform", "quantity": "radiance", "value": 1.0, "value_units": "W/km^2/sr/nm", }) # Produced kernel dict is valid ctx = KernelDictContext() assert load_dict(onedict_value(s.kernel_dict(ctx))) is not None # Unit scaling is properly applied with ucc.override({"radiance": "W/m^2/sr/nm"}): s = UniformSpectrum(quantity="radiance", value=1.0) with uck.override({"radiance": "kW/m^2/sr/nm"}): ctx = KernelDictContext() d = s.kernel_dict(ctx) assert np.allclose(d["spectrum"]["value"], 1e-3)
def test_homogeneous_sigma_s(mode_mono): """ Assigns custom 'sigma_s' value. """ spectral_ctx = SpectralContext.new() r = HomogeneousAtmosphere(sigma_s=1e-5) assert r.eval_sigma_s(spectral_ctx) == ureg.Quantity(1e-5, ureg.m ** -1)
def test_ramiatm_experiment_surface_adjustment(mode_mono): """Create a Rami4ATM experiment and assert the central patch surface is created with the correct parameters, according to the canopy and atmosphere.""" from mitsuba.core import ScalarTransform4f ctx = KernelDictContext() s = Rami4ATMExperiment( atmosphere=HomogeneousAtmosphere(width=ureg.Quantity(42.0, "km")), canopy=DiscreteCanopy.homogeneous( lai=3.0, leaf_radius=0.1 * ureg.m, l_horizontal=10.0 * ureg.m, l_vertical=2.0 * ureg.m, padding=0, ), surface=CentralPatchSurface(central_patch=LambertianSurface(), background_surface=LambertianSurface()), ) expected_trafo = ScalarTransform4f.scale( 1400) * ScalarTransform4f.translate((-0.499642857, -0.499642857, 0.0)) kernel_dict = s.kernel_dict(ctx=ctx) assert np.allclose(kernel_dict["bsdf_surface"]["weight"]["to_uv"].matrix, expected_trafo.matrix)
def test_on_quantity(): v = on_quantity(attr.validators.instance_of(float)) # This should succeed v(None, None, 1.) v(None, None, ureg.Quantity(1., "km")) # This should fail @attr.s class Attribute: # Tiny class to pass an appropriate attribute argument name = attr.ib() attribute = Attribute(name="attribute") with pytest.raises(TypeError): v(None, attribute, "1.") with pytest.raises(TypeError): v(None, attribute, ureg.Quantity("1.", "km"))
def test_array_rad_props_profile_invalid_values(mode_mono): """ Mismatching shapes in albedo_values and sigma_t_values arrays raise. """ with pytest.raises(ValueError): ArrayRadProfile( levels=ureg.Quantity(np.linspace(0, 100, 12), "km"), albedo_values=np.linspace(0.0, 1.0, 11), sigma_t_values=np.linspace(0.0, 1e-5, 10), )
def test_sigma_s_air_wavelength_dependence(): """ Test that the Rayleigh scattering coefficient scales with the 4th power of wavelength. """ wavelength = ureg.Quantity(np.linspace(240.0, 2400.0), "nm") sigma_s = compute_sigma_s_air(wavelength) prod = sigma_s.magnitude * np.power(wavelength, 4) assert np.allclose(prod, prod[0], rtol=0.2)
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_ramiatm_experiment_kernel_dict(mode_mono, padding): from mitsuba.core import ScalarTransform4f ctx = KernelDictContext() # Surface width is appropriately inherited from canopy, when no atmosphere is present s = Rami4ATMExperiment( atmosphere=None, canopy=DiscreteCanopy.homogeneous( lai=3.0, leaf_radius=0.1 * ureg.m, l_horizontal=10.0 * ureg.m, l_vertical=2.0 * ureg.m, padding=padding, ), measures=[ { "type": "distant", "id": "distant_measure" }, { "type": "radiancemeter", "origin": [1, 0, 0], "id": "radiancemeter" }, ], ) kernel_scene = s.kernel_dict(ctx) assert np.allclose( kernel_scene["surface"]["to_world"].transform_point([1, -1, 0]), [5 * (2 * padding + 1), -5 * (2 * padding + 1), 0], ) # -- Measures get no external medium assigned assert "medium" not in kernel_scene["distant_measure"] assert "medium" not in kernel_scene["radiancemeter"] # Surface width is appropriately inherited from atmosphere s = Rami4ATMExperiment( atmosphere=HomogeneousAtmosphere(width=ureg.Quantity(42.0, "km")), canopy=DiscreteCanopy.homogeneous( lai=3.0, leaf_radius=0.1 * ureg.m, l_horizontal=10.0 * ureg.m, l_vertical=2.0 * ureg.m, padding=padding, ), ) kernel_dict = s.kernel_dict(ctx) assert np.allclose( kernel_dict["surface"]["to_world"].matrix, ScalarTransform4f.scale([21000, 21000, 1]).matrix, )
def test_particle_layer_construct_attrs(): """Assigns parameters to expected values.""" bottom = ureg.Quantity(1.2, "km") top = ureg.Quantity(1.8, "km") tau_550 = ureg.Quantity(0.3, "dimensionless") layer = ParticleLayer( bottom=bottom, top=top, distribution=UniformParticleDistribution(), tau_550=tau_550, n_layers=9, dataset="tests/radprops/rtmom_aeronet_desert.nc", ) assert layer.bottom == bottom assert layer.top == top assert isinstance(layer.distribution, UniformParticleDistribution) assert layer.tau_550 == tau_550 assert layer.n_layers == 9 assert layer.dataset == path_resolver.resolve( "tests/radprops/rtmom_aeronet_desert.nc" )
def test_converter(mode_mono): # Dicts are correctly processed s = spectrum_factory.converter("radiance")({ "type": "uniform", "value": 1.0 }) assert s == UniformSpectrum(quantity="radiance", value=1.0) s = spectrum_factory.converter("irradiance")({ "type": "uniform", "value": 1.0 }) assert s == UniformSpectrum(quantity="irradiance", value=1.0) # Floats and quantities are correctly processed s = spectrum_factory.converter("radiance")(1.0) assert s == UniformSpectrum(quantity="radiance", value=1.0) s = spectrum_factory.converter("radiance")(ureg.Quantity( 1e6, "W/km^2/sr/nm")) assert s == UniformSpectrum(quantity="radiance", value=1.0) with pytest.raises(pinttr.exceptions.UnitsError): spectrum_factory.converter("irradiance")(ureg.Quantity( 1, "W/m^2/sr/nm"))
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 test_us76_approx_rad_profile_levels(mode_mono, us76_approx_test_absorption_data_set): """ Collision coefficients' shape match altitude levels shape. """ p = US76ApproxRadProfile( thermoprops=dict(levels=ureg.Quantity(np.linspace(0, 120, 121), "km")), absorption_data_set=us76_approx_test_absorption_data_set, ) spectral_ctx = SpectralContext.new() for field in ["sigma_a", "sigma_s", "sigma_t", "albedo"]: x = getattr(p, f"eval_{field}")(spectral_ctx) assert x.shape == (120, )
def test_onedim_experiment_kernel_dict(modes_all): """ Test non-trivial kernel dict generation behaviour. """ from mitsuba.core import ScalarTransform4f ctx = KernelDictContext() # Surface width is appropriately inherited from atmosphere exp = OneDimExperiment(atmosphere=HomogeneousAtmosphere( width=ureg.Quantity(42.0, "km"))) kernel_dict = exp.kernel_dict(ctx) assert np.allclose( kernel_dict["surface"]["to_world"].matrix, ScalarTransform4f.scale([21000, 21000, 1]).matrix, ) # Setting atmosphere to None exp = OneDimExperiment( atmosphere=None, surface={ "type": "lambertian", "width": 100.0, "width_units": "m" }, measures=[ { "type": "distant", "id": "distant_measure" }, { "type": "radiancemeter", "origin": [1, 0, 0], "id": "radiancemeter" }, ], ) # -- Surface width is not overridden kernel_dict = exp.kernel_dict(ctx) assert np.allclose( kernel_dict["surface"]["to_world"].matrix, ScalarTransform4f.scale([50, 50, 1]).matrix, ) # -- Atmosphere is not in kernel dictionary assert "atmosphere" not in kernel_dict # -- Measures get no external medium assigned assert "medium" not in kernel_dict["distant_measure"] assert "medium" not in kernel_dict["radiancemeter"]
def test_cos_angle_to_direction(): # Old-style call assert np.allclose(cos_angle_to_direction(1.0, 0.0), [0, 0, 1]) assert np.allclose(cos_angle_to_direction(0.0, 0.0), [1, 0, 0]) assert np.allclose(cos_angle_to_direction(0.5, 0.0), [np.sqrt(3) / 2, 0, 0.5]) assert np.allclose( cos_angle_to_direction(-1.0, ureg.Quantity(135.0, "deg")), [0, 0, -1]) # Vectorised call assert np.allclose( cos_angle_to_direction([1.0, 0.0, 0.5, -1.0], [0, 0, 0.0, 0.75 * np.pi]), ([0, 0, 1], [1, 0, 0], [np.sqrt(3) / 2, 0, 0.5], [0, 0, -1]), )
def test_array_rad_props_profile(mode_mono): """ Assigns attributes. """ levels = ureg.Quantity(np.linspace(0, 100, 12), "km") albedo_values = ureg.Quantity(np.linspace(0.0, 1.0, 11), ureg.dimensionless) sigma_t_values = ureg.Quantity(np.linspace(0.0, 1e-5, 11), "m^-1") p = ArrayRadProfile( levels=levels, albedo_values=albedo_values, sigma_t_values=sigma_t_values, ) spectral_ctx = SpectralContext.new() assert isinstance(p.levels, ureg.Quantity) assert isinstance(p.eval_albedo(spectral_ctx=spectral_ctx), ureg.Quantity) assert isinstance(p.eval_sigma_t(spectral_ctx=spectral_ctx), ureg.Quantity) assert isinstance(p.eval_sigma_a(spectral_ctx=spectral_ctx), ureg.Quantity) assert isinstance(p.eval_sigma_s(spectral_ctx=spectral_ctx), ureg.Quantity) assert np.allclose(p.levels, levels) assert np.allclose(p.eval_albedo(spectral_ctx=spectral_ctx), albedo_values) assert np.allclose(p.eval_sigma_t(spectral_ctx=spectral_ctx), sigma_t_values)
def test_sigma_s_air(): """ Test computation of Rayleigh scattering coefficient for air with default values. """ ref_cross_section = ureg.Quantity(4.513e-27, "cm**2") ref_sigmas = ref_cross_section * _LOSCHMIDT expected = ref_sigmas # Compare to reference value computed from scattering cross section in # Bates (1984) Planetary and Space Science, Volume 32, No. 6. assert np.allclose(compute_sigma_s_air(number_density=_LOSCHMIDT), expected, rtol=1e-2)
def test_afgl_1986_rad_profile_levels(mode_mono, afgl_1986_test_absorption_data_sets): """ Collision coefficients' shape match altitude levels shape. """ n_layers = 101 p = AFGL1986RadProfile( thermoprops=dict( levels=ureg.Quantity(np.linspace(0.0, 100.0, n_layers + 1), "km")), absorption_data_sets=afgl_1986_test_absorption_data_sets, ) spectral_ctx = SpectralContext.new(wavelength=550.0) for field in ["sigma_a", "sigma_s", "sigma_t", "albedo"]: x = getattr(p, f"eval_{field}")(spectral_ctx) assert x.shape == (n_layers, )
def test_rami4atm_experiment_construct_normalize_measures(mode_mono, padding): # When canopy is not None, measure target matches canopy unit cell exp = Rami4ATMExperiment( atmosphere=None, canopy=DiscreteCanopy.homogeneous( lai=3.0, leaf_radius=0.1 * ureg.m, l_horizontal=10.0 * ureg.m, l_vertical=2.0 * ureg.m, padding=padding, ), measures=MultiDistantMeasure(), ) target = exp.measures[0].target canopy = exp.canopy assert np.isclose(target.xmin, -0.5 * canopy.size[0]) assert np.isclose(target.xmax, 0.5 * canopy.size[0]) assert np.isclose(target.ymin, -0.5 * canopy.size[1]) assert np.isclose(target.ymax, 0.5 * canopy.size[1]) assert np.isclose(target.z, canopy.size[2]) # The measure target does not depend on the atmosphere exp = Rami4ATMExperiment( atmosphere=HomogeneousAtmosphere(width=ureg.Quantity(42.0, "km")), canopy=DiscreteCanopy.homogeneous( lai=3.0, leaf_radius=0.1 * ureg.m, l_horizontal=10.0 * ureg.m, l_vertical=2.0 * ureg.m, padding=padding, ), measures=MultiDistantMeasure(), ) target = exp.measures[0].target canopy = exp.canopy assert np.isclose(target.xmin, -0.5 * canopy.size[0]) assert np.isclose(target.xmax, 0.5 * canopy.size[0]) assert np.isclose(target.ymin, -0.5 * canopy.size[1]) assert np.isclose(target.ymax, 0.5 * canopy.size[1]) assert np.isclose(target.z, canopy.size[2])
def test_air_refractive_index(): """ Test computation of the air refractive index for different wavelength values. We compare the results with the values given in Table III of :cite:`Peck1972DispersionAir`. """ wavelength = ureg.Quantity( np.array([ 1.6945208, 1.01425728, 0.64402492, 0.54622707, 0.3889751, 0.230289 ]), "micrometer", ) indices = air_refractive_index(wavelength) # compute the refractivities in parts per 1e8 results = (indices - 1) * 1e8 expected = np.array( [27314.19, 27410.90, 27638.092, 27789.843, 28336.843, 30787.68]) assert np.allclose(results, expected, rtol=1e-5)
def test_compute_sigma_a_no_t_coord(): # data set with no t coordinate ds = xr.Dataset( data_vars={ "xs": (("w", "p"), np.random.random((4, 3)), dict(units="cm^2")) }, coords={ "w": ("w", np.linspace(18000, 18010, 4), dict(units="cm^-1")), "p": ("p", np.linspace(90000, 110000, 3), dict(units="Pa")), }, ) # returns a single absorption coefficient value when input pressure is # a scalar wl = ureg.Quantity(555.5, "nm") x = compute_sigma_a(ds, wl=wl) assert isinstance(x, ureg.Quantity) assert x.check("[length]^-1") assert len(x) == 1 # handles multiple wavelength values x = compute_sigma_a(ds, wl=ureg.Quantity(np.array([555.50, 555.51]), "nm")) assert len(x) == 2 # does not raise when 'fill_value' is used x = compute_sigma_a(ds, wl=wl, p=ureg.Quantity(120000.0, "Pa"), fill_values=dict(pt=0.0)) # raises when wavelength out of range with pytest.raises(ValueError): compute_sigma_a(ds, wl=ureg.Quantity(560.0, "nm")) # raises when pressure out of range with pytest.raises(ValueError): compute_sigma_a(ds, wl=wl, p=ureg.Quantity(120000.0, "Pa")) # returns an array of absorption coefficient values when input pressure is # an array ds_p = to_quantity(ds.p) x = compute_sigma_a(ds=ds, wl=wl, p=np.linspace(ds_p.min(), ds_p.max(), num=10)) assert isinstance(x, ureg.Quantity) assert x.check("[length]^-1") assert len(x) == 10 # absorption coefficient scales with number density x = compute_sigma_a(ds, wl=wl, n=ureg.Quantity(np.array([1.0, 2.0]), "m^-3")) assert x[1] == 2 * x[0] # dataset with no temperature coordinate is not interpolated on temperature x = compute_sigma_a(ds, wl=wl, t=ureg.Quantity(200.0, "K"), n=ureg.Quantity(1.0, "m^-3")) y = compute_sigma_a(ds, wl=wl, t=ureg.Quantity(300.0, "K"), n=ureg.Quantity(1.0, "m^-3")) assert x == y
assert my_singleton1 is my_singleton2 @pytest.mark.parametrize( "value, expected", [ ("aaa", False), ([0, 1], False), ([0, 2], False), ([0, 1, 2], True), ([0.0, 1, 2], True), ([0, 1, "2"], False), (np.array([0, 1, 2]), True), (np.array([0, 1]), False), (np.array(["0", 1, 2]), False), (ureg.Quantity([0, 1, 2], "m"), True), ], ) def test_is_vector3(value, expected): result = is_vector3(value) assert result == expected def test_natsort(): assert natsorted(["10", "1.2", "9"]) == ["1.2", "9", "10"] assert natsorted(["1.2", "a1", "9"]) == ["1.2", "9", "a1"] def test_deduplicate(): assert deduplicate([1, 1, 2, 3], preserve_order=True) == [1, 2, 3] assert deduplicate([2, 1, 3, 1], preserve_order=True) == [2, 1, 3]
# .. note:: # When the atmosphere's width is not specified, # :class:`~eradiate.scenes.atmosphere.HomogeneousAtmosphere` automatically # determines the width so that the atmosphere is optically thick in the # horizontal direction. # %% # Set the atmosphere's dimensions # ------------------------------- # Set the atmosphere's height and width using the ``top`` and ``width`` # attributes, respectively: from eradiate import unit_registry as ureg atmosphere = eradiate.scenes.atmosphere.HomogeneousAtmosphere( top=ureg.Quantity(120, "km"), width=ureg.Quantity(500, "km")) # %% # The atmosphere above now has the dimensions 500 x 500 x 120 km. # %% # Set the atmosphere's radiative properties # ----------------------------------------- # Set the atmosphere's scattering and absorption coefficients using the # ``sigma_s`` and ``sigma_a`` attributes, respectively: atmosphere = eradiate.scenes.atmosphere.HomogeneousAtmosphere( sigma_s=ureg.Quantity(1e-3, "km^-1"), sigma_a=ureg.Quantity(1e-5, "km^-1")) # %% # The above atmosphere is now characterised by
# thermophysical model. # # Refer to the :mod:`~eradiate.radprops` module documentation for a list of # available radiative properties profiles. # %% # Set the atmosphere's level altitude mesh # ---------------------------------------- # Set the atmosphere's level altitude mesh using the ``levels`` attribute of # the radiative properties profile object: import numpy as np atmosphere = eradiate.scenes.atmosphere.HeterogeneousAtmosphereLegacy( profile=eradiate.radprops.US76ApproxRadProfile(thermoprops=dict( levels=ureg.Quantity(np.linspace(0, 120, 61), "km")))) # %% # This level altitude mesh defines 60 layers, each 2 km-thick. # The atmosphere's vertical extension automatically inherits that of the # underlying radiative properties profile object so that, in the example above, # the atmosphere also extends from 0 to 120 km. # Use the ``levels`` parameter to control the atmosphere vertical extension as # well as the number of atmospheric layers and their thicknesses. # # For example, you can define a completely arbitrary level altitude mesh with # four layers of varying thicknesses: atmosphere = eradiate.scenes.atmosphere.HeterogeneousAtmosphereLegacy( profile=eradiate.radprops.US76ApproxRadProfile(thermoprops=dict( levels=ureg.Quantity(np.array([0.0, 4.0, 12.0, 64.0, 100.0]), "km"))))
class HemisphericalDistantMeasure(Measure): """ Hemispherical distant radiance measure scene element [``hdistant``, ``hemispherical_distant``]. This scene element records radiance leaving the scene in a hemisphere defined by its ``direction`` parameter. A distinctive feature of this measure is that it samples continuously the direction space instead of computing radiance values for a fixed set of directions, thus potentially capturing effects much harder to distinguish using *e.g.* the :class:`.MultiDistantMeasure` class. On the other side, features located at a precise angle will not be captured very well by this measure. This measure is useful to get a global view of leaving radiance patterns over a surface. Notes ----- * Setting the ``target`` parameter is required to get meaningful results. Experiment classes should take care of setting it appropriately. """ # -------------------------------------------------------------------------- # Fields and properties # -------------------------------------------------------------------------- _film_resolution: t.Tuple[int, int] = documented( attr.ib( default=(32, 32), validator=attr.validators.deep_iterable( member_validator=attr.validators.instance_of(int), iterable_validator=validators.has_len(2), ), ), doc="Film resolution as a (width, height) 2-tuple. " "If the height is set to 1, direction sampling will be restricted to a " "plane.", type="array-like", default="(32, 32)", ) orientation: pint.Quantity = documented( pinttr.ib( default=ureg.Quantity(0.0, ureg.deg), validator=[ validators.is_positive, pinttr.validators.has_compatible_units ], units=ucc.deferred("angle"), ), doc="Azimuth angle defining the orientation of the sensor in the " "horizontal plane.\n" "\n" "Unit-enabled field (default: ucc['angle']).", type="float", default="0.0 deg", ) direction = documented( attr.ib( default=[0, 0, 1], converter=np.array, validator=validators.is_vector3, ), doc="A 3-vector orienting the hemisphere mapped by the measure.", type="array-like", default="[0, 0, 1]", ) flip_directions = documented( attr.ib(default=None, converter=attr.converters.optional(bool)), doc=" If ``True``, sampled directions will be flipped.", type="bool", default="False", ) target: t.Optional[Target] = documented( attr.ib( default=None, converter=attr.converters.optional(Target.convert), validator=attr.validators.optional( attr.validators.instance_of(( TargetPoint, TargetRectangle, ))), on_setattr=attr.setters.pipe(attr.setters.convert, attr.setters.validate), ), doc="Target specification. The target can be specified using an " "array-like with 3 elements (which will be converted to a " ":class:`.TargetPoint`) or a dictionary interpreted by " ":meth:`Target.convert() <.Target.convert>`. If set to " "``None`` (not recommended), the default target point selection " "method is used: rays will not target a particular region of the " "scene.", type=":class:`.Target` or None", init_type=":class:`.Target` or dict or array-like, optional", ) @property def film_resolution(self): return self._film_resolution flags: MeasureFlags = documented( attr.ib(default=MeasureFlags.DISTANT, converter=MeasureFlags, init=False), doc=get_doc(Measure, "flags", "doc"), type=get_doc(Measure, "flags", "type"), ) @property def viewing_angles(self) -> pint.Quantity: """ quantity: Viewing angles computed from stored film coordinates as a (width, height, 2) array. The last dimension is ordered as (zenith, azimuth). """ # Compute viewing angles at pixel locations # Angle computation must match the kernel plugin's direction sampling # routine angle_units = ucc.get("angle") # Compute pixel locations in film coordinates xs = (np.linspace(0, 1, self.film_resolution[0], endpoint=False) + 0.5 / self.film_resolution[0]) ys = (np.linspace(0, 1, self.film_resolution[1], endpoint=False) + 0.5 / self.film_resolution[1]) # Compute corresponding angles xy = np.array([(x, y) for x in xs for y in ys]) angles = direction_to_angles( square_to_uniform_hemisphere(xy)).to(angle_units) # Normalise azimuth to [0, 2π] angles[:, 1] %= 360.0 * ureg.deg # Reshape array to match film size on first 2 dimensions return angles.reshape((len(xs), len(ys), 2)) # -------------------------------------------------------------------------- # Kernel dictionary generation # -------------------------------------------------------------------------- def _kernel_dict(self, sensor_id, spp): result = { "type": "distant", "id": sensor_id, "direction": self.direction, "orientation": [ np.cos(self.orientation.m_as(ureg.rad)), np.sin(self.orientation.m_as(ureg.rad)), 0.0, ], "sampler": { "type": "independent", "sample_count": spp, }, "film": { "type": "hdrfilm", "width": self.film_resolution[0], "height": self.film_resolution[1], "pixel_format": "luminance", "component_format": "float32", "rfilter": { "type": "box" }, }, } if self.target is not None: result["ray_target"] = self.target.kernel_item() if self.flip_directions is not None: result["flip_directions"] = self.flip_directions return result def kernel_dict(self, ctx: KernelDictContext) -> KernelDict: sensor_ids = self._sensor_ids() sensor_spps = self._sensor_spps() result = KernelDict() for spp, sensor_id in zip(sensor_spps, sensor_ids): result.data[sensor_id] = self._kernel_dict(sensor_id, spp) return result # -------------------------------------------------------------------------- # Post-processing information # -------------------------------------------------------------------------- @property def var(self) -> t.Tuple[str, t.Dict]: return "radiance", { "standard_name": "radiance", "long_name": "radiance", "units": symbol(uck.get("radiance")), }
def test_target_origin(modes_all): from mitsuba.core import Point3f # TargetPoint: basic constructor with ucc.override({"length": "km"}): t = TargetPoint([0, 0, 0]) assert t.xyz.units == ureg.km with pytest.raises(ValueError): TargetPoint(0) # TargetPoint: check kernel item with ucc.override({"length": "km"}), uck.override({"length": "m"}): t = TargetPoint([1, 2, 0]) assert ek.allclose(t.kernel_item(), [1000, 2000, 0]) # TargetRectangle: basic constructor with ucc.override({"length": "km"}): t = TargetRectangle(0, 1, 0, 1) assert t.xmin == 0.0 * ureg.km assert t.xmax == 1.0 * ureg.km assert t.ymin == 0.0 * ureg.km assert t.ymax == 1.0 * ureg.km with ucc.override({"length": "m"}): t = TargetRectangle(0, 1, 0, 1) assert t.xmin == 0.0 * ureg.m assert t.xmax == 1.0 * ureg.m assert t.ymin == 0.0 * ureg.m assert t.ymax == 1.0 * ureg.m with pytest.raises(ValueError): TargetRectangle(0, 1, "a", 1) with pytest.raises(ValueError): TargetRectangle(0, 1, 1, -1) # TargetRectangle: check kernel item t = TargetRectangle(-1, 1, -1, 1) with uck.override({"length": "mm"}): # Tricky: we can't compare transforms directly kernel_item = t.kernel_item()["to_world"] assert ek.allclose(kernel_item.transform_point(Point3f(-1, -1, 0)), [-1000, -1000, 0]) assert ek.allclose(kernel_item.transform_point(Point3f(1, 1, 0)), [1000, 1000, 0]) assert ek.allclose(kernel_item.transform_point(Point3f(1, 1, 42)), [1000, 1000, 42]) # Factory: basic test with ucc.override({"length": "m"}): t = Target.new("point", xyz=[1, 1, 0]) assert isinstance(t, TargetPoint) assert np.allclose(t.xyz, ureg.Quantity([1, 1, 0], ureg.m)) t = Target.new("rectangle", 0, 1, 0, 1) assert isinstance(t, TargetRectangle) # Converter: basic test with ucc.override({"length": "m"}): t = Target.convert({"type": "point", "xyz": [1, 1, 0]}) assert isinstance(t, TargetPoint) assert np.allclose(t.xyz, ureg.Quantity([1, 1, 0], ureg.m)) t = Target.convert([1, 1, 0]) assert isinstance(t, TargetPoint) assert np.allclose(t.xyz, ureg.Quantity([1, 1, 0], ureg.m)) with pytest.raises(ValueError): Target.convert({"xyz": [1, 1, 0]})