Tutorial 2: Task Generation and CANN Simulation

Note

Reading Time: 20-25 minutes

Difficulty: Beginner

Prerequisites: Tutorial 1

This tutorial teaches you how to generate task data using the Task module and run simulations with CANN models.


1. Task Module Overview

The CANNs Task module generates experimental paradigms and input data. The relationship between Task and Model:

  • Task: Generates external stimulus sequences (input data)

  • Model: Consumes input data, runs in simulation loop

Task Categories

CANNs provides two main task types:

Tracking Tasks:

  • PopulationCoding1D/2D—Population coding

  • TemplateMatching1D/2D—Template matching

  • SmoothTracking1D/2D—Smooth tracking

Navigation Tasks:

  • ClosedLoopNavigation—Closed-loop navigation

  • OpenLoopNavigation—Open-loop navigation

Note

This tutorial uses the simplest PopulationCoding1D as an example. Other tasks follow similar usage patterns with different initialization parameters. We’ll demonstrate different tasks in later tutorials.


2. PopulationCoding1D in Detail

PopulationCoding1D is a simple population coding task: no stimulus → stimulus → no stimulus. This tests the network’s ability to form and maintain a memory bump.

2.1 Import and Create Task

[ ]:
from canns.task.tracking import PopulationCoding1D
from canns.models.basic import CANN1D
import brainpy.math as bm  # :cite:p:`wang2023brainpy`

# First create model instance
bm.set_dt(0.1)
model = CANN1D(num=256, tau=1.0, k=8.1, a=0.5, A=10, J0=4.0)

# Create task
task = PopulationCoding1D(
    cann_instance=model,      # CANN model instance
    before_duration=10.0,     # Duration before stimulus
    after_duration=50.0,      # Duration after stimulus
    Iext=1.0,                 # Stimulus position in feature space
    duration=10.0,            # Stimulus duration
    time_step=0.1,            # Time step
)

2.2 Parameter Descriptions

Parameter

Type

Description

cann_instance

BaseCANN1D

CANN model instance—task calls its get_stimulus_by_pos()

before_duration

float

Duration before stimulus presentation (no input period)

after_duration

float

Duration after stimulus ends (observe bump maintenance)

Iext

float

Stimulus position in feature space, typically in [z_min, z_max]

duration

float

Duration of stimulus presentation

time_step

float

Simulation time step—should match bm.set_dt(...)

Why these parameters matter:

  • cann_instance is required because the task needs to call the model’s get_stimulus_by_pos() method to generate appropriate stimulus

  • before_duration and after_duration allow observing bump formation and maintenance

  • Iext determines where the bump will form

  • All durations use the same unit as time_step

2.3 Getting Task Data

After creating a task, call get_data() to generate and store input data in task.data:

[2]:
# Generate task data
task.get_data()

# Access task properties
print(f"Total time steps: {task.total_steps}")
print(f"Total duration: {task.total_duration}")
print(f"Data shape: {task.data.shape}")
<PopulationCoding1D>Generating Task data(No For Loop)
Total time steps: 700
Total duration: 70.0
Data shape: (700, 256)

Important

get_data() does not return a value—it modifies task.data in-place. Access the data via task.data.


3. Running Simulations with bm.for_loop

3.1 Why use for_loop?

BrainPy [18] provides bm.for_loop for efficient simulation loops. Compared to Python for loops, it offers:

  • JIT Compilation: Entire loop compiled to efficient machine code

  • GPU Acceleration: Automatic GPU utilization

  • Auto-vectorization: Better memory access patterns

See also

See BrainPy Loops Tutorial for detailed for_loop usage.

3.2 Basic Usage

[3]:
import brainpy.math as bm  # :cite:p:`wang2023brainpy`

# Define step function
def run_step(t, inp):
    """
    Single simulation step.

    Args:
        t: Current time step index
        inp: Input data at current time step

    Returns:
        State variables to record
    """
    model(inp)  # Or model.update(inp)
    return model.u.value, model.r.value

# Run simulation using task.data
results = bm.for_loop(
    run_step,           # Step function
    operands=(task.run_steps, task.data),  # Number of time steps and input data
    progress_bar=10  # Optional progress bar (updates every 10 steps)
)

3.3 Handling Return Values

for_loop returns values corresponding to step function returns:

[4]:
# results is a tuple of return values across all time steps
u_history, r_history = results

print(f"Membrane potential history shape: {u_history.shape}")  # (run_steps, num)
print(f"Firing rate history shape: {r_history.shape}")  # (run_steps, num)
Membrane potential history shape: (700, 256)
Firing rate history shape: (700, 256)

3.4 JIT Compilation Benefits

First run includes compilation time (few seconds), but subsequent runs are much faster:

[9]:
import time

model = CANN1D(num=256, tau=1.0, k=8.1, a=0.5, A=10, J0=4.0)

# Create a new longer task
task = PopulationCoding1D(
    cann_instance=model,      # CANN model instance
    before_duration=10000.0,     # Duration before stimulus
    after_duration=10000.0,      # Duration after stimulus
    Iext=1.0,                 # Stimulus position in feature space
    duration=10000.0,            # Stimulus duration
    time_step=0.1,            # Time step
)
task.get_data()

