Mesa 3.0.0b2__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +1 -1
- mesa/agent.py +15 -3
- mesa/batchrunner.py +26 -1
- mesa/examples/README.md +11 -11
- mesa/examples/__init__.py +2 -2
- mesa/examples/advanced/epstein_civil_violence/agents.py +44 -38
- mesa/examples/advanced/epstein_civil_violence/app.py +29 -28
- mesa/examples/advanced/epstein_civil_violence/model.py +33 -65
- mesa/examples/advanced/pd_grid/app.py +9 -5
- mesa/examples/advanced/pd_grid/model.py +1 -1
- mesa/examples/advanced/sugarscape_g1mt/app.py +5 -13
- mesa/examples/advanced/sugarscape_g1mt/model.py +3 -1
- mesa/examples/advanced/wolf_sheep/agents.py +53 -39
- mesa/examples/advanced/wolf_sheep/app.py +37 -19
- mesa/examples/advanced/wolf_sheep/model.py +68 -74
- mesa/examples/basic/boid_flockers/agents.py +49 -18
- mesa/examples/basic/boid_flockers/app.py +2 -2
- mesa/examples/basic/boid_flockers/model.py +55 -19
- mesa/examples/basic/boltzmann_wealth_model/agents.py +23 -5
- mesa/examples/basic/boltzmann_wealth_model/app.py +22 -13
- mesa/examples/basic/boltzmann_wealth_model/model.py +48 -13
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +2 -2
- mesa/examples/basic/conways_game_of_life/app.py +15 -3
- mesa/examples/basic/schelling/agents.py +9 -5
- mesa/examples/basic/schelling/app.py +5 -5
- mesa/examples/basic/schelling/model.py +48 -26
- mesa/examples/basic/virus_on_network/app.py +25 -47
- mesa/experimental/cell_space/cell_collection.py +14 -2
- mesa/experimental/cell_space/discrete_space.py +16 -2
- mesa/experimental/devs/simulator.py +59 -14
- mesa/model.py +4 -4
- mesa/space.py +0 -30
- mesa/time.py +4 -4
- mesa/visualization/__init__.py +17 -6
- mesa/visualization/components/__init__.py +83 -0
- mesa/visualization/components/{altair.py → altair_components.py} +34 -2
- mesa/visualization/components/matplotlib_components.py +175 -0
- mesa/visualization/mpl_space_drawing.py +593 -0
- mesa/visualization/solara_viz.py +156 -67
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/METADATA +6 -8
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/RECORD +46 -44
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/WHEEL +1 -1
- mesa/visualization/components/matplotlib.py +0 -386
- /mesa/visualization/{UserParam.py → user_param.py} +0 -0
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
"""Matplotlib based solara components for visualization MESA spaces and plots."""
|
|
2
|
-
|
|
3
|
-
import warnings
|
|
4
|
-
|
|
5
|
-
import matplotlib.pyplot as plt
|
|
6
|
-
import networkx as nx
|
|
7
|
-
import numpy as np
|
|
8
|
-
import solara
|
|
9
|
-
from matplotlib.cm import ScalarMappable
|
|
10
|
-
from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
|
|
11
|
-
from matplotlib.figure import Figure
|
|
12
|
-
|
|
13
|
-
import mesa
|
|
14
|
-
from mesa.experimental.cell_space import Grid, VoronoiGrid
|
|
15
|
-
from mesa.space import PropertyLayer
|
|
16
|
-
from mesa.visualization.utils import update_counter
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def make_space_matplotlib(agent_portrayal=None, propertylayer_portrayal=None):
|
|
20
|
-
"""Create a Matplotlib-based space visualization component.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
agent_portrayal (function): Function to portray agents
|
|
24
|
-
propertylayer_portrayal (dict): Dictionary of PropertyLayer portrayal specifications
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
function: A function that creates a SpaceMatplotlib component
|
|
28
|
-
"""
|
|
29
|
-
if agent_portrayal is None:
|
|
30
|
-
|
|
31
|
-
def agent_portrayal(a):
|
|
32
|
-
return {"id": a.unique_id}
|
|
33
|
-
|
|
34
|
-
def MakeSpaceMatplotlib(model):
|
|
35
|
-
return SpaceMatplotlib(model, agent_portrayal, propertylayer_portrayal)
|
|
36
|
-
|
|
37
|
-
return MakeSpaceMatplotlib
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@solara.component
|
|
41
|
-
def SpaceMatplotlib(
|
|
42
|
-
model,
|
|
43
|
-
agent_portrayal,
|
|
44
|
-
propertylayer_portrayal,
|
|
45
|
-
dependencies: list[any] | None = None,
|
|
46
|
-
):
|
|
47
|
-
"""Create a Matplotlib-based space visualization component."""
|
|
48
|
-
update_counter.get()
|
|
49
|
-
space_fig = Figure()
|
|
50
|
-
space_ax = space_fig.subplots()
|
|
51
|
-
space = getattr(model, "grid", None)
|
|
52
|
-
if space is None:
|
|
53
|
-
space = getattr(model, "space", None)
|
|
54
|
-
|
|
55
|
-
# https://stackoverflow.com/questions/67524641/convert-multiple-isinstance-checks-to-structural-pattern-matching
|
|
56
|
-
match space:
|
|
57
|
-
case mesa.space._Grid():
|
|
58
|
-
_draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
|
|
59
|
-
case mesa.space.ContinuousSpace():
|
|
60
|
-
_draw_continuous_space(space, space_ax, agent_portrayal, model)
|
|
61
|
-
case mesa.space.NetworkGrid():
|
|
62
|
-
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
63
|
-
case VoronoiGrid():
|
|
64
|
-
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
65
|
-
case Grid(): # matches OrthogonalMooreGrid, OrthogonalVonNeumannGrid, and Hexgrid
|
|
66
|
-
# fixme add a separate draw method for hexgrids in the future
|
|
67
|
-
_draw_discrete_space_grid(space, space_ax, agent_portrayal)
|
|
68
|
-
case None:
|
|
69
|
-
if propertylayer_portrayal:
|
|
70
|
-
draw_property_layers(space_ax, space, propertylayer_portrayal, model)
|
|
71
|
-
|
|
72
|
-
solara.FigureMatplotlib(
|
|
73
|
-
space_fig, format="png", bbox_inches="tight", dependencies=dependencies
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def draw_property_layers(ax, space, propertylayer_portrayal, model):
|
|
78
|
-
"""Draw PropertyLayers on the given axes.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
ax (matplotlib.axes.Axes): The axes to draw on.
|
|
82
|
-
space (mesa.space._Grid): The space containing the PropertyLayers.
|
|
83
|
-
propertylayer_portrayal (dict): Dictionary of PropertyLayer portrayal specifications.
|
|
84
|
-
model (mesa.Model): The model instance.
|
|
85
|
-
"""
|
|
86
|
-
for layer_name, portrayal in propertylayer_portrayal.items():
|
|
87
|
-
layer = getattr(model, layer_name, None)
|
|
88
|
-
if not isinstance(layer, PropertyLayer):
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
data = layer.data.astype(float) if layer.data.dtype == bool else layer.data
|
|
92
|
-
width, height = data.shape if space is None else (space.width, space.height)
|
|
93
|
-
|
|
94
|
-
if space and data.shape != (width, height):
|
|
95
|
-
warnings.warn(
|
|
96
|
-
f"Layer {layer_name} dimensions ({data.shape}) do not match space dimensions ({width}, {height}).",
|
|
97
|
-
UserWarning,
|
|
98
|
-
stacklevel=2,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
# Get portrayal properties, or use defaults
|
|
102
|
-
alpha = portrayal.get("alpha", 1)
|
|
103
|
-
vmin = portrayal.get("vmin", np.min(data))
|
|
104
|
-
vmax = portrayal.get("vmax", np.max(data))
|
|
105
|
-
colorbar = portrayal.get("colorbar", True)
|
|
106
|
-
|
|
107
|
-
# Draw the layer
|
|
108
|
-
if "color" in portrayal:
|
|
109
|
-
rgba_color = to_rgba(portrayal["color"])
|
|
110
|
-
normalized_data = (data - vmin) / (vmax - vmin)
|
|
111
|
-
rgba_data = np.full((*data.shape, 4), rgba_color)
|
|
112
|
-
rgba_data[..., 3] *= normalized_data * alpha
|
|
113
|
-
rgba_data = np.clip(rgba_data, 0, 1)
|
|
114
|
-
cmap = LinearSegmentedColormap.from_list(
|
|
115
|
-
layer_name, [(0, 0, 0, 0), (*rgba_color[:3], alpha)]
|
|
116
|
-
)
|
|
117
|
-
im = ax.imshow(
|
|
118
|
-
rgba_data.transpose(1, 0, 2),
|
|
119
|
-
extent=(0, width, 0, height),
|
|
120
|
-
origin="lower",
|
|
121
|
-
)
|
|
122
|
-
if colorbar:
|
|
123
|
-
norm = Normalize(vmin=vmin, vmax=vmax)
|
|
124
|
-
sm = ScalarMappable(norm=norm, cmap=cmap)
|
|
125
|
-
sm.set_array([])
|
|
126
|
-
ax.figure.colorbar(sm, ax=ax, orientation="vertical")
|
|
127
|
-
|
|
128
|
-
elif "colormap" in portrayal:
|
|
129
|
-
cmap = portrayal.get("colormap", "viridis")
|
|
130
|
-
if isinstance(cmap, list):
|
|
131
|
-
cmap = LinearSegmentedColormap.from_list(layer_name, cmap)
|
|
132
|
-
im = ax.imshow(
|
|
133
|
-
data.T,
|
|
134
|
-
cmap=cmap,
|
|
135
|
-
alpha=alpha,
|
|
136
|
-
vmin=vmin,
|
|
137
|
-
vmax=vmax,
|
|
138
|
-
extent=(0, width, 0, height),
|
|
139
|
-
origin="lower",
|
|
140
|
-
)
|
|
141
|
-
if colorbar:
|
|
142
|
-
plt.colorbar(im, ax=ax, label=layer_name)
|
|
143
|
-
else:
|
|
144
|
-
raise ValueError(
|
|
145
|
-
f"PropertyLayer {layer_name} portrayal must include 'color' or 'colormap'."
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def _draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model):
|
|
150
|
-
if propertylayer_portrayal:
|
|
151
|
-
draw_property_layers(space_ax, space, propertylayer_portrayal, model)
|
|
152
|
-
|
|
153
|
-
agent_data = _get_agent_data(space, agent_portrayal)
|
|
154
|
-
|
|
155
|
-
space_ax.set_xlim(0, space.width)
|
|
156
|
-
space_ax.set_ylim(0, space.height)
|
|
157
|
-
_split_and_scatter(agent_data, space_ax)
|
|
158
|
-
|
|
159
|
-
# Draw grid lines
|
|
160
|
-
for x in range(space.width + 1):
|
|
161
|
-
space_ax.axvline(x, color="gray", linestyle=":")
|
|
162
|
-
for y in range(space.height + 1):
|
|
163
|
-
space_ax.axhline(y, color="gray", linestyle=":")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _get_agent_data(space, agent_portrayal):
|
|
167
|
-
"""Helper function to get agent data for visualization."""
|
|
168
|
-
x, y, s, c, m = [], [], [], [], []
|
|
169
|
-
for agents, pos in space.coord_iter():
|
|
170
|
-
if not agents:
|
|
171
|
-
continue
|
|
172
|
-
if not isinstance(agents, list):
|
|
173
|
-
agents = [agents] # noqa PLW2901
|
|
174
|
-
for agent in agents:
|
|
175
|
-
data = agent_portrayal(agent)
|
|
176
|
-
x.append(pos[0] + 0.5) # Center the agent in the cell
|
|
177
|
-
y.append(pos[1] + 0.5) # Center the agent in the cell
|
|
178
|
-
default_size = (180 / max(space.width, space.height)) ** 2
|
|
179
|
-
s.append(data.get("size", default_size))
|
|
180
|
-
c.append(data.get("color", "tab:blue"))
|
|
181
|
-
m.append(data.get("shape", "o"))
|
|
182
|
-
return {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _split_and_scatter(portray_data, space_ax):
|
|
186
|
-
"""Helper function to split and scatter agent data."""
|
|
187
|
-
for marker in set(portray_data["m"]):
|
|
188
|
-
mask = [m == marker for m in portray_data["m"]]
|
|
189
|
-
space_ax.scatter(
|
|
190
|
-
[x for x, show in zip(portray_data["x"], mask) if show],
|
|
191
|
-
[y for y, show in zip(portray_data["y"], mask) if show],
|
|
192
|
-
s=[s for s, show in zip(portray_data["s"], mask) if show],
|
|
193
|
-
c=[c for c, show in zip(portray_data["c"], mask) if show],
|
|
194
|
-
marker=marker,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
199
|
-
graph = space.G
|
|
200
|
-
pos = nx.spring_layout(graph, seed=0)
|
|
201
|
-
nx.draw(
|
|
202
|
-
graph,
|
|
203
|
-
ax=space_ax,
|
|
204
|
-
pos=pos,
|
|
205
|
-
**agent_portrayal(graph),
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _draw_continuous_space(space, space_ax, agent_portrayal, model):
|
|
210
|
-
def portray(space):
|
|
211
|
-
x = []
|
|
212
|
-
y = []
|
|
213
|
-
s = [] # size
|
|
214
|
-
c = [] # color
|
|
215
|
-
m = [] # shape
|
|
216
|
-
for agent in space._agent_to_index:
|
|
217
|
-
data = agent_portrayal(agent)
|
|
218
|
-
_x, _y = agent.pos
|
|
219
|
-
x.append(_x)
|
|
220
|
-
y.append(_y)
|
|
221
|
-
|
|
222
|
-
# This is matplotlib's default marker size
|
|
223
|
-
default_size = 20
|
|
224
|
-
size = data.get("size", default_size)
|
|
225
|
-
s.append(size)
|
|
226
|
-
color = data.get("color", "tab:blue")
|
|
227
|
-
c.append(color)
|
|
228
|
-
mark = data.get("shape", "o")
|
|
229
|
-
m.append(mark)
|
|
230
|
-
return {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
231
|
-
|
|
232
|
-
# Determine border style based on space.torus
|
|
233
|
-
border_style = "solid" if not space.torus else (0, (5, 10))
|
|
234
|
-
|
|
235
|
-
# Set the border of the plot
|
|
236
|
-
for spine in space_ax.spines.values():
|
|
237
|
-
spine.set_linewidth(1.5)
|
|
238
|
-
spine.set_color("black")
|
|
239
|
-
spine.set_linestyle(border_style)
|
|
240
|
-
|
|
241
|
-
width = space.x_max - space.x_min
|
|
242
|
-
x_padding = width / 20
|
|
243
|
-
height = space.y_max - space.y_min
|
|
244
|
-
y_padding = height / 20
|
|
245
|
-
space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
|
|
246
|
-
space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
|
|
247
|
-
|
|
248
|
-
# Portray and scatter the agents in the space
|
|
249
|
-
_split_and_scatter(portray(space), space_ax)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
253
|
-
def portray(g):
|
|
254
|
-
x = []
|
|
255
|
-
y = []
|
|
256
|
-
s = [] # size
|
|
257
|
-
c = [] # color
|
|
258
|
-
|
|
259
|
-
for cell in g.all_cells:
|
|
260
|
-
for agent in cell.agents:
|
|
261
|
-
data = agent_portrayal(agent)
|
|
262
|
-
x.append(cell.coordinate[0])
|
|
263
|
-
y.append(cell.coordinate[1])
|
|
264
|
-
if "size" in data:
|
|
265
|
-
s.append(data["size"])
|
|
266
|
-
if "color" in data:
|
|
267
|
-
c.append(data["color"])
|
|
268
|
-
out = {"x": x, "y": y}
|
|
269
|
-
out["s"] = s
|
|
270
|
-
if len(c) > 0:
|
|
271
|
-
out["c"] = c
|
|
272
|
-
|
|
273
|
-
return out
|
|
274
|
-
|
|
275
|
-
x_list = [i[0] for i in space.centroids_coordinates]
|
|
276
|
-
y_list = [i[1] for i in space.centroids_coordinates]
|
|
277
|
-
x_max = max(x_list)
|
|
278
|
-
x_min = min(x_list)
|
|
279
|
-
y_max = max(y_list)
|
|
280
|
-
y_min = min(y_list)
|
|
281
|
-
|
|
282
|
-
width = x_max - x_min
|
|
283
|
-
x_padding = width / 20
|
|
284
|
-
height = y_max - y_min
|
|
285
|
-
y_padding = height / 20
|
|
286
|
-
space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
|
|
287
|
-
space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
|
|
288
|
-
space_ax.scatter(**portray(space))
|
|
289
|
-
|
|
290
|
-
for cell in space.all_cells:
|
|
291
|
-
polygon = cell.properties["polygon"]
|
|
292
|
-
space_ax.fill(
|
|
293
|
-
*zip(*polygon),
|
|
294
|
-
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
295
|
-
c="red",
|
|
296
|
-
) # Plot filled polygon
|
|
297
|
-
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def _draw_discrete_space_grid(space: Grid, space_ax, agent_portrayal):
|
|
301
|
-
if space._ndims != 2:
|
|
302
|
-
raise ValueError("Space must be 2D")
|
|
303
|
-
|
|
304
|
-
def portray(g):
|
|
305
|
-
x = []
|
|
306
|
-
y = []
|
|
307
|
-
s = [] # size
|
|
308
|
-
c = [] # color
|
|
309
|
-
|
|
310
|
-
for cell in g.all_cells:
|
|
311
|
-
for agent in cell.agents:
|
|
312
|
-
data = agent_portrayal(agent)
|
|
313
|
-
x.append(cell.coordinate[0])
|
|
314
|
-
y.append(cell.coordinate[1])
|
|
315
|
-
if "size" in data:
|
|
316
|
-
s.append(data["size"])
|
|
317
|
-
if "color" in data:
|
|
318
|
-
c.append(data["color"])
|
|
319
|
-
out = {"x": x, "y": y}
|
|
320
|
-
out["s"] = s
|
|
321
|
-
if len(c) > 0:
|
|
322
|
-
out["c"] = c
|
|
323
|
-
|
|
324
|
-
return out
|
|
325
|
-
|
|
326
|
-
space_ax.set_xlim(0, space.width)
|
|
327
|
-
space_ax.set_ylim(0, space.height)
|
|
328
|
-
|
|
329
|
-
# Draw grid lines
|
|
330
|
-
for x in range(space.width + 1):
|
|
331
|
-
space_ax.axvline(x, color="gray", linestyle=":")
|
|
332
|
-
for y in range(space.height + 1):
|
|
333
|
-
space_ax.axhline(y, color="gray", linestyle=":")
|
|
334
|
-
|
|
335
|
-
space_ax.scatter(**portray(space))
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def make_plot_measure(measure: str | dict[str, str] | list[str] | tuple[str]):
|
|
339
|
-
"""Create a plotting function for a specified measure.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
|
|
343
|
-
|
|
344
|
-
Returns:
|
|
345
|
-
function: A function that creates a PlotMatplotlib component.
|
|
346
|
-
"""
|
|
347
|
-
|
|
348
|
-
def MakePlotMeasure(model):
|
|
349
|
-
return PlotMatplotlib(model, measure)
|
|
350
|
-
|
|
351
|
-
return MakePlotMeasure
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
@solara.component
|
|
355
|
-
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
356
|
-
"""Create a Matplotlib-based plot for a measure or measures.
|
|
357
|
-
|
|
358
|
-
Args:
|
|
359
|
-
model (mesa.Model): The model instance.
|
|
360
|
-
measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
|
|
361
|
-
dependencies (list[any] | None): Optional dependencies for the plot.
|
|
362
|
-
|
|
363
|
-
Returns:
|
|
364
|
-
solara.FigureMatplotlib: A component for rendering the plot.
|
|
365
|
-
"""
|
|
366
|
-
update_counter.get()
|
|
367
|
-
fig = Figure()
|
|
368
|
-
ax = fig.subplots()
|
|
369
|
-
df = model.datacollector.get_model_vars_dataframe()
|
|
370
|
-
if isinstance(measure, str):
|
|
371
|
-
ax.plot(df.loc[:, measure])
|
|
372
|
-
ax.set_ylabel(measure)
|
|
373
|
-
elif isinstance(measure, dict):
|
|
374
|
-
for m, color in measure.items():
|
|
375
|
-
ax.plot(df.loc[:, m], label=m, color=color)
|
|
376
|
-
ax.legend(loc="best")
|
|
377
|
-
elif isinstance(measure, list | tuple):
|
|
378
|
-
for m in measure:
|
|
379
|
-
ax.plot(df.loc[:, m], label=m)
|
|
380
|
-
ax.legend(loc="best")
|
|
381
|
-
ax.set_xlabel("Step")
|
|
382
|
-
# Set integer x axis
|
|
383
|
-
ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
|
|
384
|
-
solara.FigureMatplotlib(
|
|
385
|
-
fig, format="png", bbox_inches="tight", dependencies=dependencies
|
|
386
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|