voxel mountain

学校は最悪なこともある。多くの人がそう思っている。でも、学校は学ぶための最良の方法ではない。伝統的な学校はたった200年前に始まっただけで、人類は何千年も存在していた。学校は個々の興味に合わないことも多い、硬直したカリキュラムを押し付ける。『無駄』なことを強制的に学ばせるのは自然な好奇心を殺す。自分で学ぶことは、自分の情熱や目的を発見する助けになる—学校はむしろその逆をすることが多い。授業で受け身で聞くだけよりも、実際に手を動かして学ぶ方がはるかに効果的。宿題や成績は余計なストレスを生み、学ぶことが義務のように感じられてしまう。好奇心があると、強制的に暗記するよりずっと速く情報を吸収できる。独学で得たスキル(例えばプログラミング!)は充実したキャリアに繋がることが多い。自由に探求することで創造力が育まれる—これは学校ではよく抑えられてしまう。詳しくはこちら: なぜ遊ぶ本能を解放すると子どもはより幸せで自立し、長く良い学習者になるのか
啓蒙時代後の自己教育
学生の教育の自由の権利
教育に反対する理由
ホームスクーリングに関する研究レビュー
if you want to know more about this, go to https://glub.chat and talk to @aswq
script
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')

lr = 0.01
adaptrate = 0.03

class DenseConnection:
	def __init__(self, w,scale):
		self.w=w
		self.scale = scale
	def make(sender,receiver,scale=1.0):
		return DenseConnection(torch.rand(sender.size,receiver.size),scale/sender.size)
	def make2(sender,receiver,scale=1.0):
		w=torch.rand(sender.size,receiver.size)
		return DenseConnection(w,scale/sender.size),DenseConnection(w.T,scale/receiver.size)
	def forward(self, x): return [email protected]*self.scale
	def updateWeight(self, sender,receiver):
		change = sender.act[:,None]*receiver.act[None,:] - sender.minusPhaseAct[:,None]*receiver.minusPhaseAct[None,:]
		self.w += change*lr
		self.w.clamp(0,1)
		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,:] - senderMinusPhase[:,None]*receiver.minusPhaseAct[None,:]
		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, w, 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
		"""
		self.w = w
		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.neighborhood_offsets = [(dx, dy) for dx in [-neighborhood_size, neighborhood_size+1] for dy in [-neighborhood_size, neighborhood_size+1]]
		# Fix: neighborhood_offsets should be correct
		self.neighborhood_offsets = []
		for dx in range(-neighborhood_size, neighborhood_size+1):
			for dy in range(-neighborhood_size, neighborhood_size+1):
				self.neighborhood_offsets.append((dx, dy))

	@classmethod
	def make(cls, sender, receiver, 
			 stride_x=1, stride_y=1, scale=1.0, neighborhood_size=1):
		"""
		Creates a local pool connection.
		"""
		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 = round(rx * stride_x) + dx
					spy = round(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 if avg_fan_in > 0 else scale
		
		return cls(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)

	@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.w.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 = conn.scale / (conn.scale / (conn.w.shape[0] / total_new_receiver_neurons if total_new_receiver_neurons > 0 else 1)) 
		# Simpler: use the same scale factor logic
		scale_factor = conn.scale / avg_fan_in if avg_fan_in > 0 else conn.scale
		
		new_offsets = conn.neighborhood_offsets
		
		return conn,cls(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,
				   conn.stride_x, conn.stride_y, conn.neighborhood_size)

	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):
		"""
		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 - change_minus
		
		self.w += change * lr
		self.w.clamp_(0, 1)
		
		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.w) * target_diff_vals.abs() * adaptrate
		self.w += target_update

	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 - change_minus
		
		self.w += change * lr
		self.w.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.w) * target_diff_vals.abs() * adaptrate
			self.w += target_update

class Layer:
	def __init__(self, shape):
		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.minusPhaseAct=None
		self.act=None
		self.prevAct=torch.zeros(self.size)
		self.avgAct = torch.zeros(self.size)
	def update(self, input):
		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):
		self.avgAct += (self.act-self.avgAct)*adaptrate
		inPoolDim=len(self.shape)-1
		self.target = self.avgAct.reshape(self.shape).mean(inPoolDim,keepdim=True).expand(self.shape).flatten()
		self.targetDiff = self.target - self.avgAct

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

E1=Layer((8,8,1))

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

inpp=Layer(inpLayer.shape)
V1p=Layer(V1.shape)
V2p=Layer(V2.shape)
V3p=Layer(V3.shape)
V4p=Layer(V4.shape)

