Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"sessionId":"0e42f8a2-416e-4d3f-9ea6-c2764947b5a3","pid":14267,"acquiredAt":1774543530483}
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
push:
branches: [master, feature/*]
pull_request:
branches: [master]

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt -- --check

test:
name: Test (${{ matrix.os }}, ${{ matrix.toolchain }})
needs: fmt
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable, beta]

steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.toolchain }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Build
run: cargo build --release --verbose

- name: Run tests
run: cargo test --release --verbose
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ Cargo.lock

# Fmt
**/*.rs.bk

# Local files
CURRENT_FEATURE.md
best_config.json
champion.json
champion_backup.json
experiment_results.json
plan.md
CLAUDE.md
AGENT.md
30 changes: 0 additions & 30 deletions .travis.yml

This file was deleted.

15 changes: 9 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "MIT"
name = "rustneat"
repository = "https://github.com/TLmaK0/rustneat"
version = "0.3.0"
edition = "2018"

[dependencies]
conv = "0.3.2"
Expand All @@ -20,18 +21,16 @@ rulinalg = "0.3.4"
rusty_dashed = { version = "0.2.2", optional = true }
open = { version = "1.2.1", optional = true }
clippy = { version = "0.0.103", optional = true }
serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }

cpython = { version = "0.6", optional = true }
python3-sys = { version = "0.6", optional = true }
pyo3 = { version = "0.22", features = ["auto-initialize"], optional = true }
ctrlc = { version = "3.1.4", optional = true }

[features]
default = []
telemetry = ["rusty_dashed", "open", "serde", "serde_derive", "serde_json"]
openai = ["cpython", "python3-sys", "ctrlc"]
telemetry = ["rusty_dashed", "open", "serde", "serde_json"]
openai = ["pyo3", "ctrlc", "serde", "serde_json"]
ctrnn_telemetry = []

[[example]]
Expand All @@ -50,3 +49,7 @@ name = "function_approximation"

[[example]]
name = "ctrnn"

[[example]]
name = "test_champion"
required-features = ["openai"]
117 changes: 61 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,87 @@
Working-in-progress

