File size: 11,681 Bytes
306ec86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbd8607
306ec86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# boids_streamlit_advanced.py

import streamlit as st
import numpy as np
import plotly.graph_objects as go
import time

# Define the Boid class
class Boid:
    def __init__(self, position, velocity):
        self.position = np.array(position, dtype='float64')
        self.velocity = np.array(velocity, dtype='float64')
        self.acceleration = np.zeros(2, dtype='float64')
        self.history = []

    def update(self, boids, width, height, params):
        self.flock(boids, params)
        self.velocity += self.acceleration
        speed = np.linalg.norm(self.velocity)
        if speed > params['max_speed']:
            self.velocity = (self.velocity / speed) * params['max_speed']
        self.position += self.velocity
        self.acceleration = np.zeros(2, dtype='float64')

        # Screen wrapping
        self.position[0] = self.position[0] % width
        self.position[1] = self.position[1] % height

        # Add current position to history
        if params['draw_trail']:
            self.history.append(self.position.copy())
            if len(self.history) > params['max_history']:
                self.history.pop(0)

    def flock(self, boids, params):
        self.acceleration = np.zeros(2, dtype='float64')
        self.fly_towards_center(boids, params)
        self.avoid_others(boids, params)
        self.match_velocity(boids, params)
        self.limit_speed(params)
        self.avoid_boundaries(width=params['width'], height=params['height'], params=params)

    def fly_towards_center(self, boids, params):
        centering_factor = params['centering_factor']
        center_x = 0.0
        center_y = 0.0
        num_neighbors = 0

        for other in boids:
            if other is not self and self.distance(other) < params['visual_range']:
                center_x += other.position[0]
                center_y += other.position[1]
                num_neighbors += 1

        if num_neighbors > 0:
            center_x /= num_neighbors
            center_y /= num_neighbors
            direction = np.array([center_x, center_y]) - self.position
            self.acceleration += centering_factor * direction

    def avoid_others(self, boids, params):
        min_distance = params['min_distance']
        avoid_factor = params['avoid_factor']
        move = np.zeros(2, dtype='float64')

        for other in boids:
            if other is not self and self.distance(other) < min_distance:
                move += self.position - other.position

        if np.linalg.norm(move) > 0:
            move = move / np.linalg.norm(move) * avoid_factor
            self.acceleration += move

    def match_velocity(self, boids, params):
        matching_factor = params['matching_factor']
        avg_velocity = np.zeros(2, dtype='float64')
        num_neighbors = 0

        for other in boids:
            if other is not self and self.distance(other) < params['visual_range']:
                avg_velocity += other.velocity
                num_neighbors += 1

        if num_neighbors > 0:
            avg_velocity /= num_neighbors
            self.acceleration += matching_factor * (avg_velocity - self.velocity)

    def limit_speed(self, params):
        speed = np.linalg.norm(self.velocity)
        if speed > params['max_speed']:
            self.velocity = (self.velocity / speed) * params['max_speed']

    def avoid_boundaries(self, width, height, params):
        margin = params['boundary_margin']
        turn_factor = params['boundary_turn_factor']

        if self.position[0] < margin:
            self.acceleration[0] += turn_factor
        elif self.position[0] > width - margin:
            self.acceleration[0] -= turn_factor

        if self.position[1] < margin:
            self.acceleration[1] += turn_factor
        elif self.position[1] > height - margin:
            self.acceleration[1] -= turn_factor

    def distance(self, other):
        return np.linalg.norm(self.position - other.position)

# Simulation parameters
params = {
    'num_boids': 100,
    'visual_range': 75.0,
    'min_distance': 20.0,
    'centering_factor': 0.005,
    'avoid_factor': 0.05,
    'matching_factor': 0.05,
    'max_speed': 15.0,
    'draw_trail': False,
    'max_history': 50,
    'width': 800,
    'height': 600,
    'boundary_margin': 100.0,
    'boundary_turn_factor': 0.05
}

