In this section, we will describe how the afwizard
data model can be extended with custom backends. Such extensions can be done from your project that depends on afwizard
- you do not necessarily need to contribute your custom backend to afwizard
for it to integrate with the rest of afwizard
.
In this documentation we will treat the following use case: You do have an executable myfilter
that performs ground point filtering. It accepts an LAS input filename, an output filename and floating point finetuning value as command line arguments. You want to expose this filtering backend in afwizard
.
Note
This is an advanced topic. A certain familiarity with object-oriented Python programming is required to understand and productively use this feature.
Custom backends are created by inheriting from the afwizard.filter.Filter
class. When inheriting, your derived class needs to specify an identifier
that will be used to register your derived class with the base class. Having done that you only need to implement two methods on the derived class: schema
describes the configuration space of your custom backend and execute
contains the execution logic of your backend:
import afwizard
import shutil
import subprocess
class MyBackend(afwizard.filter.Filter, identifier="mybackend"):
@classmethod
def schema(cls):
# The configuration schema here follows the JSONSchema standard.
return {
"anyOf": [
{
"type": "object",
"title": "My Filtering Backend",
"properties": {
"_backend": {
"type": "string",
"const": "mybackend",
},
"myparameter": {
"type": "number",
"default": 0.5,
"title": "My tuning parameter",
}
},
}
]
}
def execute(self, dataset):
# Ensure that the dataset is of type DataSet (maybe applying conversion)
dataset = afwizard.DataSet.convert(dataset)
# Create a temporary filename for the output
filename = afwizard.paths.get_temporary_filename("las")
# Run the filter program as a subprocess
subprocess.run(
["myfilter", dataset.filename, filename, self.config["myparameter"]],
check=True,
)
# Construct a new DataSet object with the result
return afwizard.DataSet(filename, spatial_reference=dataset.spatial_reference)
@classmethod
def enabled(cls):
# We only enable this backend if the executable 'myfilter' is in our path
return shutil.which("myfilter") is not None
The implementation of schema
needs to return a dictionary that follows the JSONSchema specification. If you do not know JSONSchema, you might want to read this introduction guide: Understanding JSONSchema. We require the schema for your filter to be wrapped into an anyOf
rule that allows schema composition between backends. This anyOf
rule does also allow you to expose multiple filters per backend class (e.g. because they share the same execution logic). Each of the schemas contained in the anyOf
rule must be of type object
and define at least the _backend
property as shown in the code example.
The execute
method implements the core functionality of your filter. It is passed a dataset and returns a filtered dataset. We first assert that we are dealing with a dataset that is represented by a LAS file by converting it to afwizard.DataSet
. The actual execution is done using subprocess.run
.
The enabled
method in the above can be used to exclude the custom backend if some condition is not met e.g. the necessary executable was not found. This methods defaults to True
.
As backend classes register themselves with the base class, it is only necessary to ensure that the module that contains the class has been imported before other functionality of afwizard
is used. This can e.g. be done from __init__.py
.
In above example, the ground point filtering algorithm operated directly on LAS files from the file system. Other backends might operate on other data representations, e.g. OPALS is working with its own OPALS Data Manager object. If your backend should work on a different representation, you can inherit from afwizard.DataSet
and implement the following methods which are shown as no-op here:
class CustomDataSet(afwizard.DataSet):
@classmethod
def convert(cls, dataset):
# Make sure that conversion is idempotent
if isinstance(dataset, CustomDataSet):
return dataset
# Here, you can do custom things
return CustomDataSet(dataset.filename, dataset.spatial_reference)
def save(self, filename, overwrite=False):
# Save the dataset as LAS - using DataSet here
return DataSet.convert(self).save(filename, overwrite=overwrite)
The convert
method will be used by filters to ensure the correct dataset representation as shown in above example.