Skip to content

Commit

Permalink
Merge pull request #65 from yaricom/parallel-experiment-simulation
Browse files Browse the repository at this point in the history
Parallel experiments evaluation
  • Loading branch information
yaricom authored Sep 2, 2024
2 parents ad5c5c2 + 373dd96 commit 77ac105
Show file tree
Hide file tree
Showing 13 changed files with 800 additions and 452 deletions.
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ run-cartpole-two-markov:
-trials $(TRIALS_NUMBER) \
-log_level $(LOG_LEVEL)


# The target to run double-pole Markov experiment in parallel objective
# function evaluation mode
#
run-cartpole-two-parallel-markov:
$(GORUN) executor.go -out $(OUT_DIR)/pole2_markov_parallel \
-context $(DATA_DIR)/pole2_markov.neat \
-genome $(DATA_DIR)/pole2_markov_startgenes \
-experiment cart_2pole_markov_parallel \
-trials $(TRIALS_NUMBER) \
-log_level $(LOG_LEVEL)

# The target to run single-pole experiment
#
run-cartpole:
Expand All @@ -53,6 +65,17 @@ run-cartpole:
-trials $(TRIALS_NUMBER) \
-log_level $(LOG_LEVEL)

# The target to run single-pole experiment in parallel objective
# function evaluation mode
#
run-cartpole-parallel:
$(GORUN) executor.go -out $(OUT_DIR)/pole1_parallel \
-context $(DATA_DIR)/pole1_150.neat \
-genome $(DATA_DIR)/pole1startgenes \
-experiment cart_pole_parallel \
-trials 100 \
-log_level $(LOG_LEVEL)

# The target to run disconnected XOR experiment
#
run-xor-disconnected:
Expand Down
146 changes: 2 additions & 144 deletions examples/pole/cartpole.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@ import (
"github.com/yaricom/goNEAT/v4/experiment/utils"
"github.com/yaricom/goNEAT/v4/neat"
"github.com/yaricom/goNEAT/v4/neat/genetics"
"github.com/yaricom/goNEAT/v4/neat/network"
"math"
"math/rand"
)

const twelveDegrees = 12.0 * math.Pi / 180.0

