voxel mountain

学校は最悪なこともある。多くの人がそう思っている。でも、学校は学ぶための最良の方法ではない。伝統的な学校はたった200年前に始まっただけで、人類は何千年も存在していた。学校は個々の興味に合わないことも多い、硬直したカリキュラムを押し付ける。『無駄』なことを強制的に学ばせるのは自然な好奇心を殺す。自分で学ぶことは、自分の情熱や目的を発見する助けになる—学校はむしろその逆をすることが多い。授業で受け身で聞くだけよりも、実際に手を動かして学ぶ方がはるかに効果的。宿題や成績は余計なストレスを生み、学ぶことが義務のように感じられてしまう。好奇心があると、強制的に暗記するよりずっと速く情報を吸収できる。独学で得たスキル(例えばプログラミング!)は充実したキャリアに繋がることが多い。自由に探求することで創造力が育まれる—これは学校ではよく抑えられてしまう。詳しくはこちら: なぜ遊ぶ本能を解放すると子どもはより幸せで自立し、長く良い学習者になるのか
啓蒙時代後の自己教育
学生の教育の自由の権利
教育に反対する理由
ホームスクーリングに関する研究レビュー
doesn't work, A1p can't predict in correct position. no categories in A1. LIPp doesn't even predict position correctly. it seems like connection from LIPp to LIPct interferes with learning, but it is needed for LIPct to represent predicted position.
import torch
from torch import tensor
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('macosx')
import math

lr = 0.01
adaptrate = 0.03

class DenseConnection:
	def __init__(self, w,scale):
		self.w=w
		self.scale = scale
		self.off = False
	def make(sender,receiver,scale=1.0):
		con = DenseConnection(torch.rand(sender.size,receiver.size),scale/sender.size)
		receiver.connect(sender,con)
		return con
	def make2(sender,receiver,scale=1.0):
		w=torch.rand(sender.size,receiver.size)
		con = DenseConnection(w,scale/sender.size)
		con2 = DenseConnection(w.T,scale/receiver.size)
		receiver.connect(sender,con)
		sender.connect(receiver,con2)
		return con,con2
	def forward(self, x): return [email protected]*self.scale
	def updateWeight(self, sender,receiver,adaptAct=True):
		change = sender.act[:,None]*receiver.act[None,:]*1.01 - sender.minusPhaseAct[:,None]*receiver.minusPhaseAct[None,:]*0.99
		self.w += change*lr
		self.w.clamp(0,1)
		if adaptAct:
			self.w += (torch.where(receiver.targetDiff[None,:]<0.0, 0.0, 1.0) - self.w) * (receiver.targetDiff[None,:].abs()) * adaptrate
	def updateWeightSpecific(self, senderMinusPhase,senderPlusPhase,receiver,adaptAct=True):
		change = senderPlusPhase[:,None]*receiver.act[None,:]*1.01 - senderMinusPhase[:,None]*receiver.minusPhaseAct[None,:]*0.99
		self.w += change*lr
		self.w.clamp(0,1)
		if adaptAct:
			self.w += (torch.where(receiver.targetDiff[None,:]<0.0, 0.0, 1.0) - self.w) * (receiver.targetDiff[None,:].abs()) * adaptrate


