simba-uw-tf-dev 4.7.4__py3-none-any.whl → 4.7.6__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 simba-uw-tf-dev might be problematic. Click here for more details.

@@ -42,7 +42,7 @@ from simba.utils.checks import (check_float,
42
42
  check_valid_cpu_pool, check_valid_dict,
43
43
  check_valid_lst, check_valid_tuple)
44
44
  from simba.utils.data import (create_color_palette, create_color_palettes,
45
- terminate_cpu_pool)
45
+ get_cpu_pool, terminate_cpu_pool)
46
46
  from simba.utils.enums import Defaults, Formats, GeometryEnum, Options
47
47
  from simba.utils.errors import CountError, InvalidInputError
48
48
  from simba.utils.read_write import (SimbaTimer, find_core_cnt,
@@ -1225,9 +1225,16 @@ class GeometryMixin(object):
1225
1225
  To convert single frame animal body-part coordinates to polygon, use single core method :func:`simba.mixins.geometry_mixin.GeometryMixin.bodyparts_to_polygon`
1226
1226
 
1227
1227
  :param np.ndarray data: NumPy array of body part coordinates. Each subarray represents the coordinates of a geometry in a frame.
1228
+ :param Optional[str] video_name: Optional video name for progress messages.
1229
+ :param Optional[str] animal_name: Optional animal name for progress messages.
1230
+ :param Optional[bool] verbose: If True, prints progress messages. Default False.
1228
1231
  :param Literal['round', 'square', 'flat'] cap_style: Style of line cap for parallel offset. Options: 'round', 'square', 'flat'.
1229
1232
  :param int parallel_offset: Offset distance for parallel lines. Default is 1.
1233
+ :param Optional[float] pixels_per_mm: Pixels per millimeter conversion factor.
1230
1234
  :param float simplify_tolerance: Tolerance parameter for simplifying geometries. Default is 2.
1235
+ :param bool preserve_topology: If True, preserves topology during simplification. Default True.
1236
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
1237
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1231
1238
  :returns: A list of polygons with length data.shape[0]
1232
1239
  :rtype: List[Polygon]
1233
1240
 
@@ -1266,7 +1273,7 @@ class GeometryMixin(object):
1266
1273
 
1267
1274
  pool_terminate_flag = False if pool is not None else True
1268
1275
  if pool is not None:
1269
- check_valid_cpu_pool(value=pool, source=f'{self.__class__.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1276
+ check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_bodyparts_to_polygon.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1270
1277
  else:
1271
1278
  pool = multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value)
1272
1279
  constants = functools.partial(GeometryMixin.bodyparts_to_polygon,
@@ -1289,14 +1296,15 @@ class GeometryMixin(object):
1289
1296
  timer.stop_timer()
1290
1297
  if verbose:
1291
1298
  stdout_success(msg="Polygons complete.", elapsed_time=timer.elapsed_time_str)
1292
- if pool_terminate_flag: terminate_cpu_pool(pool=pool)
1299
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_bodyparts_to_polygon.__name__)
1293
1300
  return [l for ll in results for l in ll]
1294
1301
 
1295
1302
  @staticmethod
1296
1303
  def multiframe_bodypart_to_point(data: np.ndarray,
1297
1304
  core_cnt: Optional[int] = -1,
1298
1305
  buffer: Optional[int] = None,
1299
- px_per_mm: Optional[int] = None) -> Union[List[Point], List[List[Point]]]:
1306
+ px_per_mm: Optional[int] = None,
1307
+ pool: Optional[multiprocessing.Pool] = None) -> Union[List[Point], List[List[Point]]]:
1300
1308
  """
1301
1309
  Process multiple frames of body part data in parallel and convert them to shapely Points.
1302
1310
 
@@ -1306,10 +1314,10 @@ class GeometryMixin(object):
1306
1314
  For non-parallized call, use :func:`simba.mixins.geometry_mixin.GeometryMixin.bodyparts_to_points`
1307
1315
 
1308
1316
  :param np.ndarray data: 2D or 3D array with body-part coordinates where rows are frames and columns are x and y coordinates.
1309
- :param Optional[int] core_cnt: The number of cores to use. If -1, then all available cores.
1310
- :param Optional[int] px_per_mm: Pixels ro millimeter convertion factor. Required if buffer is not None.
1317
+ :param Optional[int] core_cnt: The number of cores to use. If -1, then all available cores. Ignored if pool is provided.
1311
1318
  :param Optional[int] buffer: If not None, then the area of the Point. Thus, if not None, then returns Polygons representing the Points.
1312
1319
  :param Optional[int] px_per_mm: Pixels to millimeter convertion factor. Required if buffer is not None.
1320
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1313
1321
  :returns Union[List[Point], List[List[Point]]]: If input is a 2D array, then list of Points. If 3D array, then list of list of Points.
1314
1322
 
1315
1323
  .. note::
@@ -1331,15 +1339,13 @@ class GeometryMixin(object):
1331
1339
  data_ndim = data.ndim
1332
1340
  if data_ndim == 2:
1333
1341
  data = np.array_split(data, core_cnt)
1334
- with multiprocessing.Pool(
1335
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
1336
- ) as pool:
1337
- constants = functools.partial(
1338
- GeometryMixin.bodyparts_to_points, buffer=buffer, px_per_mm=px_per_mm
1339
- )
1340
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1341
- results.append(result)
1342
- terminate_cpu_pool(pool=pool, force=False)
1342
+ pool_terminate_flag = False if pool is not None else True
1343
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin.multiframe_bodypart_to_point.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1344
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin.multiframe_bodypart_to_point.__name__)
1345
+ constants = functools.partial(GeometryMixin.bodyparts_to_points, buffer=buffer, px_per_mm=px_per_mm)
1346
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1347
+ results.append(result)
1348
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin.multiframe_bodypart_to_point.__name__)
1343
1349
  if data_ndim == 2:
1344
1350
  return [i for s in results for i in s]
1345
1351
  else:
@@ -1350,26 +1356,38 @@ class GeometryMixin(object):
1350
1356
  size_mm: int,
1351
1357
  pixels_per_mm: float,
1352
1358
  core_cnt: int = -1,
1353
- cap_style: Literal["round", "square", "flat"] = "round") -> List[Polygon]:
1359
+ cap_style: Literal["round", "square", "flat"] = "round",
1360
+ pool: Optional[multiprocessing.Pool] = None) -> List[Polygon]:
1361
+ """
1362
+ Buffer shapes by a specified size using multiprocessing.
1354
1363
 
