CSL - ++ Symbolic Computation Library User Manual

Grégoire Uhlrich

December 2, 2020 CSL Manual

2 Contents

1 Introduction 15

2 C++ basics 17 2.1 C++ 101...... 17 2.1.1 History, philosophy...... 17 2.1.2 Compile-time vs. run-time...... 18 2.1.3 ...... 18 2.2 Constness in C++...... 19 2.3 References, pointers...... 20 2.4 Namespaces...... 23 2.5 Enumerations...... 24 2.6 The auto keyword...... 25 2.7 Lambda functions...... 26 2.8 The standard library...... 28 2.8.1 I/O streams...... 28 2.8.2 Strings...... 29 2.8.3 Containers, C++ vector...... 30 2.8.4 Smart pointers...... 33 2.8.5 Optional variables...... 35 2.9 Lists in C++...... 36 2.10 Polymorphism...... 37

3 CSL basics 41 3.1 Philosophy...... 41 3.2 Symbolic computation...... 41 3.2.1 Principle...... 41 3.2.2 Internal representation of an expression...... 42 3.2.3 The Expr type...... 42 3.2.4 Limitations...... 46 3.3 Using CSL ...... 47 3.4 CSL type system...... 47 3.4.1 Type system...... 47 3.4.2 Primary type system...... 48 3.4.3 When CSL type system is not enough...... 50 3.5 CSL builder functions...... 50 3.6 CSL error system...... 51 3.7 Loops with CSL expressions...... 51 3.8 Canonical forms of expressions...... 51 3.9 Automatic ordering of expressions...... 53

3 CSL Manual CONTENTS

3.10 How to learn CSL ...... 55 3.10.1 The manual...... 55 3.10.2 The documentation...... 55 3.10.3 The code, for the brave...... 55

4 CSL good manners 57 4.1 Resource safety...... 57 4.2 Memory allocation...... 58 4.3 C++ good manners...... 59 4.3.1 Strings...... 59 4.3.2 C++ vectors...... 59 4.4 CSL good manners...... 63 4.4.1 The Expr type...... 63 4.4.2 Avoidable allocations...... 63 4.4.3 Heavy interface functions...... 64

5 Numbers 65 5.1 Integer...... 65 5.2 Float...... 66 5.3 IntFraction...... 67 5.4 Complex...... 68 5.5 NumericalEval...... 68

6 Scalar literals 71 6.1 Complex literals...... 71 6.2 Constant...... 71 6.3 Variable...... 72 6.4 Imaginary...... 74 6.5 IntFactorial...... 75 6.6 Arbitrary...... 76 6.7 CSL global variables...... 77

7 Mathematical operations 81 7.1 Sum...... 81 7.2 Prod...... 84 7.3 Pow...... 85 7.4 Subtraction and division...... 86

8 Mathematical functions 87 8.1 Common mathematical functions...... 88 8.2 Other functions...... 90 8.2.1 Angle...... 90 8.2.2 Factorial...... 91 8.2.3 DiracDelta...... 92

9 Operators 95 9.1 Principle...... 95 9.2 RealPart, ImaginaryPart...... 95 9.3 Derivative...... 96 9.4 Integrals...... 98

4 CSL Manual CONTENTS

9.4.1 ScalarIntegral...... 98 9.4.2 VectorIntegral...... 101

10 Vectorial expressions 103 10.1 Principle...... 103 10.2 Vector...... 103 10.3 Matrix...... 106 10.4 HighDTensor...... 108

11 Indicial expressions 111 11.1 Principle of indicial computations...... 111 11.1.1 Motivation...... 111 11.1.2 Parents and elements...... 111 11.1.3 Knowing if an expression is indexed...... 112 11.2 Space and Index...... 113 11.2.1 Space...... 113 11.2.2 Index...... 115 11.2.3 Basics on indices...... 116 11.3 Indicial tensors (Tensor)...... 117 11.3.1 Principle...... 117 11.3.2 Building...... 117 11.3.3 Manipulating parents...... 118 11.3.4 Setting properties to tensors...... 119 11.3.5 Complex, Transposed, Hermitian properties...... 121 11.3.6 Contraction properties...... 122 11.3.7 Tensor values...... 123 11.4 Indicial tensor fields (TensorField)...... 124 11.5 Tensorial derivative (TDerivative)...... 125 11.6 ISum...... 125 11.7 IProd...... 126

12 Abbreviations 129 12.1 Principle...... 129 12.2 Creating and printing...... 129 12.3 Evaluating...... 131

13 Expression modifiers 133 13.1 General principle...... 133 13.2 Copy, Refresh...... 134 13.2.1 Copy...... 134 13.2.2 Refresh...... 137 13.3 Expansion...... 138 13.3.1 Standard expansion...... 138 13.3.2 Conditional expansion...... 139 13.4 Factorization...... 140 13.5 Deriving...... 141 13.6 Evaluation...... 142 13.6.1 Principle...... 142 13.6.2 CSL evaluation flags...... 142

5 CSL Manual CONTENTS

13.6.3 Example...... 143 13.7 Replacement...... 144 13.7.1 CSL replacement...... 144 13.7.2 CSL swapping...... 148

14 Interface functions 151 14.1 Principle...... 151 14.2 Expression dependencies...... 151 14.3 Complex numbers...... 152 14.4 Interface for indicial objects...... 153 14.4.1 Space and indices...... 153 14.4.2 Tensor involution properties...... 153 14.4.3 Contraction properties...... 155 14.5 Other interface functions...... 155

15 Algorithms 157 15.1 Read-only algorithms...... 158 15.1.1 Find...... 159 15.1.2 FindIf...... 160 15.1.3 VisitEach algorithm...... 162 15.1.4 Other read-only algorithms...... 163 15.2 Read and write algorithms...... 163 15.2.1 Transform algorithm...... 164 15.2.2 ForEach algorithm...... 164 15.2.3 Other read and write algorithms...... 166

16 Tools and options 167 16.1 Tools...... 167 16.1.1 Timer...... 167 16.1.2 ScopedProperty...... 168 16.2 Options...... 169 16.2.1 errorStopsProgram (boolean)...... 169 16.2.2 checkCommutations (boolean)...... 169 16.2.3 freezeMerge (boolean)...... 169 16.2.4 applySelfContractions (boolean)...... 169 16.2.5 applyChainContractions (boolean)...... 169 16.2.6 fullComparison (boolean)...... 169 16.2.7 printIndexIds (boolean)...... 170

17 Tutorials 171 17.1 Tutorial: basic manipulations...... 171 17.1.1 Definitions...... 171 17.1.2 Using operators, deriving...... 171 17.1.3 Replacing, evaluating...... 172 17.1.4 Be careful with memory...... 172 17.1.5 Expanding, summary...... 173 17.2 Tutorial: indicial expressions...... 174 17.2.1 Space creation...... 174 17.2.2 Indices and tensors...... 174

6 CSL Manual CONTENTS

17.2.3 Basic space tensors...... 175 17.2.4 Basic indicial properties...... 176 17.3 Tutorial: going further with tensors...... 177 17.3.1 Framework...... 177 17.3.2 CSL definitions...... 178 17.3.3 CSL property definitions...... 180 17.4 Tutorial: When CSL is not enough...... 181 17.4.1 Principle...... 181 17.4.2 An equation inverter with CSL ...... 183

7 CSL Manual CONTENTS

8 List of Figures

 2πt 3.1 Internal representation of A 1 + cos ...... 42 T 3.2 Simplified inheritance hierarchy of CSL ...... 43

13.1 Memory layout of two integers...... 134 13.2 Memory layout in CSL part 1...... 134 13.3 Memory layout in CSL part 2...... 135 13.4 Memory layout in CSL part 3...... 136

9 CSL Manual LIST OF FIGURES

10 List of Tables

2.1 Some of the fundamental C++ types...... 19

3.1 Sample of ordering rules in CSL. Rows correspond to u and columns to v ...... 54

8.1 List of common CSL mathematical funtions...... 89

13.1 Evaluation flags in CSL ...... 143

11 List of sample codes

1 C++ function parameters ...... 20 2 C++ namespace use ...... 23 3 C++ enumerations, part 1 ...... 24 4 C++ enumerations, part 2 ...... 25 5 C++ lambda functions ...... 27 6 C++ I/O streams ...... 28 7 C++ file streams ...... 29 8 C++ string manipulations ...... 30 9 C++ template function example ...... 31 10 C++ vector creation ...... 32 11 C++ vector basics ...... 33 12 C++ unique pointers ...... 34 13 C++ shared pointers ...... 35 14 C++ optionals ...... 36 15 Basics on Expr ...... 44 16 Functions with Expr objects ...... 44 17 Operators with Expr ...... 45 18 Type system ...... 48 19 Primary type system ...... 49 20 When the type system is not enough ...... 50 21 Looking through an expression ...... 51 22 Example of ordering test ...... 55 23 C++ string good manners ...... 59 24 C++ vector good manners, part 1 ...... 60 25 C++ vector good manners, part 2 ...... 61 26 C++ vector good manners, part 3 ...... 62 27 Be careful with memory ...... 64 28 Construction of Integer ...... 66 29 Construction of Float ...... 67 30 Construction of IntFraction ...... 67 31 Basics on IntFraction ...... 68 32 Construction of Complex ...... 68 33 Construction of NumericalEval ...... 69 34 Construction of Constant ...... 72 35 Basics on Constant ...... 72 36 Basics on Variable ...... 74 37 Construction of Imaginary ...... 75 38 Construction of IntFactorial ...... 75 39 Basics on IntFactorial ...... 76 40 Construction of Arbitrary ...... 76 41 Basics on Arbitrary ...... 77 42 Numerical values ...... 78 43 Literal constants ...... 79 44 Other constants ...... 80 45 Construction of Sum ...... 82 46 Basics on Sum ...... 83 47 Construction of Prod ...... 84

12 48 Basics on Prod ...... 85 49 Construction of Pow ...... 85 50 Basics on Pow ...... 86 51 Basics on mathematical functions ...... 88 52 Mathematical functions basics ...... 89 53 Basics on Angle ...... 91 54 Basics on Factorial ...... 92 55 Construction of DiracDelta ...... 92 56 Basics on DiracDelta ...... 93 57 Basics on real and imaginary parts ...... 96 58 Construction of Derivative ...... 97 59 Basics on Derivative ...... 98 60 Construction of ScalarIntegral ...... 99 61 Basics on ScalarIntegral ...... 100 62 Construction of VectorIntegral ...... 101 63 Basics on VectorIntegral ...... 102 64 Construction of Vector ...... 104 65 Basics on Vector ...... 105 66 Construction of Matrix ...... 106 67 Basics on Matrix ...... 107 68 Construction of HighDTensor ...... 108 69 Basics on HighDTensor ...... 110 70 Knowing if an expression is indexed ...... 112 71 Construction of Space ...... 114 72 Basics on Space ...... 115 73 Construction of Index ...... 116 74 Basics on Index ...... 117 75 Construction of TensorElement ...... 118 76 Basics on getting tensor parents ...... 119 77 Tensor symmetries ...... 120 78 Basics on Symmetry ...... 121 79 Basics on chain contraction ...... 123 80 Tensor values ...... 124 81 Construction of TensorField ...... 124 82 Construction of TDerivative ...... 125 83 Basics on ISum ...... 126 84 Basics on IProd ...... 127 85 Abbrev::makeAbbreviation() ...... 129 86 Basics on abbreviations - part 1 ...... 130 87 Basics on abbreviations - part 2 ...... 131 88 Basics on (Deep-)Copy ...... 136 89 Basics on (Deep-)Refresh ...... 137 90 Basics on expansion ...... 139 91 Basics on conditional expansion ...... 140 92 Basics on factorization ...... 141 93 Basics on derivation ...... 142 94 Basics on evaluation ...... 144 95 Basics on replacement - part 1 ...... 146 96 Basics on replacement - part 2 ...... 147

13 97 Basics on swapping ...... 149 98 Basics on dependencies ...... 152 99 Basics on complex interface ...... 153 100 GenerateIndex() ...... 153 101 AddComplexProperty() ...... 154 102 AddTransposedProperty() ...... 154 103 AddHermitianProperty() ...... 154 104 AddSelfContraction() ...... 155 105 AddContractionProperty() ...... 155 106 Find algorithm ...... 159 107 Example for find algorithm ...... 160 108 FindIf algorithm ...... 161 109 Example for find if algorithm ...... 161 110 Visit algorithm ...... 162 111 Example for the visit algorithm ...... 163 112 Transform algorithm ...... 164 113 Example for the transform algorithm ...... 164 114 For each algorithm ...... 165 115 Example for the for each algorithm ...... 165

14 Chapter 1

Introduction

This first part is about CSL, mathematical core of CSL-HEP. Its purpose is to give a bible of all most fundamental features of CSL, and some deeper ones. The idea is not to read it entirely once, but rather to use it to find precise information about one object, one function. . . CSL-HEP interface is at first very pleasing, the user has very few things to know about what is going on. Simply calling interface functions one after the other, one can gather amplitudes and diagrams for a physical process in a given model. Within this canonical use of CSL-HEP, the CSL manual will not really be needed. However, physical models and computations become quickly complicated and some need partic- ular care that cannot be fully automatize. In these cases, the user knowing how to use CSL (or how to find information in the manual) will have many tools to manipulate the symbolic results of CSL-HEP computations. As it is hard to know in advance what could be helpful, the CSL manual is as exhaustive as possible, describing all its validated objects and features. The manual starts with a remainder of C++, explaining what is needed to understand CSL. Then it addresses basics and good manners of CSL. This is probably the most relevant part for starters. After that, a number of chapters present all CSL mathematical expressions, from numbers to tensor derivatives: how to create them, how to manipulate them. They are followed by a few chapters on the CSL interface allowing you to manipulate, modify expressions. You may want to have a quick look at them in order to know what is possible in CSL. Finally, some tutorials are presented to show practical use of CSL. In general there is as much examples as possible in this manual. This manual may be complemented with the documentation that you can find here. It is not complete, but many objects and features are described. In particular, the manual does not present all functions. The documentation is in a sense completely exhaustive. Even if some functions may not be documented because of a lack of time, they appear in it (name, location, arguments) and you may simply guess how to use them as functions and parameters names are as explicit as possible. Be sure that there is still many functions fully documented, in particular for important features.

15 16 Chapter 2

C++ basics

This chapter is dedicated to CSL users that have no particular knowledge of C++. The interface is designed to be as intuitive as possible, but still lies in, and respects, the C++ language. The idea is not to provide a full course on C++ but rather to give basic material to understand and use properly CSL.

2.1 C++ 101

2.1.1 History, philosophy C++ is a compiled language developed during the 80’s by Bjarne Stroustrup. The basic idea was to take the C language and add object oriented features, classes. The name was initially C with classes. Its first standardization dates from 1998 (C++98 standard), and a new standard came in 2003 (C++03) with minor improvements (mostly bug fixes). As the C language, C++ is low level and very efficient. It means that writing C++ will be harder than a higher-level language (especially learning it), the same things will require more lines of code, but there is a gain in efficiency and control1 on everything you are doing. While the initial idea of C++ was C with classes, it is today way more than that. C++ evolves, in particular since 2011 (C++11 standard) that is a little revolution for the language. Since then, a new standard is released every 3 years, together with better compilers. C++11, C++14, C++17, and now C++20 represent what is called Modern C++. Besides new language features and extensions of the standard library, new ways of writing and thinking C++ are developed on some principles (not exhaustive):

• Safety. Modern C++ helps developers to write safe codes without too much effort, in particular avoiding the so called Segmentation fault crash, extremely common in C or C++ programs.

• Performance. C and C++ are compiled and very efficient languages. New features, modern C++, must absolutely not make C++ less efficient.2

• Clarity of code. New features are often invented to clarify code, make things more explicit. It is becoming more and more easy to understand C++ code (when having a good knowledge of it) without any comments. 1Control does not necessarily means that it is easier to control a language feature, but that if you are well informed you have everything you need at your disposal. 2Most of the time, the overhead using C++ rather than C is not pure time performance, but space. More objects means more classes, bigger executables. In particular objects from the standard template library (STL). C is still the king for optimized and light-weight codes (especially for embedded system when the weight is limited).

17 2.1.2 Compile-time vs. run-time As we said, C++ is a compiled language. This means that before being able to run anything, a compiler must read your code and produce an executable file. This is compile-time, translating human-language to computer-language. Then, you may run the executable file that is your program, this is run-time. Interpreted languages like python do not have a compile-time. Python reads your code and executes directly the corresponding instructions, using pre-compiled functions. Compile-time is a blessing for developers. Nowadays, compilers are very efficient and detect numerous errors, often telling you how to correct them before running the first instruction of your program. C++ aims to move errors from run-time to compile-time. In general, the effect of an error must be detected the sooner possible after the cause. The longer is this time, the more difficult debugging is. There is two solutions to reduce it. The first, catching errors early at run-time by checking regularly that variables are valid. This has run-time overhead (time to check), and is not perfect. The best solution (not always possible of course) is to detect the error at compile-time. These errors are the simplest to debug because they do not depend on any program state, and as the checking takes place at compile-time the execution suffers of no overhead. Consider that if C++ forbids you do to something there may be a good reason. Compilation errors and warnings may be annoying at first. But once you realize that they are the very best debugging tool you can have, you start to like it. Compilers are just saying to you:

Warnings ’You may know what you are doing but I doubt it.’,

Errors ’Trust me this won’t work, I will not let you do it.’.

This is like having a C++ professor telling you what is wrong and very often how to make it work.

2.1.3 Type system C++ has a strong type system. This is at first a little constraining, but helps to write good code. It is important here to distinguish several kinds of objects. First, fundamental types. Those are common to almost all programming languages. Table 2.1 shows some of the fundamental c++ types. Try to remember the size_t type (a.k.a. long unsigned int) because it is specific to indexation in C++. In particular, size of objects and element access always use size_t in the standard library.3 For a container vec, you will see then for(size_t i = 0; i != vec.size(); ++i) // use vec[i]; rather than for(int i = 0; i != vec.size(); ++i) // use vec[i]; Then, C++ allows you to create you own objects. This is object oriented programming. You build a class4 with some properties to simplify and clarify further code.

3Using regular int would work with a warning. 4Or struct. In C++ the two are exactly the same despite the fact that default access specifier are different: public with struct (ensures retro-compatibility with C), private with classes.

18 Type Description Void type in C++, literally means ’nothing’. Used when void a function does not return any value. char Character type, 8 bits. bool Boolean type. Equal to true or false, however takes 8 bits. int Integer type, at least on 16 bits. long int Integer type, at least on 32 bits. Alias for long unsigned int, at least on 32 bits. Used size_t for indexing in C++ instead of int. long long int Integer type, at least on 62 bits. float Floating point types, 32 bits. double Floating point types, 64 bits. long double Floating point types, 80 bits usually.

Table 2.1: Some of the fundamental C++ types.

2.2 Constness in C++

The const keyword in C++ is here to express that a variable may not be changed in the scope where it is defined as const.5 In general, even if it may be heavy in terms of notation, it should be used everywhere a variable is not modified (global or scoped variable, function parameter, class attribute. . . ). It is however not mandatory in any case to use it. Just note that specifying const variables may allow the compiler to optimize some chunks of code (if it knows that a certain variable is read-only). I will not here detail all possible ways to declare a const variable in C++ because there is a lot of possibilities. One thing to note is that when declaring a const variable (reference, pointer, or neither) the position of the const keyword is not fixed. It may be before or after the type name. You have then

• ’const Type var;’ is equivalent to ’Type const var;’.

• ’const Type &var;’ is equivalent to ’Type const &var;’ for references.

• ’const Type *var;’ is equivalent to ’Type const *var;’ for pointers.

• ’Type*const var ’ (very rarely used) is different from the previous example. This means a const pointer to a variable that may be modified whereas the previous line means a modifiable pointer to a const variable.

In CSL, the convention may vary. Recent code will follow the second forms Type const. The reasons are the following

• The type of the underlying variable appears first.

• By reading from left to right, you get information in a pleasant order. First the type as we said, next the constness, and finally if the variable is a pointer, reference or neither.

This is a pure convention choice, no politics behind.

5It does fully guarantee that the variable (or one of its parts) will not change, only that you cannot change it in your scope.

19 2.3 References, pointers

There is for each type several ways to pass a variable (fundamental or object) as a function argument. The different possibilities are shown in sample code1. Each possibility has a meaning and should be

Sample code 1: C++ function parameters

For a given type T there is several way to write function parameters. void f(T var);// Passing by value/ copy void f(T &var);// Passing by reference void f(T const &var);// Passing by const-reference void f(T *var);// Passinga pointer void f(T const *var);// Passinga pointer to const Less common void f(T *const var);// Passinga const pointer to void f(T const*const var);// Passinga const pointer to const Note T may be any type, including a pointer type. understood. In particular: • Passing by value means copying the whole object. In practice, never used for objects (or only very light objects) and always used for fundamental types. • Passing by const-reference is used to not copy the object and ensure the function will not modify it. Used for almost every object parameter. Do not modify the semantic when passing the parameter or using it, only the function declaration changes. • Passing by reference is used to not copy the object and possibly modify it in the function call. Used for any fundamental or object type parameter that must be modified during the function call. Do not modify the semantic when passing the parameter or using it, only the function declaration changes.

Pointers Pointers are a complicated notion of C++, but also a very powerful feature. They are basically memory addresses of variables. A pointer type if denoted with a *. You may get the address of a variable using &, and de-reference a pointer using * (i.e. getting the variable that it points to). This may look like int a = 5;// fundamental integer type int *b;//b isa pointer to int b = &a;//b takes the value of the adress ofa (*b) += 1;//*b is de-referencingb, the result is the valuea. // Herea= 6,b still points toa One may compose pointers as wanted: int a = 5; int *b = &a;//b points toa int **c = &b;//c points tob int ***d = &c;//d points toc (***d) += 1;// de-referencingd three times yieldsa // Herea is equal to6

20 A pointer is just a memory address (32 or 64 bits depending on the hard-ware), but allows to access very easily the underlying objects without copying anything. Passing pointer to functions is (semantics apart) a bit like references in the sense that you do not copy anything and you may or may not (if const) modify the parameter. The difference is that a pointer may be explicitly null, a reference may not in general6. void displayContent(int const *ptr)// does not modify ptr, else int*ptr { cout << *ptr << endl; } int main() { int *ptr = nullptr; displayContent(ptr);// bad!

if (ptr)//c++ tests if the pointer is non null here displayContent(ptr);// much better! } Using pointers, one should make sure that they actually point to data.

Stack vs. Heap allocation

Static allocation, on the stack, is the declaration of variable as we know, like int a; int *ptr = &a;

