Skip to content

DataRegistration Class

Register qi2lab 3D MERFISH data using multiview-stitcher.

This module registers fiducial rounds to the first fiducial round, saves the unwarped deconvolved readout data and U-FISH predictions, and records the transforms needed by pixel decoding. If enabled, SOFIMA estimates a residual deformable field after affine registration. The field is saved as an (3, z, y, x) XYZ-channel map whose vectors point from round001 reference pixels toward the affine-initialized moving round; the map origin is the SOFIMA patch center, not the image corner.

History:
  • 2025/07:
    • Implement anistropic downsampling for registration.
    • Implement RLGC deconvolution.
    • Implement GPU based pixel-warping strategy.
    • Implement multi-GPU processing.
  • 2024/12: Refactor repo structure.
  • 2024/08:
    • Switched to qi2labdatastore for data access.
    • Implemented numba-accelerated downsampling.
    • Cleaned numpy usage for ryomen tiling.
  • 2024/07: Integrated pycudadecon and removed Dask usage.
  • 2024/04: Updated for U-FISH, removed SPOTS3D.
  • 2024/01: Adjusted for qi2lab MERFISH file format v0.1.
  • 2023/09: Initial commit.

Classes:

Name Description
DataRegistration

Register 2D or 3D MERFISH data across rounds.

GlobalRegistrationConfig

Configure global multiview-stitcher registration behavior.

DataRegistration

Register 2D or 3D MERFISH data across rounds.

Parameters:

Name Type Description Default
datastore qi2labDataStore

Initialized qi2labDataStore object

required
decon_fiducial bool

Deconvolve ALL fiducial rounds. False = only deconvolve round 1 for downstream stitching.

True
decon_readout bool

Deconvolve readout images before saving unwarped readout data for decode-time registration.

False
overwrite_outputs bool

Overwrite existing local transforms and preprocessing outputs.

False
perform_deformable_registration bool

Estimate and save a residual SOFIMA deformable flow field after affine fiducial registration.

True
num_gpus int

Number of GPUs to use for registration.

1
crop_yx_decon int

Crop size for deconvolution applied to both y and x dimensions.

2048
ufish_model str | Path | None

U-FISH model to use for feature prediction. If omitted or None, use the package default simfish weights. Known aliases include simfish, merfish, seqfish, deepspot, and exseq. A local .onnx/.pth path or HuggingFace weights filename can also be supplied.

None
verbose int

Progress verbosity. Set to 0 to suppress routine progress prints.

1

Methods:

Name Description
__init__

Initialize the object.

apply_registration_to_one_tile

Apply existing local registrations to readout bits for one tile.

fuse_global_fiducial_views

Write registered views on the downsampled fiducial segmentation grid.

fuse_global_registered

Fuse fiducials, registering globally first when transforms are absent.

global_register

Globally register first-round fiducial tiles and write fused OME-Zarr.

load_global_fiducial_views

Load native fiducial views with stage or stored global transforms.

register_all_tiles

Register all tiles.

register_one_tile

Register one tile.

Attributes:

Name Type Description
datastore qi2labDataStore

Return the qi2labDataStore object.

overwrite_outputs bool

Get the overwrite_outputs flag.

perform_deformable_registration bool

Get the deformable registration flag.

tile_id str | None

Get the current tile id.

