Vector Add

Context

Example using vector add with differentiable inputs and jacobians.

Example Tesseract (examples/vectoradd)

In this example of vectoradd, we have the 2 vectors both as Differentiable Arrays. Inputs must be Differentiable if we wish to write a Jacobian function for the Tesseract.

We also have an example of a basic apply function and how it utilizes the inputs.

class InputSchema(BaseModel):
    a: Differentiable[Array[(None,), Float32]] = Field(
        description="An arbitrary vector."
    )
    b: Differentiable[Array[(None,), Float32]] = Field(
        description="An arbitrary vector. Needs to have the same dimensions as a."
    )
    s: Differentiable[Float32] = Field(description="A scalar.", default=1)
    normalize: bool = Field(
        description="True if the output should be normalized, False otherwise.",
        default=False,
    )

    @model_validator(mode="after")
    def validate_shape_inputs(self) -> None:
        if self.a.shape != self.b.shape:
            raise ValueError(
                f"a and b must have the same shape. "
                f"Got {self.a.shape} and {self.b.shape} instead."
            )
        return self
def apply(inputs: InputSchema) -> OutputSchema:
    """Multiplies a vector `a` by `s`, and sums the result to `b`."""
    result = inputs.a * inputs.s + inputs.b

    if inputs.normalize:
        norm = np.linalg.norm(result, ord=2)
        result /= norm

    return OutputSchema(result=result)

Bonus: Jacobian

As previously mentioned, the inputs are marked as Differentiable in order for us to write a Jacobian function.

In order for us to do that, we must also write the abstract eval function.

def abstract_eval(abstract_inputs):
    """Calculate output shape of apply from the shape of its inputs."""
    result_shape = abstract_inputs.a
    assert result_shape is not None

    return {"result": result_shape}
def jacobian(
    inputs: InputSchema,
    jac_inputs: set[str],
    jac_outputs: set[str],
):
    assert jac_outputs == {"result"}
    n = len(inputs.a)

    partials = {}
    partials["a"] = np.eye(n) * inputs.s
    partials["b"] = np.eye(n)
    partials["s"] = inputs.a

    if inputs.normalize:
        result = inputs.a * inputs.s + inputs.b
        norm = np.linalg.norm(result, ord=2)
        partials["a"] = (
            partials["a"] / norm
            - np.outer(result, (inputs.a + inputs.s * inputs.b)) / norm**3
        )
        partials["b"] = (
            partials["b"] / norm
            - np.outer(result, (inputs.s * inputs.a + inputs.b)) / norm**3
        )
        partials["s"] = partials["s"] - (inputs.a + inputs.s * inputs.b) / norm**3 * (
            inputs.s * inputs.a * inputs.a + 2 * inputs.a * inputs.b
        )

    jacobian = {"result": {v: partials[v] for v in jac_inputs}}
    return jacobian