Custom Models and Effects
========================================================================================

Introduction
-------------------------------------------------------------------------------

Users can create their own custom models and effects within the LightCurveLynx framework,
allowing them to expand the range of phenomena that can be simulated. This page provides a
high-level overview of how to create custom models and effects. For more details and examples,
see the :doc:`Adding New Model Types <notebooks/adding_models>` and 
:doc:`Adding New Effect Types <notebooks/adding_effects>` notebooks.

Custom Models
-------------------------------------------------------------------------------

All models are subclasses of the ``BasePhysicalModel`` class, which provides the recipe 
for computing the noise-free light curves given values for a set of parameters. This base class
is subclassed into:

* ``SEDModel``: for models that simulate the full spectral energy distributions (SEDs).
* ``BandfluxModel``: for models that simulate band fluxes directly.

Users creating their own models will start by subclassing one of these two classes, depending on
the type of model they want to create.

For both types of models, users need to implement an ``__init__()`` function that defines the model's
parameters. This function should call the ``super().__init__()`` function to initialize the base class
and then define the model's parameters using the ``add_parameter()`` function.

For new SED-type models, the class needs to implement a ``compute_sed()`` function that generates the
noise-free flux densities (in nJy) in the object's rest-frame given information about the times (in days
relative to ``t0``), wavelengths (in Angstroms), and model parameters (called graph_state). Both the times
and wavelengths are converted to account for redshift before being passed to the ``compute_sed()`` function,
so the function takes rest-frame times and wavelengths.

For example simple code to output a constant 1 nJy for all times and wavelengths would look like:

.. code-block:: python

    def compute_sed(self, times, wavelengths, graph_state, **kwargs):
        return np.ones((len(times), len(wavelengths)))

For new Bandflux-type models, the class needs to implement a corresponding ``compute_bandflux()`` function
that generates the band fluxes in the observer-frame given the times (in days relative to ``t0``), bands,
and model parameters (called graph_state). These models do not account for redshift, since simulation is
done in the observer frame.

A user of a particular physical model only needs to understand what parameters the model has
and how they are set. A user creating a new physical model additionally needs to know how the noise-free,
rest-frame flux density values are generated from those parameters.

For examples see the :doc:`Adding New Model Types <notebooks/adding_models>` notebook.

Custom Effects
-------------------------------------------------------------------------------

Effects are transformations made to the flux densities generated by the physical model. Users can create a
new effect by subclassing the ``BaseEffect`` class and implementing the ``apply()`` function, which takes in
the  flux densities, applies the transformation to them, and outputs the transformed flux densities.

For examples see the :doc:`Adding New Effect Types <notebooks/adding_effects>` notebook.

Handling Randomness
-------------------------------------------------------------------------------

A key property in LightCurveLynx is that a simulation should be deterministically replay-able
given a ``GraphState``. What does this mean when a model uses randomness, such as a AGN computed
with a damped random walk?  LightCurveLynx supports a combination of random models and reproducibility
by allowing users to store random seeds within the ``GraphState``.

For example the basic AGN model includes a parameter ``agn_seed``.  When generating fluxes for observations,
the simulation starts by creating a random number generator with the given seed and using it to sample.
The seed saved into the ``agn_seed`` can thus be used to reproduce the samples for the AGN. If no seed is
specified, this value will itself be random:

.. code-block:: python

    if seed is not None:
        self.add_parameter(
            "agn_seed",
            seed,
            description="The seed for the AGN random number generator.",
        )
    else:
        seed_generator = NumpyRandomFunc("integers", low=0, high=2**32 - 1)
        self.add_parameter(
            "agn_seed",
            seed_generator,
            description="The seed for the AGN random number generator.",
        )

This ensures that different (non-fixed) the simulations are indeed different.

A similar approach can be used for effects that depend on randomness, such as the ``WhiteNoise`` effect.
This effect uses the ``white_noise_seed`` parameter when to create a random number generator that is
deterministic given the seed. Unless a seed is manually specified, that parameter is random chosen.