1355
- check_valid_lst(data=geometries, source=f'{GeometryMixin.multiframe_buffer_shapes.__name__} geometries',
1356
- valid_dtypes=(Polygon, LineString,), min_len=1, raise_error=True)
1364
+ .. seealso::
1365
+ For single core method, see :func:`simba.mixins.geometry_mixin.GeometryMixin.buffer_shape`
1366
+
1367
+ :param List[Union[Polygon, LineString]] geometries: List of geometries to buffer.
1368
+ :param int size_mm: Buffer size in millimeters.
1369
+ :param float pixels_per_mm: Pixels per millimeter conversion factor.
1370
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
1371
+ :param Literal["round", "square", "flat"] cap_style: Style of line cap for buffering. Options: 'round', 'square', 'flat'. Default 'round'.
1372
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1373
+ :return: List of buffered polygons.
1374
+ :rtype: List[Polygon]
1375
+ """
1376
+
1377
+ check_valid_lst(data=geometries, source=f'{GeometryMixin.multiframe_buffer_shapes.__name__} geometries', valid_dtypes=(Polygon, LineString,), min_len=1, raise_error=True)
1357
1378
  check_int(name=f'{GeometryMixin.multiframe_buffer_shapes.__name__} size_mm', value=size_mm, min_value=1)
1358
- check_float(name=f'{GeometryMixin.multiframe_buffer_shapes.__name__} pixels_per_mm', value=pixels_per_mm,
1359
- min_value=10e-6)
1360
- check_int(name=f'{GeometryMixin.multiframe_buffer_shapes.__name__} core_cnt', value=core_cnt, min_value=-1,
1361
- unaccepted_vals=[0])
1379
+ check_float(name=f'{GeometryMixin.multiframe_buffer_shapes.__name__} pixels_per_mm', value=pixels_per_mm, allow_zero=False, allow_negative=False)
1380
+ check_int(name=f'{GeometryMixin.multiframe_buffer_shapes.__name__} core_cnt', value=core_cnt, min_value=-1, unaccepted_vals=[0])
1362
1381
  core_cnt = find_core_cnt()[0] if core_cnt == -1 or core_cnt > find_core_cnt()[0] else core_cnt
1363
1382
  geomety_lst = lambda lst, core_cnt: [lst[i::core_cnt] for i in range(core_cnt)]
1364
1383
  results = []
1365
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value) as pool:
1366
- constants = functools.partial(GeometryMixin.buffer_shape,
1367
- size_mm=size_mm,
1368
- pixels_per_mm=pixels_per_mm,
1369
- cap_style=cap_style)
1370
- for cnt, mp_return in enumerate(pool.imap(constants, geomety_lst, chunksize=1)):
1371
- results.append(mp_return)
1372
- terminate_cpu_pool(pool=pool, force=False)
1384
+ pool_terminate_flag = False if pool is not None else True
1385
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin.multiframe_buffer_shapes.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1386
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin.multiframe_buffer_shapes.__name__)
1387
+ constants = functools.partial(GeometryMixin.buffer_shape, size_mm=size_mm, pixels_per_mm=pixels_per_mm, cap_style=cap_style)
1388
+ for cnt, mp_return in enumerate(pool.imap(constants, geomety_lst, chunksize=1)):
1389
+ results.append(mp_return)
1390
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin.multiframe_buffer_shapes.__name__)
1373
1391
  return [l for ll in results for l in ll]
1374
1392
 
1375
1393
  def multiframe_bodyparts_to_circle(self,
@@ -1386,11 +1404,13 @@ class GeometryMixin(object):
1386
1404
  For non-parallized call, use :func:`simba.mixins.geometry_mixin.GeometryMixin.bodyparts_to_circle`
1387
1405
 
1388
1406
  :param np.ndarray data: The body-part coordinates xy as a 2d array where rows are frames and columns represent x and y coordinates . E.g., np.array([[364, 308], [369, 309]])
1389
- :param int data: The radius of the resultant circle in millimeters.
1390
- :param int core_cnt: Number of CPU cores to use. Defaults to -1 meaning all available cores will be used.
1407
+ :param int parallel_offset: The radius of the resultant circle in millimeters.
1408
+ :param int core_cnt: Number of CPU cores to use. Defaults to -1 meaning all available cores will be used. Ignored if pool is provided.
1409
+ :param bool verbose: If True, prints progress messages. Default True.
1391
1410
  :param int pixels_per_mm: The pixels per millimeter of the video. If not passed, 1 will be used meaning revert to radius in pixels rather than millimeters.
1411
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1392
1412
  :returns: List of shapely Polygons of circular shape of size data.shape[0].
1393
- :rtype: Polygon
1413
+ :rtype: List[Polygon]
1394
1414
 
1395
1415
  :example:
1396
1416
  >>> data = np.random.randint(0, 100, (100, 2))
@@ -1405,7 +1425,7 @@ class GeometryMixin(object):
1405
1425
  core_cnt = find_core_cnt()[0] if core_cnt == -1 or core_cnt > find_core_cnt()[0] else core_cnt
1406
1426
  pool_terminate_flag = False if pool is not None else True
1407
1427
  if pool is not None:
1408
- check_valid_cpu_pool(value=pool, source=f'{self.__class__.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1428
+ check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_bodyparts_to_circle.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1409
1429
  else:
1410
1430
  pool = multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value)
1411
1431
  constants = functools.partial(GeometryMixin.bodyparts_to_circle, parallel_offset=parallel_offset, pixels_per_mm=pixels_per_mm, verbose=verbose)
@@ -1413,7 +1433,7 @@ class GeometryMixin(object):
1413
1433
  data = np.array_split(data, core_cnt)
1414
1434
  for cnt, mp_return in enumerate(pool.imap(constants, data, chunksize=1)):
1415
1435
  results.extend((mp_return))
1416
- if pool_terminate_flag: terminate_cpu_pool(pool=pool)
1436
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_bodyparts_to_circle.__name__)
1417
1437
  timer.stop_timer()
1418
1438
  if verbose: stdout_success(msg="Multiframe body-parts to circle complete", source=GeometryMixin.multiframe_bodyparts_to_circle.__name__, elapsed_time=timer.elapsed_time_str )
1419
1439
  return results
@@ -1468,7 +1488,8 @@ class GeometryMixin(object):
1468
1488
  data: np.ndarray,
1469
1489
  buffer: Optional[int] = None,
1470
1490
  px_per_mm: Optional[float] = None,
1471
- core_cnt: Optional[int] = -1) -> List[LineString]:
1491
+ core_cnt: Optional[int] = -1,
1492
+ pool: Optional[multiprocessing.Pool] = None) -> List[LineString]:
1472
1493
  """
1473
1494
  Convert multiframe body-parts data to a list of LineString objects using multiprocessing.
1474
1495
 
@@ -1477,8 +1498,9 @@ class GeometryMixin(object):
1477
1498
 
1478
1499
  :param np.ndarray data: Input array representing multiframe body-parts data. It should be a 3D array with dimensions (frames, points, coordinates).
1479
1500
  :param Optional[int] buffer: If not None, then the linestring will be expanded into a 2D geometry polygon with area ``buffer``.
1480
- :param Optional[int] px_per_mm: If ``buffer`` if not None, then provide the pixels to millimeter
1481
- :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. If set to -1, the function will automatically determine the available core count.
1501
+ :param Optional[float] px_per_mm: If ``buffer`` if not None, then provide the pixels to millimeter conversion factor.
1502
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. If set to -1, the function will automatically determine the available core count. Ignored if pool is provided.
1503
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1482
1504
  :return: A list of LineString objects representing the body-parts trajectories.
1483
1505
  :rtype: List[LineString]
1484
1506
 
@@ -1514,15 +1536,13 @@ class GeometryMixin(object):
1514
1536
  min_value=1,
1515
1537
  )
1516
1538
  results = []
1517
- with multiprocessing.Pool(
1518
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
1519
- ) as pool:
1520
- constants = functools.partial(
1521
- GeometryMixin.bodyparts_to_line, buffer=buffer, px_per_mm=px_per_mm
1522
- )
1523
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1524
- results.append(result)
1525
- terminate_cpu_pool(pool=pool, force=False)
1539
+ pool_terminate_flag = False if pool is not None else True
1540
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_bodyparts_to_line.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1541
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_bodyparts_to_line.__name__)
1542
+ constants = functools.partial(GeometryMixin.bodyparts_to_line, buffer=buffer, px_per_mm=px_per_mm)
1543
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
1544
+ results.append(result)
1545
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_bodyparts_to_line.__name__)
1526
1546
  return results
1527
1547
 
1528
1548
  def multiframe_compute_pct_shape_overlap(self,
@@ -1547,13 +1567,14 @@ class GeometryMixin(object):
1547
1567
 
1548
1568
  :param List[Polygon] shape_1: List of Polygons.
1549
1569
  :param List[Polygon] shape_2: List of Polygons with the same length as shape_1.
1550
- :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores.
1570
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
1551
1571
  :param Optional[str] video_name: If not None, then the name of the video being processed for interpretable progress msgs.
1552
- :param Optional[bool] video_name: If True, then prints interpretable progress msgs.
1572
+ :param Optional[bool] verbose: If True, then prints interpretable progress msgs.
1553
1573
  :param Optional[Tuple[str]] animal_names: If not None, then a two-tuple of animal names (or alternative shape names) interpretable progress msgs.
1554
1574
  :param Optional[Literal["difference", "shape_1", "shape_2"]] denominator: Denominator for percentage calculation. "difference" uses union minus intersection, "shape_1" uses shape_1 area, "shape_2" uses shape_2 area. Default: "difference".
1575
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1555
1576
  :return: List of length ``shape_1`` with percentage overlap between corresponding Polygons.
1556
- :rtype: List[float]
1577
+ :rtype: np.ndarray
1557
1578
 
1558
1579
  :example:
1559
1580
  >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
@@ -1578,7 +1599,7 @@ class GeometryMixin(object):
1578
1599
  data = np.array_split(data, core_cnt)
1579
1600
  pool_terminate_flag = False if pool is not None else True
1580
1601
  if pool is not None:
1581
- check_valid_cpu_pool(value=pool, source=f'{self.__class__.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1602
+ check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_compute_pct_shape_overlap.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1582
1603
  else:
1583
1604
  pool = multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value)
1584
1605
  constants = functools.partial(GeometryMixin.compute_pct_shape_overlap, denominator=denominator)
@@ -1594,7 +1615,7 @@ class GeometryMixin(object):
1594
1615
  results.append(result)
1595
1616
  timer.stop_timer()
1596
1617
  stdout_success(msg="Compute overlap complete.", elapsed_time=timer.elapsed_time_str)
1597
- if pool_terminate_flag: terminate_cpu_pool(pool=pool)
1618
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_compute_pct_shape_overlap.__name__)
1598
1619
  return np.hstack(results).astype(np.float32)
1599
1620
 
1600
1621
  def multiframe_compute_shape_overlap(self,
@@ -1616,10 +1637,13 @@ class GeometryMixin(object):
1616
1637
  .. seealso:
1617
1638
  :func:`simba.mixins.geometry_mixin.GeometryMixin.compute_shape_overlap`, :func:`simba.mixins.geometry_mixin.GeometryMixin.compute_pct_shape_overlap`, :func:`simba.mixins.geometry_mixin.GeometryMixin.multiframe_compute_pct_shape_overlap`
1618
1639
 
1619
- :param List[Polygon] shape_1: List of Polygons.
1620
- :param List[Polygon] shape_2: List of Polygons with the same length as shape_1.
1621
- :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores.
1622
- :return List[float]: List of overlap between corresponding Polygons. If overlap 1, else 0.
1640
+ :param List[Union[Polygon, LineString, None]] shape_1: List of Polygons, LineStrings, or None.
1641
+ :param List[Union[Polygon, LineString, None]] shape_2: List of Polygons, LineStrings, or None with the same length as shape_1.
1642
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
1643
+ :param Optional[bool] verbose: If True, prints progress messages. Default False.
1644
+ :param Optional[Tuple[str]] names: Optional tuple of names for progress messages.
1645
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1646
+ :return List[int]: List of overlap between corresponding Polygons. If overlap 1, else 0.
1623
1647
 
1624
1648
  :example:
1625
1649
  >>> df = read_df(file_path=r"C:/troubleshooting/two_black_animals_14bp/project_folder/csv/outlier_corrected_movement_location/Together_2.csv", file_type='csv').astype(int)
@@ -1645,7 +1669,7 @@ class GeometryMixin(object):
1645
1669
  data = np.array_split(data, core_cnt)
1646
1670
  pool_terminate_flag = False if pool is not None else True
1647
1671
  if pool is not None:
1648
- check_valid_cpu_pool(value=pool, source=f'{self.__class__.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1672
+ check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_compute_shape_overlap.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1649
1673
  else:
1650
1674
  pool = multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value)
1651
1675
  for cnt, result in enumerate(pool.imap(GeometryMixin.compute_shape_overlap, data, chunksize=1)):
@@ -1654,7 +1678,7 @@ class GeometryMixin(object):
1654
1678
  else:
1655
1679
  print(f"Computing overlap {cnt + 1}/{len(data)} (Shape 1: {names[0]}, Shape 2: {names[1]}, Video: {names[2]}...)")
1656
1680
  results.extend((result))
1657
- if pool_terminate_flag: terminate_cpu_pool(pool=pool)
1681
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_compute_shape_overlap.__name__)
1658
1682
  return results
