<jupyter_start><jupyter_text>Linear transformationsWhen working in regular vector spaces, a common tool is a linear transformation, typically in the form of a matrix.While geometric algebra already provides the rotors as a means of describing transformations (see [the CGA tutorial section](cga/index.ipynbOperations)), there are types of linear transformation that are not suitable for this representation.This tutorial leans heavily on the explanation of linear transformations in GA4CS, chapter 4. It explores the [clifford.transformations](../api/transformations.rst) submodule.  Vector transformations in linear algebraAs a brief reminder, we can represent transforms in $\mathbb{R}^3$ using the matrices in $\mathbb{R}^{3 \times 3}$:<jupyter_code>import numpy as np

rot_and_scale_x = np.array([
    [1, 0, 0],
    [0, 1, -1],
    [0, 1, 1],
])<jupyter_output><empty_output><jupyter_text>We can read this as a table, where each column corresponds to a component of the input vector, and each row a component of the output:<jupyter_code>def show_table(data, cols, rows):
    # trick to get a nice looking table in a notebook
    import pandas as pd; return pd.DataFrame(data, columns=cols, index=rows)
show_table(rot_and_scale_x, ["$\mathit{in}_%s$" % c for c in "xyz"], ["$\mathit{out}_%s$" % c for c in "xyz"])<jupyter_output><empty_output><jupyter_text>We can apply it to some vectors using the `@` matrix multiply operator:<jupyter_code>v1 = np.array([1, 0, 0])
v2 = np.array([0, 1, 0])
v3 = np.array([0, 0, 1])

(
    rot_and_scale_x @ v1,
    rot_and_scale_x @ v2,
    rot_and_scale_x @ v3,
)<jupyter_output><empty_output><jupyter_text>We say this transformation is linear because $f(a + b) = f(a) + f(b)$:<jupyter_code>assert np.array_equal(
    rot_and_scale_x @ (2*v1 + 3*v2),
    2 * (rot_and_scale_x @ v1) + 3 * (rot_and_scale_x @ v2)
)<jupyter_output><empty_output><jupyter_text>Multivector transformations in geometric algebraHow would we go about applying `rot_and_scale_x` in a geometric algebra? Clearly we can apply it to vectors in the same way as before, which we can do by unpacking coefficients and repacking them:<jupyter_code>from clifford.g3 import *

v = 2*e1 + 3*e2
v_trans = layout.MultiVector()
v_trans[1,], v_trans[2,], v_trans[3,] = rot_and_scale_x @ [v[1,], v[2,], v[3,]]
v_trans<jupyter_output><empty_output><jupyter_text>However, in geometric algebra we don't only care about the vectors, we want to transform the the higher-order blades too. This can be done via an outermorphism, which extends $f(a)$ to $f(a \wedge b) = f(a) \wedge f(b)$. This is where the `clifford.transformations` submodule comes in handy:<jupyter_code>from clifford import transformations

rot_and_scale_x_ga = transformations.OutermorphismMatrix(rot_and_scale_x, layout)<jupyter_output><empty_output><jupyter_text>To apply these transformations, we use the `()` operator, rather than `@`:<jupyter_code>rot_and_scale_x_ga(e12)
# check it's an outermorphism
rot_and_scale_x_ga(e1) ^ rot_and_scale_x_ga(e2)<jupyter_output><empty_output><jupyter_text>It shouldn't come as a surprise that applying the transformation to the psuedoscalar will tell us the determinant of our original matrix - the determinant tells us how a transformation scales volumes, and `layout.I` is a representation of the unit volume element!<jupyter_code>np.linalg.det(rot_and_scale_x), rot_and_scale_x_ga(layout.I)<jupyter_output><empty_output><jupyter_text>Matrix representationUnder the hood, clifford implements this using a matrix too - it's just now a matrix operating over all of the basis blades, not just over the vectors. We can see this by looking at the _private_ `_matrix` attribute:<jupyter_code>show_table(rot_and_scale_x_ga._matrix, ["$\mathit{in}_{%s}$" % c for c in layout.names], ["$\mathit{out}_{%s}$" % c for c in layout.names])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1036001036001036, 'fraction_numerical': 0.013468013468013467, 'mean_word_length': 6.00907441016334, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '43835', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 1325, 'n_tokens_neox': 1264, 'n_words': 424, 'path': 'docs/tutorials/linear-transformations.ipynb', 'repo_name': 'corcoted/clifford', 'size': '3861'}