Skip to content

Commit 7d98ea3

Browse files
committed
Lots of docs
Added extra documentation for much of simlib.
1 parent 34156b1 commit 7d98ea3

File tree

6 files changed

+311
-0
lines changed

6 files changed

+311
-0
lines changed

docs/compartments.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Compartments
2+
3+
Inside ngcsimlib there is the global state as the backbone of any given model.
4+
This global state is the culmination of all the dynamic or changing parts of the
5+
model. Each value that builds this state is stored in a special container that
6+
helps track these changes known as a Compartment.
7+
8+
## Practical Information
9+
10+
Practically when working with compartments there are a few simple things to keep
11+
in mind and the rest is all behind the scenes bookkeeping. The first piece to
12+
keep in mind is that each compartment holds a value and thus setting a
13+
compartment with `myCompartment = newValue` will not function as intended as
14+
this will overwrite the python object that is the compartment with `newValue`.
15+
Instead, make use of the `.set()` method to update the value stored inside a
16+
compartment so `myCompartment = newValue` becomes `myCompartment.set(newValue)`.
17+
The second piece of information is that to retrieve a value from the compartment
18+
use `myCompartment.get()`. These methods to get and set data inside a
19+
compartment are the two main pieces to remember.
20+
21+
## Technical information
22+
23+
The follow sections are devoted to more technical information regarding how a
24+
compartment functions in the broader scope of ngcsimlib and how to leverage that
25+
information.
26+
27+
### How data is stored
28+
29+
The data stored inside a compartment is not actually stored inside a
30+
compartment. Instead, it is stored inside the global state and each compartment
31+
just holds the path or `key`, in the global state to pull a specific piece of
32+
information. As such it is technically possible to manipulate the value of a
33+
compartment without actually touching the compartment object created in a
34+
component. By default, compartments have safeguards to prevent this from happening
35+
accidentally but directly addressing the compartment in the global state has no
36+
such safeguards.
37+
38+
### What is targeting
39+
40+
As discussed in the model building section there is a concept of wiring together
41+
different compartments of different components. These wires are created through
42+
the concept of targeting. In esscence targeting is just updating the path stored
43+
in a compartment with the path of a different compartment. This means that if
44+
the targeted compartment goes to retireve the value stored in it, it will
45+
actually retrieve the value for the a different compartment. When a compartment
46+
is in this state where it is targeted at another compartment it is set to read
47+
only meaning that it can not modify a different compartment.
48+
49+

