Writing a component¶
A component is a directory under components/, a CMake call that registers it, and a spec that
documents its options. Registered, it is a wmtk_app application, a Python function and a C++
library (wmtk::<name>) at once. This page is the checklist, in the order the work goes; the
existing components are the examples, shortest edge collapse the smallest.
1. The directory¶
components/<name>/wmtk/components/<name>/
CMakeLists.txt add_component, the sources, the tests
<name>_spec.json the options
<name>.hpp, <name>.cpp the two entry points
<Mesh>.h, <Mesh>.cpp the mesh class, its hooks and its passes
tests/
CMakeLists.txt add_component_test, the test sources
test_<name>.cpp
The three <name> levels are what the include path needs: add_component adds
components/<name> to the include directories, so the header is
#include <wmtk/components/<name>/<name>.hpp>, as the generated headers write it.
2. CMake¶
The component's CMakeLists.txt, after
shortest edge collapse's:
set(COMPONENT_NAME shortest_edge_collapse)
add_component(${COMPONENT_NAME})
if(NOT ${WMTK_ENABLE_COMPONENT_${COMPONENT_NAME}})
return()
endif()
set(SRC_FILES
ShortestEdgeCollapse.cpp
ShortestEdgeCollapse.h
shortest_edge_collapse.hpp
shortest_edge_collapse.cpp
)
target_sources(wmtk_${COMPONENT_NAME} PRIVATE ${SRC_FILES})
target_link_libraries(wmtk_${COMPONENT_NAME} PUBLIC
wmtk::toolkit
)
if(${WMTK_COMPONENT_TESTS})
add_subdirectory(tests)
endif()
and one line in components/CMakeLists.txt:
add_component(<name>)
(components/cmake/add_component.cmake)
defines the option WMTK_ENABLE_COMPONENT_<name> (on), the library wmtk_<name> and its alias
wmtk::<name>, embeds the spec (jse_add_spec), appends the component to the generated headers
in app/, and links it into wmtk_app, the Python module and the integration tests
(the life of a component call).
Reconfigure after adding it: the headers are written at configure time.
A component that uses another links it, as tetwild and simwild link wmtk::shortest_edge_collapse
to simplify their input.
3. The entry points¶
In <name>.hpp, in namespace wmtk::components::<name>, exactly these two signatures, which the
generated maps take the address of:
void <name>(nlohmann::json json_params);
ComponentResult <name>(nlohmann::json json_params, const MeshMap& meshes);
Write one run(nlohmann::json, io::ComponentIO&) and call it from both, as every component does
(the life of a component call). In run:
- start with
component_io.prepare(json_params, "<name>"); - validate and inject the defaults with the embedded spec,
jse::embed::wmtk_<name>_spec::<name>_spec::spec()(orutils::verify_and_setup_logger, which also openslog_file); - get every input through
component_io(path,paths,triangles,edges,mesh), so the memory route reads theMeshMapand the file route the files; - on the memory route, put the output meshes in
component_io.result.firstand write no file; put what a caller may want to check (counts, energies, times) in the report,component_io.result.second; - throw with
log_and_throw_errorwhen the result is not what was asked for, at least whenthrow_on_failis set: the integration tests only see exceptions.
The memory route's contract, input by input, is in component I/O; add the
component's row to its table. If a mesh selector has a new name, add it to the list of keys
call_component converts in
python/src/mesh_conversion.cpp,
or the Python memory route will not pass that mesh.
4. The spec¶
<name>_spec.json is a list of json-spec-engine
rules. It is what validates the options, and it is their documentation: wmtk_app --options,
help(wildmeshing.<name>), the reference and the
JSON Schemas are all generated from it. The root rule names the required
and optional keys and says what the component does:
{
"doc": "Simplifies a triangle mesh by collapsing its shortest edges first (Hoppe 1996, Progressive Meshes), until the vertex count reaches the target. With a positive eps_rel, every collapse keeps the surface inside an envelope around the input.",
"pointer": "/",
"type": "object",
"required": ["application", "input"],
"optional": ["output", "input_dir", "num_threads", "target_rel", "eps_rel", "..."]
}
and every option has a rule of its own, with a doc:
{
"pointer": "/target_rel",
"type": "float",
"default": 0.1,
"doc": "Target number of vertices relative to input number."
}
- The first sentence of the root
docis the component's line inwmtk_app --list; the first sentence of an option'sdocis its line in--options. Make both stand alone. - Give every optional key a
default;inject_defaultsfills it in, andruncan then readjson_params["..."]without checking. - Every spec has
application, with"options": ["<name>"]. Give itinput_dirtoo, whichwmtk_appsets, and, where they apply, the keys most components share:output,num_threads,log_file,report,throw_on_fail. - Options for diagnostics, unfinished work or the tests start with
DEBUG_,EXPERIMENTAL_orTESTING_: the reference folds them away. - A key the spec does not have is accepted and ignored (with a warning from
wmtk_appand the bindings), unless the component setsspec_engine.strict = true. So do not read a key that is not in the spec.
python docs/tools/gen_reference.py --check fails when the root rule or any other rule has no
doc, when the spec has no entry in docs/components.yml, or when that entry names an option
the spec does not have, a group that does not exist or a tutorial that is not there.
5. Unit tests¶
tests/CMakeLists.txt:
if(NOT BUILD_TESTING)
return()
endif()
if(NOT ${WMTK_ENABLE_COMPONENT_TEST_${COMPONENT_NAME}})
return()
endif()
add_component_test(${COMPONENT_NAME})
set(SRC_FILES
test_<name>.cpp
)
target_sources(wmtk_test_${COMPONENT_NAME} PRIVATE ${SRC_FILES})
add_component_test(<name>)
(components/cmake/add_component_test.cmake)
builds the Catch2 executable wmtk_test_<name>, linked with the component, wmtk::toolkit and
wmtk::data (for WMTK_DATA_DIR), and registers it with ctest as one entry of the same name. A
test executable that is built but not registered never runs, in CI or locally; check that it is:
Tag the cases too slow for a Debug build release_only (testing). If
the component has a memory route, add a case to
tests/test_component_io.cpp
under #ifdef WMTK_TEST_<name>, and the component to the list in components/CMakeLists.txt
that links wmtk_component_io_tests and defines that macro.
6. An integration config¶
An integration test is a whole run from a JSON file in the
data repository: integration_tests/<name>_<model>.json,
with its paths relative to that directory ("input": "../models/Octocat.obj"). Reuse a model
from models/ when one fits; add a small one otherwise. Set throw_on_fail (or the component's
equivalent) so that a bad result fails the test, and keep the run short: every Release CI job
runs every listed config.
- Add the config, and any new model, to data2, and list the config in
integration_tests/integration_tests.json; open a pull request there. - Once it is merged, set
GIT_TAGincmake/wmtk_data.cmaketo the data2 commit, with a line in the comment above it saying what changed (contributing).
7. The documentation¶
-
An entry in
docs/components.yml:shortest_edge_collapse: title: Shortest edge collapse group: surface tutorial: tutorials/shortest_edge_collapse.md key_options: [input, output, target_rel, eps_rel, boundary_eps_rel, use_link_condition, use_sample_envelope] papers: - "[Hoppe 1996, Progressive Meshes](https://hhoppe.com/pm.pdf)"groupis one of thegroupsat the top of the file; the reference page of awipcomponent opens with a work-in-progress warning. The reference page and its place in the navigation are generated from this entry and the spec;key_optionscome first on it. 2. A tutorial,docs/tutorials/<name>.md, in the order writing documentation gives, and its entry in thenavofmkdocs.yml. 3. Its examples,docs/examples/{json,python,cpp}/<name>.*, which the tests build and run (writing documentation).
8. Python¶
Nothing to do: the module defines wm.<name> with both overloads from the generated maps, and
its docstring from the spec. Classes or functions of the component's own go in
python/src/<name>_bindings.cpp, defining wmtk::python::bind_<name>(pybind11::module_&);
add_component compiles the file when it exists (the life of a component
call).
Before the pull request¶
- [ ]
wmtk_app --listshows the component, andwmtk_app --options <name>its options. - [ ]
ctest --test-dir build -Nlistswmtk_test_<name>, and it passes in Debug and Release. - [ ]
python docs/tools/gen_reference.py --checkreports no problems. - [ ]
mkdocs build --strictbuilds, with the new tutorial. - [ ] The integration config runs with
wmtk_app -j, and the data pin names the data2 commit that has it. - [ ] The code is formatted (contributing).