class LocalPoolConnection:
	"""
	Supports different strides, different pool sizes, and different neuron counts 
	in sender and receiver layers.
	
	Structure:
	- Sender: (S_px, S_py, S_neurons)
	- Receiver: (R_px, R_py, R_neurons)
	- Strides: (stride_x, stride_y) in terms of receiver pools per sender pool.
	  Actually, it's easier to define: 
	  Sender pool (i, j) connects to Receiver pools in a local neighborhood.
	  But usually, local connection means Receiver pool (r, c) connects to 
	  Sender pools (r+dx, c+dy).
	  
	Let's define strides as:
	- Sender stride: distance between sender pool centers in terms of sender pool grid.
	- Receiver stride: distance between receiver pool centers.
	
	Actually, standard "local connection" with different strides:
	If stride_x=1, receiver pool r connects to sender pools r, r+1, r+2...
	If stride_x=2, receiver pool r connects to sender pools r*2, r*2+1, r*2+2...
	
	Let's use:
	- Sender grid size: (S_px, S_py)
	- Receiver grid size: (R_px, R_py)
	- Neurons per sender pool: S_n
	- Neurons per receiver pool: R_n
	- Neighborhood size: neighborhood_size (e.g., 1 for 3x3, 2 for 5x5)
	
	For each receiver pool (rx, ry), it connects to sender pools:
	(rx * stride_x + dx, ry * stride_y + dy) for dx, dy in neighborhood
	
	Where stride_x, stride_y are the strides.
	"""
	def __init__(self, lw, sw, scale, sender_indices, receiver_indices, 
				 s_px, s_py, s_neurons, r_px, r_py, r_neurons, 
				 stride_x, stride_y, neighborhood_size=1):
		"""
		Args:
			w: 1D tensor of weights
			scale: scaling factor
			sender_indices: 1D long tensor, sender neuron indices
			receiver_indices: 1D long tensor, receiver neuron indices
			s_px, s_py: sender grid dimensions
			s_neurons: neurons per sender pool
			r_px, r_py: receiver grid dimensions
			r_neurons: neurons per receiver pool
			stride_x, stride_y: strides
			neighborhood_size: size of neighborhood
		"""
		assert float(r_px)*float(stride_x)==float(s_px) and float(r_py)*float(stride_y)==float(s_py), "stride is wrong"
		self.lw = lw
		self.sw = sw
		self.w = lw*sw
		self.scale = scale
		self.sender_indices = sender_indices
		self.receiver_indices = receiver_indices
		self.s_px = s_px
		self.s_py = s_py
		self.s_neurons = s_neurons
		self.r_px = r_px
		self.r_py = r_py
		self.r_neurons = r_neurons
		self.stride_x = stride_x
		self.stride_y = stride_y
		self.neighborhood_size = neighborhood_size
		self.off = False

	@classmethod
	def make(cls, sender, receiver, 
			 stride_x=1, stride_y=1, scale=1.0, neighborhood_size=1):
		"""
		Creates a local pool connection.
		"""
		#center_size_x = 1 if stride_x<1 else int(stride_x)
		#center_size_y = 1 if stride_y<1 else int(stride_y)
		#neighborhood_size_sender_x = neighborhood_size if stride_x<1 else neighborhood_size*stride_x
		#neighborhood_size_sender_y = neighborhood_size if stride_y<1 else neighborhood_size*stride_y
		offsets = [(dx, dy) for dx in range(-neighborhood_size, neighborhood_size+1) 
				   for dy in range(-neighborhood_size, neighborhood_size+1)]
		
		sender_indices_list = []
		receiver_indices_list = []

		r_px, r_py, r_neurons = receiver.shape
		s_px, s_py, s_neurons = sender.shape
		
		for rx in range(r_px):
			for ry in range(r_py):
				for dx, dy in offsets:
					# Sender pool coordinates
					spx = math.floor(rx * stride_x) + dx
					spy = math.floor(ry * stride_y) + dy
					
					if 0 <= spx < s_px and 0 <= spy < s_py:
						# Connect each neuron in sender pool to each neuron in receiver pool
						# This creates a dense local connection within the neighborhood
						# But if s_neurons != r_neurons, we need to handle it.
						# Let's assume full connectivity between sender and receiver neurons in the neighborhood
						# Or pointwise? The original code did pointwise: n sender -> n receiver
						# So we connect sender neuron (spx, spy, n) to receiver neuron (rx, ry, n)
						
						for n in range(s_neurons):
							# Calculate base indices for the pool connection
							base_sender_idx = spx * (s_py * s_neurons) + spy * s_neurons
							base_receiver_idx = rx * (r_py * r_neurons) + ry * r_neurons
							
							# Add indices for all receiver neurons for each sender neuron
							for r_n in range(r_neurons):
								sender_indices_list.append(base_sender_idx + n)
								receiver_indices_list.append(base_receiver_idx + r_n)
		
		sender_indices = torch.tensor(sender_indices_list, dtype=torch.long)
		receiver_indices = torch.tensor(receiver_indices_list, dtype=torch.long)
		
		w = torch.rand(len(sender_indices_list))
		
		# Calculate scale factor
		total_receiver_neurons = r_px * r_py * r_neurons
		fan_in = torch.zeros(total_receiver_neurons, dtype=torch.float)
		fan_in.index_add_(0, receiver_indices, torch.ones_like(w))
		avg_fan_in = fan_in.mean()
		
		scale_factor = scale / avg_fan_in
		
		con = cls(torch.full_like(w,0.5), w, scale_factor, sender_indices, receiver_indices, 
				  s_px, s_py, s_neurons, r_px, r_py, r_neurons, 
				  stride_x, stride_y, neighborhood_size)
		receiver.connect(sender,con)
		return con

	@classmethod
	def make2(cls, sender, receiver,
			 stride_x=1, stride_y=1, scale=1.0, neighborhood_size=1):
		#return DenseConnection.make2( s_px* s_py* s_neurons, r_px* r_py* r_neurons,scale)
		"""
		Creates transposed connection.
		"""
		conn = cls.make(sender, receiver, stride_x, stride_y, scale, neighborhood_size)
		# Transposed: swap sender and receiver
		# But we need to adjust strides and grid sizes
		# If original: receiver pool rx connects to sender pool rx*stride + dx
		# Transposed: sender pool (rx*stride + dx) connects to receiver pool rx
		
		# New receiver (old sender) grid: (s_px, s_py)
		# New sender (old receiver) grid: (r_px, r_py)
		# New strides: inverse of original strides?
		
		# Actually, let's just swap indices and keep the structure.
		# The connectivity graph is reversed.
		
		new_sender_indices = conn.receiver_indices
		new_receiver_indices = conn.sender_indices
		new_w = conn.lw.clone()
		
		# New grid dimensions
		new_r_px, new_r_py = conn.s_px, conn.s_py
		new_s_px, new_s_py = conn.r_px, conn.r_py
		new_r_neurons = conn.s_neurons
		new_s_neurons = conn.r_neurons
		
		# New strides: 
		# If original stride was sx, sy, the transposed stride is 1/sx, 1/sy?
		# Not exactly, because the grid sizes are different.
		# Let's keep the same neighborhood size and offsets.
		
		# Recalculate scale
		total_new_receiver_neurons = new_r_px * new_r_py * new_r_neurons
		fan_in = torch.zeros(total_new_receiver_neurons, dtype=torch.float)
		fan_in.index_add_(0, new_receiver_indices, torch.ones_like(new_w))
		avg_fan_in = fan_in.mean()
		
		scale_factor = scale / avg_fan_in
		
		con2 = cls(torch.full_like(new_w,0.5), new_w, scale_factor, new_sender_indices, new_receiver_indices,
				   new_s_px, new_s_py, new_s_neurons, new_r_px, new_r_py, new_r_neurons,
				   1/conn.stride_x, 1/conn.stride_y, conn.neighborhood_size)
		sender.connect(receiver,con2)
		return conn,con2

	def forward(self, x):
		"""
		Forward pass.
		x: sender activations, shape (S_px * S_py * S_n,)
		Returns: receiver activations, shape (R_px * R_py * R_n,)
		"""
		assert len(x.shape)==1 and x.shape[0]==self.s_px*self.s_py*self.s_neurons, x.shape
		sender_values = x[self.sender_indices]
		weighted_values = self.w * sender_values
		receiver_flat = torch.zeros(self.r_px * self.r_py * self.r_neurons)
		receiver_flat.index_add_(0, self.receiver_indices, weighted_values)
		return receiver_flat * self.scale

	def updateWeight(self, sender, receiver, adaptAct=True):
		"""
		Update weights.
		sender: object with .act, .minusPhaseAct
		receiver: object with .act, .minusPhaseAct, .targetDiff
		"""
	
		sender_plus = sender.act
		sender_minus = sender.minusPhaseAct
		
		receiver_act = receiver.act
		receiver_minus = receiver.minusPhaseAct
		target_diff = receiver.targetDiff
		
		sender_plus_vals = sender_plus[self.sender_indices]
		receiver_act_vals = receiver_act[self.receiver_indices]
		sender_minus_vals = sender_minus[self.sender_indices]
		receiver_minus_vals = receiver_minus[self.receiver_indices]
		
		change_plus = sender_plus_vals * receiver_act_vals
		change_minus = sender_minus_vals * receiver_minus_vals
		change = change_plus*1.01 - change_minus*0.99
		
		self.lw += change * lr
		self.lw.clamp_(0, 1)

		if adaptAct:
			target_diff_vals = target_diff[self.receiver_indices]
			target_mask = torch.where(target_diff_vals < 0, 0.0, 1.0)
			target_update = (target_mask - self.lw) * target_diff_vals.abs() * adaptrate
			self.lw += target_update

		self.w=self.lw*self.sw

	def updateWeightSpecific(self, senderMinusPhase, senderPlusPhase, receiver, adaptAct=True):
		sender_plus = senderPlusPhase
		sender_minus = senderMinusPhase
		
		receiver_act = receiver.act
		receiver_minus = receiver.minusPhaseAct
		target_diff = receiver.targetDiff
		
		sender_plus_vals = sender_plus[self.sender_indices]
		receiver_act_vals = receiver_act[self.receiver_indices]
		sender_minus_vals = sender_minus[self.sender_indices]
		receiver_minus_vals = receiver_minus[self.receiver_indices]
		
		change_plus = sender_plus_vals * receiver_act_vals
		change_minus = sender_minus_vals * receiver_minus_vals
		change = change_plus*1.01 - change_minus*0.99
		
		self.lw += change * lr
		self.lw.clamp_(0, 1)

		if adaptAct:
			target_diff_vals = target_diff[self.receiver_indices]
			target_mask = torch.where(target_diff_vals < 0, 0.0, 1.0)
			target_update = (target_mask - self.lw) * target_diff_vals.abs() * adaptrate
			self.lw += target_update

		self.w=self.lw*self.sw

