# Custom mesh classes

    library(fmesher)

## Minimal interface

Users and package developers can add `fmesher` support to their own classes. A minimal interface needs to define `fm_dof()` and `fm_basis()` methods. Assuming the class is called `custom`, the methods should be named `fm_dof.custom()` and `fm_basis.custom()`.

- The `fm_dof.custom(x)` method must take an object `x` of class `custom` and return the number of degrees of freedom of the function space.

- The `fm_basis.custom(x, loc, ..., full = FALSE)` method must take an object `x` of class `custom` and return a `sparseMatrix` or `Matrix` matrix with each column containing the basis function evaluated at the locations determined by `loc`.

  The type of `loc` may be any type (or types) that is supported by the custom class.

  The `...` part can include further named arguments specific to the custom class. These must be optional arguments so that `fm_basis(x, loc)` works.

  When `full = TRUE`, a full `fm_basis` object must be returned, which is a list containing at least the basis matrix as `A`, and a logical vector, `ok`, indicating which `loc` values were valid evaluation points. The `A` matrix must be all-zero for invalid `loc`.

- With the above requirements fulfilled, the default `fm_evaluator()` and `fm_evaluate()` methods can be used to evaluate functions at any location, without the need for the user to define any further methods.

  Special `fm_evaluator.custom()` and `fm_evaluate.custom()` methods may be defined if needed, e.g. to support semi-automated output reformatting.

### Example: Harmonic function space of order n

    # Custom class for harmonic functions up to order `n`
    create_custom <- function(n) {
      stopifnot(n >= 0)
      structure(
        list(n = n),
        class = "custom"
      )
    }
    fm_dof.custom <- function(x) {
      # Return the number of degrees of freedom
      1L + 2L * x[["n"]]
    }
    fm_basis.custom <- function(x, loc, ..., full = FALSE) {
      # Return the evaluated basis functions
      A <- Matrix::Matrix(0.0, NROW(loc), fm_dof(x))
      ok <- !is.na(loc)
      A[ok, 1L] <- 1.0
      for (k in seq_len(x[["n"]])) {
        A[ok, 2 * k] <- cos(2 * pi * k * loc[ok])
        A[ok, 2 * k + 1L] <- sin(2 * pi * k * loc[ok])
      }
      result <- structure(
        list(
          A = A,
          ok = ok, # Required prior to version 0.2.0.9003
          loc = loc
        ),
        class = "fm_basis"
      )

      # Use the fm_basis method to extract the A matrix if full is FALSE:
      fm_basis(result, full = full)
    }

Note: From version `0.2.0.9004`, the `fm_basis.matrix`, `fm_basis.Matrix`, and `fm_basis.list` methods provide an easier way to construct the `fm_basis` object, by creating the object and optionally extracting `A` in a single call:

    # 'matrix' and 'Matrix' methods:
    fm_basis(
      A = A,
      ok = ok, # If missing or NULL, inferred to be all TRUE
      loc = loc, # Optional additional content
      full = full
    )
    # 'list' method:
    fm_basis(
      list(
        A = A,
        ok = ok, # If missing or NULL, inferred to be all TRUE
        loc = loc
      ),
      full = full
    )

#### Registering the methods

These S3 methods must be registered with the `S3method()` function in scripts, and with special NAMESPACE tags in packages. In a script, one should use

    .S3method("fm_dof", "custom", "fm_dof.custom")
    .S3method("fm_basis", "custom", "fm_basis.custom")

In a package, if R is version 3.6 or newer, one can use roxygen2 tags

    #' @rawNamespace S3method(fmesher::fm_dof, custom)
    #' @rawNamespace S3method(fmesher::fm_basis, custom)

or before each method, use `@exportS3Method`, like this:

    #' @title Degrees of freedom for custom mesh
    #' @description the number of degrees of freedom
    #' # The rest of the documentation goes here
    #' @exportS3method fmesher::fm_dof
    fm_dof.custom <- function(x) {
      1L + 2L * x[["n"]]
    }