1659
1683
 
1660
1684
  def multiframe_shape_distance(self,
@@ -1682,7 +1706,7 @@ class GeometryMixin(object):
1682
1706
 
1683
1707
  :param List[Union[LineString, Polygon]] shapes_a: List of LineString or Polygon geometries.
1684
1708
  :param List[Union[LineString, Polygon]] shapes_b: List of LineString or Polygon geometries with the same length as shapes_a.
1685
- :param float pixels_per_mm: Conversion factor from pixels to millimeters. Default 1.
1709
+ :param Optional[float] pixels_per_mm: Conversion factor from pixels to millimeters. Default 1.
1686
1710
  :param Literal['mm', 'cm', 'dm', 'm'] unit: Unit of measurement for the result. Options: 'mm', 'cm', 'dm', 'm'. Default: 'mm'.
1687
1711
  :param bool verbose: If True, prints progress information during computation. Default False.
1688
1712
  :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
@@ -1719,7 +1743,7 @@ class GeometryMixin(object):
1719
1743
  data = np.array_split(data, core_cnt)
1720
1744
  pool_terminate_flag = False if pool is not None else True
1721
1745
  if pool is not None:
1722
- check_valid_cpu_pool(value=pool, source=f'{self.__class__.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1746
+ check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_shape_distance.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1723
1747
  else:
1724
1748
  pool = multiprocessing.Pool(core_cnt, maxtasksperchild=maxchildpertask)
1725
1749
  results = []
@@ -1738,7 +1762,7 @@ class GeometryMixin(object):
1738
1762
  if verbose and shape_names is None:
1739
1763
  print(f'Shape distances computed for {len(shapes_a)} comparisons (elapsed time: {timer.elapsed_time_str}s)')
1740
1764
  if pool_terminate_flag:
1741
- terminate_cpu_pool(pool=pool)
1765
+ terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_shape_distance.__name__)
1742
1766
  return results
1743
1767
 
1744
1768
  def multiframe_minimum_rotated_rectangle(self,
@@ -1746,7 +1770,8 @@ class GeometryMixin(object):
1746
1770
  video_name: Optional[str] = None,
1747
1771
  verbose: Optional[bool] = False,
1748
1772
  animal_name: Optional[bool] = None,
1749
- core_cnt: int = -1) -> List[Polygon]:
1773
+ core_cnt: int = -1,
1774
+ pool: Optional[multiprocessing.Pool] = None) -> List[Polygon]:
1750
1775
 
1751
1776
  """
1752
1777
  Compute the minimum rotated rectangle for each Polygon in a list using multiprocessing.
@@ -1758,8 +1783,9 @@ class GeometryMixin(object):
1758
1783
  :param Optional[str] video_name: Optional video name to print (if verbose is True).
1759
1784
  :param Optional[str] animal_name: Optional animal name to print (if verbose is True).
1760
1785
  :param Optional[bool] verbose: If True, prints progress.
1761
- :param core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores.
1762
- :returns: A list of rotated rectangle sof size len(shapes).
1786
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
1787
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
1788
+ :returns: A list of rotated rectangles of size len(shapes).
1763
1789
  :rtype: List[Polygon]
1764
1790
 
1765
1791
  :example:
@@ -1773,29 +1799,31 @@ class GeometryMixin(object):
1773
1799
  check_int(name="CORE COUNT", value=core_cnt, min_value=-1, max_value=find_core_cnt()[0], raise_error=True)
1774
1800
  if core_cnt == -1: core_cnt = find_core_cnt()[0]
1775
1801
  results, timer = [], SimbaTimer(start=True)
1776
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
1777
- for cnt, result in enumerate(pool.imap(GeometryMixin.minimum_rotated_rectangle, shapes, chunksize=1)):
1778
- if verbose:
1779
- if not video_name and not animal_name:
1780
- print(f"Rotating polygon {cnt + 1}/{len(shapes)}...")
1781
- elif not video_name and animal_name:
1782
- print(
1783
- f"Rotating polygon {cnt + 1}/{len(shapes)} (Animal: {animal_name})..."
1784
- )
1785
- elif video_name and not animal_name:
1786
- print(
1787
- f"Rotating polygon {cnt + 1}/{len(shapes)} (Video: {video_name})..."
1788
- )
1789
- else:
1790
- print(
1791
- f"Rotating polygon {cnt + 1}/{len(shapes)} (Video: {video_name}, Animal: {animal_name})..."
1792
- )
1793
- results.append(result)
1802
+ pool_terminate_flag = False if pool is not None else True
1803
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_minimum_rotated_rectangle.__name__} pool', raise_error=True, accepted_cores=core_cnt)
1804
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_minimum_rotated_rectangle.__name__)
1805
+ for cnt, result in enumerate(pool.imap(GeometryMixin.minimum_rotated_rectangle, shapes, chunksize=1)):
1806
+ if verbose:
1807
+ if not video_name and not animal_name:
1808
+ print(f"Rotating polygon {cnt + 1}/{len(shapes)}...")
1809
+ elif not video_name and animal_name:
1810
+ print(
1811
+ f"Rotating polygon {cnt + 1}/{len(shapes)} (Animal: {animal_name})..."
1812
+ )
1813
+ elif video_name and not animal_name:
1814
+ print(
1815
+ f"Rotating polygon {cnt + 1}/{len(shapes)} (Video: {video_name})..."
1816
+ )
1817
+ else:
1818
+ print(
1819
+ f"Rotating polygon {cnt + 1}/{len(shapes)} (Video: {video_name}, Animal: {animal_name})..."
1820
+ )
1821
+ results.append(result)
1794
1822
 
1795
1823
  timer.stop_timer()
1824
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_minimum_rotated_rectangle.__name__)
1796
1825
  if verbose:
1797
1826
  stdout_success(msg="Rotated rectangles complete.", elapsed_time=timer.elapsed_time_str)
1798
- terminate_cpu_pool(pool=pool, force=False)
1799
1827
  return results
1800
1828
 
1801
1829
  @staticmethod
@@ -1965,13 +1993,22 @@ class GeometryMixin(object):
1965
1993
  shapes: List[Union[LineString, MultiLineString]],
1966
1994
  pixels_per_mm: float,
1967
1995
  core_cnt: int = -1,
1968
- unit: Literal["mm", "cm", "dm", "m"] = "mm") -> List[float]:
1996
+ unit: Literal["mm", "cm", "dm", "m"] = "mm",
1997
+ pool: Optional[multiprocessing.Pool] = None) -> List[float]:
1969
1998
  """
1970
1999
  Calculate the length of LineStrings using multiprocessing.
1971
2000
 
1972
2001
  .. seealso::
1973
2002
  For single core process, see :func:`simba.mixins.geometry_mixin.GeometryMixin.length`
1974
2003
 
2004
+ :param List[Union[LineString, MultiLineString]] shapes: List of LineString or MultiLineString geometries.
2005
+ :param float pixels_per_mm: Pixels per millimeter conversion factor.
2006
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
2007
+ :param Literal["mm", "cm", "dm", "m"] unit: Unit of measurement for the result. Options: 'mm', 'cm', 'dm', 'm'. Default: 'mm'.
2008
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2009
+ :return: List of lengths in the specified unit.
2010
+ :rtype: List[float]
2011
+
1975
2012
  :example:
1976
2013
  >>> data = np.random.randint(0, 100, (5000, 2))
1977
2014
  >>> data = data.reshape(2500,-1, data.shape[1])
@@ -1991,19 +2028,19 @@ class GeometryMixin(object):
1991
2028
  check_float(name="PIXELS PER MM", value=pixels_per_mm, min_value=0.0)
1992
2029
  check_if_valid_input(name="UNIT", input=unit, options=["mm", "cm", "dm", "m"])
1993
2030
  results = []
1994
- with multiprocessing.Pool(
1995
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
1996
- ) as pool:
1997
- constants = functools.partial(
1998
- GeometryMixin.length, pixels_per_mm=pixels_per_mm, unit=unit
1999
- )
2000
- for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
2001
- results.append(result)
2002
- terminate_cpu_pool(pool=pool, force=False)
2031
+ pool_terminate_flag = False if pool is not None else True
2032
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_length.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2033
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_length.__name__)
2034
+ constants = functools.partial(GeometryMixin.length, pixels_per_mm=pixels_per_mm, unit=unit)
2035
+ for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
2036
+ results.append(result)
2037
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_length.__name__)
2003
2038
  return results
