Completing KroneckerProduct

KroneckerProduct[] skeletal code

Now we fill in the parts of KroneckerProduct other than the class definition line and its docstring.

None of the other built-in Mathics3 Functions serve as good model to copy from, so we’ll use a combination Transpose and PauliMatrix. The former function is selected because it takes a Matrix parameter, and PauliMatrix function because it is a SymPy function which was recently created at the time of this writing.

Here is the first cut for KroneckerProduct:

class KroneckerProduct(SympyFunction):
    """
    ... as before
    """

    attributes = A_PROTECTED | A_READ_PROTECTED
    summary_text = "Kronecker product"
    sympy_name = "physics.quantum.TensorProduct"

    def eval(self, mi: ListExpression, evaluation: Evaluation):
        "KroneckerProduct[mi__]"
        sympy_mi = [to_sympy_matrix(m) for m in mi.elements]
        return from_sympy(TensorProduct(*sympy_mi))

In the above, I needed to add imports for SympyFunction, ListExpression, Evaluation and other places. Examples of these of of course can be adapted from the place where you copied the code from.

Now let’s go over the new parts…

KroneckerProduct[] class variables

As in Undefined, we set the attributes of function KroneckerProduct by setting the class variable attributes.

Here, in addition to being “Protected”, it is also “ReadProtected” and the way we indicate both attributes is to bit-or, |, the to attribute values A_PROTECTED and A_READ_PROTECTED.

The purpose of the class value summary_text is described when we described adding Undefined so we won’t go over it here. Again, there is a unit test that checks that it exists.

However if you go into Django and type “Vector Space Operations” in the documentation section and you should see KroneckerProduct appear along with the summary text we just added.

There is a class variable sympy_name which we set, which here is not necessary. If there were no evaluation method, this is the function name that would be used by the generic evaluation method of the SympyFunction class. I added it here, because in the future we may want to have a way to list correspondences between Mathics3 builtin function names and SymPy function names.

KroneckerProduct[] evaluation method

Here we will focus on the evaluation method. Confusingly, the name needs to start with eval:

def eval(self, mi: ListExpression, evaluation: Evaluation):
    "KroneckerProduct[mi__List]"
    sympy_mi = [to_sympy_matrix(m) for m in mi.elements]
    return from_sympy(TensorProduct(*sympy_mi))

There is only one form that needs to be implemented so we can do this with one method.

The docstring for the method

"KroneckerProduct[mi__]"

in fact is a Mathics3 pattern that is matched in the apply phase of Evaluation. It is used to determine which, if any, evaluation method inside the KroneckerProduct class should be called.

By convention and to make things simple, we use the name name in the pattern, mi as we did in the class docstring description. However the name(s) that are used in the pattern must also appear in the same order in Python definition. Since we used mi__ in the pattern in the definition docstring, we must use mi as the first parameter in the evaluation method. In Mathics3 __ indicates a function definition with a nonzero number of arguments. See the BlankSequence for more information.

When we have such a pattern object, the Mathics3 object when represented in Python will be of type ListExpression.

Evaluation methods also have an parameter of type Evaluation after the parameters that are listed the definition docstring. By convention we always name this parameter evaluation.

Inside the function the first thing we do is to convert the elements of the ListExpression into a Python list of SymPy Matrix values.

In this simple, first-draft function we don’t do any validation of the arguments. We should come back later and fill this in.

After we have something that we can pass on to SymPy we call the SymPy function that corresponds to the Mathics3 function. And the return value is converted back into Mathics3 for return.

Again we don’t do checking on the return value from SymPy or from the conversion back to Mathics’ internal Element-based object. In a future version we should do this.

We have a number of Mathics3 builtin functions that work like this. One thing to notice is that evaluation this way causes a great deal of conversions from Mathics3 into SymPy followed by conversions out of Sympy into Mathics3. Without other mechanisms like caching computed values this can be slow. Even with caching, this can be slow.

KroneckerProduct[] evaluation method, second version

Instead of using pattern matching to ensure we have a list of lists, we can generalize the pattern to match anything and then do the check in Python.

def eval(self, mi: ListExpression, evaluation: Evaluation):
    "KroneckerProduct[mi__]"

    #  Code in Python mi__List matching
    if not all(m.head is for m in mi.elements):
       return None

    sympy_mi = [to_sympy_matrix(m) for m in mi.elements]

This might be a little bit faster at the expense, code that might be harder to maintain. Also, while this is faster in the current implementation, over time we may improve the interpreter so that pattern matching performs the same kind of steps that the Python code uses. If or when that is done, the difference would be negligible.

Keep in mind that while these kinds of tricks may add a small boost in performance, there are also some downsides; in some cases this kind of thing might even have to be undone in the future.

A full version of the basic implementation can be found in this commit.

Next: