Spectra: comparison between plan-parallel and spherical

In this example, we present how you can calculate a reflectance spectrum with htrdr-planets from a set of 1D data. Here, we compare between plan-parallel and spherical geometries.

Imports

We start by importing the necessary libraries.:

import numpy as np
import matplotlib.pyplot as plt
import htrdrPy as htrdr
import scipy.constants as cst
import json

Data

The first step is to generate an Data object that will contain the optical properties. This object aims at handling the data in order to generate the input files. In this example, the optical properties are loaded from an .npz file that you can find here. We first create an instance of the Data object with the correct planet radius:

radius = 2.575e6 # m

dataSp= htrdr.Data(radius = radius, nTheta=80, nPhi=80, name="Sphere")
dataPP = htrdr.Data(radius = 1e9, name="PP")

Note

In plan parallel, the radius is used to define the x and y extension of the atmosphere. Make sure this value is large enough, but also note that a too large value can result in issues within htrdr. For this example, we found that any value larger the 1e9 would result in inconsistent results.

Note

At the creation of each Data instance, an inputs_{name}/ repository is created and will contain all the input files necessary for htrdr. A outputs_{name}/ repository is also created and will contain the files created by htrdr.

Then, we read the data and convert it into a python disctionnary. The Data object will work with the dictionnary, which therefore needs to have the correct keys. In this example, the keys used to generate the .npz file are already the right ones. Each corresponds to a table with a given shape. All these informations (keys name and array shapes) are provided further down in this documentation (c.f. htrdrPy.data module).

data = np.load("Example_spectra_data/1D_spetral_data.npz")
data = dict(data)
for key, array in data.items():
    print(key, ", shape : ", array.shape)

Additional information, such as the dimensions (number of angles in the phase function, number of wavelength, etc.) are also required to be present in the dictionnary. We therefore add the necessary information:

nWavelength, nLevel, nCoeff = data["absorption (m-1)"].shape
nAngle = len(data["angles (°)"])
wavelengths = data["wavelength"]

data.update({
        'nLevel' : nLevel,
        'nWavelength' : nWavelength,
        'nCoeff' : nCoeff,
        'nAngle' : nAngle,
        })

print(data.keys())

The next step is to provide those data to the Data object. Depending on the context, different methods exist to pass the information. In this example, we use the Data.makeMixture() method, which handles the case where the data provided are those of the atmosphere mixture comprising all the gases and aerosols. The dim parameter indicates the dimension of the provided data: 1 means that we provide a column, 2 for a slice along altitude latitude and 3 for a fully 3D heteorgeneous atmosphere. Obviously, the shapes of the tables provided through the dictionnary change accordingly. dim=0 sets a plan-parallel atmosphere.

dataSp.makeMixture(data, dim=1)
dataPP.makeMixture(data, dim=0)

Then, we generate the surface by providing the temperature map (a single float in 1D) and the brdf dictionnary to the Data instance through the Data.makeGround() methods. The method to use depends on the dimension of the input data.

brdf = {
        "kind": 'lambertian',
        "albedo": data['surface albedo'],
        "wavelengths": wavelengths
        }
dataPP.makeGroundFrom1D_PP(91, brdf)
dataSp.makeGroundFrom1D(91, brdf)

Generating input files

We can now generate the input files with the command Data.writeInputs():

dataPP.writeInputs()
dataSp.writeInputs()

We can also generate the VTK files with the command Data.writeVTKfiles(). VTK files are not necessary and only serves to visualize the data that will be send to htrdr-planets.

dataPP.writeVTKfiles()
dataSp.writeVTKfiles()

Observation geometry

Once the input files are generated (this can actually be done before), we have to define the observation geometry. These informations concerns the camera (such as its position or the target point observed), the image (the (x,y) definition and the number of sample per pixel) and the source (size, radius, distance, longitude, latitude, etc.). We start by creating an instance of Geometry (two actually, one for the plan-parallel calculation and one for the spherical calulation):

geomPP = htrdr.Geometry(case='PP')
geomSp = htrdr.Geometry(case='Sphere')

Note

A geometry can be used multiple times, with different Data and Script object. Here, we have to create one insatnce for each because the target won’t be at the same position in both case: in the spherical scenario, the planet radius adds a shift to the scene.

In the current scenario, we aim to reproduce some observation with constraints being the incidence, emergence, phase and azimut angles. To calculate the camera and source position, the Geometry objects has a method Geometry.makeGeomFromAPIE() (a routine that makes you happy!) that will automatically generate the geometry from the constraints on the observation angles. Those information are read from a file (contained in the data repository downloaded earlier). Additional information are requirted by the routine, such as the distance between the camera and target point and the source properties (distance, size and temperature).