2004
2039
 
2005
- def multiframe_union(self, shapes: Iterable[Union[LineString, MultiLineString, Polygon]], core_cnt: int = -1) -> \
2006
- Iterable[Union[LineString, MultiLineString, Polygon]]:
2040
+ def multiframe_union(self,
2041
+ shapes: Iterable[Union[LineString, MultiLineString, Polygon]],
2042
+ core_cnt: int = -1,
2043
+ pool: Optional[multiprocessing.Pool] = None) -> Iterable[Union[LineString, MultiLineString, Polygon]]:
2007
2044
  """
2008
2045
  Join multiple shapes frame-wise into a single shape/
2009
2046
 
@@ -2017,8 +2054,9 @@ class GeometryMixin(object):
2017
2054
  .. seealso::
2018
2055
  For single core method, see :func:`simba.mixins.geometry_mixin.GeometryMixin.union`
2019
2056
 
2020
- :param shapes: Iterable collection of shapes (`LineString`, `MultiLineString`, or `Polygon`) to be merged. E.g, of size NxM where N is the number of frames and M is the number of shapes in each frame.
2021
- :param core_cnt: The number of CPU cores to use for parallel processing; defaults to -1, which uses all available cores.
2057
+ :param Iterable[Union[LineString, MultiLineString, Polygon]] shapes: Iterable collection of shapes (`LineString`, `MultiLineString`, or `Polygon`) to be merged. E.g, of size NxM where N is the number of frames and M is the number of shapes in each frame.
2058
+ :param int core_cnt: The number of CPU cores to use for parallel processing; defaults to -1, which uses all available cores. Ignored if pool is provided.
2059
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2022
2060
  :return: An iterable of merged shapes, where each frame is combined into a single shape.
2023
2061
  :rtype: List[Union[LineString, MultiLineString, Polygon]]
2024
2062
 
@@ -2035,16 +2073,19 @@ class GeometryMixin(object):
2035
2073
  if core_cnt == -1:
2036
2074
  core_cnt = find_core_cnt()[0]
2037
2075
  results = []
2038
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
2039
- for cnt, result in enumerate(pool.imap(GeometryMixin().union, shapes, chunksize=1)):
2040
- results.append(result)
2041
- terminate_cpu_pool(pool=pool, force=False)
2076
+ pool_terminate_flag = False if pool is not None else True
2077
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_union.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2078
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_union.__name__)
2079
+ for cnt, result in enumerate(pool.imap(GeometryMixin().union, shapes, chunksize=1)):
2080
+ results.append(result)
2081
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_union.__name__)
2042
2082
  return results
2043
2083
 
2044
2084
  def multiframe_symmetric_difference(self, shapes: Iterable[Union[LineString, MultiLineString, Polygon]],
2045
- core_cnt: int = -1):
2085
+ core_cnt: int = -1,
2086
+ pool: Optional[multiprocessing.Pool] = None):
2046
2087
  """
2047
- Compute the symmetric differences between corresponding LineString or MultiLineString geometries usng multiprocessing.
2088
+ Compute the symmetric differences between corresponding LineString or MultiLineString geometries using multiprocessing.
2048
2089
 
2049
2090
  Computes a new geometry consisting of the parts that are exclusive to each input geometry.
2050
2091
 
@@ -2053,6 +2094,12 @@ class GeometryMixin(object):
2053
2094
  .. seealso::
2054
2095
  For single core method, see :func:`simba.mixins.geometry_mixin.GeometryMixin.symmetric_difference`
2055
2096
 
2097
+ :param Iterable[Union[LineString, MultiLineString, Polygon]] shapes: Iterable collection of shapes where each element is a list containing two geometries.
2098
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
2099
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2100
+ :return: List of symmetric difference geometries.
2101
+ :rtype: List[Union[LineString, MultiLineString, Polygon]]
2102
+
2056
2103
  :example:
2057
2104
  >>> data_1 = np.random.randint(0, 100, (5000, 2)).reshape(1000,-1, 2)
2058
2105
  >>> data_2 = np.random.randint(0, 100, (5000, 2)).reshape(1000,-1, 2)
@@ -2071,23 +2118,31 @@ class GeometryMixin(object):
2071
2118
  if core_cnt == -1:
2072
2119
  core_cnt = find_core_cnt()[0]
2073
2120
  results = []
2074
- with multiprocessing.Pool(
2075
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2076
- ) as pool:
2077
- for cnt, result in enumerate(
2078
- pool.imap(GeometryMixin().symmetric_difference, shapes, chunksize=1)
2079
- ):
2080
- results.append(result)
2081
- terminate_cpu_pool(pool=pool, force=False)
2121
+ pool_terminate_flag = False if pool is not None else True
2122
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_symmetric_difference.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2123
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_symmetric_difference.__name__)
2124
+ for cnt, result in enumerate(pool.imap(GeometryMixin().symmetric_difference, shapes, chunksize=1)):
2125
+ results.append(result)
2126
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_symmetric_difference.__name__)
2082
2127
  return results
2083
2128
 
2084
- def multiframe_delaunay_triangulate_keypoints(self, data: np.ndarray, core_cnt: int = -1) -> List[List[Polygon]]:
2129
+ def multiframe_delaunay_triangulate_keypoints(self,
2130
+ data: np.ndarray,
2131
+ core_cnt: int = -1,
2132
+ pool: Optional[multiprocessing.Pool] = None) -> List[List[Polygon]]:
2085
2133
  """
2086
- Triangulates a set of 2D keypoints. E.g., can be used to polygonize animal hull, or triangulate a gridpoint areana.
2134
+ Triangulates a set of 2D keypoints. E.g., can be used to polygonize animal hull, or triangulate a gridpoint arena.
2087
2135
 
2088
2136
  .. seealso::
2089
2137
  For single core process, see :func:`simba.mixins.geometry_mixin.GeometryMixin.delaunay_triangulate_keypoints`
2090
2138
 
2139
+ :param np.ndarray data: 3D array of keypoints where shape is (frames, keypoints, coordinates).
2140
+ :param int core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
2141
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2142
+ :return: List of lists of Polygon objects representing triangles.
2143
+ :rtype: List[List[Polygon]]
2144
+
2145
+ :example:
2091
2146
  >>> data_path = '/Users/simon/Desktop/envs/troubleshooting/Rat_NOR/project_folder/csv/machine_results/08102021_DOT_Rat7_8(2).csv'
2092
2147
  >>> data = pd.read_csv(data_path, index_col=0).head(1000).iloc[:, 0:21]
2093
2148
  >>> data = data[data.columns.drop(list(data.filter(regex='_p')))]
@@ -2115,17 +2170,12 @@ class GeometryMixin(object):
2115
2170
  source=GeometryMixin.multiframe_delaunay_triangulate_keypoints.__name__,
2116
2171
  )
2117
2172
  results = []
2118
- with multiprocessing.Pool(
2119
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2120
- ) as pool:
2121
- for cnt, result in enumerate(
2122
- pool.imap(
2123
- GeometryMixin().delaunay_triangulate_keypoints, data, chunksize=1
2124
- )
2125
- ):
2126
- results.append(result)
2127
-
2128
- terminate_cpu_pool(pool=pool, force=False)
2173
+ pool_terminate_flag = False if pool is not None else True
2174
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_delaunay_triangulate_keypoints.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2175
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_delaunay_triangulate_keypoints.__name__)
2176
+ for cnt, result in enumerate(pool.imap(GeometryMixin().delaunay_triangulate_keypoints, data, chunksize=1)):
2177
+ results.append(result)
2178
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_delaunay_triangulate_keypoints.__name__)
2129
2179
  return results
2130
2180
 
2131
2181
  def multiframe_difference(
@@ -2135,7 +2185,7 @@ class GeometryMixin(object):
2135
2185
  verbose: Optional[bool] = False,
2136
2186
  animal_names: Optional[str] = None,
2137
2187
  video_name: Optional[str] = None,
2138
- ) -> List[Union[Polygon, MultiPolygon]]:
2188
+ pool: Optional[multiprocessing.Pool] = None) -> List[Union[Polygon, MultiPolygon]]:
2139
2189
  """
2140
2190
  Compute the multi-frame difference for a collection of shapes using parallel processing.
2141
2191
 
@@ -2143,10 +2193,11 @@ class GeometryMixin(object):
2143
2193
  For single core method, see :func:`simba.mixins.geometry_mixin.GeometryMixin.difference`
2144
2194
 
2145
2195
  :param Iterable[Union[LineString, Polygon, MultiPolygon]] shapes: A collection of shapes, where each shape is a list containing two geometries.
2146
- :param int core_cnt: The number of CPU cores to use for parallel processing. Default is -1, which automatically detects the available cores.
2196
+ :param int core_cnt: The number of CPU cores to use for parallel processing. Default is -1, which automatically detects the available cores. Ignored if pool is provided.
2147
2197
  :param Optional[bool] verbose: If True, print progress messages during computation. Default is False.
2148
2198
  :param Optional[str] animal_names: Optional string representing the names of animals for informative messages.
