@einsum M[i,j] := A[i,k] * B[k,j]
Or faster with @tensor from https://github.com/Jutho/TensorOperations.jl & the same notation. I'm not aware of any attempt to distinguish co-vectors; you can have g = Diagonal([-1,1,1,1]) and insert A[i,k]g[k,k′]B[k′,j] and this is handled efficiently.Well to distinguish covectors you need to define something like a manifold or just any canonical mapping between the tangent and cotangent space and then when you define your first few tensors you define them in the scope of that mapping.
There is some generalization to gauge fields where a bijective canonical mapping may not be possible in general, but I never studied that extensively.
If you had reason to do so, it would be easy to bolt something onto the front of @tensor etc. which understood say A^[i]_[j] * B^[j,k] . Something similar would not be so hard to do to np.einsum either, just messing with the string... it could take exactly your "email Latex notation". In fact I'm a bit surprised that I can't find someone who's done this, in a few minutes' googling. I did find that opt-einsum has a notation in which the indices at least follow the array:
https://optimized-einsum.readthedocs.io/en/latest/input_form...
You have honestly piqued my curiosity towards Julia. I will need to find an opportunity to dig in.
I made a C++ operator overloading implementation of Ricci calculus once upon a time. That way you could define your tensors and then write statements like:
Anyways, I find the domain specific language of this particular numpy function to be a bit abhorrent. Why not write "ik,kj->ij" as "M_ij = A_ik * B_kj" and make it much more obvious what is happening instead of requiring this understanding. Heck, if you used kwargs, you could even avoid passing the same argument multiple times like in the quadratic form example: Here I am leaving out that "x = ..." prefix in the DSL since you can take an ordering convention and a scalar has no slots anyways.