File size: 10,130 Bytes
32d978d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
"""
Physics System — Core Knowledge of Intuitive Physics

Hardcoded priors on world dynamics — "believed, not computed."
These are NOT physics simulations; they are the brain's innate
expectations about how the physical world behaves.

Implements:
1. Gravity: Objects fall downward (constant acceleration prior)
2. Friction: Moving objects slow down without force
3. Mass: Heavier objects resist acceleration
4. Elasticity: Objects bounce on collision
5. Support: Unsupported objects fall

Critical for Breakout: ball trajectory prediction without full physics engine.

Reference: Spelke (1990), Baillargeon (1987)
Author: Algorembrant, Rembrant Oyangoren Albeos (2026)
"""

import numpy as np


class PhysicsState:
    """Physical state of an object."""
    
    __slots__ = ['position', 'velocity', 'mass', 'elasticity', 
                 'is_supported', 'radius']
    
    def __init__(self, position: np.ndarray, velocity: np.ndarray = None,
                 mass: float = 1.0, elasticity: float = 0.8, radius: float = 0.5):
        self.position = np.asarray(position, dtype=np.float64)
        self.velocity = np.zeros_like(self.position) if velocity is None else np.asarray(velocity, dtype=np.float64)
        self.mass = mass
        self.elasticity = elasticity
        self.is_supported = False
        self.radius = radius


