panda-generator 0.1.0__tar.gz

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.
@@ -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,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,94 @@
1
+ # PANDA (Python Automating Numerical Dot Algorithm)
2
+
3
+ **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.
4
+
5
+ ## Main features
6
+ * Control of spatial and size parameters.
7
+ * PsychoPy integration.
8
+ * Automatic scaling and geometric calculations.
9
+
10
+ # Quick start
11
+
12
+ ## Installation
13
+
14
+ You can install the package directly from PyPI:
15
+
16
+ ```bash
17
+ pip install panda-generator
18
+ ```
19
+
20
+ ## Basic Usage
21
+ The following example demonstrates how to initialize PANDA, generate stimulus images for different conditions, and save the metadata to a CSV file:
22
+
23
+ ```python
24
+ import os
25
+ from psychopy import core, visual
26
+ from panda.generator import PANDA
27
+
28
+ WINDOW_RES = [700, 700]
29
+ OUTPUT_BASE = 'output_path'
30
+
31
+ CONDITIONS = [('ID', 'TA'), ('ID', 'TP')]
32
+ N_COUNTS = {2: 4, 3: 6, 4: 2, 5: 4}
33
+
34
+ win = visual.Window(
35
+ WINDOW_RES, color='black', units='pix', multiSample=True, numSamples=8
36
+ )
37
+
38
+ gen = PANDA(
39
+ win, n_anchor=3, base_ta=30000, base_cha=120000, color='white', seed=42
40
+ )
41
+
42
+ for spatial_type, size_type in CONDITIONS:
43
+ condition_name = f'{spatial_type}_{size_type}'
44
+ out_dir = os.path.join(OUTPUT_BASE, condition_name)
45
+ os.makedirs(out_dir, exist_ok=True)
46
+
47
+ print(f'Generating: {condition_name}')
48
+
49
+ for n_val, count in N_COUNTS.items():
50
+ for i in range(1, count + 1):
51
+ img_path = os.path.join(
52
+ out_dir, f'dot_N{n_val:02d}_{i:03d}_{condition_name}.png'
53
+ )
54
+ try:
55
+ gen.generate_and_save(
56
+ num_elements=n_val,
57
+ spatial_type=spatial_type,
58
+ size_type=size_type,
59
+ output_filepath=img_path,
60
+ sd_ratio=0.2,
61
+ save_results=True,
62
+ pause_time=0
63
+ )
64
+ except Exception as e:
65
+ print(f'Error [N={n_val}, attempt {i}]: {e}')
66
+
67
+ gen.save_csv(os.path.join(out_dir, f'results_{condition_name}.csv'))
68
+
69
+ win.close()
70
+ core.quit()
71
+ ```
72
+
73
+ ## LICENSE
74
+ **MIT License**
75
+
76
+ Copyright (c) 2026 Yehor Horin, Uniwersytet Warszawski
77
+
78
+ Permission is hereby granted, free of charge, to any person obtaining a copy
79
+ of this software and associated documentation files (the "Software"), to deal
80
+ in the Software without restriction, including without limitation the rights
81
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
82
+ copies of the Software, and to permit persons to whom the Software is
83
+ furnished to do so, subject to the following conditions:
84
+
85
+ The above copyright notice and this permission notice shall be included in all
86
+ copies or substantial portions of the Software.
87
+
88
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
89
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
90
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
91
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
92
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
93
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
94
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ from .generator import PANDA
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["PANDA"]
@@ -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,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ panda/__init__.py
5
+ panda/generator.py
6
+ panda_generator.egg-info/PKG-INFO
7
+ panda_generator.egg-info/SOURCES.txt
8
+ panda_generator.egg-info/dependency_links.txt
9
+ panda_generator.egg-info/requires.txt
10
+ panda_generator.egg-info/top_level.txt
11
+ tests/test_export.py
12
+ tests/test_generator.py
13
+ tests/test_geometry.py
@@ -0,0 +1,4 @@
1
+ numpy>=1.20.0
2
+ pandas>=1.3.0
3
+ scipy>=1.7.0
4
+ psychopy>=2021.1.0
@@ -0,0 +1,4 @@
1
+ dist
2
+ output_stimuli
3
+ panda
4
+ site
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "panda-generator"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Yehor Horin", email="danedred15@gmail.com" },
10
+ ]
11
+ description = "PANDA (Python Automating Numerical Dot Algorithm) – an open algorithm dedicated to the precise generation and controlled parameterization of multi-element dot arrays."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Information Analysis",
19
+ ]
20
+ dependencies = [
21
+ "numpy>=1.20.0",
22
+ "pandas>=1.3.0",
23
+ "scipy>=1.7.0",
24
+ "psychopy>=2021.1.0"
25
+ ]
26
+
27
+ [project.urls]
28
+ "Homepage" = "https://yehorh74.github.io/PANDA"
29
+
30
+ [tool.pytest.ini_options]
31
+ pythonpath = ["."]
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["."]
35
+ exclude = ["tests*", "docs*", "examples*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ import os
2
+ import pandas as pd
3
+
4
+
5
+ def test_dataframe_and_csv_export(panda_instance, tmp_path):
6
+ panda_instance.reset_results()
7
+
8
+ _, radii, pos = panda_instance.generate_balanced_set(5, 'CH', 'TA')
9
+ panda_instance.log_ground_truth(5, radii, pos)
10
+
11
+ df = panda_instance.get_dataframe()
12
+ assert isinstance(df, pd.DataFrame)
13
+ assert len(df) == 1
14
+ assert 'TSA' in df.columns
15
+ assert 'CHA' in df.columns
16
+
17
+ csv_file = tmp_path / "test_output.csv"
18
+ panda_instance.save_csv(str(csv_file))
19
+
20
+ assert os.path.exists(csv_file)
21
+ loaded_df = pd.read_csv(csv_file)
22
+ assert len(loaded_df) == 1
@@ -0,0 +1,37 @@
1
+ import pytest
2
+ import numpy as np
3
+
4
+
5
+ @pytest.mark.parametrize("spatial_type, size_type", [
6
+ ('CH', 'TA'),
7
+ ('CH', 'TP'),
8
+ ('ID', 'TA'),
9
+ ('ID', 'TP'),
10
+ ])
11
+ def test_generate_balanced_set_dimensions(panda_instance, spatial_type, size_type):
12
+ num_elements = 6
13
+ dots, radii, pos = panda_instance.generate_balanced_set(
14
+ num_elements=num_elements,
15
+ spatial_type=spatial_type,
16
+ size_type=size_type,
17
+ sd_ratio=0.2,
18
+ min_dist_factor=2.0,
19
+ )
20
+
21
+ assert len(radii) == num_elements
22
+ assert len(pos) == num_elements
23
+ assert all(len(p) == 2 for p in pos)
24
+
25
+
26
+ def test_log_ground_truth(panda_instance):
27
+ num_elements = 4
28
+ radii = np.array([10.0, 10.0, 10.0, 10.0])
29
+ pos = np.array([[0, 0], [50, 0], [50, 50], [0, 50]])
30
+
31
+ panda_instance.reset_results()
32
+ metrics = panda_instance.log_ground_truth(num_elements, radii, pos)
33
+
34
+ assert len(panda_instance.results) == 1
35
+ assert metrics['N'] == num_elements
36
+ assert np.isclose(metrics['TSA'], 4 * np.pi * 100)
37
+ assert metrics['CHA'] == 2500.0
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+
5
+ def test_compute_convex_hull_area(panda_instance):
6
+ square_positions = np.array([
7
+ [0, 0],
8
+ [100, 0],
9
+ [100, 100],
10
+ [0, 100]
11
+ ])
12
+ area = panda_instance._compute_convex_hull_area(square_positions)
13
+ assert np.isclose(area, 10000.0)
14
+
15
+
16
+ def test_get_radii_total_surface_area(panda_instance):
17
+ target_ta = 30000.0
18
+ num_elements = 5
19
+ sd_ratio = 0.2
20
+
21
+ radii = panda_instance._get_radii(num_elements, target_ta, sd_ratio)
22
+ calculated_ta = np.sum(np.pi * (radii ** 2))
23
+
24
+ assert len(radii) == num_elements
25
+ assert np.isclose(calculated_ta, target_ta)