Positive and negative permutations

John Fredsted's picture

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:

  • An assignable entry, because p and n has empty intersection:
    p,n := permsPosNeg([1,2,3,4],pPerms,nPerms);
    p intersect n;
    
  • A non-assignable entry (in the sense that only 0 may be assigned), because 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?

JacquesC's picture

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 := 10 to 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.

John Fredsted's picture

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 posMaps and negMaps look nicely simple (as seen using printlevel := 10).

John Fredsted's picture

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 while loop which by iteration builds the positive and negative permutations of the entry ind. 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.

D2 := rtable(1..2,1..2,'storage=sparse'):
D2[1,2] := (1,1,2):
D2[2,1] := (-1,1,2):

 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).


`index/twoform` := proc( idx :: list( posint )
                         , D :: rtable, val::list )
local d2;
    d2 := D2[idx[]];
    if _npassed = 2 then
        # retrieval
        if d2 = 0 then
            return 0;
        else
            d2[1]*D[d2[2..-1]];
        end if;
    else
        # storage
        if d2 = 0 then
            if val[] <> 0 then
                error "illegal assignment";
            else
                try
                    D[idx[]] := val[];
                catch "unable to store":
                    val[];
                end try;
            end if;
        else
            # general value
            D[d2[2..-1]] := d2[1]*val[];
        end if;
    end if;
end proc:

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]

John Fredsted's picture

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 10 tensor: in four dimensions it would yield 4^10 = 1048576 table entries.

acer's picture

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

John Fredsted's picture

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:

D2[1,2] := (1,2):
D2[2,1] := [1,2]:

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

D2[1,2] = +1
D2[2,1] = -1
John Fredsted's picture

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.

John Fredsted's picture

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 := proc(index :: list)
uses Symmetries;
local s,indx;
    (s,indx) := Anti(index[1..2],index[3..4]);
    if s = 0 then return (0,[]); end if;
    (s, map(op, Symm([indx[1..2],indx[3..4]])));
end proc:
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:

Riemannian([x,y,y,x]);
                               -1, [x, y, x, y]

Riemannian([x,y,z,z]);
                                     0, []

To check Riemannian, do

R := Array((1..4)$4, ()->Riemannian([args])): # alas, Riemannian@`[]` doesn't work
nops(map2(op,2,{entries(R)}))-1;
                                       21

which 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.

John Fredsted's picture

Very interesting

Thanks, Joe, for your very interesting approach which I will scrutinize. There are two immediate issues to contemplate:

  • Of particular importance is the question whether or not any nontrivial (see below) set of (anti)symmetries is always a group-wise one. If that is the case, then most probably your approach is the way to go because it resolves the performance and storage issues.
  • The user, though, should not have to think about how to implement the (anti)symmetries of some indexing function. The user should only be concerned with entering, as given in my original code, the positive and negative permutations. Then some code should, if possible, transform that into group-wise (anti)symmetries, in analogy with your procedure 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...

John Fredsted's picture

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 addPermIndex is, of course, not the one previously defined. Here, a1 and a2 are antisymmetries, as given by lists, and {a1,a2} is a symmetry, as given by a set. a1 and a2 are 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:

  • As already agreed upon, there cannot be any overlap at level one. This generalizes to any level: for instance, the following rank six indexing function should not be allowed because {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
    );
    
  • At each level the number of indices must match: for instance, in the symmetry {a1,a2}, the antisymmetries a1 and a2 must each have the same number of indices (as they here do).
  • At each level the "types" must match: for instance, it would make no sense to have something like {s,a}, with s being a level one symmetry, and a being a level one antisymmetry.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
}