.. _spectra-example:

Spectra: comparison between plan-parallel and spherical
=======================================================

.. highlight:: python



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 :py:class:`.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
:py:class:`.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**.

.. _here: https://www.dropbox.com/scl/fo/qk20qvsvocy9w3zuik254/ADNYNbijPepUqOOgQGoQH78?rlkey=r95p5zzsmdkylihtyacfslgoa&st=pbtkmwxa&dl=0

Then, we read the data and convert it into a python disctionnary.  The
:py:class:`.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. :ref:`data`). ::

    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 :py:class:`.Data` object.
Depending on the context, different methods exist to pass the information. In
this example, we use the :py:meth:`.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 :py:class:`.Data` instance through
the :py:meth:`.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
:py:meth:`.Data.writeInputs`::

    dataPP.writeInputs()
    dataSp.writeInputs()

We can also generate the VTK files with the command
:py:meth:`.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
:py:class:`.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
   :py:class:`.Data` and :py:class:`.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 :py:class:`.Geometry` objects has a method
:py:meth:`.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.
   :py:meth:`.Geometry.setSource` documentation).

.. note::
   The **image** information must be provided separately through
   :py:meth:`.Geometry.setImage` or when creating the
   :py:class:`.Geometry` instance.

.. note::
   :py:meth:`.Geometry.exportGeometry` will produce a file within the
   **geometries** repository, containing the source, camera and image
   parameters.

Scripts
-------

Now, we create an instance of :py:class:`.Script` that handles the last details
of the calulation and the call to **htrdr**. The different methods of the
:py:class:`.Script` object simplifies the call to **htrdr** via a bunch of
predefined scirpts (c.f. :ref:`script` for a full review of the
possibilities). Here, we start a reflectance spectrum (I/F) and we therefore use
the :py:meth:`.Script.reflectanceSpectrum` method, which requires the instance
of :py:class:`.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
:py:class:`.Script` with the previously created instance of :py:class:`.Data`::

    scriptPP(dataPP)
    scriptSp(dataSp)

Post-process
------------

Finally, a call to :py:class:`.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')

.. image:: /plot_examples/spectrum.png