inpToV1,V1ToInp=LocalPoolConnection.make2(inpLayer,V1, scale=8.0)
v1ToV2,V2ToV1=LocalPoolConnection.make2(V1,V2, scale=8.0)
v2ToV3,V3ToV2=LocalPoolConnection.make2(V2,V3, stride_x=2,stride_y=2, scale=8.0)
v3ToV4,V4ToV3=LocalPoolConnection.make2(V3,V4, scale=8.0)

v4ToV3p=LocalPoolConnection.make(V4,V3p, scale=8.0)
v3ToV2p=LocalPoolConnection.make(V3,V2p, stride_x=0.5,stride_y=0.5, scale=8.0)
v2ToV1p=LocalPoolConnection.make(V2,V1p, scale=8.0)
v1ToInpp=LocalPoolConnection.make(V1,inpp, scale=8.0)

inppToV1=LocalPoolConnection.make(inpp,V1, scale=0.1*8.0)
v1pToV2=LocalPoolConnection.make(V1p,V2, scale=0.1*8.0)
v2pToV3=LocalPoolConnection.make(V2p,V3, stride_x=2,stride_y=2, scale=0.1*8.0)
v3pToV4=LocalPoolConnection.make(V3p,V4, scale=0.1*8.0)

v1ToE1,E1ToV1=LocalPoolConnection.make2(V1,E1, scale=1.0)
E1ToV1p=LocalPoolConnection.make(E1,V1p, scale=1.0)
v1pToE1=LocalPoolConnection.make(V1p,E1, scale=0.1*1.0)

v4ToA1,A1ToV4=DenseConnection.make2(V4,A1, scale=32.0)
A1ToV4p=DenseConnection.make(A1,V4p, scale=32.0)
v4pToA1=DenseConnection.make(V4p,A1, scale=0.1*32.0)

i=0

fig, axs = plt.subplots(6,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):
	ax.clear()
	ax.axis('off')
	if v is None: return
	if len(v.shape)==1: v=v.unsqueeze(0)
	if len(v.shape)==3: v=v.flatten(start_dim=1)
	ax.imshow(v, vmin=0, vmax=1)


def draw_frame(time_step, grid_dim=100):
	# Calculate position using sine for smooth oscillation
	x = int((grid_dim / 2) * (1 + torch.sin(time_step * 0.1)))
	y = int((grid_dim / 2) * (1 + torch.cos(time_step * 0.07)))
	
	# Create empty frame
	frame = torch.zeros(grid_dim, grid_dim, 3)
	
	# Draw single pixel
	if 0 <= x < grid_dim and 0 <= y < grid_dim:
		frame[y, x, 0] = 1.0
		
	return frame

  
  