# Streamlit sidebar for parameter adjustments
st.sidebar.title("Boids Simulation Parameters")
params['num_boids'] = st.sidebar.slider("Number of Boids", 10, 300, 100)
params['visual_range'] = st.sidebar.slider("Visual Range", 10.0, 200.0, 75.0)
params['min_distance'] = st.sidebar.slider("Minimum Separation Distance", 5.0, 100.0, 20.0)
params['centering_factor'] = st.sidebar.slider("Centering Factor", 0.001, 0.02, 0.005)
params['avoid_factor'] = st.sidebar.slider("Avoidance Factor", 0.01, 0.1, 0.05)
params['matching_factor'] = st.sidebar.slider("Matching Factor", 0.01, 0.1, 0.05)
params['max_speed'] = st.sidebar.slider("Maximum Speed", 5.0, 30.0, 15.0)
params['draw_trail'] = st.sidebar.checkbox("Draw Trails")
if params['draw_trail']:
    params['max_history'] = st.sidebar.slider("Trail Length", 10, 100, 50)
params['boundary_margin'] = st.sidebar.slider("Boundary Margin", 50.0, 300.0, 100.0)
params['boundary_turn_factor'] = st.sidebar.slider("Boundary Turn Factor", 0.01, 0.2, 0.05)

# Simulation screen size
width, height = 800, 600
params['width'] = width
params['height'] = height

# Initialize Boids
boids = []
for _ in range(params['num_boids']):
    position = [np.random.uniform(0, width), np.random.uniform(0, height)]
    angle = np.random.uniform(0, 2 * np.pi)
    velocity = [np.cos(angle), np.sin(angle)]
    boids.append(Boid(position, velocity))

# Plotly graph setup
fig = go.Figure(
    layout=go.Layout(
        xaxis=dict(range=[0, width], autorange=False, showgrid=False, zeroline=False),
        yaxis=dict(range=[0, height], autorange=False, showgrid=False, zeroline=False),
        width=width,
        height=height,
        margin=dict(l=0, r=0, t=0, b=0)
    )
)

# Plot initial positions of Boids
scatter = go.Scatter(
    x=[boid.position[0] for boid in boids],
    y=[boid.position[1] for boid in boids],
    mode='markers',
    marker=dict(size=8, color='blue')
)

fig.add_trace(scatter)

# Trail trace
if params['draw_trail']:
    trail_scatter = go.Scatter(
        x=[],
        y=[],
        mode='lines',
        line=dict(color='rgba(0,0,255,0.2)', width=1),
        showlegend=False
    )
    fig.add_trace(trail_scatter)

# Simplified Title
st.title("Boids Simulation")

# Animation display area
animation_placeholder = st.empty()

# Explanation Section Title
st.header("Mathematical Background of the Boids Algorithm")

# Explanation Section
st.markdown("### **Overview of the Boids Algorithm**")
st.markdown("""
The Boids algorithm, proposed by Craig Reynolds in 1986, is a method for simulating flocking behavior in groups of agents called Boids. Each agent follows simple rules to recreate complex group dynamics. The three fundamental rules are:

1. **Separation**: Maintain a suitable distance from nearby Boids to avoid collisions.
2. **Alignment**: Align velocity with the average velocity of neighboring Boids.
3. **Cohesion**: Move towards the average position of neighboring Boids.
""")

st.markdown("### **Mathematical Model**")
st.markdown("""
The movement of each Boid is represented by its position vector \(\mathbf{p}_i(t)\) and velocity vector \(\mathbf{v}_i(t)\). The position and velocity of Boid \(i\) at time \(t\) are described by the following differential equations:
""")

st.latex(r"""
\frac{d\mathbf{p}_i(t)}{dt} = \mathbf{v}_i(t)
""")
st.latex(r"""
\frac{d\mathbf{v}_i(t)}{dt} = \mathbf{a}_i(t)
""")

st.markdown("""
Here, the acceleration \(\mathbf{a}_i(t)\) is the sum of three forces:
""")
st.latex(r"""
\mathbf{a}_i(t) = \mathbf{a}_{\text{separation}} + \mathbf{a}_{\text{alignment}} + \mathbf{a}_{\text{cohesion}}
""")

