panda-generator 0.1.0__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.
panda/__init__.py
ADDED
panda/generator.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
from typing import List, Optional, Tuple, Dict, Any
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from scipy.spatial import ConvexHull
|
|
7
|
+
from scipy.spatial.distance import pdist, cdist
|
|
8
|
+
from psychopy import visual, core
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PANDA:
|
|
12
|
+
"""PANDA (Python Automating Numerical Dot Algorithm).
|
|
13
|
+
|
|
14
|
+
An open algorithm dedicated to the precise generation and controlled
|
|
15
|
+
parameterization of multi-element dot arrays.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
window (visual.Window): The PsychoPy window in which to display the dot array.
|
|
19
|
+
n_anchor (int, optional): The number of anchor dots. Defaults to 3.
|
|
20
|
+
base_ta (float, optional): The baseline Total Surface Area. Defaults to 30000.0.
|
|
21
|
+
base_cha (float, optional): The baseline Convex Hull Area. Defaults to 120000.0.
|
|
22
|
+
color (str, optional): The color of the dots. Defaults to "black".
|
|
23
|
+
seed (int, optional): The random seed for reproducibility. Defaults to None.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
win (visual.Window): The PsychoPy window instance.
|
|
27
|
+
color (str): The color specified for the rendered dots.
|
|
28
|
+
results (list[dict]): List storing generated dot array parameters and metadata.
|
|
29
|
+
rng (np.random.Generator): NumPy random number generator.
|
|
30
|
+
n_anchor (int): Number of anchor dots used for baseline metrics.
|
|
31
|
+
base_ta (float): Baseline Total Surface Area value.
|
|
32
|
+
base_cha (float): Baseline Convex Hull Area value.
|
|
33
|
+
mean_r_anchor (float): Calculated mean radius of anchor dots.
|
|
34
|
+
base_tp (float): Calculated baseline Total Perimeter.
|
|
35
|
+
base_id (float): Calculated baseline Inter-item Distance.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
window: visual.Window,
|
|
41
|
+
n_anchor: int = 3,
|
|
42
|
+
base_ta: float = 30000.0,
|
|
43
|
+
base_cha: float = 120000.0,
|
|
44
|
+
color: str = "black",
|
|
45
|
+
seed: Optional[int] = None,
|
|
46
|
+
):
|
|
47
|
+
self.win = window
|
|
48
|
+
self.color = color
|
|
49
|
+
self.results: List[Dict[str, Any]] = []
|
|
50
|
+
|
|
51
|
+
self.rng = np.random.default_rng(seed)
|
|
52
|
+
if seed is not None:
|
|
53
|
+
random.seed(seed)
|
|
54
|
+
|
|
55
|
+
self.n_anchor = n_anchor
|
|
56
|
+
self.base_ta = base_ta
|
|
57
|
+
self.base_cha = base_cha
|
|
58
|
+
|
|
59
|
+
self.mean_r_anchor = np.sqrt((self.base_ta / self.n_anchor) / np.pi)
|
|
60
|
+
self.base_tp = self.n_anchor * (2 * np.pi * self.mean_r_anchor)
|
|
61
|
+
self.base_id = (0.51 * np.sqrt(self.base_cha)) + (2 * self.mean_r_anchor)
|
|
62
|
+
|
|
63
|
+
def _get_radii(self, num: int, target_tsa: float, sd_ratio: float = 0.0) -> np.ndarray:
|
|
64
|
+
"""Calculate individual dot radii matching a target Total Surface Area (TSA) and standard deviation ratio.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
num (int): number of dots
|
|
68
|
+
target_tsa (float): target total surface area for the dot array
|
|
69
|
+
sd_ratio (float, optional): standard deviation ratio of the dot sizes. Defaults to 0.0.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
np.ndarray: array of calculated dot radii
|
|
73
|
+
"""
|
|
74
|
+
mean_area = target_tsa / num
|
|
75
|
+
if sd_ratio == 0:
|
|
76
|
+
areas = np.full(num, mean_area)
|
|
77
|
+
else:
|
|
78
|
+
sd_area = mean_area * sd_ratio
|
|
79
|
+
areas = np.random.normal(loc=mean_area, scale=sd_area, size=num)
|
|
80
|
+
areas = np.clip(areas, mean_area * 0.1, None)
|
|
81
|
+
areas *= target_tsa / np.sum(areas)
|
|
82
|
+
return np.sqrt(areas / np.pi)
|
|
83
|
+
|
|
84
|
+
def _get_radii_tp(self, num: int, target_tp: float, sd_ratio: float = 0.0) -> np.ndarray:
|
|
85
|
+
"""Calculate individual dot radii matching a target Total Perimeter (TP) and standard deviation ratio.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
num (int): number of dots
|
|
89
|
+
target_tp (float): target total perimeter for the dot array
|
|
90
|
+
sd_ratio (float, optional): standard deviation ratio of the dot perimeters. Defaults to 0.0.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
np.ndarray: array of calculated dot radii
|
|
94
|
+
"""
|
|
95
|
+
mean_perim = target_tp / num
|
|
96
|
+
if sd_ratio == 0:
|
|
97
|
+
perims = np.full(num, mean_perim)
|
|
98
|
+
else:
|
|
99
|
+
sd_perim = mean_perim * sd_ratio
|
|
100
|
+
perims = np.random.normal(
|
|
101
|
+
loc=mean_perim, scale=sd_perim, size=num
|
|
102
|
+
)
|
|
103
|
+
perims = np.clip(perims, mean_perim * 0.1, None)
|
|
104
|
+
perims *= target_tp / np.sum(perims)
|
|
105
|
+
return perims / (2 * np.pi)
|
|
106
|
+
|
|
107
|
+
def _compute_convex_hull_area(self, positions: np.ndarray) -> float:
|
|
108
|
+
"""Calculate the convex hull area (CHA) formed by the current dot positions.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
positions (np.ndarray): An array of shape (N, 2) representing the x and y coordinates of the dot centers.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
float: The area of the convex hull enclosing the dot positions. Returns 0.0 if there are fewer than 3 points.
|
|
115
|
+
"""
|
|
116
|
+
if len(positions) < 3:
|
|
117
|
+
return 0.0
|
|
118
|
+
return float(ConvexHull(positions).volume)
|
|
119
|
+
|
|
120
|
+
def _compute_inter_distance(self, positions: np.ndarray) -> float:
|
|
121
|
+
"""Compute the mean pairwise inter-individual distance (ID) between dot centers.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
positions (np.ndarray): An array of shape (N, 2) representing the x and y coordinates of the dot centers.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
float: The mean pairwise inter-individual distance between dot centers. Returns 0.0 if there are fewer than 2 points.
|
|
128
|
+
"""
|
|
129
|
+
if len(positions) < 2:
|
|
130
|
+
return 0.0
|
|
131
|
+
return float(np.mean(pdist(positions)))
|
|
132
|
+
|
|
133
|
+
def _resolve_condition_values(self, spatial_type: str, size_type: str) -> Tuple[float, float]:
|
|
134
|
+
"""Retrieve target spatial and size baseline values corresponding to specified condition types.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
spatial_type (str): the spatial condition type ('CH' for Convex Hull, 'ID' for Inter-Distance)
|
|
138
|
+
size_type (str): the size condition type ('TA' for Total Area, 'TP' for Total Perimeter)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Tuple[float, float]: A tuple containing the resolved spatial value and size value for the specified condition types.
|
|
142
|
+
"""
|
|
143
|
+
s_val = self.base_cha if spatial_type == 'CH' else self.base_id
|
|
144
|
+
z_val = self.base_ta if size_type == 'TA' else self.base_tp
|
|
145
|
+
return s_val, z_val
|
|
146
|
+
|
|
147
|
+
def _calculate_dynamic_margin(self, num_elements: int, size_type: str, z_val: float) -> float:
|
|
148
|
+
"""Compute a dynamic safety spacing margin between dots based on their average size.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
num_elements (int): the number of dots in the array
|
|
152
|
+
size_type (str): the size condition type ('TA' for Total Area, 'TP' for Total Perimeter)
|
|
153
|
+
z_val (float): the target size value
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
float: The calculated dynamic margin to be used as a minimum distance factor between dots, ensuring they do not overlap and maintain visual clarity.
|
|
157
|
+
"""
|
|
158
|
+
if size_type == 'TA':
|
|
159
|
+
current_mean_r = np.sqrt((z_val / num_elements) / np.pi)
|
|
160
|
+
else:
|
|
161
|
+
current_mean_r = (z_val / num_elements) / (2 * np.pi)
|
|
162
|
+
return max(15.0, current_mean_r * 0.5)
|
|
163
|
+
|
|
164
|
+
def generate_balanced_set(
|
|
165
|
+
self,
|
|
166
|
+
num_elements: int,
|
|
167
|
+
spatial_type: str,
|
|
168
|
+
size_type: str,
|
|
169
|
+
sd_ratio: float = 0.0,
|
|
170
|
+
min_dist_factor: Optional[float] = None,
|
|
171
|
+
) -> Tuple[List[visual.Circle], List[float], List[List[float]]]:
|
|
172
|
+
"""Generate a spatially balanced array of dots satisfying target geometric and size constraints.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
num_elements (int): the number of dots to generate
|
|
176
|
+
spatial_type (str): the spatial condition type ('CH' for Convex Hull, 'ID' for Inter-Distance)
|
|
177
|
+
size_type (str): the size condition type ('TA' for Total Area, 'TP' for Total Perimeter)
|
|
178
|
+
sd_ratio (float, optional): standard deviation ratio of the dot sizes. Defaults to 0.0.
|
|
179
|
+
min_dist_factor (Optional[float], optional): the minimum distance factor between dots. Defaults to None.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ValueError: If the number of elements is less than 3 for Convex Hull (CH) spatial type.
|
|
183
|
+
RuntimeError: If unable to fit dots in the window while maintaining parameters after multiple attempts.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple[List[visual.Circle], List[float], List[List[float]]]: A tuple containing the generated dot objects, their radii, and their positions.
|
|
187
|
+
"""
|
|
188
|
+
if num_elements < 3 and spatial_type == 'CH':
|
|
189
|
+
raise ValueError(
|
|
190
|
+
"Numerosity of elements must be at least 3 for Convex Hull (CH) spatial type."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
spatial_val, size_val = self._resolve_condition_values(
|
|
194
|
+
spatial_type, size_type
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if min_dist_factor is None:
|
|
198
|
+
min_dist_factor = self._calculate_dynamic_margin(
|
|
199
|
+
num_elements, size_type, size_val
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if size_type == 'TA':
|
|
203
|
+
radii = self._get_radii(num_elements, size_val, sd_ratio)
|
|
204
|
+
else:
|
|
205
|
+
radii = self._get_radii_tp(num_elements, size_val, sd_ratio)
|
|
206
|
+
|
|
207
|
+
max_r = np.max(radii)
|
|
208
|
+
half_win = min(self.win.size) / 2.0
|
|
209
|
+
safe_margin = half_win - max_r - 10.0
|
|
210
|
+
|
|
211
|
+
if spatial_type == 'CH':
|
|
212
|
+
target_spawn_radius = np.sqrt(spatial_val / np.pi)
|
|
213
|
+
else:
|
|
214
|
+
target_spawn_radius = spatial_val * 1.1
|
|
215
|
+
|
|
216
|
+
target_spawn_radius = min(target_spawn_radius, safe_margin)
|
|
217
|
+
|
|
218
|
+
for attempt in range(400):
|
|
219
|
+
positions = []
|
|
220
|
+
success = True
|
|
221
|
+
|
|
222
|
+
for i in range(num_elements):
|
|
223
|
+
found = False
|
|
224
|
+
for _ in range(2000):
|
|
225
|
+
angle = random.uniform(0, 2 * np.pi)
|
|
226
|
+
dist = target_spawn_radius * np.sqrt(random.random())
|
|
227
|
+
new_pos = np.array(
|
|
228
|
+
[np.cos(angle) * dist, np.sin(angle) * dist]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if len(positions) > 0:
|
|
232
|
+
existing_arr = np.array(positions)
|
|
233
|
+
dists = np.linalg.norm(existing_arr - new_pos, axis=1)
|
|
234
|
+
min_allowed = radii[: len(positions)] + radii[i] + min_dist_factor
|
|
235
|
+
if np.any(dists < min_allowed):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
positions.append(new_pos)
|
|
239
|
+
found = True
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
if not found:
|
|
243
|
+
success = False
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
if success:
|
|
247
|
+
positions_np = np.array(positions)
|
|
248
|
+
|
|
249
|
+
if spatial_type == 'CH':
|
|
250
|
+
curr = self._compute_convex_hull_area(positions_np)
|
|
251
|
+
scale = np.sqrt(spatial_val / curr)
|
|
252
|
+
else:
|
|
253
|
+
curr = self._compute_inter_distance(positions_np)
|
|
254
|
+
scale = spatial_val / curr
|
|
255
|
+
|
|
256
|
+
final_positions = positions_np * scale
|
|
257
|
+
|
|
258
|
+
if np.max(np.abs(final_positions)) < safe_margin:
|
|
259
|
+
dists_matrix = cdist(final_positions, final_positions)
|
|
260
|
+
radii_sum = radii[:, None] + radii[None, :]
|
|
261
|
+
np.fill_diagonal(dists_matrix, np.inf)
|
|
262
|
+
|
|
263
|
+
if not np.any(dists_matrix < radii_sum):
|
|
264
|
+
dots = [
|
|
265
|
+
visual.Circle(
|
|
266
|
+
self.win,
|
|
267
|
+
radius=radii[i],
|
|
268
|
+
pos=final_positions[i],
|
|
269
|
+
fillColor=self.color,
|
|
270
|
+
lineColor=self.color,
|
|
271
|
+
edges=128,
|
|
272
|
+
)
|
|
273
|
+
for i in range(num_elements)
|
|
274
|
+
]
|
|
275
|
+
return dots, radii.tolist(), final_positions.tolist()
|
|
276
|
+
|
|
277
|
+
raise RuntimeError(
|
|
278
|
+
f"Error N={num_elements}: Unable to fit dots in window while maintaining parameters."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def log_ground_truth(self, num_elements: int, radii: list, positions: list) -> Dict[str, Any]:
|
|
282
|
+
"""Compute and log exact geometric ground-truth metrics for the generated dot array.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
num_elements (int): the number of dots in the array
|
|
286
|
+
radii (list): a list of radii for each dot
|
|
287
|
+
positions (list): a list of (x, y) coordinates for each dot
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Dict[str, Any]: A dictionary containing the computed ground-truth metrics for the generated dot array, including Total Surface Area (TSA), Total Perimeter (TP), Convex Hull Area (CHA), Inter-Distance (ID), Density, Additive Area (AA), and Mean Occupancy (MO).
|
|
291
|
+
"""
|
|
292
|
+
radii_np = np.array(radii)
|
|
293
|
+
positions_np = np.array(positions)
|
|
294
|
+
|
|
295
|
+
tsa = np.sum(np.pi * radii_np**2)
|
|
296
|
+
tp = np.sum(2 * np.pi * radii_np)
|
|
297
|
+
cha = self._compute_convex_hull_area(positions_np)
|
|
298
|
+
id_val = self._compute_inter_distance(positions_np)
|
|
299
|
+
|
|
300
|
+
density = tsa / cha if cha > 0 else 0.0
|
|
301
|
+
additive_area = np.sum(2 * radii_np)
|
|
302
|
+
mean_occupancy = cha / num_elements if num_elements > 0 else 0.0
|
|
303
|
+
|
|
304
|
+
metrics ={
|
|
305
|
+
'N': num_elements,
|
|
306
|
+
'TSA': round(tsa, 4),
|
|
307
|
+
'TP': round(tp, 4),
|
|
308
|
+
'CHA': round(cha, 4),
|
|
309
|
+
'ID': round(id_val, 4),
|
|
310
|
+
'Density': round(density, 6),
|
|
311
|
+
'AA': round(additive_area, 4),
|
|
312
|
+
'MO': round(mean_occupancy, 4),
|
|
313
|
+
}
|
|
314
|
+
self.results.append(metrics)
|
|
315
|
+
return metrics
|
|
316
|
+
|
|
317
|
+
def generate_and_save(
|
|
318
|
+
self,
|
|
319
|
+
num_elements: int,
|
|
320
|
+
spatial_type: str,
|
|
321
|
+
size_type: str,
|
|
322
|
+
output_filepath: Optional[str] = None,
|
|
323
|
+
sd_ratio: float = 0.0,
|
|
324
|
+
min_dist_factor: Optional[float] = None,
|
|
325
|
+
save_results: bool = True,
|
|
326
|
+
pause_time: float = 0.0,
|
|
327
|
+
) -> List[visual.Circle]:
|
|
328
|
+
"""Generate, render, and display a dot array in PsychoPy, optionally capturing and saving the frame.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
num_elements (int): the number of dots to generate
|
|
332
|
+
spatial_type (str): the spatial condition type ('CH' for Convex Hull, 'ID' for Inter-Distance)
|
|
333
|
+
size_type (str): the size condition type ('TA' for Total Area, 'TP' for Total Perimeter)
|
|
334
|
+
output_filepath (Optional[str], optional): the path to save the generated image. Defaults to None.
|
|
335
|
+
sd_ratio (float, optional): standard deviation ratio of the dot sizes. Defaults to 0.0.
|
|
336
|
+
min_dist_factor (Optional[float], optional): the minimum distance factor between dots. Defaults to None.
|
|
337
|
+
save_results (bool, optional): whether to save the generated results. Defaults to True.
|
|
338
|
+
pause_time (float, optional): the time to pause between frames. Defaults to 0.0.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List[visual.Circle]: the list of generated dot objects
|
|
342
|
+
"""
|
|
343
|
+
dots, radii, positions = self.generate_balanced_set(
|
|
344
|
+
num_elements=num_elements,
|
|
345
|
+
spatial_type=spatial_type,
|
|
346
|
+
size_type=size_type,
|
|
347
|
+
sd_ratio=sd_ratio,
|
|
348
|
+
min_dist_factor=min_dist_factor,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
self.log_ground_truth(num_elements, radii, positions)
|
|
352
|
+
|
|
353
|
+
for dot in dots:
|
|
354
|
+
dot.draw()
|
|
355
|
+
|
|
356
|
+
core.wait(pause_time)
|
|
357
|
+
|
|
358
|
+
self.win.flip()
|
|
359
|
+
self.win.getMovieFrame(buffer='front')
|
|
360
|
+
if save_results and output_filepath:
|
|
361
|
+
self.win.saveMovieFrames(output_filepath)
|
|
362
|
+
return dots
|
|
363
|
+
|
|
364
|
+
def get_dataframe(self) -> pd.DataFrame:
|
|
365
|
+
"""Return all logged stimulus ground-truth metrics as a pandas DataFrame.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
pd.DataFrame: A DataFrame containing all logged ground-truth metrics for the generated dot arrays, including Total Surface Area (TSA), Total Perimeter (TP), Convex Hull Area (CHA), Inter-Distance (ID), Density, Additive Area (AA), and Mean Occupancy (MO).
|
|
369
|
+
"""
|
|
370
|
+
return pd.DataFrame(self.results)
|
|
371
|
+
|
|
372
|
+
def reset_results(self) -> None:
|
|
373
|
+
"""Clear all stored ground-truth metrics from internal memory.
|
|
374
|
+
"""
|
|
375
|
+
self.results.clear()
|
|
376
|
+
|
|
377
|
+
def save_csv(self, filename: str) -> None:
|
|
378
|
+
"""Export all accumulated ground-truth metrics to a CSV file.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
filename (str): The path to the CSV file where the results will be saved.
|
|
382
|
+
"""
|
|
383
|
+
df = self.get_dataframe()
|
|
384
|
+
df.to_csv(filename, index=False)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: panda-generator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PANDA (Python Automating Numerical Dot Algorithm) – an open algorithm dedicated to the precise generation and controlled parameterization of multi-element dot arrays.
|
|
5
|
+
Author-email: Yehor Horin <danedred15@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://yehorh74.github.io/PANDA
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy>=1.20.0
|
|
15
|
+
Requires-Dist: pandas>=1.3.0
|
|
16
|
+
Requires-Dist: scipy>=1.7.0
|
|
17
|
+
Requires-Dist: psychopy>=2021.1.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# PANDA (Python Automating Numerical Dot Algorithm)
|
|
21
|
+
|
|
22
|
+
**PANDA** is a Python library, dedicated to the precise generation and controlled parameterization of multi-element dot arrays for cognitive and psychophysiological tasks and studies.
|
|
23
|
+
|
|
24
|
+
## Main features
|
|
25
|
+
* Control of spatial and size parameters.
|
|
26
|
+
* PsychoPy integration.
|
|
27
|
+
* Automatic scaling and geometric calculations.
|
|
28
|
+
|
|
29
|
+
# Quick start
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
You can install the package directly from PyPI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install panda-generator
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Usage
|
|
40
|
+
The following example demonstrates how to initialize PANDA, generate stimulus images for different conditions, and save the metadata to a CSV file:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
from psychopy import core, visual
|
|
45
|
+
from panda.generator import PANDA
|
|
46
|
+
|
|
47
|
+
WINDOW_RES = [700, 700]
|
|
48
|
+
OUTPUT_BASE = 'output_path'
|
|
49
|
+
|
|
50
|
+
CONDITIONS = [('ID', 'TA'), ('ID', 'TP')]
|
|
51
|
+
N_COUNTS = {2: 4, 3: 6, 4: 2, 5: 4}
|
|
52
|
+
|
|
53
|
+
win = visual.Window(
|
|
54
|
+
WINDOW_RES, color='black', units='pix', multiSample=True, numSamples=8
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
gen = PANDA(
|
|
58
|
+
win, n_anchor=3, base_ta=30000, base_cha=120000, color='white', seed=42
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for spatial_type, size_type in CONDITIONS:
|
|
62
|
+
condition_name = f'{spatial_type}_{size_type}'
|
|
63
|
+
out_dir = os.path.join(OUTPUT_BASE, condition_name)
|
|
64
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
print(f'Generating: {condition_name}')
|
|
67
|
+
|
|
68
|
+
for n_val, count in N_COUNTS.items():
|
|
69
|
+
for i in range(1, count + 1):
|
|
70
|
+
img_path = os.path.join(
|
|
71
|
+
out_dir, f'dot_N{n_val:02d}_{i:03d}_{condition_name}.png'
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
gen.generate_and_save(
|
|
75
|
+
num_elements=n_val,
|
|
76
|
+
spatial_type=spatial_type,
|
|
77
|
+
size_type=size_type,
|
|
78
|
+
output_filepath=img_path,
|
|
79
|
+
sd_ratio=0.2,
|
|
80
|
+
save_results=True,
|
|
81
|
+
pause_time=0
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f'Error [N={n_val}, attempt {i}]: {e}')
|
|
85
|
+
|
|
86
|
+
gen.save_csv(os.path.join(out_dir, f'results_{condition_name}.csv'))
|
|
87
|
+
|
|
88
|
+
win.close()
|
|
89
|
+
core.quit()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## LICENSE
|
|
93
|
+
**MIT License**
|
|
94
|
+
|
|
95
|
+
Copyright (c) 2026 Yehor Horin, Uniwersytet Warszawski
|
|
96
|
+
|
|
97
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
98
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
99
|
+
in the Software without restriction, including without limitation the rights
|
|
100
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
101
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
102
|
+
furnished to do so, subject to the following conditions:
|
|
103
|
+
|
|
104
|
+
The above copyright notice and this permission notice shall be included in all
|
|
105
|
+
copies or substantial portions of the Software.
|
|
106
|
+
|
|
107
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
108
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
109
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
110
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
111
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
112
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
113
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
panda/__init__.py,sha256=rT1wsxdYfv_6hCXZzivYiafIec6ly0I6Rv9DPgEcKJ0,74
|
|
2
|
+
panda/generator.py,sha256=Tg6suN189Hj_kfPq7pz2tXo21ly4LApnJ9l1KTQhKMc,16560
|
|
3
|
+
panda_generator-0.1.0.dist-info/licenses/LICENSE,sha256=PcIlGHSirl1FRXu1gBNWNJ2h8hZw30EQpWpSSyH_EXQ,1111
|
|
4
|
+
panda_generator-0.1.0.dist-info/METADATA,sha256=ZfSljyKiXzcd2HzgaKHQ_aEsL4p7RFvmHjcGLduYihQ,4067
|
|
5
|
+
panda_generator-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
panda_generator-0.1.0.dist-info/top_level.txt,sha256=bPGZujm5VbRo8EbsxWEZvNnRjBCHuHFNyrRlBZCZqcU,6
|
|
7
|
+
panda_generator-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yehor Horin, Uniwersytet Warszawski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
panda
|