Skip to content

Commit

Permalink
major cleanup of looper; docs about the logic were quite inaccurate; …
Browse files Browse the repository at this point in the history
…removed redundant Main funcs, use one type of funcs only (wrap to get rid of bool when not needed); example; better, more accurate doc string;
  • Loading branch information
rcoreilly committed Nov 5, 2024
1 parent fe19c11 commit c568961
Show file tree
Hide file tree
Showing 13 changed files with 737 additions and 630 deletions.
6 changes: 4 additions & 2 deletions etime/modes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package etime

//go:generate core generate

//gosl:start etime
// todo: this only has Train, Test in final V2 case

//gosl:start

// Modes are evaluation modes (Training, Testing, etc)
type Modes int32 //enums:enum
Expand Down Expand Up @@ -34,7 +36,7 @@ const (
Debug
)

//gosl:end etime
//gosl:end

// ModeFromString returns Mode int value from string name
func ModeFromString(str string) Modes {
Expand Down
106 changes: 91 additions & 15 deletions looper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,108 @@

Docs: [GoDoc](https://pkg.go.dev/github.com/emer/emergent/looper)

Looper implements a fully generic looping control system with extensible functionality at each level of the loop, with logic that supports reentrant stepping so each time it is Run it advances at any specified step size, with results that are identical to running.
Looper implements a fully generic looping control system, with a `Stack` of `Loop` elements that iterate over different time scales of processing, where the processing performed is provided by function closures on the Loop elements.

Each loop implements the following logic:
Critically, the loop logic supports _reentrant stepping_, such that you can iteratively `Step` through the loop processing and accomplish exactly the same outcomes as if you did a complete `Run` from the start.

Each loop implements the following logic, where it is key to understand that the time scale associated with the loop _runs the full iterations over that time scale_. For example, a `Trial` loop _iterates over Trials_ -- it is _not_ a single trial, but the whole sequence (loop) of trials.

```go
OnStart() // run at start of loop
do {
Main() // run in loop
<run subloops>
} while !IsDone() // test for completion
OnEnd() // run after loop is done
for {
Events[Counter == AtCounter] // run events at counter
OnStart()
Run Sub-Loop to completion
OnEnd()
Counter += Inc
if Counter >= Max || IsDone() {
break
}
}
```

The `Loop` object has the above function lists (`OnStart`, `OnEnd`, and `IsDone`), where function closures can be added to perform any relevant functionality. `Events` have the trigger `AtCounter` and a list of functions to call. If the Loop is the last one in the Stack, then,

Each level of loop holds a corresponding `Counter` value, which increments at each iteration, and its `Max` value determines when the loop iteration terminates.

Each `Stack` of loops has an associated `Mode` enum, e.g., `Train` or `Test`, and each `Loop` has an associated `Time` level, e.g., `Run`, `Epoch`, `Trial`.

The collection of `Stacks` has a high-level API for configuring and controlling the set of `Stack` elements, and has the logic for running everything, in the form of `Run`, `Step`, `Stop` methods, etc.

# Examples

The following examples use the [etime](../etime) `Modes` and `Times` enums. It is recommended that you define your own `Modes` enums if not using the basic `Train` and `Test` cases, to provide a better [egui](../egui) representation of the loop stack.

## Configuration

From `step_test.go` `ExampleStacks`:

```Go
stacks := NewStacks()
stacks.AddStack(etime.Train).
AddTime(etime.Epoch, 3).
AddTime(etime.Trial, 2)

// add function closures:
stacks.Loop(etime.Train, etime.Epoch).OnStart.Add("Epoch Start", func() { fmt.Println("Epoch Start") })
stacks.Loop(etime.Train, etime.Epoch).OnEnd.Add("Epoch End", func() { fmt.Println("Epoch End") })
stacks.Loop(etime.Train, etime.Trial).OnStart.Add("Trial Run", func() { fmt.Println(" Trial Run") })

// add events:
stacks.Loop(etime.Train, etime.Epoch).AddEvent("EpochTwoEvent", 2, func() { fmt.Println("Epoch==2") })
stacks.Loop(etime.Train, etime.Trial).AddEvent("TrialOneEvent", 1, func() { fmt.Println(" Trial==1") })
```

The `Loop` object has the above function lists where function closures can be added to perform any relevant functionality.
The `DocString` for this stack is:

Each level of loop holds a corresponding counter value, and the looper automatically increments and uses these counters to stop looping at a given level. Each `Stack` of loops is associated with a given `etime.Mode`, e.g., `etime.Train` or `etime.Test`.
```
Stack Train:
Epoch[0 : 3]:
Events:
EpochTwoEvent: [at 2] Events: EpochTwoEvent
Start: Epoch Start
Trial[0 : 2]:
Events:
TrialOneEvent: [at 1] Events: TrialOneEvent
Start: Trial Run
End: Epoch End
```

# Algorithm and Sim Integration
and the output when run is:

Specific algorithms use `AddLevels` to add inner, lower levels of loops to implement specific algorithm code (typically `Phase` and `Cycle`). Leabra and Axon use the `Time` struct as a context for holding the relevant counters and mode, which is then accessed directly in the callback functions as needed.
```
Epoch Start
Trial Run
Trial==1
Trial Run
Epoch End
Epoch Start
Trial Run
Trial==1
Trial Run
Epoch End
Epoch==2
Epoch Start
Trial Run
Trial==1
Trial Run
Epoch End
```

In cases where something must be done prior to looping through cycles (e.g., `ApplyInputs` and new phase startup methods), trigger it on the first cycle, before calling other functions, using a provided `AddCycle0` function.
## Running, Stepping

# Concrete Example of Looping Logic
Run a full stack:
```Go
stacks.Run(etime.Train)
```

The `stack_test.go` can generate a trace the looping -- edit the if false to true to see.
Reset first and Run, ensures that the full sequence is run even if it might have been stopped or stepped previously:
```Go
stacks.ResetAndRun(etime.Train)
```

Step by 1 Trial:
```Go
stacks.Step(etime.Train, 1, etime.Trial)
```


23 changes: 16 additions & 7 deletions looper/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,39 @@

package looper

// Counter combines an integer with a maximum value. It supports time tracking within looper.
// Counter combines an integer with a maximum value.
// It supports time tracking within looper.
type Counter struct {

// current counter value
// Cur is the current counter value.
Cur int

// maximum counter value -- only used if > 0
// Max is the maximum counter value.
// Only used if > 0 ([Loop] requires an IsDone condition to stop).
Max int

// increment per iteration
// Inc is the increment per iteration.
Inc int
}

// Incr increments the counter by 1. Does not interact with Max.
// SetMaxIncr sets the given Max and Inc value for the counter.
func (ct *Counter) SetMaxInc(mx, inc int) {
ct.Max = mx
ct.Inc = inc
}

// Incr increments the counter by Inc. Does not interact with Max.
func (ct *Counter) Incr() {
ct.Cur += ct.Inc
}

// SkipToMax sets the counter to its Max value -- for skipping over rest of loop
// SkipToMax sets the counter to its Max value,
// for skipping over rest of loop iterations.
func (ct *Counter) SkipToMax() {
ct.Cur = ct.Max
}

// IsOverMax returns true if counter is at or over Max (only if Max > 0)
// IsOverMax returns true if counter is at or over Max (only if Max > 0).
func (ct *Counter) IsOverMax() bool {
return ct.Max > 0 && ct.Cur >= ct.Max
}
Expand Down
10 changes: 5 additions & 5 deletions looper/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ import (
// in the loop, when the counter is AtCounter value.
type Event struct {

// Might be 'plus' or 'minus' for example.
// Name of this event.
Name string

// The counter value upon which this Event occurs.
// AtCounter is the counter value upon which this Event occurs.
AtCounter int

// Callback function for the Event.
// OnEvent are the functions to run when Counter == AtCounter.
OnEvent NamedFuncs
}

// String describes the Event in human readable text.
func (event *Event) String() string {
s := event.Name + ": "
s = s + "(at " + strconv.Itoa(event.AtCounter) + ") "
s = s + "[at " + strconv.Itoa(event.AtCounter) + "] "
if len(event.OnEvent) > 0 {
s = s + "\tEvents: " + event.OnEvent.String()
s = s + "Events: " + event.OnEvent.String()
}
return s
}
Expand Down
Loading

0 comments on commit c568961

Please sign in to comment.