Source code for mercurial.atlas.thornton_road_empirical

"""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()