Source code in src/merfish3danalysis/DataRegistration.py
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
class DataRegistration:
    """Register 2D or 3D MERFISH data across rounds.

    Parameters
    ----------
    datastore : qi2labDataStore
        Initialized qi2labDataStore object
    decon_fiducial: bool, default True
        Deconvolve ALL fiducial rounds. False = only deconvolve round 1 for downstream
        stitching.
    decon_readout: bool, default False
        Deconvolve readout images before saving unwarped readout data for
        decode-time registration.
    overwrite_outputs: bool, default False
        Overwrite existing local transforms and preprocessing outputs.
    perform_deformable_registration: bool, default True
        Estimate and save a residual SOFIMA deformable flow field after affine
        fiducial registration.
    num_gpus: int, default 1
        Number of GPUs to use for registration.
    crop_yx_decon: int, default 2048
        Crop size for deconvolution applied to both y and x dimensions.
    ufish_model: str or pathlib.Path or None, default None
        U-FISH model to use for feature prediction. If omitted or ``None``, use
        the package default ``simfish`` weights. Known aliases include
        ``simfish``, ``merfish``, ``seqfish``,
        ``deepspot``, and ``exseq``. A local ``.onnx``/``.pth`` path or
        HuggingFace weights filename can also be supplied.
    verbose : int, default 1
        Progress verbosity. Set to 0 to suppress routine progress prints.
    """

    def __init__(
        self,
        datastore: qi2labDataStore,
        decon_fiducial: bool = True,
        decon_readout: bool = False,
        overwrite_outputs: bool = False,
        perform_deformable_registration: bool = True,
        num_gpus: int = 1,
        crop_yx_decon: int = 2048,
        ufish_model: str | Path | None = None,
        global_registration: bool = False,
        registration_diagnostics: bool = False,
        sofima_config: SofimaRegistrationConfig | None = None,
        global_registration_config: GlobalRegistrationConfig | None = None,
        verbose: int = 1,
    ) -> None:
        """
        Initialize the object.

        Parameters
        ----------
        datastore : qi2labDataStore
            Datastore containing corrected images, PSFs, metadata, and output
            groups.
        decon_fiducial : bool
            If True, deconvolve fiducial images before registration.
        decon_readout : bool
            If True, deconvolve readout images before U-FISH prediction and
            warping.
        overwrite_outputs : bool
            If True, regenerate local transforms, optional decon images, and
            feature predictor outputs even when they already exist.
        perform_deformable_registration : bool
            If True, estimate a SOFIMA flow field after affine registration.
            The saved field uses channel order X, Y, Z; spatial order Z, Y, X;
            and a patch-centered map origin. Pixel decoding composes this field
            with affine and chromatic transforms when readout images are loaded.
        num_gpus : int
            Number of GPUs available for deconvolution and readout processing.
        crop_yx_decon : int
            Initial lateral RLGC crop size. GPU memory fallback can reduce this
            value and cache the successful size.
        ufish_model : str | Path | None
            U-FISH model alias, weights filename, local path, or None for the
            default model.
        global_registration : bool, default=False
            If True, run global tile registration and fused fiducial OME-Zarr
            generation after local tile preprocessing.
        registration_diagnostics : bool, default=False
            If True, print detailed registration diagnostics.
        sofima_config : SofimaRegistrationConfig or None, default=None
            Explicit SOFIMA deformable registration parameters.
        global_registration_config : GlobalRegistrationConfig or None, default=None
            Explicit multiview-stitcher global registration parameters.
        verbose : int
            Verbosity level for progress messages.
        """
        self._datastore = datastore
        self._decon_fiducial = decon_fiducial
        self._tile_ids = self._datastore.tile_ids
        self._round_ids = self._datastore.round_ids
        self._bit_ids = self._datastore.bit_ids
        self._psfs = self._datastore.channel_psfs
        self._num_gpus = num_gpus
        self._crop_yx_decon = crop_yx_decon
        self._perform_deformable_registration = perform_deformable_registration
        self._tile_id: str | None = None
        self._overwrite_outputs = overwrite_outputs
        self._decon_readout = decon_readout
        self._ufish_model = ufish_model
        self._global_registration = global_registration
        self._registration_diagnostics = bool(registration_diagnostics)
        self._sofima_config = sofima_config or SofimaRegistrationConfig()
        self._global_registration_config = (
            global_registration_config or GlobalRegistrationConfig()
        )
        self._verbose = verbose

    # -----------------------------------
    # property access for class variables
    # -----------------------------------
    @property
    def datastore(self) -> qi2labDataStore:
        """Return the qi2labDataStore object.

        Returns
        -------
        qi2labDataStore
            qi2labDataStore object
        """
        return self._datastore

    @property
    def tile_id(self) -> str | None:
        """Get the current tile id.

        Returns
        -------
        tile_id: int or str
            Tile id
        """
        return self._tile_id

    @tile_id.setter
    def tile_id(self, value: int | str) -> None:
        """Set the tile id.

        Parameters
        ----------
        value : int or str
            Tile id
        """
        if isinstance(value, int):
            if value < 0 or value >= self._datastore.num_tiles:
                print("Set value index >=0 and <" + str(self._datastore.num_tiles))
                return None
            else:
                self._tile_id = self._datastore.tile_ids[value]
        elif isinstance(value, str):
            if value not in self._datastore.tile_ids:
                print("set valid tile id")
                return None
            else:
                self._tile_id = value

    @property
    def perform_deformable_registration(self) -> bool:
        """Get the deformable registration flag.

        Returns
        -------
        bool
            True when SOFIMA deformable flow-field registration is enabled.
        """
        return self._perform_deformable_registration

    @perform_deformable_registration.setter
    def perform_deformable_registration(self, value: bool) -> None:
        """Set the deformable registration flag.

        Parameters
        ----------
        value : bool
            True to enable SOFIMA deformable flow-field registration.
        """
        self._perform_deformable_registration = value

    @property
    def overwrite_outputs(self) -> bool:
        """Get the overwrite_outputs flag.

        Returns
        -------
        bool
            Overwrite existing local transforms and preprocessing outputs.
        """
        return self._overwrite_outputs

    @overwrite_outputs.setter
    def overwrite_outputs(self, value: bool) -> None:
        """Set the overwrite_outputs flag.

        Parameters
        ----------
        value : bool
            Overwrite existing local transforms and preprocessing outputs.
        """
        self._overwrite_outputs = value

    def _entity_root(
        self,
        tile_id: str,
        round_id: str | None = None,
        bit_id: str | None = None,
    ) -> Path:
        """
        Return the native tile/round or tile/bit image directory.

        Parameters
        ----------
        tile_id : str
            Stored tile identifier; None selects the current tile.
        round_id : str | None
            Stored fiducial round identifier.
        bit_id : str | None
            Stored readout bit identifier.

        Returns
        -------
        Path
            Native tile/round or tile/bit directory containing the image stores.
        """
        if (round_id is None and bit_id is None) or (
            round_id is not None and bit_id is not None
        ):
            raise ValueError("Provide either round_id or bit_id, but not both.")

        return self._datastore.local_image_path(
            tile_id, "corrected_data", round=round_id, bit=bit_id
        ).parent

    def _has_valid_deconvolved_fiducial_image(
        self,
        tile_id: str | None = None,
        round_id: str | None = None,
    ) -> bool:
        """
        Has valid native-frame deconvolved fiducial image.

        Parameters
        ----------
        tile_id : str | None
            Stored tile identifier; None selects the current tile.
        round_id : str | None
            Stored fiducial round identifier.

        Returns
        -------
        bool
            True when ``decon_data`` exists and matches the corrected image shape.
        """
        if round_id is None:
            raise ValueError("round_id is required for fiducial images.")

        tile_id = self._tile_id if tile_id is None else tile_id
        entity_root = self._entity_root(tile_id=tile_id, round_id=round_id)
        corrected_shape = self._datastore.image_shape(
            entity_root / Path("corrected_data")
        )
        decon_shape = self._datastore.image_shape(entity_root / Path("decon_data"))
        return corrected_shape is not None and decon_shape == corrected_shape

    def _has_valid_deconvolved_readout_image(
        self,
        tile_id: str | None = None,
        bit_id: str | None = None,
    ) -> bool:
        """
        Has valid native-frame deconvolved readout image.

        Parameters
        ----------
        tile_id : str | None
            Tile identifier.
        bit_id : str | None
            Bit identifier.

        Returns
        -------
        bool
            True when ``decon_data`` exists and matches the corrected image shape.
        """
        tile_id = self._tile_id if tile_id is None else tile_id
        if bit_id is None:
            raise ValueError("bit_id is required for readout images.")

        entity_root = self._entity_root(tile_id=tile_id, bit_id=bit_id)
        corrected_shape = self._datastore.image_shape(
            entity_root / Path("corrected_data")
        )
        readout_shape = self._datastore.image_shape(entity_root / Path("decon_data"))
        return corrected_shape is not None and readout_shape == corrected_shape

    def _has_valid_feature_predictor_outputs(
        self,
        tile_id: str | None = None,
        bit_id: str | None = None,
    ) -> bool:
        """
        Has valid feature predictor outputs.

        Parameters
        ----------
        tile_id : str | None
            Stored tile identifier; None selects the current tile.
        bit_id : str | None
            Stored readout bit identifier.

        Returns
        -------
        bool
            True when predictor and corrected image shapes match and spot localizations exist.
        """
        tile_id = self._tile_id if tile_id is None else tile_id
        if bit_id is None:
            raise ValueError("bit_id is required for feature predictor outputs.")

        entity_root = self._entity_root(tile_id=tile_id, bit_id=bit_id)
        corrected_shape = self._datastore.image_shape(
            entity_root / Path("corrected_data")
        )
        feature_shape = self._datastore.image_shape(
            entity_root / Path(f"{self._datastore.feature_predictor_folder_name}_data")
        )
        spots_path = self._datastore.local_feature_predictor_spots_path(tile_id, bit_id)
        return (
            corrected_shape is not None
            and feature_shape == corrected_shape
            and spots_path.exists()
        )

    def _is_tile_complete(
        self,
        tile_id: str,
        *,
        process_readouts: bool = True,
    ) -> bool:
        """
        Is tile complete.

        Parameters
        ----------
        tile_id : str
            Tile identifier.
        process_readouts : bool, default=True
            If False, assess only fiducial deconvolution and local transforms.

        Returns
        -------
        bool
            True when every requested image and local transform is already present.
        """
        if self._decon_fiducial:
            for round_id in self._round_ids:
                if not self._has_valid_deconvolved_fiducial_image(
                    tile_id=tile_id,
                    round_id=round_id,
                ):
                    return False

        for round_id in self._round_ids:
            if (
                self._datastore.load_local_round_transform_zyx_um(
                    tile=tile_id,
                    round=round_id,
                )
                is None
            ):
                return False

        if self._perform_deformable_registration:
            for round_id in self._round_ids[1:]:
                field = self._datastore.load_local_sofima_flow_field(
                    tile=tile_id, round=round_id, return_future=None
                )
                if field is None or field[0] is None:
                    return False

        if not process_readouts:
            return True

        for bit_id in self._bit_ids:
            if self._decon_readout and not self._has_valid_deconvolved_readout_image(
                tile_id=tile_id, bit_id=bit_id
            ):
                return False
            if not self._has_valid_feature_predictor_outputs(
                tile_id=tile_id, bit_id=bit_id
            ):
                return False

        return True

    def register_all_tiles(self, *, process_readouts: bool = True) -> None:
        """
        Register all tiles.

        Parameters
        ----------
        process_readouts : bool, default=True
            If True, also regenerate readout deconvolution and U-FISH outputs.
            Set False to recompute only the complete fiducial registration
            chain: local affine transforms, optional SOFIMA fields, global
            transforms, and global fiducial fusion.
        """
        tile_ids = list(self._datastore.tile_ids)
        start_idx = 0
        if not self._overwrite_outputs:
            for idx, tile_id in enumerate(tile_ids):
                if self._is_tile_complete(
                    tile_id,
                    process_readouts=process_readouts,
                ):
                    start_idx = idx + 1
                    continue
                start_idx = idx
                break
            else:
                start_idx = len(tile_ids)

            if start_idx >= len(tile_ids):
                if self._verbose >= 1:
                    print(
                        time_stamp(),
                        "All tiles already have complete preprocessing outputs.",
                    )
            elif start_idx > 0:
                if self._verbose >= 1:
                    print(
                        time_stamp(),
                        f"Resuming local registration at tile id: {tile_ids[start_idx]}.",
                    )

        for tile_id in tile_ids[start_idx:]:
            self.tile_id = tile_id
            self._generate_registrations()
            if process_readouts:
                self._apply_registration_to_bits()

        if self._global_registration:
            self.global_register()

    def register_one_tile(self, tile_id: int | str) -> None:
        """Register one tile.

        Parameters
        ----------
        tile_id : int or str
            Tile id
        """
        self.tile_id = tile_id
        if tile_id not in self._tile_ids and tile_id not in range(len(self._tile_ids)):
            return
        self._generate_registrations()
        self._apply_registration_to_bits()

    def apply_registration_to_one_tile(self, tile_id: int | str) -> None:
        """
        Apply existing local registrations to readout bits for one tile.

        This uses the local transforms already stored in the datastore. It does
        not estimate or overwrite fiducial registrations.

        Parameters
        ----------
        tile_id : int or str
            Tile identifier.
        """
        self.tile_id = tile_id
        if tile_id not in self._tile_ids and tile_id not in range(len(self._tile_ids)):
            return
        self._apply_registration_to_bits()

    def load_global_fiducial_views(
        self,
        *,
        use_stored_global_transforms: bool = False,
    ) -> list[Any]:
        """Load native fiducial views with stage or stored global transforms.

        Parameters
        ----------
        use_stored_global_transforms : bool, default False
            Attach saved global transforms instead of only stage metadata.

        Returns
        -------
        list
            Multiview-stitcher images in datastore tile order. Pixels remain lazy.
        """
        from multiview_stitcher import msi_utils, param_utils

        stage_transform_key = "stage_metadata"
        global_transform_key = "global_registered"
        voxel_zyx_um = round_spacing_um(self._datastore.voxel_size_zyx_um)
        if (
            voxel_zyx_um.shape != (3,)
            or not np.all(np.isfinite(voxel_zyx_um))
            or np.any(voxel_zyx_um <= 0)
        ):
            raise ValueError(
                "Global registration requires three positive finite voxel sizes in Z, Y, X order."
            )
        scale = {
            "z": float(voxel_zyx_um[0]),
            "y": float(voxel_zyx_um[1]),
            "x": float(voxel_zyx_um[2]),
        }
        reference_round_id = self._round_ids[0]
        msims = []

        for tile_index, tile_id in enumerate(self._tile_ids):
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    "Loading global registration input "
                    f"tile={tile_id} ({tile_index + 1}/{len(self._tile_ids)})",
                )
            tile_position_zyx_um, affine_zyx_px = (
                self._datastore.load_local_stage_position_zyx_um(
                    tile_id, reference_round_id
                )
            )
            tile_grid_positions = {
                "z": float(np.round(tile_position_zyx_um[0], 2)),
                "y": float(np.round(tile_position_zyx_um[1], 2)),
                "x": float(np.round(tile_position_zyx_um[2], 2)),
            }
            input_path = _local_fiducial_path(
                datastore=self._datastore,
                tile_id=tile_id,
                round_id=reference_round_id,
            )
            sim = _read_fiducial_sim(
                input_path=input_path,
                scale=scale,
                translation=tile_grid_positions,
                affine_zyx_px=affine_zyx_px,
                transform_key=stage_transform_key,
            )
            msim = msi_utils.get_msim_from_sim(sim, scale_factors=[])

            if use_stored_global_transforms:
                affine_zyx_um, _origin_zyx_um, _spacing_zyx_um = (
                    self._datastore.load_global_coord_xforms_um(tile=tile_id)
                )
                if affine_zyx_um is None:
                    raise RuntimeError(
                        "Stored global transform is required for direct global "
                        f"fusion, but none was found for tile {tile_id!r}."
                    )
                msi_utils.set_affine_transform(
                    msim,
                    param_utils.affine_to_xaffine(
                        np.asarray(affine_zyx_um, dtype=np.float32)
                    ),
                    transform_key=global_transform_key,
                    base_transform_key=stage_transform_key,
                )

            msims.append(msim)
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    f"Loaded global registration input tile={tile_id}",
                )
            gc.collect()

        return msims

    def fuse_global_fiducial_views(
        self,
        msims: list[Any],
        *,
        create_max_proj_tiff: bool = True,
    ) -> None:
        """Write registered views on the downsampled fiducial segmentation grid.

        Parameters
        ----------
        msims : list
            Multiview-stitcher images carrying global_registered transforms.
        create_max_proj_tiff : bool, default True
            Also write the maximum-Z OME-TIFF used by Cellpose.
        """
        from multiview_stitcher import fusion, misc_utils, msi_utils
        from multiview_stitcher import spatial_image_utils as si_utils
        from yaozarrs import open_group

        global_transform_key = "global_registered"
        output_zarr_path = self._datastore.fused_image_path()
        # downsample Y/X toward Z spacing for segmentation and ROI coordinates
        voxel_zyx_um = round_spacing_um(self._datastore.voxel_size_zyx_um)
        output_spacing = {
            "z": voxel_zyx_um[0],
            "y": round_pixel_size_um(
                voxel_zyx_um[1] * np.round(voxel_zyx_um[0] / voxel_zyx_um[1], 1)
            ),
            "x": round_pixel_size_um(
                voxel_zyx_um[2] * np.round(voxel_zyx_um[0] / voxel_zyx_um[2], 1)
            ),
        }

        if self._verbose >= 1:
            print(time_stamp(), "Starting global fiducial fusion.")
        fusion_start_time = timeit.default_timer()
        fused_msim = fusion.fuse(
            images=msims,
            transform_key=global_transform_key,
            output_spacing=output_spacing,
            output_zarr_url=str(output_zarr_path),
            **_direct_zarr_fusion_kwargs(misc_utils=misc_utils),
        )
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished fusion.fuse direct-to-zarr "
                f"elapsed_s={timeit.default_timer() - fusion_start_time:.2f}",
            )

        metadata_start_time = timeit.default_timer()
        affine = msi_utils.get_transform_from_msim(
            fused_msim,
            transform_key=global_transform_key,
        ).data.squeeze()
        fused_scale0 = msi_utils.get_sim_from_msim(fused_msim)
        origin = si_utils.get_origin_from_sim(fused_scale0, asarray=True)
        spacing = round_spacing_um(
            si_utils.get_spacing_from_sim(fused_scale0, asarray=True)
        )

        qi2labDataStore.save_image_metadata(
            image_path=output_zarr_path,
            extra_attributes={
                "affine_zyx_um": np.asarray(affine, dtype=np.float32).tolist(),
                "origin_zyx_um": np.asarray(origin, dtype=np.float32).tolist(),
                "spacing_zyx_um": round_spacing_um(spacing).tolist(),
            },
            merge=True,
        )
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished fused metadata write "
                f"elapsed_s={timeit.default_timer() - metadata_start_time:.2f}",
            )

        del fused_msim
        gc.collect()

        if create_max_proj_tiff:
            projection_start_time = timeit.default_timer()
            cellpose_path = (
                self._datastore.datastore_path / Path("segmentation") / Path("cellpose")
            )
            cellpose_path.mkdir(exist_ok=True)
            filename_path = cellpose_path / Path("fiducial_max_projection.ome.tiff")
            # The TIFF tile iterator requires zarr-python slicing and chunk
            # metadata; yaozarrs opens the store and supplies that adapter.
            fused_array = open_group(output_zarr_path)["0"].to_zarr_python()
            _write_zarr_max_projection_tiff(
                array=fused_array,
                filename_path=filename_path,
                spacing_zyx_um=np.asarray(spacing, dtype=np.float32),
            )
            del fused_array
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    "Finished fused max-projection TIFF "
                    f"elapsed_s={timeit.default_timer() - projection_start_time:.2f}",
                )

        datastore_state = self._datastore.datastore_state.copy()
        datastore_state.update({"GlobalRegistered": True, "Fused": True})
        self._datastore.datastore_state = datastore_state

        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished global fiducial fusion "
                f"elapsed_s={timeit.default_timer() - fusion_start_time:.2f}",
            )

    def global_register(
        self,
        create_max_proj_tiff: bool = True,
    ) -> None:
        """
        Globally register first-round fiducial tiles and write fused OME-Zarr.

        The method reads each tile's first fiducial round, combines stage
        metadata with multiview-stitcher global registration, saves per-tile
        global transforms back to the datastore, and fuses the transformed
        views directly into the datastore as OME-Zarr v0.5.

        Parameters
        ----------
        create_max_proj_tiff : bool, default=True
            If True, write ``segmentation/cellpose/fiducial_max_projection.ome.tiff``
            from the downsampled fused fiducial OME-Zarr, retaining its spacing.

        Returns
        -------
        None
            Global transforms, fused fiducial OME-Zarr, datastore state, and
            optional max projection are written to the datastore.

        Notes
        -----
        For one tile, save its stage origin and an identity registration
        correction without a pairwise fit. Use ``fuse_global_registered`` to also produce its mosaic.
        """
        from dask import config as dask_config
        from dask.diagnostics import ProgressBar
        from multiview_stitcher import (
            msi_utils,
            param_utils,
            registration,
        )
        from multiview_stitcher import spatial_image_utils as si_utils

        if not self._tile_ids:
            raise ValueError("Global registration requires at least one tile.")
        if len(self._tile_ids) == 1:
            origin, _camera_to_stage = self._datastore.load_local_stage_position_zyx_um(
                self._tile_ids[0], self._round_ids[0]
            )
            self._datastore.save_global_coord_xforms_um(
                affine_zyx_um=np.eye(4, dtype=np.float32),
                origin_zyx_um=origin,
                spacing_zyx_um=round_spacing_um(self._datastore.voxel_size_zyx_um),
                tile=self._tile_ids[0],
            )
            state = self._datastore.datastore_state.copy()
            state["GlobalRegistered"] = True
            self._datastore.datastore_state = state
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    "Skipping global registration because datastore has one tile.",
                )
            return

        if self._verbose >= 1:
            print(time_stamp(), "Starting global fiducial registration.")

        msims = self.load_global_fiducial_views(
            use_stored_global_transforms=False,
        )

        registration_config = self._global_registration_config
        stage_transform_key = "stage_metadata"
        global_transform_key = "global_registered"
        registration_binning = dict(
            zip(
                "zyx",
                (int(value) for value in registration_config.registration_binning_zyx),
                strict=True,
            )
        )
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Running multiview-stitcher global registration "
                f"tiles={len(msims)} registration_binning={registration_binning} "
                "pre_registration_pruning_method=None "
                "post_registration_do_quality_filter="
                f"{registration_config.post_registration_do_quality_filter} "
                "n_parallel_pairwise_regs="
                f"{registration_config.n_parallel_pairwise_regs}",
            )
        registration_start_time = timeit.default_timer()
        progress = ProgressBar() if self._verbose >= 1 else nullcontext()
        with (
            dask_config.set(scheduler=registration_config.dask_scheduler),
            progress,
        ):
            global_transforms = registration.register(
                msims,
                reg_channel_index=int(registration_config.reg_channel_index),
                transform_key=stage_transform_key,
                new_transform_key=global_transform_key,
                pre_registration_pruning_method=None,
                registration_binning=registration_binning,
                post_registration_do_quality_filter=bool(
                    registration_config.post_registration_do_quality_filter
                ),
                groupwise_resolution_kwargs={
                    "reference_view": int(registration_config.reference_view),
                    "transform": registration_config.transform,
                },
                n_parallel_pairwise_regs=int(
                    registration_config.n_parallel_pairwise_regs
                ),
            )
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished multiview-stitcher global registration "
                f"elapsed_s={timeit.default_timer() - registration_start_time:.2f}",
            )

        for tile_idx, (msim, transform) in enumerate(
            zip(msims, global_transforms, strict=True)
        ):
            affine = np.asarray(transform).squeeze()
            if registration_config.affine_round_decimals is not None:
                affine = np.round(
                    affine,
                    int(registration_config.affine_round_decimals),
                )
            msi_utils.set_affine_transform(
                msim,
                param_utils.affine_to_xaffine(affine),
                transform_key=global_transform_key,
                base_transform_key=stage_transform_key,
            )
            sim = msi_utils.get_sim_from_msim(msim)
            origin = si_utils.get_origin_from_sim(sim, asarray=True)
            spacing = round_spacing_um(si_utils.get_spacing_from_sim(sim, asarray=True))
            self._datastore.save_global_coord_xforms_um(
                affine_zyx_um=affine,
                origin_zyx_um=origin,
                spacing_zyx_um=spacing,
                tile=tile_idx,
            )

        self.fuse_global_fiducial_views(
            msims=msims,
            create_max_proj_tiff=create_max_proj_tiff,
        )

        if self._verbose >= 1:
            print(time_stamp(), "Finished global fiducial registration.")

    def fuse_global_registered(
        self,
        create_max_proj_tiff: bool = True,
    ) -> None:
        """
        Fuse fiducials, registering globally first when transforms are absent.

        Existing per-tile global transforms are reused when every tile has one.
        Before normal preprocessing, or for a partially registered datastore,
        global registration is run from the first-round fiducials before the
        fused OME-Zarr is created.

        Parameters
        ----------
        create_max_proj_tiff : bool, default=True
            If True, write ``segmentation/cellpose/fiducial_max_projection.ome.tiff``
            from the downsampled fused fiducial OME-Zarr, retaining its spacing.

        Returns
        -------
        None
            Fused fiducial OME-Zarr, datastore state, and optional max
            projection are written to the datastore.
        """
        if not self._global_transforms_available():
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    "Stored global transforms are incomplete; running global "
                    "registration before fusion.",
                )
            self.global_register(create_max_proj_tiff=create_max_proj_tiff)
            if len(self._tile_ids) != 1:
                return

        if self._verbose >= 1:
            print(
                time_stamp(),
                "Starting global fiducial fusion from stored transforms.",
            )

        msims = self.load_global_fiducial_views(
            use_stored_global_transforms=True,
        )
        self.fuse_global_fiducial_views(
            msims=msims,
            create_max_proj_tiff=create_max_proj_tiff,
        )

        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished global fiducial fusion from stored transforms.",
            )

    def _global_transforms_available(self) -> bool:
        """
        Check whether every tile has a stored global affine transform.

        Returns
        -------
        bool
            ``True`` only when every tile has a non-null global affine.
        """
        for tile_id in self._tile_ids:
            affine_zyx_um, _origin_zyx_um, _spacing_zyx_um = (
                self._datastore.load_global_coord_xforms_um(tile=tile_id)
            )
            if affine_zyx_um is None:
                return False
        return True

    def _generate_registrations(self) -> None:
        """
        Deconvolve fiducials, register rounds, and save local transforms.

        The first fiducial round is loaded, optionally deconvolved, saved, and
        stored as the local reference for the tile. Remaining fiducial rounds
        are split across GPU worker processes. Each worker loads its assigned
        rounds, optionally deconvolves them, registers them to round 1, saves the
        physical-space transform, and estimates SOFIMA residuals when requested.

        Returns
        -------
        None
            Results are written to the datastore.
        """
        if self._num_gpus == 0:
            raise RuntimeError(
                "No GPUs detected. Cannot run _generate_registrations()."
            )

        start_time = timeit.default_timer()
        reference_round_id = self._round_ids[0]

        import cupy as cp

        from merfish3danalysis.utils.rlgc import chunked_rlgc, clear_rlgc_caches

        cp.cuda.Device(0).use()
        reference_image = _load_deconvolve_fiducial_round(
            dr=self,
            round_id=reference_round_id,
            gpu_id=0,
            chunked_rlgc=chunked_rlgc,
        )
        self._datastore.save_local_round_transform_zyx_um(
            np.eye(4, dtype=np.float32),
            tile=self._tile_id,
            round=reference_round_id,
        )
        if self._verbose >= 1:
            print(
                time_stamp(),
                f"Finished fiducial tile id: {self._tile_id}; round id: {reference_round_id}.",
            )

        clear_rlgc_caches(clear_memory_pool=True)

        moving_rounds = list(self._round_ids[1:])
        if not moving_rounds:
            del reference_image
            gc.collect()
            return

        result_queue = mp.Queue()
        num_workers = min(self._num_gpus, len(moving_rounds))
        chunk_size = (len(moving_rounds) + num_workers - 1) // num_workers
        processes = []

        for gpu_id in range(num_workers):
            start = gpu_id * chunk_size
            end = min(start + chunk_size, len(moving_rounds))
            if start >= end:
                break

            subset = moving_rounds[start:end]
            process = mp.Process(
                target=_process_fiducial_rounds_on_gpu,
                args=(self, subset, reference_image, gpu_id, result_queue),
            )
            process.start()
            processes.append(process)

        completed_rounds = set()
        errors = []
        while len(completed_rounds) + len(errors) < len(moving_rounds):
            try:
                status, round_id, payload = result_queue.get(timeout=5)
            except queue.Empty:
                if any(process.exitcode not in (None, 0) for process in processes):
                    break
                if all(process.exitcode is not None for process in processes):
                    break
                continue
            if status == "result":
                completed_rounds.add(round_id)
                if payload is not None:
                    requested_crop_yx = min(
                        int(self._crop_yx_decon),
                        int(max(reference_image.shape[-2:])),
                    )
                    if int(payload) < requested_crop_yx:
                        self._crop_yx_decon = int(payload)
            else:
                errors.append(payload)

        for process in processes:
            process.join()
            if process.exitcode not in (0, None):
                errors.append(
                    f"Fiducial worker pid={process.pid} failed with "
                    f"exitcode={process.exitcode}."
                )

        missing_rounds = [
            round_id for round_id in moving_rounds if round_id not in completed_rounds
        ]
        if missing_rounds:
            errors.append(f"Missing processed fiducial rounds: {missing_rounds}")
        if errors:
            raise RuntimeError("Fiducial registration failed:\n" + "\n".join(errors))

        del reference_image
        clear_rlgc_caches(clear_memory_pool=True)
        gc.collect()

        if self._perform_deformable_registration:
            sofima_queue = mp.Queue()
            sofima_processes = []

            for gpu_id in range(num_workers):
                start = gpu_id * chunk_size
                end = min(start + chunk_size, len(moving_rounds))
                if start >= end:
                    break

                subset = moving_rounds[start:end]
                process = mp.Process(
                    target=_process_sofima_rounds_on_gpu,
                    args=(self, subset, gpu_id, sofima_queue),
                )
                process.start()
                sofima_processes.append(process)

            completed_sofima_rounds = set()
            sofima_errors = []
            while len(completed_sofima_rounds) + len(sofima_errors) < len(
                moving_rounds
            ):
                try:
                    status, round_id, payload = sofima_queue.get(timeout=5)
                except queue.Empty:
                    if any(
                        process.exitcode not in (None, 0)
                        for process in sofima_processes
                    ):
                        break
                    if all(
                        process.exitcode is not None for process in sofima_processes
                    ):
                        break
                    continue
                if status == "result":
                    completed_sofima_rounds.add(round_id)
                else:
                    sofima_errors.append(payload)

            for process in sofima_processes:
                process.join()
                if process.exitcode not in (0, None):
                    sofima_errors.append(
                        f"SOFIMA worker pid={process.pid} failed with "
                        f"exitcode={process.exitcode}."
                    )

            missing_sofima_rounds = [
                round_id
                for round_id in moving_rounds
                if round_id not in completed_sofima_rounds
            ]
            if missing_sofima_rounds:
                sofima_errors.append(
                    f"Missing processed SOFIMA rounds: {missing_sofima_rounds}"
                )
            if sofima_errors:
                raise RuntimeError(
                    "SOFIMA registration failed:\n" + "\n".join(sofima_errors)
                )

        _registration_diag(
            "fiducial_registration_all_done "
            f"tile={self._tile_id} rounds={len(self._round_ids)} "
            f"elapsed_s={timeit.default_timer() - start_time:.2f}",
            enabled=self._registration_diagnostics,
        )
        gc.collect()

    def _apply_registration_to_bits(self) -> None:
        """
        Deconvolve readout bits, run U-FISH, and save unwarped outputs.

        Returns
        -------
        None
            Results are written to the datastore.
        """
        # 1) How many GPUs do we have?
        if self._num_gpus == 0:
            raise RuntimeError(
                "No GPUs detected. Cannot run _apply_registration_to_bits()."
            )

        # 2) Grab all bit IDs and split into `num_gpus` chunks
        all_bits = list(self._bit_ids)
        chunk_size = (
            len(all_bits) + self._num_gpus - 1
        ) // self._num_gpus  # ceiling division

        # 3) Launch one process per GPU (only as many as needed)
        processes = []
        for gpu_id in range(self._num_gpus):
            start = gpu_id * chunk_size
            end = min(start + chunk_size, len(all_bits))
            if start >= end:
                break  # no more bits to assign

            subset = all_bits[start:end]
            p = mp.Process(target=_apply_bits_on_gpu, args=(self, subset, gpu_id))
            p.start()
            processes.append(p)

        # 4) Wait for all GPU-workers to finish
        errors = []
        for p in processes:
            p.join()
            if p.exitcode not in (0, None):
                errors.append(
                    f"Readout worker pid={p.pid} failed with exitcode={p.exitcode}."
                )
        if errors:
            raise RuntimeError("Readout preprocessing failed:\n" + "\n".join(errors))