docs/compiling.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Compiling
2+
The term compiling for ngcsimlib refers to automatic step that happens inside a context that produces a transformed method for all of its components. This step is the most complecated part of the library and generally does not need to be touched or interacted with. Nevertheless this section will cover all the steps that it does at a high level. There is an expectation that the reader has an understanding of python abstract syntax trees, globals, and how to dynamically compile python code to run.
3+
4+
## The decorator
5+
In ngcsimlib there is a decorator marked as `@compilable` that is used to add a flag to methods that the user wants to compile. On its own it doesn't do anything but this lets the parser distinguish between method that should be compiled and methods that should be ignored.
6+
7+
## Step by Step
8+
The process started by telling the parser to compile a specific object.
9+
10+
### Step 1: Compile Children
11+
The first step to compile any object is to make sure that all the compilable objects of the top level object are compiled. So it loops through all the whole object and compiles each part that it finds that is flagged as compilable and is an instance of a class.
12+
13+
### Step 2: Extract Methods to Compile
14+
While the parser is looping through all the parts of the top level object it is also extracting the methods on the object that are flagged as compilable with the decorator. It stores them for later but this lets the parser only loop over the object once.
15+
16+
### Step 3: Parse Each Method
17+
As each method is its own entry point into the compiler the step will run for each method in the top level object
18+
19+
### Step 3a: Set up Transformer
20+
The step sets up a ContextTransformer which is a ask.NodeTransformer and will convert the method from a class method with the use of `self` and other methods that need to be removed into their more context friendly counterpart.
21+
22+
### Step 3b: Transform the function
23+
There are quite a few pieces of common python that need to be transformed. This step happens with the overall goal of replacing all object focused parts with a more global view. This means compartment's `.get` and `.set` calls are replaced with direct setting and getting from the global state based on the compartment's target. It also means that all temporally constant values such as `batch_size` are moved into the globals space for that specific file and replaced with the naming convention of `object_path_constant`. One more step that this does is ensure that there is no branching in the code. Specifically if there is a brach such as an if statement it will evaluate it and only keep the branch it will traverse down. This means that there can not be any branch logic based on inputs or computed values (this is a common restriction for just in time compiling).
24+
25+
### Step 3c: Parse Sub-Methods
26+
As it is possible to have other class methods that are not marked as entry points for compilation but need to be compiled as step 3b happens it tracks all the sub-methods needed, this step goes through and repeats steps 3a and 3b for each of them with a naming convention similar to the temporally constant values for each method.
27+
28+
### Step 3d: Compile the AST
29+
Once we have all the needed namespace and globals needed to execute the method properly transformed the method is compiled with python and executed.
30+
31+
### Step 3e: Binding
32+
The final step per method is to bind it to the original method, this replaces the method with an object which when called with act like the normal uncompiled version but has the addition of the `.compiled` attribute which contains all the compiled information to be used later. This allows for the end user to call `myComponent.myMethod.compiled()` and have it run. The exact type for `compiled` value can be found in `ngcsimlib._src.parser.utils:CompiledMethod`.
33+
34+
### Step 4: Finishing Up
35+
Some objects such as the processes have more steps to modify themselves or their compiled methods to align themselves with needed functionality but that is found in each class's expanded `compile` method and should be referred to by looking at those methods specifically.

docs/components.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Components
2+
3+
Living one step above compartments in a model hierachy rests the component. The
4+
components hold a collection of both temporally constant values and dynamic
5+
compartments, in addition they are the lowest place where logic governing the
6+
dynamics of a system are defined. Generally components are going to be the
7+
building blocks that are reused multiple times throughout a model to create a
8+
final dynmical system.
9+
10+
## Temporally Constant vs Dynamic Compartments
11+
12+
One important distinction that needs to be highlighted inside a component is the
13+
difference between a temporally constant value and a dynamic compartment.
14+
Starting with compartments these values that change over time, generally they
15+
will have the type `ngcsimlib.Compartment` and be used to track internal values
16+
of the component. These values can be ones such inputs, decaying values,
17+
counters, etc. The second catagory of values are temporally constant, these are
18+
values that will remain fixed on the model is constructed. These values tend to
19+
include ones such as matrix shapes and coefficients.
20+
21+
## Defining Compilable Methods
22+
23+
Inside a component it is expected that there are methods used to govern the
24+
temporal dynamics of a system. These compilable methods are decorated
25+
with `@compilable` and are defined like any regular method. Inside a compilable
26+
method there will be access to `self`, meaning that to reference a compartment's
27+
value it is `self.myCompartment.get()`. The only requirement is that any method
28+
decorated can not have a return value, values should be stored inside their
29+
respective compartments. In an external step from defining the component a
30+
transformer will change all of these methods into ones that function with the
31+
rest of ngcsimlib.

