robintw / Py6S

A Python interface to the 6S Radiative Transfer Model
GNU Lesser General Public License v3.0
186 stars 105 forks source link

Pixel radiance above black surface and non-absorbing aerosol-free atmosphere is zero #73

Closed nollety closed 3 years ago

nollety commented 3 years ago

I am a inexperienced 6S/Py6S user so this might not be an issue with Py6S, but rather, my misunderstanding of how 6S/Py6S works.

I would like to compute the pixel_radiance in the principal plane when the ground is a black surface (i.e. homogeneous lambertian surface with null reflectance) and the atmosphere is free of aerosols and scatters light but does not absorb it. The sun zenith and azimuth angles are 50 and 0 degrees, respectively. The sensor altitude is satellite level (which I assume, means that the pixel radiance is measured at the top of the atmosphere) and the target altitude is sea level (which I assume, means that the sensor simply looks down at the ground).

I expect the resulting pixel radiance values to be non zero, as the atmosphere scatters some of the sun light back. However, I get zero values.

Here is the code, that I've used to obtain these results:

from Py6S import *
import numpy as np
import matplotlib.pyplot as plt

def make_scene():
    # arbitrary wavelength value
    s.wavelength = Wavelength(0.550)

    # atmosphere does not absorb
    s.atmos_profile = AtmosProfile.PredefinedType(AtmosProfile.NoGaseousAbsorption)

    # ground is a homogeneous lambertian surface with a reflectance of zero (i.e. black surface)
    s.ground_reflectance = GroundReflectance.HomogeneousLambertian(0.)

    # no aerosols
    s.aero_profile = AeroProfile.PredefinedType(AeroProfile.NoAerosols)
    s.aot550 = 0.
    s.visibility = None

    # sun zenith is 50°
    s.geometry = Geometry.User()
    s.geometry.solar_z = 50.
    s.geometry.solar_a = 0.

    # sensor is at the top of the atmosphere and looks down at the surface
    s.altitudes = Altitudes()
    s.altitudes.set_target_sea_level()
    s.altitudes.set_sensor_satellite_level()

    # no atmospheric correction
    s.atmos_corr = AtmosCorr.NoAtmosCorr()

    return s

def run_principal_plane(signed_view_z):
    s = make_scene()

    pixel_radiances = []

    s.geometry.view_a = 180.
    for view_z in signed_view_z[signed_view_z<0]:
        s.geometry.view_z = -view_z
        s.run()
        pixel_radiances.append(s.outputs.pixel_radiance)

    s.geometry.view_a = 0.
    for view_z in signed_view_z[signed_view_z>=0]:
        s.geometry.view_z = view_z
        s.run()
        pixel_radiances.append(s.outputs.pixel_radiance)

    return np.array(pixel_radiances)

# compute
signed_view_z = np.linspace(-89, 89, 179)
pixel_radiances = run_principal_plane(signed_view_z)

# plot
plt.figure(figsize=(10, 8))
plt.plot(signed_view_z, pixel_radiances)
plt.xlabel('Signed viewing zenith angle [deg]')
plt.ylabel('Pixel radiance [W/m^2/str/μm]')
plt.annotate(text="Sun", xy=(50, 0), xytext=(50, max(pixel_radiances)/2), arrowprops={"arrowstyle":"->"})
plt.show()

Am I wrong to expect non-zero pixel radiance values here? I thought that the scene prepared by the above make_scene function would be a purely molecular scattering atmosphere on top of a all-absorbing surface, that we observe from the top. Am I building the scene correctly? Does the NoGaseousAbsorption atmospheric profile only "disable" gaseous absorption or does it also disables scattering by the air molecules? If I vary the surface reflectance value so that it tends toward zero (from 1e-1 to 1e-6), the pixel radiance values also seem to tend towards zero.

If that can be useful, the corresponding 6S input file (generated by SixS.write_input_file for one value of view_z) is:

0 (User defined)
50.000000 0.000000 45.000000 0.000000 1 1
0
0
0
0.000000 value
0.000000
-1000.000000
-1
0.550000
0 Homogeneous surface
0 No directional effects
0
0.0
-1 No atm. corrections selected
robintw commented 3 years ago

I haven't had chance to look at this in detail now, but as a quick response: I think you might want the apparent_radiance rather than the pixel_radiance. The pixel radiance only includes the light coming directly from the ground surface (which will be 0 as there is 0 reflectance) - the apparent radiance includes the scattered light and so on.

Let me know if that doesn't solve it for you, and I can look into it in more depth.

nollety commented 3 years ago

Thank you for your answer 😃 Indeed, that fixed the issue I was having! The apparent radiance values are not zero.

Thanks for explaining the difference between pixel_radiance and apparent_radiance to me. By the way, I could not find* where these different outputs were described in the 6SV manual 😐 Do you know where that is documented?

*I mean, the appendix II of the 6SV-2.1 user manual gives the equation for the apparent radiance but without text, and I could not find a definition of the pixel radiance there.

robintw commented 3 years ago

Glad it worked.

Unfortunately the documentation of 6S itself is not great, and doesn't define a lot of its outputs very well. I have gradually got to understand what some of the different outputs are through reading the manuals, reading the 6S code and general experimentation. I should try and put some time aside to write some better documentation on the outputs - although I'll have to be careful to state that it's just my interpretation and I could be wrong!

nollety commented 3 years ago

If I can help, whether it's about the documentation or something else, I would be happy to contribute. I find Py6S very useful and I plan to use it more in the future, thank you for putting this all together! 😃

nollety commented 3 years ago

I'll close the issue, since it is solved, thanks again!

robintw commented 3 years ago

Thanks for the offer of help @nollety. I'm now working freelance and have a young son, so struggling to find time to work on projects that don't pay me (and Py6S has never paid me very much).

If you felt like doing any work on the documentation then I'd be happy to help guide you to the right place etc.