datastore property

Return the qi2labDataStore object.

Returns:

Type Description
qi2labDataStore

qi2labDataStore object

overwrite_outputs property writable

Get the overwrite_outputs flag.

Returns:

Type Description
bool

Overwrite existing local transforms and preprocessing outputs.

perform_deformable_registration property writable

Get the deformable registration flag.

Returns:

Type Description
bool

True when SOFIMA deformable flow-field registration is enabled.

tile_id property writable

Get the current tile id.

Returns:

Name Type Description
tile_id int or str

Tile id

__init__(datastore, decon_fiducial=True, decon_readout=False, overwrite_outputs=False, perform_deformable_registration=True, num_gpus=1, crop_yx_decon=2048, ufish_model=None, global_registration=False, registration_diagnostics=False, sofima_config=None, global_registration_config=None, verbose=1)

Initialize the object.

Parameters:

Name Type Description Default
datastore qi2labDataStore

Datastore containing corrected images, PSFs, metadata, and output groups.

required
decon_fiducial bool

If True, deconvolve fiducial images before registration.

True
decon_readout bool

If True, deconvolve readout images before U-FISH prediction and warping.

False
overwrite_outputs bool

If True, regenerate local transforms, optional decon images, and feature predictor outputs even when they already exist.

False
perform_deformable_registration bool

If True, estimate a SOFIMA flow field after affine registration. The saved field uses channel order X, Y, Z; spatial order Z, Y, X; and a patch-centered map origin. Pixel decoding composes this field with affine and chromatic transforms when readout images are loaded.

True
num_gpus int

Number of GPUs available for deconvolution and readout processing.

1
crop_yx_decon int

Initial lateral RLGC crop size. GPU memory fallback can reduce this value and cache the successful size.

2048
ufish_model str | Path | None

U-FISH model alias, weights filename, local path, or None for the default model.

None
global_registration bool

If True, run global tile registration and fused fiducial OME-Zarr generation after local tile preprocessing.

False
registration_diagnostics bool

If True, print detailed registration diagnostics.

False
sofima_config SofimaRegistrationConfig or None

Explicit SOFIMA deformable registration parameters.

None
global_registration_config GlobalRegistrationConfig or None

Explicit multiview-stitcher global registration parameters.

None
verbose int

Verbosity level for progress messages.

1
Source code in src/merfish3danalysis/DataRegistration.py
def __init__(
    self,
    datastore: qi2labDataStore,
    decon_fiducial: bool = True,
    decon_readout: bool = False,
    overwrite_outputs: bool = False,
    perform_deformable_registration: bool = True,
    num_gpus: int = 1,
    crop_yx_decon: int = 2048,
    ufish_model: str | Path | None = None,
    global_registration: bool = False,
    registration_diagnostics: bool = False,
    sofima_config: SofimaRegistrationConfig | None = None,
    global_registration_config: GlobalRegistrationConfig | None = None,
    verbose: int = 1,
) -> None:
    """
    Initialize the object.

    Parameters
    ----------
    datastore : qi2labDataStore
        Datastore containing corrected images, PSFs, metadata, and output
        groups.
    decon_fiducial : bool
        If True, deconvolve fiducial images before registration.
    decon_readout : bool
        If True, deconvolve readout images before U-FISH prediction and
        warping.
    overwrite_outputs : bool
        If True, regenerate local transforms, optional decon images, and
        feature predictor outputs even when they already exist.
    perform_deformable_registration : bool
        If True, estimate a SOFIMA flow field after affine registration.
        The saved field uses channel order X, Y, Z; spatial order Z, Y, X;
        and a patch-centered map origin. Pixel decoding composes this field
        with affine and chromatic transforms when readout images are loaded.
    num_gpus : int
        Number of GPUs available for deconvolution and readout processing.
    crop_yx_decon : int
        Initial lateral RLGC crop size. GPU memory fallback can reduce this
        value and cache the successful size.
    ufish_model : str | Path | None
        U-FISH model alias, weights filename, local path, or None for the
        default model.
    global_registration : bool, default=False
        If True, run global tile registration and fused fiducial OME-Zarr
        generation after local tile preprocessing.
    registration_diagnostics : bool, default=False
        If True, print detailed registration diagnostics.
    sofima_config : SofimaRegistrationConfig or None, default=None
        Explicit SOFIMA deformable registration parameters.
    global_registration_config : GlobalRegistrationConfig or None, default=None
        Explicit multiview-stitcher global registration parameters.
    verbose : int
        Verbosity level for progress messages.
    """
    self._datastore = datastore
    self._decon_fiducial = decon_fiducial
    self._tile_ids = self._datastore.tile_ids
    self._round_ids = self._datastore.round_ids
    self._bit_ids = self._datastore.bit_ids
    self._psfs = self._datastore.channel_psfs
    self._num_gpus = num_gpus
    self._crop_yx_decon = crop_yx_decon
    self._perform_deformable_registration = perform_deformable_registration
    self._tile_id: str | None = None
    self._overwrite_outputs = overwrite_outputs
    self._decon_readout = decon_readout
    self._ufish_model = ufish_model
    self._global_registration = global_registration
    self._registration_diagnostics = bool(registration_diagnostics)
    self._sofima_config = sofima_config or SofimaRegistrationConfig()
    self._global_registration_config = (
        global_registration_config or GlobalRegistrationConfig()
    )
    self._verbose = verbose

_apply_registration_to_bits()

Deconvolve readout bits, run U-FISH, and save unwarped outputs.

Returns:

Type Description
None

Results are written to the datastore.

Source code in src/merfish3danalysis/DataRegistration.py
def _apply_registration_to_bits(self) -> None:
    """
    Deconvolve readout bits, run U-FISH, and save unwarped outputs.

    Returns
    -------
    None
        Results are written to the datastore.
    """
    # 1) How many GPUs do we have?
    if self._num_gpus == 0:
        raise RuntimeError(
            "No GPUs detected. Cannot run _apply_registration_to_bits()."
        )

    # 2) Grab all bit IDs and split into `num_gpus` chunks
    all_bits = list(self._bit_ids)
    chunk_size = (
        len(all_bits) + self._num_gpus - 1
    ) // self._num_gpus  # ceiling division

    # 3) Launch one process per GPU (only as many as needed)
    processes = []
    for gpu_id in range(self._num_gpus):
        start = gpu_id * chunk_size
        end = min(start + chunk_size, len(all_bits))
        if start >= end:
            break  # no more bits to assign

        subset = all_bits[start:end]
        p = mp.Process(target=_apply_bits_on_gpu, args=(self, subset, gpu_id))
        p.start()
        processes.append(p)

    # 4) Wait for all GPU-workers to finish
    errors = []
    for p in processes:
        p.join()
        if p.exitcode not in (0, None):
            errors.append(
                f"Readout worker pid={p.pid} failed with exitcode={p.exitcode}."
            )
    if errors:
        raise RuntimeError("Readout preprocessing failed:\n" + "\n".join(errors))

_entity_root(tile_id, round_id=None, bit_id=None)

Return the native tile/round or tile/bit image directory.

Parameters:

Name Type Description Default
tile_id str

Stored tile identifier; None selects the current tile.

required
round_id str | None

Stored fiducial round identifier.

None
bit_id str | None

Stored readout bit identifier.

None

Returns:

Type Description
Path

Native tile/round or tile/bit directory containing the image stores.

Source code in src/merfish3danalysis/DataRegistration.py
def _entity_root(
    self,
    tile_id: str,
    round_id: str | None = None,
    bit_id: str | None = None,
) -> Path:
    """
    Return the native tile/round or tile/bit image directory.

    Parameters
    ----------
    tile_id : str
        Stored tile identifier; None selects the current tile.
    round_id : str | None
        Stored fiducial round identifier.
    bit_id : str | None
        Stored readout bit identifier.

    Returns
    -------
    Path
        Native tile/round or tile/bit directory containing the image stores.
    """
    if (round_id is None and bit_id is None) or (
        round_id is not None and bit_id is not None
    ):
        raise ValueError("Provide either round_id or bit_id, but not both.")

    return self._datastore.local_image_path(
        tile_id, "corrected_data", round=round_id, bit=bit_id
    ).parent

_generate_registrations()

Deconvolve fiducials, register rounds, and save local transforms.

The first fiducial round is loaded, optionally deconvolved, saved, and stored as the local reference for the tile. Remaining fiducial rounds are split across GPU worker processes. Each worker loads its assigned rounds, optionally deconvolves them, registers them to round 1, saves the physical-space transform, and estimates SOFIMA residuals when requested.

Returns:

Type Description
None

Results are written to the datastore.

