Extending Met.3D ================ .. _adding_derived_variables: Adding a new derived variable ----------------------------- Every derived variable is implemented as a subclass of ``MDerivedDataFieldProcessor``. Each processor declares the CF standard name of the output variable, the list of required input variable names (optionally qualified with a level type), and a ``compute()`` method that fills the output grid. All processors are registered in ``MDerivedMetVarsDataSource::MDerivedMetVarsDataSource()`` in ``src/data/derivedvars/derivedmetvarsdatasource.cpp``. For a list of existing derived variables and user-facing configuration, see :doc:`/03_user_manual/data_handling/derived_vars`. **Adding a C++ derived variable** 1. Subclass ``MDerivedDataFieldProcessor`` in ``src/data/derivedvars/derivedmetvars_standard.h/.cpp`` (or a new file): .. code-block:: c++ class MMyNewVariableProcessor : public MDerivedDataFieldProcessor { public: MMyNewVariableProcessor(); void compute(const QList>& inputGrids, MStructuredGrid *derivedGrid) override; }; 2. Implement the constructor to declare the standard name and required inputs: .. code-block:: c++ MMyNewVariableProcessor::MMyNewVariableProcessor() : MDerivedDataFieldProcessor( "my_new_variable", QStringList() << "air_temperature" << "specific_humidity") {} To restrict to a specific level type, append a qualifier to the input name and pass allowed output level types as a third argument: .. code-block:: c++ MMyNewVariableProcessor::MMyNewVariableProcessor() : MDerivedDataFieldProcessor( "my_new_variable", QStringList() << "eastward_wind/HYBRID_SIGMA_PRESSURE_3D" << "surface_air_pressure/SURFACE_2D", {HYBRID_SIGMA_PRESSURE_3D}) {} 3. Implement ``compute()``. The ``inputGrids`` list matches the declared input order. Write results to ``derivedGrid``: .. code-block:: c++ void MMyNewVariableProcessor::compute( const QList>& inputGrids, MStructuredGrid *derivedGrid) { const auto& tempGrid = inputGrids.at(0); const auto& humGrid = inputGrids.at(1); for (int k = 0; k < derivedGrid->getNumLevels(); ++k) { for (int j = 0; j < derivedGrid->getNumLats(); ++j) { for (int i = 0; i < derivedGrid->getNumLons(); ++i) { float value = 0.f; /* your computation here... */ derivedGrid->setValue(k, j, i, value); } } } } 4. Register the processor in ``MDerivedMetVarsDataSource::MDerivedMetVarsDataSource()`` in ``derivedmetvarsdatasource.cpp``: .. code-block:: c++ registerDerivedDataFieldProcessor(new MMyNewVariableProcessor()); **Adding a Python-based derived variable** Python-derived variables call MetPy (or arbitrary Python code) through Met.3D's Python interface. Data is exchanged as an ``xarray.Dataset`` via an in-memory NetCDF buffer. 1. **Add an endpoint type.** In ``src/data/derivedvars/pythonderivedprocessor.h``, add a value to the ``EndpointType`` enum before ``INVALID``: .. code-block:: c++ enum EndpointType { METPY_PV_BAROCLINIC = 0, // ... existing values ... MY_NEW_ENDPOINT = N, // next available index INVALID }; 2. **Expose the value to Python.** In ``src/system/mpyinterfacebindings.cpp``, add a ``.value()`` line to the pybind11 enum block: .. code-block:: c++ .value("MY_NEW_ENDPOINT", PyEndpoint::MY_NEW_ENDPOINT) 3. **Register the processor.** In ``MPythonDerivedProcessor::registerProcessorsTo()`` in ``pythonderivedprocessor.cpp``: .. code-block:: c++ auto myProcessor = new MPythonDerivedProcessor( "my_new_variable_metpy", {"air_pressure", "air_temperature"}, // required inputs, in order PyEndpoint::MY_NEW_ENDPOINT); dataSource->registerDerivedDataFieldProcessor(myProcessor); 4. **Add the Python function.** In ``src/python/interface/python_endpoints.py``: .. code-block:: python def my_new_variable(dataset, in_grid_names, out_grid_name): import metpy.calc as mpcalc from metpy.units import units p = dataset[in_grid_names[0]] * units('Pa') t = dataset[in_grid_names[1]] * units('K') dataset[out_grid_name] = mpcalc.some_function(p, t) return dataset 5. **Dispatch the function.** In ``invoke_python_endpoint()`` in ``python_endpoints.py``: .. code-block:: python elif func == derived_var_bindings.EndpointType.MY_NEW_ENDPOINT: return my_new_variable(dataset, in_grid_names, out_grid_name) .. note:: If you have implemented a derived variable that you think would be useful to other Met.3D users, we would be happy to include it in the main codebase. Please consider submitting a `merge request on GitLab`_. .. _`merge request on GitLab`: https://gitlab.com/wxmetvis/met.3d/-/merge_requests Adding a new actor ------------------ Actors are the visual elements of a Met.3D scene. Each actor is a C++ class that owns its OpenGL resources, a set of user-configurable properties, and one or more render methods. Choose a base class: * ``MActor`` (``src/gxfw/mactor.h``) is the general base. Use for annotation, geometry, or any actor that does not read gridded data from the pipeline. * ``MNWPMultiVarActor`` (``src/gxfw/nwpmultivaractor.h``) extends ``MActor`` with data variables. Use when the actor needs to load one or more data fields. Deriving from MActor ~~~~~~~~~~~~~~~~~~~~~ ``MExampleActor`` (``src/teaching/``) is a minimal, heavily commented example actor. It is the best starting point for a new ``MActor``-derived actor. Enable it with: .. code-block:: bash cmake -DTEACHING=ON .. The example actor covers: constructor boilerplate, property setup, shader loading, geometry upload, and rendering. Read through it alongside the notes below. Required static methods ^^^^^^^^^^^^^^^^^^^^^^^ Every actor must implement four static methods used by the registration system: .. code-block:: c++ static QString staticActorType() { return "MyActor"; } // session file key static QString staticDisplayName() { return "My actor"; } // Create actor menu static QString staticIconPath() { return ":/icons/actors/missing.png"; } static QString staticSettingsID() { return "MyActor"; } // config group name Properties ^^^^^^^^^^ Declared as member variables (``MFloatProperty``, ``MColorProperty``, ``MEnumProperty``, ``MBoolProperty``, ``MPointFProperty``, ``MButtonProperty``, …), configured in the constructor, and registered with ``actorPropertiesSupGroup.addSubProperty()``. Callbacks either call ``&MActor::emitActorChangedSignal`` (redraw only) or a custom slot that requests data, rebuilds geometry and so on. Shaders ^^^^^^^ GLSL effect files (``*.fx.glsl``) live in ``src/glsl/``. Load them in ``reloadShaderEffects()`` via ``compileShadersFromFileWithProgressDialog()``. Retrieve or register the compiled program in ``initializeActorResources()`` via ``MGLResourcesManager::generateEffectProgram()``. Render methods ^^^^^^^^^^^^^^ Override whichever applies: * ``renderToCurrentContext()``: 3-D geometry in world space (lon/lat/pressure). * ``renderToCurrentFullScreenContext()``: full-screen 2-D overlay; enable with ``setActorSupportsFullScreenVisualisation(true)`` in the constructor. * ``renderToCurrentContextUiLayer()``: 2-D UI elements on top of the 3-D view. Registration ^^^^^^^^^^^^ In ``src/system/applicationconfiguration.cpp``, ``MApplicationConfigurationManager::registerActorFactories()``: .. code-block:: c++ #include "actors/myactor.h" // inside registerActorFactories(): glRM->registerActorFactory("Base"); You can assign your actor to one of the existing groups: ``"Base"``, ``"Sections && volumes"``, ``"Atmospheric features"``, ``"Flow visualization"``, ``"Diagrams"``, ``"Annotation"``. Groups are used to categorize the actors, putting them in the corresponding context menu. Deriving from MNWPMultiVarActor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``MNWPMultiVarActor`` adds a managed list of ``MNWPActorVariable`` objects, each representing one gridded data field. The base class handles the *Add variable* / *Delete variable* UI, data requests, synchronisation with time controls, and transfer functions. Pure virtual methods ^^^^^^^^^^^^^^^^^^^^ ``supportedLevelTypes()`` returns the level types the actor can handle: .. code-block:: c++ QList MMyActor::supportedLevelTypes() { return { PRESSURE_LEVELS_3D, HYBRID_SIGMA_PRESSURE_3D, AUXILIARY_PRESSURE_3D, SINGLE_LEVEL }; } ``createActorVariable()`` is a factory called whenever the user adds a variable. Return a new instance of the appropriate typed variable class: .. code-block:: c++ MNWPActorVariable* MMyActor::createActorVariable( const MSelectableDataVariable& dataSource) { auto* var = new MNWP3DVolumeActorVariable(this); var->dataSourceID = dataSource.dataSourceID; var->levelType = dataSource.levelType; var->variableName = dataSource.variableName; return var; } Available typed variable classes (``src/gxfw/actorvartype.h``): * ``MGenericActorVariable``: generic; no render-mode presets. * ``MNWP2DSectionActorVariable``: 2-D section (shared base for horizontal and vertical sections). * ``MNWP2DHorizontalActorVariable``: horizontal section with methods for contouring and colouring. * ``MNWP2DVerticalActorVariable``: vertical section. * ``MNWP3DVolumeActorVariable``: 3-D volume / isosurface. Reacting to new data ^^^^^^^^^^^^^^^^^^^^ Called each time any variable in the actor has received a new data field (after a time step change, member change, and so on). Override it to update the state of your actor, maybe request or recompute new data based on the changed variable, and trigger a redraw: .. code-block:: c++ void MMyActor::dataFieldChangedEvent() { // Update vertex buffers and so on... emitActorChangedSignal(); // Redraw } Reacting to variable list changes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Override these slots to update internal structures when the variable list is modified: * ``onAddActorVariable(MNWPActorVariable *var)``: a new variable was added. * ``onChangeActorVariable(MNWPActorVariable *var)``: the user changed variable settings. * ``onDeleteActorVariable(MNWPActorVariable *var)``: a variable is about to be removed. Always call the base implementation first, for example ``MNWPMultiVarActor::onAddActorVariable(var);``. Accessing data in the render method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``variables`` list (``QList``) holds all registered variables. Always check ``hasData()`` before accessing ``grid``: .. code-block:: c++ void MMyActor::renderToCurrentContext(MSceneViewGLWidget *sceneView) { for (MNWPActorVariable *baseVar : variables) { if (!baseVar->hasData()) continue; auto *var = static_cast(baseVar); // var->grid is the current MStructuredGrid // var->transferFunction is the active transfer function // ... bind shader, upload grid to texture, draw ... } } Registration is the same as for ``MActor``. Add the include and factory call in ``applicationconfiguration.cpp``. .. note:: If you think your new actor would be useful to other Met.3D users, we would be happy to include it in the main codebase. Please consider submitting a `merge request on GitLab`_. .. _`merge request on GitLab`: https://gitlab.com/wxmetvis/met.3d/-/merge_requests .. _actor_icon: Adding an icon for the actor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you would like to create a new icon for a (new) actor, make sure that the new toolbar icon * is grayscale, * has a size of 112 x 112 pixels, * does not contain text or letters, * does not contain fine structures, * is simple and conveys the functionality of the actor in an abstract way. You can create such icons using a variety of software solutions. We recommend creating the icon as a vector graphic using `Inkscape `_, and exporting it as a PNG file with an optional alpha channel. The icons are located in the repository in ``resources/icons/actors/``. Add the PNG file there and update ``staticIconPath()`` to return the Qt resource path, e.g.: .. code-block:: c++ static QString staticIconPath() { return ":/icons/actors/myactor.png"; }