1. Defining new combinatorial algebras
1.1. Combinatorial algebras on a given basis
We now turn to the central feature of the MuPAD-Combinat package: the ability to easily implement new combinatorial algebras. We start by the free associative algebra over the rational numbers generated by non commutative letters . Its basis is indexed by words, and the product of two basis elements is obtained by concatenating the corresponding words:
domain FreeAlgebra
inherits Dom::FreeModule(Dom::Rational, combinat::words);
category Cat::AlgebraWithBasis(Dom::Rational);
one := dom::term([]);
mult2Basis := dom::term @ _concat;
end_domain:
We will explain the bits of this definition in a minute after a few examples of use. Let us define two elements of the free algebra:
x := FreeAlgebra([a, b, c]);
y := FreeAlgebra([d, e])
The B just stands for the name of the basis. We can compute linear combinations and products of x and y:
3 * x;
x + y;
x * y
Here is a more complicated expression:
x * (2*x + y) + (3 + y/2)^2
Note how the 3 in the expression is automatically converted into an element of the domain; declaring that FreeAlgebra was an algebra (with a unit) automatically defined the natural embedding of the coefficient ring into it.
We turn to the explanation of the implementation of FreeAlgebra above. The line
domain FreeAlgebra
states that we are defining a new domain called FreeAlgebra (a new class in the usual object oriented terminology).
inherits Dom::FreeModule(Dom::Rational, combinat::words);
lets FreeAlgebra inherit its implementation from the free module over the rationals (Dom::Rational) with basis indexed by words (combinat::words).
category Cat::AlgebraWithBasis(Dom::Rational);
states that FreeAlgebra is actually an algebra with a distinguished basis; this allows one, in particular, to define the multiplication by linearity on the basis.
one := dom::term([]);
defines that the unit of FreeAlgebra is the empty word (dom refers to the domain being defined, and dom::term is a constructor that takes an element of the basis, and returns it as an element of the domain). Finally,
mult2Basis := dom::term @ _concat;
states that two elements of the basis are multiplied by concatenating them, and making an element of the domain with the result (@ denotes the composition of functions). That's it.
Let us define the free commutative algebra on the letters :
domain FreeCommutativeAlgebra
inherits Dom::FreeModule(Dom::Rational, combinat::words);
category Cat::AlgebraWithBasis(Dom::Rational);
one := dom::term([]);
straightenBasis := dom::term @ sort;
mult2Basis := dom::straightenBasis @ _concat;
end_domain:
Note that we cheated a little bit: we declared that the basis of FreeCommutativeAlgebra consisted of words, whereas it really consists of words up to permutation of its letters: B([a,b]) and B([b,a]) represent the same element of the algebra. A careful implementation should define the combinatorial class of words up to permutation, and use it as the basis of FreeCommutativeAlgebra.
To enforce the uniqueness of the representation, we straighten the words in the basis by sorting them. This is the job of the straightenBasis constructor.
x := FreeCommutativeAlgebra([a, b]);
y := FreeCommutativeAlgebra([c, b, a])
The product of two words is then defined by concatenating them and straightening the result:
x * y;
y * x
If efficiency was at a premium, instead of sorting the concatenation of the two lists, we could have used the MuPAD function listlib::merge which merges sorted lists.
Note that two elements of FreeCommutativeAlgebra and of FreeAlgebra may happen to be printed out the same way:
x := FreeAlgebra([a]);
y := FreeCommutativeAlgebra([a])
and even share the exact same internal representation:
bool(extop(x) = extop(y))
However, they are not equal, because they are not in the same domain:
bool(x = y);
domtype(x), domtype(y)
So, even if they share the same name of basis, there is no risk of confusion; for example we are not allowed to multiply them together:
x * y
Error: Don't know how to multiply a FreeAlgebra by a FreeCommutativeAlgebra
Of course, this is still confusing for the user. He or she may always customize the basis names (as many other things) at any time should he or she wish to do so:
FreeAlgebra::basisName := hold(T):
FreeCommutativeAlgebra::basisName := hold(S):
x, y
Here, T stands for “Tensor algebra”, while S stands for “Symmetric algebra”. The hold is there for safety, in case one of the identifiers T or S is assigned a value.
We can define the natural evaluation morphism from FreeAlgebra to FreeCommutativeAlgebra by linearity on the words; a word itself is simply sorted, and converted into an element of FreeCommutativeAlgebra:
evaluation := FreeAlgebra::moduleMorphism
(FreeCommutativeAlgebra::term @ sort,
ImageSet = FreeCommutativeAlgebra):
Let us apply this morphism to the sum of two words which only differ by a permutation:
x := FreeAlgebra([c, b, a]) + FreeAlgebra([c, a, b]);
evaluation(x);
The evaluation morphism is actually quite canonical, so it can make sense to declare it as a conversion to the system. This can be achieved with the operators::overloaded::declareConversion function:
operators::overloaded::declareConversion(
FreeAlgebra, FreeCommutativeAlgebra, evaluation):
FreeCommutativeAlgebra(x)
Here, the conversion has been declared as implicit. If an expression mixes elements of FreeAlgebra and FreeCommutativeAlgebra, the former are automatically converted into FreeCommutativeAlgebra:
FreeCommutativeAlgebra([a, b]) + FreeAlgebra([c,b,a])
Of course, such a feature is questionable. Depending on the context, it can prove very practical, or on the contrary dangerous. The user is the only judge, and she or he can restrict the scope of this conversion by using the Explicit option. In this case, the conversion will only be applied if requested explicitly by convert or by new:
operators::overloaded::declareConversion(
FreeAlgebra, FreeCommutativeAlgebra,
evaluation, Explicit):
FreeCommutativeAlgebra(x);
FreeCommutativeAlgebra([a, b]) + FreeAlgebra([c,b,a])
Error: Don't know how to add a FreeCommutativeAlgebra and a FreeAlgebra
Typically, for symmetric functions, we only provided explicit conversions to construct symmetric functions from partitions because those conversions are not canonical at all: the Schur function s[3,2,1] obtained by converting the partition [3,2,1] has nothing to do with the elementary function e[3,2,1]. We refer to the design notes and to the documentation of the operators::overloaded library for details on the mechanism we use for defining automatic conversions and overloaded operators and functions. Note that it is not (yet) completely possible to declare new conversions as above when the target domain of the conversion is one of the predefined domains of the MuPAD library.
1.2. Combinatorial algebras with several representations
As usual, for shortening the notations, we export the library combinat:
export(combinat):
To continue our exploration, we implement variations on the two previous domains, where we assume that the algebra generators are indexed by 1,2,.... The basis elements of the free algebra and of the free commutative algebra are now respectively indexed by compositions and partitions.
domain FreeAlgebraInteger
inherits Dom::FreeModule(Dom::Rational,
compositions);
category Cat::AlgebraWithBasis(Dom::Rational);
basisName := hold(E);
exprTerm := dom::exprTermIndex;
one := dom::term([]);
mult2Basis := dom::term @ _concat;
end_domain:
domain FreeCommutativeAlgebraInteger
inherits Dom::FreeModule(Dom::Rational,
partitions);
category Cat::AlgebraWithBasis(Dom::Rational);
basisName := hold(e);
exprTerm := dom::exprTermIndex;
one := dom::term([]);
straightenBasis := dom::term @ revert @ sort;
mult2Basis := dom::straightenBasis @ _concat;
end_domain:
alias(NCSF = FreeAlgebraInteger,
SF = FreeCommutativeAlgebraInteger):
operators::overloaded::declareConversion(NCSF, SF,
NCSF::moduleMorphism(SF::straightenBasis,
ImageSet = SF)):
x := NCSF([1, 3, 2]);
y := SF ([1, 3, 2])
SF(x)
bool(SF(x)=y)
Let us analyze the differences with our previous implementation of the free algebras. First, we chose an indexed notation for the basis elements, as this notation is more compact and quite conventional in other systems. This is the purpose of the line exprTerm := dom::exprTermIndex: the method exprTerm of the domain is called to convert a term into an expression, as well as to print a term if there is no printTerm method; dom::exprTermIndex is a possible implementation of exprTerm, inherited from the category, which gives indexed notations. The other difference is that, following the usual convention, the integers in the partitions are sorted decreasingly. Here, this is suboptimally achieved by reverting the list after sorting it in the SF::straightenBasis method.
A disadvantage of this implementation of SF is that elements with many repetitions are not represented compactly:
SF([1])^10
One might prefer to use another basis for SF, where the element above would be represented as the first generator to the power . This can be done via the usual exponent notation for partitions. The basis of the algebra now consists of integer vectors. The product of two elements is simply obtained by adding up the vectors part by part, which can conveniently be implemented using the zipMuPAD function.
domain SFExp
inherits Dom::FreeModule(Dom::Rational,
combinat::integerVectors);
category Cat::AlgebraWithBasis(Dom::Rational);
basisName := hold(e);
exprTerm := dom::exprTermIndex;
one := dom::term([]);
mult2Basis :=
(v1,v2) -> dom::term(zip(v1,v2,_plus,0));
end_domain:
Let us do some computations:
SFExp([1]);
SFExp([2, 0, 1])*SFExp([1, 1]);
SFExp([1])^10
This notation could be confusing, so we override it:
SFExp::printTerm :=
v -> _mult(dom::basisName.i^v[i] $ i=1..nops(v)):
SFExp([1]);
SFExp([2, 0, 1])*SFExp([1, 1]);
SFExp([1])^10
As is, the elements of this algebra are not uniquely represented. For example, the first generator of the algebra can be represented by any of [1], [1,0], [1,0,0], ...:
SFExp([1]), SFExp([1, 0]), SFExp([1, 0, 0]);
bool(SFExp([1]) = SFExp([1, 0]))
We leave it up as an exercise for the reader to fix this bug by implementing a straightenBasis method which strips out the trailing zeroes of the basis elements.
Of course, SF and SFExp really represent the same algebra; only the internal data representation changes. So, we provide as conversions the reciprocal isomorphisms obtained by extending by linearity the bijections combinat::partitions::fromExp and combinat::partitions::toExp:
operators::overloaded::declareConversion(SFExp, SF,
SFExp::moduleMorphism(
SF::term @ combinat::partitions::fromExp,
ImageSet = SF)):
operators::overloaded::declareConversion(SF, SFExp,
SF::moduleMorphism(
SFExp::term @ combinat::partitions::toExp,
ImageSet = SFExp)):
Here is a simple conversion:
SF([4, 3, 3, 1]), SFExp(SF([4, 3, 3, 1]))
Let us check on an example that the conversions are indeed morphisms:
x := SF([3, 1]):
y := SF([4, 3, 2]):
x * y, SF( SFExp(x) * SFExp(y) )
We can write expressions that mix elements of both domains, and let the system find a way to convert them appropriately:
( 1 + SF([3, 1])*x ) * SFExp([2, 0]) + SFExp([1])
4 2 2
e1 e3 + e1 + e1
A priori, the representation of the result cannot be predicted; it depends on how the overloading mechanism chooses to resolve the conversions. If the user prefers one of the representations, she or he can take over the control at any level of the expression by forcing proper conversions:
(1+SF([3,1])*x) * SF( SFExp([2,0]) ) + SF( SFExp([1]) ) ;
SF( (1+SF([3,1])*x) * SFExp([2,0]) ) + SF( SFExp([1]) ) ;
SF( (1+SF([3,1])*x) * SFExp([2,0]) + SFExp([1]) ) ;
The implicit conversions are automatically applied transitively. As a consequence, we have without further work a conversion from NCSF to SF:
SFExp(NCSF([1, 4, 2, 2]))
Another consequence is that, when there are different representations for an algebra (say the symmetric functions expressed on any of the p, e, m, h, or s basis), it is sufficient a priori to implement conversions to be able to get all the possible conversions. In practice, we usually implement conversions, so that no conversion takes more than two steps. Of course it is still possible to implement some extra direct conversions for improved efficiency; when there are several ways to convert an element from one domain to another, the system always uses one of the shortest ones.
As usual, for shortening the notations, we export the library combinat:
export(combinat):
As a more advanced example, we demonstrate some computations in the so-called -shuffle algebra. This algebra has the sets of words for basis. The product is defined recursively by
TODO: Math "import_TeX("\epsilon \shuffle w = w \shuffle \epsilon = w")"
TODO: Math "ua * import_TeX("\shuffle vb := (ua \shuffle v) b + q^{|vb|} (u \shuffle vb) a")" .
where TODO: Math "import_TeX("\epsilon")" is the empty word, are words, letters and denotes the length of the word .
domain qShuffleAlgebra(K: DOM_DOMAIN)
// The parameter K is the base field
// Implementation of the vector space structure
inherits Dom::FreeModule(K, words);
// This is an algebra with a distinguished basis
category Cat::AlgebraWithBasis(K);
// Basis elements are printed as W([a,b,a,a])
basisName := hold(W);
// The unit of the algebra is the empty word
one := dom::term([]);
// The binary product of the algebra is defined
// by linearity on basis elements
mult2Basis :=
proc(ua: dom::basisIndices, vb: dom::basisIndices)
local a, b, u, v;
begin
// Base case: ua is empty
if nops(ua)=0 then return(dom::term(vb)); end_if;
// Base case: vb is empty
if nops(vb)=0 then return(dom::term(ua)); end_if;
// extract u and a from ua
u := [op(ua, 1..nops(ua)-1)]; a := ua[nops(ua)];
// extract v and b from vb
v := [op(vb, 1..nops(vb)-1)]; b := vb[nops(vb)];
// the recursion formula
dom::mapterms(dom::mult2Basis(ua, v), append, b) +
dom::mapterms(dom::mult2Basis(u, vb), append, a) *
q^nops(vb)
end_proc;
end_domain:
Note: in a free module, the entry dom::basisIndices contains the combinatorial class that indexes the basis of the module, and the function mapterms(elts, f, ...) applies f on each term of the element elts of the module. Thus, if elts writes , the call
dom::mapterms(elts, append, b)
returns the MuPAD representation of .
Let us declare this algebra over the field of expressions:
W := qShuffleAlgebra(Dom::ExpressionField()):
and try some computations:
W([a])*W([b]);
W([b])*W([a]);
Obviously, this is a non commutative algebra. One can use any object as letter:
W([b, c, d])*W([2, 3])
Moreover, the automatic conversion from the base field allows one to write complicated expression such as:
1/2*W([b])^3 + W([a])*(1 + W([1, 2]))
Finally note that this implementation is not very efficient. In particular, at each stage of the recursion in the expression:
dom::mapterms(dom::mult2Basis(ua, v ), append, b) +
dom::mapterms(dom::mult2Basis(u, vb), append, a) * q^nops(vb)
the system has to solve the overloading, i.e., to decide the meaning of + and *. It is better to decide this once and for all, i.e., to replace + by dom::plus2, and * by dom::multcoeffs. The emerging expression is less readable, but its processing is considerably faster:
dom::plus2(dom::mapterms(dom::mult2Basis(ua, v ),
append, b),
dom::multcoeffs(
dom::mapterms(dom::mult2Basis(u, vb),
append, a),
q^nops(vb)));
Moreover, a non-recursive implementation of the -shuffle is likely to be more efficient.