st.markdown("#### **1. Separation**")
st.markdown("""
To prevent collisions, the separation force is calculated based on the distance \(d_{ij}\) between Boid \(i\) and its neighboring Boids \(j\):
""")
st.latex(r"""
\mathbf{a}_{\text{separation}} = \sum_{j \in N(i)} \frac{\mathbf{p}_i - \mathbf{p}_j}{d_{ij}^2}
""")
st.markdown("where \(N(i)\) is the set of neighboring Boids around Boid \(i\).")

st.markdown("#### **2. Alignment**")
st.markdown("""
The alignment force encourages Boid \(i\) to match the average velocity \(\mathbf{v}_{\text{avg}}\) of its neighbors:
""")
st.latex(r"""
\mathbf{a}_{\text{alignment}} = \frac{\mathbf{v}_{\text{avg}} - \mathbf{v}_i}{\tau}
""")
st.markdown("where \(\tau\) is a scaling parameter.")

st.markdown("#### **3. Cohesion**")
st.markdown("""
The cohesion force steers Boid \(i\) towards the average position \(\mathbf{C}_{\text{avg}}\) of its neighbors:
""")
st.latex(r"""
\mathbf{a}_{\text{cohesion}} = \frac{\mathbf{C}_{\text{avg}} - \mathbf{p}_i}{\sigma}
""")
st.markdown("where \(\sigma\) is a scaling parameter.")

st.markdown("### **Update Rules**")
st.markdown("""
Each Boid's position and velocity are updated based on discrete time steps \(\Delta t\) as follows:
""")
st.latex(r"""
\mathbf{v}_i(t + \Delta t) = \mathbf{v}_i(t) + \mathbf{a}_i(t) \Delta t
""")
st.latex(r"""
\mathbf{p}_i(t + \Delta t) = \mathbf{p}_i(t) + \mathbf{v}_i(t + \Delta t) \Delta t
""")
st.markdown("""
These equations ensure that each Boid updates its velocity and position based on the combined separation, alignment, and cohesion forces.
""")

st.markdown("### **Additional Features**")
st.markdown("""
This simulation includes the following additional features:

1. **Boundary Avoidance**: When a Boid approaches the edge of the simulation area, it receives a steering force to remain within bounds, preventing it from moving off-screen.
2. **Trail Drawing**: The past positions of each Boid are displayed as trails, allowing visualization of their movement patterns.
""")

st.markdown("### **Parameters and Their Roles**")
st.markdown("""
- **Boundary Margin (\(M\))**: The distance from the edge of the simulation area at which Boids begin to steer away.
- **Boundary Turn Factor (\(\gamma\))**: The strength of the steering force applied when avoiding boundaries.

These parameters allow fine-tuning of Boid behavior near the edges of the simulation area.
""")


# Animation settings
frame_rate = 30  # Frames per second
sleep_time = 1.0 / frame_rate

# Reset button
if st.sidebar.button("Reset Simulation"):
    boids = []
    for _ in range(params['num_boids']):
        position = [np.random.uniform(0, width), np.random.uniform(0, height)]
        angle = np.random.uniform(0, 2 * np.pi)
        velocity = [np.cos(angle), np.sin(angle)]
        boids.append(Boid(position, velocity))

# Animation loop
while True:
    # Update Boids
    for boid in boids:
        boid.update(boids, width, height, params)

    # Update Boids' positions
    scatter.x = [boid.position[0] for boid in boids]
    scatter.y = [boid.position[1] for boid in boids]

    # Update trails
    if params['draw_trail']:
        trail_x = []
        trail_y = []
        for boid in boids:
            trail_x.extend([pos[0] for pos in boid.history])
            trail_y.extend([pos[1] for pos in boid.history])
        trail_scatter.x = trail_x
        trail_scatter.y = trail_y

    # Update the Plotly figure
    fig.data[0].x = scatter.x
    fig.data[0].y = scatter.y
    if params['draw_trail']:
        fig.data[1].x = trail_scatter.x
        fig.data[1].y = trail_scatter.y

    # Display the animation
    animation_placeholder.plotly_chart(fig, use_container_width=True)

    # Control the frame rate
    time.sleep(sleep_time)