Writing a new scene element class#

Warning

Please first read carefully the Contributing to Eradiate section. Writing scene elements requires general knowledge of the attrs, Pint and Pinttrs libraries.

Scene elements, deriving from the SceneElement class, are the core of Eradiate’s scene generation system. They provide an interface to quickly and safely generate kernel scene dictionary elements (see KernelDict).

The SceneElement base class#

SceneElement is the abstract base class for all scene elements. We will see here how this class works, and then how to write a new scene element subclass.

  • SceneElement is decorated by attr.s(). Although not required, this is a hint at the user: all scene element classes are written with the attrs library. SceneElement has an id instance attribute with a default value: consequently, all instance attributes defined for child classes must have default values.

  • SceneElement derives from abc.ABC: it is an abstract class and cannot be instantiated.

  • SceneElement has an abstract method kernel_dict() which must be implemented by its derived classes: it returns a dictionary which can be then used as an input to the kernel.

  • SceneElement is the base class of a set of abstract interfaces (e.g. Illumination, Spectrum, etc.) which are the ones from which new scene elements should derive.

Factory registration#

All interfaces derived from SceneElement are associated a specialised factory (see Factory guide). New SceneElement subclasses should be registered to the relevant factory so that Eradiate’s dictionary-based object and initialisation system works properly.

import attr
from eradiate.scenes.spectra import Spectrum, spectrum_factory
from eradiate import ureg

@spectrum_factory.register(type_id="my_spectrum")
@attr.s
class MySpectrum(Spectrum):
    field = pinttr.ib(default=1.0, units=ureg.m)
    def eval(ctx=None): ...  # Definition skipped
    def kernel_dict(ctx=None): ...  # Definition skipped

obj = spectrum_factory.convert({"type": "my_spectrum", "field": 1.0})

As mentioned in the Factory guide, factory registration occurs only upon class definition: a module defining a scene element must be imported for the defined class to be registered to a factory.

Using factory converters#

As mentioned in the Factory guide, Eradiate’s factories implement a convert() class method which can turn a dictionary into a registered object—and if the method receives something else than a dictionary, it simply does nothing.

This method can be used as a converter in the attribute initialisation sequence to automatically convert a dictionary to a specified object. This allows for the use of nested dictionaries to instantiate multiple objects.

import attr
import pinttr

from eradiate import unit_registry as ureg
from eradiate.scenes.illumination import Illumination, illumination_factory
from eradiate.scenes.spectrum import Spectrum, spectrum_factory

@spectrum_factory.register(type_id="my_spectrum")
@attr.s
class MySpectrum(Spectrum):
    field = pinttr.ib(default=1.0, units=ureg.m)
    def eval(ctx=None): ...  # Definition skipped
    def kernel_dict(ctx=None): ...  # Definition skipped

@illumination_factory.register("my_illumination")
@attr.s
class MyIllumination(Illumination):
    radiance = attr.ib(
        factory=MySpectrum,
        converter=spectrum_factory.convert
    )
    def kernel_dict(): ...  # Definition skipped

# Pass object created with constructor
obj = MyIllumination(radiance=MySpectrum(field=2.0))
# Use the factory to convert a dictionary to ElementA
obj = MyIllumination(element_a={"type": "my_spectrum", "field": 3.0})
# Instantiate MyIllumination using nested dicts
obj = illumination_factory.create({
    "type": "my_illumination",
    "radiance": {"type": "my_spectrum", "field": 4.0},
})

The kernel_dict() method#

Any scene element must implement a kernel_dict() method which will return a dictionary suitable for merge into a kernel scene dictionary. These dictionaries are written following the Mitsuba scene specification and the interested reader is referred to kernel docs for further information.

Note

When writing the kernel_dict() method, there are a few precautions to keep in mind:

  • kernel imports must be local to the method;

  • if a kernel import is required to build the dictionary, a kernel variant must be selected when it is called (in practice, this means that Eradiate’s operational mode must have been selected);

  • kernel_dict()’s signature should allow for the processing of a KernelDictContext instance, which carries around state variables during recursive kernel dictionary generation.

In practice: Steps to write a new scene element class#

Following the above description, a new scene element class requires the following steps:

  1. Derive a new class from one of the SceneElement subclasses. Decorate it with attr.s().

  2. Declare your custom attributes using attr.ib(). Don’t forget to add default values to all of them. Use pinttr.ib() if the field represents a physical quantity with units. Callables can be used to evaluate units dynamically. If the field requires it, it is possible to run custom converters and validators.

  3. Implement the kernel_dict() method. Things to keep in mind:

    • kernel imports must be local to the kernel_dict() method;

    • the function’s signature should allow for the processing of a ctx keyword argument of type KernelDictContext (but using it is not required).

The following steps are optional:

  • implement a post-init hook steps using the __attrs_post_init__() method;

  • enable factory-based instantiation using the register() decorator defined by the appropriate factory.