BRDF Data Containers
====================

``pydirectional`` contains data container classes to help with handling BRDF data. These objects can also implement helpful methods for analysis.

``BRDFMeasurements`` is used for handling multi-angular reflectance data and ``BRDFParameters`` is used for handling the parameters used by the BRDF models.

They can be imported with::

    from pydirectional import BRDFMeasurements
    from pydirectional import BRDFParameters

BRDFMeasurements
----------------

To instantiate a ``BRDFMeasurements`` object, you must provide at least:

* ``reflectance`` - array with final dimension ``angle_index``
* ``reflectance_dims`` - list of dimension names
* ``solar_zenith_angle``
* ``viewing_zenith_angle``
* ``relative_azimuth_angle`` *or* both ``solar_azimuth_angle`` and ``viewing_azimuth_angle``
* values for the dimensions in ``reflectances_dims`` (eg. ``wavelength``)

The measurand must be either ``"HCRF"`` or ``"BRF"``.
For ``"HCRF"``, the ``direct_to_diffuse_irr`` must be supplied.

Example::

    refl = np.random.rand(5, 10)
    wl   = np.linspace(400, 900, 5)
    sza  = np.linspace(20, 40, 10)
    vza  = np.linspace(0, 50, 10)
    raa  = np.linspace(0, 180, 10)

    meas = BRDFMeasurements(
        reflectance=refl,
        measurand="BRF",
        reflectance_dims=["wavelength", "angle_index"],
        solar_zenith_angle=sza,
        viewing_zenith_angle=vza,
        relative_azimuth_angle=raa,
        wavelength=wl,
    )

.. note::

   ``relative_azimuth_angle`` is computed automatically when
   ``solar_azimuth_angle`` and ``viewing_azimuth_angle`` are provided.


Coordinates and Dimensions
^^^^^^^^^^^^^^^^^^^^^^^^^^

``BRDFMeasurements`` can store one or more of:

* ``wavelength`` - spectral coordinate
* ``x`` - spatial coordinate
* ``y`` - spatial coordinate

At least one coordinate must be supplied at creation time.  
Coordinates may be scalars or 1-D arrays.


Uncertainty Inputs
^^^^^^^^^^^^^^^^^^

The class supports three uncertainty components:

* ``u_rand_reflectance`` - random uncertainty
* ``u_syst_reflectance`` - systematic uncertainty
* ``u_strct_reflectance`` - structured uncertainty

Structured uncertainties may include error-correlation matrices along:

* ``angle_index``  
* ``wavelength``  
* ``x``  
* ``y``

These are stored in the underlying ``obsarray`` dataset.


Accessing Data
^^^^^^^^^^^^^^

Convenience methods allow retrieval of geometry, reflectance, uncertainties,
and ancillary metadata::

    sza = meas.get_solar_zenith_angle()
    refl = meas.get_reflectance()
    u_tot = meas.get_total_uncertainty()
    wl = meas.get_wavelength()

Subsetting can be performed by wavelength and/or spatial position::

    refl_550 = meas.get_reflectance(wavelength=550)
    irr_xy = meas.get_direct_to_diffuse_irr(x=0, y=0)

The whole dataset can be subset using::

    subset_meas_ds = meas.subset_dataset(wavelength=550, x=0, y=0)


Selecting Geometries
^^^^^^^^^^^^^^^^^^^^

Specific angular geometries can be selected using::

    subset = meas.select_geometry(
        sza=30,
        vza=10,
        raa=150,
    )

Selection uses `nearest` interpolation with an optional tolerance set in the context, the interpolation method can also be changed within the context.


Adding Measurements
^^^^^^^^^^^^^^^^^^^

New reflectance measurements with additional angle indices may be appended::

    meas.add_measurement(
        reflectance=new_refl,
        solar_zenith_angle=new_sza,
        viewing_zenith_angle=new_vza,
        relative_azimuth_angle=new_raa,
        wavelength=new_wl,
    )

All coordinate sizes and dimensionality must be consistent with existing data.


Working with NetCDF
^^^^^^^^^^^^^^^^^^^

BRDF measurement objects can be saved to NetCDF using::

    BRDFMeasurements.to_netcdf(path = "my_measurements.nc")

BRDF measurement datasets saved in NetCDF format can be reloaded::

    loaded = BRDFMeasurements.load_netcdf("my_measurements.nc")