Source code in src/merfish3danalysis/DataRegistration.py
def _generate_registrations(self) -> None:
    """
    Deconvolve fiducials, register rounds, and save local transforms.

    The first fiducial round is loaded, optionally deconvolved, saved, and
    stored as the local reference for the tile. Remaining fiducial rounds
    are split across GPU worker processes. Each worker loads its assigned
    rounds, optionally deconvolves them, registers them to round 1, saves the
    physical-space transform, and estimates SOFIMA residuals when requested.

    Returns
    -------
    None
        Results are written to the datastore.
    """
    if self._num_gpus == 0:
        raise RuntimeError(
            "No GPUs detected. Cannot run _generate_registrations()."
        )

    start_time = timeit.default_timer()
    reference_round_id = self._round_ids[0]

    import cupy as cp

    from merfish3danalysis.utils.rlgc import chunked_rlgc, clear_rlgc_caches

    cp.cuda.Device(0).use()
    reference_image = _load_deconvolve_fiducial_round(
        dr=self,
        round_id=reference_round_id,
        gpu_id=0,
        chunked_rlgc=chunked_rlgc,
    )
    self._datastore.save_local_round_transform_zyx_um(
        np.eye(4, dtype=np.float32),
        tile=self._tile_id,
        round=reference_round_id,
    )
    if self._verbose >= 1:
        print(
            time_stamp(),
            f"Finished fiducial tile id: {self._tile_id}; round id: {reference_round_id}.",
        )

    clear_rlgc_caches(clear_memory_pool=True)

    moving_rounds = list(self._round_ids[1:])
    if not moving_rounds:
        del reference_image
        gc.collect()
        return

    result_queue = mp.Queue()
    num_workers = min(self._num_gpus, len(moving_rounds))
    chunk_size = (len(moving_rounds) + num_workers - 1) // num_workers
    processes = []

    for gpu_id in range(num_workers):
        start = gpu_id * chunk_size
        end = min(start + chunk_size, len(moving_rounds))
        if start >= end:
            break

        subset = moving_rounds[start:end]
        process = mp.Process(
            target=_process_fiducial_rounds_on_gpu,
            args=(self, subset, reference_image, gpu_id, result_queue),
        )
        process.start()
        processes.append(process)

    completed_rounds = set()
    errors = []
    while len(completed_rounds) + len(errors) < len(moving_rounds):
        try:
            status, round_id, payload = result_queue.get(timeout=5)
        except queue.Empty:
            if any(process.exitcode not in (None, 0) for process in processes):
                break
            if all(process.exitcode is not None for process in processes):
                break
            continue
        if status == "result":
            completed_rounds.add(round_id)
            if payload is not None:
                requested_crop_yx = min(
                    int(self._crop_yx_decon),
                    int(max(reference_image.shape[-2:])),
                )
                if int(payload) < requested_crop_yx:
                    self._crop_yx_decon = int(payload)
        else:
            errors.append(payload)

    for process in processes:
        process.join()
        if process.exitcode not in (0, None):
            errors.append(
                f"Fiducial worker pid={process.pid} failed with "
                f"exitcode={process.exitcode}."
            )

    missing_rounds = [
        round_id for round_id in moving_rounds if round_id not in completed_rounds
    ]
    if missing_rounds:
        errors.append(f"Missing processed fiducial rounds: {missing_rounds}")
    if errors:
        raise RuntimeError("Fiducial registration failed:\n" + "\n".join(errors))

    del reference_image
    clear_rlgc_caches(clear_memory_pool=True)
    gc.collect()

    if self._perform_deformable_registration:
        sofima_queue = mp.Queue()
        sofima_processes = []

        for gpu_id in range(num_workers):
            start = gpu_id * chunk_size
            end = min(start + chunk_size, len(moving_rounds))
            if start >= end:
                break

            subset = moving_rounds[start:end]
            process = mp.Process(
                target=_process_sofima_rounds_on_gpu,
                args=(self, subset, gpu_id, sofima_queue),
            )
            process.start()
            sofima_processes.append(process)

        completed_sofima_rounds = set()
        sofima_errors = []
        while len(completed_sofima_rounds) + len(sofima_errors) < len(
            moving_rounds
        ):
            try:
                status, round_id, payload = sofima_queue.get(timeout=5)
            except queue.Empty:
                if any(
                    process.exitcode not in (None, 0)
                    for process in sofima_processes
                ):
                    break
                if all(
                    process.exitcode is not None for process in sofima_processes
                ):
                    break
                continue
            if status == "result":
                completed_sofima_rounds.add(round_id)
            else:
                sofima_errors.append(payload)

        for process in sofima_processes:
            process.join()
            if process.exitcode not in (0, None):
                sofima_errors.append(
                    f"SOFIMA worker pid={process.pid} failed with "
                    f"exitcode={process.exitcode}."
                )

        missing_sofima_rounds = [
            round_id
            for round_id in moving_rounds
            if round_id not in completed_sofima_rounds
        ]
        if missing_sofima_rounds:
            sofima_errors.append(
                f"Missing processed SOFIMA rounds: {missing_sofima_rounds}"
            )
        if sofima_errors:
            raise RuntimeError(
                "SOFIMA registration failed:\n" + "\n".join(sofima_errors)
            )

    _registration_diag(
        "fiducial_registration_all_done "
        f"tile={self._tile_id} rounds={len(self._round_ids)} "
        f"elapsed_s={timeit.default_timer() - start_time:.2f}",
        enabled=self._registration_diagnostics,
    )
    gc.collect()

_global_transforms_available()

Check whether every tile has a stored global affine transform.

Returns:

Type Description
bool

True only when every tile has a non-null global affine.

Source code in src/merfish3danalysis/DataRegistration.py
def _global_transforms_available(self) -> bool:
    """
    Check whether every tile has a stored global affine transform.

    Returns
    -------
    bool
        ``True`` only when every tile has a non-null global affine.
    """
    for tile_id in self._tile_ids:
        affine_zyx_um, _origin_zyx_um, _spacing_zyx_um = (
            self._datastore.load_global_coord_xforms_um(tile=tile_id)
        )
        if affine_zyx_um is None:
            return False
    return True

_has_valid_deconvolved_fiducial_image(tile_id=None, round_id=None)

Has valid native-frame deconvolved fiducial image.

Parameters:

Name Type Description Default
tile_id str | None

Stored tile identifier; None selects the current tile.

None
round_id str | None

Stored fiducial round identifier.

None

Returns:

Type Description
bool

True when decon_data exists and matches the corrected image shape.

Source code in src/merfish3danalysis/DataRegistration.py
def _has_valid_deconvolved_fiducial_image(
    self,
    tile_id: str | None = None,
    round_id: str | None = None,
) -> bool:
    """
    Has valid native-frame deconvolved fiducial image.

    Parameters
    ----------
    tile_id : str | None
        Stored tile identifier; None selects the current tile.
    round_id : str | None
        Stored fiducial round identifier.

    Returns
    -------
    bool
        True when ``decon_data`` exists and matches the corrected image shape.
    """
    if round_id is None:
        raise ValueError("round_id is required for fiducial images.")

    tile_id = self._tile_id if tile_id is None else tile_id
    entity_root = self._entity_root(tile_id=tile_id, round_id=round_id)
    corrected_shape = self._datastore.image_shape(
        entity_root / Path("corrected_data")
    )
    decon_shape = self._datastore.image_shape(entity_root / Path("decon_data"))
    return corrected_shape is not None and decon_shape == corrected_shape

_has_valid_deconvolved_readout_image(tile_id=None, bit_id=None)

Has valid native-frame deconvolved readout image.

Parameters:

Name Type Description Default
tile_id str | None

Tile identifier.

None
bit_id str | None

Bit identifier.

None

Returns:

Type Description
bool

True when decon_data exists and matches the corrected image shape.

Source code in src/merfish3danalysis/DataRegistration.py
def _has_valid_deconvolved_readout_image(
    self,
    tile_id: str | None = None,
    bit_id: str | None = None,
) -> bool:
    """
    Has valid native-frame deconvolved readout image.

    Parameters
    ----------
    tile_id : str | None
        Tile identifier.
    bit_id : str | None
        Bit identifier.

    Returns
    -------
    bool
        True when ``decon_data`` exists and matches the corrected image shape.
    """
    tile_id = self._tile_id if tile_id is None else tile_id
    if bit_id is None:
        raise ValueError("bit_id is required for readout images.")

    entity_root = self._entity_root(tile_id=tile_id, bit_id=bit_id)
    corrected_shape = self._datastore.image_shape(
        entity_root / Path("corrected_data")
    )
    readout_shape = self._datastore.image_shape(entity_root / Path("decon_data"))
    return corrected_shape is not None and readout_shape == corrected_shape

_has_valid_feature_predictor_outputs(tile_id=None, bit_id=None)

Has valid feature predictor outputs.

Parameters:

Name Type Description Default
tile_id str | None

Stored tile identifier; None selects the current tile.

None
bit_id str | None

Stored readout bit identifier.

None

Returns:

Type Description
bool

True when predictor and corrected image shapes match and spot localizations exist.

Source code in src/merfish3danalysis/DataRegistration.py
def _has_valid_feature_predictor_outputs(
    self,
    tile_id: str | None = None,
    bit_id: str | None = None,
) -> bool:
    """
    Has valid feature predictor outputs.

    Parameters
    ----------
    tile_id : str | None
        Stored tile identifier; None selects the current tile.
    bit_id : str | None
        Stored readout bit identifier.

    Returns
    -------
    bool
        True when predictor and corrected image shapes match and spot localizations exist.
    """
    tile_id = self._tile_id if tile_id is None else tile_id
    if bit_id is None:
        raise ValueError("bit_id is required for feature predictor outputs.")

    entity_root = self._entity_root(tile_id=tile_id, bit_id=bit_id)
    corrected_shape = self._datastore.image_shape(
        entity_root / Path("corrected_data")
    )
    feature_shape = self._datastore.image_shape(
        entity_root / Path(f"{self._datastore.feature_predictor_folder_name}_data")
    )
    spots_path = self._datastore.local_feature_predictor_spots_path(tile_id, bit_id)
    return (
        corrected_shape is not None
        and feature_shape == corrected_shape
        and spots_path.exists()
    )

_is_tile_complete(tile_id, *, process_readouts=True)

Is tile complete.

Parameters:

Name Type Description Default
tile_id str

Tile identifier.

required
process_readouts bool

If False, assess only fiducial deconvolution and local transforms.

True

Returns:

Type Description
bool

True when every requested image and local transform is already present.

Source code in src/merfish3danalysis/DataRegistration.py
def _is_tile_complete(
    self,
    tile_id: str,
    *,
    process_readouts: bool = True,
) -> bool:
    """
    Is tile complete.

    Parameters
    ----------
    tile_id : str
        Tile identifier.
    process_readouts : bool, default=True
        If False, assess only fiducial deconvolution and local transforms.

    Returns
    -------
    bool
        True when every requested image and local transform is already present.
    """
    if self._decon_fiducial:
        for round_id in self._round_ids:
            if not self._has_valid_deconvolved_fiducial_image(
                tile_id=tile_id,
                round_id=round_id,
            ):
                return False

    for round_id in self._round_ids:
        if (
            self._datastore.load_local_round_transform_zyx_um(
                tile=tile_id,
                round=round_id,
            )
            is None
        ):
            return False

    if self._perform_deformable_registration:
        for round_id in self._round_ids[1:]:
            field = self._datastore.load_local_sofima_flow_field(
                tile=tile_id, round=round_id, return_future=None
            )
            if field is None or field[0] is None:
                return False

    if not process_readouts:
        return True

    for bit_id in self._bit_ids:
        if self._decon_readout and not self._has_valid_deconvolved_readout_image(
            tile_id=tile_id, bit_id=bit_id
        ):
            return False
        if not self._has_valid_feature_predictor_outputs(
            tile_id=tile_id, bit_id=bit_id
        ):
            return False

    return True

apply_registration_to_one_tile(tile_id)

Apply existing local registrations to readout bits for one tile.

This uses the local transforms already stored in the datastore. It does not estimate or overwrite fiducial registrations.

Parameters:

Name Type Description Default
tile_id int or str

Tile identifier.

required
Source code in src/merfish3danalysis/DataRegistration.py
def apply_registration_to_one_tile(self, tile_id: int | str) -> None:
    """
    Apply existing local registrations to readout bits for one tile.

    This uses the local transforms already stored in the datastore. It does
    not estimate or overwrite fiducial registrations.

    Parameters
    ----------
    tile_id : int or str
        Tile identifier.
    """
    self.tile_id = tile_id
    if tile_id not in self._tile_ids and tile_id not in range(len(self._tile_ids)):
        return
    self._apply_registration_to_bits()

fuse_global_fiducial_views(msims, *, create_max_proj_tiff=True)

Write registered views on the downsampled fiducial segmentation grid.

Parameters:

Name Type Description Default
msims list

Multiview-stitcher images carrying global_registered transforms.

required
create_max_proj_tiff bool

Also write the maximum-Z OME-TIFF used by Cellpose.

True
Source code in src/merfish3danalysis/DataRegistration.py
def fuse_global_fiducial_views(
    self,
    msims: list[Any],
    *,
    create_max_proj_tiff: bool = True,
) -> None:
    """Write registered views on the downsampled fiducial segmentation grid.

    Parameters
    ----------
    msims : list
        Multiview-stitcher images carrying global_registered transforms.
    create_max_proj_tiff : bool, default True
        Also write the maximum-Z OME-TIFF used by Cellpose.
    """
    from multiview_stitcher import fusion, misc_utils, msi_utils
    from multiview_stitcher import spatial_image_utils as si_utils
    from yaozarrs import open_group

    global_transform_key = "global_registered"
    output_zarr_path = self._datastore.fused_image_path()
    # downsample Y/X toward Z spacing for segmentation and ROI coordinates
    voxel_zyx_um = round_spacing_um(self._datastore.voxel_size_zyx_um)
    output_spacing = {
        "z": voxel_zyx_um[0],
        "y": round_pixel_size_um(
            voxel_zyx_um[1] * np.round(voxel_zyx_um[0] / voxel_zyx_um[1], 1)
        ),
        "x": round_pixel_size_um(
            voxel_zyx_um[2] * np.round(voxel_zyx_um[0] / voxel_zyx_um[2], 1)
        ),
    }

    if self._verbose >= 1:
        print(time_stamp(), "Starting global fiducial fusion.")
    fusion_start_time = timeit.default_timer()
    fused_msim = fusion.fuse(
        images=msims,
        transform_key=global_transform_key,
        output_spacing=output_spacing,
        output_zarr_url=str(output_zarr_path),
        **_direct_zarr_fusion_kwargs(misc_utils=misc_utils),
    )
    if self._verbose >= 1:
        print(
            time_stamp(),
            "Finished fusion.fuse direct-to-zarr "
            f"elapsed_s={timeit.default_timer() - fusion_start_time:.2f}",
        )

    metadata_start_time = timeit.default_timer()
    affine = msi_utils.get_transform_from_msim(
        fused_msim,
        transform_key=global_transform_key,
    ).data.squeeze()
    fused_scale0 = msi_utils.get_sim_from_msim(fused_msim)
    origin = si_utils.get_origin_from_sim(fused_scale0, asarray=True)
    spacing = round_spacing_um(
        si_utils.get_spacing_from_sim(fused_scale0, asarray=True)
    )

    qi2labDataStore.save_image_metadata(
        image_path=output_zarr_path,
        extra_attributes={
            "affine_zyx_um": np.asarray(affine, dtype=np.float32).tolist(),
            "origin_zyx_um": np.asarray(origin, dtype=np.float32).tolist(),
            "spacing_zyx_um": round_spacing_um(spacing).tolist(),
        },
        merge=True,
    )
    if self._verbose >= 1:
        print(
            time_stamp(),
            "Finished fused metadata write "
            f"elapsed_s={timeit.default_timer() - metadata_start_time:.2f}",
        )

    del fused_msim
    gc.collect()

    if create_max_proj_tiff:
        projection_start_time = timeit.default_timer()
        cellpose_path = (
            self._datastore.datastore_path / Path("segmentation") / Path("cellpose")
        )
        cellpose_path.mkdir(exist_ok=True)
        filename_path = cellpose_path / Path("fiducial_max_projection.ome.tiff")
        # The TIFF tile iterator requires zarr-python slicing and chunk
        # metadata; yaozarrs opens the store and supplies that adapter.
        fused_array = open_group(output_zarr_path)["0"].to_zarr_python()
        _write_zarr_max_projection_tiff(
            array=fused_array,
            filename_path=filename_path,
            spacing_zyx_um=np.asarray(spacing, dtype=np.float32),
        )
        del fused_array
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Finished fused max-projection TIFF "
                f"elapsed_s={timeit.default_timer() - projection_start_time:.2f}",
            )

    datastore_state = self._datastore.datastore_state.copy()
    datastore_state.update({"GlobalRegistered": True, "Fused": True})
    self._datastore.datastore_state = datastore_state

    if self._verbose >= 1:
        print(
            time_stamp(),
            "Finished global fiducial fusion "
            f"elapsed_s={timeit.default_timer() - fusion_start_time:.2f}",
        )

