Jupyter Notebook is a scripting environment which, among other languages, also supports C#. BoSSSpad is a C#-library which can be used to access BoSSS through Jupyter. While it is not necessary to be a master-class programmer for the tutorials which come up, it is required to have some basic knowledge about the C# syntax.
Here, the following topics are covered:
Normally, a program/application written in C# consists of one or more text files (source code, ending with .cs) which are read by a compiler and converted into an executable (ending with .exe).
In Jupyter, instead, one can enter single statements or snippets of C# code and execute them on the fly. (These snippets are not complete programs on their own, and they would not compile in the traditional way.)
CsharpAndBoSSSpad.ipynb
.
One can directly load this into Jupyter to interactively work with the following code examples.BoSSSpad.dll
library in order to load BoSSS-specific functions into Jupyter.
In the following line, the reference to BoSSSpad.dll
is required.
You must either set #r "BoSSSpad.dll"
to something which is appropirate for your computer
(e.g. C:\Program Files (x86)\FDY\BoSSS\bin\Release\net5.0\BoSSSpad.dll
if you installed the binary distribution),
or, if you are working with the source code, you must compile BoSSSpad
and put it side-by-side to this worksheet file
(from the original location in the repository, you can use the scripts getbossspad.sh
, resp. getbossspad.bat
).#r "BoSSSpad.dll"
using System;
using System.Collections.Generic;
using System.Linq;
using ilPSP;
using ilPSP.Utils;
using BoSSS.Platform;
using BoSSS.Foundation;
using BoSSS.Foundation.Grid;
using BoSSS.Foundation.Grid.Classic;
using BoSSS.Foundation.IO;
using BoSSS.Solution;
using BoSSS.Solution.Control;
using BoSSS.Solution.GridImport;
using BoSSS.Solution.Statistic;
using BoSSS.Solution.Utils;
using BoSSS.Solution.Gnuplot;
using BoSSS.Application.BoSSSpad;
using BoSSS.Application.XNSE_Solver;
using static BoSSS.Application.BoSSSpad.BoSSSshell;
Init();
Any data like single numbers, vectors, strings or composite data objects (like the numerical grid) is stored resp. referenced via a \emph{variable}. A variable declaration consists of a type and a name, and reads e.g. as:
int i;
The above statement created a variable named i of type int (for integer numbers). The most commonly used number types in BoSSS are int for integers and double for real numbers.
A full list of C#-build-in variable types for numbers can be found online (see integral-types-table and floating-point-types-table)
Next, we assign a value to the new variable:
i = 9;
If we just enter the variable name without semicolon, BoSSSpad, resp. Jupyter prompts the variable in the result window (if the variable can anyhow be presented as text):
i
Declaration and assignment can also be put into a single line:
int j = 9;
double x = 4.6;
Next, we test assign a new value to the already-defined variable i:
i = j + (int)x; // cast x to int; add to j; store result in i
i
Note on the snippet above:
**x** is **cast** to **int**, which truncates the decimal digits,
before it can be added and stored as integer.
original, non-interpreted C\# language -- this would produce a
compile error. The C\# interpreter, instead, produces a text from
the last
incomplete statement in a snippet.
the line is not interpreted and can contain any text.
While assigning double to int requires a cast operation, the other way works automatically (this is called an implicit cast), since very int can be represented as double (but not vice-versa):
double y = j + x;
y
C# defines the standard operations on numbers found in most programming languages:
y = y + 1;
y
y += 1;
y
y++;
y
string s = "some Text";
s
some Text
Which also supports some (but not all) operators:
string t = " and more string";
s + t
some Text and more string
s + t + ": " + j
some Text and more string: 9
For every type, one can define arrays; e.g. an array of integers is defined and initialized as
int[] A = new int[5];
The line above creates an array with 5 entries. Initially, all entries are set to the default value 0:
A
index | value |
---|---|
0 | 0 |
1 | 0 |
2 | 0 |
3 | 0 |
4 | 0 |
One can access and manipulate individual entires by index-brackets; Note that C# indices (usually) start at zero:
A[2] = 7;
A
index | value |
---|---|
0 | 0 |
1 | 0 |
2 | 7 |
3 | 0 |
4 | 0 |
It is also possible to initialize an array with non-default values:
string[] B = new string[] { "Zero", "One", "Two" };
B
index | value |
---|---|
0 | Zero |
1 | One |
2 | Two |
C# also defines arrays with more than one index, e.g.:
int[,] M = new int[3,4];
M[2,1] = 3;
In BoSSS, there is a separate type for multidimensional arrays of real numbers.
In contrast to the multidimensional arrays build into C#, their BoSSS counterparts offer more operations, e.g. reshaping the array or tensor multiplications.
An array with three dimensions can be initialized as:
MultidimensionalArray Q = MultidimensionalArray.Create(5,3,4);
However, a comprehensive introduction of the MultidimensionalArray type is beyond the scope of this section. We may refer the reader to the API reference
So far, we used only primitive (build-in) datatypes. The core idea of object orientation is to define composite data structures (called classes) upon the primitive types and other classes.
Furthermore, classes provide the concept of encapsulation: any member variable that should be manipulated from code outside of the class must be decorated with the public keyword. Otherwise, the member is private and can only be manipulated form inside the class. (This is demonstrated in below)
class MyVector {
// public variables.
public double x;
public double y;
public string Name;
}
(5,19): warning CS0649: Field 'MyVector.Name' is never assigned to, and will always have its default value null (4,19): warning CS0649: Field 'MyVector.y' is never assigned to, and will always have its default value 0 (3,19): warning CS0649: Field 'MyVector.x' is never assigned to, and will always have its default value 0
One may think of classes as blueprints, which are used to create objects. Whenever the new operator is applied to a class, a new object is created in computer memory. This is called instantiation.
Objects are accessed via variables, just in the same way as arrays.
We declare a variable of type MyVector and intialize it with an instance of this class:
MyVector vec = new MyVector();
Then, we can manipulate its variables (also called members):
vec.x = 1.2;
vec.y = 4.1;
vec.Name = "BoSSS";
Each object owns its own space in memory; so when we instantiate a second object of this type, the members of vec stay the same:
MyVector vec2 = new MyVector();
vec2.x = -3.1;
vec2.y = 22.4;
vec2.Name = "Another BoSSS";
We see, both objects have their independent variables:
vec.Name
BoSSS
vec2.Name
Another BoSSS
The next important idea of object orientation is to bundle data structures with the code which manipulates this data. In imperative programming languages (e.g. C, FORTRAN, Matlab) these code is organized in so-called functions, aka. subroutines.
In C# such functions are put into the class and they can access all variables in the class, including private ones. These functions are often called methods or member functions.
A function consists of the following parts:
An optional access modifier, e.g. public or private; if no access modifier is given, the method is private.
The type of the value returned by the method; this can be either a build-in type like double or some class. If the method should not return a value, its return type is void.
A function name
class MyAdvancedVector {
// two private variables, which cannot be accessed from outside:
double x;
double y;
// a function which only manipulates internal data, but has neither
// a return value nor an input argument:
public void Rotate90Deg() {
double temp = x; // back-up x in a local variable
x = y; // overwrite x
y = -temp; // set y
}
// a function with an input argument:
public void Scale(double factor) {
x *= factor;
y *= factor;
}
// a function which only returns a value:
public double LengthSquared() {
double sqSum = 0;;
sqSum += x*x;
sqSum += y*y;
return sqSum; // returning the result of the function
}
// a function with multiple input arguments, and a return value;
// to avoid confusion, it is better to chose names for the input arguments
// that do not conflict with member variables:
public double Set_xy(double new_x, double new_y) {
x = new_x;
y = new_y;
return LengthSquared();
}
}
MyAdvancedVector vecX = new MyAdvancedVector();
Next, we demonstrate how to execute member functions of our new class:
vecX.LengthSquared()
vecX.Set_xy(2,3)
vecX.Rotate90Deg()
vecX.LengthSquared()
Properties are special methods, which look like functions. Most of the time, they are used to execute additional code before setting a variable.
class PropertiesDemo {
// a private variable:
int i1;
// reading/setting the value through methods:
// (old-fashioned)
public int Get_i1() {
return i1;
}
public void Set_i1(int new_i1) {
// here, one can put code to perform additional operations,
// e.g. check that the passed argument is positive, (if this is desired)
i1 = new_i1;
}
// reading/setting the value i1 through a property
public int I1 {
get {
// the get-part: like a function,
// * no arguments
// * returns int
return i1;
}
set {
// the set-part: like a function,
// * one argument of type int, vamed value
// * no return value/void return value
i1 = value;
}
}
}
Again, we instantiate the class:
PropertiesDemo p = new PropertiesDemo();
Access to the member variable through methods would work as
p.Set_i1(49);
p.Get_i1()
while the property-notation is more compact, but has the same functionality (syntactic sugar):
p.I1
p.I1 = 33;
p.I1
So far, we always had to instantiate a class to execute its methods. Now think e.g. about mathematical functions, like the sinus. In languages like C or FORTRAN, one would just evaluate them as sin(x).
The designers of C# could have allowed functions like in C, but decided against it because it would compromise the object-oriented syntax. On the other hand, creating an object before one is able to evaluate a sinus function also seems over-complicated. The compromise are static functions:
class StaticDemo {
// in addition to normal functions, static functions are decorated
// with the keyword static:
static public int Summation(int a, int b) {
return a + b;
}
}
Then, static functions can be involved without instantiating an object:
StaticDemo.Summation(1, 2)
Like most programming languages, C# also ships with an extensive programming library (class libraries), e.g. to read and save files, mathematical functions, graphical output;
The full list of standard libraries can be found at here.aspx).
These libraries are collections of classes; they are organized in namespaces.
Any class within a loaded namespace can be accessed directly; e.g. the classes Console and Math, which are part of the System namespace:
Console.WriteLine("Hello World!");
Hello World!
(By the way: the Console class, resp. the Console.WriteLine method is the preffered way to write text output (for non-windowed apps without graphical user interface).
In BoSSSpad, it is very handy if one wants to write more information than just the last object in a snippet.
Math.Sin(2.0);
This is the same as calling the methods by their full namespace path:
System.Console.WriteLine("Hello World!");
Hello World!
System.Math.Sin(2.0);
If a namespace is not loaded, its classes must be accessed via their full path, e.g.
System.Collections.BitArray ba = new System.Collections.BitArray(3);
If we load the namespace by the using keyword, we can access the BitArray class directly:
using System.Collections;
BitArray ba = new BitArray(3);
Note that, in order to work with a class library, it may not be enough to just add a using directive. All libraries are contained in dll files, e.g. System.dll (standard C# library) or BoSSS.Foundation.dll (part of BoSSS).
These files must be loaded before any class from the library can be used. (Namespaces are only for the internal organization of the library.) Initially, BoSSSpad loads a set of libraries which are useful for working with BoSSS.
For most use-cases, this set may be sufficient; however, not that by the Mono.CSharp.InteractiveBase.LoadAssembly(string) method, it is possible to load further assemblies.
Regarding the standard syntax of the language for implementing a function, C# designers tried to stay close to popular existing languages like C, C++ and Java.
In this tutorial, we briefly discuss two control structures, branches and loops. At first, the if-branch;
We use a random number generator create random input data for our example. Therefore, the following snippet runs differently each time it is executed:
Random rnd = new Random(); // Generate new random number generator object
double randomVal = rnd.NextDouble();
Console.WriteLine("Random number is " + randomVal);
// here, the if-clause
if(randomVal > 0.5) { // some condition, which is ether true or false
// Code that will be executed when the condition is true
Console.WriteLine("greater than 1/2");
} else {
// Code that will be executed when the condition is false
Console.WriteLine("smaller or equal to 1/2");
}
Random number is 0.8152774167597996 greater than 1/2
Next, the for-loop. It has three `Arguments':
An initialization code (int j = 0): executed before the loop starts.
A runtime condition (j < J): this is checked before each iteration of the loop; if it is false, the loop terminates.
An increment statement (j++): this is executed after each iteration of the loop and usually used to increase or decrease some variables value.
int J = 7;
for(int j = 0; j < J; j++) {
// code which is executed repeatedly
double s = Math.Sin(j);
Console.WriteLine("The sinus of " + j + " is " + s);
}
The sinus of 0 is 0 The sinus of 1 is 0.8414709848078965 The sinus of 2 is 0.9092974268256817 The sinus of 3 is 0.1411200080598672 The sinus of 4 is -0.7568024953079282 The sinus of 5 is -0.9589242746631385 The sinus of 6 is -0.27941549819892586
Another kind of loops is the foreach statement. It executes the loop body for each element of a list or array:
double[] SomeValues = new double[] { 1.2, -3.4, 7.3, 3.1, 3.14 };
foreach(double x in SomeValues) {
double s = Math.Sin(x);
Console.WriteLine("The sinus of " + x + " is " + s);
}
The sinus of 1.2 is 0.9320390859672263 The sinus of -3.4 is 0.2555411020268312 The sinus of 7.3 is 0.8504366206285644 The sinus of 3.1 is 0.04158066243329049 The sinus of 3.14 is 0.0015926529164868282
In its heart, C# is an object-oriented programming language, and methods are implemented in a imperative style. However, the designers also included concepts from the functional programming model.
A key idea of functional programming is that functions/methods can be treated as variables; such variables are called delegates.
In order to demonstrate the use of delegates, we create a class with some static methods:
class DelDemo {
public static double Pow2(int x, float z) {
return x*x*z;
}
public static void Print(int x, double y) {
Console.Write("Got the numbers " + x + " and " + y + ".");
}
}
Now, we want to create variables/delegates which point to the functions/methods defined above. As for any variable declaration, we need to specify type and name. For functions which have a return type, it works like this: Note: the last type in the '< >' - List:
Func<int,float,double> delPow2 = DelDemo.Pow2;
Then, the method can be called via the delegate: Note: the 'f'-suffix is for single precision types; this also shows round-of errors of floating point arithmetics
delPow2(2, 1.1f)
Functions without a return type (void) are also called actions:
Action<int,double> a = DelDemo.Print;
a(1,1.2)
Got the numbers 1 and 1.2.
By the keyword delagate, C# designers finally (in the C# version 3.0) reversed their original decision that functions are only allowed within classes by the introduction of anonymous functions:
delPow2 = delegate(int x, float z) {
double retval;
retval = x*x*z*2.0;
return retval;
};
Now, the variable delPow2 does not anymore point to the method DelDemo.Pow2, but to the newly defined, anonymous function:
delPow2(2, 1.1f);
To make notation more compact, a function-like syntax was introduced:
delPow2 = (x, z) => x*x*z*4.0;
delPow2(2, 1.1f)
Since delegates can be treated as normal variables, they can also be used as arguments to other functions; here is a more advanced example, an action (no return value) which takes as inputs another action, a function and an integer value:
Action<Action<int,double>,Func<int,float,double>,int> apply =
delegate(Action<int,double> act, Func<int,float,double> fun, int i) {
float f = 1.0f;
double y = fun(i, f); // apply function 'fun' to argument 'i'
act(i, y); // hand both numbers to action 'act'
return;
};
apply(a,delPow2,2);
Got the numbers 2 and 16.
As already discussed, classes are blueprints for objects: once a class is implemented, it can be used to create an infinite number of objects. Of course, only until the computers memory is full.
The model of a single blueprint is, however, sometimes a bit inflexible. Engineers knew this for a long time: in a group of products, you may have two products that have a lot in common, but also significant differences regarding some functionality.
Object oriented programming languages provide the concept of inheritance: Common functionality is implemented in a common base class.
First we define some class; functions or properties which are decorated as virtual
class BaseClass {
public int num;
// this is a normal method, which will be accessible form
// any derived class
public void Method1() {
Console.WriteLine("BaseClass: Hello from Method1, num is " + num);
}
// this is a virtual method, its functionality can be overriden in derived
// classes
public virtual void VirtMeth(int i) {
Console.WriteLine("BaseClass: VirtMethod, i is " + i);
}
}
(2,16): warning CS0649: Field 'BaseClass.num' is never assigned to, and will always have its default value 0
Now, we derive two classes from BaseClass:
class DerivA : BaseClass { // indicates that thes class is derived
// from 'BaseClass'
// We can add new members:
public double SomeVal;
// a private member:
Random rnd = new Random();
// And override functionality from the base class
public override void VirtMeth(int i) {
SomeVal = rnd.NextDouble() + i;
Console.WriteLine("DerivA: VirtMethod, random value is " + SomeVal);
}
}
class DerivB : BaseClass {
// Again, some override:
public override void VirtMeth(int i) {
num += i; // we can modify members of the base class
Console.WriteLine("DerivB: VirtMethod, num is " + num);
}
}
As usual, we can instantiate and use BaseClass:
BaseClass b = new BaseClass();
b.Method1();
b.VirtMeth(1); // calls base implementation
BaseClass: Hello from Method1, num is 0 BaseClass: VirtMethod, i is 1
Of course, we can do the same with DerivA; since it is derived from BaseClass, an object of type DerivA can be stored in (resp. referenced from) a variable of type BaseClass.
In short, any DerivA-object is autamtically also a BaseClass-object, due to inhertance (but not the other way).
BaseClass b = new DerivA(); // we instantiate an object of type 'DerivA'
// but the variable is of type 'BaseClass'.
b.Method1(); // non-overridden functionality: from base implementation
b.VirtMeth(1); // calls override implementation
BaseClass: Hello from Method1, num is 0 DerivA: VirtMethod, random value is 1.2455715600832251
We can also store DerivB-objects in variable b:
b = new DerivB();
b.Method1();
b.VirtMeth(1); // 'DerivB.VirtMeth' modifies the internal state of the object,
b.VirtMeth(1); // so the result is different for each call
BaseClass: Hello from Method1, num is 0 DerivB: VirtMethod, num is 1 DerivB: VirtMethod, num is 2
Note: implicitly, all classes in C# are derived vom the class ystem.Object, and has all the methods of this class.
object o; // The 'object' keyword is a build-in alias for 'System.Object'
o = new BaseClass();
o.ToString() // 'ToString' is a virtual method of 'System.Object', which,
// if not overriden only retunrs the name of the class
Submission#69+BaseClass
In lots of situations, there is no reasonable base-implementation for a method.
abstract class AbstractBase { // if the class conatains any abstract member,
// it must be declared as abstract.
public int num;
// like above, a normal method:
public void Method1() {
Console.WriteLine("AbstractBase: Hello from Method1, num is " + num);
}
// this is an abstract method, for which only the
// signature (return value, arguments, etc.) is specified, but no
// implementation is given:
public abstract void VirtMeth(int i);
}
(4,16): warning CS0649: Field 'AbstractBase.num' is never assigned to, and will always have its default value 0
Note:
Virtual methods can be overridden, i.e. the override is optional
Abstract methods must be overridden
Abstract classes cannot be instantiated, since functionality is missing
Abstract classes can be used as a variable type
class DerivC : AbstractBase {
// we are enforced to override the base method
public override void VirtMeth(int i) {
Console.WriteLine("DerivC: VirtMeth, i is " + i);
}
}
AbstractBase a; // use 'AbstractBase' as a variable type
a = new DerivC(); // instantiate a 'DerivC' object, store it in 'a'
a.VirtMeth(2); // call the objects methods
DerivC: VirtMeth, i is 2
When we finish the concept of abstract classes, we come to classes that only have abstract members. These are called interfaces and C# has a special keyword for them:
interface IMethodsInterface {
void MethodA(); // methods in an interface are always public and need
void MethodB(); // no modifier.
public void MethodC() {
Console.WriteLine("DerivD: MethodA");
}
}
interface IPropertyInterface {
int Property { get; set; }
}
there is a widly used convention to start interface names with capital letter `I'. This is only a convention, not a rule.
An important difference between classes and interfaces is, that any class can have only one base-class, but it can implement an unlimited number of interfaces:
class DerivD : AbstractBase, IMethodsInterface, IPropertyInterface {
// form 'AbstractBase'
public override void VirtMeth(int i) {
Console.WriteLine("DerivD: VirtMeth, i is " + i);
}
// from 'IMethodsInterface'
public void MethodA() {
Console.WriteLine("DerivD: MethodA");
}
public void MethodB() {
Console.WriteLine("DerivD: MethodB");
}
// form 'IPropertyInterface'
public int Property {
get {
Console.WriteLine("This is the get property.");
return num;
}
set { num = value; }
}
}
Of course, we can instantiate DerivD and call any method or property:
DerivD d = new DerivD();
d.VirtMeth(1);
d.MethodB();
DerivD: VirtMeth, i is 1 DerivD: MethodB
But we can also store the object in references with the interface type:
IMethodsInterface imd = new DerivD();
imd.MethodA();
imd.MethodB();
DerivD: MethodA DerivD: MethodB
IPropertyInterface ipd = new DerivD();
ipd.Property = 44;
ipd.Property
This is the get property.
The execution model of BoSSSpad
The term which describes the execution model of BoSSSpad best is called read-eval-print-loop (REPL): commands are read from the input window, evaluated by the interpreter, the result is printed to the screen and this is repeated in an infinite loop (until the app is terminated).
Each time a command is executed, the internal state of the interpreter
is changed. E.g. new variables or classes are defined. If a variable or
class name is already used, it will be overwritten. The special command
restart resets the interpreter to an initial state.
On top of that, BoSSSpad puts a document model: the user can navigate backward and re-execute any command; this may or may not change the behavior of the entire script. A simple example is:
We are defining an int variable:
int i = 9; // int-definition
Next, we overwrite the name i with an new variable
double i = 1.1; // double-definition
And execute some code which depends on i, e.g.:
Obviously, if we would navigate back to the int-definition-statement, re-execute it, but skip the execution of the double-definition statement, the following statement will behave differently:
Math.Cos(i) // depends on last 'i'
As we see,...
This behavior might seem confusing (and very often, it is). However, it is desired because it speeds up the workflow, because it allows to alter or correct a script, without (time-consuming) re-evaluation of the entire script. As a rule of thumb, take this:
Obviously, this minimal tutorial does not cover the entire C# language. It should, however, demonstrate everything which is needed to understand about 90\% of the tutorials in the BoSSS handbook.
Finally: there are lots of tutorials on C# online. Some of them are excellent, some are not so good. And: Google is your friend (at least in this respect)...