class PhysicsSystem:
    """
    Innate physics engine — the brain's "believed" physics.
    
    This is NOT a real physics simulator. It's the set of innate
    expectations that infants have about how objects behave.
    Violations of these expectations create surprise signals
    (analogous to infants looking longer at impossible events).
    """
    
    def __init__(self, gravity: float = 9.8, friction: float = 0.02, dt: float = 0.016):
        """
        Args:
            gravity: Gravitational acceleration (downward).
            friction: Kinetic friction coefficient.
            dt: Time step for physics predictions.
        """
        self.gravity = gravity
        self.friction = friction
        self.dt = dt
    
    def predict_trajectory(self, state: PhysicsState, steps: int = 10,
                           bounds: tuple = None) -> list[np.ndarray]:
        """
        Predict the future trajectory of an object using intuitive physics.
        
        This is how the brain predicts where a ball will go in Breakout —
        not by solving equations, but by "feeling" the trajectory based
        on innate gravity, friction, and bounce priors.
        
        Args:
            state: Current physical state of the object.
            steps: Number of future timesteps to predict.
            bounds: Optional (min_pos, max_pos) for bounce boundaries.
            
        Returns:
            List of predicted positions.
        """
        pos = state.position.copy()
        vel = state.velocity.copy()
        trajectory = [pos.copy()]
        
        gravity_vec = np.zeros_like(pos)
        if len(pos) >= 2:
            gravity_vec[1] = self.gravity  # Downward
        
        for _ in range(steps):
            # --- GRAVITY: Objects accelerate downward ---
            if not state.is_supported:
                vel += gravity_vec * self.dt
            
            # --- FRICTION: Moving objects slow down ---
            speed = np.linalg.norm(vel)
            if speed > 0.01:
                friction_force = -self.friction * vel / speed * state.mass
                vel += friction_force * self.dt / state.mass
            
            # --- UPDATE POSITION ---
            pos = pos + vel * self.dt
            
            # --- BOUNCE at boundaries (elasticity) ---
            if bounds is not None:
                min_b, max_b = bounds
                min_b = np.asarray(min_b, dtype=np.float64)
                max_b = np.asarray(max_b, dtype=np.float64)
                for dim in range(len(pos)):
                    if pos[dim] - state.radius < min_b[dim]:
                        pos[dim] = min_b[dim] + state.radius
                        vel[dim] = -vel[dim] * state.elasticity
                    elif pos[dim] + state.radius > max_b[dim]:
                        pos[dim] = max_b[dim] - state.radius
                        vel[dim] = -vel[dim] * state.elasticity
            
            trajectory.append(pos.copy())
        
        return trajectory
    
    def check_support(self, obj_pos: np.ndarray, obj_radius: float,
                      surfaces: list[dict]) -> bool:
        """
        Check if an object is supported by a surface.
        
        Innate prior: unsupported objects fall. Infants expect this.
        
        Args:
            obj_pos: Object center position.
            surfaces: List of dicts with 'y' (surface height), 'x_min', 'x_max'.
            
        Returns:
            True if supported, False if should fall.
        """
        for surface in surfaces:
            surface_y = surface['y']
            x_min = surface.get('x_min', -float('inf'))
            x_max = surface.get('x_max', float('inf'))
            
            # Object is on this surface if:
            # 1. Object bottom is at or below surface level
            # 2. Object center x is within surface extent
            obj_bottom = obj_pos[1] + obj_radius  # y-down convention
            if (abs(obj_bottom - surface_y) < obj_radius * 0.5 and 
                x_min <= obj_pos[0] <= x_max):
                return True
        
        return False
    
    def predict_collision(self, state_a: PhysicsState, state_b: PhysicsState) -> dict:
        """
        Predict if and when two objects will collide.
        
        Innate contact principle: objects cannot pass through each other.
        
        Returns:
            Dict with 'will_collide' (bool), 'time' (float), 'position' (ndarray).
        """
        # Relative position and velocity
        rel_pos = state_b.position - state_a.position
        rel_vel = state_b.velocity - state_a.velocity
        min_dist = state_a.radius + state_b.radius
        
        # Currently overlapping?
        current_dist = np.linalg.norm(rel_pos)
        if current_dist <= min_dist:
            return {
                'will_collide': True,
                'time': 0.0,
                'position': (state_a.position + state_b.position) / 2.0
            }
        
        # Compute closest approach time via quadratic
        a_coeff = np.dot(rel_vel, rel_vel)
        if a_coeff < 1e-10:
            return {'will_collide': False, 'time': float('inf'), 'position': None}
        
        b_coeff = 2.0 * np.dot(rel_pos, rel_vel)
        c_coeff = np.dot(rel_pos, rel_pos) - min_dist**2
        
        discriminant = b_coeff**2 - 4.0 * a_coeff * c_coeff
        if discriminant < 0:
            return {'will_collide': False, 'time': float('inf'), 'position': None}
        
        t1 = (-b_coeff - np.sqrt(discriminant)) / (2.0 * a_coeff)
        t2 = (-b_coeff + np.sqrt(discriminant)) / (2.0 * a_coeff)
        
        t = t1 if t1 > 0 else t2
        if t < 0:
            return {'will_collide': False, 'time': float('inf'), 'position': None}
        
        collision_pos = state_a.position + state_a.velocity * t
        return {
            'will_collide': True,
            'time': float(t),
            'position': collision_pos
        }
    
    def resolve_collision(self, state_a: PhysicsState, 
                          state_b: PhysicsState) -> tuple[np.ndarray, np.ndarray]:
        """
        Resolve a collision between two objects using mass and elasticity priors.
        
        The brain's intuitive collision model: heavier objects push lighter ones,
        and things bounce based on material (elasticity prior).
        
        Returns:
            Tuple of (new_velocity_a, new_velocity_b).
        """
        # Normal vector
        normal = state_b.position - state_a.position
        dist = np.linalg.norm(normal)
        if dist < 1e-8:
            normal = np.array([1.0, 0.0]) if len(state_a.position) == 2 else np.array([1.0, 0.0, 0.0])
        else:
            normal = normal / dist
        
        # Relative velocity along normal
        rel_vel = state_a.velocity - state_b.velocity
        vel_normal = np.dot(rel_vel, normal)
        
        if vel_normal <= 0:
            # Objects already separating
            return state_a.velocity.copy(), state_b.velocity.copy()
        
        # Coefficient of restitution (average elasticity)
        e = (state_a.elasticity + state_b.elasticity) / 2.0
        
        # Impulse magnitude (conservation of momentum + restitution)
        j = -(1.0 + e) * vel_normal / (1.0 / state_a.mass + 1.0 / state_b.mass)
        
        new_vel_a = state_a.velocity + (j / state_a.mass) * normal
        new_vel_b = state_b.velocity - (j / state_b.mass) * normal
        
        return new_vel_a, new_vel_b
    
    def check_violation(self, expected_pos: np.ndarray, observed_pos: np.ndarray,
                        expected_exists: bool, observed_exists: bool) -> dict:
        """
        Check if a physical event violates intuitive physics expectations.
        
        This generates surprise signals — analogous to infant looking-time
        paradigms where babies look longer at "impossible" events.
        
        Returns:
            Dict with violation type and surprise magnitude.
        """
        violations = {}
        
        # Object disappeared (violates permanence + physics)
        if expected_exists and not observed_exists:
            violations['vanishing'] = 1.0
        
        # Object appeared from nowhere
        if not expected_exists and observed_exists:
            violations['spontaneous_generation'] = 0.8
        
        # Teleportation (violates continuity)
        if expected_exists and observed_exists:
            displacement = np.linalg.norm(
                np.asarray(observed_pos) - np.asarray(expected_pos)
            )
            if displacement > 10.0:  # Unreasonable jump
                violations['teleportation'] = min(1.0, displacement / 20.0)
        
        return violations