type cartPoleGenerationEvaluator struct {
// The output path to store execution results
OutputPath string

// The flag to indicate if cart emulator should be started from random position
RandomStart bool
// The number of emulation steps to be done balancing pole to win
Expand All @@ -41,7 +37,7 @@ func (e *cartPoleGenerationEvaluator) GenerationEvaluate(ctx context.Context, po
}
// Evaluate each organism on a test
for _, org := range pop.Organisms {
res, err := e.orgEvaluate(org)
res, err := OrganismEvaluate(org, e.WinBalancingSteps, e.RandomStart)
if err != nil {
return err
}
Expand Down Expand Up @@ -98,141 +94,3 @@ func (e *cartPoleGenerationEvaluator) GenerationEvaluate(ctx context.Context, po

return nil
}

// orgEvaluate evaluates provided organism for cart pole balancing task
func (e *cartPoleGenerationEvaluator) orgEvaluate(organism *genetics.Organism) (bool, error) {
phenotype, err := organism.Phenotype()
if err != nil {
return false, err
}

// Try to balance a pole now
if fitness, err := e.runCart(phenotype); err != nil {
return false, nil
} else {
organism.Fitness = float64(fitness)
}

if neat.LogLevel == neat.LogLevelDebug {
neat.DebugLog(fmt.Sprintf("Organism #%3d\tfitness: %f", organism.Genotype.Id, organism.Fitness))
}

// Decide if it's a winner
if organism.Fitness >= float64(e.WinBalancingSteps) {
organism.IsWinner = true
}

// adjust fitness to be in range [0;1]
if organism.IsWinner {
organism.Fitness = 1.0
organism.Error = 0.0
} else if organism.Fitness == 0 {
organism.Error = 1.0
} else {
// we use logarithmic scale because most cart runs fail to early within ~100 steps, but
// we test against 500'000 balancing steps
logSteps := math.Log(float64(e.WinBalancingSteps))
organism.Error = (logSteps - math.Log(organism.Fitness)) / logSteps
organism.Fitness = 1.0 - organism.Error
}

return organism.IsWinner, nil
}

// runCart runs the cart emulation and return number of emulation steps pole was balanced
func (e *cartPoleGenerationEvaluator) runCart(net *network.Network) (steps int, err error) {
var x float64 /* cart position, meters */
var xDot float64 /* cart velocity */
var theta float64 /* pole angle, radians */
var thetaDot float64 /* pole angular velocity */
if e.RandomStart {
/*set up random start state*/
x = float64(rand.Int31()%4800)/1000.0 - 2.4
xDot = float64(rand.Int31()%2000)/1000.0 - 1
theta = float64(rand.Int31()%400)/1000.0 - .2
thetaDot = float64(rand.Int31()%3000)/1000.0 - 1.5
}

netDepth, err := net.MaxActivationDepthWithCap(0) // The max depth of the network to be activated
if err != nil {
neat.WarnLog(fmt.Sprintf(
"Failed to estimate maximal depth of the network with loop.\nUsing default depth: %d", netDepth))
} else if netDepth == 0 {
// possibly disconnected - return minimal fitness score
return 1, nil
}

in := make([]float64, 5)
for steps = 0; steps < e.WinBalancingSteps; steps++ {
/*-- set up the input layer based on the four inputs --*/
in[0] = 1.0 // Bias
in[1] = (x + 2.4) / 4.8
in[2] = (xDot + .75) / 1.5
in[3] = (theta + twelveDegrees) / .41
in[4] = (thetaDot + 1.0) / 2.0
if err = net.LoadSensors(in); err != nil {
return 0, err
}

/*-- activate the network based on the input --*/
if res, err := net.ForwardSteps(netDepth); !res {
//If it loops, exit returning only fitness of 1 step
neat.DebugLog(fmt.Sprintf("Failed to activate Network, reason: %s", err))
return 1, nil
}
/*-- decide which way to push via which output unit is greater --*/
action := 1
if net.Outputs[0].Activation > net.Outputs[1].Activation {
action = 0
}
/*--- Apply action to the simulated cart-pole ---*/
x, xDot, theta, thetaDot = e.doAction(action, x, xDot, theta, thetaDot)

/*--- Check for failure. If so, return steps ---*/
if x < -2.4 || x > 2.4 || theta < -twelveDegrees || theta > twelveDegrees {
return steps, nil
}
}
return steps, nil
}

// doAction was taken directly from the pole simulator written by Richard Sutton and Charles Anderson.
// This simulator uses normalized, continuous inputs instead of discretizing the input space.
/*----------------------------------------------------------------------
Takes an action (0 or 1) and the current values of the
four state variables and updates their values by estimating the state
TAU seconds later.
----------------------------------------------------------------------*/
func (e *cartPoleGenerationEvaluator) doAction(action int, x, xDot, theta, thetaDot float64) (xRet, xDotRet, thetaRet, thetaDotRet float64) {
// The cart pole configuration values
const Gravity = 9.8
const MassCart = 1.0
const MassPole = 0.5
const TotalMass = MassPole + MassCart
const Length = 0.5 /* actually half the pole's length */
const PoleMassLength = MassPole * Length
const ForceMag = 10.0
const Tau = 0.02 /* seconds between state updates */
const FourThirds = 1.3333333333333

force := -ForceMag
if action > 0 {
force = ForceMag
}
cosTheta := math.Cos(theta)
sinTheta := math.Sin(theta)

temp := (force + PoleMassLength*thetaDot*thetaDot*sinTheta) / TotalMass

thetaAcc := (Gravity*sinTheta - cosTheta*temp) / (Length * (FourThirds - MassPole*cosTheta*cosTheta/TotalMass))

xAcc := temp - PoleMassLength*thetaAcc*cosTheta/TotalMass

/*** Update the four state variables, using Euler's method. ***/
xRet = x + Tau*xDot
xDotRet = xDot + Tau*xAcc
thetaRet = theta + Tau*thetaDot
thetaDotRet = thetaDot + Tau*thetaAcc

return xRet, xDotRet, thetaRet, thetaDotRet
}
144 changes: 144 additions & 0 deletions examples/pole/cartpole_parallel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package pole

import (
"context"
"fmt"
"github.com/yaricom/goNEAT/v4/experiment"
"github.com/yaricom/goNEAT/v4/experiment/utils"
"github.com/yaricom/goNEAT/v4/neat"
"github.com/yaricom/goNEAT/v4/neat/genetics"
"sync"
)

type cartPoleParallelGenerationEvaluator struct {
cartPoleGenerationEvaluator
}

type parallelEvaluationResult struct {
genomeId int
fitness float64
error float64
winner bool
err error
}

// NewCartPoleParallelGenerationEvaluator is to create generations evaluator for single-pole balancing experiment.
// This experiment performs evolution on single pole balancing task in order to produce appropriate genome.
func NewCartPoleParallelGenerationEvaluator(outDir string, randomStart bool, winBalanceSteps int) experiment.GenerationEvaluator {
return &cartPoleParallelGenerationEvaluator{
cartPoleGenerationEvaluator{
OutputPath: outDir,
RandomStart: randomStart,
WinBalancingSteps: winBalanceSteps,
},
}
}

// GenerationEvaluate evaluates one epoch for given population and prints results into output directory if any.
func (e *cartPoleParallelGenerationEvaluator) GenerationEvaluate(ctx context.Context, pop *genetics.Population, epoch *experiment.Generation) error {
options, ok := neat.FromContext(ctx)
if !ok {
return neat.ErrNEATOptionsNotFound
}

organismMapping := make(map[int]*genetics.Organism)

popSize := len(pop.Organisms)
resChan := make(chan parallelEvaluationResult, popSize)
// The wait group to wait for all GO routines
var wg sync.WaitGroup

// Evaluate each organism in generation
for _, org := range pop.Organisms {
if _, ok = organismMapping[org.Genotype.Id]; ok {
return fmt.Errorf("organism with %d already exists in mapping", org.Genotype.Id)
}
organismMapping[org.Genotype.Id] = org
wg.Add(1)

// run in separate GO thread
go func(organism *genetics.Organism, resChan chan<- parallelEvaluationResult, wg *sync.WaitGroup) {
defer wg.Done()

// create simulator and evaluate
winner, err := OrganismEvaluate(organism, e.WinBalancingSteps, e.RandomStart)
if err != nil {
resChan <- parallelEvaluationResult{err: err}
return
}

// create result
result := parallelEvaluationResult{
genomeId: organism.Genotype.Id,
fitness: organism.Fitness,
error: organism.Error,
winner: winner,
}
resChan <- result

}(org, resChan, &wg)
}

// wait for evaluation results
wg.Wait()
close(resChan)

for result := range resChan {
if result.err != nil {
return result.err
}
// find and update original organism
org, ok := organismMapping[result.genomeId]
if ok {
org.Fitness = result.fitness
org.Error = result.error
} else {
return fmt.Errorf("organism not found in mapping for id: %d", result.genomeId)
}

if result.winner && (epoch.Champion == nil || org.Fitness > epoch.Champion.Fitness) {
// This will be winner in Markov case
epoch.Solved = true
epoch.WinnerNodes = len(org.Genotype.Nodes)
epoch.WinnerGenes = org.Genotype.Extrons()
epoch.WinnerEvals = options.PopSize*epoch.Id + org.Genotype.Id
epoch.Champion = org
org.IsWinner = true
}
}

// Fill statistics about current epoch
epoch.FillPopulationStatistics(pop)

// Only print to file every print_every generation
if epoch.Solved || epoch.Id%options.PrintEvery == 0 {
if _, err := utils.WritePopulationPlain(e.OutputPath, pop, epoch); err != nil {
neat.ErrorLog(fmt.Sprintf("Failed to dump population, reason: %s\n", err))
return err
}
}

if epoch.Solved {
// print winner organism's statistics
org := epoch.Champion
utils.PrintActivationDepth(org, true)

genomeFile := "pole1_winner_genome"
// Prints the winner organism to file!
if orgPath, err := utils.WriteGenomePlain(genomeFile, e.OutputPath, org, epoch); err != nil {
neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's genome, reason: %s\n", err))
} else {
neat.InfoLog(fmt.Sprintf("Generation #%d winner's genome dumped to: %s\n", epoch.Id, orgPath))
}

// Prints the winner organism's phenotype to the Cytoscape JSON file!
if orgPath, err := utils.WriteGenomeCytoscapeJSON(genomeFile, e.OutputPath, org, epoch); err != nil {
neat.ErrorLog(fmt.Sprintf("Failed to dump winner organism's phenome Cytoscape JSON graph, reason: %s\n", err))
} else {
neat.InfoLog(fmt.Sprintf("Generation #%d winner's phenome Cytoscape JSON graph dumped to: %s\n",
epoch.Id, orgPath))
}
}

return nil
}
2 changes: 1 addition & 1 deletion examples/pole/cartpole_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"time"
)

// The integration test running running over multiple iterations
// The integration test running over multiple iterations
func TestCartPoleGenerationEvaluator_GenerationEvaluate(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short Unit Test mode.")
Expand Down
Loading

0 comments on commit 77ac105

Please sign in to comment.