A mechanistic theory of planning in prefrontal cortex.
The 12 matches
- [1] § Methods › Analyses › Decoding analyses ↔ pysta/subspace_utils.py, lines 81 to 200 · score 0.78 · logistic regression models, L2 regularised, regularisation strength, decoder, encoding, predicted · Report a mismatch
- [2] § Methods › The spacetime attractor › Network dynamics ↔ pysta/agents.py, lines 715 to 810 · score 0.78 · exponential decay, temperature parameter, neural potential, analytically, simplicity, nonlinearity · Report a mismatch
- [3] § Methods › Analyses › Decoding analyses ↔ pysta/analysis_utils.py, lines 84 to 188 · score 0.76 · logistic regression, predict location, neural activity, future location, inverse, crossvalidation · Report a mismatch
- [4] § Methods › Analyses › Fixed point analyses ↔ pysta/agents.py, lines 422 to 481 · score 0.67 · recurrent noise, network iterations, firing rate, network dynamics · Report a mismatch
- [5] § Methods › Analyses › Subspace identification ↔ scripts/analyse_changing_maze_rnn.py, lines 167 to 297 · score 0.63 · orthogonal subspaces, planning subspaces, working memory, activity, location · Report a mismatch
- [6] § Methods › Tasks ↔ pysta/envs.py, lines 13 to 100 · score 0.62 · wall configuration, reward landscape, reward function, execution, mazes, transitions · Report a mismatch
- [7] § Methods › Analyses › Estimating effective connectivity ↔ scripts/analyse_changing_maze_rnn.py, lines 99 to 152 · score 0.58 · point biserial correlations, adjacency matrices, weights, connectivity · Report a mismatch
- [8] § Methods › Analyses › Perturbation analyses ↔ figure_code/plot_attractor_dynamics.py, lines 107 to 126 · score 0.58 · raw firing rates, stimulation strength, dynamics, RNN · Report a mismatch
- [9] § Methods › The spacetime attractor › Practical considerations ↔ pysta/agents.py, lines 422 to 481 · score 0.55 · update neurons, network iterations, feedforward, agent, dynamics · Report a mismatch
- [10] § Methods › The spacetime attractor › Network dynamics ↔ pysta/agents.py, lines 635 to 680 · score 0.54 · exponential nonlinearity, firing rate, iteration, recurrent, network, dynamics · Report a mismatch
- [11] § Results › STAs are efficient planners › RNNs learn a world model ↔ figure_code/plot_rnn_connectivity.py, lines 152 to 206 · score 0.54 · recurrent weights, RNN learned, adjacency matrix, std, connections, subspaces · Report a mismatch
- [12] § Methods › Analyses › Decoding analyses ↔ pysta/analysis_utils.py, lines 84 to 188 · score 0.52 · predict location, neural activity, held, crossvalidation, decoders, behaviour · Report a mismatch
Paper
Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 1,020 lines · 42 KB · MIT · 4 matches
- import numpy as np
- import pickle
- import torch
- from torch import nn
- import torch.nn.functional as F
- import matplotlib.pyplot as plt
- #%% base agent
- class BaseAgent(nn.Module):
- classname = "BaseAgent"
- label = "base"
- def __init__(self, env, rec_noise = 1e-3, ent_reg = 1e-5, greedy = False, force_optimal = False, iters_per_action = 1, tau = 1, **kwargs):
- """
- RNN agent that learns to navigate in a maze
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- rec_noise : float
- Noise added to the recurrent dynamics
- ent_reg: float
- Amount of entropy regularization to add
- greedy : bool
- If True, take greedy actions, otherwise sample from the policy
- force_optimal : bool
- Only allow optimal actions. If there are multiple actions, renormalize the policy over these.
- iters_per_action : int or list of ints
- Number of RNN iterations to run for each environment iteration.
- tau : float
- timescale of the dynamics. r_{t+1} = (1-1/tau) * r_t + (1/tau) * f(r_t, x_{t+1})
- """
- # pytorch boilerplate
- super(BaseAgent, self).__init__()
- # store some hyperparameters
- self.env = env
- self.greedy = greedy
- self.force_optimal = force_optimal
- self.iters_per_action = iters_per_action
- self.tau = tau
- self.rec_noise = rec_noise
- self.store_all_activity = False
- self.ent_reg = ent_reg
- # and get some from the environment
- self.Nout = self.env.output_dim # dimensionality of the policy we're learning
- self.Nin = self.env.obs_dim # dimensionality of the observations
- # instantiate parameters
- self.initialise_weights()
- # reset the model to its initial conditions
- self.reset()
- return
- def initialise_weights(self):
- """
- Instantiate the model parameters
- """
- raise NotImplementedError
- def allo_to_ego_pi(self, allo_pi):
- """
- Convert an allocentric policy to an egocentric policy by renormalizing over neighboring states
- In this version, each action is given the non-normalized probability associated with the resulting state (instead of distributing the probability mass)
- This is done to ensure that the greedy actions agree
- """
- ego_pi = torch.zeros(allo_pi.shape[0], self.env.num_actions) # new egocentric policy
- new_locs = self.env.neighbors[self.env.batch_inds, self.env.loc, :] # for each action, where do I end up
- for a in range(self.env.num_actions): # for each action
- new_locs_a = new_locs[:, a] # where would I end up
- ego_pi[:, a] = allo_pi[self.env.batch_inds, new_locs_a] # what is the probability of going here
- return ego_pi / ego_pi.sum(-1, keepdims = True)
- @property
- def name(self):
- """Generates a string representation of the agent"""
- if type(self.iters_per_action) in [int, np.int32, np.int64]:
- iter_str = f"iter{self.iters_per_action}"
- else:
- iter_str = f"iter{'-'.join([str(val) for val in self.iters_per_action])}"
- tau_str = f"tau{self.tau}"
- force_str = "opt" if self.force_optimal else "agent"
- return f"{self.classname}/{iter_str}_{tau_str}_{force_str}"
- def reset(self):
- """
- Reset the agent.
- This function sets the initial condition and empties caches storing data.
- """
- # no gradients for resetting environment
- with torch.no_grad():
- self.env.reset() # reset environment state
- # also initialise some lists to store data along the way
- self.store = [] # store many things at the time of each action
- self.all_acts = [[], [], []] # also store some information for dynamics in between actions
- # need gradients for setting initial z
- self.z = torch.zeros(torch.Size([self.env.batch])+self.z0.shape, device = self.z0.device) + self.z0[None, ...]
- self.r = self.phi(self.z)
- # instantiate loss functions
- self.acc_loss = torch.tensor(0.0) # accuracy
- self.weight_loss = self.env.batch * self.calc_parameter_reg() # parameter regularization
- self.rate_loss = self.calc_activity_reg() # rate regularization
- self.ent_loss = torch.tensor(0.0) # entropy regularization
- self.update_optimal_actions() # cache optimal actions
- return
- def sample_action(self):
- """
- This function samples an action for each trial in a batch from the current policy.
- Note that 'self.action' is the action chosen by an unconstrained agent.
- 'self.env_action' is the action passed to the environment and can be different if we enforce optimality.
- We distinguish between the two because e.g. accuracies are still computed from the sampled action.
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- with torch.no_grad():
- if self.env.output_format == "allocentric":
- adj = self.env.adjacency[self.env.batch_inds, self.env.loc, :] # renormalize over adjacent states
- assert adj.sum(-1).min() >= 2 # we should always have adjacent states
- pi = (self.pi+1e-20) * adj # add some jitter to make sure the policy is not exactly zero. Otherwise we sometimes run into nans
- pi = pi/pi.sum(-1, keepdims = True) # normalise
- else:
- pi = self.pi # if we're already in egocentric space, everything is possible
- if self.greedy: # pick the most likely action
- self.action = torch.argmax(pi, -1)
- else: # sample an action
- try:
- self.action = torch.multinomial(pi, 1)[..., 0]
- except RuntimeError:
- # something went wrong, so we save the current state of the agent for debugging.
- # This may happen if the policy becomes near-deterministic, and it can often be resolved by increasing the entropy regularisation.
- print(pi.min(), pi.max(), self.pi.min(), self.pi.max(), self.logpi.min(), self.logpi.max())
- pickle.dump(self, open("./temp.p", "wb"))
- raise Error
- if self.force_optimal: # optionally renormalize over optimal actions first
- # add small epsilon in case there are no optimal actions -> uniform policy (e.g. if the goal is too far away to be reached within the trial)
- opt_pis = pi * self.optimal_actions + torch.rand(pi.shape)*1e-20
- if self.greedy: # pick most likely action
- self.env_action = torch.argmax(opt_pis, -1)
- else: # sample an action
- self.env_action = torch.multinomial(opt_pis, 1)[..., 0]
- else: # if we're not enforcing optimality, the action passed to environment is just the original action
- self.env_action = self.action
- return self.action
- def step(self, observation):
- """
- Perform all the computations happening before the next environment update step
- This can include multiple iterations of recurrent network dynamics.
- Parameters
- ----------
- observation : tensor
- The observation at this point in time
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- raise NotImplementedError
- def calc_activity_reg(self, not_finished = None):
- """
- Compute firing rate regularization loss. 0 by default.
- """
- return 0.0
- def calc_parameter_reg(self):
- """
- Compute weight regularization loss. 0 by default.
- """
- return 0.0
- def update_loss(self):
- """
- Accumulate losses.
- Treat accuracy loss, rate loss, and parameter loss separately.
- Losses are only added for trials that have not finished.
- """
- not_finished = torch.where(~self.env.finished)[0] # trials that have not finished
- if self.env.step_num >= 0: # only apply prediction and parameter loss during execution phase
- # policy loss (sum_{a \in opt_as} pi(a))
- opt_probs = (self.pi*self.optimal_actions)[not_finished, :].sum(-1)
- assert opt_probs.max() < 1.0 + 1e-5 # check that things are not too crazy
- self.acc_loss = self.acc_loss + (1.0 - opt_probs).sum() # turn our objective into a loss and sum across batches
- # entropy loss
- jitter = 1e-5 # add a little bit of jitter to avoid nans
- pi_ent = (self.pi + jitter) / (1+self.pi.shape[-1] * jitter) # make sure things are normalised
- self.ent_loss = self.ent_loss + self.ent_reg * (pi_ent*pi_ent.log())[not_finished, :].sum() # want to maximize entropy; minimize -H = E[pi logpi]
- return
- def update_store(self):
- """
- Update list of environment/agent states to include the current state.
- """
- corrects = self.optimal_actions[self.env.batch_inds, self.action] # was the action correct for each trial?
- corrects[self.env.finished] = torch.nan # nans if trial is finished
- self.store.append({
- "rs": self.r.detach(), # firing rate
- "zs": self.z.detach(), # neural potential
- "loc": self.env.loc, # location
- "action": self.action, # action generated by the agent
- "env_action": self.env_action, # action passed to the environment
- "optimal_actions": self.optimal_actions, # optimal action
- "step_num": self.env.step_num, # how far along are we in the trial (negative during the planning phase)
- "finished": self.env.finished.clone(), # which trials have finished
- "pi": self.pi.detach(), # policy
- "xs": self.env.observation(), # inputs
- "corrects" : corrects # whether actions are correct
- })
- def update_optimal_actions(self):
- """
- Cache optimal actions
- """
- with torch.no_grad():
- self.optimal_actions = self.env.optimal_actions() # tensor (batch, output_dim)
- def forward(self, store = False):
- """
- Run a single batch of trials
- Parameters
- ----------
- store : bool
- if true, store environment and agent states after every action
- Returns
- ----------
- avg_loss : tensor
- average loss across trials in a batch
- """
- self.reset() # reset agent
- while not torch.all(self.env.finished): # as long as there are some trials left to act in
- self.update_optimal_actions() # cache the optimal actions at the current location
- x = self.env.observation().to(self.z0.device) # observation at this point in time
- self.step(x) # update RNN state, compute policy, and sample an action
- self.update_loss() # update performance and entropy loss
- # now update environment and optionally store env+agent state (don't propagate gradients through this)
- with torch.no_grad():
- if store:
- self.update_store()
- self.env.step(self.env_action) # action passed to the environment (optionally restricted to be optimal)
- # loss is combined accuracy and regularization losses, normalized by the batch size
- return (self.acc_loss + self.weight_loss + self.rate_loss + self.ent_loss) / self.env.batch
- def eval(self, num_eval = 5):
- """
- Compute average loss and accuracy across a number of trials
- Parameters
- ----------
- num_eval : int
- number of batches to average over
- Returns
- ----------
- avg_loss : float
- average loss across trials in all batches
- avg_acc : float
- average accuracy across trials in all batches
- """
- losses, accs = [], [] # lists to concatenate across batches
- for _ in range(num_eval): # for each batch
- # simulate the trials
- loss = self.forward(store = True).detach().cpu().numpy()
- losses.append(loss) # append loss
- corrects = torch.stack([s["corrects"] for s in self.store if s["step_num"]>=0]) # accuracy for each trial and step in the execution phase
- accs.append(torch.nanmean(corrects, axis = 0).mean()) # average
- return np.mean(losses), np.mean(accs)
- def plot_trial(self, filename = None, run_trial = True, trial_num = 0, values = True, vmap = None, cmap = "coolwarm"):
- """
- Plot a summary plot for a trial
- Parameters
- ----------
- filename : str
- filename to save to
- run_trial : bool
- whether to run a new trial (True) or use cached information (False)
- trial_num : int
- which trial number within the batch to plot
- values : bool
- for the reward landscape task, whether to plot a value map (True) or reward map (False)
- vmap : tensor
- optional data to plot as a heatmap for each time point
- cmap : str
- colormap to use
- """
- if run_trial: # optionally simulate a new trial
- self.forward(store = True)
- # find out which time points have not finished
- fs = 1 - np.array([store["finished"][trial_num].numpy() for store in self.store])
- T = min(np.sum(fs)+1, len(self.store)) # number of time points to plot
- fig, axs = plt.subplots(1, T, figsize = (2*T, 2))
- for t, ax in enumerate(axs): # for each time point
- data = self.store[t] # get data for this time point
- # plot a panel
- vmap_t = None if vmap is None else vmap[t] # optionally plot some useful data
- self.env.plot(loc = data["loc"][trial_num], step_num = data["step_num"], ax = ax, trial_num = trial_num, values = values, cmap = cmap, vmap = vmap_t)
- plt.tight_layout = True
- if filename is not None:
- plt.savefig(filename, bbox_inches = "tight")
- plt.close()
- return
- #%% Vanilla RNN
- class VanillaRNN(BaseAgent):
- classname = "VanillaRNN"
- label = "rnn"
- def __init__(self, env, Nrec = 800, W_reg = 1e-3, r_reg = 1e-3, nonlin_output = False, **kwargs):
- """
- RNN agent that learns to navigate in a maze
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- Nrec : int
- Number of relu units in the RNN's hidden layer
- W_reg : float
- Regularization strength for the parameters
- r_reg : float
- Regularization strength for the hidden layer activity
- nonlin_output : bool
- If True, have an additional hidden layer in the readout. Otherwise use a linear readout from the hidden state.
- """
- # store some model-specific parameters
- self.Nrec = Nrec
- self.nonlin_output = nonlin_output
- self.W_reg, self.r_reg = W_reg, r_reg
- self.phi = F.relu # use a ReLU nonlinearity
- # initialise BaseAgent
- super(VanillaRNN, self).__init__(env, **kwargs)
- @property
- def name(self):
- """generates a string representation of the agent"""
- nonlin_str = "nonlinout" if self.nonlin_output else "linout"
- basename = super(VanillaRNN, self).name
- return f"{basename}/N{self.Nrec}_{nonlin_str}"
- def initialise_weights(self):
- """
- Instantiate the learnable model parameters.
- We just initialise the parameters as iid Gaussian variables.
- """
- # RNN initial condition
- self.z0 = nn.Parameter(torch.randn(self.Nrec, 1), requires_grad=True)
- # recurrent weight matrix
- self.Wrec = nn.Parameter(torch.randn(self.Nrec, self.Nrec) / np.sqrt(self.Nrec), requires_grad=True)
- # input weight matrix
- self.Win = nn.Parameter(torch.randn(self.Nrec, self.Nin) / np.sqrt(self.Nin), requires_grad=True)
- # hidden state bias
- self.brec = nn.Parameter(torch.zeros(self.Nrec, 1), requires_grad=True)
- # now initialise the output function, which can be either nonlinear or linear
- if self.nonlin_output:
- # create one hidden layer between the RNN and policy
- # weights and bias to hidden output layer
- self.Wout1 = nn.Parameter(torch.randn(int(np.round(self.Nrec/2)), self.Nrec) / np.sqrt(self.Nrec), requires_grad=True)
- self.bout1 = nn.Parameter(torch.zeros(self.Wout1.shape[0], 1), requires_grad=True)
- # weights and bias to policy
- self.Wout = nn.Parameter(torch.randn(self.Nout, self.Wout1.shape[0]) / np.sqrt(self.Wout1.shape[0]), requires_grad=True)
- self.bout = nn.Parameter(torch.zeros(self.Nout, 1), requires_grad=True)
- else:
- # just a single weight and bias for a linear readout to out policy
- self.Wout = nn.Parameter(torch.randn(self.Nout, self.Nrec) / np.sqrt(self.Nrec), requires_grad=True)
- self.bout = nn.Parameter(torch.zeros(self.Nout, 1), requires_grad=True)
- return
- def step(self, observation):
- """
- Perform all the computations happening before the next environment update step
- This can include multiple iterations of recurrent network dynamics.
- Parameters
- ----------
- observation : tensor
- The observation at this point in time
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- batch = observation.shape[0] # batch size
- # decide how many network iterations to run for this environment iteration
- network_iters = self.iters_per_action if type(self.iters_per_action) in [int, np.int32, np.int64] else np.random.choice(self.iters_per_action)
- for _ in range(network_iters): # optionally several network iterations
- # recurrent noise
- rec_noise = torch.randn(batch, self.Nrec, 1, device = self.z0.device) * self.rec_noise
- # feedforward input
- ff_inp = self.Win @ observation[..., None]
- # recurrent input
- rec_inp = self.Wrec @ self.r
- # update neuron potentials
- self.z = (1 - 1/self.tau) * self.z + (1/self.tau)*(rec_inp + ff_inp + self.brec + rec_noise)
- # compute firing rates
- self.r = self.phi(self.z) # firing rate
- # update rate loss for trials that have not finished
- self.rate_loss = self.rate_loss + self.calc_activity_reg(torch.where(~self.env.finished)[0])
- if self.store_all_activity: # optinoally store activity at every RNN iteration
- self.all_acts[0].append(self.r.detach().numpy())
- self.all_acts[1].append( self.env.loc.detach().numpy())
- self.all_acts[2].append(self.env.step_num)
- # compute output
- if self.nonlin_output:
- rout = self.phi(self.Wout1 @ self.r + self.bout1) # pass through hidden layer
- else:
- rout = self.r # just linear readout
- self.logpi = (self.Wout @ rout + self.bout)[..., 0] # flatten for each batch (batch x actions x 1) -> (batch, actions)
- # normalize log policy
- self.logpi = self.logpi - self.logpi.logsumexp(-1, keepdims = True)
- self.pi = self.logpi.exp() # compute policy (batch, actions)
- # sample an action from the policy
- self.action = self.sample_action()
- return self.action
- def calc_activity_reg(self, not_finished = None):
- """
- Compute firing rate regularization loss
- Parameters
- ----------
- not_finished : bool
- trials within the batch which have not yet finished.
- if None: assume no trials are finished.
- Returns
- ----------
- reg_loss : tensor
- firing rate regularization loss. Summed across trials and neurons
- """
- if not_finished is None:
- reg_loss = self.r_reg * (self.r**2).sum() # magnitude of hidden state vector
- else:
- reg_loss = self.r_reg * (self.r[not_finished]**2).sum() # magnitude of hidden state vector
- return reg_loss
- def calc_parameter_reg(self):
- """
- Compute weight regularization loss
- Returns
- ----------
- reg_loss : tensor
- weight regularization loss. sum across all parameters.
- """
- reg_loss = self.W_reg * torch.stack([torch.square(p).sum() for p in self.parameters()]).sum() # magnitude of total weight vecto
- return reg_loss
- #%% Handcrafted spacetime attractor
- class SpaceTimeAttractor(BaseAgent):
- classname = "SpaceTimeAttractor"
- label = "sta"
- def __init__(self, env, beta = 9.0, tau = 50, iters_per_action = 400, shift_time = 2.0, rec_noise = 1e-1, adj_noise = 1e-2, **kwargs):
- """
- Spacetime attractor that optimises a reward function
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- beta : Float
- Temperature parameter for the reward function. Performance is somewhat sensitive to this.
- If beta is too low, the network can converge to a diffuse representation instead of a clean trajectory to the goal.
- If beta is too high, the representation can 'teleport' for long trajectories.
- tau : Float
- Time constant for the dynamics
- shift_time : Float
- Number of time constants for which a feedforward component is included in the dynamics.
- rec_noise : Float
- Magnitude of the noise added to the recurrent dynamics.
- adj_noise : Float
- Magnitude of the noise added to each element of the recurrent weights
- """
- # set some hyperparameters
- self.num_modules = env.max_steps+1 # Number of subspaces; current loc plus max_steps future locs
- self.num_locs = env.num_locs
- self.batch = env.batch
- self.adj_noise = adj_noise
- self.shift_time = shift_time
- self.Nrec = self.num_modules * self.num_locs # total number of neurons across modules
- self.beta = beta # reward function temperature parameters
- self.exact = False # perform exact inference
- # initialise BaseAgent
- kwargs["tau"] = tau
- kwargs["iters_per_action"] = iters_per_action
- super(SpaceTimeAttractor, self).__init__(env, **kwargs)
- self.rec_noise = rec_noise # recurrent noise to ensure robustness
- def phi(self, x):
- """
- We use an exponential nonlinearity.
- """
- return x.exp()
- def initialise_weights(self):
- """
- Instantiate the handcrafted model parameters
- """
- A = self.env.adjacency.clone() # adjacency matrix of the environment
- adj_noise = self.adj_noise # how much noise will we add
- bias = -5e-3 # bias added to the noise
- # instantiate recurrent weight matrices
- self.Wrec_fwd, self.Wrec_bwd = [torch.zeros(self.batch, self.Nrec, self.Nrec) for _ in range(2)]
- self.Win = torch.zeros(self.Nrec, self.Nin) # input weights
- self.Wout = torch.zeros(self.env.num_locs, self.Nrec) # inherently allocentric; can convert to egocentric subsequently
- self.Wrec_shift = torch.zeros(self.Nrec, self.Nrec) # weights for the 'shift' dynamics
- # set recurrent weights
- for mod1 in range(self.num_modules-1): # for each subspace
- # indices of the weight matrices corresponding to this subspace and the next
- mod1_inds, mod2_inds = [torch.arange(self.num_locs)+self.num_locs*ind for ind in [mod1, mod1+1]]
- self.Wrec_shift[mod1_inds, mod2_inds] = torch.ones(self.num_locs) # shift connections from delta+1 to delta
- for i1, ind1 in enumerate(mod1_inds):
- for i2, ind2 in enumerate(mod2_inds):
- # weights are just the adjacency matrix plus some noise
- self.Wrec_fwd[:, ind2, ind1] = A[:, i2, i1].clone() + (torch.rand(self.batch) - 2.0)*adj_noise + bias
- self.Wrec_bwd[:, ind1, ind2] = A[:, i1, i2].clone() + (torch.rand(self.batch) - 2.0)*adj_noise + bias
- # total recurrent weights combine forward and backward connections between subspaces
- self.Wrec = self.Wrec_fwd + self.Wrec_bwd
- # input weights
- self.Win[:self.num_locs, :self.num_locs] = torch.eye(self.num_locs) # current loc
- self.Win[self.num_locs:, 2*self.num_locs:-2*self.num_locs] = torch.eye(self.Win.shape[0] - self.num_locs) # future reward (zero for _current_ reward)
- # output is just the delta=1 subspace
- self.Wout[:, self.num_locs:2*self.num_locs] = torch.eye(self.num_locs)
- # initial condition is uniform in space at each time
- self.z0 = torch.log(torch.ones(self.num_modules, self.num_locs) / self.num_locs)
- #bias is zero for this agent
- self.brec = torch.zeros(self.Nrec, 1)
- return
- def calc_policy(self):
- """compute policy from firing rate"""
- self.pi = (self.Wout @ self.r.reshape(self.batch, -1, 1))[..., 0] # policy ends up being activity in second module
- if self.env.output_format == "egocentric": # optionally convert to an egocentric policy
- self.pi = self.allo_to_ego_pi(self.pi)
- def obs_to_rew_func(self, observation, flat = False):
- """This function extract the location and reward information from the inputs and puts it into the right format"""
- obs_mod = observation.reshape(self.batch, -1, self.num_locs) # batch by input type by location within module
- rew_inds = torch.cat([torch.arange(1), torch.arange(2, obs_mod.shape[1]-2)]) # indicies of the inputs corresponding to current location and future reward
- logrews = obs_mod[:, rew_inds, :] # reward function (batch, modules, locs)
- logrews[:, 0, :] *= 20.0 # strong signal to start at current location
- logrews *= self.beta # multiply by temperature parameter
- logrews -= logrews.logsumexp(axis = -1, keepdims = True) # turn into a distribution over locations and add final vector dimension
- if flat: # flatten back into a vector instead of a matrix
- obs_mod[:, rew_inds, :] = logrews # replace reward function in observation
- return obs_mod.reshape(self.batch, -1, 1)
- else:
- return logrews[..., None]
- def step(self, observation):
- clip_minval = torch.tensor(-100) # threshold value for inhbition
- # first extract reward function from observation
- logrews = self.obs_to_rew_func(observation.clone(), flat = True) # flatten for each trial (batch, modules*locs)
- # select how many network iterations to run
- network_iters = self.iters_per_action if type(self.iters_per_action) in [int, np.int32, np.int64] else np.random.choice(self.iters_per_action)
- for iter_ in range(network_iters): # for each iteration
- rs = self.r.reshape(self.batch, -1, 1) # firing rate but add vector dimension
- # compute change in activity
- dzdt = (self.Win @ logrews + torch.maximum(clip_minval.exp(), self.Wrec_fwd @ rs).log() + torch.maximum(clip_minval.exp(), self.Wrec_bwd @ rs).log())
- # optionally add a 'shift component' to the dynamics for a period after each action
- if (iter_ < self.tau*self.shift_time) and (self.env.step_num > 0):
- dzdt += torch.maximum(clip_minval.exp(), self.Wrec_shift @ rs).log()
- # reshape update and bias
- dzdt = dzdt.reshape(self.batch, -1, self.num_locs)
- bias = self.brec.reshape(1, -1, self.num_locs)
- # sample recurrent noise
- rec_noise = torch.randn(dzdt.shape, device = self.z0.device) * self.rec_noise
- # update activity
- self.z = (1-1/self.tau) * self.z + 1/self.tau * (dzdt + rec_noise + bias)
- # normalize and threshold
- self.z = self.z - self.z.logsumexp(axis = -1, keepdims = True) # normalise
- self.z = torch.clip(self.z, min = clip_minval, max = 0.0) # threshold
- self.r = self.phi(self.z) # apply exponential nonlinearity to compute firing rates
- if self.store_all_activity: # optionally store activity at every RNN iteration
- self.all_acts[0].append(self.r.detach().numpy())
- self.all_acts[1].append( self.env.loc.detach().numpy())
- self.all_acts[2].append(self.env.step_num)
- #compute the policy
- self.calc_policy()
- # sample an action from the policy
- self.action = self.sample_action()
- return self.action
- def reset(self):
- """Reset the agent. This involves the BaseAgent reset and then re-initialising weights in case the environment changed"""
- super(SpaceTimeAttractor, self).reset()
- self.initialise_weights() # reinitialize weights
- return
- def plot_representation(self, filename = None, trial_num = 0, **kwargs):
- """
- Generate a summaruy plot for a trial
- Parameters
- ----------
- filename : str
- filename to save to
- trial_num : int
- which trial number within the batch to plot
- """
- # find out which time points have not finished
- fig, axs = plt.subplots(1, self.num_modules, figsize = (2*self.num_modules, 2))
- for t, ax in enumerate(axs): # for each time point, plot a panel
- loc = self.env.loc[trial_num] if t == 0 else torch.argmax(self.r[trial_num, t]) # predicted location at this time
- self.env.plot(loc = loc, vmap = self.r[trial_num, t, :], step_num = t, ax = ax, trial_num = trial_num,
- cmap = "YlOrRd", plot_optimal_actions = t<len(axs)-1, vmin = -0.1, vmax = 1.2, **kwargs)
- plt.tight_layout = True
- if filename is not None:
- plt.savefig(filename, bbox_inches = "tight")
- plt.close()
- return
- #%% Successor representation agent
- class SRLearner(BaseAgent):
- classname = "SRLearner"
- label = "sr"
- def __init__(self, env, gamma = 0.95, beta = 5.0, **kwargs):
- """
- Successor representation agent that navigates in a maze
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- gamma : Float
- The discount factor used to compute the successor matrix
- beta : Float
- The temperature parameter used to compute the policy
- """
- # instantiate class and save some parameters
- self.gamma = gamma
- self.beta = beta # temperature parameter
- super(SRLearner, self).__init__(env, **kwargs)
- self.Nrec = 2*self.env.num_locs # expected occupancy of each location and expected value of each location
- return
- def phi(self, x):
- return x # no nonlinearity
- def reset(self):
- """Reset Baseagent then recompute successor matrix"""
- super(SRLearner, self).reset()
- adj = self.env.adjacency # (batch, to, from)
- T = adj / adj.sum(1, keepdims = True) # transition matrix
- self.M = torch.linalg.inv(torch.eye(T.shape[-1])[None, ...] - self.gamma*T) # analytically compute successor matrix
- self.r = None # no neural activity
- return
- def initialise_weights(self):
- """
- Model parameters are empty; we will compute everything analytically
- """
- self.z0 = torch.zeros(self.env.num_locs) # z is value, which we initialize to zero
- return
- def step(self, observation):
- """
- Perform one 'update step'
- For simplicity, we just multiply the average occupancy with the average reward function.
- If the reward format is 'relative', this ends up being the 'reward-to-go'.
- In the future, we could think about also using the 'occupancy-to-go' instead of just exponentially decayed occupancy.
- Parameters
- ----------
- observation : tensor
- The observation at this point in time
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- rew_func = observation.reshape(self.env.batch, -1, self.env.num_locs)[:, 1:-2, :].clone() # reward function (batch, steps, locs)
- # compute average reward
- mean_rew = rew_func[:, 1:, :].mean(1, keepdims = True) # ignore reward at current time
- # compute value function
- self.values = (mean_rew @ self.M)[:, 0, :] # (batch, locs)
- self.z = self.values # neural potential is just the value function
- exp_occ = self.M[self.env.batch_inds, :, self.env.loc] # expected occupancy
- # 'neural activity' is values and occupancy. This is not used for any computation, but may be interesting for decoding analyses.
- self.r = torch.cat([self.values, exp_occ], 1)[..., None]
- # compute policy
- self.pi = (self.beta * self.values).exp() # allocentric policy
- self.pi /= self.pi.sum(-1, keepdims = True) # normalize
- if self.env.output_format == "egocentric":
- self.pi = self.allo_to_ego_pi(self.pi) # optionally convert to egocentric policy
- self.action = self.sample_action() # sample an action from the policy
- return self.action
- def plot_trial_values(self, filename = None, run_trial = True, trial_num = 0, cmap = "coolwarm"):
- """Function for plotting the value function for a trial"""
- if run_trial: # optionally simulate a new trial
- self.forward(store = True)
- vmap = [s["zs"][trial_num].flatten() for s in self.store]
- self.plot_trial(filename = filename, run_trial = False, trial_num = trial_num, vmap = vmap, cmap = cmap)
- #%% TD learning agent
- class TDLearner(BaseAgent):
- classname = "TDLearner"
- label = "td"
- def __init__(self, env, beta = 5.0, tau = 20, **kwargs):
- """
- Temporal difference learner that navigates in a maze
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- beta : Float
- The temperature parameter used to compute a policy
- tau : Float
- The inverse of the learning rate
- """
- # instantiate super class
- self.beta = beta # temperature parameter
- kwargs["tau"] = tau
- super(TDLearner, self).__init__(env, **kwargs)
- return
- def phi(self, x):
- return x # no nonlinearity
- def reset(self):
- """Reset BaseAgent and store initial location"""
- super(TDLearner, self).reset()
- self.prev_loc = self.env.loc
- self.prev_not_finished = torch.ones(self.env.num_locs)
- return
- def initialise_weights(self):
- """
- Model parameters are the values
- """
- self.values = torch.zeros(self.env.num_locs)+1.0 # optimistic value initialisation
- self.z0 = self.values.clone() # our firing rates will also be the values
- return
- def run_td_update(self):
- """Run a TD update on the value function"""
- new_rew = self.env.latest_rew # the reward we just got
- prev_V = self.values[self.prev_loc] # value of previous location
- new_V = self.values[self.env.loc] * (~self.env.finished).to(float) # value of new location (no more value for finished trials)
- self.td_error = (new_rew + new_V - prev_V) * self.prev_not_finished # TD error (set to zero for finished episodes)
- # previous locations
- prev_locs_1hot = F.one_hot(self.prev_loc, num_classes = self.env.num_locs) # (batch, num_locs)
- # batched TD update
- updates = (prev_locs_1hot * self.td_error[:, None]).sum(0) / self.prev_not_finished.sum() # avg over all td errors for each location in the batch
- self.values += (1/self.tau)*updates # update values
- def step(self, observation):
- """
- Perform one 'update step'
- This involves both choosing an action and updating our value function
- Parameters
- ----------
- observation : tensor
- The observation at this point in time
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- # first select an action
- self.pi = (self.beta * self.values).exp()[None, ...] + torch.zeros(observation.shape[0], self.env.num_locs) # # allocentric policy is proportional to exponential value
- self.pi /= self.pi.sum(-1, keepdims = True) # normalize
- if self.env.output_format == "egocentric":
- self.pi = self.allo_to_ego_pi(self.pi) # optionally convert to egocentric
- self.action = self.sample_action() # sample an action from the policy
- # now update our values based on previous experience
- if self.env.step_num >= 1: # can only run a TD update once we start collecting experience
- self.run_td_update()
- self.z = self.values[None, :] + torch.zeros(self.env.batch, self.env.num_locs) # potential is just value
- self.r = self.phi(self.z) # firing rate is just value (this is not used for computation, just for visualisation)
- self.prev_loc = self.env.loc # save previous location
- self.prev_not_finished = (~self.env.finished).to(float) # which trials are done?
- return self.action
- def forward(self, store = False):
- """
- Run a single batch of trials
- Parameters
- ----------
- store : bool
- if true, store environment and agent states after every action
- Returns
- ----------
- avg_loss : tensor
- average loss across trials in a batch
- """
- loss = super(TDLearner, self).forward(store = store) # run default forward pass
- self.run_td_update() # important to update values based on final sample as well
- return loss
- def plot_trial_values(self, filename = None, run_trial = True, trial_num = 0, cmap = "YlOrRd"):
- """Function for plotting value function after a batch of trials"""
- if run_trial: # optionally simulate a new trial
- self.forward(store = True)
- vmap = [s["zs"][trial_num].flatten() for s in self.store]
- self.plot_trial(filename = filename, run_trial = False, trial_num = trial_num, vmap = vmap, cmap = cmap)
- #%% Dynamic programming agent (value agent in space and time)
- class DPAgent(BaseAgent):
- classname = "DPAgent"
- label = "dp"
- def __init__(self, env, beta = 5.0, **kwargs):
- """
- Dynamic programming agent that just computes an optimal spacetime value function once and for all
- Parameters
- ----------
- env : MazeEnv
- Environment that the agent is going to interact with
- beta : Float
- Temperature parameter for computing a policy
- """
- # store parameters
- self.beta = beta
- self.Nrec = env.num_locs + env.max_steps + np.prod(env.vs[0].shape) # location, time-in-trial, and spacetime value function
- # initialise BaseAgent
- super(DPAgent, self).__init__(env, **kwargs)
- return
- def phi(self, x):
- return x # no nonlinearity
- def initialise_weights(self):
- self.z0 = torch.zeros(self.Nrec, 1) # no parameters
- def update_z_and_r(self):
- """This just involves concatenating the relevant environment variables into a vector that will be used for decoding analyses"""
- self.t = torch.tensor(max(0, self.env.step_num)) # current time (everything during planning is called '0')
- self.flat_t = F.one_hot(self.t, num_classes = self.env.max_steps)+torch.zeros(self.env.batch, self.env.max_steps) # one-hot version
- self.loc = F.one_hot(self.env.loc, num_classes = self.env.num_locs) # current location as a one-hot
- self.values = self.env.vs # value function
- # set potential and firing rate to the concatenation of time, location, and values
- self.z = torch.cat([self.values.reshape(self.env.batch, -1), self.loc, self.flat_t], dim = -1)[..., None]
- self.r = self.z.clone()
- def reset(self):
- """Run BaseAgent reset step, and then re-cache the value function and recompute representation since the environment may have changed"""
- super(DPAgent, self).reset()
- self.values = self.env.vs
- self.update_z_and_r()
- return
- def step(self, observation):
- """
- Perform one 'update step'
- This involves updating the neural representation and sampling an action
- Parameters
- ----------
- observation : tensor
- The observation at this point in time
- Returns
- ----------
- action : tensor
- action taken for each trial in the batch
- """
- # first update representations
- self.update_z_and_r()
- # then compute policy
- next_values = self.values[:, self.t+1, :] # values at every upcoming point in spacetime
- self.pi = (self.beta * next_values).exp() # allocentric policy is proportional to exponential value
- self.pi /= self.pi.sum(-1, keepdims = True) # normalize
- if self.env.output_format == "egocentric":
- self.pi = self.allo_to_ego_pi(self.pi) # optionally convert to egocentric
- # and sample an action
- self.action = self.sample_action() # sample an action from the policy
- return self.action
agents.py at commit fdd6dd3, under MIT · at the source
Overview
- Sainsbury Wellcome Centre, University College London London United Kingdom
- Oxford Centre for Integrative Neuroimaging, University of Oxford Oxford United Kingdom
- Department of Experimental Psychology, University of Oxford Oxford United Kingdom
- Gatsby Computational Neuroscience Unit, University College London London United Kingdom
Abstract
Planning is critical for adaptive behaviour in a changing world, because it lets us anticipate the future and adjust our actions accordingly. While prefrontal cortex is crucial for this process, it remains unknown how planning is implemented in neural circuits. Prefrontal representations were recently discovered in simpler sequence memory tasks, where different populations of neurons represent different future time points. We demonstrate that combining such representations with the ubiquitous principle of neural attractor dynamics allows circuits to solve much richer problems, including planning. This is achieved by embedding the environment structure directly in synaptic connections to implement an attractor network that infers desirable futures. The resulting ‘spacetime attractor’ excels at planning in challenging tasks known to depend on the prefrontal cortex. Recurrent neural networks trained by gradient descent on such tasks learn a solution that precisely recapitulates the spacetime attractor – in representation, in dynamics, and in connectivity. Analyses of networks trained across different environment structures reveal a generalisation mechanism that rapidly reconfigures the world model used for planning, without the need for synaptic plasticity. The spacetime attractor is a testable mechanistic theory of planning. If true, it would provide a path towards detailed mechanistic understanding of how prefrontal cortex structures adaptive behaviour.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 12 matches between paragraphs and lines of code.
KrisJensen/pysta
fdd6dd39df54a67aa451a24d3adc280f25136601, 10 October 2026Availability: 1 check, the latest on 25 October 2026: the link answers
- 25 October 2026: the link answers
50 files
- figure_code/
plot_all_figures.py , Python, 65 lines - figure_code/
plot_attractor_dynamics. , Python, 209 lines, 1 matchpy - figure_code/
plot_by_size.py , Python, 81 lines - figure_code/
plot_changing_maze.py , Python, 389 lines - figure_code/
plot_fig1_schematics.py , Python, 221 lines - figure_code/
plot_fig2_schematics.py , Python, 248 lines - figure_code/
plot_fig4_schematics.py , Python, 183 lines - figure_code/
plot_fig5_schematics.py , Python, 171 lines - figure_code/
plot_fig6_schematics.py , Python, 127 lines - figure_code/
plot_handcrafted_example , Python, 99 liness.py - figure_code/
plot_handcrafted_perform , Python, 68 linesance.py - figure_code/
plot_rnn_connectivity.py , Python, 370 lines, 1 match - figure_code/
plot_rnn_decoding.py , Python, 350 lines - figure_code/
plot_rnn_generalization. , Python, 89 linespy - figure_code/
plot_rnn_performance.py , Python, 113 lines - figure_code/
plot_simple_tasks.py , Python, 101 lines - figure_code/
plot_supplementary_schem , Python, 44 linesatics.py - pysta/
V1_panel_utils.py , Python, 123 lines - pysta/
__init__.py , Python, 12 lines - pysta/
agents.py , Python, 1,020 lines, 4 matches - pysta/
analysis_utils.py , Python, 192 lines, 2 matches - pysta/
argparser.py , Python, 61 lines - pysta/
envs.py , Python, 428 lines, 1 match - pysta/
maze_utils.py , Python, 98 lines - pysta/
plot_utils.py , Python, 557 lines - pysta/
subspace_utils.py , Python, 201 lines, 1 match - pysta/
train_rnn.py , Python, 85 lines - pysta/
utils.py , Python, 175 lines - scripts/
analyse_attractor_dynami , Python, 165 linescs.py - scripts/
analyse_by_size.py , Python, 71 lines - scripts/
analyse_changing_maze_rn , Python, 393 lines, 2 matchesn.py - scripts/
analyse_handcrafted_mode , Python, 78 linesls.py - scripts/
analyse_rnn.py , Python, 208 lines - scripts/
analyse_rnn_behaviour.py , Python, 88 lines - scripts/
analyse_rnn_generalisati , Python, 102 lineson.py - scripts/
analyse_simple_tasks.py , Python, 246 lines - scripts/
generate_handcrafted_exa , Python, 158 linesmples.py - scripts/
initialise_directories.p , Python, 37 linesy - scripts/
pysta_example_code.py , Python, 98 lines - scripts/
run_handcrafted_analyses , Python, 16 lines.py - scripts/
run_rnn_analyses.py , Python, 80 lines - scripts/
submit_slurm.py , Python, 42 lines - scripts/
train_all_rnns.py , Python, 80 lines - setup.py, Python, 7 lines
- tests/
run_all_tests.py , Python, 15 lines - tests/
test_agents.py , Python, 147 lines - tests/
test_envs.py , Python, 80 lines - tests/
test_training.py , Python, 33 lines - LICENSE, License, 21 lines
- readme.md, Text, 31 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 48 scripts, each with its path and the digest of its content;
- 12 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data availability
This is a computational manuscript, and no new data has been generated. Code is available at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 25 October 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 8 authors, 5 keywords, 8 MeSH terms, 9 funders, 68 references.
Cite
This paper
Jensen, K. T., Doohan, P. T., Sablé-Meyer, M., Reinert, S., Baram, A. B., Sahani, M., Akam, T., & Behrens, T. E. (2026). A mechanistic theory of planning in prefrontal cortex. eLife, 15, RP109757. https://
BibTeX
@article{jensen2026mecha
author = {Jensen, Kristopher T and Doohan, Peter T and Sablé-Meyer, Mathias and Reinert, Sandra and Baram, Alon Boaz and Sahani, Maneesh and Akam, Thomas and Behrens, Timothy E},
title = {{A mechanistic theory of planning in prefrontal cortex}},
journal = {eLife},
year = {2026},
month = oct,
volume = {15},
pages = {RP109757},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/
url = {https://
pmid = {42904774},
pmcid = {PMC13686619}
}
RIS
TY - JOUR
AU - Jensen, Kristopher T
AU - Doohan, Peter T
AU - Sablé-Meyer, Mathias
AU - Reinert, Sandra
AU - Baram, Alon Boaz
AU - Sahani, Maneesh
AU - Akam, Thomas
AU - Behrens, Timothy E
TI - A mechanistic theory of planning in prefrontal cortex
T2 - eLife
J2 - eLife
PY - 2026
DA - 2026/
VL - 15
SP - RP109757
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.7554/
"type": "article-journal",
"title": "A mechanistic theory of planning in prefrontal cortex",
"container-title": "eLife",
"author": [
{
"family": "Jensen",
"given": "Kristopher T"
},
{
"family": "Doohan",
"given": "Peter T"
},
{
"family": "Sablé-Meyer",
"given": "Mathias"
},
{
"family": "Reinert",
"given": "Sandra"
},
{
"family": "Baram",
"given": "Alon Boaz"
},
{
"family": "Sahani",
"given": "Maneesh"
},
{
"family": "Akam",
"given": "Thomas"
},
{
"family": "Behrens",
"given": "Timothy E"
}
],
"container-title-short":
"volume": "15",
"page": "RP109757",
"DOI": "10.7554/
"PMID": "42904774",
"PMCID": "PMC13686619",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
21
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1038/s41586-024-08145-x [code]
- A cellular basis for mapping behavioural structure.Journal: NatureIn common: scikit-learn, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 9 references, 2 authors
- [2] doi:10.1038/s41593-024-01675-7 [code]
- A recurrent network model of planning explains hippocampal replay and human behavior.Journal: Nature neuroscienceIn common: NumPy, computational modeling (no new data), 8 references, author Kristopher T Jensen
- [3] doi:10.1038/s41593-022-01149-8 [code]
- Complementary task representations in hippocampus and prefrontal cortex for generalizing the structure of problems.Journal: Nature neuroscienceIn common: scikit-learn, SciPy, Matplotlib, 1 other tool, 4 references, 2 authors
- [4] doi:10.1038/s41593-025-01908-3 [code]
- Constructing future behavior in the hippocampal formation through composition and replay.Journal: Nature neuroscienceIn common: PyTorch, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 6 references, author Timothy E Behrens
- [5] doi:10.1038/s42256-026-01254-4 [code]
- Neural sampling from cognitive maps enables goal-directed imagination and planning.Journal: Nature machine intelligenceIn common: PyTorch, scikit-learn, Matplotlib, 1 other tool, computational modeling (no new data), 9 references
- [6] doi:10.7554/elife.101134 [code]
- Flexible neural representations of abstract structural knowledge in the human entorhinal cortex.Journal: eLifeIn common: SciPy, NumPy, 2 references, 2 authors
- [7] doi:10.1371/journal.pcbi.1010547 [code]
- Multiple bumps can enhance robustness to noise in continuous attractor networks.Journal: PLoS computational biologyIn common: PyTorch, SciPy, Matplotlib, 1 other tool, computational modeling (no new data), 8 references
- [8] doi:10.1038/s41467-025-64591-9 [code]
- Hippocampus supports multi-task reinforcement learning under partial observability.Journal: Nature communicationsIn common: PyTorch, scikit-learn, SciPy, 2 other tools, 8 references
- [9] doi:10.1038/s41467-024-54257-3 [code]
- A hierarchical active inference model of spatial alternation tasks and the hippocampal-prefrontal circuit.Journal: Nature communicationsIn common: PyTorch, SciPy, Matplotlib, 1 other tool, 8 references
- [10] doi:10.7554/elife.106032 [code]
- Neural signatures of model-based and model-free reinforcement learning across prefrontal cortex and striatum.Journal: eLifeIn common: scikit-learn, SciPy, Matplotlib, 1 other tool, 4 references, author Timothy E Behrens
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 48 scripts, and 12 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:4261c225db679a92…
Add the badge to the code's README
If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.
Markdown
<!-- oscr-badge:start -->
[](https://openscicode.org/badge/doi/10.7554/elife.109757)
<!-- oscr-badge:end -->reStructuredText
.. oscr-badge:start
.. image:: https://openscicode.org/badge/doi/10.7554/elife.109757.svg
:target: https://openscicode.org/badge/doi/10.7554/elife.109757
:alt: OSCR
.. oscr-badge:endHTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.7554/elife.109757"><img src="https://openscicode.org/badge/doi/10.7554/elife.109757.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".
If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.
Report a mismatch between the code and the paper · Report a code error (signed in)
Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.
Report a reproduction failure (signed in)
Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