2149
- :param Optional[str]video_name: Optional string representing the name of the video for informative messages.
2199
+ :param Optional[str] video_name: Optional string representing the name of the video for informative messages.
2200
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2150
2201
  :return: A list of geometries representing the multi-frame difference.
2151
2202
  :rtype: List[Union[Polygon, MultiPolygon]]
2152
2203
  """
@@ -2183,37 +2234,32 @@ class GeometryMixin(object):
2183
2234
  if core_cnt == -1:
2184
2235
  core_cnt = find_core_cnt()[0]
2185
2236
  results, timer = [], SimbaTimer(start=True)
2186
- with multiprocessing.Pool(
2187
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2188
- ) as pool:
2189
- for cnt, result in enumerate(
2190
- pool.imap(GeometryMixin().difference, shapes, chunksize=1)
2191
- ):
2192
- if verbose:
2193
- if not video_name and not animal_names:
2194
- print(
2195
- f"Computing geometry difference {cnt + 1}/{len(shapes)}..."
2196
- )
2197
- elif not video_name and animal_names:
2198
- print(
2199
- f"Computing geometry difference {cnt + 1}/{len(shapes)} (Animals: {animal_names})..."
2200
- )
2201
- elif video_name and not animal_names:
2202
- print(
2203
- f"Computing geometry difference {cnt + 1}/{len(shapes)} (Video: {video_name})..."
2204
- )
2205
- else:
2206
- print(
2207
- f"Computing geometry difference {cnt + 1}/{len(shapes)} (Video: {video_name}, Animals: {animal_names})..."
2208
- )
2209
- results.append(result)
2237
+ pool_terminate_flag = False if pool is not None else True
2238
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_difference.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2239
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_difference.__name__)
2240
+ for cnt, result in enumerate(pool.imap(GeometryMixin().difference, shapes, chunksize=1)):
2241
+ if verbose:
2242
+ if not video_name and not animal_names:
2243
+ print(
2244
+ f"Computing geometry difference {cnt + 1}/{len(shapes)}..."
2245
+ )
2246
+ elif not video_name and animal_names:
2247
+ print(
2248
+ f"Computing geometry difference {cnt + 1}/{len(shapes)} (Animals: {animal_names})..."
2249
+ )
2250
+ elif video_name and not animal_names:
2251
+ print(
2252
+ f"Computing geometry difference {cnt + 1}/{len(shapes)} (Video: {video_name})..."
2253
+ )
2254
+ else:
2255
+ print(
2256
+ f"Computing geometry difference {cnt + 1}/{len(shapes)} (Video: {video_name}, Animals: {animal_names})..."
2257
+ )
2258
+ results.append(result)
2210
2259
 
2211
2260
  timer.stop_timer()
2212
- stdout_success(
2213
- msg="Multi-frame difference compute complete",
2214
- elapsed_time=timer.elapsed_time_str,
2215
- )
2216
- terminate_cpu_pool(pool=pool, force=False)
2261
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_difference.__name__)
2262
+ stdout_success(msg="Multi-frame difference compute complete", elapsed_time=timer.elapsed_time_str)
2217
2263
  return results
2218
2264
 
2219
2265
  def multiframe_area(self,
@@ -2222,7 +2268,8 @@ class GeometryMixin(object):
2222
2268
  core_cnt: Optional[int] = -1,
2223
2269
  verbose: Optional[bool] = False,
2224
2270
  video_name: Optional[bool] = False,
2225
- animal_names: Optional[bool] = False) -> List[float]:
2271
+ animal_names: Optional[bool] = False,
2272
+ pool: Optional[multiprocessing.Pool] = None) -> List[float]:
2226
2273
 
2227
2274
  """
2228
2275
  Calculate the area of geometries in square millimeters using multiprocessing.
@@ -2232,10 +2279,11 @@ class GeometryMixin(object):
2232
2279
 
2233
2280
  :param List[Union[MultiPolygon, Polygon]] shapes: List of polygons of Multipolygons.
2234
2281
  :param float pixels_per_mm: Pixel per millimeter conversion factor. Default: 1.0.
2235
- :param Optional[int] core_cnt: The number of CPU cores to use for parallel processing. Default is -1, which automatically detects the available cores.
2282
+ :param Optional[int] core_cnt: The number of CPU cores to use for parallel processing. Default is -1, which automatically detects the available cores. Ignored if pool is provided.
2236
2283
  :param Optional[bool] verbose: If True, prints progress.
2237
- :param Optional[bool] video_name: If string, prints video name string during progress if verbose.
2238
- :param Optional[bool] animal_names: If string, prints animal name during progress if verbose.
2284
+ :param Optional[str] video_name: If string, prints video name string during progress if verbose.
2285
+ :param Optional[str] animal_names: If string, prints animal name during progress if verbose.
2286
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2239
2287
  :return: List of length ``len(shapes)`` with area values.
2240
2288
  :rtype: List[float]
2241
2289
  """
@@ -2250,24 +2298,26 @@ class GeometryMixin(object):
2250
2298
  if core_cnt == -1:
2251
2299
  core_cnt = find_core_cnt()[0]
2252
2300
  results, timer = [], SimbaTimer(start=True)
2253
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
2254
- constants = functools.partial(GeometryMixin.area, pixels_per_mm=pixels_per_mm)
2255
- for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
2256
- if verbose:
2257
- if not video_name and not animal_names:
2258
- print(f"Computing area {cnt + 1}/{len(shapes)}...")
2259
- elif not video_name and animal_names:
2260
- print(f"Computing % area {cnt + 1}/{len(shapes)} (Animals: {animal_names})...")
2261
- elif video_name and not animal_names:
2262
- print(f"Computing % area {cnt + 1}/{len(shapes)} (Video: {video_name})...")
2263
- else:
2264
- print(
2265
- f"Computing % area {cnt + 1}/{len(shapes)} (Video: {video_name}, Animals: {animal_names})...")
2266
- results.append(result)
2301
+ pool_terminate_flag = False if pool is not None else True
2302
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_area.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2303
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_area.__name__)
2304
+ constants = functools.partial(GeometryMixin.area, pixels_per_mm=pixels_per_mm)
2305
+ for cnt, result in enumerate(pool.imap(constants, shapes, chunksize=1)):
2306
+ if verbose:
2307
+ if not video_name and not animal_names:
2308
+ print(f"Computing area {cnt + 1}/{len(shapes)}...")
2309
+ elif not video_name and animal_names:
2310
+ print(f"Computing % area {cnt + 1}/{len(shapes)} (Animals: {animal_names})...")
2311
+ elif video_name and not animal_names:
2312
+ print(f"Computing % area {cnt + 1}/{len(shapes)} (Video: {video_name})...")
2313
+ else:
2314
+ print(
2315
+ f"Computing % area {cnt + 1}/{len(shapes)} (Video: {video_name}, Animals: {animal_names})...")
2316
+ results.append(result)
2267
2317
 
2268
2318
  timer.stop_timer()
2319
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_area.__name__)
2269
2320
  stdout_success(msg="Multi-frame area compute complete", elapsed_time=timer.elapsed_time_str)
2270
- terminate_cpu_pool(pool=pool, force=False)
2271
2321
  return results
2272
2322
 
2273
2323
  def multiframe_bodyparts_to_multistring_skeleton(
@@ -2278,7 +2328,8 @@ class GeometryMixin(object):
2278
2328
  verbose: Optional[bool] = False,
2279
2329
  video_name: Optional[bool] = False,
2280
2330
  animal_names: Optional[bool] = False,
2281
- ) -> List[Union[LineString, MultiLineString]]:
2331
+ pool: Optional[multiprocessing.Pool] = None) -> List[Union[LineString, MultiLineString]]:
2332
+
2282
2333
  """
2283
2334
  Convert body parts to LineString skeleton representations in a videos using multiprocessing.
2284
2335
 
@@ -2287,10 +2338,11 @@ class GeometryMixin(object):
2287
2338
 
2288
2339
  :param pd.DataFrame data_df: Pose-estimation data.
2289
2340
  :param Iterable[str] skeleton: Iterable of body part pairs defining the skeleton structure. Eg., [['Center', 'Lat_left'], ['Center', 'Lat_right'], ['Center', 'Nose'], ['Center', 'Tail_base']]
2290
- :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores.
2341
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
2291
2342
  :param Optional[bool] verbose: If True, print progress information during computation. Default is False.
2292
- :param Optional[bool] video_name: If True, include video name in progress information. Default is False.
2293
- :param Optional[bool] animal_names: If True, include animal names in progress information. Default is False.
2343
+ :param Optional[str] video_name: If string, include video name in progress information.
2344
+ :param Optional[str] animal_names: If string, include animal names in progress information.
2345
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2294
2346
  :return: List of LineString or MultiLineString objects representing the computed skeletons.
2295
2347
  :rtype: List[Union[LineString, MultiLineString]]
2296
2348
 
@@ -2341,33 +2393,27 @@ class GeometryMixin(object):
2341
2393
  else:
2342
2394
  skeleton_data = np.concatenate((skeleton_data, line), axis=1)
2343
2395
  skeleton_data = skeleton_data.reshape(len(data_df), len(skeleton), 2, -1)