with open("Example_spectra_data/observation.json", 'r') as f:
    obs = json.loads(f.read())
cameraDist = 1000 * cst.kilo    # m
solDist = 10 * cst.au   # m
solRad = 7e8    # m
solTemp = 5800  # K

geomPP.setImage([64,64], 10)
geomPP.makeGeomFromAPIE(obs, 0, cameraDist, solDist, solRad, srcTemp=solTemp)
geomPP.exportGeometry()

geomSp.setImage([64,64], 10)
geomSp.makeGeomFromAPIE(obs, radius, cameraDist, solDist, solRad, srcTemp=solTemp)
geomSp.exportGeometry()

Note

The source temperature will be used by htrdr to determine the spectral distribution of the incoming stellar radiation, but note that is it also pausible to directly use a spectrum (c.f. Geometry.setSource() documentation).

Note

The image information must be provided separately through Geometry.setImage() or when creating the Geometry instance.

Note

Geometry.exportGeometry() will produce a file within the geometries repository, containing the source, camera and image parameters.

Scripts

Now, we create an instance of Script that handles the last details of the calulation and the call to htrdr. The different methods of the Script object simplifies the call to htrdr via a bunch of predefined scirpts (c.f. htrdrPy.script module for a full review of the possibilities). Here, we start a reflectance spectrum (I/F) and we therefore use the Script.reflectanceSpectrum() method, which requires the instance of Geometry previously created, the type of calculation (“sw” or “lw”) and the list of wavlengths:

scriptPP = htrdr.Script(case='Spectrum',
        MPIcmd="mpirun -np 4 --map-by socket:PE=9")
scriptPP.reflectanceSpectrum(geomPP, "sw", wavelengths)

scriptSp = htrdr.Script(case='Spectrum',
        MPIcmd="mpirun -np 4 --map-by socket:PE=9")
scriptSp.reflectanceSpectrum(geomSp, "sw", wavelengths)

Then, we can start the calculation by calling the instance of Script with the previously created instance of Data:

scriptPP(dataPP)
scriptSp(dataSp)

Post-process

Finally, a call to Postprocess will treat the raw output of htrdr to generate the required results, here it will be spectra, stored in the results_Sphere/ and the results_PP/ repositories:

htrdr.Postprocess(scriptPP)
htrdr.Postprocess(scriptSp)

We can now recover and plot the calculated spectra along with the observed specrtum as well as additonal data, all provided in the spectrum.txt file previously downloaded:

with open("results_PP/reflectance_spectrum_Spectrum.json", 'r') as f:
    resPP = json.loads(f.read())

with open("results_Sphere/reflectance_spectrum_Spectrum.json", 'r') as f:
    resSp= json.loads(f.read())

with open("Example_spectra_data/spectrum.txt", 'r') as f:
    f.readline()
    wvl = []
    IF_obs   = []
    IF_SHDOM = []
    IF_MCC   = []
    for line in f:
        l = line.split()
        wvl     .append(l[0])
        IF_obs  .append(l[2])
        IF_SHDOM.append(l[3])
        IF_MCC  .append(l[4])

wvl      = np.array(wvl     , dtype=float)
IF_obs   = np.array(IF_obs  , dtype=float)
IF_SHDOM = np.array(IF_SHDOM, dtype=float)
IF_MCC   = np.array(IF_MCC  , dtype=float)

print(wvl)
print(IF_obs)

fig, ax = plt.subplots()

ax.plot(wvl, IF_obs  , label="Observed", ls="", marker="o")
ax.plot(wvl, IF_SHDOM, label="SHDOMPP", ls="", marker="o")
ax.plot(wvl, IF_MCC  , label="MCCSPHE", ls="", marker="o")

ax.errorbar(np.array(resPP['wavelength'])/cst.micro,
            np.array(resPP['reflectance spectrum']),
            yerr = 3 * np.array(resPP['reflectance std deviation']),
            label = "plan-parallel")

ax.errorbar(np.array(resSp['wavelength'])/cst.micro,
            np.array(resSp['reflectance spectrum']),
            yerr = 3 * np.array(resSp['reflectance std deviation']),
            label = "spherical")

ax.set_ylabel(r'$\frac{I}{F}$', rotation='horizontal', fontsize=15)
ax.set_xlabel('Wavlength [µm]', fontsize=15)

ax.legend()

fig.savefig('Spectrum.png')
_images/spectrum.png