In connection with tensors, I would like to be able to treat arbitrary symmetries (positive permutations) and antisymmetries (negative permutations) of the entries of an Array.
An example is the Riemannian curvature tensor which has the following positive and negative permutations:
pPerms := {[3,4,1,2]};
nPerms := {[2,1,3,4],[1,2,4,3]};
In order to create an indexing function which, among other things, must determine whether or not an entry [a,b,c,d] can be assigned some (nonzero) number, it seems necessary first to figure out what are the positive and negative permutations of [a,b,c,d]. For that purpose I have come up with the following procedure (which is valid for any rank of the tensor, and any sets of symmetries and antisymmetries):
permsPosNeg := proc(
ind::list,posPerms::'set'(list),negPerms::'set'(list)
)
local perms,posMaps,negMaps,posInds,negInds,posInds_,negInds_;
posMaps,negMaps := seq(map((perm::list) ->
(x::list) -> [seq(op(perm[i],x),i=1..nops(perm))]
,perms),perms in [posPerms,negPerms]);
posInds ,negInds := {ind},{};
posInds_,negInds_ := {} ,{};
while (posInds <> posInds_) or (negInds <> negInds_) do
posInds_ := posInds;
negInds_ := negInds;
posInds := posInds
union map(posMaps,posInds)[]
union map(negMaps,negInds)[];
negInds := negInds
union map(posMaps,negInds)[]
union map(negMaps,posInds)[];
end do;
posInds,negInds
end proc:
which is used as in the following two examples:
p and n has empty intersection:p,n := permsPosNeg([1,2,3,4],pPerms,nPerms); p intersect n;
p and n has nonempty intersection:p,n := permsPosNeg([1,1,3,4],pPerms,nPerms); p intersect n;
My question, at long last, is: can permsPosNeg be improved upon, or is there some altogether different method that is better?
Small improvement
The only improvement I can think of right now is to change the first line to
posMaps,negMaps := seq(map((perm::list) -> unapply([seq(x[perm[i]],i=1..nops(perm))],x),perms),perms in [posPerms,negPerms]);
which basically just makes things faster for large permutations. Use
printlevel := 10to see the change this makes. I am basically using Maple's expression manipulation tools as a cheap partial evaluator to generate inline code instead of going through lexical scoping to achieve the same ends.Nicely looking maps
Thanks, Jacques. Not only does it make things faster for larger permutations (something I take your word for), but it also makes
posMapsandnegMapslook nicely simple (as seen usingprintlevel := 10).Iteration worries
The blog entry Refactoring Maple code has yielded some very nice improvement of the first part of
permsPosNeg.However, what really worried me the most was the use of the
whileloop which by iteration builds the positive and negative permutations of the entryind. Is there any noniterative way of doing the same thing?efficiency
While theoretical improvements are nice in themselves, the practical concern of whether to improve the effiicency of this routine depends on how you intend to deploy it. My assumption is that this procedure would be part of a separate procedure that is called one time, with a particular set of symmetries, and is used to create an indexing function for that class of tensor. For example, for a tensor with four indices, you would create an index function for a 4 dimensional array with indices ranging from 1 to 4 (or similar). To do that, you'd use this routine to partition the 44 = 256 set of indices and then use that to create a special array that encodes the desired symmetry. The actual indexing function would use this array. Because you only have to generate that array once (the indexing function can be reused for any tensor with that symmetry class), inefficiencies in it construction may not be significant. This presumes that your goal is to actually compute with the tensors, and not merely study symmetry classes of tensors.
Maybe that wasn't clear. Here is a demo of the indexing technique I had in mind for a two-form (completely antisymmetric two-covector):
First, generate the special index table. I do that by hand here, rather than using your code. A zero entry (the default) indicates a zero component, otherwise the first entry in the sequence is the symmetry factor (-1,+1), the rest the indices of the stored component.
Create an index function that uses D2 (this general procedure remains the same for all tensors, you merely have to create it in a procedure that makes D2 local to it).
Now create a two-form, say F
F := Array('twoform',1..2,1..2,'storage=sparse'): F[1,1] := 0.; # just checking F[1,2] := 3: print(F); [ 0 3] [ ] [-3 0]Rank worries
Thanks for this very explicit post of yours.
What makes me feel a bit uncomfortable about your approach, though, is that I am trying to create a framework that works, in principle, for any rank of the tensor. Imagine, for instance, what happens for a rank
10tensor: in four dimensions it would yield4^10 = 1048576table entries.how will it be used?
The question of how the Array will eventually get used it important here.
Do you want to shift the costs to element assignment time, or access time? Do you want to pay efficiency costs in access/assignment time or in storage? Do you expect most or few entries to get accessed/assigned at some point?
I believe that rtable indexing-functions are flexible enough (sometimes with ingenuity required) to control those aspects and to choose where to put the cost.
acer
Good points
You have some very important points there. I am afraid that I will have to rethink what I want to achieve.
but
Wouldn't there be storage issues with any approach with that sized tensor? At least with this approach you only have to create one such large array; the actual tensor Arrays could be much smaller since all symmetries map to one location (be sure to use sparse Arrays). You can optimize the coding. Instead of including a scale factor (+1,-1), omit it and use a sequence to encode a positive symmetry and a list to encode an antisymmetry:
Modify the index function accordingly. Maybe you could use a second-level of indirection to compress the index table or put some 'intelligence' into the index procedure without requiring the full computation of symmetries with each access (hand-waving).
A more efficient storage method for the indirection table is to insert +1 for positive symmetries, -1 for antsymmetries, and use the sorted indices as the base location. That will encure a speed penalty for reading and writing, since you'll have to sort the indices. In our example
Sparse
Of course, you are right that any high ranked tensor would be problematic with respect to storage.
It may come as a complete surprise to you, but I have actually never used the sparse option of an Array. Quite obviously that is something I will have to remedy.
I begin to see the advantage of your approach, having only one indexing function (which may take up quite some memory, depending on rank and dimensionality) for each class of tensor (anti)symmetries, and sparse Arrays for the tensors (each taking up only little memory).
ignore
Don't pay too much attention to my last comment (which I added in an edit without too much thinking) about using sort. That mainly works with some classes of symmetries (in particular, with completely antisymmetric tensors, but there are better ways to handle them, use the much builtin index function, antisymmetric). Note that the index table I used in the example is also sparse, so that helps somewhat.
Sparse indexing table
You are correct that the indexing tables themselves can profit from the sparse indexing option, but only for antisymmetries of the tensor, because only antisymmetries imply only-zero-assignable entries.
PS: I am happy to be able to say, that I have quite often used the antisymmetric indexing function for both matrices, arrays, and tables. So, luckily, I am not completely lost in "indexing space", only partially.
Alternate approach
Here is an alternate approach you might consider. It assumes that you can decompose the symmetries of a tensor into group-wise symmetries and antisymmetries. For example, for the Riemannian curvature tensor, the indices 1 and 2, as well as 3 and 4, are anti-symmetric, while the groups [12] and [34] are symmetric. These two symmetries can be efficiently handled with Maple's builtin 'symmetric' and 'antisymmetric' index functions for tables. Here is a rough outline of what I have in mind.
Symmetries := module() export Anti,Symm; local anti,symm; anti := table('antisymmetric'): symm := table('symmetric'): # this can be improved... Anti := proc( L :: seq(list), $) local s,S; S := map2(`?[]`, anti, [L]); if ormap(`=`, S, 0) then return (0,[]); else (mul(sign(s),s=S) , [seq(`if`(sign(s)=-1 , op(-s) , op(s) ), s=S)] ); end if; end proc; Symm := proc( L :: seq(list), $) local s; [seq(op(symm[s[]]), s=[L])]; end proc; end module:Now, assign a function that uses the Anti and Symm exports to handle the symmetries of the curvature tensor. This procedure returns two elements, the computed sign (-1,0,1) and a list of indices (empty if sign is 0).
Riemannian([1,1,2,1]); 0, [] Riemannian([1,2,4,3]); -1, [1, 2, 3, 4] Riemannian([4,3,2,1]); 1, [1, 2, 3, 4] Riemannian([3,4,2,1]); -1, [1, 2, 3, 4]This example was nice because the anti-symmetric indices were conveniently grouped. However, isn't that usually the case? One might have to do an initial permutation given an arbitrary order.
A bonus of this approach is that it will happily work with symbolic indices, however, the actual permutation generated is session dependent:
To check Riemannian, do
R := Array((1..4)$4, ()->Riemannian([args])): # alas, Riemannian@`[]` doesn't work nops(map2(op,2,{entries(R)}))-1; 21which is as expected (we subtract 1 because the set included the empty list for the zero components). These symmetries don't encode the Bianchi identity, of course.
Very interesting
Thanks, Joe, for your very interesting approach which I will scrutinize. There are two immediate issues to contemplate:
Riemannian.The second issue is nicely connected with the first one: if some code exists that transforms any nontrivial (anti)symmetries into group-wise ones, then the answer to the former is affirmative. By nontrivial I mean an indexing function which allows nonzero entries somewhere. For instance, the following rank two example is clearly trivial:
addPermIndex("rankTwo",{[2,1]},{[2,1]}):But what about the following rank three example, which is symmetric in the first two indices, and antisymmetric in the last two?
addPermIndex("rankThree",{[2,1,3]},{[1,3,2]}):It is in fact trivial too (most fortunate because it is not group-wise, unless I have misunderstood your use of that term), but it is not obvious to me. Because higher rank examples will only make worse this ignorance of mine, some general method for settling this issue has to be established.
alas
I don't see how my approach would work with symmetries that "overlap". It assumes that the order of application does not matter. For your rank3 example, given, say [2,1,1], if we applied the 12 symmetry first we get [1,2,1], but then the 23 antisymmetry gives -[1,1,2]. However, if applied in the reverse order the result is 0 (which is, I presume, what you want).
But is that a "real" symmetry? Given say (shorter notation) we get (applying the two symmetries appropriately):
123 = 213 = -231 = -321 = 312 = 132 = -123
so any arbitrary indexes are zero. Oh, right, that's what you meant be trivial. Regardless, I suspect this approach won't work...
I agree
I have reached the very same conclusion: there cannot be any overlap between various (anti)symmetries. Using your idea of using (symmetric and antisymmetric) tables, I am now working along the following lines:
The (anti)symmetries come in different levels, from level one and upwards, the higher levels building upon the lower ones. I am considering using a notation in which, for instance, the indexing function of the Riemannian curvature tensor may be created as
addPermIndex("riemann", a1 = [1,2],a2 = [3,4], # Level one {a1,a2} # Level two );where
addPermIndexis, of course, not the one previously defined. Here,a1anda2are antisymmetries, as given by lists, and{a1,a2}is a symmetry, as given by a set.a1anda2are level one, and{a1,a2}is level two. With this notation, as compared with my original notation, it should be much easier to implement in general your approach using tables. Of course, there are things that must be checked, for instance:{a1,a2}and{a2,a3}overlap:addPermIndex("rankSix", a1 = [1,2],a2 = [3,4],a3 = [5,6], # Level one {a1,a2},{a2,a3} # Level two );Actually, this particular indexing function is, I believe, equivalent to the following one with no overlaps:
addPermIndex("rankSix", a1 = [1,2],a2 = [3,4],a3 = [5,6], # Level one {a1,a2,a3} # Level two );{a1,a2}, the antisymmetriesa1anda2must each have the same number of indices (as they here do).{s,a}, withsbeing a level one symmetry, andabeing a level one antisymmetry.