Here a and ptr are two stack variables, i.e. created one after the other in memory. These variables are no longer accessible once the current scope ends. They are very quickly accessible and should always be preferred to heap variables when possible. Dynamic allocation, on the heap, intervenes when one wants to control the lifetime of an object himself, i.e. not being limited by the scope in which the variable lives. Dynamically allocated variables are still accessible after the end of the scope provided we keep a pointer that points to it. The variable is destroyed on demand, only when needed. Creation and destruction of variables on the heap in C++ is done through new and delete functions. Here is an example of heap variable: int *ptr; { int *a = new int(4); cout << *a << endl; // >>4 ptr = a; }// End of the scope cout << *ptr << endl; // >> 4, the variable stills lives delete ptr;// >> End of the story

In this example, both ptr and a are stack variables. The heap-allocated variable is the underlying integer pointed by a and then ptr. The line delete ptr; does not actually do anything to the ptr variable, but to the pointed integer. After the instruction, ptr is still a perfectly usable pointer,

6A reference may dangle, i.e. reference actually no variable. It is as dangerous as null pointer. Reasons are either that the referenced variable is destroyed at some point, or that the reference comes from the de-referencing of a null pointer. However this is less common that a null pointer.

21 pointing still to the same address (where there is no more int). It is then invalid, but could be reassigned. Heap allocation is heavy in terms of performance. It is however fundamental to write code easily. It is used in many places (in particular every dynamic containers, strings, . . . ). However, dynamic allocation is dangerous for several reasons:

• Risk to have dangling pointers: we use a pointer whose pointed data has already been destroyed.

• Risk of double delete: deleting an object that has already been deleted.

• Risk of memory leak: all pointers pointing to a given data are destroyed, but the data is not. You will then never be able to access this data. It takes memory space for nothing until the end of the program.

Dangerous means that you can of course avoid all these problems if you are programming super carefully, but that in practice nobody is that perfect, everybody will have (many) problems. The standard library aims to maximally reduce the number of new and delete a developer has to write. And by maximally I mean reduce to zero. Heap allocation has two purposes: containers, and life-time controlled objects. Using respectively standard containers (see section 2.8.3) and smart pointers (see section 2.8.4), the modern C++ developer may almost forget about new and delete. Their use may still be very practical in some cases, but in case of doubt always choose the solution where you do not have to do it yourself.

Objects and pointers To illustrate this paragraph, consider a class representing rational numbers: class Rational { private: int num; int denom; public: int getNum() { return num; } int getDenom() { return denom; } void reduce(); // Constructors, other methods ... }; Objects in c++ may have attributes (variables) and methods (functions to access or modify the object). All public ones may be accessed with a dot. In the rational example, attributes are private so we cannot access them, but methods are public. Rational r(4, 6);// 4/6 cout << r.num << endl;// Forbidden, will not compile. cout << r.getNum() << endl;// Ok, callsa public method // >>4 r.reduce();// public method, 4/6 becomes 2/3 cout << r.getNum() << endl; // >>2 Note that is you are using a pointer, you have two options. Either de-referencing it with *, or using -> instead of the dot. Rational* ptr = getPointerToRational(4, 6); // points to 4/6 cout << (*ptr).getNum() << endl;// Ok, de-referencing ptr

22 // >>4 ptr->reduce();// Ok, using -> instead of. cout << ptr->getNum() << endl; // >>2

2.4 Namespaces

Namespaces are a pure compiler feature of C++. As class access specifier (public, private. . . ) they have zero impact on the final executable. They are used to avoid name conflicts. An object or function in a namespace has to be access with its full name, including its namespace (with double colon): namespace myFirstNamespace { int global = 5; } // ... cout << global << endl;// Does not compile, global is not defined cout << myFirstNamespace::global << endl; // >>5 All the standard library (see next section) lies in the namespace std. As for every namespace, you have two choices. Either specifying each time the namespace, or declare that you are using it. The compiler will automatically search in that namespace to understand which function or object you want to use. The two ways are presented in sample code2.

Sample code 2: C++ namespace use

First way: specify each time #include// contains std::cout, std::endl

int main() { std::cout <<"Hello␣world!" << std::endl; } Second way: using declaration #include using namespace std;

int main() { cout <<"Hello␣world!" << endl; std::cout <<"Hello␣world!" << std::endl;// still works if you want } Note The using declaration works for all the translation unit. In particular, all files including the one in which it is defined will have the using. This should be avoided in general.

Warning Always declare the using namespace after any file inclusion.

Within CSL, and more generally with most of C++ libraries, you will have the same choice. I advise people that are not accustomed to C++ to declare all using they can in their program files. It will make the code more readable.

23 2.5 Enumerations

Enumerations in C++ are the definition of a set of integers. You may or may not define specific values for each one. If not defined, each value is one plus the previous one. The first value is 0 by default. A simple enumeration example is presented in sample code3. Enumerations are very

Sample code 3: C++ enumerations, part 1

#include using namespace std;

enum Color { Red,//0 by default Green = 7, Blue//1+ Green=8 };

int main() { cout << Red << endl;// >>0 cout << Green << endl;// >>7 cout << Blue << endl; // >>8 cout << (Red == Blue) << endl;// >>0 cout << (Green == Green) << endl;// >>1 } Note In this first example, a color may be implicitly converted to an integer type. The contrary is also true. practical to manipulate variables with a verbose meaning, without touching strings. The code is extremely clear. You manipulate words, and the computer manipulates simple integers. The dream of any . Now let’s see the second type of enumerations, enum class. The principle is exactly the same, only semantics differ from the first case. In particular:

• The implicit conversion between an enum class element and an integer is forbidden. You must convert it explicitly.

• The elements are no longer accessible directly, you have to access them by first typing the enu- meration’s name, like a namespace enumName::enumValue. This allows to avoid name conflicts as enum element names are often short and very generic.

A summary of enum class is presented in sample code4.

24 Sample code 4: C++ enumerations, part 2 The enum class #include using namespace std;

enum class Color { Red, Green, Blue };

void printColor(Color color) { if (color == Color::Red) cout <<"Red" << endl; else if (color == Color::Green) cout <<"Green" << endl; else if (color == Color::Blue) cout <<"Blue" << endl; }

int main() { cout << int(Color::Red) << endl;// >>0 cout << (Color::Red == Color::Blue) << endl;// >>0

printColor(7);// Does not compile printColor(Color(7));// Works but bad printColor(Color::Green);// Way safer and clearer! } Note The fact that implicit conversion does not work is not a weakness. It forces you to specify what you want to do.

Note You cannot declare something like using enum as for namespaces. You must write it each time explicitly. See the previous section for details on namespaces.

2.6 The auto keyword

The auto keyword is one of the numerous C++11 features. It delegates the type deduction to the compiler when it is possible. In other words, when the declaration of a variable in not ambiguous (and when the type is long) you may use auto. std::vector>::const_reverse_iterator f() { // ... } std::vector>::const_reverse_iterator variableWithLongType = f(); auto variable = f();// the compiler knows the return type off, no need to write it auto copy = variable;// same here, the same type is deduced for copy The use of auto should be dedicated to clarity of the code. Using auto everywhere is a bad idea, as one of the advantages of C++ is that we know which type each variable is by reading the code. To use with parsimony.

25 2.7 Lambda functions

Lambda functions are particular functions that you can define on the go, store in a variable, pass as arguments to other functions. . . Their use is mostly for passing a function as argument of another only once. You do not want to write your function in a source file to use it once. With lambdas you can define it just where you need it. This produces clearer codes. In this section we will not detail how lambda work theoretically. If you want to know, see cppreference. Principal features of lambda are:

• Parameter passing follows the same rules as regular C++ functions.

• Function body follows the same rules as regular C++ functions.

• It may be passed to another function that take a function pointer.

• It does not see what is outside its body, unless it is captured (reason begin of [&] in the following example, that captures automatically all the lambda needs by reference).

• As regular C++ functions, it may be stored in a variable, either a std::function (needs template parameters for return type and parameter types), or without bothering too much with auto, see the previous section.

• Deduction type for lambda functions may be annoying on return types. In particular when using implicit conversion in lambda return-type, this may cause compilation problems. If this happens, just convert explicitly the returned value in the relevant type.

Examples of lambda functions are shown in sample code5.

26 Sample code 5: C++ lambda functions

First consider two integer global variables. We want a generic function taking another as pa- rameter, that will modify the two global variables. This function may be of the type void(int&): returns nothing and takes an integer reference to modify it. #include #include using namespace std;

inta=3; intb=4;

void modifyBoth(function func) { func(a); func(b); } This is the principle. We give the function once, and another one applies it to several elements, an entire container, or a mathematical expression. You have then 3 options. Use a regular function, a lambda stored in a variable, or an inline lambda. int add = 2; void firstOption(int &i) { i += add; }

int main() { auto secondOption = [&](int &i) { i += add; }; modifyBoth(firstOption); modifyBoth(secondOption); modifyBoth([&](int &i) { i += add; });// third option

cout << a << endl;// >>9 cout << b << endl;// >> 10 } Note The capture [&] is necessary for the lambda to access add because it is not passed as a parameter. This method is extremely powerful as the details of modifyBoth() are written only once, and then the function it takes may be anything.

See also cppreference for more details on std::function.

27 2.8 The standard library

The standard library is a collection of objects built on the C++ language in order to make our lives easier. A very well written documentation exists and can be found on cppreference.

2.8.1 I/O streams I/O streams allow to read and write from and to different places. In C++, writing and reading from the terminal is done using std::cout (output) and std::cin (input) as shown in sample code 6. Streams are handled with stream operator<< (output) and operator>> (input). They may be chained as wanted with different kind of variables.

Sample code 6: C++ I/O streams

#include

int main() { int age; std::cout <<"Enter␣your␣age:␣"; std::cin >> age; std::cout <<"Your␣age␣is␣" << age << std::endl; } Note std::endl means end line. It is equivalent to the more universal character '\n'.

You may also read and write from files. In C++, the way of doing it is the same as for std::cout and std::cin once you have created your own stream to or from a file with fstream. An example in presented in sample code7.

28 Sample code 7: C++ file streams

#include #include using namespace std;

int main() { int writeThisToFile = 5; fstream out("data.txt", ios::out); out << writeThisToFile; out.close();

int readThisInFile; fstream in("data.txt", ios::in); in >> readThisInFile; in.close();

cout << readThisInFile << endl; // >>5 } Warning Do not forget to close files after use. Otherwise the RAM taken by the opening of the file will not be released.

2.8.2 Strings

Strings in C++ are not a fundamental type. A std::string is a collection of characters (char). We will not here say much about it. They are very close to what we could expect of a std::vector (see next section). But what really is of particular interest is how to create string and how to manipulate them. In sample code8 are shown basic manipulations.

29 Sample code 8: C++ string manipulations Header #include using namespace std; Creation stringa="myFirstString"; cout << a << endl; // >> myFirstString Comparisons stringa="alpha"; stringb="alphb"; cout << (a == b) << endl; // >>0 cout << (a < b) << endl;// alphabetical order // >>1 Passing strings as parameters (always by reference is better, std::string is heavy to copy) void someFunction( std::string const ¬ModifiedString, std::string &modifiedString) { ... } See also cppreference for more details on strings.

2.8.3 Containers, C++ vector

Containers are collection of objects of the same type. The standard library provides various con- tainers, each one specialized for a particular purpose. I will present here the most important one, because it is most of the time a good container for what you want to do, std::vector.

C++ templates

As all containers it lies in the standard template library (STL). Templates in C++ are generic functions or classes that depend on one or several types (or variables). A canonical example is the min function that returns the least of two arguments. Within the c++ type system, you should in general write that function for all different types (int, float, double, long int, user-defined objects. . . ) and this would be rather long. Instead, you may write a template function that takes an arbitrary type T and returns the minimum of two arguments of type T. As is, no function is built by the compiler. But at each call in the code, the compiler deduces for you the type of min function you need and compile it if it does not already exist. This example is shown in sample code9. Compiler creates a specialization if it is possible of course. In the min function example, objects of the template type must be comparable with operator<.

30 Sample code 9: C++ template function example

#include using namespace std;

template Comparable mini(Comparable a, Comparable b) { if (a < b) return a; return b; }

int main() { // int mini(int, int) cout << mini(4, 5) << endl; cout << mini(4, 5) << endl; // >>4

// string mini(string, string) cout << mini("c++","python") << endl; cout << mini("c++","python") << endl; // >>c++ } Note Template parameter deduction in C++ allows to not specify the type of function you want in this example. In other cases, you have to specify the template parameters between <>.

C++ vector std::vector is a template class, it means that it may be specialized with any type that respects some (minor) requirements. In the STL, the declaration looks like namespace std{ template class vector{ // Provide functions fora container of Type }; } A std::vector have many features. We will not be exhaustive here. For more explanations see cppreference. As we have seen in the min function example, we have to specify the template parameter when declaring the vector variable. Then, several functions allow to access and manipulate vectors. Ex- amples are shown in sample code 10 and sample code 11. As for std::string, vectors should always be passed by (const-)reference (a copy of a vector is always to avoid if possible).

Iterators

A word about iterators. They are the return values of functions like vec.begin() or vec.end(). Basically, an iterator is a particular pointer-type object that allows one to run through a container.

31 Sample code 10: C++ vector creation

#include using namespace std;

vector empty; vector explicitVec = {3, 5, -1}; vector unInitializedQuatuor(4);// {?,?,?,?} vector initializedQuatuor(4, 3.14159);// {3.14, 3.14, 3.14, 3.14} vector copy = initializedQuatuor;// {3.14, 3.14, 3.14, 3.14}

Its properties are the following:

• An iterator (if valid) points to an element of the vector. You may in particular de-reference it to access the element (const or not, depending on the constness of the iterator), or use -> to use a member function if the element is an object.

• container.begin() returns an iterator to the beginning of the container, i.e. a pointer(-type) to the first element.

• container.end() returns an iterator to the end of the container, i.e. a pointer(-type) to the memory address just after the last element. It means that *vec.end() is memory violation. The last element may be accessed through *(vec.end() - 1).

• Incrementing (resp. decrementing) an iterator makes the pointer point to the next (resp. previous) element.

There is much more things to say about iterators. But what you need to know is above. And the fact that many of standard containers member functions take iterators as arguments. So you should know how they work.

32 Sample code 11: C++ vector basics Accessing elements vector vec = {1, 4, -99, -5}; vec[2] = 6; cout << vec[2] << endl;// >>6

for(size_t i = 0; i != vec.size(); ++i) cout << vec[i] <<"␣";// >>146 -5 Adding and removing elements vector vec = {1, 4, -99, -5}; vec.erase(vec.begin() + 2);// {1, 4, -5} vec.insert(vec.begin() + 1, -99);// {1, -99, 4, -5} vec.push_back(-1);// {1, -99, 4, -5, -1} vec.clear();//{} Using iterators vector vec = {1, 4, -5}; for(auto iter = vec.begin(); iter != vec.end(); ++iter) { *iter = -3; cout << *iter <<"␣"; // >> -3 -3 -3 } Using the range-based for loop (simple language shortcut for the iterator formu- lation) for(int &value : vec) { value = +7; cout << value <<"␣"; // >>777 } See also Paragraph 2.8.3 for iterators, and cppreference for more explanations.

2.8.4 Smart pointers

Smart pointers are a new feature from C++11. The idea is very simple. Provide objects (i.e. not fundamental types) that manage the lifetime of objects for you (instead of having to use new and delete). In other words, you build your variable on the heap through a smart pointer (see 2.3 for heap allocation) and it will take care of destroying it when you do not need it anymore. The idea is to replace int *dynamic = new int(5);// dynamic points to5 // ... many lines of code delete dynamic; by smart_ptr dynamic = make_smart(5);// dynamic still points to5 // ... many lines of code The smart pointer owns the data it points to, and destroys it when needed. In this picture, raw pointers (T*) should never own the underlying data (you never should have to delete anything your- self). For more explanations, see the very well written cppCoreGuidelines. Note that as std::vector,

33 smart pointers are template classes. It means that you have to specify the type of the pointed data when building it. There is two types of smart pointers7 in C++. The type depends on the ownership pattern that your data follows.

• std::unique_ptr: the smart pointer is the only owner of the data. In particular, it cannot be copied (two unique_ptr may not point to the same data), only moved. When destroyed, the unique_ptr destroys the data it points to. An example of use is shown in sample code 12.

• std::shared_ptr: the ownership may be shared between several shared_ptr. Along with the data, a counter counting the number of shared_ptr pointing to the same data is built. When a shared_ptr is destroyed, the counter is decremented. If it hits 0, it means that no smart pointer points to the data anymore and it is destroyed. An example of use is shown in sample code 13.

Sample code 12: C++ unique pointers

#include using namespace std;

{ unique_ptr ptr = make_unique(5); int *raw_ptr = ptr.get(); cout << *ptr << endl; // >>5 cout << *raw_ptr << endl;// >>5

unique_ptr ptr2 = ptr;// Not possible! }// End of scope, ptr is detroyed along with the pointed integer See also cppreference for more explanations.

7Between C++11 and C++14, there was a third, auto_ptr. But it has been deprecated.

34 Sample code 13: C++ shared pointers

#include using namespace std;

{ shared_ptr ptr = make_shared(5); { shared_ptr ptr2 = ptr;// Ok, shared_ptr may be copied int *raw_ptr = ptr.get(); cout << *ptr2 << endl;// >>5 cout << *raw_ptr << endl;// >>5 cout << ptr2.use_count() << endl;// >>2 }// End of scope, ptr2 is detroyed,1 ref less cout << *ptr << endl; // >>5 cout << ptr.use_count() << endl;// >>1 }// ptr is destroyed along with the pointed int because it is the last reference See also cppreference for more explanations.

2.8.5 Optional variables

The (template) object std::optional since C++17 allows to express that a variable may or may not exist. It is more safe than the traditional way, a null pointer. This is because the optional forces you to treat the case explicitly. A simple example is shown in sample code 14. The null value of an optional is std::nullopt.

35 Sample code 14: C++ optionals Function returning an optional integer #include #include using namespace std;

optional maybeInt(int init) { if (init > 0) return -init; return nullopt;// no value here } Getting and treating the optional. .value() or de-referencing the optional with *. optional maybe_1 = maybeInt(5); if (maybe_1) cout << maybe_1.value() << endl; // >> -5 optional maybe_2 = maybeInt(-5); if (maybe_2) cout << *maybe_2 << endl; // no output here, maybe_2 not valid Recovering a variable with default value if the optional is not valid int defaultValue = 0; int value1 = maybe_2.value_or(defaultValue); cout << value1 << endl;// >>0(maybe_2 invalid, default is 0) See also cppreference for more details on optionals.

2.9 Lists in C++

Here we are not going to talk about std::list, but std::initializer_list. There is a big difference. Standard lists are what we may imagine for lists in a . Initializer lists (collection of variables of a same type between curly braces {}) are much simpler objects and are used mostly for simplifying C++ syntax. I will not detail here all the possible use of initializer lists (you may have more details on cppref- erence), only present one that is particularly ubiquitous in CSL. Whenever a collection of objects is passed to a function, a std::vector (see section 4.3.2 for more details on vectors) must be given each time. For example, consider a function taking a set of integers to print the sum void printSum(vector const &integers) { int sum = 0; for(int value : integers) sum += value; cout << sum << endl; } If you want to use it, you should in principle write std::vector values({1, 2, 3}); printSum(values); // >>6

36 This could be rather lengthy. Initializer lists are directly convertible to vectors (in particular) in C++8. This means that whenever CSL asks you to give a collection of objects (almost always a std::vector) you can give it simply between curly braces {}. In the example, you may write printSum({1, 2, 3});// >>6

2.10 Polymorphism

In this section we treat the last important feature to understand CSL design. Polymorphism is the act of manipulating objects of different types under the same common type. This is exactly what we need to manipulate mathematical expressions. An exponential function may be function of anything, any other expression. Mathematical expressions must be unified in a single type in order to manipulate arbitrary ones, but still represent the underlying ecosystem of specialized objects with different types (numbers, literals, operations, . . . ). For a true neophyte this section will probably be rather technical, but fully understanding it is not crucial. In C++, a class may inherit from another. This is called inheritance. This is the core of object- oriented programming (OOP): each object represents a concept, and concepts may be related to each other. For example, cats and dogs are both animals. In OOP, this translates to the fact that Cat and Dog classes both inherit from the Animal class. They will both have automatically all the features of Animal class, and may implement new specific ones. This could look like class Animal { public: double weight; void printWeight() { cout << weight << endl; }; }; class Cat: public Animal { public: void speak() { cout <<"Meow␣!" << endl; }; }; class Dog: public Animal { public: void speak() { cout <<"Bark␣!" << endl; }; }; int main() { Cat c; c.weight = 3; c.printWeight();// >>3 c.speak();// >> Meow!

Dog d; d.weight = 9; d.printWeight();// >>9 d.speak();// >> Bark! }

8Between the two examples there is actually no difference on what function is called. You still build a vector from a list between {}. But this is simply meant to show you that you can do it without building explicitly a vector.

37 Inheritance without polymorphism avoid code duplication. Here both cats and dogs have a weight and may print it, so we do not want to implement it two times. Polymorphism is more than that. Suppose that you want to manipulate animals independently of the fact that they are cats or dogs. C++ allows this feature through pointer to the base class and virtual functions. The object C++ sees is not a Cat or a Dog but a pointer to Animal. He does not know (neither do you in general) which type the underlying object is. Using a virtual function9 speak() in the Animal class, you can make an Animal (Cat or Dog) speak, C++ will determine the right function to call and call it. Forgetting the weight of animals, this gives class Animal { virtual void speak() { cout <<"Oups␣the␣speech␣of␣this␣animal␣is␣not␣defined␣!" << endl; }; }; class Cat: public Animal { public: void speak() { cout <<"Meow␣!" << endl; }; }; class Dog: public Animal { public: void speak() { cout <<"Bark␣!" << endl; }; }; int main() { Animal *firstAnimal = new Cat; Animal *secondAnimal = new Dog; Cat->speak();// >> Meow! Dog->speak();// >> Bark!

delete firstAnimal; delete secondAnimal; } We say that the speak() functions of Cat and Dog override the one of the Animal class. If virtual, the specialized functions will indeed be called, even if called from a pointer the the base class. This is polymorphism in a nutshell. Creating a pointer to a base class and building a derived class, letting C++ know what to do through virtual functions. Each specialized class implements its own properties. Notice that we had to use here dynamic allocation. Static variables cannot produce the same effect as Animal c = Cat; is understood as a type conversion. The Cat is then converted to an Animal and looses its specializa- tion.10 Dynamic allocation forces you to think about deleting animals at the end. Or . . . use smart pointers ! Here is the same example using std::unique_ptr (see section 2.8.4 for more details): int main() { unique_ptr firstAnimal = make_unique(); unique_ptr secondAnimal = make_unique();

9Virtual functions are a bit technical. The way they work will not be detailed here. 10The same thing happens with pointers but this time the pointer is converted, not the underlying object.

38 firstAnimal->speak();// >> Meow! secondAnimal->speak();// >> Bark! } The construction of smart pointers is more complicated as the template parameters are different from left to right. But once you accept the declaration, resource safety smart pointers are providing you becomes very pleasant. In this last example, you see that from pointers to animals, we may determine who they are (cat or dog) using the virtual function speak(). This is called RTTI (run-time type information) in C++ and is exactly what we will see in CSL. Having only (smart-)pointers to a base expression class, and calling virtual functions that are specialized in every specific mathematical expression.

39 40 Chapter 3

CSL basics

3.1 Philosophy

CSL is developed in C++ based only on the standard library (C++17 standard) and GSL (GNU Scientific library). I chose the C++ for several reasons. A compiled language will provide better performances than an interpreted one (comparing to python, the main competitor in the physics community). Then, C++ is really good for Object Oriented Programming (OOP), allowing in my opinion to have well structured codes. It represents the ideal compromise between a time- consuming but powerful language for my skills and tastes. CSL is built from scratch. The idea behind that is to have a free software independant of any other program. The cost is big in terms of developping work, compared to the direct use of an already built library like a symbolic manipulation library. We may cite Mathematica [?], a symbolic manipulation software with its own language (Wolfram language), or SageMath [?] a free python library. They provide very high-level features that allow direct manipulation of mathematical expressions. It is of course very tempting to use them. And it would be perfectly justified. However, if it is possible to develop only what it needed from scratch, the reward is very nice. You then have full control on the program, even its deepest features. You are also independent of any bug or update that could slow you down. This is particularly the case with Mathematica. Its language is a very high-level one and this has two consequences. It is very practical and powerful for what it has been designed. But the counterpart is that it will be much less conveniant when trying to get rid of its design patterns, you may only use it as a black box. CSL has no pretension of being better or even equivalent to professional symbolic computation libraries. The idea being it is to provide a self-made mathematical basis for CSL-HEP and physicists using it. CSL is designed for high-energy physics, even if no physics appear at this stage. CSL must be fast and powerful enough to manipulate expressions from the theory to experimental predictions (for CSL-HEP and users that may want to interact themselves with the results), but will not be a complete symbolic manipulation library. In particular, no equation resolution is proposed, and in general features not really useful for high-energy physics are absent or limited.

3.2 Symbolic computation

3.2.1 Principle

Symbolic computation is the automatic manipulation of mathematical expressions by a computer program. This fields aims to automate computations because doing them by hand is long, fastidious, and error prone.

41 3.2.2 Internal representation of an expression

An expression is internally a tree as expressions are in general arbitrary functions of others. When looking at an expression, the program runs through all nodes recursively, starting from the root. An example of such a tree is shown figure 3.1 for the internal representation of

 2πt A 1 + cos , (3.1) T expression from which the root node is a product.

*

A +

1 cos

*

2 π t ^

T -1

 2πt Figure 3.1: Internal representation of A 1 + cos . The red nodes (the leafs) are the building T blocks, and the intermediate nodes are function of other sub-expressions, as a sum, a product, or a cos function.

Expressions may be composed arbitrarily. It means that a particular node in a tree may be any expression, and the number of arguments is also arbitrary (when not fixed, as for sums and products). Technically, nothing may be static, all must be dynamic (see section 2.3). In C++, when the size of a container or the type of an object is not predetermined (at compile-time), some dynamic memory allocation must appear. Expressions are indeed fully built dynamically, and this has a cost. As we said in the previous section, automatic manipulation of expressions is mush more expensive than by hand (with respect to what we could naively imagine). The (dynamic) type behind each mathematical expression in CSL, allowing to have arbitrary combinations, is Expr which we detail in the next section.

3.2.3 The Expr type

The first things to know about Expr is that:

• It is the type representing a fully general mathematical expression in CSL.

• There is no other expression type, so you should get used to it.

42 • It is a pointer-type, in particular a std::shared_ptr (see section 2.8.4). An expression may be shared between others, and the destruction of it does not depend on the user (or me), but on the standard library.

Expr is a pointer-type because as we said, the underlying expression may be anything. Anything means any type, and in C++ it is not possible to handle different types in the same container. It is however possible to manipulate pointers of the same type, but pointing to objects of different types. This is polymorphism (see section 2.10). A very simplified version of the inheritance hierarchy in CSL is presented figure 3.2. Mathematical expressions all inherit from an abstract base class, Abstract, and specialize step by step up to the final objects you may encounter in expressions. An important feature is that no intermediate abstract class may be built. This prevents any user to build objects that are not enough specialized to have a meaning.

Figure 3.2: Simplified inheritance hierarchy of CSL. You may found the full one in the documen- tation of Abstract.

The Expr class is a std::shared_ptr with more interface. This pointer does not point to a pure Abstract object, but to one specialization that is a particular mathematical expression. See section 3.4 to learn more about the CSL type system. The Expr type has few subtleties in its use for the user. The only one is the way to pass it as arguments. As each copy of a shared pointer increases its reference count, it means that more operations are done than a simple copy of pointer. Expr object are then always passed by reference in every function. This reference is const if the function does not modify the expression, non-const else. Examples are presented in sample code 16. Arithmetic and comparison operators are defined with Expr object, as shown in sample code 17.

43 Sample code 15: Basics on Expr

In this box we consider the following header #include #include

using namespace std; using namespace csl; Using std::shared_ptr features of Expr, using a dot Expr expr = functionReturningExpr(); // Displaying the reference count of the shared ptr cout << expr.use_count() << endl; // Getting the raw pointer from the shared ptr Abstract *raw_ptr = expr.get(); Using features of the underlying CSL object, using the arrow, de-referencing, or interface functions Expr expr = functionReturningExpr(); // Directly cout << expr->getType() << endl;// may display"Cos" for ex cout << (*expr).getType() << endl;// may display"Cos" for ex // Using an interface function cout << GetType(expr) << endl;// may display"Cos" for ex Note When calling expr->getType(), C++ gets to know the exact type of the pointed expres- sion and calls the corresponding virtual function of that type. See section 2.10 for more details.

See also Section2 for basics on C++.

Sample code 16: Functions with Expr objects

Arithmetic and comparison operators work on Expr as expected for mathematical expressions. // ReplacedsB byA+1 and returns the old value ofB Expr func( Expr const &A, Expr&B ) { Expr B_copy = B; //A=A+1 would not compile here,A is constant B = A + 1;

return B_copy; } See also Section2 for basics on C++.

44 Sample code 17: Operators with Expr

Full example showing how to pass and return Expr Expr a = ...; Expr b = ...; Expr c = 4 * (a + b)/(a - b); cout << (a == c) << endl;// >>0 cout << (a != c) << endl;// >>1 Note There is no operator as in C++ no one is defined. See section 7.3 for exponentiation.

See also Section 3.9 for the operator<() acting on expressions (simplicity ordering).

45 3.2.4 Limitations We are smarter than computers While it is often possible to automate some computations, keep in mind that the automatic nature of symbolic computations come along with some limitations. In particular, all clever tricks humans may find to get out of complicated mathematical expressions are impossible to automate. The only hope would be artificial intelligence. A deterministic program can only test a number of predefined tricks but will have no cleverness. To illustrate this, consider the two following expression:

A = x2/x, (3.2) B = x. (3.3)

While it is trivial for us to know that A and B are equal, a computer program will not in general know it if A is not simplified. Of course, one could write a more complex function to compare expressions. But symbolic manipulation are heavy for a computer and the idea is to have the least systematic operations possible. In this first example, basic simplifications do the trick because x2/x becomes x automatically. Here is a (not much) more complicated example. Consider this time

A = a · (1 + x), (3.4) B = a + ax. (3.5)

It is again clear that A = B mathematically. However, no symbolic manipulation system would expand automatically expressions (or it would be catastrophic). So without extra-help or particular care, a symbolic manipulation program will say that A 6= B. Take away: An affirmation (true or false) for a computer program will not necessarily mean the same thing as for a mathematician. In particular, A = B has different meanings in both. While a mathematician will test any theorem he knows to answer correctly, the computer will not bother in complications and tell you the horrible truth: 0 + x is not the same thing as x. This is true for numerous boolean questions you may ask about expressions. Be sure to correctly interpret the answers.

Symbolic computations do not fit computers At least what we think symbolic computations is does not. We do calculations by hand in a very human way: we skip, consciously or not, many intermediate steps. Despite the fact that the purely dynamic nature of symbolic computations is technically hard to program, automating them reveal all the intermediate steps we do not consider anymore since a long time. What this paragraph is really about is the difference between a computer operation (let’s consider bytes operations, not on single bits) and a symbolic operation. Consider x and y two program inte- gers. Then x+y corresponds basically to one or few computer operations. Consider now that x and y are unknown variables, literals. Then x+y corresponds to one human operation. But for a computer, this really simple expression is in fact much more complex, probably more than a hundred operations.

What you need to keep in mind is that behind one mathematical operations are hidden dynamic memory allocations, deallocations, several checks to simplify common cases, possible checks of ex- pression validity. . . All of these operations are necessary. In particular, the more checks are done, the more heavy and powerful becomes the program. If no check is done, something like 0 + 1 · x would not be simplified and the program would be unusable. If too many checks are done, expressions will be well simplified but building a sum with ten arguments would take for ever. The idea is to find a good compromise.

46 This simplicity gap between a human and computer operations should force you to be careful when writing symbolic code. Remember that each operation has a big cost, much more than you probably expects. This does not prevent a symbolic computation program to do millions of basic operations per second but when expressions are very big it becomes really important to have that in mind.

3.3 Using CSL

Besides the Expr type that is the only exception, all objects and interface functions (not member of a class) must be accessed in principle specifying the namespace csl::. As explained in section2, namespaces allow to avoid name conflicts between libraries. It yields code like csl::Object myObj = csl::CreateObject(); csl::DoSomthingWith(myObj->getSomething()); You may however declare a using namespace csl;. In this case, you can omit csl:: using namespace csl;

Object myObj = CreateObject(); DoSomthingWith(myObj->getSomething()); In the manual, we omit quasi-systematically the namespace csl:: (sometimes keeping it makes things more clear). All examples in the present manual are using the following header (to write at the beginning of your scripts): #include using namespace csl;

3.4 CSL type system

As we said in the previous section, an expression is a (shared-) pointer to an Abstract base class. The pointed object is specialized in a valid mathematical expression, but the information is a priori inaccessible. However, each expression can return its type when asked. This is called RTTI (run-time type information) in C++. The user may ask the type of an object to decide what will happen to it. The enumeration of (primary-)types may be found in file enum.h.

3.4.1 Type system

CSL types are enumerated in the enum csl::Type. See section 2.5 for a description of enumerations in C++. It contains all possible mathematical expressions in CSL. In particular, the type of any CSL object has the same name as the object itself in the namespace csl::Type, except ISum and IProd that have types csl::Type::Sum and csl::Type::Prod respectively (see section 11 for more details).

• Numerical types: Integer, Float, IntFraction, Complex, NumericalEval.

• Literal types: Constant, Variable, Imaginary, IntFactorial, Arbitrary.

• Mathematical operations types: Sum, Prod, Pow.

47 • Mathematical function types: Abs, Exp, Log, Cos, Sin, Tan, Cosh, Sinh, Tanh, ACos, ASin, ATan, ACosh, ASinh, ATanh, Angle, Factorial, DiracDelta.

• Linear operators types: RealPart, ImaginaryPart, Integral, ScalarIntegral, VectorIntegral, Derivative.

• Vectorial types: Vector, Matrix, HighDTensor.

• Indicial types: TensorElement, TensorFieldElement, TDerivativeElement, ISum, IProd.

• Other types: Scalar, ScalarField, StandardDuo, StandardMult, Polynomial, Commutator.

Note that ISum and IProd objects, respectively indicial sum and product, do not appear. This is because their type are Sum and Prod, they have just a supplementary specialization allowing them to treat correctly indicial expressions (see section 11). The type of an expression may be accessed directly or through an interface function as shown in sample code 18.

Sample code 18: Type system

The type of an object is an integer in the enum csl::Type. This type may be displayed in the standard output in a human-readable way. Expr integer = 3; Expr floating = 4.5; Expr sum = 2 + cos_s(3); cout << integer->getType() << endl;// or GetType(integer) // >> Integer cout << floating->getType() << endl;// or GetType(floating) // >> Float cout << sum->getType() << endl;// or GetType(sum) // >> Sum Types may of course be compared in conditions, or directly by interface functions. Expr sum = 2 + cos_s(3); cout << (sum->getType() == Type::Sum) << endl; // >>1 cout << IsSum(sum) << endl; // >>1 cout << IsProd(sum) << endl; // >>0 See also Sections about numerical types (5), about the cos function (8), and interface functions (14).

3.4.2 Primary type system

A less used but still interesting information about an expression is its csl::PrimaryType (also an enum). It does not give the precise type of the expression, only the family. It works exactly the same way as the Type, replacing Type by PrimaryType. The primary types do not exactly correspond to the segregation of the manual’s chapters. Here are the different primary types and which expressions are included int them:

48 • Numerical primary type: Integer, Float, IntFraction, Complex, NumericalEval.

• Literal primary type: Constant, Variable, Imaginary, IntFactorial.

• ScalarFunction primary type (one argument): Abs, Exp, Log, Cos, Sin, Tan, Cosh, Sinh, Tanh, ACos, ASin, ATan, ACosh, ASinh, ATanh, Factorial, DiracDelta, RealPart, Imaginary- Part.

• MultiFunction primary type (more than one argument): Sum, Prod, Pow, Integral, Scalar- Integral, VectorIntegral, Derivative, Scalar, StandardDuo, StandardMult, Polynomial, Angle, Commutator.

• Vectorial primary type: Vector, Matrix, HighDTensor.

• Indicial primary type: TensorElement, TensorFieldElement, TensorialDerivative.

• Field primary type: ScalarField.

• Arbitrary primary type: Arbitrary.

The primary type of an expression may be accessed directly or through an interface function as shown in sample code 19.

Sample code 19: Primary type system

The primary type of an object is an integer in the enum csl::PrimaryType. This type may be displayed in the standard output in a human-readable way. Expr integer = 3; Expr floating = 4.5; Expr sum = 2 + cos_s(3); cout << integer->getPrimaryType() << endl; // or GetPrimaryType(integer) // >> Numerical cout << floating->getPrimaryType() << endl;// or GetPrimaryType(floating) // >> Numerical cout << sum->getPrimaryType() << endl;// or GetPrimaryType(sum) // >> MultiFunction Types must of course be compared in conditions, or directly by interface functions. Expr sum = 2 + cos_s(3); cout << (sum->getPrimaryType() == PrimaryType::MultiFunction) << endl; // >>1 cout << IsMultiFunction(sum) << endl; // >>1 cout << IsScalarFunction(sum) << endl; // >>0 See also Sections about numerical types (5), about the cos function (8), and interface functions (14).

49 3.4.3 When CSL type system is not enough

It should not happen, as every expression may be recognized from others simply in CSL. But suppose that csl::Type and csl::PrimaryType are not enough for you, you may use base C++ features to know the type of an object pointed by a generic pointer. This is shown in sample code 20.

Sample code 20: When the type system is not enough Testing the type #include #include using namespace std; class Base {}; class A : Base{}; class B : Base{}; int main() { Base *a = new A(); Base *b = new B(); cout << (typeid(*a) == typeid(A)) << endl;// >>1 cout << (typeid(*a) == typeid(B)) << endl;// >>0 cout << (typeid(*b) == typeid(A)) << endl;// >>0 cout << (typeid(*b) == typeid(B)) << endl;// >>1 } Converting raw pointers Base *a = new A(); A *a_bis = dynamic_cast(a);// Points to the same object Converting shared pointers shared_ptr a = make_shared(); shared_ptr a_bis = dynamic_pointer_cast(a); // Points to the same object, the reference count is now of2 Note This is up to you. The dynamic cast is the safer way to cast pointers. If the type is incorrect, the result will be a null pointer (this is safer because it will cause a bug probably sooner). This works on any C++ object, not only CSL expressions or objects.

3.5 CSL builder functions

All expressions are constructed through functions, and not directly. All builder function have the same name as the object with lowercase letters and with a ’_s’ suffix (stands for symbolic), like sum_s, cos_s, pow_s. . . This is for two reasons

• All expressions are unified in the Expr type, so you cannot write something like ’Expr obj(args);’ (you do not know what type to build here).

• When building a given type of expression, you are never guaranteed that it will stay the same. For example, the sum 0 + 0 yields an integer, not a sum. So keep in mind that builder func- tions do not necessarily return the initial type you asked because of simplifications.

50 3.6 CSL error system

CSL does permanently a number of checks on expression (indicial expressions in particular), verifying they are valid. When calling a function or building an object, checks are also done in general. When CSL detects that some objects is invalid for the program run, it raises an error and stops the execution. This is brutal in order to allow a debugger (gdb for example) to print the back-trace of function calls. This helps a lot to find the error. This is meant to help the user (and even the developer) to catch errors early, to force him correct the error before going further. It is sometimes even necessary when CSL does not even know how to continue. You may however disable the program stopping. CSL will print the error but continue the execution. This will in general cause nothing good because the run-time of CSL is not built for these cases. However, if you know what you are doing you may try to set csl::option::errorStopsProgram to false. See section 16.2 for more details.

3.7 Loops with CSL expressions

You may iterate in any CSL expression, in particular on its arguments. By iterating, you can look at or modify them. The system presented in sample code 21 is completely general and may be applied on any expression, even zero-size expressions (leafs of the expression tree).

Sample code 21: Looking through an expression Sample expressions Expra= constant_s("a"); Exprf= sin_s(a); Exprg=2+ cos_s(a); Getting the size of an expression (number of arguments) cout << a->size() << endl;// >>0 cout << f->size() << endl;// >>1 // Using interface cout << Size(g) << endl;// >>2 Iterating on the arguments for(size_t i = 0; i != f->size(); ++i) cout << f->getArgument(i) << endl;// >>a // Modify withf->setArgument(newArg,i); // Using interface and operator[] for(size_t i = 0; i != Size(g); ++i) cout << g[i] << endl; // Modify withg[i]= newArg; // >>2 // >> cos(a)

3.8 Canonical forms of expressions

Expressions are always kept in a canonical form when created. It allows to perform the first simplifi- cations and to manipulate expressions of a given form in the program. Canonicalization is mandatory

51 in symbolic computations to have not only correct, but readable and manageable expressions. These checks are performed every single time an expression is created. So it must also be the lightest possible. This is why some systematic simplifications are sometimes not done: the cost would be too big with respect to the number of times they would be useful. Here are some canonicalization rules of CSL:

• A term of a sum cannot be itself a sum (a + (b + c) becomes a + b + c).

• An element of a product cannot be itself a product (a · (bc) becomes abc).

• If a sum or a product contains only one element it is converted into this element.

• Repetition of an operation is transformed in the appropriate operation: a + 2a = 3a, 2a · 4a2 = 8a3.

• Special values of sum, product, or functions are applied immediately (0x = 0, 1x = x, cos(0) = 1, . . . ).

• There exists no minus or fraction in the program (except integer fractions): a−b ≡ a+(−1)∗b and a/b ≡ a ∗ b−1.

To illustrate the canonicalization of expressions let’s see an example, the taylor development of cos(π/2 + ωt) around t = 0 up to the order t2 included. The standard calculation at hand gives:

cos(π/2 + ωt) ≈ cos(π/2) − ωt sin(π/2) + O t3 = −ωt + O t3

This takes two steps, considering taking one step to evaluate special values of cos and sin functions. Let’s see what the program would do without canonicalization. Here are the rules followed by the program for derivatives of functions fi(t) with respect to a variable t:

d X X dfi Sum: fi(t) = dt dt i i ! d Y X Y dfi Y Product: fi(t) = fj(t) · · fk(t) dt dt i i ji d dg df  Composition: (f ◦ g)(t) = · ◦ g (t) dt dt dt

f For the taylor development, here is the rule to get Tn (x), the coefficient of order n of a function f(x). f Coefficients are calculated recursively for efficiency, getting intermediate steps Cn (x) and evaluating the derivative part at the end:

d Cf (x) = Cf (x) for n > 0, n dx n−1 f C0 (x) = f(x) (x − x )n T f (x) = 0 · Cf (x ). n n! n 0

52 Applying those rules on cos(π/2 + ωt) gives explicitly:  t  cos(π/2 + ωt) ≈ cos(π/2 + ω ∗ 0) + (0 + (0 ∗ t + ω ∗ 1)) ∗ (−1 ∗ sin(π/2 + ω ∗ 0)) 1  t   t  + ∗ [0 + ((0 ∗ t + 0 ∗ 1) + (0 ∗ 1 + ω ∗ 0)) ∗ (−1 ∗ sin(π/2 + ω ∗ 0)) 2 1 + (0 + ((0 ∗ t) + ω ∗ 1)) ∗ (0 ∗ sin(π/2 + ω ∗ 0) + (−1) ∗ ((0 + (0 ∗ t + ω ∗ 1)) ∗ cos(π/2 + ω ∗ 0)))]

You see here that automatic simplifications, canonicalization, must absolutely take place in a symbolic computation program. Applying arithmetic rules on 0 and 1 simplifies drastically the result but it is still too complicated (we let parenthesis around sums or products that contain one element only): t cos(π/2 + ωt) ≈ cos((π/2)) + (t)(ω)(−1 ∗ sin((π/2))) + · (t) [(ω)(−1 ∗ ((ω) ∗ cos((π/2))))] (ω) 2 Then let’s applying canonicalization of sums and product, it is better but not quite good: −1 cos(π/2 + ωt) ≈ cos(π/2) + (−1) ∗ ωt · sin(π/2) + ω2t2 cos(π/2) 2 And finally special values of functions, sin and cos here, to recover our calculation at hand: cos(π/2 + ωt) ≈ (−1)ωt + O t3

This example is here to show how important canonicalization is in symbolic computation: it corre- sponds to steps of calculation we do neither explicitly nor consciously since a very long time but that must be done by a computer in order to produce readable results.

3.9 Automatic ordering of expressions

Automatic ordering of expressions has been integrated in CSL in order to have human-readable expressions and to better simplify them. It allows to order different expressions by simplicity, for example one has

x is simpler than y, xy is simpler than yx, 1 is simpler than cos(x)2 + sin(x)2, (3.6) x2 x·y2/z x is simpler than x+y2/z + x+y2/z . The ordering rule must be total, i.e. if E is the set of CSL mathematical expressions, the rule must respect the following conditions: ∀x, y ∈ E (3.7) if x ≤ y and y ≤ z then x ≤ z (transitivity), (3.8) if x ≤ y and y ≤ x then x = y (antisymmetry), (3.9) x ≤ y or y ≤ x, (connexity). (3.10) CSL follows the rules1 given in [?]. They provide a total and very practical order of expressions. For example, one has with this ordering tyx + 1 + cos(x + 1) −−−−→becomes 1 + txy + cos(1 + x)

1There is some minor differences but that do not deserve to be explicitly given.

53 Table 3.1 shows a table of rules for some (not all) CSL expressions. Rules referenced in the table are the following.

• O1 for two integers: arithmetic comparison of values of u and v.

• O2 for two products: Compares arguments from the end of the two products (last arguments). When the first non-equivalent arguments are encountered, the result is return. Else, return true if u has less arguments than v.

• O3 for two pow objects: Compares arguments from the beginning of the two pow objects. When the first non-equivalent arguments are encountered, the result is return. Else, return false.

• O4 for two sums: Compares arguments from the end of the two sums (last arguments). When the first non-equivalent arguments are encountered, the result is return. Else, return true if u has less arguments than v.

• O5 for two functions: If the types of functions are different, return the corresponding order of type in csl::Type. Else, return the order of the arguments of both function: Arg(u) < Arg(v).

• O6 for two indexed tensors follows three steps. See section 11 for more details on indicial expressions.

– If the two tensors have different names, return the alphabetical order of them. – Else if on of the tensors is complex conjugated and not the other, return true if v is complex conjugated. – Else returns the alphabetical order of the two index structures of tensors.

u↓ < v→ table Integer · ˆ + Function Indexed tensor Integer O1(u, v) true true true true true · false O2(u, v) u < ·v u < ·v u < ·v u < ·v 1 1 1 ˆ false ·u < v O3(u, v) u < v u < v u < v 1 + false ·u < v u < v O4(u, v) u < +v u < +v 1 Function false ·u < v u < v +u < v O5(u, v) false 1 Indexed tensor false ·u < v u < v +u < v true O6(u, v)

Table 3.1: Sample of ordering rules in CSL. Rows correspond to u and columns to v

In case of doubt, you may at all time test the order of two expressions using operator<(), operator<=(), operator>(), operator>=(). This is presented in sample code 22.

54 Sample code 22: Example of ordering test

#include #include using namespace std; using namespace csl;

int main() { Expra= constant_s("a"); Exprb= constant_s("b"); Exprx= variable_s("x"); Expry= variable_s("y");

cout << boolalpha; cout << (a < b) << endl;// >> true cout << (y >= x) << endl;// >> true cout << (cos_s(x) < sin_s(x)) << endl;// >> true cout << (1 + b*x > 1 + a*y) << endl;// >> false } See also Section6 for constants and variables, and section8 for the mathematical functions.

3.10 How to learn CSL

In this short section we present some advice to learn CSL. There is many information and you may lose yourself in them.

3.10.1 The manual The present manual is there mostly for the neophyte. The philosophy and basic code structure are presented in order to give a good overview of CSL interface. The purpose is that the (extremely rare) reader getting through the whole manual knows how CSL works on every aspect, or knows where to find the information in the documentation. The manual may also be useful to find an information you forgot. The structure is made to facilitate the search on a particular subject (one object, function, deep CSL feature or basic one).

3.10.2 The documentation The documentation is much more interactive than this manual, also much more convoluted. It is composed by many HTML pages referencing each other. In particular, you will not easily find information if you do not know what you are looking for. It is then not recommended for a first look. However, if looking for specific information on a function or object, the documentation will help you. It is more exhaustive, in particular you may access to all member functions of any CSL object. And if you are lucky, this function will be clearly documented. The manual does not provide this precision but aims to redirect to the documentation on any aspect you want.

3.10.3 The code, for the brave When the manual or documentation is not enough, the best way is always to either ask the author or someone well informed, or. . . look at the code. This is certainly not the aim of CSL. No user should

55 enter the code to understand any feature, but in last resort you may want to do it.

56 Chapter 4

CSL good manners

This section is dedicated to a user knowing already basics of C++ and CSL. Here are some pieces of advice to use them properly, avoiding bad surprises. I present first general principles, and then how they should be applied to C++ (standard library objects) and CSL. Note that in general, interface functions are here to keeping you from asking yourself too much questions. This section is mostly for the user that will go further and do some things on his own. However, all interface functions are not equivalent. Some are way slower on large expressions. Any user should be aware of that in order to properly optimize his code. Last word before talking about code optimization. The advice given in the following are not golden rules to follow each time. In particular, keep in mind that any optimization makes the code more complex and less readable. For small and/or few objects, optimization is often not necessary. In these cases, it is just a loss of time and code clarity. In general for these questions you may want to look at CppCoreGuidelines.

4.1 Resource safety

Resource safety is manipulating only valid objects. A quick example: int *ptr = new int(4); f(ptr);//f may modify ptr; cout << *ptr << endl;

This example shows an unsafe use of ptr. If the function invalidates it, the next line will probably crash. Of course, in most simple cases you know when a pointer is valid or not. But when a code becomes complex, it is better to be sure that all pointers are always valid. This is the case in CSL. All pointers of type Expr are guaranteed to be valid at any time, except in very particular cases when it is on purpose. You can verify it calling csl::CheckValidity(Expr const&) at any time on an expression. It should always return true. In particular, if you want to express the fact that a variable may or may not exist, consider using std::optional rather than a possibly null pointer (see cppreference or section 2.8.5 for more details). This object is nullable but forces you to use it properly. In other words, if the optional is empty and you try to use it, an error will be thrown immediately. CSL uses sometimes optional expressions in member functions as expand, factor. . . 1 They are used in cases where an expression has not to be modified. No expression is then created, the user chooses next to take the very same expression or copy it as wanted. This may save some heap allocation.

1Note that CSL interface functions do not force you to use optional objects, they appear only on member functions.

57 4.2 Memory allocation

Memory allocation is used everywhere in CSL, and in the standard library. The idea is not to avoid it, but to be aware that its use should be maximally reduced. For more details on heap allocation, see section 2.3.

Always prefer static allocation

If not needed, there is no reason to allocate a variable on the heap rather that on the stack (static variable). Overheads on heap allocation are the following:

• Takes time. Static allocation is basically changing the stack pointer value, and possibly call a constructor. Heap allocation is asking to the operating system a memory address where a certain quantity of contiguous blocks are available, allocating this memory and returning the pointer to it. This may take time, especially when the quantity of memory unavailable becomes important.

• Risk of pointers. By allocating variables dynamically, you are manipulating pointers with all the risks we talked about in section2 (null, dangling pointer).

• Life-time management. Although you may use smart-pointers to avoid to do it yourself (and the risks coming with it), it may be preferable to copy a value between static variables rather than creating a smart pointer.

Be aware of what causes heap allocation, and avoid it if not necessary

Not only building variables can cause heap allocations, but also simple use of certain objects. Note that any object allowing you to have dynamic storage (that may change size or type) always uses dynamic allocation under the hood. So if your are using dynamic objects, avoid copying or modifying them if it is not necessary. We will see examples in the standard library and in CSL.

Avoid copies

We already talked about this particular point several times, but it deserves its own paragraph, in particular for python users. In C++, a = b; means copy of the object. If a and b are pointer-type (like the Expr type in CSL), there is no particular problem. However, if a and b are containers or big objects, unlike in python, it will always mean (deep-)copy. So for types that are heavy or using dynamic allocation, consider:

• Always pass them to functions as (const-)references. It is by far the safer and better way to pass arguments to functions. Pass a const reference if the object may not be modified, a simple reference instead. See section2 for more details.

• Consider the move semantic if a heavy object must be moved from one variable to another: a = std::move(b);. After the instruction b is invalid a priori, but a contains now what was in b and no copy has been done. Consider this if you do not need b anymore (typically at the end of a loop). See cppreference for more details on move semantics.

• Use inline references if you do not need copy. For example, BigType const &a = b; or BigType &a = b;. This works the same way as for function parameters.

58 4.3 C++ good manners

The CppCoreGuidelines are dedicated to this. It is often advanced considerations, but you may take home any advice you see if it is doable for you. Here I will talk about critical topics in the use of CSL, with less details.

4.3.1 Strings The only take home message for strings is: do not copy them. They seem to be a simple objects, but are very similar to a std::vector. They dynamically allocate memory, in particular each time a string is copied2. You may either pass strings by reference, or use string_view. An example is shown in sample code 23. Sample code 23: C++ string good manners Passing by const-reference string getFirst( string const &a, string const &b) { if (a < b)// alphabetical order return a; return b; } Using std::string_view #include string getFirst( string_view a, string_view b) { if (a < b)// alphabetical order return string(a); return string(b); } Note Views do not need to be passed by reference. They are basically a pointer to a string, and may not modify it. They can be used (for read-only features) the same way as strings. You may build a new std::string from a std::string_view.

See also cppreference for more details on std::string_view.

4.3.2 C++ vectors For all copy considerations, please remember to pass vectors by (const-)reference to functions if the copy is not mandatory. We will concentrate here on basic use of std::vector that may be performance critical. 2Strings have small string optimization (SSO) that allows to not allocate memory if there is less than 15 characters. But this is not guaranteed, and in general consider this as a bonus and not a reason to copy strings.

59 • If you know the size of the vector in advance, build it entirely once.

• If you do not know the size of the vector in advance when building it, reserve some memory space before filling it.

• Avoid inserting / erasing elements in a vector if not necessary. Each time you do it, copies and memory allocations (for insertion) may take place.

• Prefer adding elements to the end of the vector with .push_back() to inserting in the middle or at the beginning with .insert().

The last two points are illustrated in sample code 24. If you are not convinced, see the example in sample code 25.

Sample code 24: C++ vector good manners, part 1

Knowing the size N of the vector in advance, consider std::vector vec(N); for(size_t i = 0; i != N; ++i) vec[i] = fill(i); If you do not know the size in advance, consider std::vector reallyBad; for(size_t i = 0; i != N; i++) if (condition(i)) reallyBad.push_back(someElement());

std::vector good; good.reserve(N); // Or good.reserve(whatever_number_is_not_too_big_but_big_enough_in_most_cases); for(size_t i = 0; i != N; i++) if (condition(i)) good.push_back(someElement()); Note Reserving memory space means only one allocation. The vector owns the memory space, and does not use it before you add elements. If you do not reserve anything, each push_back may reallocate a bigger space somewhere else, copy all the elements there and de-allocate the previous location.

See also cppreference for more details on std::vector.

60 Sample code 25: C++ vector good manners, part 2

Here is an example to demonstrate the value of reserve. We overload operators new and delete in order to count memory allocations / de-allocations. We create also an arbitrary object that also counts the number of times it is copied. Do not bother with the setup, it is here for you if you want to make the test yourself. The setup is the following: #include #include using namespace std;

size_t alloc = 0; size_t dealloc = 0; size_t copy = 0;

void* operator new(std::size_t sz) { alloc+= 1; return std::malloc(sz); } void operator delete(void* ptr) noexcept { dealloc+= 1; std::free(ptr); } structA{ int value; A(): value(0) {} A(int t_value): value(t_value) {} A(A const &a): value(a.value) { ++copy; } }; See the test in sample code 26.

61 Sample code 26: C++ vector good manners, part 3

Here is the test of the setup presented in sample code 25 (without and with reserve of 1000 elements, which is not that much). First method, really bad size_t N = 1000; std::vector vec; for(size_t i = 0; i != N; ++i) vec.push_back(A(7)); std::cout <<"Copies␣:␣" << copy << std::endl;// >> 2023 std::cout <<"Alloc␣␣:␣" << alloc << std::endl;// >> 11 std::cout <<"Dealloc:␣" << dealloc << std::endl;// >> 10 Second method, using reserve std::vector vec2; vec2.reserve(N); for(size_t i = 0; i != N; ++i) vec2.push_back(A(7)); std::cout <<"Copies␣:␣" << copy << std::endl;// >> 1000 std::cout <<"Alloc␣␣:␣" << alloc << std::endl;// >>1 std::cout <<"Dealloc:␣" << dealloc << std::endl;//0 Second method, using emplace_back std::vector vec3; vec3.reserve(N); for(size_t i = 0; i != N; ++i) vec3.emplace_back(7); std::cout <<"Copies␣:␣" << copy << std::endl;// >>0 std::cout <<"Alloc␣␣:␣" << alloc << std::endl;// >>1 std::cout <<"Dealloc:␣" << dealloc << std::endl;//0 You see the importance of reserving the memory space. Using emplace_back instead of push_back allows to create the object in place (giving arguments of construction directly to the function) instead of creating it and copying it into the vector. This will have a big impact on heavy objects (here for a simple int, probably not measurable).

Note Do not forget to set all three counters to 0 before each test, even the first one. Starting a C++ program implies memory (de-)allocation.

62 4.4 CSL good manners

CSL provides its own objects, that use (a lot) memory allocations and de-allocations. The user should be aware of that and avoid doing dangerous things. Keep in mind that in order to have dynamic and arbitrary mathematical expressions, one has to do a lot of memory allocations. Having basic simplification (canonicalization) adds again more. Manipulating a sum of ten CSL expressions (copying, modifying) will be much slower than ten integers in a std::vector. CSL expressions are not just containers.

4.4.1 The Expr type Pass it by (const-)reference A word on the Expr type. It is a pointer type. It means that, even if it is heavier than a raw pointer and you should always pass it by (const-)reference, do not be too afraid of copying it. For example, the following code does not copy any expression, just the pointer to it Expra=2+ exp_s(3); Exprb=a;// Just pointer copy

Beware of the null Expr There is nothing fundamentally wrong about writing something like Exprx= nullptr; Expr y; You can do it, in particular to transport a boolean value (represented on if the Expr is null or not). But in any case a null Expr should enter a CSL expression. CSL guarantees itself to manipulate always valid Expr. No test is done anywhere to check if they are valid, because they are meant to be, no need to lose time. Giving a null Expr to CSL will certainly cause a bug but it may be much later, so hard to find for you. Beware in particular that because of the fact that Expr is a pointer type, an un-initialized Expr is null.

4.4.2 Avoidable allocations This section concerns sums and products (possibly Vectorial expressions also) because they are dynamic containers (arbitrary number of arguments). Remember the advice on vectors (section 4.3.2). Here it is even more important do write clean code. Consider making a sum (or product) of a number of elements. I present in the following a very bad way to do it, then a bad way, and finally the good one. First, the very bad way: Expr mySum = 0; for(size_t i = 0; i != Nelements; ++i) mySum = mySum + getElement(i); This is very bad. At each step, you are reallocating a sum, with all checks it causes. For big expres- sions, this may be a true performance killer. Less bad, consider storing arguments in a std::vector and building the sum once with directly all arguments. vector arguments; for(size_t i = 0; i != Nelements; ++i) arguments.push_back(element(i)); Expr mySum = sum_s(arguments);

63 This is much better. But you may still do even better. Recall what we learned about allocations with std::vector. // If knowing the size in advance vector arguments(Nelements); for(size_t i = 0; i != Nelements; ++i) arguments[i] = element(i); Expr mySum = sum_s(arguments);

// else vector arguments; arguments.reserve(Nelements); for(size_t i = 0; i != Nelements; ++i) if (someCondition(i)) arguments.push_back(element(i)); Expr mySum = sum_s(arguments);

Sample code 27: Be careful with memory

Please be aware of the number of memory allocations/deallocations necessary to get the final result. Many intermediate steps not appearing in results by hand are however necessary to get things done in general. Memory management being heavy in terms of performance, we want to minimize it. Example: std::cout << x * x * x * x * x << std::endl; // ~40 allocations/deallocations of pure Expr // >>x^5 std::cout << prod_s({x, x, x, x, x}) << std::endl; // ~15 allocations/deallocations of pure Expr // >>x^5 When taking a sum or product of multiple arguments, it is much better to give them in a list (between curly braces) to csl::prod_s() or csl::sum_s() than to use chained operators that corresponds in this case to do x ∗ (x ∗ (x ∗ (x ∗ x))) which implies much more calculations and intermediate steps. See also Section4 entirely.

4.4.3 Heavy interface functions Beware of not using too much heavy modifiers, interface functions or algorithms, i.e. that (re-)allocate many expressions. In particular, • DeepCopy() re-allocates the entire expression. If big and repeated, this may take time in the program.

• DeepRefresh() is worse. It does all DeepCopy() does, but reapply all simplification rules at each depth. • Repeated calls to modifier functions in general must be used with parsimony, in particular the Replaced() function that refreshes expressions after replacing.

• The ForEachNode() algorithm should be preferred in general to Transform(), because it does not refresh the (whole) expression on the go. If it does not need to be refresh, make the right decision.

64 Chapter 5

Numbers

Numbers are the most fundamental objects entering expressions. We need them absolutely every- where, even when it seems that there are more letters than numbers. There are five types of number in CSL. Integers, floating-point, rational, complex numbers, and a particular number type handling uncertainties. Operations between numbers are done automatically when needed by CSL in order to always keep expressions simplified. CSL makes sure to create the relevant number after each operation. In particular, an operation between two non-integer numbers may decay into an integer. For example, 3 1 2 − 2 decays into the integer 1, 3.5 + 0.5 probably decays into the integer 4.

5.1 Integer

Integers store one long long int. They are by far the most used numbers in general. Consider the following code sample (let’s say that x is a variable): Expr y = x*x - x*x; The expression y is equal to zero. Even though no number appears explicitly, there is 9 allocations and 8 deallocations of Integer objects1. Here are the details of what is going on, the numbers in indices of equal signs correspond to the number of integer allocations / deallocations on that line (basically numbers that appear or disappear):

y =0/0 x ∗ x − x ∗ x 1+1 1+1 =5/0 x + (−1) ∗ x 2 2 =2/4 x + (−1) ∗ x 2 =1/1 (1 + (−1)) ∗ x 2 =1/2 0 ∗ x

=0/1 0.

Beware that Expr being a pointer type, one may want to make it null. Since c++11 there is three ways to null a pointer: int *ptr = ...; ptr = 0; // Bad since thec++11 standard ptr = NULL;// Looks better but still bad, NULL isa macro for0 ptr = nullptr;// Good! 11 allocation more for the result that is an integer, 0.

65 Sample code 28: Construction of Integer Explicitly Expr myInt = int_s(5); Using CSL global variables (see section 6.7) Expr zero = CSL_0; Expr one = CSL_1; Expr two = CSL_2; Using implicit conversion Expr myInt = 13; Expr y = 2 * x + (17 - 3); Using an automatic function Expr integer = autonumber_s(5.5 - 3.5); Expr floating = autonumber_s(5.5); Here CSL determines automatically if the number is a round value (creates an Integer) or a floating point number (create a Float).

The reason the two first solutions are bad is that for object pointer types (not just raw pointer Type*), an integer (0 or NULL) may mean something different that the null pointer. Here, nullptr is not an integer, is has the type nullptr_t. This type difference allows to differentiate a null pointer (nullptr_t) or an arithmetic value (0). In the CSL case for Expr, this gives Expr zero_1 = 0;// Integer equal to0 Expr zero_2 = NULL;// Integer equal to0 Expr reallyNull = nullptr;// Null expression So to declare a number, do it without wondering anything, but to declare a null Expr (in this case you should know what you are doing) you must use nullptr.

5.2 Float

Floats store long double numbers. Note that despite the fact that they are called floats, they have a better precision. The way for building a float is presented in sample code 29.

66 Sample code 29: Construction of Float Explicitly Expr myFloat = float_s(5.5); Using implicit conversion Expr myFloat = 2.3; Expr y = 2.4 * x + 0.3; Using an automatic function Expr integer = autonumber_s(5.5 - 3.5); Expr floating = autonumber_s(5.5); Here CSL determines automatically if the number is a round value (creates an Integer) or a floating point number (create a Float).

5.3 IntFraction

This class stores two long long int, as the numerator and denominator of a numerical fraction. The fraction is always kept reduced maximally computing the greatest common denominator each time (GCD). The way of building a rational number is presented in sample code 30. Rational numbers may be evaluated to a floating point number. This will not be automatic however. See sample code 31.

Sample code 30: Construction of IntFraction Explicitly Expr myFraction = intfraction_s(4, 14);//= 2/7 Using CSL global variables (see section 6.7) Expr half = CSL_HALF;//= 1/2 Expr third = CSL_THIRD;//= 1/3 Using implicit conversion Expry= int_s(2) / 7; Exprz= CSL_2 / 7; Warning Using implicit conversion works only if one of the two operands is already an Expr. If not, c++ will interpret it as integer division, yield an integer, and the resulting expression will be garbage. Example: Expr ok = x + int_s(2)/7; Expr garbage = x + 2/7;//=x+ 0. Runs but wrong result

67 Sample code 31: Basics on IntFraction Evaluating: Expr fraction = intfraction_s(3, 7); std::cout << fraction << std::endl; // >> 3/7 std::cout << Evaluated(fraction) << std::endl; // >> 3/7 std::cout << Evaluated(fraction, eval::numerical) << std::endl; // >> 0.428571 See also Section 13.6 for evaluation.

5.4 Complex

The Complex number of CSL is a container of two Expr, the real and imaginary parts respectively. These expressions must always be numerical. If you try to create a complex with non numerical expressions (or expressions that are themselves complex), it will raise an error. It is possible to given already complex numbers as arguments (see sample code 32). Consider two complex numbers z1 and z2, we have:

Complex(z1, z2) = z1 + iz2 ≡ (a1 + ib1) + i(a2 + ib2) = (a1 − b2) + i(a2 + b1). (5.1)

The purpose of this object is mainly to store complex values for physical constants in a unique variable. In computations, CSL_I is used instead in general (see section 6.7). Using CSL_I or Complex numbers is perfectly equivalent mathematically, even if used at the same time.2

Sample code 32: Construction of Complex Explicitly Expr complex = complex_s(3, 4);//3+4i Expr complex = complex_s(-3.5, 2);// -3.5+4i Expr complex = complex_s( int_s(2), intfraction_s(-3, 6) );//2- 3/6i

Please note that using CSL_I does not give you a Complex number. It is just a constant in expressions with particular exponentiation rules. To get a Complex number from CSL_I one may evaluate it, it is then converted in an actual Complex number.

5.5 NumericalEval

A NumericalEval is a real number that holds a positive and a negative uncertainty. In other words, it is something like mb = 4.18+0.04 (5.2) 1 GeV −0.03 2However, powers of CSL_I will simplify with each other, complex numbers also, but powers of CSL_I will not simplify with complex numbers.

68 for the b quark mass, or symmetrical

MZ = 91.1876 ± 0.0021 (5.3) 1 GeV for the Z boson mass. Rules to compose uncertainties are applied each time needed. For example,

+0.04 +0.04 mb + mb = 4.18−0.03 + 4.18−0.03 √ 2 2 = (4.18 + 4.18)+√0.04 +0.04 − 0.032+0.032 (5.4) +0.06 = 8.36−0.04.

For mathematical functions, the derivative rule at the first order is applied:

df(x) 2 f(x + dx) − f(x) = dx + O dx . (5.5) dx For now error propagation is not supported for mathematical functions, but could be implemented in the future if needed. Consider an angle

+0.08 θ = 1.03−0.04. (5.6) we would like to propagate the uncertainty of θ onto sin θ that appears in a physical result. This gives

+0.08 sin θ = sin(1.03−0.04) = sin(1.03)+0.08(− cos 1.03) −0.04(− cos 1.03) (5.7) −0.041 = 0.85+0.021 +0.021 = 0.85−0.041.

Such operations are not available in CSL and would require some implementations. One should not hesitate to contact us if in need for this particular feature. NumericalEval is a numerical class (see

Sample code 33: Construction of NumericalEval Explicitly Expr eval1 = numericaleval_s(3, 0.1); Expr eval2 = numericaleval_s(3, 0.1, 0.05); +0.1 The 2 numbers here are equal respectively to 3 ± 0.1 and 3−0.05.

Note The sign of the negative uncertainty does not matter. CSL adds the minus sign if not given. sample code 33 for basic building). It means that it will be composed with other numerical values automatically. In particular you may build a complex from two NumericalEval. This may look like Expr realPart = numericaleval_s(-1, 0.2, -0.1); Expr imagPart = numericaleval_s(0.00, 0.01); Expr myComplexValue = 0.5 + complex_s(realPart, imagPart);

69 +0.2 In this example myComplexValue is −0.5−0.1 + (0 ± 0.01)i. Then, uncertainties will be automatically computed at each operation. For example you may write: std::cout << myComplexValue * 2 + complex_s(0, 1) << std::endl; // >> -(1 ^{+ 0.141421}_{- 0.282843})+i(1 +- 0.0141421) std::cout.precision(2);// Setting2 significant digits for display std::cout << myComplexValue * 2 + complex_s(0, 1) << std::endl; -(1 ^{+ 0.14} _{- 0.28}) +i(1 +- 0.014) Note that NumericalEval does not handle significant digits. Computations are done with full preci- sion, the user must then discard irrelevant digits himself.

70 Chapter 6

Scalar literals

Literals are building blocks of expressions that are not pure numbers. They are represented by characters. There are several types of literal objects. The most important are Constant, Variable and Imaginary. Other building blocks exist, in particular indicial objects that are presented section 11

6.1 Complex literals

This section concerns a whole class of objects. The ones that may be complex, i.e. x such that x∗ 6= x. Those may be simple literals or indicial tensors / fields. They all share common properties through the Complexified class from which they all inherit. Their behavior will be determined by their ComplexProperty defined in the file enum.h. It is an enumeration defined as namespace csl{ enum class ComplexProperty{ Complex, Real, Imaginary} } By default, all objects are real. If you want a complex object, you have to define it explicitly. All literals have a complex property except IntFactorial and Arbitrary1.

6.2 Constant

This class represents a literal that will not depend on any other. Consider a constant c, it means that dc 6= 0 ⇔ c = x. (6.1) dx It is used in particular for physical constants in equations. You want them to get out of any linear operator (see section9), but sometimes you may want to derive by that parameter. Constants may or may not contain a numerical value that will come out if the expression is evaluated the right way. Ways to define constants are presented in sample code 34. One may build a constant with any numerical value, in particular any expression from section5. Here is an example that shows how to define a complex value with uncertainties for a constant. First define real part and imaginary parts (both having uncertainties): Expr a_real = numericaleval_s(1.23, 0.1); Expr a_imag = numericaleval_s(-0.02, 0.01, 0.00);

1The Imaginary is complex but its complex property may not be modified.

71 Sample code 34: Construction of Constant Bare constant Exprc= constant_s("c"); Constant with some numerical value Exprc= constant_s("c", 2.99792458e8); Complex constant Exprz= constant_s("z", ComplexProperty::Complex); See also Sections 6.1 and5 for ComplexProperty and numbers respectively.

And then building a complex from them, and the value to the constant: Expr A_0 = constant_s( "A_0", complex_s(a_real, a_imag), ComplexProperty::Complex ); Please note that CSL does not check if the value is complex to set the complex property. This means that giving a complex value without setting the property could lead to wrong results if the literal is conjugated before being evaluated. Basic on Constant are presented in sample code 35.

Sample code 35: Basics on Constant Changing properties Expr A_0 = constant_s(""); A_0->setName("A_0"); A_0->setComplexProperty(ComplexProperty::Complex); A_0->setValue(complex_s(1, 2));//1+2i Getting complex conjugate Expr cc = GetComplexConjugate(A_0); std::cout << cc << std::endl; // >> A_0^(*) Evaluating std::cout << Evaluated(cc, eval::literal) << std::endl; // >>1-2i See also: section 13.6 for evaluation.

6.3 Variable

The variable object is very similar to the constant object. For basic properties please see the pre- vious paragraph, in particular building a variable is the same thing as for a constant, replacing csl::constant_s() by csl::variable_s(). The only difference is that a Variable may depend (or

72 not) on other variables. By default, if x and y are two variables we have that ∂x 6=0, (6.2) ∂y ∂y 6=0. (6.3) ∂x However this can be set by the user as we will see. The dependencies are determined by three properties:

• elementary, a boolean. If true, the variable is elementary and ∂x/∂y = 0 if x 6= y. In this case, the two other properties have no effect.

• allDependencies, a boolean. If non elementary, tells if by default the variable depends on every other variable (true) or the contrary, on no other variable (false);

• dependencies, a list of variables. Depending on the value of allDependencies, it is the list of y such that ∂x/∂y = 0 (true) or such that ∂x/∂y 6= 0 (true). The user has not to manipulate directly this list, just to add or remove dependencies. Setting one of the two first properties (elementary or allDependencies) clears this list.

To illustrate this, let’s see an example. Consider an electric field E and a magnetic field B in a 3-dimensional space-time. The coordinates are considered elementary, i.e.

∂xi = δij, (6.4) ∂xj with xi representing the three different coordinates t, x and y, and δij the Kronecker delta. In physical quantities, derivatives of E and B may appear. Suppose a system where we have ∂E = 0, ∂t ∂B ∂B (6.5) = = 0. ∂x ∂y

This could be done within CSL writing: Exprt= variable_s("t"); Exprx= variable_s("x"); Expry= variable_s("y"); x->setElementary(true); t->setElementary(true); y->setElementary(true);

ExprE= variable_s("E"); ExprB= variable_s("B");

E->removeDependency(t);

B->setAllDependencies(false); B->addDependency(t); std::cout << Derived(E, t) << std::endl; // >>0

73 std::cout << Derived(E, x) << std::endl; // >>d(E)/d(x) std::cout << Derived(E, y) << std::endl; // >>d(E)/d(y) std::cout << Derived(B, t) << std::endl; // >>d(B)/d(t) std::cout << Derived(B, x) << std::endl; // >>0 std::cout << Derived(B, y) << std::endl; // >>0 For more details on how to derive expressions, see section 13.5.

Sample code 36: Basics on Variable Changing properties Exprx= variable_s(""); x->setName("x"); x->setComplexProperty(ComplexProperty::Real); x->setValue(CSL_0); Deriving Expry= variable_s("y"); std::cout << Derived(x, y) << std::endl; // >>d(x)/d(y) x->removeDependency(y); std::cout << Derived(x, y) << std::endl; // >>0 x->removeDependency(y); std::cout << Derived(x, y) << std::endl; // >>d(x)/d(y) See also Sections 13.5 for derivation, 13.6 for evaluation.

6.4 Imaginary

This class is the type of the imaginary i in equations. It is really just a symbol named i that has specific exponentiation properties. If k is an integer, i4∗k = 1 (6.6) i4∗k+1 = i (6.7) i4∗k+2 = −1 (6.8) i4∗k+3 = −i (6.9) (6.10)

This class has a built-in instance, CSL_I. You should use it any time you want the imaginary i. See sample code 37 for the use of CSL_I. See also section 6.7 on CSL global variables. The Imaginary class is practically a singleton class, i.e. a class with only one instance. The difference is that you could create more than one instance if you want. This would work as well but would not have any sense.

74 Sample code 37: Construction of Imaginary

Using CSL_I Exprz=1+2* CSL_I; Exprw=2-3* CSL_I; std::cout << Expanded(z * w) << std::endl; // >>8+i Note Using CSL_I does not create an Imaginary, only use the predefined one. It would be possible to create a new by csl::make_shared(), but this would allocate memory unnecessarily.

See also Section 13.3 for expansion.

Note that CSL_I may decay to a complex number (see section 6.1) if evaluated. In other words, one has Expr z = 1 - 2*CSL_I;// Symbolic sum std::cout << IsComplex(z) << std::endl;// Not complex, it isa sum // >>0 std::cout << IsComplex(Evaluated(z, eval::numerical)) << std::endl;// Becomes Complex(1, -2) // >>1

6.5 IntFactorial

This class is very rarely used. It may be useful for factorial of big numbers. You cannot evaluate it immediately, so you may want to store it in an IntFactorial, like 23!. It lies in the literals section because it will not compose with other numbers automatically. It may be evaluated through CSL evaluation, even if in general you want to evaluate more carefully. CSL does not provide this kind of careful evaluation.2

Sample code 38: Construction of IntFactorial Explicitly Expr bigNumber = intfactorial_s(29); std::cout << bigNumber << std::endl; // >> 29!

2 25! For example, 23! and 25! are not contained in integers. But 23! may be smartly evaluated and is equal to 25 ∗ 24 = 600. CSL does not provide a smart evaluation like this one.

75 Sample code 39: Basics on IntFactorial Evaluating Expr bigNumber = intfactorial_s(29); std::cout << bigNumber << std::endl; // >> 29! std::cout << Evaluated(bigNumber) << std::endl; // >> -7055958792655077376 Note The evaluation of a factorial may be out of bounds of a long long int. This is the case here. CSL does not provide an automatic way to resolve this problem. If this happen you will have to evaluate yourself properly the expression.

See also Section 13.6 for evaluation.

6.6 Arbitrary

Arbitrary objects compare always to any other, given that it is a unique expression. Consider an arbitrary A, we have

x + y = x + A, (6.11) 2 ∗ x = A ∗ x, (6.12) (x + cos(x)) · y = A ∗ y, (6.13) 2 ∗ x ∗ y 6= A ∗ y. (6.14)

There may be more that one Arbitrary object in a comparison. Each object may be mapped to only one other expression. For example, considering two distinct Arbitrary A1 and A2 we have

2 ∗ x ∗ y 6= A1 ∗ A1 ∗ y, (6.15)

2 ∗ x ∗ y = A1 ∗ A2 ∗ y, (6.16) y x + ye 6= x + A1 ∗ A1, (6.17) y x + ye = x + A1 ∗ A2, (6.18)

y A1 x + ye = x + A1e . (6.19) (6.20)

The automatic comparison is not activated by default. Otherwise no expression containing Ar- bitrary objects could be constructed. If A1 compares to x for example, trying to build x · A1 would yield x2. One must create its arbitrary objects and call a particular function that will activate the comparison of arbitrary during the call only. This function is a member function of Comparator.

Sample code 40: Construction of Arbitrary From Comparator Expr A_1 = Comparator::dummy(1); Expr A_2 = Comparator::dummy(2); Expr test = 2 * A_1 * A_2;

76 Sample code 41: Basics on Arbitrary Comparing a regular expression with a one containing Arbitrary objects: Exprx= constant_s("x"); Expry= constant_s("y");

Expr A1 = Comparator::dummy(1); Expr A2 = Comparator::dummy(2);

std::cout << std::boolalpha; std::cout << Comparator::dummyComparison(2*x*y, A1*A1*y) << std::endl; // >> false std::cout << Comparator::dummyComparison(2*x*y, A1*A2*y) << std::endl; // >> true std::cout << Comparator::dummyComparison( x + y*exp_s(y), x + A1*A1 ) << std::endl; // >> false std::cout << Comparator::dummyComparison( x + y*exp_s(y), x + A1*A2 ) << std::endl; // >> true std::cout << Comparator::dummyComparison( x + y*exp_s(y), x + A1*exp_s(A1) ) << std::endl; // >> true Note If you do not use the proper comparison function, nothing will be activated and A_1 and A_2 will be treated as regular constants.

6.7 CSL global variables

Global variables are defined to avoid too many memory allocations / deallocations, or by convenience. From a performance point of vue, sharing numbers representing 0, 1 and -1 in all the program (having only one copy of each) reduces considerably the number of memory allocations. Those numbers always appear in intermediate steps and having a unique object from which every expression share a pointer is a huge gain. All constants are defined the same way as in the standard library (semantically, the structure is not the same). For example π that is called M_PI in c standard library (M stands for maths) is called CSL_PI in CSL. Numerical constants are presented in sample code 42, literal constants in sample code 43. Other global variables are presented in sample code 44. Note that not using explicitly those constants will be somehow equivalent. There is still no memory allocation because CSL checks for you if the constant exists. However, using directly the provided constants allows CSL to not check anything.

77 Sample code 42: Numerical values

They are defined in header numerical.h. To use any time you need those values to avoid unnecessary memory allocations / deallocations. All those expressions are constants and may not be modified.

• CSL_0 = 0 (Integer).

• CSL_1 = 1 (Integer).

• CSL_2 = 2 (Integer).

• CSL_M_1 = -1 (Integer).

• CSL_M_2 = -2 (Integer).

• CSL_HALF = 1/2 (IntFraction).

• CSL_THIRD = 1/3 (IntFraction).

• CSL_M_HALF = -1/2 (IntFraction).

• CSL_M_THIRD = -1/3 (IntFraction). Example: Exprx= variable_s("x"); Expr numerical = CSL_M_1+ CSL_HALF * x; std::cout << numerical << std::endl; // >> -1+ 1/2*x

78 Sample code 43: Literal constants

Numerical constants are defined for convenience, not having to redefine them. All those ex- pressions are constants and may not be modified.

• CSL_PI, π ≈ 3.14159:

– Name: \pi. – Type: Constant. – Complex property: real. – Value: M_PI from cmath.

• CSL_E, e ≈ 2.71828:

– Name: e. – Type: Constant. – Complex property: real. – Value: M_E from cmath. Example: Expr i_to_the_i = pow_s(CSL_E, CSL_M_HALF* CSL_PI); std::cout << i_to_the_i << std::endl; // >>e^(-1/2*\pi)

79 Sample code 44: Other constants

Here are other global (constant) expressions defined in CSL. The most important one is of course CSL_I.

• CSL_I, imaginary i:

– Name: i. – Type: Imaginary. – Complex property: imaginary. – Value: undefined. – Comment: decays to Complex(0, 1) when evaluated with flag csl::eval::numerical. See section 13.6 for evaluation.

• CSL_INF, ∞:

– Name: INF. – Type: Constant. – Complex property: real. – Value: undefined. – Comment: does not have particular property. See it as a flag that tells that the computation went bad (division by zero) if seen in a result.

• CSL_UNDEF:

– Name: CSL_UNDEF. – Type: Constant. – Complex property: real. – Value: undefined. – Comment: does not have particular property. See it as a flag that tells that the computation went bad (some computation was not defined) if seen in a result. Example: Expr zero = pow_s(CSL_E, CSL_I* CSL_PI)- CSL_I*CSL_I; std::cout << zero << std::endl; // >>1+e^(i*\pi)

80 Chapter 7

Mathematical operations

7.1 Sum

Sums are dynamical sets of expressions following certain rules. In particular, sums are always commutative. Up to now we saw only building blocks. Building expressions function of others need special care. In particular, all expression should be kept in a canonical form at all time. A canonical form obeys a set of rules. For more details, see section 3.8. In particular for a sum, one must respect the following conditions:

• All arguments must be in a canonical form.

• There must be more than one argument, otherwise it is not a sum.

• No argument must be itself a sum.

• There must be at most one numerical argument. Expressions like 0.5+3/7 or 2+3.5+x should be simplified.

• There must not be two arguments with the same term. For example, 2x+4x must be simplified in 6x.

• Arguments should be ordered following a set of rules that define a total order. See section 3.9 for more details.

• A sum in a product always lose the numerical factor of its first term. This allows some sim- plifications, for example (a − b)(2b − 2a) becomes (a − b)(−2a + 2b) when ordered. Numerical factors get out, and we have −2(a − b)(a − b) = −2(a − b)2.

One may however bypass the canonicalization if needed. The last three item can be disabled for one particular sum when building it, or for sums (and products) setting the option csl::option::freezeMerge to true (see section 16.2 for more details). Once a sum is has been built, you can of course access and modify arguments.

81 Sample code 45: Construction of Sum

In this box we consider the following sub-expressions: Exprx= variable_s("x"); Expry= variable_s("y"); From two arguments Expr myFirstSum = sum_s(x, y);//x+y From a std::vector std::vector args = {CSL_2, x, y}; Expr myFirstSum = sum_s(args);//2+x+y Directly from an initializer list between curly braces Expr myFirstSum = sum_s({CSL_2, x, y});//2+x+y Building explicit sums sum_s(x, x);// 2*x sum_s(x, x, true);//x+x sum_s({CSL_2, x, y, x, CSL_M_1});//1+ 2*x+y sum_s({CSL_2, x, y, x, CSL_M_1}, true);//2+x+y+x-1 // Or: option::freezeMerge = true; // Build explicit sums(also affects products). option::freezeMerge = false;

82 Sample code 46: Basics on Sum Getter and setter for arguments Expr mySum = sum_s({CSL_2, x, y});//2+x+y std::cout << mySum->getArgument(2) << std::endl; // >>y mySum->setArgument(x, 2); std::cout << mySum << std::endl; // >>2+x+x std::cout << Refreshed(mySum) << std::endl; // >>2+ 2*x Note When setting a particular argument, you just changes the expression at a certain position. The whole sum does not change. The expression is not canonical anymore. You may refresh it to restore the sum. See section 13.2.2 for more details.

Testing if an expression is a sum std::cout << std::boolalpha; std::cout << IsSum(x + y) << std::endl;//x+y // >> true std::cout << IsSum(x + x) << std::endl;// 2*x // >> false Using operator[] overload Expr mySum = sum_s({CSL_2, x, y});//2+x+y std::cout << mySum[2] << std::endl; // >>y mySum[2] = x; std::cout << mySum << std::endl; // >>2+x+x std::cout << Refreshed(mySum) << std::endl; // >>2+ 2*x Iterating through a sum with indices for(size_t i = 0 ; i != mySum->size(); ++i) { mySum[i] = CSL_I; } // >> mySum=i+i+i Iterating through a sum with a range-based for loop if you do not need any indexa: for(Expr &arg : mySum) arg = CSL_I; // >> mySum=i+i+i Note For the range-based for loop, the const is totally optional if you do not modify the argument. However, if you want to modify it you have to take a reference writing Expr& (or auto&).

See also Section 6.7 for CSL_I and section 13.2.2 for Refresh(). aFor more details on this kind of loop see cppreference.

83 7.2 Prod

Products in CSL are very similar to sums (previous section). In particular, canonicalization rules are strictly the same. The only difference is that arguments of a product do not necessarily commute. Merging and comparisons take that into account. For example, consider two variables x and y that do not commute with each other, we have

x ∗ y ∗ x 6= x2 ∗ y, (7.1) x ∗ y − y ∗ x 6= 0. (7.2)

Sample code 47: Construction of Prod

In this box we consider the following sub-expressions: Exprx= variable_s("x"); Expry= variable_s("y"); From two arguments Expr myFirstProduct = prod_s(x, y);//x*y From a std::vector: std::vector args = {CSL_2, x, y}; Expr myFirstProd = prod_s(args);//2*x*y Directly from an initializer list between curly braces Expr myFirstProd = prod_s({CSL_2, x, y});//2*x*y Building explicit products prod_s(x, x);//x^2 prod_s(x, x, true);//x*x prod_s({CSL_2, x, y, x, CSL_M_1});// -2*x^2*y prod_s({CSL_2, x, y, x, CSL_M_1}, true);//2*x*y*x* (-1) // Or: option::freezeMerge = true; // Build explicit prods(also affects sums). option::freezeMerge = false;

84 Sample code 48: Basics on Prod

For getter, setter function, ways to read / write in a product, please refer to the previous section on sums. The interface is strictly identical.

Testing if an expression is a prod std::cout << std::boolalpha; std::cout << IsProd(x * y) << std::endl;//x*y // >> true std::cout << IsProd(x * x) << std::endl;//x^2 // >> false

7.3 Pow

Exponentiation is the last mathematical operator defined by a CSL object. It has always exactly two arguments. Canonicalization rules for Pow are:

• The first argument cannot be itself a Pow. In other words, (ab)c → ab∗c. You may have however c a(b ) that may not be simplified.

• Special exponentiation cases are simplified:

– x1 = x. – 1x = 1. – x0 = 1, CSL_UNDEF if x = 0. – 0x = 0, CSL_UNDEF if x = 0. – 0−x = ∞ (CSL_INF) if x > 0.

Sample code 49: Construction of Pow

In this box we consider the following sub-expressions: Exprx= variable_s("x"); Expry= variable_s("y"); From two expressions Expr myFirstProduct = pow_s(x, y);//x^y

85 Sample code 50: Basics on Pow Getter and setter for arguments Expr myPow = pow_s(x, 2);//x^2 std::cout << myPow->getArgument(1) << std::endl; // >>2 myPow->setArgument(y, 0); std::cout << myPow << std::endl; // >>y^2 Testing if an expression is a pow std::cout << std::boolalpha; std::cout << IsPow(pow_s(x, 2)) << std::endl;//x^2 // >> true std::cout << IsPow(pow_s(x, 0)) << std::endl;//1 // >> false Iterating through a sum with indices for(size_t i = 0 ; i != myPow->size(); ++i) { myPow[i] = CSL_I; } // >> myPow=i^i Here we know that the size is exactly 2. The explicit notation with size() allows however to generalize this loop to any expression.

Note There is (for now at least) no ranged-based for loop (type for(Expr& arg : myPow) ) because the underlying container structure is different from sums and products.

See also Section 6.7 for CSL_I.

7.4 Subtraction and division

There is no expression strictly like a − b or a/b in CSL. These two operations are decomposed with simpler ones, facilitating considerably simplifications. We have then

a − b = a + (−1) ∗ b, (7.3) a/b = a · b−1. (7.4)

CSL while not having these objects defined, allows the user to write expressions like a − b or a/b. The forwarding to the relevant operations is done automatically.

86 Chapter 8

Mathematical functions

Mathematical functions are all the one defined in section 3.4. They share (almost) all the following properties:

• Function of one unique argument.

• May be evaluated (use of functions) for numerical values with flag eval::numerical (see section 13.6 for evaluation).

• Have special values evaluated automatically to simplify expressions (ex: cos(0) = 1, log(e) = 1, . . . ).

To get or set arguments of mathematical functions, you may use the operator[], or member functions. The size of such an expression is 1 or 2 (depending on the function) allowing you to loop on arguments in a general way. This is shown in sample code 51.

87 Sample code 51: Basics on mathematical functions Building Exprx= variable_s("x"); Expry= variable_s("y"); Exprf= cos_s(x);// cos(x) Exprg= angle_s(y, x);// angle(x,y) Getting, setting arguments f->setArgument(y); cout << f->getArgument() << endl;// >>y g->setArgument(y, 1); cout << g << endl;// >> angle(y,y) Looping, using member functions or interface (equivalent) Exprf= cos_s(x);// cos(x) for(size_t i = 0; i != f->size(); ++i) cout << f[i] << endl; // >>y

Exprg= angle_s(y, x);// angle(x,y) for(size_t i = 0; i != Size(g); ++i) cout << g[i] << endl; // >>x // >>y

8.1 Common mathematical functions

In table 8.1 are presented all common mathematical function types in CSL together with their name in the standard library and the symbolic version of CSL, applied on an arbitrary argument x. In sample code 52 is presented a quick example showing how to use these functions in CSL. Note that you have two equivalent ways to create an exponential

y = exp(x), csl::exp_s(x), (8.1) x y = e , csl::pow_s(CSL_E, x). (8.2)

These will be equivalent in the end but will not compose with each other. It is then recommended to use only one option. The exponential function may be more practical because it is easier to recognize and manipulate.

88 Maths name Standard name CSL type name CSL builder function abs(x) std::abs(x) csl::Type::Abs csl::abs_s(x) exp(x) std::exp(x) csl::Type::Exp csl::exp_s(x) log(x) std::log(x) csl::Type::Log csl::log_s(x) cos(x) std::cos(x) csl::Type::Cos csl::cos_s(x) sin(x) std::sin(x) csl::Type::Sin csl::sin_s(x) tan(x) std::tan(x) csl::Type::Tan csl::tan_s(x) acos(x) std::acos(x) csl::Type::ACos csl::acos_s(x) asin(x) std::asin(x) csl::Type::ASin csl::asin_s(x) atan(x) std::atan(x) csl::Type::ATan csl::atan_s(x) cosh(x) std::cosh(x) csl::Type::Cosh csl::cosh_s(x) sinh(x) std::sinh(x) csl::Type::Sinh csl::sinh_s(x) tanh(x) std::tanh(x) csl::Type::Tanh csl::tanh_s(x) acosh(x) std::acosh(x) csl::Type::ACosh csl::acosh_s(x) asinh(x) std::asinh(x) csl::Type::ASinh csl::asinh_s(x) atanh(x) std::atanh(x) csl::Type::ATanh csl::atanh_s(x)

Table 8.1: List of common CSL mathematical functions. For standard library functions, x is a number. For CSL functions, x may be a number or a symbolic expression.

Sample code 52: Mathematical functions basics Creating functions, deriving Exprx= variable_s("x"); Exprf= exp_s(cos_s(x)); Exprg= sin_s(x) * exp_s(cos_s(x));

cout << f << endl;// >> exp(cos(x)) cout << g << endl;// >> exp(cos(x))*sin(x) cout << g + Derived(f, x) << endl;// >>0 Same example using CSL_E Exprx= variable_s("x"); Exprf= pow_s(CSL_E, cos_s(x)); Exprg= sin_s(x) * pow_s(CSL_E, cos_s(x));

cout << f << endl;// >> exp(cos(x)) cout << g << endl;// >> exp(cos(x))*sin(x) cout << g + Derived(f, x) << endl;// >>0 Note In the second example, a special value of the log function has to be applied. When deriving, log(e) appears and is replaced by 1 automatically by CSL. Numerous special values are implemented.

See also Section 13.5 for derivation, section 6.7 for CSL_E.

89 8.2 Other functions

There is three functions left that are less common. The angle and factorial functions, and the Dirac delta.

8.2.1 Angle The angle corresponds to the careful arctan function. When computing an angle in a triangle from lengths, it is tempting to write things like b θ = arctan , (8.3) a if a and b are lengths of two sides of the triangle. However by writing this, we lose the absolute sign of a, keeping only the relative sign between a and b. This means that the result θ will be between −π/2 and π/2. We lose then the information about [−π, −π/2[ and ]π/2, π[. The angle function is exactly the resolution of this problem, writing instead

θ = angle(b, a). (8.4)

The angle function, when evaluated, takes care of the signs of a and b. The mathematical function corresponding in the standard library is std::atan2. Sample code 53 shows the basics on the angle function. In particular, this function may be evaluated with the flag csl::eval::numerical. See section 13.6 for details on evaluation.

90 Sample code 53: Basics on Angle Building some constants Expra= constant_s("a"); Exprb= constant_s("b"); Building the angle function Expr angle = angle_s(b, a); Expr atan_ = atan_s(b / a); Evaluating for different values a->setValue(1); b->setValue(1);// theta= 0.785398 cout << angle << endl; // >> angle(b,a) cout << Evaluated(angle, eval::literal) << endl; // >> angle(1, 1) cout << Evaluated(angle, eval::literal | eval::numerical) << endl; // >> 0.785398( = π/4 ) cout << Evaluated(atan_, eval::literal | eval::numerical) << endl; // >> 0.785398( = π/4 )

a->setValue(1); b->setValue(-1); // theta= 2.35619 cout << Evaluated(angle, eval::literal | eval::numerical) << endl; // >> 2.35619( = 3π/4 ) cout << Evaluated(atan_, eval::literal | eval::numerical) << endl; // >> -0.785398( = −π/4 ) Note You see here the proof that for angles not in [−π/2, π/2] the angle function gives a correct result through std::atan2, whereas the arctan function does not.

See also Section 13.6 for more details on evaluation.

8.2.2 Factorial

The factorial function represents x! and may be evaluated with the flag csl::eval::numerical as the other functions. Beware that large factorials will overflow C++ number types. Sample code 54 shows the basics on Factorial.

91 Sample code 54: Basics on Factorial Building Exprn= constant_s("n"); Exprf= factorial_s(n); cout << f << endl; // >>n! Evaluating n->setValue(5); cout << f << endl; // >>a! cout << Evaluated(f, eval::literal) << endl; // >> 5! cout << Evaluated(f, eval::literal | eval::numerical) << endl; // >> 120 See also Section 13.6 for more details on evaluation.

8.2.3 DiracDelta The Dirac delta is a very particular object that may not be evaluated. A Dirac delta is an object such that  0 if x 6= 0, δ(x) = ∞ else. (8.5) It may be generalized to tensors. If X and Y are two vectors in 4 dimensions we may define

(4) 0 0 1 1 2 2 3 3 δ (X − Y ) = δ X − Y δ X − Y δ X − Y δ X − Y . (8.6)

CSL may generate these two possible Dirac delta. Not that to create a multi-dimensional delta, you need to give indicial expressions as argument. The resulting expression is however never indexed. Sample code 55 presents how to build Dirac delta objects.

Sample code 55: Construction of DiracDelta Scalar delta Exprx= variable_s("x"); Expry= variable_s("y"); Expr delta_scalar = diracdelta_s(x-y); // >> delta{x+-y} Indicial delta TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Index mu("mu",&Minkowski); Expr delta_tensor = diracdelta_s(X(mu)-Y(mu)); // >> delta{X_mu+-Y_mu} See also Section 11.3 for more details on tensors.

This object is mostly manipulated by CSL-HEP in order to simplify integrals in computations,

92 applying the two following properties Z ddXδ(d)(X − Y )f(X,Y ) = f(Y,Y ), (8.7) Z d ddXe±iP ·X = (2π) δ(d)(P ), (8.8) with X, Y and P vectors in a d−dimensional space-time. This means that you should in general not use yourself a Dirac delta. However, note that you may use a CSL delta to use the following property yourself Z 1 y dxδ(ax − y)f(x, y) = f( , y), (8.9) |a| a with x and y scalar or tensorial variables. You may use the member fucntion applyDiracDelta() and give it the expression on which the delta must be applied, and the variable. An example is shown in sample code 56.

Sample code 56: Basics on DiracDelta Applying a scalar delta Exprx= variable_s("x"); Expry= variable_s("y"); Expra= constant_s("a"); Expr delta_scalar = diracdelta_s(a*x-y); Expr f = 2*a*x + y*x; cout << f << endl; // >> 2*a*x+x*y cout << delta_scalar->applyDiracDelta(f, x) << endl; // >> 2*(x+ 1/2*y^2/a)/abs(a) Applying a tensor delta TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Index mu("mu",&Minkowski); Expra= constant_s("a");

Expr delta_tensor = diracdelta_s(a*X(mu) - Y(mu)); Expr f = 2*a*Y(mu) + X(mu); std::cout << f << std::endl; // >> X_mu+ 2*a*Y_mu std::cout << delta_tensor->applyDiracDelta(f, X(mu)) << std::endl; // >> 2*(a*Y_mu+ 1/2*Y_mu/a)/abs(a) Note For the tensor delta, you have to give an expression as a variable, even for a tensor (X(mu) instead of X). Indices are not important you may give anything.

See also Section 11.3 for more details on tensors.

93 94 Chapter 9

Operators

9.1 Principle

All operators in CSL are linear operators. It means that for an operator O acting only on x and y for example, we have O (λ1x + λ2y) = λ1O(x) + λ2O(y). (9.1) These properties are applied automatically in CSL. Operators may be of any kind in CSL, because the linear behavior may have to be applied to several objects (standard derivative, integral, tensorial derivative). This is done in CSL through template inheritance. It is a bit technical, no need to know how it works. Simply know that any CSL object may be specialized in an operator using the Operator class that inherits from a template type (may be a function, a tensor. . . ). What defines an operator is simply what type of objects it acts on. With only this mathematical definition we may apply any type of linear operator in expressions.

9.2 RealPart, ImaginaryPart

Real and imaginary part apply on complex objects. CSL builds them if you ask for an undefined quantity. For example cout << GetRealPart(CSL_HALF) << endl;// >> 1/2 Exprc= constant_s("c", ComplexProperty::Complex); cout << GetRealPart(2*c + 1) << endl;// >>1+ 2*Re(c) They act as operators since we want to let only undetermined objects in them. Basics on real and imaginary parts are presented in sample code 57.

95 Sample code 57: Basics on real and imaginary parts Building a complicated complex expression Exprc= constant_s("c", ComplexProperty::Complex); Exprz= constant_s("z", ComplexProperty::Complex); Exprf=c+ CSL_I*c*(1 + z); Getting real part of f Expr real = GetRealPart(f); cout << real << endl; // >> Re(c)+ -(1+ Re(z))*Im(c)+-Re(c)*Im(z) Getting imaginary part of f Expr imaginary = GetImaginaryPart(f); cout << imaginary << endl; // >> Re(c)*(1+ Re(z))+ Im(c)+-Im(c)*Im(z) See also Section6 for constants, 14 for interface functions.

9.3 Derivative

Derivative objects, like real and imaginary parts, are used when some object is not defined. In this dx case, when the derivative of an object is not defined, for example . It allows to have symbolic dy derivative objects to perform computations, and evaluate them at the end (see also section 13.5 that explains how to derive expressions). Derivative objects may be evaluated without any flag (see section 13.6 for evaluation) when their argument becomes explicit, simply calling Evaluate() or Evaluated(). Ways to create derivatives are presented in sample code 58. Derivatives may also be created empty and enter expression. In this case, the derivative applies on all arguments to its right in products. Once a derivative has been applied, it is closed and does not apply to any other object. Consider three variables, y and z both depending on x. With CSL rules on empty derivative, one gets d d d  dz  y z = y (9.2) dx dx dx dx  d   d  dy dz · y · z = · . (9.3) dx dx dx dx

In CSL this example reads Exprx= variable_s("x"); Expry= variable_s("y"); Exprz= variable_s("z"); Expr dx = derivative_s(x);// Empty derivativew.r.t.x std::cout << prod_s({dx, y, dx, z}) << std::endl; // >>d/d(x)(y*d/d(x)(z)) std::cout << (dx * y) * (dx * z) << std::endl; // >>d/d(x)(y)*d/d(x)(z) Please note that in order to really make a product of several elements at the same time, you must use the prod_s() function taking a list of arguments (see section 7.2). If you do not, using operator* for example, C++ will decompose sub-products and the result is not well-defined. Always use the

96 Sample code 58: Construction of Derivative Building two variables Exprx= variable_s("x"); Expry= variable_s("y"); Building a derivative dy/dx from deriving Expr derivative = Derived(y, x); cout << derivative << endl;// >>d/d(x)(y) cout << GetType(derivative) << endl;// >> Derivative Building a derivative dy/dx explicitly Expr derivative = derivative_s(y, x); cout << derivative << endl;// >>d/d(x)(y) cout << GetType(derivative) << endl;// >> Derivative Building an empty derivative d/dx( Expr derivative = derivative_s(x);// stays opened cout << derivative << endl;// >>d/d(x)( cout << GetType(derivative) << endl;// >> Derivative Note Building a derivative explicitly, the mathematical derivation (when possible) will not take place if the expression is not evaluated. Only linear operator rules are applied.

See also Section 3.4 for type system, 13.5 for derivation. explicit function to build products of multiple arguments if there is operators inside. This gives Expr good = prod_s({dx, y, dx, z}); Expr bad = dx * y * dx * z; // You do not know in general how the product will be decomposed! cout << good << endl; // >>d/d(x)(y*d/d(x)(z)) cout << bad << endl;// actually corresponds to (1*2)*(3*4), not 1*2*3*4 // >>d/d(x)(y)*d/d(x)(z) You should in principle limit the most possible the use of chained operator* (or operator+) anyway, because it is very time-consuming as explained in section 7.2. Basics on derivatives, in particular how to evaluate them, is presented in sample code 59.

97 Sample code 59: Basics on Derivative

Building f(x) = 3a (x + y)3 Expra= constant_s("a"); Exprx= variable_s("x"); Expry= variable_s("y"); Expr f = 3*a*pow_s(x + y, 3); df  dy  Deriving, = 9a (x + y)2 1 + dx dx cout << Derived(f, x) << endl; // >> 9*a*(x+y)^2*(1+d/d(x)(y)) cout << derivative_s(f, x) << endl; // >> 3*a*d/d(x)((x+y)^3) You see here that using the Derived() function is more powerful that building a Derivative object. In this case, linear operator properties are applied (3a gets out) but the mathematical derivation does not take place. It is when the derivative is evaluated: cout << Evaluated(derivative_s(f, x)) << endl; // >> 9*a*(x+y)^2*(1+d/d(x)(y)) Deriving using properties of operators cout << derivative_s(x) * f << endl; // >> 3*a*d/d(x)((x+y)^3) cout << Evaluated(derivative_s(x) * f) << endl; // >> 9*a*(x+y)^2*(1+d/d(x)(y)) See also Section 13.5 for derivation, 13.6 for evaluation,6 for literals.

9.4 Integrals

Integrals in CSL are basically just containers. They act like linear operators (the same way as derivatives) but cannot be evaluated. The evaluation depends on the user. CSL-HEP uses only R 4 VectorIntegral objects like d X. In the following are presented basics on both types of integral, we do not present much details about them as they are very simple objects. Integrals have a variable, an operand, and bounds. By default, integrals go from −∞ to +∞. Please note that when a replacing takes place (see section 13.7), both the operand and the variable of integrals may be replaced.

9.4.1 ScalarIntegral Scalar integrals take a variable and an operand in construction (or just the variable to let the integral R empty and letting it act as an operator). They allow to manipulate objects like dxf(x). Ways to build a scalar integral are presented in sample code 60. You may access at any time the variable, operand, or bounds of a scalar integral. This is summed up in sample code 61.

98 Sample code 60: Construction of ScalarIntegral

Building a function f(x) Expra= constant_s("a"); Exprx= variable_s("x"); Expr f = a + 2*x + sin_s(x) / x; cout << f << endl; // >>a+ 2*x+ sin(x)/x R +∞ R 1 Building the integral explicitly, first −∞ (default), then −1. Expr integral = scalarintegral_s(f, x); cout << integral << endl; // >> 2*int{x}(x)+ int{x}(sin(x)/x) integral = scalarintegral_s(f, x, -1, 1);// integral between -1 and1 cout << integral << endl; // >> 2*int{x, -1, 1}(x)+ int{x, -1, 1}(sin(x)/x) Building the integral using operator properties integral = scalarintegral_s(x); cout << integral * f << endl; // >> 2*int{x}(x)+ int{x}(sin(x)/x) integral = scalarintegral_s(x, -1, 1); cout << integral * f << endl; // >> 2*int{x, -1, 1}(x)+ int{x, -1, 1}(sin(x)/x)

99 Sample code 61: Basics on ScalarIntegral Building an integral Exprx= variable_s("x"); Exprf= sin_s(x) / x; Expr integral = scalarintegral_s(f, x, -1, 1); cout << integral << endl; // >> int{x, -1, 1}(sin(x)/x) Manipulating the variable cout << integral->getVariable() << endl; // >>x Expry= variable_s("y"); integral->setVariable(y); cout << integral << endl; // >> int{y, -1, 1}(sin(x)/x) Manipulating the bounds cout <<"Inf␣=␣" << integral->getInfBoundary() << endl; // >> -1 cout <<"Sup␣=␣" << integral->getSupBoundary() << endl; // >>1 integral->setInfBoundary(-CSL_INF); integral->setSupBoundary(CSL_INF); cout << integral << endl; // >> int{y}(sin(x)/x) Manipulating the operand cout << integral[0] << endl; integral[0] = CSL_I; cout << integral << endl;// >> int{y}(i) cout << Refreshed(integral) << endl;// >>0 Note Refreshing the integral gives 0 because it does not depend on y.

Note You can manipulate the operand with get/setOperand() (for a name more explicit) or get/setArgument(). The operator[] and get/setArgument() versions are here to stay general when iterating in expressions (an integral has size 1 for example).

See also Section 13.2.2 for more details on refreshing CSL expressions.

100 9.4.2 VectorIntegral Vector integrals take a tensor as variable (Tensor, see section 11.3) and an operand in the construction (or just the tensor variable to let the integral empty and letting it act as an operator). They allow R 4 to manipulate objects like d Xf(X). Ways to build a vector integral are presented in sample code 62.

Sample code 62: Construction of VectorIntegral

Building a function f(X) Expra= constant_s("a"); TensorX("X",&Minkowski); TensorP("P",&Minkowski); Index mu("mu",&Minkowski); Exprf=a+ CSL_I*exp_s(CSL_I*X(mu)*P(+mu)); cout << f << endl; // >>a+i*exp(i*P_+%mu*X_%mu) R +∞ R 1 Building the integral explicitly, first −∞ (default), then −1. Expr integral = vectorintegral_s(f, X); cout << integral << endl; // >>i*int{X-4d}(exp(i*P_+%mu*X_%mu)) integral = vectorintegral_s(f, X, -1, 1);// integral between -1 and1 cout << integral << endl; // >>i*int{X-4d, -1, 1}(exp(i*P_+%mu*X_%mu)) Building the integral using operator properties integral = vectorintegral_s(X); cout << integral * f << endl; // >>i*int{X-4d}(exp(i*P_+%mu*X_%mu)) integral = vectorintegral_s(X, -1, 1); cout << integral * f << endl; // >>i*int{X-4d, -1, 1}(exp(i*P_+%mu*X_%mu)) See also Section 11.3 for more details on tensors.

You may access ay any time the tensor variable, operand, or bounds of a vector integral. This is summed up in sample code 63.

101 Sample code 63: Basics on VectorIntegral Building an integral TensorX("X",&Minkowski); TensorP("P",&Minkowski); Index mu("mu",&Minkowski); Exprf= exp_s(CSL_I*X(mu)*P(+mu)); Expr integral = vectorintegral_s(f, X, -1, 1); cout << integral << endl; // >> int{X-4d, -1, 1}(exp(i*P_+%mu*X_%mu)) Manipulating the parent of the tensor variable cout << integral->getParent()->getName() << endl; // >>X TensorY("Y",&Minkowski); integral->setParent(Y); cout << integral << endl; // >> int{Y-4d, -1, 1}(exp(i*P_+%mu*X_%mu)) Manipulating the bounds cout <<"Inf␣=␣" << integral->getInfBoundary() << endl; // >> -1 cout <<"Sup␣=␣" << integral->getSupBoundary() << endl; // >>1 integral->setInfBoundary(-CSL_INF); integral->setSupBoundary(CSL_INF); cout << integral << endl; // >> int{Y-4d}(exp(i*P_+%mu*X_%mu)) Manipulating the operand cout << integral[0] << endl; integral[0] = CSL_I; cout << integral << endl;// >> int{Y-4d}(i) cout << Refreshed(integral) << endl;// >>0 Note Refreshing the integral gives 0 because it does not depend on Y µ.

Note You can manipulate the operand with get/setOperand() (for a name more explicit) or get/setArgument(). The operator[] and get/setArgument() versions are here to stay general when iterating in expressions (an integral has size 1 for example).

See also Section 13.2.2 for more details on refreshing CSL expressions, 11.3 for tensors.

102 Chapter 10

Vectorial expressions

10.1 Principle

The idea behind vectorial expressions is not really to do symbolic manipulations with them. Vectors, matrices, high-dimensional tensors, are mostly used as simple containers for indexed tensors with which computations are done. Although you may do some calculations with vectorial expressions, I recommend to prefer other ways when possible because they have not been built for complex calculations. We will see however how to build and use these objects in the following. All containers are themselves expressions. They may contain any CSL expressions, given that the dimension is correct. In particular, CSL matrices have vectors as arguments, vectors have scalar objects . . . This design allows to generalize very quickly containers within CSL, but make them slow if using large ones. This is why their use should be restricted to simple containers. You may however use simple operations (addition, multiplication. . . ) in order to create more easily your containers. All operations on vectors are element-wise (no dot product with operator*). This requires that both operands have exactly the same shape.

10.2 Vector

Vectors are 1-dimensional containers of CSL expressions. They may be created through the function vector_s(). Basics on how to create vectors is presented in sample code 64. You may use basic mathematical operations with vectors. If the other operand is a scalar, the operation is applied to all the vector elements. If it is another vector, the element-wise operation is applied. Examples are shown in sample code 65.

103 Sample code 64: Construction of Vector

Building a vector of a given size (filled with 0) Expr vec4 = vector_s(4); cout << vec4 << endl; // >> {0,0,0, 0} Building a vector of a given size, filling it with a given expression Expr vec4 = vector_s(4, 2 + constant_s("a")); cout << vec4 << endl; // >> {2+a,2+a,2+a,2+a} Building a vector explicitly Expr vec4 = vector_s({1, 2, constant_s("a"), CSL_PI}); cout << vec4 << endl; // >> {1,2,a,\pi}

104 Sample code 65: Basics on Vector Building two vectors Expra= constant_s("a"); Exprb= constant_s("b"); ExprA= vector_s(4, a); ExprB= vector_s({1, a*b, a*b, 1}); Using operations to create a new vector Expr C = (A + 1) * (B / 2) - CSL_HALF; cout << A << endl; // >>{a,a,a,a} cout << B << endl; // >> {1,a*b,a*b, 1} cout << C << endl; // >> {1/2*a, -1/2+ 1/2*a*(1+a)*b, -1/2+ 1/2*a*(1+a)*b, 1/2*a} Iterating through the vector Expr vec = vector_s(5); // Member functions for(size_t i = 0; i != vec->size(); ++i) cout << vec->getArgument(i) << endl;// displays zeros // to change the argument: vec->setArgument(arg);

// operator[] and interface for(size_t i = 0; i != Size(vec); ++i) vec[i] = i*a + pow(i, 2); cout << vec << endl; // >> {0,1+a,4+ 2*a,9+ 3*a, 16+ 4*a} Using range-based for loop for(Expr &el : vec) el = CSL_I; cout << vec << endl; // >>{i,i,i,i,i} Note As all expressions with arguments, they may be accessed through operator[], the size with the member function ->size() or interface one, Size().

105 10.3 Matrix

Matrices are 2-dimensional containers of CSL expressions. They may be created through the function matrix_s(). Basics on how to create matrices is presented in sample code 66.

Sample code 66: Construction of Matrix

Building a 3 × 2 matrix (filled with 0) Expr mat_3_2 = matrix_s(3, 2); cout << mat_3_2 << endl; // >> {{0, 0}, {0, 0}, {0, 0}} Expr square = matrix_s(3); cout << square << endl; // >> {{0,0, 0}, {0,0, 0}, {0,0, 0}} Building a 2 × 2 matrix, filling it with a given expression Expr square = matrix_s(2, 2, 2 + constant_s("a")); cout << square << endl; // >> {{2+a,2+a}, {2+a,2+a}} Building a 2 × 2 matrix explicitly Expr square = matrix_s( {{1, 2}, {constant_s("a"), CSL_PI}}); cout << square << endl; // >> {{1, 2},{a,\pi}} Building a special matrix cout << identity_s(2) << endl; // >> {{1, 0}, {0, 1}} cout << diagonal_s({1, a}) << endl; // >> {{1, 0}, {0,a}}

You may use basic mathematical operations with matrices. If the other operand is a scalar, the operation is applied to all the matrix elements. If it is another matrix, the element-wise operation is applied. Examples are shown in sample code 67.

106 Sample code 67: Basics on Matrix Building two matrices Expra= constant_s("a"); Exprb= constant_s("b"); ExprA= diagonal_s({a, b}); ExprB= matrix_s( {{1, a*b}, {a*b, 1}}); Using operations to create a new matrix Expr C = (A + 1) * (B / 2) - CSL_HALF; cout << A << endl; // >>{{a, 0}, {0,b}} cout << B << endl; // >> {{1,a*b},{a*b, 1}} cout << C << endl; // >> {{1/2*a, -1/2+ 1/2*a*b}, {-1/2+ 1/2*a*b, 1/2*b}} Iterating through the matrix Expr mat = matrix_s(2, 3); for(size_t i = 0; i != Size(mat); ++i) for(size_t j = 0; j != Size(mat[i]); ++j) mat[i][j] = i*a + pow(j, 2); // Or mat->setArgument(arg,{i,j}); //+ mat->getArgument({i,j}) cout << mat << endl; // >> {{0,1, 4},{a,1+a,4+a}} Using range-based for loop for(Expr &row : mat) for(Expr &el : row) el = CSL_I; cout << mat << endl; // >>{{i,i,i},{i,i,i}} Note As all expressions with arguments, they may be accessed through operator[], the size with the member function ->size() or interface one, Size().

107 10.4 HighDTensor

High-D tensors are generic D-dimensional containers of CSL expressions. They may be created through the function highdtensor_s(). Note that when creating a high-D tensor of dimension 1 or 2, a vector or matrix will be automatically created by CSL instead. Basics on how to create high-D tensors is presented in sample code 68.

Sample code 68: Construction of HighDTensor From a shape Expr _3DTensor = highdtensor_s({2, 2, 2});// tensor2x2x2 cout << _3DTensor << endl; // >> {{{0, 0}, {0, 0}}, // >> {{0, 0}, {0, 0}}} From a shape, filling it with a value Expr _3DTensor = highdtensor_s({2, 2, 2}, CSL_PI);// tensor2x2x2 cout << _3DTensor << endl; // >> {{{\pi,\pi},{\pi,\pi}}, // >> {{\pi,\pi},{\pi,\pi}}} Explicitly, only for 3D tensors Expr _3DTensor = highdtensor_s( {{{0, 1}, {1, 0}},

{{0, -CSL_I}, {CSL_I, 0}},

{{1, 0}, {0, -1}}}); cout << _3DTensor << endl; // >> {{{0, 1}, {1, 0}}, // >> {{0,-i},{i, 0}}, // >> {{1, 0}, {0, -1}}}

You may use basic mathematical operations with high-D tensors. If the other operand is a scalar, the operation is applied to all the tensor elements. If it is another high-D tensor, the element-wise operation is applied. Examples are shown in sample code 69. Note that for high-dimensional tensors1 you can assign directly tensor elements. In other words, you may for example assign a matrix to the element of a 3D tensor. This is allowed only if shapes are matching of course. This gives for example, building a 3D tensor containing the three Pauli matrices Expr pauli_1 = CSL_HALF* matrix_s( {{0, 1}, {1, 0}}); Expr pauli_2 = CSL_HALF* matrix_s( {{0, -CSL_I}, {CSL_I, 0}});

1It is also possible for matrices but it is less useful.

108 Expr pauli_3 = CSL_HALF* matrix_s( {{1, 0}, {0, -1}}); Expr Pauli = highdtensor_s({3, 2, 2}); Pauli[0] = pauli_1; Pauli[1] = pauli_2; Pauli[2] = pauli_3; cout << Pauli << endl; // >> {{{0, 1/2}, {1/2, 0}}, // >> {{0, -1/2*i}, {1/2*i, 0}}, // >> {{1/2, 0}, {0, -1/2}}}

109 Sample code 69: Basics on HighDTensor Building two 3D tensors Expra= constant_s("a"); Exprb= constant_s("b"); ExprA= highdtensor_s({2, 2, 2}, -2); ExprB= highdtensor_s({2, 2, 2}, CSL_I); Using operations to create a new matrix Expr C = (A + 1) * (B / 2) - CSL_HALF; cout << A << endl; // >>{{a, 0}, {0,b}} cout << B << endl; // >> {{1,a*b},{a*b, 1}} cout << C << endl; // >> {{{-1/2+ -1/2*i, -1/2+ -1/2*i}, {-1/2+ -1/2*i, -1/2+ -1/2*i}}, // >> {{-1/2+ -1/2*i, -1/2+ -1/2*i}, {-1/2+ -1/2*i, -1/2+ -1/2*i}}} Iterating through the tensor Expr tensor = highdtensor_s({2, 2, 2}); for(size_t i = 0; i != Size(tensor); ++i) for(size_t j = 0; j != Size(tensor[i]); ++j) for(size_t k = 0; k != Size(tensor[i][j]); ++k) tensor[i][j][k] = i + a*j + a*a*k; // Or tensor->setArgument(arg,{i,j,k}); //+ tensor->getArgument({i,j,k}) cout << tensor << endl; // >> {{{0,a^2},{a,a+a^2}}, // >> {{1,1+a^2}, {1+a,1+a+a^2}}} Using range-based for loop for(Expr &matrix : tensor) for(Expr &row : matrix) for(Expr &el : row) el = CSL_I; cout << tensor << endl; // >> {{{i,i},{i,i}}, // >>{{i,i},{i,i}}} Note As all expressions with arguments, they may be accessed through operator[], the size with the member function ->size() or interface one, Size().

110 Chapter 11

Indicial expressions

11.1 Principle of indicial computations

11.1.1 Motivation Indicial computations are crucial in high-energy physics. Most of the calculations use indices, prop- erties of tensors etc. This is why CSL gives a particular care to indexed tensors. When manipulating them, it is very often more practical to de computations with explicit indices rather than matrices. For example, a vectorial expression like bT Ma, (11.1) with a and b two vectors and M a matrix, may be written with explicit indices X X biMijaj. (11.2) i j

Using Einstein’s convention, we do not write sums explicitly and consider that an index appearing twice is automatically summed over. This gives

biMijaj. (11.3)

For simple vectors and matrices, indicial manipulation allows to get rid of commutation problems. To matrices A and B do not commute in general, AB 6= BA. But with indexed expressions we manipulate numbers, that commute in general, AijBjk = BjkAij. Furthermore, with matrices we may represent contractions only linearly (from left to right). Writing conventions do not really allow more. With high-dimensional tensors, we cannot in general represent the contractions so we are forced to use indices. CSL makes the choice to have no hybrid object, i.e. indicial on some dimensions and vectorial on others. An example of hybrid object in high-energy physics is the γ−matrix. This tensor has 3 dimensions. The first in the Minkowski space, the two other in Dirac space. Usually we index the Minkowski space and keep the Dirac one vectorial. This gives a 4x4 matrix (Dirac space in 4-dimensional) with one Minkowski index γµ. In µ CSL we use instead fully indexed expressions like γαβ to manipulate only literals commuting with each other, and have a completely general framework.

11.1.2 Parents and elements This is a fundamental feature to understand how indicial computations work in CSL. It is motivated by the separation between intrinsic (abstract) and not intrinsic (specific) features of a tensor.

111 • Abstract features are the properties shared by all tensor elements (name, vector spaces, contraction property).

• Specific features are different between each element (indices, complex conjugated or not, . . . ).

You can see tensors the same way as functions. Consider a function

f : R → R (11.4) x 7→ f(x). (11.5)

There is an abstract function f (containing all properties) and elements f(x) that simply reference f. f is unique, whereas the number of elements f(x) has no limit. For a tensor, the story is the same. You may define similarly the γ−matrix as a function

γ : {IM ,ID,ID} → C (11.6) µ {µ, α, β} 7→ γαβ, (11.7) with IM and ID index spaces for Minkowski and Dirac respectively. µ In CSL γ is called the Parent, and γαβ are called Element. Here are requirements of parents and elements:

• Parents: contain all properties of tensors and are unique in the program for a given tensor. May generate elements on demand giving that the corresponding indices are given.

• Elements: contains only properties specific to the element, plus a pointer to its parent. The element can then now any of its property asking its parent.

Parents follow the same inheritance hierarchy as elements. They all inherit from AbstractParent (whose shared pointer is called Parent) and implement new features through virtual functions. You may in particular convert any time a parent with Parent IKnowThisIsATensor = getParentFromSomeTensor(); Tensor tensor = std::dynamic_pointer_cast(IKnowThisIsTensor); If however the conversion does not work (meaning the it was impossible because the wrong type has been given), the returned tensor contains nullptr.

11.1.3 Knowing if an expression is indexed

Sample code 70: Knowing if an expression is indexed Using member function Expr expr = maybeIndexed(); cout << expr->isIndexed() << endl;//1 or0 Using interface function Expr expr = maybeIndexed(); cout << IsIndexed(expr) << endl;//1 or0

112 11.2 Space and Index

Before talking about tensors, let’s concentrate on simpler objects. We saw that the link between parents and elements is indices. In particular parent(indices) may generate an element. Indices live in vector spaces.

11.2.1 Space Principle

Each CSL index keeps a pointer to its vector space. This space is basically a name, a dimension, and a set of base tensors common to each space:

• The Kronecker delta δij.

• The metric gij if it is not trivial (equal to the Kronecker delta).

• The epsilon symbol like ijk in 3D.

When building a space, you must give it a name and a dimension. You may give a tensor metric i or not. If yes, the indices will be signed, i.e. A 6= Ai. Construction of Space is presented in sample code 71. You may give a list of index names when building a space. You can at any time build an index with the name you want, but if you do not provide a name the space will pick one in the list you provided (or in the default list i, j, k, l). The way a space is given to function is through pointers. In very particular cases, an index may have no space for example. This means that you should be careful when giving these pointers. Concerning spaces, please note several points:

• All CSL built-in spaces are static global variables.

• Declaring your own spaces should follow the same principle: static and global (Space mySpace(args) before your main for example). This ensures that the pointer to it stays valid all along.

• Spaces are always passed to functions through (const-)pointers, like f(&mySpace). See section 2 for details on pointers.

• CSL functions that return spaces return pointers. In that case, you may use directly the return value in other functions like f(cslSpace).

Sample code 72 presents how to gather basic tensors. Other properties of spaces will be presented will associated objects (Index, Tensor).

Global space variables

There is four global space variables in CSL. They all are Space const. In particular, as they are not pointers, you must give them with a & to CSL functions (all take Space const*).

• Euclid_R2. Flat 2D space. Pass as &Euclid_R2 to functions.

• Euclid_R3. Flat 3D space. Pass as &Euclid_R3 to functions.

• Euclid_R4. Flat 4D space. Pass as &Euclid_R4 to functions.

113 Sample code 71: Construction of Space Flat space Space flat3F("R_3", 3);// Defaulti,j,k,l indices Space flat3F("R_3", 3, {"l","m","n","p"}); Signed space with a metric Expr g_matrix = matrix_s( {{-1, 0, 0}, {0, 1, 0}, {0, 0, 1}} ); Space minko3D("Minko_3", 3,"g", g_matrix);// Defaulti,j,k,l indices Space minko3D("Minko_3", 3,"g", g_matrix, {"\\mu","\\nu","\\rho"}); Making sure the space stays alive // Declare before or in the main scope, not ina sub-scope Space good1("good1", 3); int main() { Expr expr; Space good2("good2", 3); { Space bad("bad", 3); expr = functionUsing3Spaces(); } cout << expr << endl; // May crash if tensors in expr reference the bad space! } Warning For now there is no support in CSL for not constant metric, i.e. depending on coordinates for example.

See also Section 10 for details on vectorial expressions.

• Minkowski. 4-dimensional Minkowski space-time, with {µ, ν, ρ, σ, λ, τ} as default index names.  1 0 0 0   0 −1 0 0  Pass as &Minkowski to functions. The metric is gµν =  .  0 0 −1 0  0 0 0 −1

114 Sample code 72: Basics on Space Getting tensors directly const Space *space = getSpaceOfSomething(); Tensor delta = space->getDelta(); Tensor metric = space->getMetric(); Tensor eps = space->getEpsilon(); Using auto and interface to forget about types auto space = getSpaceOfSomething(); auto delta = GetDelta(space); auto metric = GetMetric(space); auto eps = GetEpsilon(space); See also Section2 if you want details on pointers, auto or const keywords.

11.2.2 Index Indices are designed to be as light as possible. It is one of the rare CSL object that is static, i.e. that is not dynamically allocated. They must have a name, an id, a sign (if the metric is not trivial), a type (summed or not), and keep a pointer to the space they live in. All of this is contained in 24 Bytes.

Index ids Ids are not used in computations by hand in general. It is however extremely important in symbolic manipulations. Index names are limited, and when an expression becomes big, some indices will have the same name even if they are different. Consider the equation

ijkilm = δjlδkm − δjmδkl. (11.8)

Now if you are able to use only i, j, k indices it may look like

i1j1k1 i1i2j2 = δj1i2 δk1j2 − δj1j2 δk1i2 . (11.9)

It is harder to read, but much more practical to automate than a name finder. While it is possible to create indices yourself given a name and a space, I highly recommend to delegate the index creation to its space that will generate automatically an index with an id independent of others. Ways to build a csl::Index are presented in sample code 73. The ids become very long in a short amount of time when doing computations, and you get indices like i_3452. This may be particularly messy on standard output. You can enable/disable the ids in outputs with the option csl::option::printIndexIds. See section 16.2 for more details.

115 Sample code 73: Construction of Index Directly, not recommended in general Space mySpace("mySpace", 3); Index i("i", &mySpace); Index j("j", &mySpace); Delegating to a space (pointer in the example) const Space* space = getSpaceFromSomething(); Index i = space->generateIndex("i"); Index j = space->generateIndex();// Let the space find the name Using interface const Space* space = getSpaceFromSomething(); Indexi= GenerateIndex(space,"i"); Indexj= GenerateIndex(space);// Let the space find the name

11.2.3 Basics on indices The basic use of indices is building and giving them to tensors. They have a simple interface that you can use, see the documentation (Index) for more information. In sample code 74 are presented the most important features that you really need to know to use indices.

116 Sample code 74: Basics on Index

Raise, lower, flip indices with +/-, getFlipped(). By default, indices are lowered. const Space* someSpace = getSpaceFromSomething(); Index mu_low("mu", someSpace); Index mu_up = +mu_low;

cout << mu_low << endl;// >> mu cout << +mu_low << endl;// >>+mu cout << mu_low.getFlipped() << endl;// >>+mu

cout << mu_up << endl;// >>+mu cout << -mu_up << endl;// >> mu cout << mu_up.getFlipped() << endl;// >> mu Create indices with special (integer) values Tensor tensor = tensorTakingOneIndex(); cout << tensor(Index(0)) << endl;// >>A_+0 cout << tensor(-Index(1)) << endl;// >> A_1 // Possible to not specify Index, but dangerous for // the -0 case cout << tensor(0) << endl;// >>A_+0 cout << tensor(-0) << endl;// >> A_+0 Warning You see that from special lower values you must build an index from an integer first in general. This is because for the 0 case, -0 gives directly 0. You then have the wrong sign on your index.

11.3 Indicial tensors (Tensor)

11.3.1 Principle

i Indicial tensors are the simplest indexed building blocks of expressions, like Ai or δj. As with explained in section 11.1.2, there are several objects defining tensors:

• A TensorParent, abstract tensor containing all the properties. This object is unique in the program and generates the elements.

• TensorElement objects, actually entering in expressions. They do not possess information about their own properties but have a pointer to their parent to access them.

11.3.2 Building In order to create an indexed tensor, one first has to build or get its parent, then give it indices to generate an element. As always, we do not manipulate static objects. To express the fact that lifetimes of tensors are not fixed (may live until we do not need them anymore), we create shared pointers to them. This gives:

• TensorParent becomes std::shared_ptr (a.k.a Tensor).

• TensorElement becomes std::shared_ptr (a.k.a Expr).

117 Keep in mind that both objects (parent and element) are (shared-) pointers. In particular you will need to use the arrow -> (or interface functions) to access features. The user interface to use them is however very simple. It consists in code like Tensor A(constructionArgs); Expr A_ij = A({i, j}); When giving indices to a tensor, you either give it directly if there only one, or give the list on indices between curly braces {}. An example of construction of tensor is given in sample code 75.

Sample code 75: Construction of TensorElement Building the parent directly TensorA("A", {&Euclid_R3, &Euclid_R3}); Building equivalently the parent using tensor_s() TensorA= tensor_s("A", {&Euclid_R3,&Euclid_R3}); Creating the indices you need Indexi= GenerateIndex(&Euclid_R3,"i"); Indexj= GenerateIndex(&Euclid_R3,"j"); Playing with the tensor TensorX("X",&Euclid_R3); cout << A({i, j}) << endl;// >>A_{i,j} cout << 2*A({i, j})*X(j) << endl;// >> 2*A_{i,%j}*X_%j Note The % symbol in front of j means that it has been recognized as summed over by CSL.

See also Section 11.2 for spaces and indices.

You can access the parent of an element calling the member function ->getParent() or the interface function GetParent(). This gives TensorX("X",&Minkowski); Index mu("mu",&Minkowski); Expr element = X(mu); Tensor X_from_el = GetTensorParent(X); cout << (X.get() == X_from_el.get()) << endl;// Comparing memory addresses // >>1

11.3.3 Manipulating parents There is several interface functions taking an expression (must be a tensor) as argument and returning the corresponding parent. If you try to get an ill-defined parent (tensor field parent for a simple tensor for example) CSL will raise an error.

• GetTensorParent(). Returns the tensor parent (Tensor) of the element.

• GetTensorFieldParent(). Returns the tensor field parent (TensorField) of the element.

• GetTDerivativeParent(). Returns the tensor derivative parent (TDerivative) of the element.

118 • GetParent(). Returns the parent (Parent) whatever the tensor type is. Allows to compare pointers and call member functions. To compare parents, independently on their types, you may just compare pointers. As they are unique, comparing their memory address is enough. Recall that parents are shared pointers. This is shown in sample code 76.

Sample code 76: Basics on getting tensor parents

Expr someTensor = someTensor(); Expr someOtherTensor = someOtherTensor(); Tensor parent1 = GetTensorParent(someTensor); TensorField parent2 = GetTensorFieldParent(someOtherTensor); if (parent1.get() == parent2.get()) cout <<"Same␣parents!" << endl; else cout <<"Different␣parents!" << endl; Warning Beware that because of automatic ordering of indices, getting an element may yield something like −Aij that is not a tensor but a product. When getting the parent, be sure that the object is indeed a tensor, by checking its type for example (see section 3.4).

11.3.4 Setting properties to tensors In practice indexed tensors have in general some properties we want to apply. In the following are presented a few features of tensor properties. See tutorial 17.3 that shows examples.

Symmetry, anti-symmetry Tensors have symmetries. You may define either full (anti-)symmetry of a given tensor, or a custom one. Let’s start with simple cases. Consider two tensors Sij and Aij, symmetric and anti-symmetric respectively. One has

Sij + Sji = Sij + Sij = 2Sij, (11.10)

Aij + Aji = Aij − Aij = 0. (11.11) Within CSL you can define this kind of property very easily. CSL does not compute anything to simplify expressions like the previous example. It simply orders indices within the possible tensor symmetries, and keeps the simplest1. In other words if i, j, k are three fully (anti-)symmetric indices, CSL will always reorder them as ijk → ijk, (11.12) ikj → (−)ijk, (11.13) jik → (−)ijk, (11.14) jki → ijk, (11.15) kij → ijk, (11.16) kji → (−)ijk. (11.17) (11.18)

1As we said in section 3.9, index structures are sorted alphabetically.

119 An example of such simplifications is shown in sample code 77. There is three levels of precision to define a the symmetries of a tensor.

• Full (anti-)symmetry. May be set with ->setFullySymmetric() or ->setFullyAntiSymmetric() acting on a Tensor object.

• (Anti-)symmetry by pairs. May be set with ->addSymmetry() and ->addAntiSymmetry() acting on a Tensor object, giving integer positions of the two indices in the symmetry.

• Using Symmetry. When the symmetry is more complex. See the end of this section.

Sample code 77: Tensor symmetries Defining two tensors and indices TensorS("S", {&Euclid_R3, &Euclid_R3}); TensorA("A", {&Euclid_R3, &Euclid_R3}); Indexi= GenerateIndex(&Euclid_R3,"i"); Indexj= GenerateIndex(&Euclid_R3,"j"); Setting the respective full (anti-) symmetries S->setFullySymmetric(); A->setFullyAntiSymmetric(); Setting the respective (anti-) symmetries explicitly from index positions S->addSymmetry(0, 1); A->addAntiSymmetry(0, 1); Testing the properties cout << S({i, j}) + S({j, i}) << endl;// >> 2*S_{i,j} cout << A({i, j}) + A({j, i}) << endl;// >>0

When tensors have more complicated symmetries it may not be reduced to a set of (anti- )symmetric pairs of indices. When this is the case, you may use the Symmetry object. To illustrate this, consider the Riemann tensor in General Relativity Rµνρσ. Its fundamental symmetries are

Rνµρσ = −Rµνρσ, (11.19)

Rµνσρ = −Rµνρσ, (11.20)

Rρσνµ = Rµνρσ. (11.21)

The last symmetry is not just a pair permutation. The permutation maps

µ 7→ ρ ν 7→ σ ρ 7→ µ σ 7→ ν,

120 or replacing (µ, ν, ρ, σ) by (0123)

0 7→ 2 1 7→ 3 2 7→ 0 3 7→ 1.

In other words, the Riemann tensor has two anti-symmetries with pairs (01) and (23), and a symmetry whose cycles are {(02), (13)}. A Symmetry in CSL is defined exactly with these numbers, i.e. the set of the cycles in the symmetry. The Riemann tensor example is shown in sample code 78.

Sample code 78: Basics on Symmetry Building the tensor from Minkowski space Tensor Riemann( "R", {&Minkowski,&Minkowski,&Minkowski,&Minkowski}); Building an empty symmetry of 4 indices Symmetry riemannSymmetry(4); Adding the three Riemann symmetries riemannSymmetry.addSymmetry({0, 1}, -1); riemannSymmetry.addSymmetry({2, 3}, -1); riemannSymmetry.addSymmetry({{0, 2}, {1, 3}}, 1); Setting the symmetry to the tensor Riemann->setSymmetry(riemannSymmetry); Supposing indices exist, testing some permutations cout << Riemann({mu, nu, rho, sigma}) << endl;// >>R_{mu, nu, rho, sigma} cout << Riemann({nu, mu, rho, sigma}) << endl;// >>-R_{mu, nu, rho, sigma} cout << Riemann({mu, rho, sigma, nu}) << endl;// >>-R_{mu, rho, nu, sigma} cout << Riemann({rho, sigma, nu, mu}) << endl;// >>-R_{mu, nu, rho, sigma} Warning You must define the number of indices in the symmetry when building a symmetry object in general. If you don’t, CSL will tell you and raise an error when trying to add a permutation.

11.3.5 Complex, Transposed, Hermitian properties For a given tensor, you may define any property under transposition, complex or hermitian conjuga- tion, like

AT = −A, (11.22) A∗ = 1, (11.23) 㵆 = γ0γµγ0. (11.24)

121 These properties are easy to define in CSL through setTransposedProperty(), setComplexProperty(), setHermitianProperty() for tensors. For examples on these properties, please refer to the already detailed tutorial 17.3, or the documentation of tensors (TensorParent).

11.3.6 Contraction properties For a given tensor, you may define any self contraction (involves exactly two tensors of the same kind) property like

AijAjk = Aik, (11.25)

AijAji = 4, (11.26) or more complicated chain contractions like

µ γαβpµuβ = muα. (11.27)

By default, self contractions are enabled and chain contraction are not. See section 16.2 for details on these options. For examples on self contraction properties, please refer to the already detailed tutorial 17.3, or the documentation of tensors (TensorParent). The chain contractions are heavier for CSL. They should be used with parsimony. They consist in detecting a set of tensors contracted in a particular way. I took the Dirac equation example, pu/ = mu. There is three tensors contracted together γ, p and u, in two different spaces, Dirac and Minkowski. You may define this kind of properties in CSL. The Dirac equation example is shown in sample code 792. Note that contraction chains will not allow you to define very complex properties (in particular with conditions that are not index contraction). In these cases, consider using algorithms instead (section 15). They are completely general and will be lighter in terms of execution time (although it will force you to write some code yourself).

2Replacing the Dirac space by a standard R4 space.

122 Sample code 79: Basics on chain contraction Defining the tensors Tensor p("p",&Minkowski); Tensor u("u",&Euclid_R4); Tensor gamma("gamma", {&Minkowski,&Euclid_R4,&Euclid_R4}); Defining the indices Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Index a("a",&Euclid_R4); Index b("b",&Euclid_R4); Defining the property. You must give the list of tensors in the contraction, and of course the result. Exprm= constant_s("m"); u->addContractionProperty( {gamma({+mu,a,b}), p(mu), u(b)}, m*u(a)); Testing the contraction option::applyChainContractions = true; cout << gamma({+mu,a,b}) * p(nu) * u(b) << endl;// mu and nu not contracted // >> p_nu*u_%b*gamma_{+mu,a,%b} cout << gamma({+mu,b,a}) * p(mu) * u(b) << endl;// gamma is transposed // >> p_%mu*u_%b*gamma_{+%mu,%b,a} cout << gamma({+mu,a,b}) * p(mu) * u(b) << endl;// Right contraction // >>m*u_a Warning You must set option::applyChainContractions to true or the example will not work.

Note You need to define the property only for one of the tensors in the contraction. To save time, please define it for the less frequent one. If the tensor appears frequently but without the property, it will be tested many times for nothing.

11.3.7 Tensor values

A tensor may have values. These will be used if the expression is evaluated. Consider xixi , with xi a tensor. You may evaluate the expression if xi has values, for example (x, y, z). In this case the evaluation yields 2 2 2 xixi = x + y + z . (11.28)

By default, a tensor has constant values. For example, defining Aij in a 2D space in CSL, A gets automatically a tensor filled with Constant objects of the type  A A  00 01 . (11.29) A10 A11 You may however define your own tensor as you want. Suppose that you want  a c  . c∗ b (11.30) In sample code 80 are shown the two ways to build such a tensor.

123 Sample code 80: Tensor values The wanted tensor values Expra= constant_s("a"); Exprb= constant_s("b"); Exprc= constant_s("c", ComplexProperty::Complex); Expr c_start = GetComplexConjugate(c); Expr tensor = matrix_s( {{a, c}, {c_star, b}}); At the construction TensorA("A", {&Euclid_R2, &Euclid_R2}, tensor); cout << A->getTensor() << endl; Later TensorA("A", {&Minko2D, &Minko2D}); cout << A->getTensor() << endl; // >>{{A_{0,0},A_{0,1}}, // >>{A_{1,0},A_{1,1}}} A->setTensor(tensor); cout << A->getTensor() << endl; // >>{{a,c}, // >>{c^(*),b}} See also Section 10 for details on matrices.

11.4 Indicial tensor fields (TensorField)

Tensor fields inherits from all regular tensor properties. For any question about indices you may want to see section 11.3. A tensor field is also indexed by a point in space, like Aµ(X). This allows to tensor to carry an additional information that may be mandatory in some computations. The point is itself a tensor, in particular a one-index tensor in a given space. You may see how to build a tensor field in sample code 81. You must give the space in which the point lives. The rest is like a regular tensor.

Sample code 81: Construction of TensorField

Building gµν(X) TensorField g("g",&Minkowski, {&Minkowski,&Minkowski}); TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); cout << g({mu, nu}, X) * g({+nu, +mu}, Y) << endl; // >> g_{%mu,%nu}(X)*g_{+%nu,+%mu}(Y)

124 11.5 Tensorial derivative (TDerivative)

The tensorial derivative inherits from all regular tensor field properties. It behaves like a linear operator, see section9 for more details. See sample code 82 to see how to create and manipulate a tensorial derivative.

Sample code 82: Construction of TDerivative

1 µ Building 2 (∂µ φ(X))(∂ φ(X)) TensorField phi("phi",&Minkowski, {});// No indices, empty list TDerivative d("d",&Minkowski); TensorX("X",&Minkowski); Index mu("mu",&Minkowski); cout << CSL_HALF * (d(+mu, X)*phi(X))*(d(mu, X)*phi(X)) << endl; // >> 1/2*d_+%mu(X)(phi(X))*d_%mu(X)(phi(X)) Warning Do not forget parenthesis when using any operator, in particular a tensorial derivative.

See also Section9 for more information on operators.

11.6 ISum

Indicial sums in CSL are a specialization of simple sums. In other words, when building a sum with sum_s or the operator+, CSL checks if there is an indicial argument. If yes, an indicial sum is built, else a regular one. It means in particular that regular sums have no tool to treat indicial objects. For details on how sums work, please see section 7.1. Indicial sums have the same type as regular ones, csl::Type::Sum. Specialization of the sum consists in checks for indicial validity and careful comparisons (of arguments or with each other). For example,

AiBi + AjBj (11.31) would not be simplified at first because i and j are different indices. CSL knows however how to compare properly indicial expressions and indicial sums do it to further simplify expressions. An example is shown in sample code 83.

125 Sample code 83: Basics on ISum Building tensors TensorA("A",&Euclid_R3); TensorB("B",&Euclid_R3); TensorC("C",&Euclid_R3); Building indices Index i("i",&Euclid_R3); Index j("j",&Euclid_R3); Building sums Expr expr = A(i)*B(i) + A(j)*C(j);// Builds an ISum cout << Factored(expr) << endl; // >>A_%i*(B_%i+C_%i) cout << A(i) + B(j) << endl; // >> Error 13: Invalid sum of indicial objects in // >> Sum::selfCheckIndexStructure() const Note The indicial sum checks that free structures are the same between all arguments. If not, an error is raised and the program stops.

See also Section 13.4 for details about factorization.

11.7 IProd

Indicial products in CSL are a specialization of simple products. In other words, when building a product with prod_s or the operator*, CSL checks if there is an indicial argument. If yes, an indicial product is built, else a regular one. It means in particular that regular products have no tool to treat indicial objects. For details on how products work, please see section 7.2. Indicial products have the same type as regular ones, csl::Type::Prod. Specialization of the prod consists in checks for indicial validity and careful comparisons (of arguments or with each other). Products have much more work than sums in the treatment of indices. They have to contract them properly (following Einstein’s convention), check that there is no redundancy (no more than two times the same index in a product), and apply properties. Properties and contractions are applied in chain until there is no more contraction found. This may be heavy for some expressions, but it is mandatory to maximally simplify them. Any index contraction (A_i*B_i → A_%i*B_%i), any contraction property (with delta or metric tensor, or user defined properties) happen in the construction of an indicial product. An example is shown in sample code 84.

126 Sample code 84: Basics on IProd Building tensors TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Tensorg= GetMetric(&Minkowski); Building indices Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Index rho("rho",&Minkowski); Building products. In this example, the indicial product must contract first µ, then recognize the metric contraction, replace µ by ν in X, and finally contract ν. cout << X(+mu)*Y(+nu)*g({mu, nu}) << endl; // >>X_%nu*Y_+%nu µ Same example with a contraction property for X Yµ Exprm= constant_s("m"); option::applyChainContractions = true; X->addContractionProperty({X(+mu), Y(mu)}, pow_s(m, 2)); cout << X(+mu)*Y(+nu)*g({mu, nu}) << endl; // >>m^2 Warning You must set option::applyChainContractions to true or the example will not work.

See also Section 11.3 for details on tensor properties.

127 128 Chapter 12

Abbreviations

12.1 Principle

Abbreviations have been introduced in CSL in order to save time when some expressions appear a lot of time but do not intervene in computations. CSL does not define abbreviations himself, but may create them checking each time that no similar abbreviation has already been defined. Abbreviations may be scalar or indexed, CSL handles both cases.

12.2 Creating and printing

To create an abbreviation, you just have to call the function Abbrev::makeAbbreviation() described in sample code 85. It is a static member function of class Abbrev. You have to call it as if it where in a namespace Abbrev.

Sample code 85: Abbrev::makeAbbreviation()

Parameters name Name of the abbreviation (optional). If not given, a default name ’Ab’ is used. expr Expression you want to abbreviate. May be indexed. Returns

The abbreviation, constant or indexed tensor.

Note The name of an abbreviation is generic. Making two abbreviations with the same name just changes an ID. The name is still considered the same (like Ab_0001 and Ab_0002).

To print all current abbreviations, you may call the function Abbrev::printAbbreviations(). Examples of abbreviations are shown in sample code 86.

129 Sample code 86: Basics on abbreviations - part 1 Building an abbreviation twice Expra= constant_s("a"); Exprb= constant_s("b"); Expr ab = Abbrev::makeAbbreviation(2*a*b); Expr ab2 = Abbrev::makeAbbreviation(2*a*b); Expr ab3 = Abbrev::makeAbbreviation(2*b*b); cout << GetType(ab) << endl;// >> Constant cout << ab << endl;// >> Ab Abbrev::printAbbreviations(); // >>2 abbreviations: // >> Ab: 2*a*b // >> Ab_0001: 2*b^2 One can see that the abbreviation 2ab has not been created twice. CSL recognizes that it already exists and does not create a new one.

Building an indexed abbreviation TensorA("A",&Minkowski); TensorB("B",&Minkowski); Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Expr ab = Abbrev::makeAbbreviation( "Ind", 2*A(mu)*B(nu)); cout << GetType(ab) << endl;// >> TensorElement cout << ab << endl;// >> Ind_{mu,nu} Abbrev::printAbbreviations(); // >>1 abbreviations: // >> Ind: 2*A_mu*B_nu Trying to create the same indexed abbreviation Index rho(rho",␣&Minkowski); ␣␣␣␣Index␣sigma("sigma",␣&Minkowski); ␣␣␣␣Expr␣ab2␣=␣Abbrev::makeAbbreviation( ␣␣␣␣␣␣␣␣␣␣␣␣"Ind", ␣␣␣␣␣␣␣␣␣␣␣␣2*A(rho)*B(sigma)); ␣␣␣␣cout␣<<␣GetType(ab2)␣<<␣endl;␣//␣>>␣TensorElement ␣␣␣␣cout␣<<␣ab2␣<<␣endl;␣//␣>>␣Ind_{rho,␣sigma} ␣␣␣␣Abbrev::printAbbreviations(); ␣␣␣␣//␣>>␣1␣abbreviations: ␣␣␣␣//␣>>␣Ind␣:␣2*A_mu*B_nu ␣␣␣␣ One can see that the abbreviation is not created twice, but indices of the second abbreviation demand are adapted.

See also Section 11.3 for more details on tensors.

130 12.3 Evaluating

Abbreviations may be evaluated, i.e. be replaced by the expressions they contain. You can either

• Evaluate all abbreviations with flag csl::eval::abbreviation.

• Evaluate only abbreviations with a given name (independently of their id).

• Evaluate only one abbreviation.

All three ways are presented in sample code 87. The first one just uses an evaluation flag (see section 13.6). The others need the use of Abbrev::enableEvaluation() and Abbrev::disableEvaluation() giving a name (name of the family, independent on the id) or a specific abbreviation. By default evaluation is disabled. If enabled for a given abbreviation, it will be replaced by the initial expression if evaluated.

Sample code 87: Basics on abbreviations - part 2 Creating three sample abbreviations Expra= constant_s("a"); Exprb= constant_s("b"); Expr ab_1 = Abbrev::makeAbbreviation("Square", a*a); Expr ab_2 = Abbrev::makeAbbreviation("Square", b*b); Expr ab_3 = Abbrev::makeAbbreviation("Mixed", 2*a*b);

Expr f = ab_1 + ab_2 + ab_3; cout << f << endl;// >> Mixed+ Square+ Square_0001 Evaluating only Mixed Abbrev::enableEvaluation("Mixed"); cout << Evaluated(f) << endl;// Square+ Square_0001+ 2*a*b Abbrev::disableEvaluation("Mixed"); Always remember to disable the evaluation after the computation if not needed anymore.

Evaluating only Square_0001 Abbrev::enableEvaluation(ab_2); cout << Evaluated(f) << endl;// Mixed+ Square+b^2 Abbrev::disableEvaluation(ab_2); Evaluating all abbreviations cout << Evaluated(f, eval::abbreviation) << endl; //a^2+ 2*a*b+b^2 Always remember to disable the evaluation after the computation if not needed anymore.

See also Section 13.6 for more details on evaluation in general.

131 132 Chapter 13

Expression modifiers

Expression modifiers are the very first features you may want to know to effectively do symbolic manipulations. The modifiers presented in the following are standard manipulations of expressions. Their number is limited, and you may need sometimes more complicated functions. For less standard modifiers, the next logical step is to look at algorithms, detailed in section 15. They are very general functions that allow to write relatively custom modifiers of expressions.

13.1 General principle

Functions presented in this chapter are all present in files utils.h or interface.h. In the following, only interface-type functions are presented. For each one, an expression member function exists (expr->derive(args) instead of Derive(expr, args) for example), but sometimes the return-type is not trivial and in general it is recommended to use the interface. All expression modifiers work the same way. They take the expression to modify (and possibly other arguments), each time with two possibilities. The two kinds are • First kind. Takes a const argument and returns a new expression. These functions have a ’-ed’ suffix, like Replaced(). This means that it returns the ’replaced version’ of the given expression, without modifying the initial one.

• Second kind. Returns nothing and modifies the argument. These functions have no suffix, like Replace(). This means that it does the replacement directly in the given expression.

All modifiers follow this convention, except the Copy() (together with DeepCopy()) function. It is not a modifier as it just copies the expression without modifying anything. The second version is then irrelevant and the choice has been made to not add the suffix ’-ed’ even if it corresponds to the first kind. Another binary choice you will often have to do is if you want the modifier to apply only on the first expression layer or in all the expression tree. To be fully recursive you must use the function (same name) starting with ’Deep’. This does not concern derivation, evaluation, and replacement that are by default fully recursive. The two binary choices make then 4 possibilities. Here is the example for the expansion function.

• Expand(). Modifies the argument, expanding only the first layer.

• Expanded(). Keeps the argument constant and returns the calculated expression, expanded only on the first layer.

• DeepExpand(). Modifies the argument, expanding recursively the whole tree.

133 • DeepExpanded(). Keeps the argument constant and returns the calculated expression, expanded recursively in the whole tree.

13.2 Copy, Refresh

13.2.1 Copy

Copy functions are really important when you want to be sure that an expression will not be modified. Let’s see a first example copying integers. You may write int a = 5; int b = a; b = 4; cout << a << endl;// >>5

Here ’a’ does not change. This is because we made a copy in ’b’, we have two different variables. The memory layout is presented figure 13.1.

Figure 13.1: Memory layout of two integers.

With pointer types, it is not the case anymore. Or more exactly, it is not the case for the underlying variables. For CSL expressions, you must be really careful with assignment of type Expr. If you only modify the root expression, you may however copy without much effort: Expra= constant_s("a"); Exprf= cos_s(a); Exprg=f; g = sin_s(a); cout << f << endl;// >> cos(a)

When assigning ’f’ to ’g’, you copy the pointer to cos_s(a). If you modify ’g’ directly, it will not point to cos_s(a) but to a new expression. ’f’ still points to the same variable. The memory layout is presented figure 13.2.

Figure 13.2: Memory layout of two CSL expressions with only an Expr assignment. No allocation takes place. The only way to modify ’g’ and not ’f’ is to change the root node of ’g’.

However, if you modify a sub-expression of ’g’, you do not reassign it, you modify the underlying expression. You then change the expression ’f’ points to. This gives

134 Expra= constant_s("a"); Exprf= cos_s(a); Exprg=f; g->setArgument(a*a); cout << f << endl;// >> cos(a^2) To avoid this problem, you may (deep-)copy ’f’. Starting with simple Copy(), you may avoid the shared cos function. A new one will be created, pointing to the same argument ’a’. But this time, changing the argument of ’g’ will not change ’f’ because the two cos functions are different. This gives Expra= constant_s("a"); Exprf= cos_s(a); Exprg= Copy(f); g->setArgument(a*a); cout << f << endl;// >> cos(a) The memory layout is presented figure 13.3.

Figure 13.3: Memory layout of two CSL expressions using Copy(). When changing the argument of the second cos function, ’f’ stays unchanged.

You may go further with DeepCopy(). This function guarantees to reallocate the entire tree so that no node is shared with the initial expression. It is safer, and heavier in terms of performance. In this simple example it does not allow us to go further, but one can see the difference on the memory layout presented figure 13.4. Examples of basic use of Copy() and DeepCopy() functions is summarized in sample code 88.

135 Figure 13.4: Memory layout of two CSL expressions using DeepCopy(). The entire tree is reallocated to guarantee a safe use of g not modifying ’f’.

Sample code 88: Basics on (Deep-)Copy

Building an expression ’f’ that should stay unchanged Expra= constant_s("a"); Exprf=2+ cos_s(a); Using assignment, works at depth 0 maximum Expr g = f; g = sin_s(a); cout << f << endl;//2+ cos(a)(Ok!) g = f; g[0] = 3;//g=3+ cos(a) cout << f << endl;// >>3+ cos_s(a)(Bad,f modified!) Using Copy(), works at depth 1 maximum Exprg= Copy(f); g[0] = 3;//g=3+ cos(a) cout << f << endl;// >>2+ cos_s(a)(Ok!) g[1][0] = a*a;//g=3+ cos_s(a*a) cout << f << endl;// >>2+ cos_s(a*a)(Bad,f modified!) Using DeepCopy(), works at all depths Exprg= DeepCopy(f); g[0] = 3;//g=3+ cos(a) cout << f << endl;// >>2+ cos_s(a)(Ok!) g[1][0] = a*a;//g=3+ cos_s(a*a) cout << f << endl;// >>2+ cos_s(a)(Ok!) See also Sections 7.1 and8 for explanations on how to modify sums and mathematical func- tions.

136 13.2.2 Refresh

Refreshing a CSL expression is similar to a copy (see the previous section), but instead of reallocating exactly the same expression, all simplifications are done. The purpose of refreshing an expression is to restore it when it is not canonical anymore (see section 3.8). It means that the expression may not be the same after being refreshed. Here is a very simple example showing the difference between copy and refresh

Expra= constant_s("a"); Expr f = 2 - a; f[0] = a;//f=a-a, not canonical cout << Copy(f) << endl;// >>a-a cout << Refreshed(a) << endl;// >>0

The memory layout is the same as for the copy function when no refresh has to be done. Refresh() works at depth 1, and DeepRefresh() at maximal depth. In particular, using DeepRefreshed() it is guaranteed that the expression if dully re-allocated (no risk of modifying the initial expression by modifying the new one). The summary is presented in sample code 89.

Sample code 89: Basics on (Deep-)Refresh

Building an expression to refresh Expra= constant_s("a"); Exprb= constant_s("b"); Exprf=2-a+ cos_s(b); f[0] = a; f[2][0] = 0; cout << f << endl; // >>a-a+ cos(0) Refresh modifying f, depth 1 max Refresh(f); cout << f << endl;// >> cos(0) Deep refresh modifying f, max depth DeepRefresh(f); cout << f << endl;// >>1 Using function with ’-ed’ suffix. cout << Refreshed(f) << endl;// >> cos(0) cout << DeepRefreshed(f) << endl;// >>1 cout << f << endl;//a-a+ cos(0)(not modified) Warning Using DeepRefresh() may be heavy in terms of performance. Try to minimize its use.

See also Sections 7.1 and8 for explanations on how to modify sums and mathematical func- tions.

137 13.3 Expansion

In CSL you may expand an expression. This will affect products and exponential functions. For example, fully expanding (a + b)ea+b (13.1) yields aeaeb + beaeb. (13.2) Note that in principle, factorization could undo expansion. It is not the case in general. Exponential functions cannot be refactored directly in CSL, neither complicated initial products. For example

(a + b)(b + c) = ab + b2 + ac + bc (13.3) cannot be refactored by CSL. So when expanding expressions, keep in mind that you will probably not be able to refactor them automatically if necessary. Not also that expansion may multiply the size of expressions by very big factors and in this case slow down significantly the program. Consider the expression 2x cos(t/T )(a + b)(c + d)(e + f)e−t/T . (13.4) Expanding it yields

2x cos(t/T )ace · e−t/T +2x cos(t/T )acf · e−t/T +2x cos(t/T )ade · e−t/T +2x cos(t/T )adf · e−t/T (13.5) +2x cos(t/T )bce · e−t/T +2x cos(t/T )bcf · e−t/T +2x cos(t/T )bde · e−t/T +2x cos(t/T )bdf · e−t/T .

It is a very simple example but in the real world of quantum field theory, expressions are much bigger and may have many sums to expand. Expanding expressions without any care may be deadly. Golden rules on expansion:

• Do not expand if it is not absolutely necessary.

• Consider first conditional expansion that may save many factors that would be expanded for nothing.

Expansion may be applied only on the first layer (default) or recursively in all the expression depending on which specialization you call (Expand() or DeepExpand()).

13.3.1 Standard expansion This expansion is the simplest. You simply have to give an expression to expand, choosing if it must apply to the first layer (Expand()) or the whole expression tree (DeepExpand()). As explained in section 13.1, there is two kinds of expansion functions. One modifying the argument and returning nothing (first kind), the other keeping it constant and returning a new expression (second kind). Examples of expression expansion are presented in sample code 90.

138 Sample code 90: Basics on expansion Building an expression to expand Expra= constant_s("a"); Exprb= constant_s("b"); Expr f = 2*(a + b)*cos_s(a*(a + b)); Using the second kind keeping f constant cout << Expanded(f) << endl;// First layer expansion // >> 2*a*cos(a*(a+b))+ 2*b*cos(a*(a+b)) cout << DeepExpanded(f) << endl;// Recursive expansion // >> 2*a*cos(a^2+a*b)+ 2*b*cos(a^2+a*b) Using the first kind, modifying f Expr f_save = Copy(f); Expand(f);// First layer expansion cout << f << endl; // >> 2*a*cos(a*(a+b))+ 2*b*cos(a*(a+b)) f = f_save; DeepExpand(f);// Recursive expansion cout << f << endl; // >> 2*a*cos(a^2+a*b)+ 2*b*cos(a^2+a*b) See also Section 13.2.1 for details on Copy() function.

13.3.2 Conditional expansion

Conditional expansion (ExpandIf()) may be much more powerful than the standard one (Expand()). The problem with expansion is the multiplication of the number of terms if there are many sums in products. Conditional expansion allows you to give a function (standard C++ one or lambda, see section 2.7 for more details) that will be applied on each sum found in the expansion process. The function must take an Expr const& as argument and return a boolean. Depending on the returned boolean, the sum will be distributed (true) or not (false). Suppose you want to develop only sums containing a particular symbol, this CSL feature is really helpful to keep expressions as factored as possible. Examples of expression conditional expansion are presented in sample code 91.

139 Sample code 91: Basics on conditional expansion Building an expression to expand Expra= constant_s("a"); Exprb= constant_s("b"); Exprc= constant_s("c"); Exprd= constant_s("d"); Expr f = 2*(a + b)*(c + d)*cos_s(c*(a + b)); Using the one layer expansion Expr expanded = ExpandedIf(f, [&](Expr const &sub) { return DependsOn(sub, c); }); cout << expanded << endl; // >> 2*(a+b)*c*cos(c*(a+b)) // >>+ 2*(a+b)*d*cos(c*(a+b)) Using the recursive expansion (same result here) expanded = DeepExpandedIf(f, [&](Expr const &sub) { return DependsOn(sub, c); }); cout << expanded << endl; // >> 2*(a+b)*c*cos(c*(a+b)) // >>+ 2*(a+b)*d*cos(c*(a+b)) See also We use here only the first kind of function (ExpandedIf()) and not the second kind (ExpandIf()). See sample code 90 for more details on this kind (would work the same here).

See also Section 2.7 for details on lambda functions, 14 for the DependsOn() function.

13.4 Factorization

Factorization in CSL is relatively simple. In particular it will not factor complicated expressions or polynomials. It is a common factor recognition process. Indicial expressions may also be factored. You may factor for example µ ν ρ σ (p1 p2µ)(p3p4ν) + (p1p3ρ)(p2 p4σ) (13.6) into µ ν σ p1 (p2µ (p3p4ν) + p3µ (p2 p4σ)) (13.7) with CSL. Indices are automatically replaced. As for expansion, factorization may be only applied on the first layer or recursively giving the function used (Factor() or DeepFactor() respectively). Basics on factorization are shown in sample code 92.

140 Sample code 92: Basics on factorization Building an expression to factor Expra= constant_s("a"); Exprb= constant_s("b"); Expr f = 2*a + pow_s(a, 3*CSL_HALF)*cos_s(a + a*b); // f = 2a + a3/2 cos(a + ab) Factor a particular symbol cout << Factored(f, a) << endl;// First layer factorization // >> 2*a*(1+ 1/2*a^(1/2)*cos(a+a*b)) cout << DeepFactored(f, a) << endl;// Recursive factorization // >> 2*a*(1+ 1/2*a^(1/2)*cos(a*(1+b))) Factor letting CSL find common factors cout << Factored(f) << endl;// First layer factorization // >> 2*a*(1+ 1/2*a^(1/2)*cos(a+a*b)) cout << DeepFactored(f) << endl;// Recursive factorization // >> 2*a*(1+ 1/2*a^(1/2)*cos(a*(1+b))) Note We did not show here the second kind of factorization function Factor(). See section 13.1 for more details.

13.5 Deriving

In CSL you can derive mathematical expressions. When they are defined, for operations or common mathematical functions, the formula is applied explicitly. When they are not, a Derivative object is created (see section9). For example, deriving

x + y2 cos(2πy) (13.8) by x yields (if y depends on x) dy dy 1 + 2y cos(2πy) − 2πy2 sin(2πy) . (13.9) dx dx

dy Known derivatives are evaluated, and the unknown dx is left as is for further treatment. If you know the expression of y as a function of x, you may replace it and evaluate the derivative. Consider that y = x2, it yields 1 + 4x3 cos(2πx2) − 4πx5 sin(2πx2). (13.10) In CSL it is possible to derive tensors (see section 11.3). It works however only for explicit dependencies. In particular, the derivation of tensors is defined for a tensor Aµ1...µn as ∂ ν1...νn ν1...νn ∂µ ...µn A = A 1 ∂Aµ1...µn n Y (13.11) = δνi . µi i=1

You can do all that in CSL. See sample code 93 for examples.

141 Sample code 93: Basics on derivation Building an expression to derive Exprx= variable_s("x"); Expry= variable_s("y"); Exprf=x+ pow_s(y, 2)*cos_s(2*CSL_PI*y); // f = x + y2 cos(2πy) Deriving f with respect to x Expr df = Derived(f, x); cout << df << endl; // >>1+ 2*y*cos(2*\pi*y)*d/d(x)(y)+ (-2)*\pi*y^2*sin(2*\pi*y)*d/d(x)(y) Replacing y by x2 and evaluating the derivative Replace(df, y, pow_s(x, 2)); Evaluate(df); cout << df << endl; // >>1+ 4*x^3*cos(2*\pi*x^2)+ (-4)*\pi*x^5*sin(2*\pi*x^2) Deriving tensors, ∂ eiP ·X = iP µeiP ·X (only explicit dependence possible) ∂Xµ TensorX("X",&Minkowski); TensorP("P",&Minkowski); Index mu("mu",&Minkowski); cout << Derived(exp_s(CSL_I*X(mu)*P(+mu)), X(mu)) << endl; // >>i*exp(i*P_+%mu*X_%mu)*P_+mu Note We did not show here the second kind of derivation function Derive(). See section 13.1 for more details.

See also Sections 13.7 for replacing,9 for derivative object, 13.6 for evaluation, and 11.3 for tensors.

13.6 Evaluation

13.6.1 Principle

Evaluation in CSL allows in principle to get a number from an expression when it is possible. For example, if x = 3 and y = π, then x + cos y = 3 − 1 = 2. For a symbolic manipulation program making physics predictions, this is what you expect. Being able to evaluate the symbolic result of a computation to get a final number. There is however several possibilities to evaluate only some kind of expressions. You may want in particular to evaluate just some objects, letting the rest symbolic. In the following are presented the different evaluation flags to pass to Evaluate() and Evaluated() together with practical examples. Note that if CSL evaluation flags are not enough, you can very easily evaluate only what you want with ForEachNode() or Transform() algorithms. See section 15 for more details.

13.6.2 CSL evaluation flags

There is several evaluation flags in CSL. Each flag is recognized by particular expressions to know if they must be evaluated. Table 13.1 presents the different flags in CSL. They all lie in namespace csl::eval.

142 Flag Consequence Rational numbers, factorial and mathematical csl::eval::numerical functions are evaluated. csl::eval::literal Valued literals are evaluated. csl::eval::indicial All indices are expanded in terms of tensor elements. csl::eval::abbreviation All abbreviations are replaced by their values. csl::eval::all Combination of all the mentioned flags.

Table 13.1: Evaluation flags in CSL. Objects not present in this list and that have a specific evaluation (like Derivative) do not need any particular flag to be evaluated. For more details on expressions discussed here please see the corresponding sections.

Evaluation flags may be combined with the operator|. You may for example evaluate literals together with numerical values with flag csl::eval::numerical | csl::eval::literal. In particular, csl::eval::all is equivalent to auto all_flag = csl::eval::numerical | csl::eval::literal | csl::eval::indicial | csl::eval::abbreviation;

13.6.3 Example A short example in presented in sample code 94. To evaluate specific objects, see the corresponding sections.

143 Sample code 94: Basics on evaluation Sample expression to evaluate Expra= constant_s("a", CSL_PI); Exprf= CSL_HALF+ cos_s(a); Base evaluation (nothing to do) cout << Evaluated(f) << endl; // >> 1/2+ cos(a) Evaluation with one flag cout << Evaluated(f, eval::numerical) << endl; // >> 0.5+ cos(a) cout << Evaluated(f, eval::literal) << endl; // >> 1/2+ cos(3.14159) Evaluation with multiple flags cout << Evaluated(f, eval::numerical | eval::literal) << endl; // >> -0.5 cout << Evaluated(f, eval::all) << endl; // >> -0.5 See also We use here only the first kind of function (Evaluated()) and not the second kind (Evaluate()). See sample code 90 for more details on this kind.

13.7 Replacement

Replacement is a very important feature for symbolic computations. One object (scalar expression, indexed tensor, index) is searched in an initial expression and is replaced by another. There is no conditional replacement here. It is however possible in CSL with algorithms (see section 15). Note that you cannot do replacements for complicated expressions. Replacing a 7→ c (13.12) b a for example will work only if is found as a single node. With a CSL replacement, it would give b a a · ea/b 7→ · ec. (13.13) bc bc a The first occurrence of is not seen by CSL. This is because it is not in a single node. It would be b possible to search partial sub-expressions, but it would take much more time and we almost always can avoid this problem. In this case, you may simply replace a by bc and it works: a · ea/b 7→ ec. (13.14) bc In more complicated cases you may need conditional replacement, doable with algorithms (see section 15).

13.7.1 CSL replacement There is four different replace functions. All follow the two possible kinds defined in section 13.1. The four possible replacements are

144 • Replace a non indexed expression by another.

• Replace an indexed tensor by another.

• Replace an indexed tensor by a more complex expression.

• Replace an index by another.

Examples of expression / tensor / index replacements are shown in sample code 95 and sample code 96. There is two subtleties. For index replacement, beware to give the good sign to the replace function (if they have one). For example, replacing upper +mu by upper +nu will do nothing if the expression contains only a lowered mu. For complex tensor replacement as Aµν 7→ aXµYν, you have to give an explicit realization of the initial tensor and the result, with indices (whereas when replacing a tensor by another, only giving respective parents of the two tensors is enough). For example, if you want to replace Aij by Bij, you can write Replace(expr, A, B);

This is because the structures are exactly the same. But suppose you want to replace Aij by Bji (or something with a complicated expression), you have to specify indices of the initial tensor in order to allow CSL to know how to replace properly the tensor like Replace(expr, A({i, j}), B({j, i})); or Replace(expr, A({i, j}), complicated_function_of_i_and_j);

145 Sample code 95: Basics on replacement - part 1

µ ν µν ρ Base expression aX X − g X Xρ TensorX("X",&Minkowski); Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Index rho("rho",&Minkowski); Tensorg= GetMetric(&Minkowski); Expra= constant_s("a");

Expr f = a*X(+mu)*X(+nu) - g({+mu, +nu})*X(+rho)*X(rho); Replacing a by 1/b2 Exprb= constant_s("b"); Replace(f, a, 1 / pow_s(b, 2)); cout << f << endl; // >>b^(-2)*X_+mu*X_+nu+-X_%rho*X_+%rho*g_{+mu,+nu} Replacing X by a tensor Y TensorY("Y",&Minkowski); Replace(f, X, Y); cout << f << endl; // >>b^(-2)*Y_+mu*Y_+nu+-Y_%rho*Y_+%rho*g_{+mu,+nu} Note We use here only the second kind of function (Replace()) and not the first kind (Replaced()). See sample code 90 for more details on this kind.

See also Section 11.3 for more details on tensors.

146 Sample code 96: Basics on replacement - part 2

ρ Base expression Aµν − Aρgµν TensorA("A", {&Minkowski, &Minkowski}); Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Index rho("rho",&Minkowski); Tensorg= GetMetric(&Minkowski);

Expr f = A({mu, nu}) - A({+rho, rho})*g({mu, nu}); Replacing µ by σ Index sigma("sigma",&Minkowski); Replace(f, mu, sigma); cout << f << endl; // >>A_{sigma,nu}+-A_{+%rho,%rho}*g_{nu,sigma} Replacing Aµν by aXνY µ TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Expra= constant_s("a"); Replace(f, A({mu, +sigma}), a*X(+sigma)*Y(mu)); cout << f << endl; // >>a*X_nu*Y_sigma+-a*X_%rho*Y_+%rho*g_{nu,sigma} Warning When indices have a sign, you must specify the good one in the replace function. If the sign is not the right one, the index will not be seen (and then not replaced).

Note For the second replacement (A to aXY ), you may give any indices you want giving that they are consistent between the initial and final expressions.

Note We use here only the second kind of function (Replace()) and not the first kind (Replaced()). See sample code 90 for more details on this kind.

See also Section 11.3 for more details on tensors.

147 13.7.2 CSL swapping Swapping in CSL is just a shortcut for 3 replacements. If you have to swap x and y in a mathematical expression, say f(x, y), you need to

• Replace x by a dummy variable, say z.

• Replace y by x.

• Replace z by y.

It is the same thing as for variables in general, if you do not create a temporary variable you lose the information about one of the two swapped objects: void swap(int &a, int& b) { int foo = a; a = b; b = foo; } In CSL you may swap

• Two non indexed expressions.

• Two tensors, giving their parents.

• Two indices.

This is shown in sample code 97.

148 Sample code 97: Basics on swapping Building a base expression TensorX("X",&Minkowski); TensorY("Y",&Minkowski); Index mu("mu",&Minkowski); Index nu("nu",&Minkowski); Expra= constant_s("a"); Exprb= constant_s("b");

Expr f = a*X(+mu)*X(nu) + X(+mu)*Y(nu)*cos_s(b); µ µ // f = aX Xν + X Yν cos b Swapping a and b Swap(f, a, b); cout << f << endl; // >>b*X_+mu*X_nu+ cos(a)*X_+mu*Y_nu Swapping µ and ν Swap(f, +mu, nu); cout << f << endl; // >>b*X_+%mu*X_%mu+ cos(a)*X_+%mu*Y_%mu Swapping X and Y tensors Swap(f, X, Y); cout << f << endl; // >> cos(a)*X_%mu*Y_+%mu+b*Y_+%mu*Y_%mu Warning When replacing or swapping indices, it is sensitive to their sign. In particular, you must give +mu and -nu to the function or it will not work properly.

Note We did not show here the first kind of swapping function Swapped(). See section 13.1 for more details.

See also Section 11.3 for more details on tensors.

149 150 Chapter 14

Interface functions

14.1 Principle

Interface functions are meant to simplify the life of the new CSL user. They add nothing more to the core program. In particular, a user accustomed to CSL or even just C++ in general may prefer use class members. Interface functions are very often only shortcuts for a (little bit) more complex code. For example replacing obj->func() by an interface function Func(obj). It has one big advantage in particular. It is the fact that using an interface function you do not have to know what type obj is (pointer, not pointer, shared pointer . . . ). These functions are built to always avoid (when possible) any type conversion to the user. It may also make the code more readable. Some member functions of expressions return a std::optional rather than a simple Expr (see section 2.8.5 for more details on optionals). Know that the goal is to avoid unnecessary memory allocations. Interface functions are defined to hide this object from the user. This chapter does not aim to present all interface functions. There is a lot, and you can find them in the documentation of file interface.h. We will see only a few examples here to show how it works in general.

14.2 Expression dependencies

You may ask CSL if an expression depends on another mathematically. There is two types of dependencies

• Explicit dependency: the symbol appears itself in the expression.

• Generic dependency: the symbol appears or another one that may depend on it, especially Variable objects (see section6) that may depend on each other.

Beware that as in other fields, dependencies will not be mathematically exact. For example, cos x depends on x but the inverse will not be true in CSL. In general, I recommend to use the dependency if searching a single symbol. For indexed tensors (see section 11.3), only an explicit dependency may be tested. In particular, Xµ will always be independent of Y µ. Examples on the use of dependency functions is presented in sample code 98.

151 Sample code 98: Basics on dependencies Explicit dependency Exprx= variable_s("x"); Expry= variable_s("y"); cout << DependsExplicitlyOn(cos_s(x + y), y) << endl;// >>1 cout << DependsExplicitlyOn(cos_s(2*x), y) << endl;// >>0 Generic dependency Exprx= variable_s("x"); Expry= variable_s("y"); cout << DependsOn(cos_s(y), x) << endl;// >>1 y->removeDependency(x); cout << DependsOn(cos_s(y), x) << endl;// >>0 See also Section6 for details on variables.

14.3 Complex numbers

Expressions in CSL can be complex. You can take real, imaginary part, modulus, . . . Some of the corresponding member functions return a std::optional. Interface functions allow you to avoid this feature. All the functions presented in the following take an expression as argument (Expr const&) and return a new expression depending on the function. The argument is not modified.

• GetRealPart(). Returns the real part of an expression.

• GetImaginaryPart(). Returns the imaginary part of an expression.

• GetComplexModulus(). Returns the complex modulus of an expression.

• GetComplexArgument(). Returns the complex argument (angle) of an expression.

• GetComplexConjugate(). Returns the complex conjugate of an expression.

Examples are shown in sample code 99.

152 Sample code 99: Basics on complex interface Building some expressions Expra= constant_s("a"); Exprc= constant_s("c", ComplexProperty::Complex); Testing properties Expr num = 1 + 2*CSL_I; Expr lit = a + CSL_I*c; cout << GetRealPart(num) << endl;// >>1 cout << GetImaginaryPart(num) << endl;// >>2 cout << GetComplexConjugate(num) << endl;// >>1+ (-2)*i cout << GetRealPart(lit) << endl;// >>a- Im(c) cout << GetImaginaryPart(lit) << endl;// >> Re(c) cout << GetComplexConjugate(lit) << endl;// >>a-i*c^(*) See also Section9 for more details about real and imaginary part objects.

14.4 Interface for indicial objects

Indicial objects are very important in CSL, because they are in CSL-HEP. The interface is then more developed for these objects.

14.4.1 Space and indices

You may generate an index with the GenerateIndex() function presented in sample code 100. You may access basic tensors of a given space (Dirac delta, metric, epsilon symbol) with respectively GetDelta(), GetMetric() and GetEpsilon(). Each of these three functions take a Space pointer as parameter (do not forget to use & if the variable is not a pointer, see section2 for more information) and return the tensor. For more details about vector spaces and indices, see section 11.2.

Sample code 100: GenerateIndex()

Generates an index properly (with a new id) from a vector space (pointer) and an optional name.

Parameters space Space pointer in which the index lives. name Name of the index (optional). Returns

An index with the proper name, a new id, living in space.

14.4.2 Tensor involution properties Interface functions for complex, transposed, and hermitian setting of tensor properties are shown respectively in sample codes 101, 102 and 103. For more details about these properties, see section 11.3, and tutorial 17.3.

153 Sample code 101: AddComplexProperty()

Defines a property under complex conjugation for a tensor, applied when A∗ is found in expressions (for a tensor A).

Parameters tensor Tensor on which the property is applied init Witness expression before complex conjugation res New expression, special result of the complex conjugation of init. See also Tutorial 17.3 that shows an example of hermitian conjugation.

Sample code 102: AddTransposedProperty()

Defines a property under transposition for a tensor, applied when AT is found in expressions (for a tensor A).

Parameters tensor Tensor on which the property is applied space Space pointer for the transposition. init Witness expression before transposition. res New expression, special result of the transposition of init. Warning As mentioned in tutorial 17.3, you should revert indices in the result with respect T T T to standard matrix identity (A = A becomes with indices Aij = Aji and not Aij = Aij).

See also Tutorial 17.3 that shows an example of hermitian conjugation.

Sample code 103: AddHermitianProperty()

Defines a property under hermitian conjugation for a tensor, applied when A† is found in expressions (for a tensor A).

Parameters tensor Tensor on which the property is applied space Space pointer for the transposition. init Witness expression before hermitian conjugation res New expression, special result of the hermitian conjugation of init. Warning As mentioned in tutorial 17.3, you should revert indices in the result with respect † † † to standard matrix identity (A = A becomes with indices Aij = Aji and not Aij = Aij).

See also Tutorial 17.3 that shows an explicit example.

154 14.4.3 Contraction properties As we have seen in section 11.3, tensors may have also contraction properties. Two types exist, self contractions (between two tensors of the same type) and chain contractions that may involve an arbitrary number of possibly different tensors. These two kind of properties may be defined and have a corresponding interface function, pre- sented in sample codes 104 and 105 respectively for self and chain contractions.

Sample code 104: AddSelfContraction()

Defines a self contraction property for a given tensor, giving two initial tensors in the contraction and the result.

Parameters tensor Tensor on which the property is applied. A First tensor in the contraction. B Second tensor in the contraction. res Result of the contraction. See also Tutorial 17.3 that presents an explicit example of self contraction.

Sample code 105: AddContractionProperty()

Defines a chain contraction property between several tensors.

Parameters tensor Tensor on which the contraction is applied. tensorList List of the tensors in the contraction. res Result of the contraction. Warning The tensor on which the property is applied should be the less frequent one. The property will only be tested when it appears.

See also Section 11.3 that presents an explicit example of chain contraction.

14.5 Other interface functions

As for algorithms, all interface functions are not given here. There is already too much and the number is constantly growing. I encourage you, if you are interested in using those functions, to look at the documentation (file interface.h). Remember however that interface functions are shortcuts for some of CSL features and that others may not be accessible without looking at expression member functions.

155 156 Chapter 15

Algorithms

Algorithms are a set of functions defined in header file algo.h. Their purpose is to allow you to go where you want to go when CSL objects and interface are not enough. Consider the expression  2πt x(t) = A 1 + cos e−α(T ). (15.1) T You may legitimately wonder if an expression depends on the exponential of t. Bare CSL functions do not provide that kind of tests. But algorithms allow you to perform them anyway. The idea is the following. As we have seen in section 2.7, we can in C++ have a generic function applying other functions to complicated sets or containers of variables. Here the generic functions are CSL algorithms, and the specific ones applied on expressions are user-defined. In our example, the question we want to ask here is ’Is there an exponential function that depends on t ?’. The answer is no. There is an exponential function but depending only on the period T . Without algorithms, you should write a recursive function to explore the expression as they are trees. Algorithms takes the charge of recursion, you just have to provide the function applied on each node, or leaf. In pseudo-code, our condition looks like for a sub-expression expr function condition(sub): return sub.is_exponential_function() and sub.depends_on(t) for sub in expr.node(): if (condition(sub)) return true; return false; It is not more complicated than that and should not need recursive functions with loops and other conditions. With the csl::AnyOfNodes algorithm you can express your condition as simply as in the pseudo-code, it iterates on all nodes recursively for you. It yields auto condition = [&](Expr const &sub) { return(IsExp(expr) and DependsOn(expr, t)); }; return csl::AnyOfNodes(expr, condition); For details about lambda functions please see section 2.7. Almost all algorithms detailed in the following work the same way. • Takes as a parameter the full expression on which apply the algorithm. • Takes as parameter the function to apply on the tree elements. It may be a regular C++ function, or a lambda (stored in a variable or inline). This function must take an Expr as parameter and may have to return something depending on the algorithm.

157 • Takes an optional int as parameter specifying the maximal depth required. The default is -1, fully recursive. • Is specialized to apply on all nodes or only leafs. In the previous example, the leaf specialization of csl::AnyOfNodes() is csl::AnyOfLeafs(). In this section we present first read-only algorithm, then read and write ones. Some are explicitly explained, the others are just mentioned. If you want precise information about the other, see file algo.h in the documentation. Here is a full list of all algorithms. First the main ones that you should know:

• csl::AnyOfLeafs() (read-only). • csl::AnyOfNodes() (read-only). • csl::AllOfLeafs() (read-only). • csl::AllOfNodes() (read-only). • csl::VisitEachNode() (read-only). • csl::VisitEachLeaf() (read-only). • csl::ForEachNode() (read and write). • csl::ForEachLeaf() (read and write). • csl::Transform() (read and write). and the rest that you should be using less often: • csl::FindLeaf() (read-only). • csl::FindNode() (read-only). • csl::FindIfLeaf() (read-only). • csl::FindIfNode() (read-only). • csl::VisitEachNodeCut() (read-only). • csl::ForEachNodeCut() (read and write).

15.1 Read-only algorithms

Those algorithms do not modify the expression they take. The parameter the function or lambda given to the algorithm must take is Expr const& .

In all this section, we suppose the following definitions: #include #include using namespace std; using namespace csl;

Expra= constant_s("a"); Exprb= constant_s("b"); Exprx= variable_s("x"); Expry= variable_s("y");

158 15.1.1 Find This algorithm searches a user-defined sub-expression. It is returned if found, else it returns an empty Expr. It is described in sample code 106 and an example is presented in sample code 107.

Sample code 106: Find algorithm

This concerns csl::FindNode() and csl::FindLeaf() algorithms.

Parameters init Expression on which the algorithm is applied. value Expression to search in init. depth Depth of the algorithm (default = -1, full depth). Returns

The expression corresponding to value in init if found.

nullptr else.

Warning This function may return an invalid Expr, meaning that it did not find anything. Please be sure to handle this case correctly.

Note As for every specialized algorithm, the leaf specialization will not explore intermediate nodes of the given expression.

159 Sample code 107: Example for find algorithm First try Expr expr = a + cos_s(b); Expr test1 = FindLeaf(expr, a); if (test1) cout << test1 << endl;// >>a Finding a node to modify its arguments Expr expr = a + cos_s(b); Expr test2 = FindNode(expr, cos_s(b)); if (test2) { cout << test2 << endl;// >> cos_s(b) test2->setArgument(a); cout << expr << endl;// >>a+ cos(a) test2 = a; cout << expr << endl;// >>a+ cos(a) } Warning Here you see that the return value of the algorithm is a copy of the found argument. All its sub-expressions may be modified directly, but not the argument itself as the returned pointer is just a copy.

Trying with the Leaf algorithm Expr expr = a + cos_s(b); Expr test3 = FindLeaf(expr, cos_s(b)); if (test3) cout <<"found" << endl; else cout <<"not␣found" << endl; // >> not found Here cos_s(b) is not a leaf so is not seen by this algorithm.

15.1.2 FindIf This algorithm is very similar to the previous one, but uses a user function as a condition instead of a comparison with a given expression. It is returned if found, else it returns an empty Expr. It is described in sample code 108 and an example is presented in sample code 109.

160 Sample code 108: FindIf algorithm

This concerns csl::FindIfNode() and csl::FindIfLeaf() algorithms.

Parameters init Expression on which the algorithm is applied. f Boolean predicate to search in init, type bool(Expr const&). depth Depth of the algorithm (default = -1, full depth). Returns

The first expression respecting the predicate f in init if found.

nullptr else.

Warning This function may return an invalid Expr, meaning that it did not find anything. Please be sure to handle this case correctly.

Note As for every specialized algorithm, the leaf specialization will not explore intermediate nodes of the given expression.

Sample code 109: Example for find if algorithm Using a C++ function bool condition(Expr const &sub) { return IsCos(sub); }

Expr expr = a + cos_s(b); Expr test1 = FindIfLeaf(expr, condition); if (test1) cout << test1 << endl; else cout <<"not␣found" << endl; // >> not found Using a lambda Expr expr = a + cos_s(b); Expr test1 = FindIfNode(expr, [&](Expr const &sub) { return IsCos(sub); }); if (test1) cout << test1 << endl; else cout <<"not␣found" << endl; // >> cos_s(b)

161 15.1.3 VisitEach algorithm

The visit algorithm, respectively csl::VisitEachNode() and csl::VisitEachLeaf() allow to look through an expression that is not modified. This is by far the most powerful algorithm to gather information about an expression, using lambda capture. All information you may get depends on the function given to the algorithm. Captured variables may be modified along the algorithm (counting elements, filling a container, modifying another expression. . . ). It is described in sample code 110 and an example is presented in sample code 111.

Sample code 110: Visit algorithm

This concerns csl::VisitEachNode() and csl::VisitEachLeaf() algorithms.

Parameters init Expression on which the algorithm is applied. f Simple function applied to init, type void(Expr const&). depth Depth of the algorithm (default = -1, full depth). Note As for every specialized algorithm, the leaf specialization will not explore intermediate nodes of the given expression.

162 Sample code 111: Example for the visit algorithm Counting Expr expr = a + cos_s(b); size_t numberOfCosin = 0; VisitEachNode(expr, [&](Expr const &sub) { if(IsCos(sub)) ++numberOfCosin; }); cout << numberOfCosin << endl;// >>1 Gathering sub-expressions in a std::vector Expr expr = a + cos_s(b); std::vector constants; VisitEachLeaf(expr, [&](Expr const &sub) { if(IsConstant(sub)) constants.push_back(sub); }); for(Expr const &c : constants) cout << c << endl; // >>a // >>b Note Here we do not reserve memory for the vector as said in section 4.3.2. In general for possibly many terms, you should reserve some memory space.

See also Section 4.3.2 for the range-based for loop.

15.1.4 Other read-only algorithms

In CSL documentation you may found all the other algorithms. For the read-only ones not presented, here is their list (see file algo.h):

• csl::AnyOfLeafs(), csl::AnyOfNodes(). Tests if any of the nodes/leafs respects some boolean predicate given by the user.

• csl::AllOfLeafs(), csl::AllOfNodes(). Tests if all of the nodes/leafs respects some boolean predicate given by the user.

• csl::VisitEachNodeCut(). Similar to csl::VisitEachNode(), but the algorithm may stop on particular branches if the function given by the user returns true. See section 17 where an example is given.

15.2 Read and write algorithms

These algorithm will allow you to modify on the go the expression. They are very useful to apply properties that are not automatic in CSL.

163 15.2.1 Transform algorithm This algorithm applies a user function on all nodes of a particular expression. This user function may modify sub-expressions as wanted, and return a boolean. This boolean tells if the expression must be refreshed (see section 13.2.2) after the call. It is described in sample code 112 and an example is presented in sample code 113.

Sample code 112: Transform algorithm Parameters init Expression on which the algorithm is applied, may be modified. f Function applied to init, type bool(Expr&). Modifies sub-expressions. depth Depth of the algorithm (default = -1, full depth).

Sample code 113: Example for the transform algorithm Modifying Expr expr = a + cos_s(b); Transform(expr, [&](Expr &sub) { if (sub == a) {//a ->x sub = x; return false;// do not refresh } if (sub == cos_s(b)) {// cos(b) ->x sub = x; return true;// refresh } return false; }); cout << expr << endl; // >> 2*x Note If you do not want to refresh the expression (may be lengthy), consider using csl::ForEachNode() instead.

15.2.2 ForEach algorithm The ForEach algorithm is very similar to Transform. The only difference is that no refresh is done on the modified expression. The user function is applied on all nodes/leafs (possibly modifying them), and the expression is left as is. It is described in sample code 114 and an example is presented in sample code 115.

164 Sample code 114: For each algorithm

This concerns csl::ForEachNode() and csl::ForEachLeaf().

Parameters init Expression on which the algorithm is applied, may be modified. f Function applied to init, type void(Expr&). Modifies sub-expressions. depth Depth of the algorithm (default = -1, full depth).

Sample code 115: Example for the for each algorithm Taking the same example as for Transform Expr expr = a + cos_s(b); ForEachLeaf(expr, [&](Expr &sub) { if (sub == a) sub = x; else if (sub == cos_s(b)) sub = x; }); cout << expr << endl; // >>x+ cos(b) Using specialization for each node Expr expr = a + cos_s(b); ForEachNode(expr, [&](Expr &sub) { if (sub == a) sub = x; else if (sub == cos_s(b)) sub = x; }); cout << expr << endl; // >>x+x cout << Refreshed(expr) << endl; // >>2x Note If you do not want to refresh directly the expression (may be lengthy), consider using csl::Transform() instead. Here you have the choice to (deep-)refresh the expression or not. If you know that it is not necessary, this is the right choice of algorithm.

165 15.2.3 Other read and write algorithms In CSL documentation you may found all the other algorithms. For the read and write ones not presented, here is their list (see file algo.h):

• FirstOfNode() and FirstOfLeaf(). Modifies the expression through the user function. When it returns true, the algorithm stops.

• csl::ForEachNodeCut(). Similar to csl::ForEachNode(), but the algorithm may stop on partic- ular branches if the function given by the user returns true. See section 17 where an example of csl::VisitEachNodeCut() is given.

166 Chapter 16

Tools and options

16.1 Tools

16.1.1 Timer

The timer class allows to measure the elapsed time for a process without having to do it by hand. Its use is design for a scoped process. The clock is started automatically as the construction of the timer, and the elapsed time is displayed at the destruction. It may be used three different ways:

Define scopes yourself

Useful in particular when measuring one process that does not need a new scope. int main() { Timer timer;

firstProcess() // ... otherProcess(); } // >> Elapsed time: 00h 34m 11s.

Define spurious scopes to define measured blocks in your code int main() { { // Scope1 Timer timer; myComplicatedProcess(); } // >> Elapsed time: 02h 13m 23s. { // Scope2 Timer timer; myOptimizedProcess(); } // >> Elapsed time: 00h 32m 58s. }

167 Use the restart() function int main() { Timer timer; myComplicatedProcess(); timer.restart(); // >> Elapsed time: 02h 13m 23s. myOptimizedProcess(); // >> Elapsed time: 00h 32m 58s. } 16.1.2 ScopedProperty This class allows to sets a public1 property to a given value during the execution of a scope, restoring the old value at the end. The example is as follows. Suppose you want to change a computation rule for a given function, and restore the old value (whatever it is) at the end. Here is what you have to do, consider a boolean value just for the example: void f(){ bool oldValue = myProperty; myProperty = valueIWant; // ... computationsThatMayTakeManyLines(); // myProperty = oldValue; } This requires three lines of code and is error prone, in particular for restoring the old value. What you want to do is very simple and should take one line, not be separated around other lines of code. This is what allows ScopedProperty. The only function the user has to use is its constructor. At the destruction the old value is restored automatically. Here is the example using ScopedProperty. { ScopedProperty secureProp(&myProperty, valueIWant); // ... computationsThatMayTakeManyLines(); // } // End of scope: the old value is restored automatically This class ensures that all changed properties recover their old values at the end of the scope. There is however two things to know. The constructor takes a pointer as parameter. No check is done on it so the user must be sure that it is valid in all the current scope. Secondly, and surely more important because more likely, beware to create an actual variable in the scope. Consider this: ScopedProperty(&myProperty, valueIWant); // ... // computationsThatMayTakeManyLines(); // Here the ScopedProperty has not been assigned to any variable. It is automatically destroyed at the end of the line and myProperty restores immediately its old value. This is better than not restoring the old value at the end but still annoying.2 So be sure that: 1Public means any variable accessible directly (scope variable, global variable, public member variable . . . ). 2It is better because the cause of the bug lies in the same scope as the effect. Whereas if not restoring a parameter value, it may cause a bug later on, in a completely different function.

168 • The pointer to the property is valid in all the scope (non null and points to actual data).

• You store the ScopedProperty in a variable that will be destroyed at the end of your scope, when you want the old value to be restored.

16.2 Options

This section is dedicated to public CSL options accessible in file options.h. In order to set an option to a given value and restoring properly the old one after your computation, section 16.1.2 presents csl::ScopedProperty() that allow to do that without thinking about it.

16.2.1 errorStopsProgram (boolean)

If set to false, errors in CSL will let the program run after them. This is dangerous, but if you know that the program could run anyway you can try it. See section 3.6 for more details on errors. Default value: true.

16.2.2 checkCommutations (boolean)

If set to false, the boolean disables all commutations checks in products. If you know in advance that all objects in a given expression commute, you may set this option to false to save time. Default value: true.

16.2.3 freezeMerge (boolean)

If set to true, the merging of arguments in sums and products is disabled. This is equivalent to build explicit sums and products (see sections 7.1 for Sum and 7.2 for Prod). This has two consequences. You will surely gain some time in a particular computation, but expressions will not be canonical anymore. You really should know what you are doing using this option. Default value: false.

16.2.4 applySelfContractions (boolean) This boolean tells if self contractions of indexed tensors must be tested. See section 11 for more details. If set to false, no self contraction will be possible but you will gain some computation time. Default value: true.

16.2.5 applyChainContractions (boolean) This boolean tells if chain contractions of indexed tensors must be tested. See section 11 for more details. If set to true, chain contractions will be possible but you will lose a measurable amount of time in computations. Default value: false.

16.2.6 fullComparison (boolean) Tells if the comparison rule (a < b between two expressions) must be the canonical one, defined in section 3.9, ore a simpler one. The simpler one is much quicker. It compares directly expression types (see section 3.4 on CSL type system) and number of arguments before getting into more precise

169 comparisons. Setting this option to false definitely saves time in computation, but expression layout may forbid some simplifications in particular with indexed tensors. Default value: true.

16.2.7 printIndexIds (boolean) Tells if the Index ids (see section 11.2.2) are printed in output. Setting this option to false will yield indices like {i,j} instead of {i_2344,j_413}. Default value: true.

170 Chapter 17

Tutorials

17.1 Tutorial: basic manipulations

In this first tutorial we will see how to declare simple expressions and how to perform basic transfor- mations. Remember that Expr is the expression type. It is a pointer type but may be used directly in functions and operators. In order to define properly an expression, one must use builder functions. Those functions all lie in namespace csl and are of the form objectname_s(arguments). The _s stands for symbolic.

17.1.1 Definitions Let’s define some literals that we will use in more complicated expressions (see section6 for literal definitions): Expra= constant_s("a"); Exprx= variable_s("x"); Exprt= variable_s("t"); The difference between constants and variables in CSL is how they depend on each other. A Constant will only depend on itself, whereas a Variable will (by default) depend on all other variables.

17.1.2 Using operators, deriving For our first try, let’s build a simple expression. d g(x, t; a) = 2.5 (a · f(x)) + 4, dt with f(x) = ax. Using CSL, this gives: Exprf= prod_s(a, x); Expr res = sum_s( prod_s( float_s(2.5), Derived(prod_s(a, f), t)), int_s(4)); Or in a more human-readable way, using operators: Expr f = a * x; Expr res = 2.5 * Derived(a*f, t) + 4;

171 To see in more details how sums, products and derivation work, please see respectively sections 7.1, 7.2, 13.5. With operators, one may write mathematical expressions the usual way (as in c++). For the exponentiation however, no operator is properly defined.1 One must then use csl::pow_s(x, y) that corresponds to xy. We can then display the result in the standard output: std::cout << res << std::endl; // >>4+ 2.5*a^2*d/d(t)(x)

We see that the constant ’a’ got out of the derivative while the variable x stayed in.

17.1.3 Replacing, evaluating

t 2 To go further, we derive again the expression with respect to t, replace x by a and evaluate the result. This gives   2  2  d 2 dx 2 d t 4 + 2.5 · a = 0 + 2.5 · a 2 2 = 5. dt dt t 2 dt a x→( a )

In CSL this corresponds to three instructions. First deriving by t, then replace x, and finally evaluate the derivative (not done automatically)2. For more details about replacing and evaluation, see sections 13.7 and 13.6.

Derive(res, t); Replace(res, x, pow_s(t/a, 2)); Evaluate(res); std::cout << res << std::endl; // >>5

17.1.4 Be careful with memory

Please be aware of the number of memory allocations/deallocations necessary to get the final result. Despite the fact that the computation is very simple, the count is between 50 and 100. Many inter- mediate steps not appearing in results by hand are however necessary to get things done in general. Memory management being heavy in terms of performance, we want to minimize it. Example: std::cout << x * x * x * x * x << std::endl; // ~40 allocations/deallocations // >>x^5 std::cout << prod_s({x, x, x, x, x}) << std::endl; // ~15 allocations/deallocations // >>x^5

When taking a sum or product of multiple arguments, it is much better to give them in a list (between curly braces) to csl::prod_s() or csl::sum_s() than to use chained operators that corresponds in this case to do x ∗ (x ∗ (x ∗ (x ∗ x))) which implies much more calculations and intermediate steps.

1The ˆ operator exists in c++, however in does not respect operation order. For explanations on the exponentiation object see section 7.3. 2Derivatives are not evaluated automatically in CSL. If the derivative is defined, evaluating it explicitly will yield the result. Else the derivative object stays unchanged (for example dx/dt will not be evaluated if x is not replaced).

172 17.1.5 Expanding, summary Now let’s summarize all we have learned in this tutorial with a (not much) more complex example. Consider the expression f(x, y) = e(x−y)2 − (x − y) cos(x − y). It is clearly invariant by translation along x and y at the same time: x → x + a, y → y + a.

However if we develop the expression, we get ex2 ey2 f(x, y) = − x cos(x − y) + y cos(x − y), e2xy which makes a little less obvious the translation invariance. The invariance being still visible with the eye, it will be extremely difficult to determine it automatically just by looking at the expression. In general, if one wants to check the invariance of f along x and y, one may check that d g(x, y, a) = 0, da with g(x, y, a) ≡f(x + a, y + a) e(x+a)2 e(y+a)2 = − (x + a) cos(x − y) + (y + a) cos(x − y). e2(x+a)(y+a) Here is how to answer the question of the invariance within CSL: Expry= variable_s("y"); Expr delta = x - y;// shortcut for(x-y) in the next res = exp_s(delta*delta) - delta*cos_s(delta); // res is invariant by replacingx ->x+a,y ->y+a // by expanding it, the invariance becomes less obvious Expand(res, true);

Replace(res, x, x + a); Replace(res, y, y + a); std::cout << res << std::endl; // >>(a+y)*cos(x+-y) // >>+ exp((a+x)^2)*exp((a+y)^2)*exp((-2)*(a+x)*(a+y)) // >>+-(a+x)*cos(x+-y)

Derive(res, a); Expand(res, true); std::cout << res << std::endl; // >>0 To expand an expression, there is two interface functions possible: Expand() and Expanded(). See section 13.3 for more details. Expr expanded = Expanded(res);// Do not modify res, // we store the result in what we want Expand(res);// Modifies res and returs nothing

173 Both functions take an optional boolean argument. If true, all sub-expressions will be expanded. Else, only the first layer (depth = 0) is. Please note here the use of Expr delta = x - y. This sub-expression appears multiple times. So in order to optimize memory management, we create it once and use it when we need to. In this example, x − y = x + (−1) ∗ y. There is 3 memory allocations when building it: considering x and y are already allocated, one then must allocate −1, the product of −1 and y, and the sum with x. By using a the variable delta we get rid of 3 ∗ (N − 1) allocations if N is the number of times we use delta. In this case, 9 allocations less on the 12 initially for x − y.

17.2 Tutorial: indicial expressions

In this tutorial we will learn how to create and use indicial expressions in CSL. To do so, we will need vector spaces, indices, and tensors. Some vector spaces are already built in CSL but in this tutorial we do not use them and create them from scratch.

17.2.1 Space creation

To create a Space, one must provide a name, a dimension, and the metric tensor (with its name) if it is not trivial (see section 11.2 for more details). Here is how to create two 3D spaces, one euclidean and one with a Minkowski metric3  −1 0 0  g =  0 1 0  . 0 0 1

// Flat space Space space3D("R_3", 3);

// Minkowski space Expr metricTensor = matrix_s( {{-1, 0, 0}, {0, 1, 0}, {0, 0, 1}} ); Space signedSpace3D("R_1_2", 3,"g", metricTensor); When building a vector space, the Kronecker delta, metric, and epsilon tensor are automatically constructed. For more details on matrices and vectorial objects in general see section 10.

17.2.2 Indices and tensors

In an indicial object like gµν there is two types of entities. The abstract tensor g, unique, with some properties, and indices. The abstract tensors are Tensor in CSL. They are in fact (shared) pointers to IndicialParent. The object that the user must create is an Tensor through the function csl::tensor_s(), not IndicialParent directly. This object represents the abstract tensor. In par- ticular, it is unique in the program, contains all the properties, and may generate actual tensors (TensorElement) that appear in expressions. See section 11.3 for more details.

3Here we take a Minkowski metric with a sign on the time component in order to have a negative signature in the example.

174 Tensorg= tensor_s(...);// Abstract tensor, unique Index mu = ...; // First index Index nu = ...; // Second index Expr expr = g({mu, nu});// pointer to TensorElement, symbolic expression

17.2.3 Basic space tensors

Here we will see how to access built-in tensors for a given Space (as a pointer to a space). In the following is a function showing how to get the Kronecker delta, metric, and epsilon tensor for any vector space, and to print elements of these tensors. void printTensors(Space *space) { Tensor delta = GetDelta(space); Tensorg= GetMetric(space); Tensor eps = GetEpsilon(space);

Indexi= GenerateIndex(space,"i"); Indexj= GenerateIndex(space,"j"); Indexk= GenerateIndex(space,"k");

std::cout <<"Delta␣:␣" << delta({+i, j}) << std::endl; std::cout <<"Metric␣:␣" << g({+j, +i}) << std::endl; std::cout <<"Epsilon␣:␣" << eps({k, j, i}) << std::endl; } i ij This function gathers the three tensors, creates three indices in the space and displays δj, g , and ijk (see section 14 for details on interface functions). Applying this to our two vector spaces space3D and signedSpace3D, we get: printTensors(&space3D); // >> Delta: delta_{i,j} // >> Metric: delta_{i,j} // >> Epsilon:-eps_{i,j,k} printTensors(&signedSpace3D); // >> Delta: delta_{+i,j} // >> Metric: g_{+i,+j} // >> Epsilon:-eps_{i,j,k} An index is displayed with a possible sign (upper indices have a + sign before the name), a possible dummy condition (if the index is summed over, a % sign appears before it) and usually an ID (to distinguish indices with different names) whose display has been disabled here. Please note two things here. First, the metric tensor does not really exist for a non signed space. Signs of indices are ignored and all calls to the metric are redirected to the delta. gij becomes then 4 δij. Then, indices are sorted by alphabetical order among possible symmetries of the tensor, and a sign is introduced if it is an anti-symmetry. Here we have in alphabetical order i < j < k, and CSL does the following replacements:

gji → gij, g is symmetric,

kji → −ijk,  is anti-symmetric and the permutation is odd.

4In general g and delta may be used arbitrarily by the user, CSL will always take the right tensor in the end.

175 17.2.4 Basic indicial properties CSL sums automatically repeated indices, takes care of index checking, and applies basic contraction properties without asking. Expressions like Xµ +Y ν (free indices not matching) or XµY µ in a signed space (contracted indices should have opposite signs) make CSL raise an error. This helps a lot when writing indicial expressions, to catch errors early and correct them. CSL raises and lowers indices automatically, in particular for any tensor A the properties

µ...... gµνA = Aν , ν µ... ν... δµA = A , µν ν g Aµ... = A..., µ δν Aµ... = Aν..., µ δµ = D, µ gµ = D, and all symmetric counter parts if they have, are applied automatically. Here D is the dimension of the space. Here are some examples of tensor manipulations. CSL code is showed next. On left-hand sides there is what is given by the user, and to the right the simplified output of CSL:

i gijX = Xj, i δjXi = Xj,

ikjlnm = ijklmn, im g ijklmn = ±(gjngkl − gjlgkn), il jm kn g g g ijklmn = ±D!.

In these equalities lie all properties discussed in this section, plus the epsilon contraction property

i jkilm = s · (gjlgkm − gjmgkl), (17.1) with all its (anti-) symmetric counter parts, where s is the sign of the metric tensor. For a flat space, s = 1 and s = −1 for a (3 + 1)D Minkowski space. In CSL this gives: void printProperties(const Space *space) { Tensor delta = GetDelta(space); Tensorg = GetMetric(space); Tensor eps = GetEpsilon(space);

Indexi= GenerateIndex(space,"i"); Indexj= GenerateIndex(space,"j"); Indexk= GenerateIndex(space,"k"); Indexl= GenerateIndex(space,"l"); Indexm= GenerateIndex(space,"m"); Indexn= GenerateIndex(space,"n");

TensorX("X", {space}); TensorY("Y", {space});

176 std::cout << g({i, j}) * X(+i) << std::endl; std::cout << delta({+i, j}) * X(i) << std::endl; std::cout << eps({i, k, j}) * eps({l, n, m}) << std::endl; std::cout << g({+i, +m}) * eps({i, j, k}) * eps({l, m, n}) << std::endl; std::cout << g({+j, +m}) * g({+i, +l}) * g({+k, +n}) * eps({i, j, k}) * eps({l, m, n}) << std::endl; }

// // ... // printProperties(&space3D); // >> X_j // >> X_j // >> eps_{i,j,k}*eps_{l,m,n} // >> delta_{j,n}*delta_{k,l}+-delta_{j,l}*delta_{k,n} // >>6 printProperties(&signedSpace3D);\\ // >> X_j // >> X_j // >> eps_{i,j,k}*eps_{l,m,n} // >>-(g_{j,n}*g_{k,l}+-g_{j,l}*g_{k,n}) // >> -6 You see here how to create a basic tensor. As we said, one must call the function csl::tensor_s() and give at least a name and a list of Space pointers (in curly braces). We will see later how to create more complex tensors.

17.3 Tutorial: going further with tensors

In the previous section we saw how to define and use basic indicial tensors. Now let’s dig a little more and talk about tensor properties. In this tutorial we will show how to define γ−matrices in 2D (for simplicity) and their multiple properties.

17.3.1 Framework We will work here with a Clifford algebra in 2D Minkowski space-time. The metric is then  1 0  g = . µν 0 −1

The Clifford algebra is then defined as

{γµ, γν} = 2gµν.

A possible realization of this algebra is  0 1  γ0 = , 1 0  −i 0  γ1 = . 0 i

177 17.3.2 CSL definitions

Within CSL, we may define the vector space, the metric, the gamma tensor and indices from scratch by writing: Expr metricTensor = matrix_s( {{1, 0}, {0, -1}} );

Space minko2D("Minko2D", 2,"g", metricTensor); Space dirac2D("Dirac2D", 2);

Expr gammaTensor = highDTensor_s( { {{0, 1}, {1, 0}},

{{-CSL_I, 0}, {0, CSL_I}} } ); Tensor gamma( "\\gamma", {&minko2D, &dirac2D, &dirac2D}, gammaTensor ); SetComplexProperty(gamma, ComplexProperty::Complex);

Tensor delta = GetDelta(&dirac2D);

Index mu = GenerateIndex(&minko2D,"\\mu"); Index a1 = GenerateIndex(&dirac2D,"a"); Index a2 = GenerateIndex(&dirac2D,"a"); Index a3 = GenerateIndex(&dirac2D,"a"); Index a4 = GenerateIndex(&dirac2D,"a"); We have already seen how to define 1D and 2D tensors. To create a tensor with more than 2 dimen- sions, one must use csl::highDTensor_s(). The only new feature left is the use of csl::SetComplexProperty() that specifies the complex property of the gamma tensor (Real is the default, can be Imaginary or Complex in this case). Concerning gamma matrices, one can check that we have the following properties:

(γµ)† = γ0γµγ0, γ0γ0 = 1, γ1γ1 = −1, µ γ γµ = 2.

CSL allows to define this kind of simple properties that will be used automatically when needed. Now let’s use these properties on a simple example. Consider

µ 0† γ γµγ . (17.2)

178 We may simplify this expression now as

µ 0† µ 0† † γ γµγ = γ · γ · (γµ) µ 0 0 0 0 0 = γ · γ γ γ · γ γµγ µ 0 = γ γµγ = 2γ0.

CHANGE THAT PART ACCORDINGLY TO RECENT MODIFICATIONS Before seeing how this works with CSL, let’s do a quick reminder of how indices computations work. Consider the scalar aT Mb, with a and b two vectors and M a matrix in a given vector space. With explicit indices, the expression becomes T X X a Mb = aiMijbj. i j

The right-hand side is decomposed as a sum of matrix and vector elements. In particular, Mij are T just numbers. So we should have (Mij) = Mij. If we apply transposition on both sides of the equation we get T T X X X X b M a = bjMijai = aiMijbj. i j i j This is correct. Knowing that, identities like AT = A, AT = BCT are expressed with indicial notation the following way:

T Aij = Aji, T Aij = BjkCik. Indices have to be reverted in the result to have a correct expression. In particular, for gamma matrices we have µ† 0 µ 0 γαβ = γ γ γ βα . Now let’s simplify the expression 17.2 using CSL. In indicial notation this corresponds to

µ 0† µ † 0† γ γµγ = γ γ γ a1a2 a3a2 a1a2 µa3a4 a4a2 = γµ γ0γµγ0 γ0γ0γ0 a1a2 a4a3 a2a4 µ µ 0 = γa1a2 γa2a4 γa4a3 0 = 2γa2a3 . Note that indices of the initial product have been reversed as γµγ0† = γµγ0 becomes µ 0† µ 0 γ γ αβ = γ γ βα with explicit indices, and we want the resulting γµ to contract with the first one.

179 17.3.3 CSL property definitions The definitions of properties in CSL is extremely simple. You just have to call an interface func- tion and give it the needed information about the property. All the rest is automatic. For a self- contraction, you must provide the (abstract) indicial tensor the two sides of the contraction and the result. For our three γ−matrices contractions, it yields: // gamma^0* gamma^0=1 AddSelfContraction( gamma, gamma({0, a1, a2}), gamma({0, a2, a3}), delta({a1, a3}) ); // gamma^1* gamma^1= -1 AddSelfContraction( gamma, gamma({1, a1, a2}), gamma({1, a2, a3}), -delta({a1, a3}) ); // gamma^mu* gamma_mu=2 AddSelfContraction( gamma, gamma({mu, a1, a2}), gamma({+mu, a2, a3}), 2 * delta({a1, a3}) ); That leaves use with the hermitian conjugation of γµ. To define it, one must provide the same objects than for a contraction (except the left-hand side is composed now of an only tensor, not two) plus the space that is concerned with the transposition. We have then: AddHermitianProperty( gamma, &dirac2D, gamma({mu, a1, a2}), gamma({0, a2, a3})*gamma({mu, a3, a4})*gamma({0, a4, a1}) ); Do not forget to reverse indices when defining such property! You may also define a transposed prop- erty with addTransposedProperty() in exactly the same way, or a complex one with addComplexProperty() (also the same way, but no need to give any vector space this time). Now let’s try to write equation 17.2 in CSL, before and after property definitions: std::cout << gamma({+mu, a1, a2}) * GetHermitianConjugate( gamma({mu, a4, a3}) * gamma({0, a3, a2}), &dirac2D) << std::endl; // >>\gamma_{+%\mu_1,a_1,%a_3} //*\gamma_{%\mu_1,a_7,%a_5}^(*) //*\gamma_{+0,%a_5,%a_3}^(*)

// // ... Property definitions ... //

180 std::cout << gamma({+mu, a1, a2}) * GetHermitianConjugate( gamma({mu, a4, a3}) * gamma({0, a3, a2}), &dirac2D) << std::endl; // >> 2*\gamma_{+0,a_1,a_7} You see that CSL applied automatically the hermitian property of γ and then contraction properties of both γ0 and γµ. There is a priori no limit on the properties one can define, however please make sure that the defined properties are consistent with each other and not recursive5 otherwise nothing good will happen. In this example we used the interface function csl::GetHermitianConjugate() that takes an expression to modify, and the space (pointer) in which the transposition is done, here dirac2D.

17.4 Tutorial: When CSL is not enough

When bare features of CSL do not allow you to go where you want to go, algorithms are here to help. You may want to do conditional replacements, applying complex properties. . . Those algorithms are all documented in the file algo.h. We won’t detail here all algorithms, just show some examples. They all work the same way, learning to use one is enough.

17.4.1 Principle Recall that the internal representation of an expression in a tree as shown figure 17.1. In order

*

A +

1 cos

*

2 π t ^

T -1

2πt Figure 17.1: Internal representation of A(1 + cos T ). The red nodes (the leafs) are the building blocks, and the intermediate nodes are function of other sub-expressions, as a sum, a product, or a cos function. to apply arbitrary functions to each node of an expression, one should in general write a recursive function each time. Write one is ok, but if all your code is composed of recursive functions it becomes quickly difficult to read and maintain. Algorithms solve this problem. If you want to look over a whole expression, you only have to give the function (in the form of a c++ lambda expression) to apply to each node (or leaf). A lambda

5By recursive I mean properties that will call each other (or themselves), for example AT = BT defined for A and BT = AT defined for B (or worse, a property truly recursive like AT = B ∗ AT ). This can only cause a crash.

181 expression is an un-named function declared on the go, that can be stored in a variable6. You may create a lambda function that compares an integer with another: int witness = 5; auto compare = [&](int a) { return a < witness; }; std::cout << compare(2) << std::endl; // >>1 std::cout << compare(6) << std::endl; // >>0 The & symbol between brackets is needed if the lambda function uses variables outside its body, here the integer witness. Then the parameter definition and body are the same as for a regular function. If you store a lambda in a variable, do not forget the semi-colon after the lambda body or it will not compile. Let’s see a really quick example before getting in the tutorial. Consider the expression of figure 17.1  2πt f(t) = A 1 + cos . 0 T It is legitimate for you to ask the expression contains a sin or cos depending on the time t. This is a complex question that CSL cannot answer directly. One way out is to use the csl::AnyOfNodes() algorithm: Exprt= variable_s("t"); Expr A0 = constant_s("A_0"); ExprT= constant_s("T"); Expr f = A0 * (1 + cos_s(2 * CSL_PI * t / T)); auto myComplexCondition = [&](Expr const &expr) { if(!IsCos(expr) and!IsSin(expr)) return false; Expr argument = GetArgument(expr); return DependsExplicitlyOn(argument, t); }; std::cout << std::boolalpha; std::cout << AnyOfNodes(f, myComplexCondition) << std::endl; // >> true Or in the one-liner style: Exprt= variable_s("t"); Expr A0 = constant_s("A_0"); ExprT= constant_s("T"); Expr f = A0 * (1 + cos_s(2 * CSL_PI * t / T)); bool res = AnyOfNodes(f, [&](Expr const &expr)

6One possible type is std::function but one may use the auto keyword as well.

182 { if(!IsCos(expr) and!IsSin(expr)) return false; Expr argument = GetArgument(expr); return DependsExplicitlyOn(argument, t); }); std::cout << std::boolalpha << res << std::endl; // >> true Each algorithm is specialized either for all nodes, or all leafs. If the node specialization is chosen, each node of the tree will be used, including leafs. If the leaf specialization is chosen, only building blocks will be seen (in red figure 17.1). The lambda expressions given to the algorithms should take either:

• A Expr const& parameter if the algorithm is read-only (as csl::AnyOfLeafs()).

• A Expr& parameter if the algorithm may modify the expression (as csl::ForEachNode()).

17.4.2 An equation inverter with CSL Taking again the same expression as example, this time associating it to a variable x, we have  2πt x = A 1 + cos . (17.3) T

We propose in this tutorial to create a simple equation inverter with CSL. Equation 17.3 gives x as a function of t, but we may be interested in general to have t as a function of x. Of course it is not always possible, when the result has to analytical solution for example.7 In our example, the result is T  x  t = arccos − 1 , 2π A x with A 6= 0 and A − 1 ∈ [−1, 1].

The requirements of our inverter are the following:

• Treats only sums, products and cos function for simplicity (may be extended very easily to other types).

• Does not treat the multi-dependency case in sums and products(x = t+t2 for example) because those cases have no solution in general, so are hard to automate.

• Keeps track of requirements on different variables and constants in the inversion process. For example, x = a ∗ t is equivalent to t = x/a with a 6= 0.

• Be contained within 100 lines of c++ code, example included.

• Be as clear and simple as possible, using CSL interface and algorithms.

In this tutorial we will give the full code, in order to show that we respect de 100 lines requirements. First, we need a simple structure to keep information about inversion conditions:

7x = t cos t for example cannot be inverted using common functions.

183 #include using namespace csl; struct Condition {

Expr expr; std::string spec;

void print() const{ std::cout << expr << spec <<".\n";} }; The object contains the expression that has to respect some condition, and a std::string as the actual condition. This is really just an information container, it has not the pretension to be more than that. Then, we need the function that inverts each type of expression. The idea is the following. We start from x on the left-hand side, and our function to the right. At each step, x is modified the same way as the right-hand side. Step by step, we have  2πt x = A 1 + cos T x 2πt = 1 + cos A T x 2πt − 1 = cos A T  x  2πt arccos − 1 = A T T  x  arccos − 1 = t 2π A We do not actually have to modify the right-hand side of the equation because we know the result is t by construction. So we simply keep track of the left-hand side modifications. In order to have the most general inverter respecting the requirements, one has to write a function that looks over all the expression, down to its leafs. Each sub-expression containing t must be inverted, not the others. Here is such a function: void applyInverse( Expr &lhs, Expr const &rhs, Expr const &t, std::vector &conditions// conditions to respect, may be extended ) { Type type = GetType(rhs); switch(type) {

case Type::Cos: conditions.push_back({lhs,"␣in␣[-1,␣1]"}); lhs = acos_s(lhs); break; case Type::ACos: lhs = cos_s(lhs); break;

case Type::Sum:

184 case Type::Prod: { bool dependencyFound = false; for(size_t i = 0; i != Size(rhs); ++i) { Expr argument = GetArgument(rhs, i); if(DependsExplicitlyOn(argument, t)) { if (dependencyFound) { std::cerr <<"Error:␣Double␣dependency␣not␣possible.\n"; return; } else dependencyFound = true; } else{ if (type == Type::Sum) lhs = lhs - argument; else{ lhs = lhs / argument; conditions.push_back({argument,"␣!=␣0"}); } } } break; }

default: break; } } It is easy to understand what is going on with cos and arccos here. For sums and products, we look every arguments checking that at most one depends on the variable, and apply the inverse on lhs. For sum we subtract the argument, and for products we divide by it, keeping the condition that it must be different than 0. The last piece of code is now dedicated to the use of our function acting on one node, applying it to the whole x(t). This is more tricky. In particular one has to choose the right algorithm. Here is one solution: std::vector invert( Expr &lhs,// left-hand side containing the result at the end Expr const &rhs,// Initial expression to invert Expr const &t ) { std::vector conditions; VisitEachNodeCut(rhs, [&](Expr const &node) { if(not DependsExplicitlyOn(node, t)) // Stop digging return true;

applyInverse(lhs, node, t, conditions);

185 return false; });

return conditions; } The right algorithm is csl::VisitEachNodeCut(). First, the algorithm must apply on every nodes, not only leafs. Then, we do not want to modify the initial expression (so we do not use csl::ForEachNodeCut()) and we want the algorithm to stop digging when arriving on nodes that does not depend on t. In our example, T −1 is of course not inverted. Once arriving on that node that does not depend on t, the al- gorithm stops. This is what csl::VisitEachNodeCut() allows to do more than csl::VisitEachNode(). Our lambda must return a boolean. If true, the recursion stops on this node. If the node depends on the variable we apply the inverse, else we stop digging. Finally, the main function that tests the inverter: int main() {

ExprA= constant_s("A"); ExprT= constant_s("T"); Exprt= variable_s("t");

Expr x = A * (1 + cos_s(2* CSL_PI * t / T)); std::cout <<"x␣=␣" << x << std::endl;

Expr target = variable_s("x"); auto conditions = invert(target, x, t); std::cout <<"t␣=␣" << target << std::endl; for(const auto& cond : conditions) cond.print();

return 0; } The output is the following: >> x = A*(1 + cos(2*\pi*t/T)) >> t = 1/2*T*acos(-1 + x/A)/\pi >> A != 0. >> -1 + x/A in [-1, 1]. >> 2 != 0. >> T^(-1) != 0. >> \pi != 0.

We see that the inverter inverts successfully the expression x(t), keeping track of all conditions.8 This sample code could be improved and extended. The idea here is just to show that one can do relatively complex operations on expressions using algorithms. We write a function that acts simply on one node as wanted and we call the right algorithm that automatically applies that recursively in the expression tree. The example presented here is a bit complicated. Most of the time, simpler algorithms work perfectly. For a detailed explanation on a particular algorithm, please see the file algo.h.

83 out of 5 conditions are correct but irrelevant. For simplicity and because it is not crucial, I let them. A solution would be to specialize Condition to recognize and discard irrelevant conditions.

186