# First run (includes compilation)
start = time.time()
results = bm.for_loop(run_step, operands=(task.run_steps, task.data))
print(f"First run: {time.time() - start:.2f}s")

# Second run (already compiled)
start = time.time()
results = bm.for_loop(run_step, operands=(task.run_steps, task.data))
print(f"Second run: {time.time() - start:.2f}s")
<PopulationCoding1D>Generating Task data(No For Loop)
First run: 0.75s
Second run: 0.24s

4. Complete Example

Here’s a complete example from model creation to simulation:

[10]:
import brainpy.math as bm  # :cite:p:`wang2023brainpy`
from canns.models.basic import CANN1D
from canns.task.tracking import PopulationCoding1D

# ============================================================
# Step 1: Setup environment and create model
# ============================================================
bm.set_dt(0.1)

model = CANN1D(num=256, tau=1.0, k=8.1, a=0.5, A=10, J0=4.0)

# ============================================================
# Step 2: Create task
# ============================================================
task = PopulationCoding1D(
    cann_instance=model,
    before_duration=10.0,
    after_duration=50.0,
    Iext=0.0,
    duration=10.0,
    time_step=0.1,
)

# Get task data
task.get_data()

print("Task Information:")
print(f"  Total time steps: {task.total_steps}")
print(f"  Total duration: {task.total_duration}")
print(f"  Data shape: {task.data.shape}")

# ============================================================
# Step 3: Define simulation step function
# ============================================================
def run_step(t, inp):
    model.update(inp)
    return model.u.value, model.r.value

# ============================================================
# Step 4: Run simulation
# ============================================================
u_history, r_history = bm.for_loop(
    run_step,
    operands=(task.run_steps, task.data),
)

# ============================================================
# Step 5: Inspect results
# ============================================================
print("\nSimulation Results:")
print(f"  Membrane potential history shape: {u_history.shape}")
print(f"  Firing rate history shape: {r_history.shape}")

# Check states at different phases
before_steps = int(10.0 / 0.1)  # Before stimulus
stim_end = int(20.0 / 0.1)      # End of stimulus
after_steps = int(70.0 / 0.1)   # End of simulation

print(f"\nBefore stimulus (t={before_steps-1}) max firing rate: {bm.max(r_history[before_steps-1]):.6f}")
print(f"During stimulus (t={stim_end-1}) max firing rate: {bm.max(r_history[stim_end-1]):.6f}")
print(f"After stimulus (t={after_steps-1}) max firing rate: {bm.max(r_history[after_steps-1]):.6f}")
<PopulationCoding1D>Generating Task data(No For Loop)
Task Information:
  Total time steps: 700
  Total duration: 70.0
  Data shape: (700, 256)

Simulation Results:
  Membrane potential history shape: (700, 256)
  Firing rate history shape: (700, 256)

Before stimulus (t=99) max firing rate: 0.000000
During stimulus (t=199) max firing rate: 0.002426
After stimulus (t=699) max firing rate: 0.002348

Expected output:

  • Before stimulus: firing rate ≈0

  • During stimulus: firing rate increases (bump forms)

  • After stimulus: firing rate maintained (memory persists)


5. Next Steps

Congratulations! You now understand how to generate tasks and run CANN simulations. You’ve learned:

  • How to create smooth tracking tasks with SmoothTracking1D and SmoothTracking2D

  • How to generate navigation tasks with OpenLoopNavigationTask

  • How to run simulations using bm.for_loop for efficiency

  • How to record and analyze network states over time

  • The difference between open-loop (pre-generated trajectory) and closed-loop (interactive) tasks

What You’ve Learned

Task Generation

You can create various input patterns for CANN models—from simple tracking to complex navigation trajectories.

Simulation Workflows

You understand the standard simulation pattern: initialize model → create task → run loop → analyze results.

Performance Optimization

You know how to use JIT-compiled loops for fast simulation of long time series.

Data Organization

You can structure simulation outputs for downstream analysis and visualization.

Continue Learning

Now proceed to visualization and analysis:

Then continue with:

  • Tutorial 4: Parameter Effects—Systematic exploration of how parameters affect dynamics

  • Tutorial 5: Hierarchical Path Integration—Multi-scale navigation with grid cells

  • Tutorial 6: Theta Sweep System—Theta-modulated head direction and grid cells

Or explore other workflows:

  • Scenario 2: Data Analysis—Analyze experimental neural recordings

  • Scenario 3: Brain-Inspired Learning—Train networks with Hebbian learning

  • Scenario 4: Pipeline—End-to-end research workflows

Key Takeaways

  1. Tasks define inputs—Task modules generate the external stimuli that drive CANN dynamics

  2. Separation of concerns—Models define dynamics, tasks define inputs, analyzers handle visualization

  3. Flexibility—The same model can be used with different tasks for various applications

  4. Efficiency matters—Use compiled loops (bm.for_loop) for production simulations

Best Practices

When running simulations:

  1. Start small—Test with short durations first, then scale up

  2. Monitor memory—Long simulations with many neurons can consume significant memory

  3. Save intermediate results—Use np.save to checkpoint long simulations

  4. Document parameters—Keep track of task and model parameters for reproducibility

Next: Tutorial 3: Analysis and Visualization