### Rust NEAT
[![travis-ci](https://img.shields.io/travis/TLmaK0/rustneat/master.svg)](https://travis-ci.org/TLmaK0/rustneat)
[![CI](https://github.com/TLmaK0/rustneat/actions/workflows/ci.yml/badge.svg)](https://github.com/TLmaK0/rustneat/actions/workflows/ci.yml)
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/rustneat/rustneat)

Implementation of **NeuroEvolution of Augmenting Topologies NEAT** http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf

This implementations uses a **CTRNN** based on**On the Dynamics of Small Continuous-Time Recurrent Neural Network** (Beer, 1995) http://www.cs.uvm.edu/~jbongard/2014_CS206/Beer_CTRNNs.pdf
This implementation uses a **CTRNN** (Continuous-Time Recurrent Neural Network) based on **On the Dynamics of Small Continuous-Time Recurrent Neural Network** (Beer, 1995) http://www.cs.uvm.edu/~jbongard/2014_CS206/Beer_CTRNNs.pdf

### CTRNN time constant (τ)

The time constant τ controls neuron response speed — like biological membrane resistance time. What matters is the ratio `dt/τ` where `dt=0.01` is the simulation step. Configurable via `MutationConfig::tau`:

- **Small τ** (e.g. 0.01): `dt/τ = 1.0` — neurons react instantly, state resets each step. Network behaves as **feedforward**. Use for stateless problems like XOR.
- **Large τ** (e.g. 0.1): `dt/τ = 0.1` — neurons update only 10% per step, retaining 90% of previous state. Network has **temporal memory**. Use for control tasks like Lunar Lander where the agent integrates information over time.
- **Very large τ** (e.g. 1.0): `dt/τ = 0.01` — neurons barely respond, very strong inertia. Needs many steps to react to new inputs.

## Telemetry Dashboard

![telemetry](docs/img/rustneat.png)

## Install Rust
```bash
cargo run --release --example simple_sample --features=telemetry
```

`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
`source $HOME/.cargo/env`
Then go to `http://localhost:3000` to see how the neural network evolves.

## Run test
![telemetry](docs/results/cart_pole_dashboard.gif)

To speed up tests, run them with `--release` (XOR classification/simple_sample should take less than a minute)
## Cart Pole

`cargo test --release`
## Run example
![cart pole](docs/results/cart_pole.gif)

`cargo run --release --example simple_sample --features=telemetry`
## Lunar Lander

then go to `http://localhost:3000` to see how neural network evolves
NEAT+CTRNN agent evolved to land on the OpenAI Gym LunarLander-v3 environment using discrete actions with independent threshold-based output control.

![telemetry](docs/results/cart_pole_dashboard.gif)
![lunar lander](docs/results/lunar_lander.gif)

## Run openai tests
### Results
- **Verified average reward**: +70 (fitness 570 with +500 offset)
- **Peak reward**: +278 (fitness 778)
- **Solved threshold**: average reward +200

![telemetry](docs/results/cart_pole.gif)
The agent uses 2 independent CTRNN outputs (main thruster, lateral direction) with lateral priority — a key insight for CTRNN control problems where argmax causes state lock-in.

Install python dependencies
### Run

```bash
sudo apt install python3
sudo apt install python3-pip
sudo apt install libpython3.8-dev
sudo apt-get remove swig
sudo apt-get install swig3.0
sudo ln -s /usr/bin/swig3.0 /usr/bin/swig
sudo pip3 install gym
sudo pip3 install gym[box2d]
sudo apt install python3-opengl
cargo build --release --example openai_lunar_lander --features openai
./target/release/examples/openai_lunar_lander
```

Run test
### Test champion

```bash
cargo build --release --example test_champion --features openai
./target/release/examples/test_champion
```
cargo run --release --example openai --features=openai,telemetry
```

### Windows Openai

https://github.com/openai/gym/issues/11#issuecomment-242950165
## Install Rust

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
Update to the latest version of Windows (>version 1607, "Anniversary Update")
Enable Windows Subsystem for Linux (WSL)
Open cmd, run bash
Install python & gym (using sudo, and NOT PIP to install gym). So by now you should probably be able to run things and get really nasty graphics related errors. This is because WSL doesn't support any displays, so we need to fake it.
Install vcXsrv, and run it (you should just have a little tray icon)
In bash run "export DISPLAY=:0" Now when you run it you should get a display to pop-up, there may be issues related to graphics drivers. Sadly, this is where the instructions diverge if you don't have an NVIDIA graphics card.
Get the drivers: "sudo apt-get install nvidia-319 nvidia-settings-319 nvidia-prime"
Run!

## Run tests

To speed up tests, run them with `--release` (XOR classification/simple_sample should take less than a minute):

```bash
cargo test --release
```

## Sample usage

Create a new cargo project:
Create a new cargo project and add rustneat to Cargo.toml:

Add rustneat to Cargo.toml
```
```toml
[dependencies]
rustneat = "0.2.1"
```

Then use the library i.e. to implement the above example, use the library as follows:
Then use the library to implement XOR classification:

```rust
extern crate rustneat;
Expand Down Expand Up @@ -117,23 +122,23 @@ fn main() {
}
println!("{:?}", champion.unwrap().genome);
}

```

# Develop
Check style guidelines with:
## Develop

`rustup component add rustfmt-preview`
`cargo fmt`
Check style guidelines with:

# Thanks
Thanks for the icon nerves by Delwar Hossain from the Noun Project
```bash
rustup component add rustfmt-preview
cargo fmt
```

# References:
**NeuroEvolution of Augmenting Topologies NEAT** http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf
## References

**On the Dynamics of Small Continuous-Time Recurrent Neural Network** (Beer, 1995) http://www.cs.uvm.edu/~jbongard/2014_CS206/Beer_CTRNNs.pdf
- **NeuroEvolution of Augmenting Topologies NEAT** http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf
- **On the Dynamics of Small Continuous-Time Recurrent Neural Network** (Beer, 1995) http://www.cs.uvm.edu/~jbongard/2014_CS206/Beer_CTRNNs.pdf
- **An Investigation into the Dynamics of a Continuous Time Recurrent Neural Network Node** http://www.tinyblueplanet.com/easy/FCSReport.pdf

**An Investigation into the Dynamics of a Continuous Time Recurrent Neural Network Node** http://www.tinyblueplanet.com/easy/FCSReport.pdf
## Thanks

**http://indy9000.github.io/posts/analysis-of-a-simple-ctrnn.html** http://indy9000.github.io/posts/analysis-of-a-simple-ctrnn.html
Thanks for the icon nerves by Delwar Hossain from the Noun Project
27 changes: 27 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::process::Command;

fn main() {
// Only run for the openai feature (which uses PyO3)
#[cfg(feature = "openai")]
{
// Get Python library path from python3-config
let output = Command::new("python3")
.args(&[
"-c",
"import sysconfig; print(sysconfig.get_config_var('LIBDIR'))",
])
.output()
.expect("Failed to get Python library path");

let libdir = String::from_utf8(output.stdout)
.expect("Invalid UTF-8 from Python")
.trim()
.to_string();

// Tell the linker to add this path to rpath so the binary can find libpython at runtime
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", libdir);

// Also add standard Python lib search path
println!("cargo:rustc-link-search=native={}", libdir);
}
}
Binary file added docs/results/lunar_lander.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 15 additions & 4 deletions examples/function_approximation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ use rustneat::Population;
static mut BEST_FITNESS: f64 = 0.0;
struct FunctionApproximation;

impl FunctionApproximation {
fn funct(&self, x: i64) -> i64 {
x.pow(3) - (3 * x) + 2
}
}

impl Environment for FunctionApproximation {
fn test(&self, organism: &mut Organism) -> f64 {
let mut output = vec![0f64];
Expand All @@ -24,7 +30,7 @@ impl Environment for FunctionApproximation {

for x in -10..11 {
organism.activate(vec![x as f64 / 10f64], &mut output);
distance += ((x as f64).powf(2f64) - (output[0] * 100f64)).abs();
distance += (self.funct(x) as f64 - (output[0] * 100f64)).abs();
outputs.push([x, (output[0] * 100f64) as i64]);
}

Expand All @@ -42,7 +48,7 @@ impl Environment for FunctionApproximation {

fn main() {
let mut population = Population::create_population(150);
let mut environment = FunctionApproximation;
let environment = FunctionApproximation;
let mut champion: Option<Organism> = None;

#[cfg(feature = "telemetry")]
Expand All @@ -55,15 +61,20 @@ fn main() {
telemetry!(
"approximation1",
1.0,
format!("{:?}", (-10..11).map(|x| [x, x * x]).collect::<Vec<_>>())
format!(
"{:?}",
(-10..11)
.map(|x| [x, environment.funct(x)])
.collect::<Vec<_>>()
)
);

#[cfg(feature = "telemetry")]
std::thread::sleep(std::time::Duration::from_millis(2000));

while champion.is_none() {
population.evolve();
population.evaluate_in(&mut environment);
population.evaluate_in(&environment);
for organism in &population.get_organisms() {
if organism.fitness >= 96f64 {
champion = Some(organism.clone());
Expand Down
Loading
Loading