which semi-automates it.

We can the use the new methods with

    m <- create_custom(2)

    # How many latent variables are needed?
    fm_dof(m)
    #> [1] 5

    # Evaluate the basis functions at some locations:
    fm_basis(m, seq(0, 1, length.out = 6))
    #> 6 x 5 sparse Matrix of class "dgCMatrix"
    #>
    #> [1,] 1  1.000000  .             1.000000  .
    #> [2,] 1  0.309017  9.510565e-01 -0.809017  5.877853e-01
    #> [3,] 1 -0.809017  5.877853e-01  0.309017 -9.510565e-01
    #> [4,] 1 -0.809017 -5.877853e-01  0.309017  9.510565e-01
    #> [5,] 1  0.309017 -9.510565e-01 -0.809017 -5.877853e-01
    #> [6,] 1  1.000000 -2.449294e-16  1.000000 -4.898587e-16
    fm_basis(m, seq(0, 1, length.out = 6), full = TRUE)
    #> fm_basis object
    #>   Projection matrix (A): 6-by-5
    #>   Valid evaluations (ok): 6 out of 6
    #>   Additional information: loc

    # Check if missing values are handled correctly:
    fm_basis(m, c(0.1, NA, 0.2))
    #> 3 x 5 sparse Matrix of class "dgCMatrix"
    #>
    #> [1,] 1 0.809017 0.5877853  0.309017 0.9510565
    #> [2,] . .        .          .        .
    #> [3,] 1 0.309017 0.9510565 -0.809017 0.5877853
    fm_basis(m, c(0.1, NA, 0.2), full = TRUE)
    #> fm_basis object
    #>   Projection matrix (A): 3-by-5
    #>   Valid evaluations (ok): 2 out of 3
    #>   Additional information: loc

## Expanded implementations

The main additional method that can be defined is the `fm_int()` integration scheme method. This must have the call structure `fm_int.custom(domain, samplers = NULL, name = "x", ...)`.

- The `domain` argument is the `custom` class object over which to integrate.
- The `samplers` argument is any object, typically an `sf` or `tibble` specifying one or more subsets of the domain, e.g. polygons. When `NULL`, the entire domain should be integrated.
- The `name` argument is a character string specifying the name of the integration point variable.
- The `...` arguments can be augmented with further optional arguments, e.g. options controlling the integration scheme construction and/or the output format.

`out <- fm_int(domain, samplers, name)` should return a `data.frame`, `tibble`, or `sf` object with integration points in a column with the name indicated by , and additional columns `weight` with corresponding integration weights, and a `.block` column.

- The column format should be compatible with `fm_basis(domain, out[[name]])`.
- The `.block` column should be an integer vector indicating which subdomain each integration point belongs to, usable by `fm_block_eval()`:

    #>      x weight .block
    #> 1  0.0   0.05      1
    #> 2  0.1   0.10      1
    #> 3  0.2   0.10      1
    #> 4  0.3   0.05      1
    #> 5  0.3   0.05      2
    #> 6  0.4   0.10      2
    #> 7  0.5   0.05      2
    #> 8  0.5   0.05      3
    #> 9  0.6   0.10      3
    #> 10 0.7   0.10      3
    #> 11 0.8   0.10      3
    #> 12 0.9   0.10      3
    #> 13 1.0   0.05      3

    values <- fm_evaluate(
      m,
      field = c(1, 1, 0, 0, 0),
      loc = out[["x"]]
    )

    # Blockwise aggregation:
    fm_block_eval(
      block = out$.block,
      weights = out$weight,
      values = values
    )
    #> [1] 0.44635255 0.05364745 0.50000000

    # Exact integrals:
    c(0.3, 0.2, 0.5) +
      c(
        sin(2 * pi * 0.3),
        sin(2 * pi * 0.5) - sin(2 * pi * 0.3),
        sin(2 * pi) - sin(2 * pi * 0.5)
      ) / (2 * pi)
    #> [1] 0.45136535 0.04863465 0.50000000