The NEAT-Python library exports trained neural networks to a JSON format that is:
- Framework-agnostic: Can be converted to ONNX, TensorFlow, PyTorch, CoreML, etc.
- Human-readable: Easy to inspect, debug, and understand
- Version-controlled: Includes format version for future compatibility
- Complete: Faithfully represents all network types and configurations
This document specifies the JSON format structure, provides examples, and offers guidance for creating converters to other formats.
Current Version: 1.0
The format includes a format_version field to enable future evolution while maintaining backward compatibility. Third-party tools should check this version and handle accordingly.
{
"format_version": "1.0",
"network_type": "feedforward | recurrent | ctrnn | iznn",
"metadata": { ... },
"topology": { ... },
"nodes": [ ... ],
"connections": [ ... ]
}The version of this JSON format. Currently "1.0".
The type of neural network. Valid values:
"feedforward"- Feed-forward network (no cycles)"recurrent"- Recurrent network (allows cycles)"ctrnn"- Continuous-Time Recurrent Neural Network"iznn"- Izhikevich Spiking Neural Network
Contextual information about the network:
{
"created_timestamp": "2025-11-09T15:23:45Z",
"neat_python_version": "1.0.0",
"fitness": 98.5,
"generation": 42,
"genome_id": 123
}Fields:
created_timestamp(string, required): ISO 8601 timestampneat_python_version(string, optional): Version of neat-python usedfitness(number, optional): Fitness score of the genomegeneration(integer, optional): Generation number when exportedgenome_id(integer, optional): Unique genome identifier- Additional custom fields may be included
Network input/output structure:
{
"num_inputs": 3,
"num_outputs": 2,
"input_keys": [-1, -2, -3],
"output_keys": [0, 1]
}Fields:
num_inputs(integer): Number of input nodesnum_outputs(integer): Number of output nodesinput_keys(list of integers): Node IDs for inputs (typically negative)output_keys(list of integers): Node IDs for outputs (typically non-negative)
List of nodes (neurons) in the network:
{
"id": 0,
"type": "output",
"activation": {
"name": "sigmoid",
"custom": false
},
"aggregation": {
"name": "sum",
"custom": false
},
"bias": 0.5,
"response": 1.0
}Common fields:
id(integer): Unique node identifiertype(string): One of"input","hidden","output"activation(object): Activation function informationname(string): Function namecustom(boolean): True if user-defined, false if built-in
aggregation(object): Aggregation function informationname(string): Function namecustom(boolean): True if user-defined, false if built-in
bias(number): Bias valueresponse(number): Response multiplier
CTRNN-specific fields:
time_constant(number): Time constant for continuous-time dynamics
IZNN-specific fields:
a(number): Time scale of recovery variableb(number): Sensitivity of recovery variablec(number): After-spike reset value of membrane potentiald(number): After-spike reset of recovery variable
List of weighted connections between nodes:
{
"from": -1,
"to": 0,
"weight": 0.75,
"enabled": true
}Fields:
from(integer): Source node IDto(integer): Destination node IDweight(number): Connection weightenabled(boolean): Whether connection is active
All built-in activation functions are defined in neat.activations:
| Name | Formula | Notes |
|---|---|---|
sigmoid |
1 / (1 + exp(-5z)) |
Clamped to [-60, 60] |
tanh |
tanh(2.5z) |
Clamped to [-60, 60] |
sin |
sin(5z) |
Clamped to [-60, 60] |
gauss |
exp(-5z²) |
Clamped to [-3.4, 3.4] |
relu |
max(0, z) |
Rectified Linear Unit |
elu |
z if z > 0 else exp(z) - 1 |
Exponential Linear Unit |
lelu |
z if z > 0 else 0.005z |
Leaky ReLU |
selu |
λz if z > 0 else λα(exp(z) - 1) |
Scaled ELU (λ=1.0507, α=1.6732) |
softplus |
0.2 * log(1 + exp(5z)) |
Smooth approximation of ReLU |
identity |
z |
Linear/pass-through |
clamped |
clamp(z, -1, 1) |
Clamped to [-1, 1] |
inv |
1/z |
Inverse (returns 0 on overflow) |
log |
log(max(1e-7, z)) |
Natural logarithm |
exp |
exp(z) |
Exponential (clamped to [-60, 60]) |
abs |
` | z |
hat |
`max(0, 1 - | z |
square |
z² |
Quadratic |
cube |
z³ |
Cubic |
All built-in aggregation functions are defined in neat.aggregations:
| Name | Description |
|---|---|
sum |
Sum of all inputs |
product |
Product of all inputs |
max |
Maximum value |
min |
Minimum value |
maxabs |
Value with maximum absolute value |
median |
Median value |
mean |
Arithmetic mean |
When a network uses custom (user-defined) activation or aggregation functions, the custom field is set to true. Third-party converters should handle these cases appropriately:
- Error: Refuse to convert if custom functions are unsupported
- Warn: Convert with a warning that behavior may not match
- Approximate: Map to closest built-in function in target framework
{
"format_version": "1.0",
"network_type": "feedforward",
"metadata": {
"created_timestamp": "2025-11-09T15:30:00Z",
"neat_python_version": "1.0.0",
"fitness": 3.95,
"generation": 150,
"genome_id": 789
},
"topology": {
"num_inputs": 2,
"num_outputs": 1,
"input_keys": [-1, -2],
"output_keys": [0]
},
"nodes": [
{
"id": -1,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0
},
{
"id": -2,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0
},
{
"id": 0,
"type": "output",
"activation": {"name": "sigmoid", "custom": false},
"aggregation": {"name": "sum", "custom": false},
"bias": -0.123,
"response": 1.0
},
{
"id": 1,
"type": "hidden",
"activation": {"name": "relu", "custom": false},
"aggregation": {"name": "sum", "custom": false},
"bias": 0.456,
"response": 1.0
}
],
"connections": [
{"from": -1, "to": 1, "weight": 0.7, "enabled": true},
{"from": -2, "to": 1, "weight": -0.5, "enabled": true},
{"from": 1, "to": 0, "weight": 1.2, "enabled": true},
{"from": -1, "to": 0, "weight": 0.3, "enabled": true}
]
}{
"format_version": "1.0",
"network_type": "recurrent",
"metadata": {
"created_timestamp": "2025-11-09T15:31:00Z",
"neat_python_version": "1.0.0"
},
"topology": {
"num_inputs": 1,
"num_outputs": 1,
"input_keys": [-1],
"output_keys": [0]
},
"nodes": [
{
"id": -1,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0
},
{
"id": 0,
"type": "output",
"activation": {"name": "tanh", "custom": false},
"aggregation": {"name": "sum", "custom": false},
"bias": 0.0,
"response": 1.0
}
],
"connections": [
{"from": -1, "to": 0, "weight": 0.5, "enabled": true},
{"from": 0, "to": 0, "weight": 0.8, "enabled": true}
]
}Note the recurrent connection: {"from": 0, "to": 0, ...} creates a self-loop.
{
"format_version": "1.0",
"network_type": "ctrnn",
"metadata": {
"created_timestamp": "2025-11-09T15:32:00Z",
"neat_python_version": "1.0.0"
},
"topology": {
"num_inputs": 2,
"num_outputs": 1,
"input_keys": [-1, -2],
"output_keys": [0]
},
"nodes": [
{
"id": -1,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0,
"time_constant": 1.0
},
{
"id": -2,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0,
"time_constant": 1.0
},
{
"id": 0,
"type": "output",
"activation": {"name": "tanh", "custom": false},
"aggregation": {"name": "sum", "custom": false},
"bias": 0.1,
"response": 1.0,
"time_constant": 5.0
}
],
"connections": [
{"from": -1, "to": 0, "weight": 0.6, "enabled": true},
{"from": -2, "to": 0, "weight": 0.4, "enabled": true}
]
}Note the time_constant field in CTRNN nodes for continuous-time dynamics.
{
"format_version": "1.0",
"network_type": "iznn",
"metadata": {
"created_timestamp": "2025-11-09T15:33:00Z",
"neat_python_version": "1.0.0"
},
"topology": {
"num_inputs": 1,
"num_outputs": 1,
"input_keys": [-1],
"output_keys": [0]
},
"nodes": [
{
"id": -1,
"type": "input",
"activation": {"name": "identity", "custom": false},
"aggregation": {"name": "none", "custom": false},
"bias": 0.0,
"response": 1.0,
"a": 0.0,
"b": 0.0,
"c": 0.0,
"d": 0.0
},
{
"id": 0,
"type": "output",
"activation": {"name": "izhikevich", "custom": false},
"aggregation": {"name": "sum", "custom": false},
"bias": 5.0,
"response": 1.0,
"a": 0.02,
"b": 0.20,
"c": -65.0,
"d": 8.0
}
],
"connections": [
{"from": -1, "to": 0, "weight": 10.0, "enabled": true}
]
}Note the Izhikevich model parameters (a, b, c, d) in IZNN nodes. These correspond to regular spiking behavior.
- Parse JSON: Load and validate the JSON using the schema
- Map topology: Create input/output nodes in target framework
- Map nodes: Convert each node to target framework's equivalent
- Map connections: Create weighted connections
- Map functions: Convert activation/aggregation functions
- Export: Save in target format
Many activation functions have direct equivalents in popular frameworks:
| NEAT Function | ONNX | TensorFlow/Keras | PyTorch |
|---|---|---|---|
sigmoid |
Sigmoid |
sigmoid |
torch.sigmoid |
tanh |
Tanh |
tanh |
torch.tanh |
relu |
Relu |
relu |
torch.relu |
identity |
Identity |
linear |
nn.Identity |
For functions without direct equivalents (e.g., gauss, hat), you may need to:
- Compose from basic operations
- Use custom operators (framework-dependent)
- Approximate with similar functions (with warnings)
Most aggregations map straightforwardly:
| NEAT Aggregation | Operation |
|---|---|
sum |
Element-wise sum |
product |
Element-wise product |
max/min |
Reduce-max/min operations |
mean |
Average across inputs |
NEAT's response parameter scales the aggregated input before activation:
output = activation(bias + response * aggregation(inputs))
In target frameworks, this typically requires:
- Aggregate inputs
- Multiply by
response - Add
bias - Apply
activation
For recurrent networks:
- ONNX: Use LSTM/GRU operators or explicit recurrent connections
- TensorFlow: Use
tf.keras.layers.RNNor custom cell - PyTorch: Use
torch.nn.RNNor manual state tracking
Continuous-time networks require differential equation solvers. Options:
- Discretize using Euler or Runge-Kutta methods
- Use framework's ODE solver (e.g.,
torchdiffeq) - Approximate with standard RNN
Spiking neural networks require specialized frameworks:
- SpikingJelly (PyTorch-based)
- Norse (PyTorch-based)
- Brian2 (Python simulator)
- Custom implementation using the Izhikevich equations
After conversion, validate that:
- Network topology matches (same input/output dimensions)
- Activation functions behave similarly
- Forward pass produces similar results (within numerical tolerance)
- Metadata is preserved or documented
The format_version field enables format evolution:
- Minor changes (backward-compatible): Add optional fields, new network types
- Major changes (breaking): Change required fields, remove fields, change semantics
When the format changes:
- Increment
format_version - Document changes in this file
- Keep old versions supported in exporters for one major version
- Provide migration tools if breaking changes occur
Q: Can I add custom metadata fields?
A: Yes, add any additional fields to the metadata object.
Q: How do I handle custom activation functions?
A: The custom: true flag indicates user-defined functions. Converters should either error, warn, or approximate.
Q: Are weights and biases guaranteed to be exact?
A: Yes, all numeric values are exported as Python floats (IEEE 754 double precision).
Q: Can I convert networks back to NEAT genomes?
A: Not currently. Export is one-way (phenotype only). Genome export may be added in a future version.
Q: Which network types are supported?
A: All four types: FeedForwardNetwork, RecurrentNetwork, CTRNN, and IZNN.
Q: Do I need neat-python to read the JSON?
A: No, the JSON format is self-contained and can be parsed by any JSON-compliant tool.
While neat-python provides the JSON export capability, conversion to specific frameworks is left to third-party tools. This design:
- Keeps neat-python dependency-free
- Allows community-maintained converters
- Prevents "flavor of the year" bloat
If you create a converter tool, please consider:
- Open-sourcing it
- Documenting which format version you support
- Sharing it with the community
For questions about this format:
- GitHub Issues: https://github.com/CodeReclaimers/neat-python/issues
- Documentation: http://neat-python.readthedocs.io
For converter-specific questions, contact the converter's maintainer.