Plotting
^^^^^^^^

There are several internal plotting functions that wrap ``pydirectional.plotting`` functionality:

* ``plot_reflectance_polar``
* ``plot_reflectance_spectral``
* ``plot_reflectance_timeseries``

BRDFParameters
--------------

To instantiate a ``BRDFParameters`` object, you must provide at least:

* ``brdf_model`` - name of the BRDF model, e.g. ``"RPV"``  
* ``params`` - parameter array
* ``parameter_dims``  - list of dimension names 
* values for the dimensions in ``parameter_dims`` (eg. ``wavelength``)

Example::

    params = np.array([[0.1, 0.3, 0.2],
                        [0.2, 0.5, 0.1]])   # example parameter set
    wl = np.linspace(400, 900, 2)

    brdf_params = BRDFParameters(
        brdf_model="RPV",
        params=params,
        parameter_dims=["wavelength", "parameter_names"],
        wavelength=wl,
    )

The class automatically retrieves the correct parameter names from the model via
``BRDFModelFactory`` and associates them with the final dimension.

.. note::
   Parameter names must match the underlying BRDF model.  
   They can be checked using::
    
        BRDFModelFactory().get_brdf_model("RPV", sza=0, vza=0, raa=0).get_coefficient_names()


Coordinates and Dimensions
^^^^^^^^^^^^^^^^^^^^^^^^^^

The following optional coordinate axes may be defined:

* ``wavelength``  
* ``x``  
* ``y``

You must provide at least one of these. Coordinates may be scalar or vector
values.

All parameter and uncertainty inputs must be consistent with these dimensions.


Uncertainty Inputs
^^^^^^^^^^^^^^^^^^

Two uncertainty inputs are supported:

* ``uncertainties`` - uncertainty values for each parameter  
* ``err_corr`` - error correlation matrices along the ``parameter_names`` axis  

Example with uncertainties::

    BRDFParameters(
        brdf_model="RTLS",
        params=params,
        parameter_dims=["wavelength", "parameter_names"],
        wavelength=wl,
        uncertainties=np.full_like(params, 0.01),
        err_corr='rand'
    )

Adding Parameter Sets
^^^^^^^^^^^^^^^^^^^^^

Additional parameter sets can be appended using::

    brdf_params.add_measurement(
        params=new_params,
        wavelength=new_wl,
    )

All dimensions must be compatible with the existing dataset, and the provided
coordinate values must match (or extend) existing dimensions.


Selecting Parameters
^^^^^^^^^^^^^^^^^^^^

Individual parameters may be selected using::

    ds_sel = brdf_params.select_parameter("theta")

If ``rewrite_ds=True`` is used, the internal dataset will be replaced with the
subset.


Accessing Parameters and Metadata
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Several helper methods extract subsets of the stored data::

    wl   = brdf_params.get_wavelengths()
    pars = brdf_params.get_params(wavelength=550)
    unc  = brdf_params.get_uncertainties(wavelength=550)
    corr = brdf_params.get_err_corr(wavelength=550)


Evaluating BRF and HCRF
^^^^^^^^^^^^^^^^^^^^^^^

``BRDFParameters`` allows direct evaluation of BRF or HCRF using the stored
parameter sets.

Evaluating BRF::

    brf = brdf_params.evaluate_BRF(
        sza=30,
        vza=10,
        raa=150,
        wavelength=550,
    )

Evaluating HCRF (requires ``direct_to_diffuse_irr``)::

    hcrf = brdf_params.evaluate_HCRF(
        sza=30,
        vza=10,
        raa=150,
        wavelength=550,
    )

Both methods return a ``BRDFMeasurements`` object for further analysis or
plotting.


Working with NetCDF
^^^^^^^^^^^^^^^^^^^

BRDF parameter objects can be saved to NetCDF using::

    BRDFParameters.to_netcdf(path = "my_parameters.nc")

BRDF parameter datasets saved in NetCDF format can be reloaded::

    loaded = BRDFParameters.load_netcdf("my_parameters.nc")

Plotting
^^^^^^^^

There are several internal plotting functions that wrap ``pydirectional.plotting`` functionality:

* ``plot_parameter_spectral``
* ``plot_uncertainty_spectral``
* ``plot_err_corr_matrix``