docs/context.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Contexts
2+
3+
Contexts in ngcsimlib are the top level container that holds everything used to
4+
define the model. On their own they have no runtime logic, they rely on their
5+
internal processes and component to build a working model.
6+
7+
## Defining a Context
8+
9+
To define a context ngcsimlib leverages the `with` block meaning that to create
10+
a new context simply start with `with Context("myContext") as ctx:` and a new
11+
context will be created. (Important note: names are unique, if a context is
12+
created with the same name they will be the same context and thus there might be
13+
conflicts). This defined context does not do anything on its own.
14+
15+
## Adding Components
16+
17+
To add components to the context simply initialize components while inside
18+
the `with` block of the context. Any component defined while inside the block
19+
will automatically be added and tacked by the context object.
20+
21+
## Wiring Components
22+
23+
Inside a model components will need to pass data to one another that setup is
24+
done here inside the context. To connect the compartments of two components
25+
follow the pattern `mySource.output >> myDestination.input` where output and
26+
input are compartments inside their respective components. This will ensure that
27+
when processes are being run the value will flow properly from component to
28+
component.
29+
30+
### Operators
31+
32+
There is a special type of wire called an operator that performs a simple
33+
operation on the compartment values as the data flows from one component to
34+
another. Generally these are use for simple math operations such as
35+
negation `Negate(mySource.output) >> myDestination.input` or the summation of
36+
multiple compartments into
37+
one `Summation(mySource1.output, mySource2.output, ...) >> myDestination.input`.
38+
These can be chained, so it would be possible to negate one or more of the inputs
39+
to the summation.
40+
41+
## Adding Processes
42+
43+
To add processes to the context simple initialize the process and add all of its
44+
steps while inside the `with` block of the process.
45+
46+
## Exiting the `with` block
47+
48+
When the context exits the `with` block it will recompile the entire model. This
49+
means that all parts of the model that needed to be compiled before views (such
50+
as the compiled methods of a process) needs to happen after the `with` block is
51+
exited. Behind the scenes this is calling `recompile` on the context, it is
52+
possible to manually trigger the recompile step but that can break certain
53+
connections so use this functionality sparingly.
54+
55+
## Saving and Loading
56+
57+
The context's one unique job is the handling of the saving and loading of models
58+
to disk. By default, calling `save_to_json(...)` will create the correct file
59+
structure and files needed and load the context in the future. To load a model
60+
calling `Context.load(...)` will load the context in from a directory, something
61+
important to note while loading in a context is that it is effectively
62+
recreating the components with their initial values using their arguments and
63+
keywords arguments (excluding those that can't be serialized). This means that
64+
if you have a trained model ensure that your components have a save method
65+
defined that will handle the saving and loading of all values in their
66+
compartments.

docs/global_state.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# The Global State
2+
3+
As ngcsimlib is a simulation library focused on temporal models; or models that
4+
change over time there, all models have some concept of a state. These states
5+
might be comprised of a single value that changes or of a complex set of values
6+
that when combined all together make up the dynamical system for the model. In
7+
both cases these sets of values are stored in what is known as the global state.
8+
9+
## Interacting with the global state
10+
11+
Since the global state will contain a large amount of information about a given
12+
model there is a need to be able to interact with and modify the values in the
13+
global state. In most use cases this is not done directly. The most common way
14+
to interact with the global state is through the use of the state manager. The
15+
state manager exists to provide a set of helper methods for interacting with the
16+
global state. While the manager is there to assist you it is not going to stop
17+
you from changing the state, or breaking the state. When changing the state
18+
beyond setting it as a result of processes be careful not to add or remove
19+
anything that is needed for your model.
20+
21+
### Adding new fields to the global state
22+
23+
If you are new to using ngcsimlib and looking for a way to add values to the
24+
global state, stop for a moment. Unless you know exactly what you are doing do
25+
not manually add values to the global state, instead see compartments, or
26+
components as those are the most common ways for adding fields to the global
27+
state.
28+
29+
If you are actually trying to manually add values to the global state it is done
30+
through the use of the `add_key` method. This will create the appropriate key in
31+
the global state for at the given path and name, and it will be retrieved
32+
with `from_key` calls. This value however is not linked to a compartment and
33+
thus will be hard to get working properly in the compiled methods without some
34+
specific references.
35+
36+
### Getting the current state
37+
38+
To get the current state simply call `global_state_manager.state` this will give
39+
you a copy of the current state, meaning that any modifications made to it are
40+
not reflected in the global state.
41+
42+
### Updating the global state
43+
44+
To manually update the global state after modifying a local
45+
copy `global_state_manager.state = new_state` can be uased. This will update the
46+
state with the `.update` call for dictionaries meaning that a partial state will
47+
still update correctly.
48+

docs/processes.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Processes
2+
3+
Processes in ngcsimlib are a way of defining a specific transition across a
4+
given model. They take in as many compilable method's across any number of
5+
components and produce a single top level method and a varying number of
6+
submethods needed to execute the entire chain of compilable methods in one step.
7+
This is done to interface nicely with just in time compilers and minimize the
8+
amount of read and write calls done across the chain of methods.
9+
10+
## Building the chain
11+
12+
Building the chain that a process will use is done in an iterative process. Once
13+
the process object is created steps are added using either `.then()` or `>>`.
14+
As an example
15+
16+
```
17+
myProcess.then(myCompA.forward).then(myCompB.forward).then(myCompA.evolve).then(myCompB.evolve)
18+
```
19+
20+
or
21+
22+
```
23+
myProcess >> myCompA.forward >> myCompB.forward >> myCompA.evolve >> myCompB.evolve
24+
```
25+
26+
In both cases this process will chain the four methods together into a single
27+
step, only updating the final state all steps are complete.
28+
29+
## Types of processes
30+
31+
There are two types of processes, the above example would be with what is
32+
referred to as a `MethodProcess`, these are used to chain together any
33+
compilable
34+
methods from any number of different components. The second type of process,
35+
called a `JointProcess`, is used to chain together entire processes.
36+
JointProcesses are especially useful if there are multiple method processes that
37+
need to be called but different orders of the processes are needed at different
38+
times.
39+
40+
## Extras
41+
42+
There are a few extra methods that come standard with each process type that can
43+
be useful for both regular operation and debugging.
44+
45+
### Viewing the compiled method
46+
47+
Behind the scenes a process is transforming and compiling down all the steps
48+
used to build it, these means that the exact code it is running to do its
49+
calculation is not what the user wrote. To allow for the end user to view and
50+
make sure that the two pieces of code; theirs and the compiled version, are
51+
equivalent every process has a `view_compiled_method()` call that can be used
52+
after the model is compiled. This call will return the code that it is running
53+
as a string. There will be some stark differences between the produced code and
54+
the code in the components used to build the steps. Please refer to the
55+
compiling page for a more indepth guide to comparing the outputs.
56+
57+
### Needed Keywords
58+
59+
As some methods will require external values such as `t` or `dt` for a given
60+
execution a process will also track all the keyword arguments that are needed to
61+
run their compiled process. To view which keywords a given process is expected
62+
calling `get_keywords()`. This is mostly used for debugging or a sanity check.
63+
64+
### Packing keywords
65+
66+
To add onto the needed keywords the process also provides an interface to
67+
produce the keywords needed to run in the form of two methods. The first method
68+
is `pack_keywords(...)`, this method packs together a single row of values
69+
needed to run a single execution of the process. The arguments are
70+
the `row_seed` which is a seed to be passed to all the keyword generators (only
71+
needed if generators are being used). The second set of arguments are keyword
72+
arguments that are either constant `dt=0.1` or
73+
generators `lambda row_seed: 0.1 * row_seed`. The second method for generating
74+
the keywords for a process is with `pack_rows(...)`. This method created many
75+
sets of keywords needed to run multiple iterations of the process. The arguments
76+
are slightly different, first it now has a `length` argument to indicate the
77+
number of rows being produced, second it has a `seed_generator` use to generate
78+
the seed of each row (for example to have only even
79+
seeds `seed_generator = lambda x: 2 * x`) if the generator
80+
is `None` `seed_generator = lamda x: x` is used. After that the same keyword
81+
arguments to define the needed parameters is used as in `pack_keywords`
82+

0 commit comments

Comments
 (0)