Module Implementation¶
Fig. 6 and Fig. 7 show schematic representations of the
three different aspects of a module, using the StarCatalog
and
Observatory
modules as examples, respectively. Every module has a
prototype that defines the module’s standard attributes and methods, including their input/output structure.
Prototype implementations also frequently implement common functionality that is reused by all or most
implementations of that module type. The various implementations inherit the prototype and
add/overload any attributes and methods required for their particular
tasks, limited only by the preset input/output scheme for prototype methods. Finally, in the
course of running a simulation, an object is generated for each module
class selected for that simulation. The generated objects can be used interchangeably in the downstream code, regardless of what
implementation they are instances of, due to the strict interface
defined in the class prototypes. These objects are always called the generic module type throughout the code (implementation class
names are used only when specifying which modules to select for a given simulation).
For lower level (downstream) modules, the input specification is much
more loosely defined than the output specification, as different
implementations may draw data from a wide variety of sources. For
example, the StarCatalog
may be implemented as reading values from a
static file on disk, or may represent an active connection to a local or
remote database. The output specification for these modules, however, as
well as both the input and output for the upstream modules, is entirely
fixed so as to allow for generic use of all module objects in the
simulation.
Module Inheritance and Initialization¶
The only requirement on any implemented module is that it
inherits the appropriate prototype (either directly or by inheriting another module implementation
that inherits the prototype). It is similarly
expected (but not required) that the prototype __init__
will be called from the
__init__
of the newly implemented class (if the class overloads the __init__
method).
Here is an example of the beginning of an OpticalSystem
module implementation:
from EXOSIMS.Prototypes.OpticalSystem import OpticalSystem
class ExampleOpticalSystem(OpticalSystem):
def __init__(self, **specs):
OpticalSystem.__init__(self, **specs)
...
Important
The filename must match the class name for all modules.
Important
If overloading the prototype __init__
, the implemented module’s __init__
method must have a keyword argument dictionary input (the **specs
argument in the example, above). This must be the last argument to the method. See here for an explanation of the syntax, and see Input Specification for further discussion on this input. Note that the name of the input is arbitrary, but is always **specs
in the EXOSIMS prototypes.
Module Type¶
It is always possible to check whether a module is an instance of a given prototype, for example:
isinstance(obj,EXOSIMS.Prototypes.Observatory.Observatory)
However, it can be tedious to look up all of a given object’s base
classes so, for convenience, every prototype will provide a private
variable _modtype
, which will always return the name of the
prototype and should not be overwritten by any module code. Thus, if the
above example evaluates as True
, obj._modtype
will be equal to
Observatory
.
Callable Attributes¶
Certain module attributes may be represented in a way that allows them
to be parametrized by other values. For example, the instrument
throughput and contrast are functions of both the wavelength and the
angular separation, and so must be encodable as such in the OpticalSystem
.
To accommodate this, as well as simpler descriptions
where these parameters may be treated as static values, these and other
attributes are defined as ‘callable’. This means that they must be set
as objects that can be called in the normal Python fashion, i.e.,
object(arg1,arg2,...)
.
These objects can be function definitions defined in the code, or imported from other modules. They can be lambda expressions defined inline in the code. Or they can be callable object instances, such as the various scipy interpolants. In cases where the description is just a single value, these attributes can be defined as dummy functions that always return the same value, for example:
def throughput(wavelength,angle):
return 0.5
or, more simply:
throughput = lambda wavelength,angle: 0.5
Warning
It is important to remember that Python differentiates between how it treats class attributes and methods in inheritance. If a value is originally defined as an attribute (such as a lambda function), then it cannot be overloaded by a method in an inheriting class implementation. So, if a prototype contains a callable value as an attribute, it must be implemented as an attribute in all inheriting implementations that wish to change the value. For this reason, the majority of callable attributes in prototype modules are instead defined as methods to avoid potential overloading issues.
Units¶
All attributes/variables representing quantities with units are encoded using astropy.units.quantity.Quantity
objects. Docstrings will often state the default unit used for quantities, but it is never necessary to assume a unit, other than for inputs (see Input Specification).
Coding Conventions¶
EXOSIMS attempts to follow standard Python coding conventions (PEP-8, etc.) and it is required that all new code be blackened. Descriptive variable and module names are strongly encouraged. Documentation of existing modules follows the Google docstring style, although the NumPy style is acceptable for new contributions. For more details, see Docstrings.
The existing codebase (as it was written by many different contributors) contains a wide variety of naming conventions and naming styles, including lots of CamelCase and mixedCase names. The project PI thinks these look pretty and is firmly unapologetic on this point.
Interface Specification¶
The docstrings for the prototypes (see Framework) are the interface control documentation (ICD) for EXOSIMS
.
Warning
Module implementations overloading a prototype method may not modify the calling syntax to the method. Doing so will almost invariably cause the new module to not function properly within the broader framework and will almost certainly cause unit tests to fail for that implementation.
New implementations must adhere to the interface specification, and should seek to overload as few methods as possible to produce the desired results. Any change in the method declaration in any prototype is considered interface breaking and will result in a software version bump.