"""Thornton Road poltergeist (P.1b) – persistent environmental impression, no agent."""
import matplotlib.pyplot as plt
import numpy as np
from mercurial.core.neural_field import NeuralField2D
from mercurial.core.pattern_completion import HopfieldPatternCompletion
from mercurial.simulation.engine import SimulationEngine
[docs]
def run_thornton_road():
engine = SimulationEngine(dim=20)
engine.apply_empirical_parameters()
# 1. House environment: 2D neural field (32x32) that stores an impression
nx, ny = 32, 32
house_field = NeuralField2D(nx, ny, dx=0.5, wc_params=engine.wc_params.__dict__)
# 2. Persistent impression: a pattern stored in a Hopfield network
n_neurons = 200
# Create a sparse pattern representing the "stone‑throwing" event
impression_pattern = np.zeros(n_neurons)
impression_pattern[:20] = 1.0 # sparse representation
completion = HopfieldPatternCompletion(
n_neurons=n_neurons,
stored_patterns=[impression_pattern],
tau_a=0.020,
beta_sigmoid=10.0,
hebb_params=engine.hebbian_params,
)
# 3. Simulate the house field over time, with spontaneous recall of the impression
dt = 0.001
t_span = (0.0, 60.0) # simulate 1 minute (stone‑throwing events occur intermittently)
steps = int((t_span[1] - t_span[0]) / dt)
# Record house field activity bursts (mean intensity)
house_activity = []
burst_times = []
# Random number generator for spontaneous recall
rng = np.random.default_rng(42)
# We will trigger the impression pattern at random intervals (average every 5 seconds)
next_trigger = rng.exponential(5.0) # seconds
for step in range(steps):
t = step * dt
# Check if it's time for a spontaneous recall event
if t >= next_trigger:
# Trigger the pattern completion network (recall the impression)
# The recall is initiated by a small random input (environmental fluctuation)
inp = rng.normal(0, 0.01, size=n_neurons)
_ = completion.step(dt, inp)
recalled = completion.r # activity pattern (0..1)
# The recalled pattern biases the house field (as external input)
# Map the pattern to the 2D field (simple: use mean activity as bias)
bias_strength = 5.0 * np.mean(recalled)
# Create a random spatial pattern to simulate a diffuse influence
noise_pattern = rng.normal(0, 0.1, size=(nx, ny))
P_ext_house = bias_strength * (noise_pattern + 0.5)
burst_times.append(t)
# Schedule next trigger (exponential distribution, mean 5 s)
next_trigger += rng.exponential(5.0)
else:
P_ext_house = np.zeros((nx, ny))
house_field.step(dt, P_ext=P_ext_house)
house_activity.append(np.mean(house_field.E))
# Count the number of bursts and compute average interval
num_bursts = len(burst_times)
avg_interval = t_span[1] / num_bursts if num_bursts > 0 else 0
print(f"Number of activity bursts (stone‑throwing events): {num_bursts}")
print(f"Average interval between bursts: {avg_interval:.2f} s")
# Plot house activity over time
time = np.arange(steps) * dt
plt.figure(figsize=(12, 4))
plt.plot(time, house_activity, label="House field activity")
# Mark burst times with vertical lines
for bt in burst_times:
plt.axvline(x=bt, color="red", alpha=0.3, linestyle="--")
plt.xlabel("Time (s)")
plt.ylabel("Mean activity")
plt.title("Thornton Road – Spontaneous activity bursts (persistent impression)")
plt.legend()
plt.grid(True)
plt.show()
return num_bursts
if __name__ == "__main__":
run_thornton_road()