fuse_global_registered(create_max_proj_tiff=True)

Fuse fiducials, registering globally first when transforms are absent.

Existing per-tile global transforms are reused when every tile has one. Before normal preprocessing, or for a partially registered datastore, global registration is run from the first-round fiducials before the fused OME-Zarr is created.

Parameters:

Name Type Description Default
create_max_proj_tiff bool

If True, write segmentation/cellpose/fiducial_max_projection.ome.tiff from the downsampled fused fiducial OME-Zarr, retaining its spacing.

True

Returns:

Type Description
None

Fused fiducial OME-Zarr, datastore state, and optional max projection are written to the datastore.

Source code in src/merfish3danalysis/DataRegistration.py
def fuse_global_registered(
    self,
    create_max_proj_tiff: bool = True,
) -> None:
    """
    Fuse fiducials, registering globally first when transforms are absent.

    Existing per-tile global transforms are reused when every tile has one.
    Before normal preprocessing, or for a partially registered datastore,
    global registration is run from the first-round fiducials before the
    fused OME-Zarr is created.

    Parameters
    ----------
    create_max_proj_tiff : bool, default=True
        If True, write ``segmentation/cellpose/fiducial_max_projection.ome.tiff``
        from the downsampled fused fiducial OME-Zarr, retaining its spacing.

    Returns
    -------
    None
        Fused fiducial OME-Zarr, datastore state, and optional max
        projection are written to the datastore.
    """
    if not self._global_transforms_available():
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Stored global transforms are incomplete; running global "
                "registration before fusion.",
            )
        self.global_register(create_max_proj_tiff=create_max_proj_tiff)
        if len(self._tile_ids) != 1:
            return

    if self._verbose >= 1:
        print(
            time_stamp(),
            "Starting global fiducial fusion from stored transforms.",
        )

    msims = self.load_global_fiducial_views(
        use_stored_global_transforms=True,
    )
    self.fuse_global_fiducial_views(
        msims=msims,
        create_max_proj_tiff=create_max_proj_tiff,
    )

    if self._verbose >= 1:
        print(
            time_stamp(),
            "Finished global fiducial fusion from stored transforms.",
        )

global_register(create_max_proj_tiff=True)

Globally register first-round fiducial tiles and write fused OME-Zarr.

The method reads each tile's first fiducial round, combines stage metadata with multiview-stitcher global registration, saves per-tile global transforms back to the datastore, and fuses the transformed views directly into the datastore as OME-Zarr v0.5.

Parameters:

Name Type Description Default
create_max_proj_tiff bool

If True, write segmentation/cellpose/fiducial_max_projection.ome.tiff from the downsampled fused fiducial OME-Zarr, retaining its spacing.

True

Returns:

Type Description
None

Global transforms, fused fiducial OME-Zarr, datastore state, and optional max projection are written to the datastore.

Notes

For one tile, save its stage origin and an identity registration correction without a pairwise fit. Use fuse_global_registered to also produce its mosaic.

Source code in src/merfish3danalysis/DataRegistration.py
def global_register(
    self,
    create_max_proj_tiff: bool = True,
) -> None:
    """
    Globally register first-round fiducial tiles and write fused OME-Zarr.

    The method reads each tile's first fiducial round, combines stage
    metadata with multiview-stitcher global registration, saves per-tile
    global transforms back to the datastore, and fuses the transformed
    views directly into the datastore as OME-Zarr v0.5.

    Parameters
    ----------
    create_max_proj_tiff : bool, default=True
        If True, write ``segmentation/cellpose/fiducial_max_projection.ome.tiff``
        from the downsampled fused fiducial OME-Zarr, retaining its spacing.

    Returns
    -------
    None
        Global transforms, fused fiducial OME-Zarr, datastore state, and
        optional max projection are written to the datastore.

    Notes
    -----
    For one tile, save its stage origin and an identity registration
    correction without a pairwise fit. Use ``fuse_global_registered`` to also produce its mosaic.
    """
    from dask import config as dask_config
    from dask.diagnostics import ProgressBar
    from multiview_stitcher import (
        msi_utils,
        param_utils,
        registration,
    )
    from multiview_stitcher import spatial_image_utils as si_utils

    if not self._tile_ids:
        raise ValueError("Global registration requires at least one tile.")
    if len(self._tile_ids) == 1:
        origin, _camera_to_stage = self._datastore.load_local_stage_position_zyx_um(
            self._tile_ids[0], self._round_ids[0]
        )
        self._datastore.save_global_coord_xforms_um(
            affine_zyx_um=np.eye(4, dtype=np.float32),
            origin_zyx_um=origin,
            spacing_zyx_um=round_spacing_um(self._datastore.voxel_size_zyx_um),
            tile=self._tile_ids[0],
        )
        state = self._datastore.datastore_state.copy()
        state["GlobalRegistered"] = True
        self._datastore.datastore_state = state
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Skipping global registration because datastore has one tile.",
            )
        return

    if self._verbose >= 1:
        print(time_stamp(), "Starting global fiducial registration.")

    msims = self.load_global_fiducial_views(
        use_stored_global_transforms=False,
    )

    registration_config = self._global_registration_config
    stage_transform_key = "stage_metadata"
    global_transform_key = "global_registered"
    registration_binning = dict(
        zip(
            "zyx",
            (int(value) for value in registration_config.registration_binning_zyx),
            strict=True,
        )
    )
    if self._verbose >= 1:
        print(
            time_stamp(),
            "Running multiview-stitcher global registration "
            f"tiles={len(msims)} registration_binning={registration_binning} "
            "pre_registration_pruning_method=None "
            "post_registration_do_quality_filter="
            f"{registration_config.post_registration_do_quality_filter} "
            "n_parallel_pairwise_regs="
            f"{registration_config.n_parallel_pairwise_regs}",
        )
    registration_start_time = timeit.default_timer()
    progress = ProgressBar() if self._verbose >= 1 else nullcontext()
    with (
        dask_config.set(scheduler=registration_config.dask_scheduler),
        progress,
    ):
        global_transforms = registration.register(
            msims,
            reg_channel_index=int(registration_config.reg_channel_index),
            transform_key=stage_transform_key,
            new_transform_key=global_transform_key,
            pre_registration_pruning_method=None,
            registration_binning=registration_binning,
            post_registration_do_quality_filter=bool(
                registration_config.post_registration_do_quality_filter
            ),
            groupwise_resolution_kwargs={
                "reference_view": int(registration_config.reference_view),
                "transform": registration_config.transform,
            },
            n_parallel_pairwise_regs=int(
                registration_config.n_parallel_pairwise_regs
            ),
        )
    if self._verbose >= 1:
        print(
            time_stamp(),
            "Finished multiview-stitcher global registration "
            f"elapsed_s={timeit.default_timer() - registration_start_time:.2f}",
        )

    for tile_idx, (msim, transform) in enumerate(
        zip(msims, global_transforms, strict=True)
    ):
        affine = np.asarray(transform).squeeze()
        if registration_config.affine_round_decimals is not None:
            affine = np.round(
                affine,
                int(registration_config.affine_round_decimals),
            )
        msi_utils.set_affine_transform(
            msim,
            param_utils.affine_to_xaffine(affine),
            transform_key=global_transform_key,
            base_transform_key=stage_transform_key,
        )
        sim = msi_utils.get_sim_from_msim(msim)
        origin = si_utils.get_origin_from_sim(sim, asarray=True)
        spacing = round_spacing_um(si_utils.get_spacing_from_sim(sim, asarray=True))
        self._datastore.save_global_coord_xforms_um(
            affine_zyx_um=affine,
            origin_zyx_um=origin,
            spacing_zyx_um=spacing,
            tile=tile_idx,
        )

    self.fuse_global_fiducial_views(
        msims=msims,
        create_max_proj_tiff=create_max_proj_tiff,
    )

    if self._verbose >= 1:
        print(time_stamp(), "Finished global fiducial registration.")

load_global_fiducial_views(*, use_stored_global_transforms=False)

Load native fiducial views with stage or stored global transforms.

Parameters:

Name Type Description Default
use_stored_global_transforms bool

Attach saved global transforms instead of only stage metadata.

False

Returns:

Type Description
list

Multiview-stitcher images in datastore tile order. Pixels remain lazy.

Source code in src/merfish3danalysis/DataRegistration.py
def load_global_fiducial_views(
    self,
    *,
    use_stored_global_transforms: bool = False,
) -> list[Any]:
    """Load native fiducial views with stage or stored global transforms.

    Parameters
    ----------
    use_stored_global_transforms : bool, default False
        Attach saved global transforms instead of only stage metadata.

    Returns
    -------
    list
        Multiview-stitcher images in datastore tile order. Pixels remain lazy.
    """
    from multiview_stitcher import msi_utils, param_utils

    stage_transform_key = "stage_metadata"
    global_transform_key = "global_registered"
    voxel_zyx_um = round_spacing_um(self._datastore.voxel_size_zyx_um)
    if (
        voxel_zyx_um.shape != (3,)
        or not np.all(np.isfinite(voxel_zyx_um))
        or np.any(voxel_zyx_um <= 0)
    ):
        raise ValueError(
            "Global registration requires three positive finite voxel sizes in Z, Y, X order."
        )
    scale = {
        "z": float(voxel_zyx_um[0]),
        "y": float(voxel_zyx_um[1]),
        "x": float(voxel_zyx_um[2]),
    }
    reference_round_id = self._round_ids[0]
    msims = []

    for tile_index, tile_id in enumerate(self._tile_ids):
        if self._verbose >= 1:
            print(
                time_stamp(),
                "Loading global registration input "
                f"tile={tile_id} ({tile_index + 1}/{len(self._tile_ids)})",
            )
        tile_position_zyx_um, affine_zyx_px = (
            self._datastore.load_local_stage_position_zyx_um(
                tile_id, reference_round_id
            )
        )
        tile_grid_positions = {
            "z": float(np.round(tile_position_zyx_um[0], 2)),
            "y": float(np.round(tile_position_zyx_um[1], 2)),
            "x": float(np.round(tile_position_zyx_um[2], 2)),
        }
        input_path = _local_fiducial_path(
            datastore=self._datastore,
            tile_id=tile_id,
            round_id=reference_round_id,
        )
        sim = _read_fiducial_sim(
            input_path=input_path,
            scale=scale,
            translation=tile_grid_positions,
            affine_zyx_px=affine_zyx_px,
            transform_key=stage_transform_key,
        )
        msim = msi_utils.get_msim_from_sim(sim, scale_factors=[])

        if use_stored_global_transforms:
            affine_zyx_um, _origin_zyx_um, _spacing_zyx_um = (
                self._datastore.load_global_coord_xforms_um(tile=tile_id)
            )
            if affine_zyx_um is None:
                raise RuntimeError(
                    "Stored global transform is required for direct global "
                    f"fusion, but none was found for tile {tile_id!r}."
                )
            msi_utils.set_affine_transform(
                msim,
                param_utils.affine_to_xaffine(
                    np.asarray(affine_zyx_um, dtype=np.float32)
                ),
                transform_key=global_transform_key,
                base_transform_key=stage_transform_key,
            )

        msims.append(msim)
        if self._verbose >= 1:
            print(
                time_stamp(),
                f"Loaded global registration input tile={tile_id}",
            )
        gc.collect()

    return msims

register_all_tiles(*, process_readouts=True)

Register all tiles.

Parameters:

Name Type Description Default
process_readouts bool

If True, also regenerate readout deconvolution and U-FISH outputs. Set False to recompute only the complete fiducial registration chain: local affine transforms, optional SOFIMA fields, global transforms, and global fiducial fusion.

True
Source code in src/merfish3danalysis/DataRegistration.py
def register_all_tiles(self, *, process_readouts: bool = True) -> None:
    """
    Register all tiles.

    Parameters
    ----------
    process_readouts : bool, default=True
        If True, also regenerate readout deconvolution and U-FISH outputs.
        Set False to recompute only the complete fiducial registration
        chain: local affine transforms, optional SOFIMA fields, global
        transforms, and global fiducial fusion.
    """
    tile_ids = list(self._datastore.tile_ids)
    start_idx = 0
    if not self._overwrite_outputs:
        for idx, tile_id in enumerate(tile_ids):
            if self._is_tile_complete(
                tile_id,
                process_readouts=process_readouts,
            ):
                start_idx = idx + 1
                continue
            start_idx = idx
            break
        else:
            start_idx = len(tile_ids)

        if start_idx >= len(tile_ids):
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    "All tiles already have complete preprocessing outputs.",
                )
        elif start_idx > 0:
            if self._verbose >= 1:
                print(
                    time_stamp(),
                    f"Resuming local registration at tile id: {tile_ids[start_idx]}.",
                )

    for tile_id in tile_ids[start_idx:]:
        self.tile_id = tile_id
        self._generate_registrations()
        if process_readouts:
            self._apply_registration_to_bits()

    if self._global_registration:
        self.global_register()

register_one_tile(tile_id)

Register one tile.

Parameters:

Name Type Description Default
tile_id int or str

Tile id

required
Source code in src/merfish3danalysis/DataRegistration.py
def register_one_tile(self, tile_id: int | str) -> None:
    """Register one tile.

    Parameters
    ----------
    tile_id : int or str
        Tile id
    """
    self.tile_id = tile_id
    if tile_id not in self._tile_ids and tile_id not in range(len(self._tile_ids)):
        return
    self._generate_registrations()
    self._apply_registration_to_bits()

GlobalRegistrationConfig dataclass

Configure global multiview-stitcher registration behavior.

Attributes:

Name Type Description
registration_binning_zyx tuple[int, int, int]

Integer registration binning in physical Z, Y, X axis order.

reg_channel_index int

Channel index used to register each tile.

post_registration_do_quality_filter bool

Whether multiview-stitcher removes low-quality pairwise links.

reference_view int

Tile index fixed during groupwise transform resolution.

transform str

Transform family used for groupwise resolution.

n_parallel_pairwise_regs int

Maximum number of concurrent pairwise registrations.

dask_scheduler str

Dask scheduler used while evaluating registration tasks.

affine_round_decimals int or None

Decimal places retained when transforms are stored. None disables rounding.

Notes

MVS transform-key strings are intentionally not configurable. They are implementation-local identifiers that must agree across image loading, registration, and fusion.