2344
- with multiprocessing.Pool(
2345
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2346
- ) as pool:
2347
- for cnt, result in enumerate(
2348
- pool.imap(
2349
- GeometryMixin.bodyparts_to_multistring_skeleton,
2350
- skeleton_data,
2351
- chunksize=1,
2396
+ pool_terminate_flag = False if pool is not None else True
2397
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_bodyparts_to_multistring_skeleton.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2398
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_bodyparts_to_multistring_skeleton.__name__)
2399
+ for cnt, result in enumerate(pool.imap(GeometryMixin.bodyparts_to_multistring_skeleton, skeleton_data, chunksize=1)):
2400
+ if verbose:
2401
+ if not video_name and not animal_names:
2402
+ print(f"Computing skeleton {cnt + 1}/{len(data_df)}...")
2403
+ elif not video_name and animal_names:
2404
+ print(
2405
+ f"Computing skeleton {cnt + 1}/{len(data_df)} (Animals: {animal_names})..."
2352
2406
  )
2353
- ):
2354
- if verbose:
2355
- if not video_name and not animal_names:
2356
- print(f"Computing skeleton {cnt + 1}/{len(data_df)}...")
2357
- elif not video_name and animal_names:
2358
- print(
2359
- f"Computing skeleton {cnt + 1}/{len(data_df)} (Animals: {animal_names})..."
2360
- )
2361
- elif video_name and not animal_names:
2362
- print(
2363
- f"Computing skeleton {cnt + 1}/{len(data_df)} (Video: {video_name})..."
2364
- )
2365
- else:
2366
- print(
2367
- f"Computing skeleton {cnt + 1}/{len(data_df)} (Video: {video_name}, Animals: {animal_names})..."
2368
- )
2369
- results.append(result)
2370
-
2407
+ elif video_name and not animal_names:
2408
+ print(
2409
+ f"Computing skeleton {cnt + 1}/{len(data_df)} (Video: {video_name})..."
2410
+ )
2411
+ else:
2412
+ print(
2413
+ f"Computing skeleton {cnt + 1}/{len(data_df)} (Video: {video_name}, Animals: {animal_names})..."
2414
+ )
2415
+ results.append(result)
2416
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_bodyparts_to_multistring_skeleton.__name__)
2371
2417
  timer.stop_timer()
2372
2418
  stdout_success(
2373
2419
  msg="Multistring skeleton complete.",
@@ -2551,7 +2597,8 @@ class GeometryMixin(object):
2551
2597
  def multiframe_is_shape_covered(self,
2552
2598
  shape_1: List[Polygon],
2553
2599
  shape_2: List[Polygon],
2554
- core_cnt: Optional[int] = -1) -> List[bool]:
2600
+ core_cnt: Optional[int] = -1,
2601
+ pool: Optional[multiprocessing.Pool] = None) -> List[bool]:
2555
2602
  """
2556
2603
  For each shape in time-series of shapes, check if another shape in the same time-series fully covers the
2557
2604
  first shape.
@@ -2563,6 +2610,13 @@ class GeometryMixin(object):
2563
2610
  .. seealso::
2564
2611
  For single core method, see :func:`simba.mixins.geometry_mixin.GeometryMixin.is_shape_covered`
2565
2612
 
2613
+ :param List[Union[LineString, Polygon, MultiPolygon]] shape_1: List of geometries to check if covered.
2614
+ :param List[Union[LineString, Polygon, MultiPolygon]] shape_2: List of geometries that may cover shape_1.
2615
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which uses all available cores. Ignored if pool is provided.
2616
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2617
+ :return: List of booleans indicating if each shape_1 is covered by corresponding shape_2.
2618
+ :rtype: List[bool]
2619
+
2566
2620
  :example:
2567
2621
  >>> shape_1 = GeometryMixin().multiframe_bodyparts_to_polygon(data=np.random.randint(0, 200, (100, 6, 2)))
2568
2622
  >>> shape_2 = [Polygon([[0, 0], [20, 20], [20, 10], [10, 20]]) for x in range(len(shape_1))]
@@ -2602,14 +2656,12 @@ class GeometryMixin(object):
2602
2656
  core_cnt = find_core_cnt()[0]
2603
2657
  shapes = [list(x) for x in zip(shape_1, shape_2)]
2604
2658
  results = []
2605
- with multiprocessing.Pool(
2606
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2607
- ) as pool:
2608
- for cnt, mp_return in enumerate(
2609
- pool.imap(GeometryMixin.is_shape_covered, shapes, chunksize=1)
2610
- ):
2611
- results.append(mp_return)
2612
- terminate_cpu_pool(pool=pool, force=False)
2659
+ pool_terminate_flag = False if pool is not None else True
2660
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_is_shape_covered.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2661
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_is_shape_covered.__name__)
2662
+ for cnt, mp_return in enumerate(pool.imap(GeometryMixin.is_shape_covered, shapes, chunksize=1)):
2663
+ results.append(mp_return)
2664
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_is_shape_covered.__name__)
2613
2665
  return results
2614
2666
 
2615
2667
  @staticmethod
@@ -2774,7 +2826,8 @@ class GeometryMixin(object):
2774
2826
  lag: Optional[int] = 2,
2775
2827
  core_cnt: Optional[int] = -1,
2776
2828
  pixels_per_mm: int = 1,
2777
- parallel_offset: int = 1) -> np.ndarray:
2829
+ parallel_offset: int = 1,
2830
+ pool: Optional[multiprocessing.Pool] = None) -> np.ndarray:
2778
2831
 
2779
2832
  """
2780
2833
  Perform geometry histocomparison on multiple video frames using multiprocessing.
@@ -2788,11 +2841,12 @@ class GeometryMixin(object):
2788
2841
 
2789
2842
  :param Union[str, os.PathLike] video_path: Path to the video file.
2790
2843
  :param np.ndarray data: Input data, typically containing coordinates of one or several body-parts.
2791
- :param Literal['rectangle', 'circle'] shape_type: Type of shape for comparison.
2844
+ :param Literal['rectangle', 'circle', 'line'] shape_type: Type of shape for comparison.
2792
2845
  :param Optional[int] lag: Number of frames to lag between comparisons. Default is 2.
2793
- :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1 which is all available cores.
2794
- :param Optional[int] pixels_per_mm: Pixels per millimeter for conversion. Default is 1.
2795
- :param Optional[int] parallel_offset: Size of the geometry ROI in millimeters. Default 1.
2846
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1 which is all available cores. Ignored if pool is provided.
2847
+ :param int pixels_per_mm: Pixels per millimeter for conversion. Default is 1.
2848
+ :param int parallel_offset: Size of the geometry ROI in millimeters. Default 1.
2849
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
2796
2850
  :returns: The difference between the successive geometry histograms.
2797
2851
  :rtype: np.ndarray
2798
2852
 
@@ -2821,22 +2875,13 @@ class GeometryMixin(object):
2821
2875
  for i in range(core_cnt)
2822
2876
  ]
2823
2877
  results = [[0] * lag]
2824
- with multiprocessing.Pool(
2825
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
2826
- ) as pool:
2827
- constants = functools.partial(
2828
- GeometryMixin()._multifrm_geometry_histocomparison_helper,
2829
- video_path=video_path,
2830
- data=data,
2831
- shape_type=shape_type,
2832
- pixels_per_mm=pixels_per_mm,
2833
- parallel_offset=parallel_offset,
2834
- )
2835
- for cnt, result in enumerate(
2836
- pool.imap(constants, split_frm_idx, chunksize=1)
2837
- ):
2838
- results.append(result)
2839
-
2878
+ pool_terminate_flag = False if pool is not None else True
2879
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multifrm_geometry_histocomparison.__name__} pool', raise_error=True, accepted_cores=core_cnt)
2880
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multifrm_geometry_histocomparison.__name__)
2881
+ constants = functools.partial(GeometryMixin()._multifrm_geometry_histocomparison_helper,video_path=video_path,data=data,shape_type=shape_type,pixels_per_mm=pixels_per_mm,parallel_offset=parallel_offset)
2882
+ for cnt, result in enumerate(pool.imap(constants, split_frm_idx, chunksize=1)):
2883
+ results.append(result)
2884
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multifrm_geometry_histocomparison.__name__)
2840
2885
  return [item for sublist in results for item in sublist]
2841
2886
 
2842
2887
  @staticmethod
@@ -3258,7 +3303,8 @@ class GeometryMixin(object):
3258
3303
  geometries: Dict[Tuple[int, int], Polygon],
3259
3304
  fps: Optional[int] = None,
3260
3305
  core_cnt: Optional[int] = -1,
3261
- verbose: Optional[bool] = True) -> np.ndarray:
3306
+ verbose: Optional[bool] = True,
3307
+ pool: Optional[multiprocessing.Pool] = None) -> np.ndarray:
3262
3308
 