class SameUnitConnection(LocalPoolConnection):
	""" each unit connects to the same unit index in every pool """
	@classmethod
	def make(cls, sender, receiver, scale=1.0):
		sender_indices_list = []
		receiver_indices_list = []

		r_px, r_py, r_neurons = receiver.shape
		s_px, s_py, s_neurons = sender.shape

		assert r_neurons==s_neurons
		
		for rx in range(r_px):
			for ry in range(r_py):
				for ni in range(r_neurons):
					for sx in range(s_px):
						for sy in range(s_py):
							if sx == rx and sy == ry: continue
							# Calculate base indices for the pool connection
							base_sender_idx = sx * (s_py * s_neurons) + sy * s_neurons
							base_receiver_idx = rx * (r_py * r_neurons) + ry * r_neurons
							
							sender_indices_list.append(base_sender_idx + ni)
							receiver_indices_list.append(base_receiver_idx + ni)
		
		sender_indices = torch.tensor(sender_indices_list, dtype=torch.long)
		receiver_indices = torch.tensor(receiver_indices_list, dtype=torch.long)
		
		w = torch.ones(len(sender_indices_list))
		
		# Calculate scale factor
		total_receiver_neurons = r_px * r_py * r_neurons
		fan_in = torch.zeros(total_receiver_neurons, dtype=torch.float)
		fan_in.index_add_(0, receiver_indices, torch.ones_like(w))
		avg_fan_in = fan_in.mean()
		
		scale_factor = scale / avg_fan_in if avg_fan_in > 0 else scale
		
		con = cls(torch.full_like(w,0.5), w, scale_factor, sender_indices, receiver_indices, 
					s_px, s_py, s_neurons, r_px, r_py, r_neurons, 
					1,1,None)
		receiver.connect(sender,con)
		return con