Source code in src/merfish3danalysis/DataRegistration.py
@dataclass(frozen=True)
class GlobalRegistrationConfig:
    """
    Configure global multiview-stitcher registration behavior.

    Attributes
    ----------
    registration_binning_zyx : tuple[int, int, int]
        Integer registration binning in physical Z, Y, X axis order.
    reg_channel_index : int
        Channel index used to register each tile.
    post_registration_do_quality_filter : bool
        Whether multiview-stitcher removes low-quality pairwise links.
    reference_view : int
        Tile index fixed during groupwise transform resolution.
    transform : str
        Transform family used for groupwise resolution.
    n_parallel_pairwise_regs : int
        Maximum number of concurrent pairwise registrations.
    dask_scheduler : str
        Dask scheduler used while evaluating registration tasks.
    affine_round_decimals : int or None
        Decimal places retained when transforms are stored. ``None`` disables
        rounding.

    Notes
    -----
    MVS transform-key strings are intentionally not configurable. They are
    implementation-local identifiers that must agree across image loading,
    registration, and fusion.
    """

    registration_binning_zyx: tuple[int, int, int] = (3, 6, 6)
    reg_channel_index: int = 0
    post_registration_do_quality_filter: bool = True
    reference_view: int = 0
    transform: str = "translation"
    n_parallel_pairwise_regs: int = 1
    dask_scheduler: str = "single-threaded"
    affine_round_decimals: int | None = 2

_apply_bits_on_gpu(dr, bit_list, gpu_id=0)

Optionally deconvolve readout bits and always save U-FISH outputs.

Parameters:

Name Type Description Default
dr DataRegistration

DataRegistration instance pickled into this process.

required
bit_list list

Bit identifiers to process on this GPU.

required
gpu_id int

CUDA device index visible within this worker process.

0

Returns:

Type Description
bool

True after all assigned bits are processed.