3263
3309
  """
3264
3310
  Compute the cumulative time a body-part has spent inside a grid of geometries using multiprocessing.
@@ -3269,9 +3315,11 @@ class GeometryMixin(object):
3269
3315
  :param np.ndarray data: Input data array where rows represent frames and columns represent body-part x and y coordinates.
3270
3316
  :param Dict[Tuple[int, int], Polygon] geometries: Dictionary of polygons representing spatial regions. E.g., created by :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_square` or :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_hexagon`.
3271
3317
  :param Optional[int] fps: Frames per second (fps) for time normalization. If None, cumulative sum of frame count is returned.
3272
- :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1 which is all available cores.
3318
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1 which is all available cores. Ignored if pool is provided.
3273
3319
  :param Optional[bool] verbose: If True, prints progress.
3274
- :returns:
3320
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3321
+ :returns: Matrix of size (frames x horizontal bins x vertical bins) with cumulative time values.
3322
+ :rtype: np.ndarray
3275
3323
 
3276
3324
  :example:
3277
3325
  >>> img_geometries = GeometryMixin.bucket_img_into_grid_square(img_size=(640, 640), bucket_grid_size=(10, 10), px_per_mm=1)
@@ -3299,18 +3347,14 @@ class GeometryMixin(object):
3299
3347
  frm_id = np.arange(0, data.shape[0]).reshape(-1, 1)
3300
3348
  data = np.hstack((frm_id, data))
3301
3349
  img_arr = np.zeros((data.shape[0], h + 1, w + 1))
3302
- with multiprocessing.Pool(
3303
- core_cnt, maxtasksperchild=Defaults.MAXIMUM_MAX_TASK_PER_CHILD.value
3304
- ) as pool:
3305
- constants = functools.partial(
3306
- self._cumsum_coord_geometries_helper,
3307
- geometries=geometries,
3308
- verbose=verbose,
3309
- )
3310
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3311
- if result[1] != -1:
3312
- img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3313
- terminate_cpu_pool(pool=pool, force=False)
3350
+ pool_terminate_flag = False if pool is not None else True
3351
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().cumsum_coord_geometries.__name__} pool', raise_error=True, accepted_cores=core_cnt)
3352
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().cumsum_coord_geometries.__name__)
3353
+ constants = functools.partial(self._cumsum_coord_geometries_helper, geometries=geometries, verbose=verbose)
3354
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3355
+ if result[1] != -1:
3356
+ img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3357
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().cumsum_coord_geometries.__name__)
3314
3358
  timer.stop_timer()
3315
3359
  stdout_success(
3316
3360
  msg="Cumulative coordinates in geometries complete",
@@ -3340,7 +3384,8 @@ class GeometryMixin(object):
3340
3384
  bool_data: np.ndarray,
3341
3385
  fps: Optional[float] = None,
3342
3386
  verbose: bool = True,
3343
- core_cnt: Optional[int] = -1) -> np.ndarray:
3387
+ core_cnt: Optional[int] = -1,
3388
+ pool: Optional[multiprocessing.Pool] = None) -> np.ndarray:
3344
3389
  """
3345
3390
  Compute the cumulative sums of boolean events within polygon geometries over time using multiprocessing. For example, compute the cumulative bout count of classified events within spatial locations at all time-points of the video.
3346
3391
 
@@ -3359,8 +3404,10 @@ class GeometryMixin(object):
3359
3404
  :param np.ndarray bool_data: Boolean array with shape (data.shape[0],) or (data.shape[0], 1) indicating the presence or absence in each frame.
3360
3405
  :param Optional[float] fps: Frames per second. If provided, the result is normalized by the frame rate.
3361
3406
  :param bool verbose: If true, prints progress. Default: True.
3362
- :param Optional[float] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which means using all available cores.
3363
- :returns: Matrix of size (frames x horizontal bins x verical bins) with times in seconds (if fps passed) or frames (if fps not passed)
3407
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which means using all available cores. Ignored if pool is provided.
3408
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3409
+ :returns: Matrix of size (frames x horizontal bins x vertical bins) with times in seconds (if fps passed) or frames (if fps not passed)
3410
+ :rtype: np.ndarray
3364
3411
  :rtype: np.ndarray
3365
3412
 
3366
3413
  :example:
@@ -3396,14 +3443,14 @@ class GeometryMixin(object):
3396
3443
  data = np.hstack((frm_id, data))
3397
3444
  img_arr = np.zeros((data.shape[0], h + 1, w + 1))
3398
3445
  data = data[np.argwhere((data[:, 3] == 1))].reshape(-1, 4)
3399
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
3400
- constants = functools.partial(self._cumsum_bool_helper,
3401
- geometries=geometries,
3402
- verbose=verbose)
3403
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3404
- if result[1] != -1:
3405
- img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3406
- terminate_cpu_pool(pool=pool, force=False)
3446
+ pool_terminate_flag = False if pool is not None else True
3447
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().cumsum_bool_geometries.__name__} pool', raise_error=True, accepted_cores=core_cnt)
3448
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().cumsum_bool_geometries.__name__)
3449
+ constants = functools.partial(self._cumsum_bool_helper, geometries=geometries, verbose=verbose)
3450
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3451
+ if result[1] != -1:
3452
+ img_arr[result[0], result[2] - 1, result[1] - 1] = 1
3453
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().cumsum_bool_geometries.__name__)
3407
3454
  if fps is None:
3408
3455
  return np.cumsum(img_arr, axis=0)
3409
3456
  else:
@@ -3430,7 +3477,8 @@ class GeometryMixin(object):
3430
3477
  grid: Dict[Tuple[int, int], Polygon],
3431
3478
  fps: Optional[int] = None,
3432
3479
  core_cnt: Optional[int] = -1,
3433
- verbose: Optional[bool] = True) -> np.ndarray:
3480
+ verbose: Optional[bool] = True,
3481
+ pool: Optional[multiprocessing.Pool] = None) -> np.ndarray:
3434
3482
  """
3435
3483
  Compute the cumulative time the animal has spent in each geometry.
3436
3484
 
@@ -3447,10 +3495,12 @@ class GeometryMixin(object):
3447
3495
 
3448
3496
  :param List[Polygon] data: List of polygons where every index represent a frame and every value the animal convex hull
3449
3497
  :param Dict[Tuple[int, int], Polygon] grid: Dictionary of polygons representing spatial regions. E.g., created by :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_square` or :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_hexagon`.
3450
- :param Optional[float] fps: Frames per second. If provided, the result is normalized by the frame rate.
3451
- :param Optional[float] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which means using all available cores.
3498
+ :param Optional[int] fps: Frames per second. If provided, the result is normalized by the frame rate.
3499
+ :param Optional[int] core_cnt: Number of CPU cores to use for parallel processing. Default is -1, which means using all available cores. Ignored if pool is provided.
3452
3500
  :param Optional[bool] verbose: If True, then prints progress.
3453
- :returns: Matrix of size (frames x horizontal bins x verical bins) with values representing time in seconds (if fps passed) or frames (if fps not passed)
3501
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3502
+ :returns: Matrix of size (frames x horizontal bins x vertical bins) with values representing time in seconds (if fps passed) or frames (if fps not passed)
3503
+ :rtype: np.ndarray
3454
3504
  :rtype: np.ndarray
3455
3505
  """
3456
3506
 
@@ -3467,12 +3517,13 @@ class GeometryMixin(object):
3467
3517
  frm_id = np.arange(0, len(data)).reshape(-1, 1)
3468
3518
  data = np.hstack((frm_id, np.array(data).reshape(-1, 1)))
3469
3519
  img_arr = np.zeros((data.shape[0], h + 1, w + 1))
3470
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
3471
- constants = functools.partial(self._cumsum_animal_geometries_grid_helper, grid=grid, size=(h, w),
3472
- verbose=verbose)
3473
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3474
- img_arr[cnt] = result
3475
-
3520
+ pool_terminate_flag = False if pool is not None else True
3521
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().cumsum_animal_geometries_grid.__name__} pool', raise_error=True, accepted_cores=core_cnt)
3522
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().cumsum_animal_geometries_grid.__name__)
3523
+ constants = functools.partial(self._cumsum_animal_geometries_grid_helper, grid=grid, size=(h, w), erbose=verbose)
3524
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3525
+ img_arr[cnt] = result
3526
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().cumsum_animal_geometries_grid.__name__)
3476
3527
  timer.stop_timer()
3477
3528
  stdout_success(msg="Cumulative animal geometries in grid complete", elapsed_time=timer.elapsed_time_str, )
3478
3529
  if fps is None:
@@ -3501,8 +3552,8 @@ class GeometryMixin(object):
3501
3552
  def geometry_transition_probabilities(data: np.ndarray,
3502
3553
  grid: Dict[Tuple[int, int], Polygon],
3503
3554
  core_cnt: Optional[int] = -1,
3504
- verbose: Optional[bool] = False) -> (
3505
- Dict[Tuple[int, int], float], Dict[Tuple[int, int], int]):
3555
+ verbose: Optional[bool] = False,
3556
+ pool: Optional[multiprocessing.Pool] = None) -> (Dict[Tuple[int, int], float], Dict[Tuple[int, int], int]):
3506
3557
  """
3507
3558
  Calculate geometry transition probabilities based on spatial transitions between grid cells.
3508
3559
 
@@ -3515,8 +3566,9 @@ class GeometryMixin(object):
3515
3566
  :param np.ndarray data: A 2D array where each row represents a point in space with two coordinates [x, y].
3516
3567
  :param Dict[Tuple[int, int], Polygon] grid: A dictionary mapping grid cell identifiers (tuple of int, int) to their corresponding polygon objects.
3517
3568
  Each grid cell is represented by a tuple key (e.g., (row, col)) and its spatial boundaries as a `Polygon`. Can be computed with E.g., created by :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_square` or :func:`simba.mixins.geometry_mixin.GeometryMixin.bucket_img_into_grid_hexagon`.
3518
- :param Optional[int] core_cnt: The number of cores to use for parallel processing. Default is -1, which uses the maximum available cores.
3569
+ :param Optional[int] core_cnt: The number of cores to use for parallel processing. Default is -1, which uses the maximum available cores. Ignored if pool is provided.
3519
3570
  :param Optional[bool] verbose: If True, the function will print additional information, including the elapsed time for processing.
3571
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3520
3572
  :return: A tuple containing two dictionaries:
3521
3573
  - A dictionary of transition probabilities between grid cells, where each key is a grid cell tuple (row, col),
3522
3574
  and each value is another dictionary representing the transition probabilities to other cells.
@@ -3533,20 +3585,20 @@ class GeometryMixin(object):
3533
3585
  """