class Layer:
	def __init__(self, shape, adaptAct=True, target=None, targetSameUnitAllPools=False, poolAvgAct=False):
		self.shape = shape
		if isinstance(shape, tuple):
			prod = 1
			for s in shape: prod *= s
			self.size = prod
		else:
			self.size = shape
			self.shape = (shape,)
		self.inConnections=[]
		self.inConnectionsSender=[]
		self.inConnectionsUsePrevAct=[]
		self.minusPhaseAct=None
		self.act=None
		self.prevAct=torch.zeros(self.size)
		self.avgAct = torch.zeros(self.size)
		self.input = None
		self.adaptAct = adaptAct
		self.target = target
		self.targetSameUnitAllPools = targetSameUnitAllPools
		self.poolAvgAct = poolAvgAct
		self.off=False
	def connect(self, sender, connection, usePrevAct=False):
		if connection in self.inConnections:
			i = self.inConnections.index(connection)
			self.inConnectionsUsePrevAct[i] = usePrevAct
		else:
			self.inConnections.append(connection)
			self.inConnectionsSender.append(sender)
			self.inConnectionsUsePrevAct.append(usePrevAct)
	def update(self):
		if self.off:
			self.act = None
			return
		input = None
		if self.input is not None: input = self.input.clone()
		for connection,sender,usePrevAct in zip(self.inConnections,self.inConnectionsSender,self.inConnectionsUsePrevAct):
			if connection.off or sender.off: continue
			senderAct = sender.prevAct if usePrevAct else sender.act
			if senderAct is not None:
				thisInput = connection.forward(senderAct)
				if input is None: input = thisInput
				else: input += thisInput
		assert input is not None
		feedforwardInhibition = input.mean()*0.7 + input.amax()*0.3
		if len(self.shape)>1 and self.shape[-1]>1:
			inPoolDim=len(self.shape)-1
			inputView = input.reshape(self.shape)
			feedforwardInhibitionPool = inputView.mean(inPoolDim,keepdim=True)*0.7 + inputView.amax(inPoolDim,keepdim=True)*0.3
			feedforwardInhibition = torch.maximum(feedforwardInhibition,feedforwardInhibitionPool.expand(self.shape).flatten())
		self.feedforwardInhibition=feedforwardInhibition
		input = (input - feedforwardInhibition)
		self.act = (input*64.0).tanh_().clamp_(0)
		return self.act
	def updatePlusPhase(self, input):
		self.update(input)
		#input = (input - self.feedforwardInhibition)
		#self.act = (input*64.0).tanh_().clamp_(0)
		return self.act
	def slowUpdate(self):
		if self.off: return
		self.avgAct += (self.act-self.avgAct)*adaptrate
		if self.poolAvgAct:
			assert len(self.shape)==3, "it needs pools to use poolAvgAct"
			# this doesn't prevent pools from becoming too active, but doesn't keep pushing up less active ones at the edges
			self.avgActTarget = self.avgAct.reshape(self.shape).mean(2,keepdim=True).expand(self.shape).flatten()
		else:
			self.avgActTarget = self.avgAct.mean()
		self.targetDiff = self.avgActTarget - self.avgAct
		for connection,sender,usePrevAct in zip(self.inConnections,self.inConnectionsSender,self.inConnectionsUsePrevAct):
			if connection.off or sender.off: continue
			if usePrevAct: connection.updateWeightSpecific(sender.prevAct,sender.prevAct,self,self.adaptAct)
			else: connection.updateWeight(sender,self,self.adaptAct)

inpLayer=Layer((8,8,3))
objVel=Layer((8,8))
V1=Layer((8,8,16))
V2=Layer((8,8,40))
V3=Layer((4,4,40))
V4=Layer((4,4,40))

V1p=Layer(V1.shape, adaptAct=False, target=V1)
V2p=Layer(V1.shape, adaptAct=False, target=V1)
V3p=Layer(V1.shape, adaptAct=False, target=V1)
V4p=Layer(V1.shape, adaptAct=False, target=V1)

V3ct=Layer(V3.shape)
V4ct=Layer(V4.shape)

MtPos=Layer((8,8,1), poolAvgAct=True)
LIP=Layer((8,8,16), poolAvgAct=True)
LIPct=Layer((8,8,16), poolAvgAct=True)
LIPP=Layer((8,8,1), adaptAct=False, target=MtPos)

DP=Layer((10,10))
DPct=Layer((10,10))
DPp=Layer(V1.shape, adaptAct=False, target=V1)

A1=Layer((4,4,40))
A2=Layer((4,4,40))

A1ct=Layer(A1.shape)
A2ct=Layer(A2.shape)
A1p=Layer((4,4,40+16), adaptAct=False, target=[V1,V4], targetSameUnitAllPools=True)
A2p=Layer((4,4,40+16), adaptAct=False, target=[V1,V4], targetSameUnitAllPools=True)

v1ToV2,V2ToV1=LocalPoolConnection.make2(V1,V2, scale=8.0)
v2ToV3,V3ToV2=LocalPoolConnection.make2(V2,V3, stride_x=2,stride_y=2, scale=8.0)
v2ToV4,V4ToV2=LocalPoolConnection.make2(V2,V4, stride_x=2,stride_y=2, scale=8.0)

# helps to make the same units in different pools more similar
SameUnitConnection.make(V2,V2, scale=0.1*8.0)
SameUnitConnection.make(V3,V3, scale=0.1*8.0)
SameUnitConnection.make(V4,V4, scale=0.1*8.0)

V2p.connect(V2,LocalPoolConnection.make(V2,V2p, scale=8.0),usePrevAct=True)
V1p.connect(V1,LocalPoolConnection.make(V1,V1p, neighborhood_size=0, scale=8.0),usePrevAct=True)

V4ct.connect(V4,LocalPoolConnection.make(V4,V4ct, scale=8.0),usePrevAct=True)
V4ct.connect(V4ct,LocalPoolConnection.make(V4ct,V4ct, scale=0.25*8.0),usePrevAct=True)
LocalPoolConnection.make2(V4ct,V4p, stride_x=0.5,stride_y=0.5, scale=8.0)
LocalPoolConnection.make2(V2p,V4p, scale=8.0)

V3ct.connect(V3,LocalPoolConnection.make(V3,V3ct, scale=8.0),usePrevAct=True)
V3ct.connect(V3ct,LocalPoolConnection.make(V3ct,V3ct, scale=0.25*8.0),usePrevAct=True)
LocalPoolConnection.make2(V3ct,V3p, stride_x=0.5,stride_y=0.5, scale=8.0)

LocalPoolConnection.make(V1p,V1, scale=0.1*8.0)
LocalPoolConnection.make(V2p,V2, scale=0.1*8.0)
LocalPoolConnection.make(V3p,V3, stride_x=2,stride_y=2, scale=0.1*8.0)
LocalPoolConnection.make(V4p,V4, stride_x=2,stride_y=2, scale=0.1*8.0)

LocalPoolConnection.make(V1p,V2, scale=0.1*8.0)
LocalPoolConnection.make(V1p,V3, stride_x=2,stride_y=2, scale=0.1*8.0)
LocalPoolConnection.make(V1p,V4, stride_x=2,stride_y=2, scale=0.1*8.0)

LocalPoolConnection.make(V4ct,V3ct, scale=8.0)
LocalPoolConnection.make(V3ct,V2p, stride_x=0.5,stride_y=0.5, scale=8.0)
LocalPoolConnection.make(V2p,V1p, scale=8.0)

v1ToMtPos=LocalPoolConnection.make(V1,MtPos, scale=1.0, neighborhood_size=0)
MtPosToLIP=LocalPoolConnection.make(MtPos, LIP, scale=1.0, neighborhood_size=0)
DenseConnection.make(objVel,LIP, scale=1.0)
LIPct.connect(LIP,LocalPoolConnection.make(LIP,LIPct, scale=8.0),usePrevAct=True)
DenseConnection.make(objVel,LIPct, scale=8.0)
DenseConnection.make(LIPct, LIPP, scale=8.0)
LocalPoolConnection.make(LIPP, LIPct, scale=0.5*8.0) #back connection, helps LIPct represent new position
LocalPoolConnection.make(LIPP,LIP, scale=0.1*1.0, neighborhood_size=0)

LIPToV2=LocalPoolConnection.make(LIP,V2, scale=8.0, neighborhood_size=0)
LIPToV3=LocalPoolConnection.make(LIP,V3, stride_x=2,stride_y=2, scale=8.0, neighborhood_size=0)

LocalPoolConnection.make(LIPct,V1p, scale=8.0)#todo change to LIPct
LocalPoolConnection.make(LIPct,V2p, scale=8.0)
LocalPoolConnection.make(LIPct,V3ct, stride_x=2,stride_y=2, scale=8.0) # larger neighborhood_size because the connection is not centered currently
#LocalPoolConnection.make(MtPos,V4p, stride_x=2,stride_y=2, neighborhood_size=2, scale=8.0)
#LocalPoolConnection.make(MtPos,A1ct, stride_x=2,stride_y=2, neighborhood_size=2, scale=8.0)

DenseConnection.make2(V3,DP, scale=8.0)
DenseConnection.make(DPp,DP, scale=0.1*8.0)
DenseConnection.make(V3p,DP, scale=0.1*8.0)
DenseConnection.make(A1,DP, scale=8.0)
DenseConnection.make(A1p,DP, scale=0.1*8.0)
DPct.connect(DP,DenseConnection.make(DP,DPct, scale=8.0), usePrevAct=True)
DenseConnection.make(A1ct,DPct, scale=8.0)
DenseConnection.make2(DPct,DPp, scale=8.0)
DenseConnection.make(A1ct,DPp, scale=8.0)
DenseConnection.make2(V3ct,DPp, scale=8.0)
DenseConnection.make(DPp,V3, scale=0.1*8.0)
DenseConnection.make(DPct,V3ct, scale=8.0)
DenseConnection.make(DP,V3ct, scale=8.0)
DenseConnection.make(DPct,V3p, scale=8.0)

v4ToA1,A1ToV4=DenseConnection.make2(V4,A1, scale=8.0) # almost full connection
LocalPoolConnection.make(A1p,A1, neighborhood_size=2, scale=0.1*8.0)
LocalPoolConnection.make(V1p,A1, stride_x=2,stride_y=2, neighborhood_size=2, scale=0.1*8.0)
A1ct.connect(A1,LocalPoolConnection.make(A1,A1ct, scale=8.0),usePrevAct=True)
A1ct.connect(A1ct,LocalPoolConnection.make(A1ct,A1ct, neighborhood_size=0, scale=8.0),usePrevAct=True)
LocalPoolConnection.make2(V4ct,A1p, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(V3ct,A1p, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make2(A1ct,A1p, scale=8.0)
LocalPoolConnection.make2(A2ct,A1p, scale=8.0)
LocalPoolConnection.make(A1p,A1, scale=0.1*8.0)

LocalPoolConnection.make(A1ct,V4p, stride_x=0.5,stride_y=0.5, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(A1ct,V3, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(A1ct,V3p, stride_x=0.5,stride_y=0.5, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(A1ct,V2, stride_x=0.5,stride_y=0.5, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make2(A1ct,V1p, stride_x=0.5,stride_y=0.5, neighborhood_size=2, scale=8.0)

LocalPoolConnection.make(A1,V3ct, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(A1,V2p, stride_x=0.5,stride_y=0.5, neighborhood_size=2, scale=8.0)

LocalPoolConnection.make2(A1,A2, scale=8.0)
LocalPoolConnection.make(A2p,A2, neighborhood_size=2, scale=0.1*8.0)
LocalPoolConnection.make(V1p,A2, stride_x=2,stride_y=2, neighborhood_size=2, scale=0.1*8.0)
A2ct.connect(A2,LocalPoolConnection.make(A2,A2ct, scale=8.0),usePrevAct=True)
A2ct.connect(A2ct,LocalPoolConnection.make(A2ct,A2ct, neighborhood_size=0, scale=8.0),usePrevAct=True)
LocalPoolConnection.make2(V4ct,A2p, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make(V3ct,A2p, neighborhood_size=2, scale=8.0)
LocalPoolConnection.make2(A1ct,A2p, scale=8.0)

LocalPoolConnection.make2(A2ct,A1ct, scale=8.0)

i=0

fig, axs = plt.subplots(7,3)
plt.tight_layout(pad=0)
plt.subplots_adjust(wspace=0.05, hspace=0.05)
for ax in axs.flat: ax.tick_params(labelbottom=False, labelleft=False)
def showAt(ax, v, title=None, vmin=0,vmax=1):
	ax.clear()
	ax.axis('off')
	if v is None or v is False: return
	if hasattr(v,"minusPhaseAct"):
		if v.minusPhaseAct is None: return
		v = v.minusPhaseAct.reshape(v.shape)
	if len(v.shape)==1: v=v.unsqueeze(0)
	if len(v.shape)==3: v=v.flatten(1)
	ax.imshow(v, vmin=vmin, vmax=vmax)
	if title: ax.set_title(title)


  

for layer in [V2,V2p,V3,V3ct,V3p,V4,V4ct,V4p,DP,DPct,DPp,A1,A1ct,A1p,A2,A2ct,A2p]:
		layer.off = True
		layer.prevAct=torch.zeros(layer.size)
  
  
  
video_sigs=[]
for j in range(1000):
	i+=1
	input_image,input_velx,input_vely=next_frame(i,moveSpeed=1.0)
	v1Input=F.conv2d(
    input=input_image,
    weight=gabor_filters_tensor,
    bias=None,
    stride=1,
    padding=4 # (9-1)/2 = 4, to keep spatial dimensions same
	)
	V1.input=F.avg_pool2d(v1Input, kernel_size=4, stride=4).permute(0,2,3,1).flatten() / 8.0

	objVel.act=objVel.minusPhaseAct=gaussian_blob_tensor((8,8),input_velx*4+4,input_vely*4+4,2).flatten()

	for layer in [V1,V1p,V2,V2p,V3,V3ct,V3p,V4,V4ct,V4p,MtPos,LIP,LIPct,LIPP,DP,DPct,DPp,A1,A1ct,A1p,A2,A2ct,A2p]:
		layer.act = None

	#V1.update();MtPos.update();LIP.update()# temp
	V1.update()
	MtPos.input = V1.act.reshape(V1.shape).mean(2).flatten()/4.0 # normal connection doesn't work because the ones at the edge are less active, so they get pushed up and become too strong when active
	
	# update ct and p layers first because they use prevAct
	for layer in [A1ct,A2ct,A1ct,DPct,LIPct,V4ct,V3ct]:
		layer.update()
	for layer in [A2p,A1p,DPp,LIPP,V4p,V3p,V2p,V1p]:
		layer.update()

	#forward
	for layer in [V1,V2,V3,V4,MtPos,LIP,A1,A2,DP]:
		layer.update()
	# update p layers because some connect to super layers which were just updated. again update ct because it depends on lower p layers, and for backwards from same level p to ct
	for layer in [A1ct,A2ct,A1ct,DPct,LIPct,V4ct,V3ct]:
		layer.update()
	for layer in [A2p,A1p,DPp,LIPP,V4p,V3p,V2p,V1p]:
		layer.update()
	#spread backward so it spreads more completely
	for layer in [A1,V4,V3,V2,V1]:
		layer.update()
	#forward again
	for layer in [V1,V2,V3,V4,MtPos,LIP,A1,A2,DP]:
		layer.update()

	# do again but clamp p layers

	for layer in [V1,V1p,V2,V2p,V3,V3ct,V3p,V4,V4ct,V4p,MtPos,LIP,LIPct,LIPP,DP,DPct,DPp,A1,A1ct,A1p,A2,A2ct,A2p]:
		layer.minusPhaseAct = layer.act

	for layer in [V1p,V2p,V3p,LIPP,DPp,A1p,A2p]:
		if layer.targetSameUnitAllPools:
			assert len(layer.shape) == 3
			offset = 0
			newAct = torch.zeros(layer.shape)
			for target in layer.target:
				assert len(target.shape) == 3
				if target.act is None: continue
				targetView = target.act.reshape(target.shape)
				sameUnitAllPools = targetView.mean((0,1), keepdim=True)#*0.5 + targetView.amax((0,1), keepdim=True)*0.5

				# this is kind of wrong way to do it, but its simpler
				feedforwardInhibition = sameUnitAllPools.mean()*0.7 + sameUnitAllPools.amax()*0.3
				sameUnitAllPools = (sameUnitAllPools - feedforwardInhibition)
				sameUnitAllPools=(sameUnitAllPools*64.0).tanh_().clamp_(0)

				newAct[:,:,offset:offset+target.shape[2]] = sameUnitAllPools
				offset += target.shape[2]
			layer.act = newAct.flatten()
		else:
			layer.act = layer.target.act

	# also update ct layers without updating p layers
	for layer in [A1ct,A2ct,A1ct,DPct,LIPct,V4ct,V3ct]:
		layer.update()

	#forward
	for layer in [V1,V2,V3,V4,MtPos,LIP,A1,A2,DP]:
		layer.update()
	# again update ct because it depends on lower layers
	for layer in [A2ct,A1ct,A2ct,DPct,LIPct,V4ct,V3ct]:
		layer.update()
	#spread backward so it spreads more completely
	for layer in [A1,V4,V3,V2,V1]:
		layer.update()
	#forward again
	for layer in [V1,V2,V3,V4,MtPos,LIP,A1,A2,DP]:
		layer.update()

	#update weight
	for layer in [V1,V1p,V2,V2p,V3,V3ct,V3p,V4,V4ct,V4p,MtPos,LIP,LIPct,LIPP,DP,DPct,DPp,A1,A1ct,A1p,A2,A2ct,A2p]:
		layer.slowUpdate()

	#update prevAct
	for layer in [V1,V1p,V2,V2p,V3,V3ct,V3p,V4,V4ct,V4p,MtPos,LIP,LIPct,LIPP,DP,DPct,DPp,A1,A1ct,A1p,A2,A2ct,A2p]:
		layer.prevAct = layer.act

	if j>0:
		#showAt(axs[0,0], input_image.view(32,32))
		#showAt(axs[0,0], inp.view(inpLayer.shape))
		#showAt(axs[0,1], inppa.view(inpp.shape))
		#showAt(axs[0,2], -10*inpp.targetDiff.view(inpp.shape))
		showAt(axs[0,0], V1)
		showAt(axs[0,1], V1p)
		showAt(axs[0,2], V1p.minusPhaseAct is not None and (V1p.act.view(V1p.shape).amax(dim=2)-V1p.minusPhaseAct.view(V1p.shape).amax(dim=2)), None, -1,1)
		axs[0,2].text(0.1,0.1, f"err: {F.mse_loss(V1p.minusPhaseAct,V1p.act).item()}")
		#conw=torch.zeros(V1.size)
		#contoshow=MtPos.inConnections[0]
		#selw=contoshow.receiver_indices==36
		#conw=contoshow.w[selw]#;conw=(conw-conw.mean())*10+0.5
		showAt(axs[1,0], V2)
		#showAt(axs[1,0], conw.reshape(1,-1))
		showAt(axs[1,1], V2p)
		showAt(axs[1,2], V3p.minusPhaseAct is not None and (V3p.act.view(V3p.shape).amax(dim=2)-V3p.minusPhaseAct.view(V3p.shape).amax(dim=2)), None, -1,1)
		showAt(axs[2,0], V3)
		showAt(axs[2,1], V3p)
		showAt(axs[2,2], V4p,"v4p")
		showAt(axs[3,0], V4)
		showAt(axs[3,1], A1p,"A1p")
		showAt(axs[3,2], A2p,"A2p")
		showAt(axs[4,0], MtPos,"MtPos")
		#showAt(axs[4,1], V1.act.view(8,8,16).mean(2),"should be")
		showAt(axs[4,2], LIPP,"LIPP")
		#V1.act.view(8,8,16).mean(2)
		showAt(axs[4,1], LIPct,"LIPct")
		showAt(axs[5,2], LIP,"LIP")
		showAt(axs[5,0], A1,"A1")
		showAt(axs[5,1], A2,"A2")
		showAt(axs[5,1], objVel,"objVel")
		#showAt(axs[5,2], A1p.minusPhaseAct is not None and (A1p.act.view(A1p.shape).amax(dim=2)-A1p.minusPhaseAct.view(A1p.shape).amax(dim=2)), None, -1,1)
		#showAt(axs[4,0], A1ct,"A1ct")
		#showAt(axs[4,1], A2ct,"A2ct")
		showAt(axs[6,0], DP,"DP")
		showAt(axs[6,1], DPct,"DPct")
		showAt(axs[6,2], DPp,"DPp")
		if A1.act is not None: video_sigs.append(A1.act)
		if len(video_sigs)>50:video_sigs.pop(0)
		if 1 and len(video_sigs) >= 2:
			sigs = torch.stack(video_sigs)
			sigs = sigs / sigs.norm(dim=1, keepdim=True).clamp(min=1e-8)
			sim = (sigs @ sigs.T).numpy()
			axs[4,2]
			axs[4,2].imshow(sim, vmin=-1, vmax=1, cmap='RdBu', aspect='auto')
		plt.pause(1)
plt.pause(100000)
  
  
  
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt

def make_obj(rows):
    return torch.tensor(rows, dtype=torch.float32)

objects = [
    # Pyramid 1
    make_obj([
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [0,0,0,1,1,0,0,0],
        [0,0,1,1,1,1,0,0],
        [0,1,1,1,1,1,1,0],
        [1,1,1,1,1,1,1,1]
    ]),
    # Round 1
    make_obj([
        [0,0,0,1,1,0,0,0],
        [0,0,1,1,1,1,0,0],
        [0,1,1,1,1,1,1,0],
        [1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1],
        [0,1,1,1,1,1,1,0],
        [0,0,1,1,1,1,0,0],
        [0,0,0,1,1,0,0,0]
    ]),
    # Box 1
    make_obj([
        [1,1,1,1,1,1,1,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,1],
        [1,0,0,0,0,0,0,1],
        [1,1,1,1,1,1,1,1]
    ]),
    # Horizontal 1
    make_obj([
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [1,1,1,1,1,1,1,1],
        [1,1,1,1,1,1,1,1],
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0]
    ]),
]

def next_frame(i, moveSpeed=1.0):
    # Grid for output frame coordinates normalized to [-1, 1]
    y_out = torch.linspace(-4, 4, 32)
    x_out = torch.linspace(-4, 4, 32)
    y_out_grid, x_out_grid = torch.meshgrid(y_out, x_out, indexing='ij')
	
    objNum = round(i/10-0.5)%len(objects)
    obj_bitmap = objects[objNum]
    i=torch.tensor(i+objNum*33)
    rot = i*0.4
    
    # Map position to [-1, 1]
    pos_x_norm = torch.sin(i*0.5*moveSpeed)*2.0
    pos_y_norm = torch.sin(i*0.3*moveSpeed)*2.0

    velx = pos_x_norm - torch.sin((i-1)*0.5*moveSpeed)*2.0
    vely = pos_y_norm - torch.sin((i-1)*0.3*moveSpeed)*2.0
    
    # Relative coordinates in frame space
    x_rel = x_out_grid - pos_x_norm
    y_rel = y_out_grid - pos_y_norm
    
    # Inverse rotation to map frame coordinates to object local space
    cos_neg = torch.cos(-rot)
    sin_neg = torch.sin(-rot)
    
    x_local = x_rel * cos_neg - y_rel * sin_neg
    y_local = x_rel * sin_neg + y_rel * cos_neg
    
    # Normalize local coordinates to [-1, 1] for grid_sample
    # Assuming the object bitmap is centered at 0 and scaled to fit [-1, 1]
    # Since the bitmap is 8x8, we assume it maps 0..8 to -1..1
    x_norm = x_local
    y_norm = y_local
    
    grid = torch.stack([x_norm, y_norm], dim=-1).unsqueeze(0)
    obj_expanded = obj_bitmap.unsqueeze(0).unsqueeze(0)
    
    frame = F.grid_sample(
        obj_expanded,
        grid,
        mode='bilinear',
        padding_mode='zeros',
        align_corners=True
    )
    
    frame = torch.clamp(frame, 0.0, 1.0)
    return frame, velx,vely

#plt.imshow(next_frame(100).reshape(32,32))

def gaussian_blob_tensor(shape, mean_x, mean_y, sigma):
    """
    Generates a 2D tensor with a Gaussian blob.
    
    Args:
        shape (tuple): The spatial dimensions of the output tensor (height, width).
        mean_x (float): The x-coordinate (column) of the center.
        mean_y (float): The y-coordinate (row) of the center.
        sigma (float): The standard deviation (width) of the Gaussian.
        
    Returns:
        torch.Tensor: A 2D tensor of shape (height, width) with values from 
                      a normalized Gaussian distribution centered at (mean_x, mean_y).
    """
    height, width = shape
    
    # Create coordinate grids
    # y corresponds to rows (0 to height-1)
    # x corresponds to columns (0 to width-1)
    y_coords = torch.arange(height, dtype=torch.float32)
    x_coords = torch.arange(width, dtype=torch.float32)
    
    # Create meshgrid
    # yy has shape (height, 1), xx has shape (1, width)
    yy, xx = torch.meshgrid(y_coords, x_coords, indexing='ij')
    
    # Calculate squared distance from the center
    dist_sq = ((xx - mean_x) ** 2 + (yy - mean_y) ** 2)
    
    # Calculate Gaussian values
    # Normalization factor for 2D Gaussian is 1 / (2 * pi * sigma^2)
    # However, often we just want the relative intensities.
    # If you need it to sum to 1, divide by the sum.
    # If you need peak intensity 1, don't normalize by sum.
    
    exponent = -dist_sq / (2 * sigma ** 2)
    blob = torch.exp(exponent)
            
    return blob

  
  
import torch
import matplotlib.pyplot as plt

def create_gabor_kernel(length, wavelength, theta, sigma_len, sigma_wd, phase_deg):
    """Generate a Gabor kernel matching the Go code's structure."""
    half_length = length // 2
    
    # Create coordinate tensors
    x = torch.arange(-half_length, half_length + 1, dtype=torch.float32)
    y = torch.arange(-half_length, half_length + 1, dtype=torch.float32)
    xx, yy = torch.meshgrid(x, y, indexing='ij')
    
    # Rotation to align with orientation theta
    theta=torch.tensor(theta)
    cos_theta = torch.cos(theta)
    sin_theta = torch.sin(theta)
    
    nx = xx * cos_theta + yy * sin_theta
    ny = -xx * sin_theta + yy * cos_theta
    
    # Gaussian envelope
    len_norm = 1.0 / (2.0 * sigma_len**2)
    wd_norm = 1.0 / (2.0 * sigma_wd**2)
    gauss = torch.exp(-(len_norm * nx**2 + wd_norm * ny**2))
    
    # Sinusoidal wave
    two_pi_norm = (2.0 * torch.pi) / wavelength
    phase_rad = torch.tensor(phase_deg * torch.pi / 180.0) # Convert degrees to radians
    sin_wave = torch.sin(two_pi_norm * ny + phase_rad)
    
    val = gauss * sin_wave
    return val

filters = []
num_filters = 16
for i in range(num_filters):
    kernel = create_gabor_kernel(9,6,i/16.0*torch.pi*2.0,0.3*9,0.2*9,0)
    filters.append(kernel)

# Stack into [16, 1, 9, 9]
#gabor_filters_tensor = torch.stack(filters).unsqueeze(1)
#input_image=next_frame(1)
#v1Input=F.conv2d(
#    input=input_image,
#    weight=gabor_filters_tensor,
#    bias=None,
#    stride=1,
#    padding=4 # (9-1)/2 = 4, to keep spatial dimensions same
#)
#plt.imshow(v1Input[0,5])