Source code in src/merfish3danalysis/DataRegistration.py
def _apply_bits_on_gpu(dr, bit_list: list, gpu_id: int = 0) -> bool:  # noqa: ANN001
    """
    Optionally deconvolve readout bits and always save U-FISH outputs.

    Parameters
    ----------
    dr : DataRegistration
        DataRegistration instance pickled into this process.
    bit_list : list
        Bit identifiers to process on this GPU.
    gpu_id : int
        CUDA device index visible within this worker process.

    Returns
    -------
    bool
        True after all assigned bits are processed.
    """
    local_gpu_id = _restrict_worker_to_assigned_gpu(gpu_id)

    import cupy as cp
    import torch

    torch.cuda.set_device(local_gpu_id)
    cp.cuda.Device(local_gpu_id).use()

    import os

    os.environ["ORT_LOG_SEVERITY_LEVEL"] = "3"
    import onnxruntime as ort

    ort.set_default_logger_severity(3)
    from ufish.api import UFish

    from merfish3danalysis.utils.rlgc import chunked_rlgc, clear_rlgc_caches

    spacing_zyx_um = dr._datastore.voxel_size_zyx_um
    for bit_id in bit_list:
        r_idx = dr._datastore.load_local_round_linker(tile=dr._tile_id, bit=bit_id) - 1
        decon_on_disk = dr._has_valid_deconvolved_readout_image(bit_id=bit_id)
        feature_predictor_on_disk = dr._has_valid_feature_predictor_outputs(
            bit_id=bit_id
        )

        if (
            (decon_on_disk or not dr._decon_readout)
            and feature_predictor_on_disk
            and not dr._overwrite_outputs
        ):
            continue

        if (
            (dr._decon_readout and not decon_on_disk)
            or (not feature_predictor_on_disk)
            or dr._overwrite_outputs
        ):
            # load data
            corrected_image = dr._datastore.load_local_corrected_image(
                tile=dr._tile_id, bit=bit_id, return_future=False
            )
            _registration_diag(
                "bit_start "
                f"tile={dr._tile_id} bit={bit_id} linked_round={dr._round_ids[r_idx]} "
                f"raw_shape={tuple(int(v) for v in corrected_image.shape)} "
                f"spacing_zyx_um={tuple(float(v) for v in spacing_zyx_um)}",
                enabled=dr._registration_diagnostics,
            )

            # deconvolution
            if dr._decon_readout:
                start_time = timeit.default_timer()
                metadata = dr._datastore.load_image_metadata(
                    dr._datastore.local_image_path(
                        dr._tile_id, "corrected_data", bit=bit_id
                    )
                )
                predictor_input_image = _run_chunked_rlgc_remembering_crop(
                    dr=dr,
                    chunked_rlgc=chunked_rlgc,
                    image=corrected_image,
                    psf=_resolve_psf(dr._psfs, metadata["psf_idx"]),
                    gpu_id=local_gpu_id,
                    release_memory=True,
                )
                predictor_input_image = predictor_input_image.clip(0, 2**16 - 1).astype(
                    np.uint16
                )
                _registration_diag(
                    "bit_decon "
                    f"tile={dr._tile_id} bit={bit_id} "
                    f"input_shape={tuple(int(v) for v in corrected_image.shape)} "
                    f"output_shape={tuple(int(v) for v in predictor_input_image.shape)} "
                    f"elapsed_s={timeit.default_timer() - start_time:.2f}",
                    enabled=dr._registration_diagnostics,
                )
                dr._datastore.save_local_deconvolved_readout_image(
                    predictor_input_image,
                    tile=dr._tile_id,
                    bit=bit_id,
                )
            else:
                predictor_input_image = corrected_image

            ufish = UFish(device=f"cuda:{local_gpu_id}")
            load_ufish_model(ufish, dr._ufish_model)
            start_time = timeit.default_timer()
            feature_predictor_loc, feature_predictor_data = ufish.predict(
                predictor_input_image, axes="zyx", blend_3d=False, batch_size=1
            )
            _registration_diag(
                "bit_ufish "
                f"tile={dr._tile_id} bit={bit_id} "
                f"image_shape={tuple(int(v) for v in predictor_input_image.shape)} "
                f"spots={len(feature_predictor_loc)} "
                f"elapsed_s={timeit.default_timer() - start_time:.2f}",
                enabled=dr._registration_diagnostics,
            )
            feature_predictor_loc = feature_predictor_loc.rename(
                columns={"axis-0": "z", "axis-1": "y", "axis-2": "x"}
            )
            del ufish
            torch.cuda.empty_cache()
            gc.collect()

            _registration_diag(
                "bit_feature_predictor_done "
                f"tile={dr._tile_id} bit={bit_id} "
                f"round={dr._round_ids[r_idx]} "
                f"image_shape={tuple(int(v) for v in predictor_input_image.shape)} "
                f"feature_shape={tuple(int(v) for v in feature_predictor_data.shape)} "
                f"spots={len(feature_predictor_loc)}",
                enabled=dr._registration_diagnostics,
            )

            # feature_predictor ROI sums
            roi_z, roi_y, roi_x = 7, 5, 5

            def sum_pixels_in_roi(row, image, roi_dims):  # noqa
                """
                Sum image intensities in a fixed-size ROI centered on one spot.

                Parameters
                ----------
                row : Any
                    Feature predictor spot row containing ``z``, ``y``, and
                    ``x`` coordinates.
                image : Any
                    Image from which to sample intensities.
                roi_dims : Any
                    ROI shape in Z, Y, X order.

                Returns
                -------
                float
                    Sum of pixel intensities inside the clipped ROI.
                """
                z, y, x = row["z"], row["y"], row["x"]
                rz, ry, rx = roi_dims
                zmin = max(0, z - rz // 2)
                ymin = max(0, y - ry // 2)
                xmin = max(0, x - rx // 2)
                zmax = min(image.shape[0], z - rz // 2 + rz)
                ymax = min(image.shape[1], y - ry // 2 + ry)
                xmax = min(image.shape[2], x - rx // 2 + rx)
                roi = image[
                    int(zmin) : int(zmax), int(ymin) : int(ymax), int(xmin) : int(xmax)
                ]
                return np.sum(roi)

            feature_predictor_loc["sum_prob_pixels"] = feature_predictor_loc.apply(
                sum_pixels_in_roi,
                axis=1,
                image=feature_predictor_data,
                roi_dims=(roi_z, roi_y, roi_x),
            )
            feature_predictor_loc["sum_decon_pixels"] = feature_predictor_loc.apply(
                sum_pixels_in_roi,
                axis=1,
                image=predictor_input_image,
                roi_dims=(roi_z, roi_y, roi_x),
            )

            feature_predictor_loc["tile_idx"] = dr._tile_ids.index(dr._tile_id)
            feature_predictor_loc["bit_idx"] = dr._bit_ids.index(bit_id) + 1
            feature_predictor_loc["tile_z_px"] = feature_predictor_loc["z"]
            feature_predictor_loc["tile_y_px"] = feature_predictor_loc["y"]
            feature_predictor_loc["tile_x_px"] = feature_predictor_loc["x"]

            # save results
            dr._datastore.save_local_feature_predictor_image(
                feature_predictor_data, tile=dr._tile_id, bit=bit_id
            )
            dr._datastore.save_local_feature_predictor_spots(
                feature_predictor_loc, tile=dr._tile_id, bit=bit_id
            )
            if dr._verbose >= 1:
                print(
                    time_stamp(),
                    f"Finished readout tile id: {dr._tile_id}; bit id: {bit_id}.",
                )

            del predictor_input_image, feature_predictor_data, feature_predictor_loc
            gc.collect()

    try:
        clear_rlgc_caches(clear_memory_pool=False)
        cp.cuda.Stream.null.synchronize()
        cp.get_default_memory_pool().free_all_blocks()
        cp.get_default_pinned_memory_pool().free_all_blocks()
    except Exception:
        pass

    return True

_cleanup_fusion_worker_semaphore(name)

Unregister a worker semaphore even if another cleanup already removed it.

Python 3.12's SemLock cleanup skips unregistering when sem_unlink raises FileNotFoundError. That leaves a stale entry in the standard resource tracker, producing another missing-semaphore warning at shutdown. Only ENOENT is harmless here; other unlink errors must remain visible and tracked.

Source code in src/merfish3danalysis/DataRegistration.py
def _cleanup_fusion_worker_semaphore(name: str) -> None:
    """Unregister a worker semaphore even if another cleanup already removed it.

    Python 3.12's SemLock cleanup skips unregistering when sem_unlink raises
    FileNotFoundError. That leaves a stale entry in the standard resource
    tracker, producing another missing-semaphore warning at shutdown. Only
    ENOENT is harmless here; other unlink errors must remain visible and tracked.
    """
    from multiprocessing.resource_tracker import unregister
    from multiprocessing.synchronize import sem_unlink

    try:
        sem_unlink(name)
    except FileNotFoundError:
        pass
    unregister(name, "semaphore")

_configure_loky_fusion_worker()

Configure a spawned Loky process for long-running fusion work.

Returns:

Type Description
None

Disable Loky's process-level RSS-growth recycler and make standard multiprocessing semaphore cleanup tolerate an already-removed name.

Notes

This must remain a module-level callable because Loky pickles the initializer when starting worker processes.

Source code in src/merfish3danalysis/DataRegistration.py
def _configure_loky_fusion_worker() -> None:
    """
    Configure a spawned Loky process for long-running fusion work.

    Returns
    -------
    None
        Disable Loky's process-level RSS-growth recycler and make standard
        multiprocessing semaphore cleanup tolerate an already-removed name.

    Notes
    -----
    This must remain a module-level callable because Loky pickles the
    initializer when starting worker processes.
    """
    from joblib.externals.loky import process_executor

    process_executor._USE_PSUTIL = False
    if os.name == "posix":
        from multiprocessing.synchronize import SemLock

        # This initializer runs in the child only. Keep Loky's own tracker and
        # synchronization primitives unchanged; this handles stdlib locks that
        # libraries create inside fusion workers.
        SemLock._cleanup = staticmethod(_cleanup_fusion_worker_semaphore)

_direct_zarr_fusion_kwargs(*, misc_utils, fusion_workers=None, compression='blosc-zstd', compression_level=1)

Build direct-to-Zarr fusion and process-parallelization options.

Parameters:

Name Type Description Default
misc_utils Any

Imported multiview_stitcher.misc_utils module.

required
fusion_workers int or None

Number of Loky worker processes. The available CPU count is used when omitted.

None
compression str

Lossless output codec: blosc-zstd, blosc-lz4, or zstd. Blosc codecs use bitshuffle with the array's element size. The codec applies to both full-resolution fusion and the OME-Zarr pyramid.

"blosc-zstd"
compression_level int

Compression effort from 1 through 9. Compression is always enabled.

1

Returns:

Type Description
dict[str, Any]

Keyword arguments for multiview_stitcher.fusion.fuse.

Raises:

Type Description
ValueError

If the worker count, codec, or compression level is invalid.

Source code in src/merfish3danalysis/DataRegistration.py
def _direct_zarr_fusion_kwargs(
    *,
    misc_utils: Any,
    fusion_workers: int | None = None,
    compression: str = "blosc-zstd",
    compression_level: int = 1,
) -> dict[str, Any]:
    """
    Build direct-to-Zarr fusion and process-parallelization options.

    Parameters
    ----------
    misc_utils : Any
        Imported ``multiview_stitcher.misc_utils`` module.
    fusion_workers : int or None, default=None
        Number of Loky worker processes. The available CPU count is used when
        omitted.
    compression : str, default="blosc-zstd"
        Lossless output codec: ``blosc-zstd``, ``blosc-lz4``, or ``zstd``.
        Blosc codecs use bitshuffle with the array's element size. The codec
        applies to both full-resolution fusion and the OME-Zarr pyramid.
    compression_level : int, default=1
        Compression effort from 1 through 9. Compression is always enabled.

    Returns
    -------
    dict[str, Any]
        Keyword arguments for ``multiview_stitcher.fusion.fuse``.

    Raises
    ------
    ValueError
        If the worker count, codec, or compression level is invalid.
    """
    from joblib._parallel_backends import LokyBackend
    from zarr.codecs import BloscCodec, BytesCodec, ZstdCodec

    if compression not in ("blosc-zstd", "blosc-lz4", "zstd"):
        raise ValueError(
            "Compression must be blosc-zstd, blosc-lz4, or zstd; "
            "uncompressed fusion output is not supported."
        )
    if not 1 <= compression_level <= 9:
        raise ValueError("Compression level must be between 1 and 9.")
    if compression == "zstd":
        compressor = ZstdCodec(level=compression_level)
    else:
        compressor = BloscCodec(
            cname=compression.removeprefix("blosc-"),
            clevel=compression_level,
            shuffle="bitshuffle",
        )

    if fusion_workers is None:
        fusion_workers = max(1, os.cpu_count() or 1)
    elif fusion_workers < 1:
        raise ValueError("fusion_workers must be at least 1.")
    fusion_backend = LokyBackend(
        idle_worker_timeout=24 * 60 * 60,
        inner_max_num_threads=1,
        initializer=_configure_loky_fusion_worker,
    )
    return {
        "zarr_options": {
            "ome_zarr": True,
            "ngff_version": "0.5",
            "overwrite": True,
            "zarr_array_creation_kwargs": {
                "codecs": [BytesCodec(endian="little"), compressor],
            },
        },
        "batch_options": {
            "batch_func": misc_utils.process_batch_using_joblib,
            "n_batch": 4 * fusion_workers,
            "batch_func_kwargs": {
                "n_jobs": fusion_workers,
                "backend": fusion_backend,
            },
        },
        "backend": "numpy",
    }

_iter_zarr_max_projection_tiles(array, *, tile_shape_yx=(1024, 1024))

Yield padded tiles from a streaming Z maximum projection.

Parameters:

Name Type Description Default
array Any

Open Zarr array containing Z, Y, and X dimensions. Any additional dimensions must be singleton.

required
tile_shape_yx tuple[int, int]

Output tile height and width.

(1024, 1024)

Yields:

Type Description
ndarray

One YX projection tile. Tiles at the image boundary are zero-padded to tile_shape_yx.

Raises:

Type Description
ValueError

If required spatial axes are missing, a non-spatial axis is not singleton, or the Z axis is empty.

Source code in src/merfish3danalysis/DataRegistration.py
def _iter_zarr_max_projection_tiles(
    array: Any,
    *,
    tile_shape_yx: tuple[int, int] = (1024, 1024),
) -> Iterator[np.ndarray]:
    """
    Yield padded tiles from a streaming Z maximum projection.

    Parameters
    ----------
    array : Any
        Open Zarr array containing Z, Y, and X dimensions. Any additional
        dimensions must be singleton.
    tile_shape_yx : tuple[int, int], default=(1024, 1024)
        Output tile height and width.

    Yields
    ------
    numpy.ndarray
        One YX projection tile. Tiles at the image boundary are zero-padded to
        ``tile_shape_yx``.

    Raises
    ------
    ValueError
        If required spatial axes are missing, a non-spatial axis is not
        singleton, or the Z axis is empty.
    """
    dims = _zarr_array_dims(array)
    if not {"z", "y", "x"}.issubset(dims):
        raise ValueError(f"Expected z, y, and x axes in fused Zarr array; got {dims}.")

    axis = {dim: dims.index(dim) for dim in dims}
    for dim in dims:
        if dim not in {"z", "y", "x"} and int(array.shape[axis[dim]]) != 1:
            raise ValueError(
                "Fused max projection requires singleton non-spatial axes; "
                f"axis {dim!r} has size {array.shape[axis[dim]]}."
            )

    z_size = int(array.shape[axis["z"]])
    y_size = int(array.shape[axis["y"]])
    x_size = int(array.shape[axis["x"]])
    z_chunk = max(1, int(array.chunks[axis["z"]]))
    tile_y, tile_x = (int(tile_shape_yx[0]), int(tile_shape_yx[1]))
    spatial_order = [dim for dim in dims if dim in {"z", "y", "x"}]
    transpose_order = tuple(spatial_order.index(dim) for dim in ("z", "y", "x"))

    for y_start in range(0, y_size, tile_y):
        y_stop = min(y_start + tile_y, y_size)
        for x_start in range(0, x_size, tile_x):
            x_stop = min(x_start + tile_x, x_size)
            projection = None
            for z_start in range(0, z_size, z_chunk):
                z_stop = min(z_start + z_chunk, z_size)
                selection: list[int | slice] = [0] * array.ndim
                selection[axis["z"]] = slice(z_start, z_stop)
                selection[axis["y"]] = slice(y_start, y_stop)
                selection[axis["x"]] = slice(x_start, x_stop)
                block = np.asarray(array[tuple(selection)])
                block = np.transpose(block, transpose_order)
                block_projection = np.max(block, axis=0)
                if projection is None:
                    projection = np.array(block_projection, copy=True)
                else:
                    np.maximum(projection, block_projection, out=projection)

            if projection is None:
                raise ValueError(
                    "Cannot project a fused Zarr array with an empty z axis."
                )
            padded_tile = np.zeros((tile_y, tile_x), dtype=array.dtype)
            padded_tile[: y_stop - y_start, : x_stop - x_start] = projection
            yield padded_tile

_load_deconvolve_fiducial_round(dr, round_id, gpu_id, chunked_rlgc)

Load and optionally deconvolve one fiducial round.

Parameters:

Name Type Description Default
dr DataRegistration

Registration object containing datastore and deconvolution settings.

required
round_id str

Fiducial round identifier to load.

required
gpu_id int

CUDA device index used for RLGC.

required
chunked_rlgc Any

RLGC function imported in the process using this helper.

required

Returns:

Type Description
ndarray

Deconvolved fiducial image if deconvolution is enabled, otherwise the corrected fiducial image, as uint16.

Source code in src/merfish3danalysis/DataRegistration.py
def _load_deconvolve_fiducial_round(
    dr,  # noqa: ANN001
    round_id: str,
    gpu_id: int,
    chunked_rlgc: Any,
) -> np.ndarray:
    """
    Load and optionally deconvolve one fiducial round.

    Parameters
    ----------
    dr : DataRegistration
        Registration object containing datastore and deconvolution settings.
    round_id : str
        Fiducial round identifier to load.
    gpu_id : int
        CUDA device index used for RLGC.
    chunked_rlgc : Any
        RLGC function imported in the process using this helper.

    Returns
    -------
    numpy.ndarray
        Deconvolved fiducial image if deconvolution is enabled, otherwise the
        corrected fiducial image, as ``uint16``.
    """
    raw = dr._datastore.load_local_corrected_image(
        tile=dr._tile_id, round=round_id, return_future=False
    )
    _registration_diag(
        "fiducial_decon_start "
        f"tile={dr._tile_id} round={round_id} "
        f"raw_shape={tuple(int(v) for v in raw.shape)}",
        enabled=dr._registration_diagnostics,
    )
    start_time = timeit.default_timer()
    if dr._decon_fiducial:
        metadata = dr._datastore.load_image_metadata(
            dr._datastore.local_image_path(
                dr._tile_id, "corrected_data", round=round_id
            )
        )
        decon = _run_chunked_rlgc_remembering_crop(
            dr=dr,
            chunked_rlgc=chunked_rlgc,
            image=raw,
            psf=_resolve_psf(dr._psfs, metadata["psf_idx"]),
            gpu_id=gpu_id,
            release_memory=True,
        )
        decon = decon.clip(0, 2**16 - 1).astype(np.uint16)
        dr._datastore.save_local_deconvolved_fiducial_image(
            decon,
            tile=dr._tile_id,
            round=round_id,
        )
    else:
        decon = raw.copy().astype(np.uint16)
    _registration_diag(
        "fiducial_decon_done "
        f"tile={dr._tile_id} round={round_id} "
        f"input_shape={tuple(int(v) for v in raw.shape)} "
        f"output_shape={tuple(int(v) for v in decon.shape)} "
        f"elapsed_s={timeit.default_timer() - start_time:.2f}",
        enabled=dr._registration_diagnostics,
    )
    del raw
    gc.collect()
    return decon

_local_fiducial_path(datastore, tile_id, round_id)

Return the best available native fiducial OME-Zarr path for one tile.

Parameters:

Name Type Description Default
datastore qi2labDataStore

Open datastore containing fiducial images.

required
tile_id str

Tile identifier, such as tile0000.

required
round_id str

Round identifier, such as round001.

required

Returns:

Type Description
Path

Path to decon_data.ome.zarr when present, otherwise corrected_data.ome.zarr.

Source code in src/merfish3danalysis/DataRegistration.py
def _local_fiducial_path(
    datastore: qi2labDataStore,
    tile_id: str,
    round_id: str,
) -> Path:
    """
    Return the best available native fiducial OME-Zarr path for one tile.

    Parameters
    ----------
    datastore : qi2labDataStore
        Open datastore containing fiducial images.
    tile_id : str
        Tile identifier, such as ``tile0000``.
    round_id : str
        Round identifier, such as ``round001``.

    Returns
    -------
    pathlib.Path
        Path to ``decon_data.ome.zarr`` when present, otherwise
        ``corrected_data.ome.zarr``.
    """
    decon_path = datastore.local_image_path(tile_id, "decon_data", round=round_id)
    if decon_path.exists():
        return decon_path
    return datastore.local_image_path(tile_id, "corrected_data", round=round_id)

_process_fiducial_rounds_on_gpu(dr, round_list, reference_image, gpu_id, result_queue)

Deconvolve, register, warp, and optionally save fiducials on one GPU.

Parameters:

Name Type Description Default
dr DataRegistration

Registration object pickled into the worker process. The worker uses its datastore, PSF list, deconvolution settings, and current tile id.

required
round_list list

Moving fiducial round identifiers assigned to this worker.

required
reference_image ndarray

Round-1 deconvolved fiducial image in Z, Y, X order.

required
gpu_id int

CUDA device index visible within the worker process.

required
result_queue Queue

Queue used to send ("result", round_id, crop_yx) messages back to the parent. On failure the worker sends an "error" message containing the formatted traceback.

required

Returns:

Type Description
None

Results are saved to the datastore and status is returned through result_queue.

Source code in src/merfish3danalysis/DataRegistration.py
def _process_fiducial_rounds_on_gpu(
    dr,  # noqa: ANN001
    round_list: list,
    reference_image: np.ndarray,
    gpu_id: int,
    result_queue: Any,
) -> None:
    """
    Deconvolve, register, warp, and optionally save fiducials on one GPU.

    Parameters
    ----------
    dr : DataRegistration
        Registration object pickled into the worker process. The worker uses its
        datastore, PSF list, deconvolution settings, and current tile id.
    round_list : list
        Moving fiducial round identifiers assigned to this worker.
    reference_image : numpy.ndarray
        Round-1 deconvolved fiducial image in Z, Y, X order.
    gpu_id : int
        CUDA device index visible within the worker process.
    result_queue : multiprocessing.Queue
        Queue used to send ``("result", round_id, crop_yx)`` messages back to
        the parent. On failure the worker sends an ``"error"`` message
        containing the formatted traceback.

    Returns
    -------
    None
        Results are saved to the datastore and status is returned through
        ``result_queue``.
    """
    assigned_gpu_id = int(gpu_id)
    local_gpu_id = _restrict_worker_to_assigned_gpu(assigned_gpu_id)

    import cupy as cp
    import torch

    try:
        torch.cuda.set_device(local_gpu_id)
        cp.cuda.Device(local_gpu_id).use()

        from merfish3danalysis.utils.multiview_registration import (
            register_pair_to_fixed,
        )
        from merfish3danalysis.utils.rlgc import chunked_rlgc, clear_rlgc_caches

        spacing_zyx_um = dr._datastore.voxel_size_zyx_um
        reference_float = reference_image.astype(np.float32, copy=False)
        for round_id in round_list:
            decon = _load_deconvolve_fiducial_round(
                dr=dr,
                round_id=round_id,
                gpu_id=local_gpu_id,
                chunked_rlgc=chunked_rlgc,
            )
            clear_rlgc_caches(clear_memory_pool=True)
            _release_worker_gpu_memory(cp)

            _registration_diag(
                "fiducial_phase_registration_start "
                f"tile={dr._tile_id} round={round_id} "
                f"fixed_shape={tuple(int(v) for v in reference_image.shape)} "
                f"moving_shape={tuple(int(v) for v in decon.shape)} "
                f"spacing_zyx_um={tuple(float(v) for v in spacing_zyx_um)}",
                enabled=dr._registration_diagnostics,
            )
            start_time = timeit.default_timer()
            local_transform_zyx_um = register_pair_to_fixed(
                reference_float,
                decon.astype(np.float32, copy=False),
                spacing_zyx_um=spacing_zyx_um,
                diagnostics=dr._registration_diagnostics,
            )
            clear_rlgc_caches(clear_memory_pool=True)
            _release_worker_gpu_memory(cp)
            dr._datastore.save_local_round_transform_zyx_um(
                transform_zyx_um=local_transform_zyx_um,
                tile=dr._tile_id,
                round=round_id,
            )
            _registration_diag(
                "fiducial_phase_registration_done "
                f"tile={dr._tile_id} round={round_id} "
                f"elapsed_s={timeit.default_timer() - start_time:.2f}",
                enabled=dr._registration_diagnostics,
            )

            if dr._verbose >= 1:
                print(
                    time_stamp(),
                    f"Finished fiducial tile id: {dr._tile_id}; round id: {round_id}.",
                )
            result_queue.put(("result", round_id, int(dr._crop_yx_decon)))
            del decon
            clear_rlgc_caches(clear_memory_pool=True)
            _release_worker_gpu_memory(cp)

        clear_rlgc_caches(clear_memory_pool=True)
        _release_worker_gpu_memory(cp)
    except Exception:
        result_queue.put(("error", None, traceback.format_exc()))
        raise

_process_sofima_rounds_on_gpu(dr, round_list, gpu_id, result_queue)

Estimate SOFIMA residual flow fields in an isolated GPU worker.

Parameters:

Name Type Description Default
dr DataRegistration

Registration object pickled into the worker process.

required
round_list list

Moving fiducial round identifiers assigned to this worker.

required
gpu_id int

Physical GPU index assigned by the parent process.

required
result_queue Queue

Queue used to send completion or error messages to the parent.

required

Returns:

Type Description
None

SOFIMA flow fields are saved to the datastore.

Source code in src/merfish3danalysis/DataRegistration.py
def _process_sofima_rounds_on_gpu(
    dr,  # noqa: ANN001
    round_list: list,
    gpu_id: int,
    result_queue: Any,
) -> None:
    """
    Estimate SOFIMA residual flow fields in an isolated GPU worker.

    Parameters
    ----------
    dr : DataRegistration
        Registration object pickled into the worker process.
    round_list : list
        Moving fiducial round identifiers assigned to this worker.
    gpu_id : int
        Physical GPU index assigned by the parent process.
    result_queue : multiprocessing.Queue
        Queue used to send completion or error messages to the parent.

    Returns
    -------
    None
        SOFIMA flow fields are saved to the datastore.
    """
    local_gpu_id = _restrict_worker_to_assigned_gpu(int(gpu_id))

    import cupy as cp

    try:
        cp.cuda.Device(local_gpu_id).use()

        from merfish3danalysis.utils.multiview_registration import (
            warp_array_to_reference_gpu,
        )
        from merfish3danalysis.utils.rlgc import clear_rlgc_caches
        from merfish3danalysis.utils.sofima_registration import (
            estimate_sofima_flow_field_xyz_px,
        )

        spacing_zyx_um = dr._datastore.voxel_size_zyx_um
        reference_round_id = dr._round_ids[0]
        fixed = dr._datastore.load_local_fiducial_image(
            tile=dr._tile_id,
            round=reference_round_id,
            return_future=False,
        )
        if fixed is None:
            raise RuntimeError(
                f"Missing reference fiducial for tile={dr._tile_id} "
                f"round={reference_round_id}."
            )
        fixed = fixed.astype(np.float32, copy=False)

        for round_id in round_list:
            moving = dr._datastore.load_local_fiducial_image(
                tile=dr._tile_id,
                round=round_id,
                return_future=False,
            )
            if moving is None:
                raise RuntimeError(
                    f"Missing moving fiducial for tile={dr._tile_id} round={round_id}."
                )
            transform_zyx_um = dr._datastore.load_local_round_transform_zyx_um(
                tile=dr._tile_id,
                round=round_id,
            )
            if transform_zyx_um is None:
                raise RuntimeError(
                    f"Missing local affine transform for tile={dr._tile_id} "
                    f"round={round_id}."
                )

            _release_worker_gpu_memory(cp)
            warp_start_time = timeit.default_timer()
            warped = warp_array_to_reference_gpu(
                moving,
                transform_zyx_um=transform_zyx_um,
                spacing_zyx_um=spacing_zyx_um,
                reference_shape=fixed.shape,
                gpu_id=local_gpu_id,
                diagnostics=dr._registration_diagnostics,
            )
            _registration_diag(
                "fiducial_affine_warp "
                f"tile={dr._tile_id} round={round_id} "
                f"shape={tuple(int(v) for v in moving.shape)} "
                f"elapsed_s={timeit.default_timer() - warp_start_time:.2f}",
                enabled=dr._registration_diagnostics,
            )
            clear_rlgc_caches(clear_memory_pool=True)
            _release_worker_gpu_memory(cp)

            sofima_start_time = timeit.default_timer()
            sofima_flow_field, metadata = estimate_sofima_flow_field_xyz_px(
                fixed,
                warped.astype(np.float32, copy=False),
                config=dr._sofima_config,
            )
            dr._datastore.save_local_sofima_flow_field(
                sofima_flow_field,
                tile=dr._tile_id,
                round=round_id,
                reference_round=reference_round_id,
                map_stride_zyx_px=metadata["map_stride_zyx_px"],
                map_box_start_xyz_px=metadata["map_box_start_xyz_px"],
                map_box_size_xyz_px=metadata["map_box_size_xyz_px"],
                reference_shape_zyx_px=fixed.shape,
                moving_shape_zyx_px=warped.shape,
                sofima_status=metadata.get("status", "ok"),
                valid_flow_vectors=metadata.get("valid_flow_vectors"),
                return_future=False,
            )
            _registration_diag(
                "fiducial_sofima_done "
                f"tile={dr._tile_id} round={round_id} "
                f"status={metadata.get('status', 'ok')} "
                f"valid_flow_vectors={metadata.get('valid_flow_vectors')} "
                f"elapsed_s={timeit.default_timer() - sofima_start_time:.2f}",
                enabled=dr._registration_diagnostics,
            )
            result_queue.put(("result", round_id, None))
            del moving, warped, sofima_flow_field
            clear_rlgc_caches(clear_memory_pool=True)
            _release_worker_gpu_memory(cp)

        del fixed
        clear_rlgc_caches(clear_memory_pool=True)
        _release_worker_gpu_memory(cp)
    except Exception:
        result_queue.put(("error", None, traceback.format_exc()))
        raise

_read_fiducial_sim(input_path, scale, translation, affine_zyx_px, transform_key)

Read one native fiducial OME-Zarr as a SpatialImage.

Parameters:

Name Type Description Default
input_path Path

Local fiducial OME-Zarr path.

required
scale dict[str, float]

Physical pixel spacing by spatial dimension.

required
translation dict[str, float]

Stage-derived physical origin by spatial dimension.

required
affine_zyx_px Any

Camera-to-stage affine transform loaded from datastore metadata.

required
transform_key str

Transform key used for stage metadata in the returned SpatialImage.

required

Returns:

Type Description
Any

SpatialImage with datastore stage metadata attached under stage_metadata.

Source code in src/merfish3danalysis/DataRegistration.py
def _read_fiducial_sim(
    input_path: Path,
    scale: dict[str, float],
    translation: dict[str, float],
    affine_zyx_px: Any,
    transform_key: str,
) -> Any:
    """
    Read one native fiducial OME-Zarr as a SpatialImage.

    Parameters
    ----------
    input_path : pathlib.Path
        Local fiducial OME-Zarr path.
    scale : dict[str, float]
        Physical pixel spacing by spatial dimension.
    translation : dict[str, float]
        Stage-derived physical origin by spatial dimension.
    affine_zyx_px : Any
        Camera-to-stage affine transform loaded from datastore metadata.
    transform_key : str
        Transform key used for stage metadata in the returned SpatialImage.

    Returns
    -------
    Any
        SpatialImage with datastore stage metadata attached under
        ``stage_metadata``.
    """
    import zarr
    from multiview_stitcher import spatial_image_utils as si_utils
    from yaozarrs import open_group

    array = open_group(input_path)["0"].to_zarr_python()
    dims = _zarr_array_dims(array)

    metadata = array.metadata.to_dict()
    if metadata.get("zarr_format") == 3 and "dimension_names" in metadata:
        # Build a second read-only handle without storage-level axis labels.
        # multiview-stitcher receives the explicit dims above and can therefore
        # add singleton t/c axes without producing rank-inconsistent virtual
        # Zarr metadata. The source store and its metadata are never modified.
        metadata.pop("dimension_names")
        array = zarr.Array(
            zarr.AsyncArray(
                metadata=metadata,
                store_path=array.store_path,
            )
        )

    scale = {axis: round_pixel_size_um(value) for axis, value in scale.items()}
    return si_utils.get_sim_from_array(
        array,
        dims=dims,
        scale=scale,
        translation=translation,
        affine=affine_zyx_px,
        transform_key=transform_key,
        c_coords=None,
        t_coords=None,
    )

_registration_diag(message, *, enabled)

Print one timestamped registration diagnostic message when enabled.

Parameters:

Name Type Description Default
message str

Diagnostic message body.

required
enabled bool

If True, print the diagnostic message.

required

Returns:

Type Description
None

The message is printed only when diagnostics are enabled.

Source code in src/merfish3danalysis/DataRegistration.py
def _registration_diag(message: str, *, enabled: bool) -> None:
    """
    Print one timestamped registration diagnostic message when enabled.

    Parameters
    ----------
    message : str
        Diagnostic message body.
    enabled : bool
        If True, print the diagnostic message.

    Returns
    -------
    None
        The message is printed only when diagnostics are enabled.
    """
    if enabled:
        print(time_stamp(), f"[registration-diagnostics] {message}", flush=True)

_release_worker_gpu_memory(cp)

Release cached CuPy allocations in a worker process.

Parameters:

Name Type Description Default
cp Any

Imported CuPy module for the worker process.

required

Returns:

Type Description
None

The current CUDA stream is synchronized and GPU memory caches are released.

Source code in src/merfish3danalysis/DataRegistration.py
def _release_worker_gpu_memory(cp: Any) -> None:
    """
    Release cached CuPy allocations in a worker process.

    Parameters
    ----------
    cp : Any
        Imported CuPy module for the worker process.

    Returns
    -------
    None
        The current CUDA stream is synchronized and GPU memory caches are
        released.
    """
    gc.collect()
    cp.cuda.Stream.null.synchronize()
    try:
        cp.fft.config.get_plan_cache().clear()
    except Exception:
        pass
    try:
        import cupyx

        cupyx.scipy.fft.clear_plan_cache()
    except Exception:
        pass
    cp.get_default_memory_pool().free_all_blocks()
    cp.get_default_pinned_memory_pool().free_all_blocks()
    try:
        import torch

        if torch.cuda.is_available():
            torch.cuda.synchronize()
            torch.cuda.empty_cache()
    except Exception:
        pass
    gc.collect()

_resolve_psf(psfs, psf_idx)

Fetch PSF by index from uniform or ragged channel PSF storage.

Parameters:

Name Type Description Default
psfs Any

PSF container. This may be a list of arrays or a stacked numpy-like array whose first axis indexes channels.

required
psf_idx int

Channel PSF index to return.

required

Returns:

Type Description
ndarray

Selected PSF as a float32 array.

Source code in src/merfish3danalysis/DataRegistration.py
def _resolve_psf(psfs: Any, psf_idx: int) -> np.ndarray:
    """
    Fetch PSF by index from uniform or ragged channel PSF storage.

    Parameters
    ----------
    psfs : Any
        PSF container. This may be a list of arrays or a stacked numpy-like
        array whose first axis indexes channels.
    psf_idx : int
        Channel PSF index to return.

    Returns
    -------
    numpy.ndarray
        Selected PSF as a float32 array.
    """
    if isinstance(psfs, list):
        if psf_idx < 0 or psf_idx >= len(psfs):
            raise IndexError(f"PSF index {psf_idx} out of range for {len(psfs)} PSFs.")
        return np.asarray(psfs[psf_idx], dtype=np.float32)

    psf_array = np.asarray(psfs)
    if psf_array.ndim < 3:
        raise ValueError(f"Invalid PSF array shape: {psf_array.shape}")
    if psf_idx < 0 or psf_idx >= psf_array.shape[0]:
        raise IndexError(
            f"PSF index {psf_idx} out of range for shape {psf_array.shape}."
        )
    return np.asarray(psf_array[psf_idx], dtype=np.float32)

_restrict_worker_to_assigned_gpu(gpu_id)

Restrict a spawned worker process to one physical CUDA device.

Parameters:

Name Type Description Default
gpu_id int

Physical GPU index assigned by the parent process.

required

Returns:

Type Description
int

Local CUDA device index visible inside the restricted worker. This is always 0 after setting CUDA_VISIBLE_DEVICES.

Source code in src/merfish3danalysis/DataRegistration.py
def _restrict_worker_to_assigned_gpu(gpu_id: int) -> int:
    """
    Restrict a spawned worker process to one physical CUDA device.

    Parameters
    ----------
    gpu_id : int
        Physical GPU index assigned by the parent process.

    Returns
    -------
    int
        Local CUDA device index visible inside the restricted worker. This is
        always 0 after setting ``CUDA_VISIBLE_DEVICES``.
    """
    os.environ["CUDA_VISIBLE_DEVICES"] = str(int(gpu_id))
    return 0

_run_chunked_rlgc_remembering_crop(dr, chunked_rlgc, image, psf, gpu_id, release_memory=False)

Run RLGC and remember the lateral chunk size that succeeds in this worker.

Parameters:

Name Type Description Default
dr Any

DataRegistration-like object containing the current cached _crop_yx_decon value.

required
chunked_rlgc Any

RLGC callable. It must accept on_successful_crop_yx.

required
image ndarray

Image to deconvolve.

required
psf ndarray

PSF to use for deconvolution.

required
gpu_id int

CUDA device index.

required
release_memory bool

If True, ask RLGC to release its CuPy memory pools before returning.

False

Returns:

Type Description
ndarray

Deconvolved image returned by chunked_rlgc.

Source code in src/merfish3danalysis/DataRegistration.py
def _run_chunked_rlgc_remembering_crop(
    dr: Any,
    chunked_rlgc: Any,
    image: np.ndarray,
    psf: np.ndarray,
    gpu_id: int,
    release_memory: bool = False,
) -> np.ndarray:
    """
    Run RLGC and remember the lateral chunk size that succeeds in this worker.

    Parameters
    ----------
    dr : Any
        DataRegistration-like object containing the current cached
        ``_crop_yx_decon`` value.
    chunked_rlgc : Any
        RLGC callable. It must accept ``on_successful_crop_yx``.
    image : np.ndarray
        Image to deconvolve.
    psf : np.ndarray
        PSF to use for deconvolution.
    gpu_id : int
        CUDA device index.
    release_memory : bool, default=False
        If True, ask RLGC to release its CuPy memory pools before returning.

    Returns
    -------
    numpy.ndarray
        Deconvolved image returned by ``chunked_rlgc``.
    """
    image_arr = np.asarray(image)
    if image_arr.ndim == 2:
        image_max_yx = max(image_arr.shape)
    else:
        image_max_yx = max(image_arr.shape[-2:])

    def _remember_successful_crop(successful_crop_yx: int) -> None:
        """
        Cache the successful RLGC crop size on the registration object.

        Parameters
        ----------
        successful_crop_yx : int
            Lateral crop size that completed without GPU memory fallback.

        Returns
        -------
        None
            The crop size is stored on ``dr``.
        """
        previous_crop_yx = int(dr._crop_yx_decon)
        requested_crop_yx = min(previous_crop_yx, int(image_max_yx))
        if int(successful_crop_yx) >= requested_crop_yx:
            return

        dr._crop_yx_decon = int(successful_crop_yx)
        if dr._verbose >= 1:
            print(
                time_stamp(),
                "RLGC reduced crop_yx after GPU memory fallback: "
                f"{previous_crop_yx} -> {successful_crop_yx}.",
            )

    try:
        import cupy as cp

        _release_worker_gpu_memory(cp)
    except Exception:
        pass

    return chunked_rlgc(
        image=image,
        psf=psf,
        gpu_id=gpu_id,
        crop_yx=dr._crop_yx_decon,
        crop_z=None,
        release_memory=release_memory,
        on_successful_crop_yx=_remember_successful_crop,
    )

_write_zarr_max_projection_tiff(*, array, filename_path, spacing_zyx_um, tile_shape_yx=(1024, 1024))

Write a tiled OME-TIFF Z projection without materializing the mosaic.

Parameters:

Name Type Description Default
array Any

Open fused Zarr array.

required
filename_path Path

Destination OME-TIFF path.

required
spacing_zyx_um ndarray

Physical voxel spacing in micrometers and Z, Y, X order.

required
tile_shape_yx tuple[int, int]

TIFF tile height and width.

(1024, 1024)

Returns:

Type Description
None

The projection is streamed to filename_path.

Source code in src/merfish3danalysis/DataRegistration.py
def _write_zarr_max_projection_tiff(
    *,
    array: Any,
    filename_path: Path,
    spacing_zyx_um: np.ndarray,
    tile_shape_yx: tuple[int, int] = (1024, 1024),
) -> None:
    """
    Write a tiled OME-TIFF Z projection without materializing the mosaic.

    Parameters
    ----------
    array : Any
        Open fused Zarr array.
    filename_path : pathlib.Path
        Destination OME-TIFF path.
    spacing_zyx_um : numpy.ndarray
        Physical voxel spacing in micrometers and Z, Y, X order.
    tile_shape_yx : tuple[int, int], default=(1024, 1024)
        TIFF tile height and width.

    Returns
    -------
    None
        The projection is streamed to ``filename_path``.
    """
    from tifffile import TiffWriter

    dims = _zarr_array_dims(array)
    axis = {dim: dims.index(dim) for dim in dims}
    shape_yx = (int(array.shape[axis["y"]]), int(array.shape[axis["x"]]))
    with TiffWriter(filename_path, bigtiff=True) as tif:
        tif.write(
            data=_iter_zarr_max_projection_tiles(
                array,
                tile_shape_yx=tile_shape_yx,
            ),
            shape=shape_yx,
            dtype=array.dtype,
            tile=tile_shape_yx,
            resolution=(
                1e4 / round_pixel_size_um(spacing_zyx_um[2]),
                1e4 / round_pixel_size_um(spacing_zyx_um[1]),
            ),
            compression="zlib",
            compressionargs={"level": 8},
            predictor=True,
            photometric="minisblack",
            resolutionunit="CENTIMETER",
            metadata={
                "axes": "YX",
                "SignificantBits": int(np.dtype(array.dtype).itemsize * 8),
                "PhysicalSizeX": round_pixel_size_um(spacing_zyx_um[2]),
                "PhysicalSizeXUnit": "µm",
                "PhysicalSizeY": round_pixel_size_um(spacing_zyx_um[1]),
                "PhysicalSizeYUnit": "µm",
            },
        )

_zarr_array_dims(array)

Resolve explicit dimension names for a Zarr image array.

Parameters:

Name Type Description Default
array Any

Open Zarr array whose metadata and rank describe the image axes.

required

Returns:

Type Description
tuple[str, ...]

Dimension names in storage order. Conventional trailing TCZYX names are inferred when the array has no dimension-name metadata.

Raises:

Type Description
ValueError

If dimension names are absent from an array with more than five axes.

Source code in src/merfish3danalysis/DataRegistration.py
def _zarr_array_dims(array: Any) -> tuple[str, ...]:
    """
    Resolve explicit dimension names for a Zarr image array.

    Parameters
    ----------
    array : Any
        Open Zarr array whose metadata and rank describe the image axes.

    Returns
    -------
    tuple[str, ...]
        Dimension names in storage order. Conventional trailing TCZYX names
        are inferred when the array has no dimension-name metadata.

    Raises
    ------
    ValueError
        If dimension names are absent from an array with more than five axes.
    """
    dimension_names = getattr(array.metadata, "dimension_names", None)
    if dimension_names is None or any(name is None for name in dimension_names):
        if array.ndim > 5:
            raise ValueError(
                f"Cannot infer axes for a {array.ndim}-dimensional Zarr array."
            )
        return ("t", "c", "z", "y", "x")[-array.ndim :]
    return tuple(str(name) for name in dimension_names)