3534
3586
 
3535
3587
  timer = SimbaTimer(start=True)
3536
- check_valid_array(data=data, source=GeometryMixin.geometry_transition_probabilities.__name__,
3537
- accepted_ndims=(2,), accepted_axis_1_shape=[2, ],
3538
- accepted_dtypes=Formats.NUMERIC_DTYPES.value)
3588
+ check_valid_array(data=data, source=GeometryMixin.geometry_transition_probabilities.__name__, accepted_ndims=(2,), accepted_axis_1_shape=[2, ], accepted_dtypes=Formats.NUMERIC_DTYPES.value)
3539
3589
  check_valid_dict(x=grid, valid_key_dtypes=(tuple,), valid_values_dtypes=(Polygon,))
3540
3590
  check_int(name="core_cnt", value=core_cnt, min_value=-1, unaccepted_vals=[0])
3541
3591
  if core_cnt == -1 or core_cnt > find_core_cnt()[0]: core_cnt = find_core_cnt()[0]
3542
3592
  frm_id = np.arange(0, data.shape[0]).reshape(-1, 1)
3543
3593
  data = np.hstack((frm_id, data)).reshape(-1, 3).astype(np.int32)
3544
3594
  data, results = np.array_split(data, core_cnt), []
3545
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
3546
- constants = functools.partial(GeometryMixin._compute_framewise_geometry_idx, grid=grid, verbose=verbose)
3547
- for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3548
- results.append(result)
3549
- terminate_cpu_pool(pool=pool, force=False)
3595
+ pool_terminate_flag = False if pool is not None else True
3596
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin.geometry_transition_probabilities.__name__} pool', raise_error=True, accepted_cores=core_cnt)
3597
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin.geometry_transition_probabilities.__name__)
3598
+ constants = functools.partial(GeometryMixin._compute_framewise_geometry_idx, grid=grid, verbose=verbose)
3599
+ for cnt, result in enumerate(pool.imap(constants, data, chunksize=1)):
3600
+ results.append(result)
3601
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin.geometry_transition_probabilities.__name__)
3550
3602
  del data
3551
3603
 
3552
3604
  results = np.vstack(results)[:, 1:].astype(np.int32)
@@ -3618,7 +3670,8 @@ class GeometryMixin(object):
3618
3670
  geometries: List[Union[Polygon, LineString]],
3619
3671
  lag: Optional[Union[float, int]] = 1,
3620
3672
  sample_rate: Optional[Union[float, int]] = 1,
3621
- core_cnt: Optional[int] = -1) -> List[float]:
3673
+ core_cnt: Optional[int] = -1,
3674
+ pool: Optional[multiprocessing.Pool] = None) -> List[float]:
3622
3675
  """
3623
3676
  The Hausdorff distance measure of the similarity between sequential time-series geometries.
3624
3677
 
@@ -3628,9 +3681,10 @@ class GeometryMixin(object):
3628
3681
  :param List[Union[Polygon, LineString]] geometries: List of geometries.
3629
3682
  :param Optional[Union[float, int]] lag: If int, then the number of frames preceeding the current frame to compare the geometry with. Eg., 1 compares the geometry to the immediately preceeding geometry. If float, then evaluated as seconds. E.g., 1 compares the geometry to the geometry 1s prior in the geometries list.
3630
3683
  :param Optional[Union[float, int]] sample_rate: The FPS of the recording. Used as conversion factor if lag is a float.
3631
- :param Optional[int] core_cnt: The number of cores to use for parallel processing. Default is -1, which uses the maximum available cores.
3684
+ :param Optional[int] core_cnt: The number of cores to use for parallel processing. Default is -1, which uses the maximum available cores. Ignored if pool is provided.
3685
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3632
3686
  :returns: List of Hausdorff distance measures.
3633
- :rtype: List[float].
3687
+ :rtype: List[float]
3634
3688
 
3635
3689
  :example:
3636
3690
  >>> df = read_df(file_path='/Users/simon/Desktop/envs/simba/troubleshooting/mouse_open_field/project_folder/csv/outlier_corrected_movement_location/SI_DAY3_308_CD1_PRESENT.csv', file_type='csv')
@@ -3669,15 +3723,12 @@ class GeometryMixin(object):
3669
3723
  for i in range(lag, len(geometries)):
3670
3724
  reshaped_geometries.append([[geometries[i - lag], geometries[i]]])
3671
3725
  results = []
3672
- with multiprocessing.Pool(
3673
- core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value
3674
- ) as pool:
3675
- for cnt, mp_return in enumerate(
3676
- pool.imap(
3677
- GeometryMixin.hausdorff_distance, reshaped_geometries, chunksize=1
3678
- )
3679
- ):
3680
- results.append(mp_return[0])
3726
+ pool_terminate_flag = False if pool is not None else True
3727
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin().multiframe_hausdorff_distance.__name__} pool', raise_error=True, accepted_cores=core_cnt)
3728
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin().multiframe_hausdorff_distance.__name__)
3729
+ for cnt, mp_return in enumerate(pool.imap(GeometryMixin.hausdorff_distance, reshaped_geometries, chunksize=1)):
3730
+ results.append(mp_return[0])
3731
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin().multiframe_hausdorff_distance.__name__)
3681
3732
  return results
3682
3733
 
3683
3734
  @staticmethod
@@ -3976,7 +4027,9 @@ class GeometryMixin(object):
3976
4027
  return results
3977
4028
 
3978
4029
  @staticmethod
3979
- def geometries_to_exterior_keypoints(geometries: List[Polygon], core_cnt: Optional[int] = -1) -> np.ndarray:
4030
+ def geometries_to_exterior_keypoints(geometries: List[Polygon],
4031
+ core_cnt: Optional[int] = -1,
4032
+ pool: Optional[multiprocessing.Pool] = None) -> np.ndarray:
3980
4033
  """
3981
4034
  Extract exterior keypoints from a list of Polygon geometries in parallel, with optional core count specification for multiprocessing.
3982
4035
 
@@ -3985,7 +4038,8 @@ class GeometryMixin(object):
3985
4038
  :align: center
3986
4039
 
3987
4040
  :param List[Polygon] geometries: A list of Shapely `Polygon` objects representing geometries whose exterior keypoints will be extracted.
3988
- :param Optional[int] core_cnt: The number of CPU cores to use for multiprocessing. If -1, it uses the maximum number of available cores.
4041
+ :param Optional[int] core_cnt: The number of CPU cores to use for multiprocessing. If -1, it uses the maximum number of available cores. Ignored if pool is provided.
4042
+ :param Optional[multiprocessing.Pool] pool: Optional multiprocessing pool to reuse. If None, creates a new pool. Default None.
3989
4043
  :return: A numpy array of exterior keypoints extracted from the input geometries.
3990
4044
  :rtype: np.ndarray
3991
4045
 
@@ -3997,15 +4051,16 @@ class GeometryMixin(object):
3997
4051
  """
3998
4052
  check_int(name="CORE COUNT", value=core_cnt, min_value=-1, max_value=find_core_cnt()[0], raise_error=True)
3999
4053
  if core_cnt == -1: core_cnt = find_core_cnt()[0]
4000
- check_valid_lst(data=geometries, source=GeometryMixin.geometries_to_exterior_keypoints.__name__,
4001
- valid_dtypes=(Polygon,), min_len=1)
4054
+ check_valid_lst(data=geometries, source=GeometryMixin.geometries_to_exterior_keypoints.__name__, valid_dtypes=(Polygon,), min_len=1)
4002
4055
  results = []
4003
4056
  geometries = np.array_split(geometries, 3)
4004
- with multiprocessing.Pool(core_cnt, maxtasksperchild=Defaults.LARGE_MAX_TASK_PER_CHILD.value) as pool:
4005
- for cnt, mp_return in enumerate(
4006
- pool.imap(GeometryMixin._geometries_to_exterior_keypoints_helper, geometries, chunksize=1)):
4007
- results.append(mp_return)
4057
+ pool_terminate_flag = False if pool is not None else True
4058
+ if pool is not None: check_valid_cpu_pool(value=pool, source=f'{GeometryMixin.geometries_to_exterior_keypoints.__name__} pool', raise_error=True, accepted_cores=core_cnt)
4059
+ else: pool = get_cpu_pool(core_cnt=core_cnt, source=GeometryMixin.geometries_to_exterior_keypoints.__name__)
4060
+ for cnt, mp_return in enumerate(pool.imap(GeometryMixin._geometries_to_exterior_keypoints_helper, geometries, chunksize=1)):
4061
+ results.append(mp_return)
4008
4062
  results = [i for xs in results for i in xs]
4063
+ if pool_terminate_flag: terminate_cpu_pool(pool=pool, source=GeometryMixin.geometries_to_exterior_keypoints.__name__)
4009
4064
  return np.ascontiguousarray(np.array(results)).astype(np.int32)
4010
4065
 
4011
4066
  @staticmethod