video_sigs=[]
for j in range(1000):
	i+=1
	input_image=next_frame(i)
	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
	)
	v1Input=F.avg_pool2d(v1Input, kernel_size=4, stride=4).permute(0,2,3,1).flatten() / 32.0

	v1pa=V1p.update(v2ToV1p.forward(V2.prevAct)+E1ToV1p.forward(E1.prevAct))
	v2pa=V2p.update(v3ToV2p.forward(V3.prevAct))
	V3p.update(v4ToV3p.forward(V4.prevAct))
	V4p.update(A1ToV4p.forward(A1.prevAct))

	# use layer.act instead of layera in the future
	v1a=V1.update(v1Input)
	v2a=V2.update(v1ToV2.forward(v1a)+v1pToV2.forward(v1pa))
	v3a=V3.update(v2ToV3.forward(v2a)+v2pToV3.forward(v2pa))
	V4.update(v3ToV4.forward(V3.act)+v3pToV4.forward(V3p.act))
	E1.update(v1ToE1.forward(V1.act)+v1pToE1.forward(V1p.act))
	A1.update(v4ToA1.forward(V4.act)+v4pToA1.forward(V4p.act))
	#spread backward so it spreads more completely
	V4.update(A1ToV4.forward(A1.act)+v3ToV4.forward(V3.act)+v3pToV4.forward(V3p.act))
	V3.update(V4ToV3.forward(V4.act)+v2ToV3.forward(V2.act)+v2pToV3.forward(V2p.act))
	v2a=V2.update(V3ToV2.forward(V3.act)+v1ToV2.forward(v1a)+v1pToV2.forward(v1pa))
	v1a=V1.update(E1ToV1.forward(E1.act)+V2ToV1.forward(v2a)+v1Input)

	# do again but clamp p layers

	V1.minusPhaseAct = V1.act
	V2.minusPhaseAct = V2.act
	V3.minusPhaseAct = V3.act
	V4.minusPhaseAct = V4.act
	E1.minusPhaseAct = E1.act
	A1.minusPhaseAct = A1.act

	V1p.minusPhaseAct=V1p.act
	V2p.minusPhaseAct=V2p.act
	V3p.minusPhaseAct=V3p.act
	V4p.minusPhaseAct=V4p.act

	V1p.act=v1a
	V2p.act=v2a
	V3p.act=V3.act
	V4p.act=V4.act

	# use layer.act instead of layera in the future
	v1a=V1.update(v1Input)
	v2a=V2.update(v1ToV2.forward(v1a)+v1pToV2.forward(v1pa))
	v3a=V3.update(v2ToV3.forward(v2a)+v2pToV3.forward(v2pa))
	V4.update(v3ToV4.forward(V3.act)+v3pToV4.forward(V3p.act))
	E1.update(v1ToE1.forward(V1.act)+v1pToE1.forward(V1p.act))
	A1.update(v4ToA1.forward(V4.act)+v4pToA1.forward(V4p.act))
	#spread backward so it spreads more completely
	V4.update(A1ToV4.forward(A1.act)+v3ToV4.forward(V3.act)+v3pToV4.forward(V3p.act))
	V3.update(V4ToV3.forward(V4.act)+v2ToV3.forward(V2.act)+v2pToV3.forward(V2p.act))
	v2a=V2.update(V3ToV2.forward(V3.act)+v1ToV2.forward(v1a)+v1pToV2.forward(v1pa))
	v1a=V1.update(E1ToV1.forward(E1.act)+V2ToV1.forward(v2a)+v1Input)

	#update averages
	V1.slowUpdate()
	V2.slowUpdate()
	V3.slowUpdate()
	V4.slowUpdate()
	E1.slowUpdate()
	A1.slowUpdate()

	V1p.slowUpdate()
	V2p.slowUpdate()
	V3p.slowUpdate()
	V4p.slowUpdate()

	#update weight
	
	v1ToV2.updateWeight(V1,V2)
	V2ToV1.updateWeight(V2,V1)
	v2ToV3.updateWeight(V2,V3)
	V3ToV2.updateWeight(V3,V2)
	v3ToV4.updateWeight(V3,V4)
	V4ToV3.updateWeight(V4,V3)

	v2ToV1p.updateWeightSpecific(V2.prevAct,V2.prevAct,V1p,adaptAct=False)
	v3ToV2p.updateWeightSpecific(V3.prevAct,V3.prevAct,V2p,adaptAct=False)
	v4ToV3p.updateWeightSpecific(V4.prevAct,V3.prevAct,V2p,adaptAct=False)

	v1pToV2.updateWeight(V1p,V2)
	v2pToV3.updateWeight(V2p,V3)
	v3pToV4.updateWeight(V3p,V4)

	v1ToE1.updateWeight(V1,E1)
	E1ToV1.updateWeight(E1,V1)
	E1ToV1p.updateWeightSpecific(E1.prevAct,E1.prevAct,V1p,adaptAct=False)
	v1pToE1.updateWeight(V1p,E1)

	v4ToA1.updateWeight(V4,A1)
	A1ToV4.updateWeight(A1,V4)
	A1ToV4p.updateWeightSpecific(A1.prevAct,A1.prevAct,V4p,adaptAct=False)
	v4pToA1.updateWeight(V4p,A1)

	#update prevAct
	V1.prevAct = V1.act
	V2.prevAct = V2.act
	V3.prevAct = V3.act
	V4.prevAct = V4.act
	E1.prevAct = E1.act
	A1.prevAct = A1.act

	V1p.prevAct = V1p.act
	V2p.prevAct = V2p.act
	V3p.prevAct = V3p.act
	V4p.prevAct = V4p.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[1,0], v1a.view(V1.shape))
		showAt(axs[1,1], v1pa.view(V1p.shape))
		showAt(axs[1,2], (V1.target*10.0).view(V1.shape))
		showAt(axs[2,0], v2a.view(V2.shape))
		showAt(axs[2,1], v2pa.view(V2p.shape))
		showAt(axs[3,0], V3.act.view(V3.shape))
		showAt(axs[3,1], V3p.act.view(V3p.shape))
		showAt(axs[4,0], V4.act.view(V4.shape))
		showAt(axs[4,1], V4p.act.view(V4p.shape))
		showAt(axs[5,0], E1.act.view(E1.shape))
		showAt(axs[5,1], A1.act.view(A1.shape))
		showAt(axs[5,2], A1.avgAct.view(A1.shape)*10.0)
		video_sigs.append(A1.act)
		if len(video_sigs)>50:video_sigs.pop(0)
		if False 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):
    # 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')
		
    if True:
        obj_bitmap = objects[round(i/10-0.5)%len(objects)]
        i=torch.tensor(i)
        rot = i*0.4
        
        # Map position to [-1, 1]
        pos_x_norm = torch.sin(i*0.5)*2.0
        pos_y_norm = torch.sin(i*0.3)*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

plt.imshow(next_frame(100).reshape(32,32))
plt.pause(10000)
  
  

  
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])