[[32 43 65 95 37 88]
[ 4 34 82 30 26 22]
[69 78 42 87 56 81]
[ 5 41 48 63 92 53]
[72 56 62 23 33 51]]
Array shape: (5, 6)
Neuroinformatics Open Software Summer School 2026
Open these slides on your laptop
neuroinformatics.dev/course-large-array-data
They contain links to exercises, polls and further references.
Also, activate your (conda) environment and install the helper functions for the course
Download the exercises

Alessandro Felder

Igor Tatarnikov

Kimberly Meechan

Harry Carey

Jim Bednar

Computers store data as bits , arranged in bytes of 8 bits.
Computers store data as bits , arranged in bytes of 8 bits. There are also binary system equivalents
We treat KB and KiB as equivalent for our purposes.
Lots of scientific data comes in (large) array form.
49.7 TB across 2048 files and 139 subjects
In a typical experiment (for example the ones described in the section Recording and quantification of social behaviours), we record from over 30 unique hardware devices and over 60 unique data streams, and when including electrophysiology data from two Neuropixels probes, the total acquisition throughput can reach up to 200 GB per hour.

Volume electron microscopy
(~2TB)
https://charts.gc.ca/charts-cartes/bathymetry-index-bathymetrique-eng.html
Exploring array size and shape with numpy
numpy arraysnumpy is the default Python library to process array data.
NumPy supports a very wide range of data types, but there are a few that are most common in scientific applications:
| NumPy datatype | Full name | Range of values |
|---|---|---|
bool |
Boolean (8-bit) | [False, True] |
uint8 |
Unsigned integer 8-bit | 0…255 |
uint16 |
Unsigned integer 16-bit | 0…65535 |
float32 |
Float 32-bit | \(-3.4 \times 10^{38}...+3.4 \times 10^{38}\) |
float64 |
Float 64-bit | \(-1.7 \times 10^{308}...+1.7 \times 10^{308}\) |
numpy arraynumpy array[[32 43 65 95 37 88]
[ 4 34 82 30 26 22]
[69 78 42 87 56 81]
[ 5 41 48 63 92 53]
[72 56 62 23 33 51]]
Array shape: (5, 6)
numpy arrayExtract its bytes.
Extract its bytes
60 bytes total
First five elements:
0000000000100000 0000000000101011 0000000001000001 0000000001011111 0000000000100101 ...
numpy is great!It works well for many applications and is very stable.
Using numpy keeps your code simple, more accessible, and easy-to-read.
So what’s the problem?

Data being processed on a computer.

Data being processed on a computer.

Data being processed on a computer.

Data being processed on a computer.
When we make a numpy array, the memory it needs is allocated eagerly (i.e. immediately).
This is usually fine, but…
…vastly exceeds an average laptop’s memory (8-32 GB) in size.
Therefore, allocating memory eagerly is a bad idea!

Data being processed on a computer.
notebooks/1_memory_cap.ipynbhttp://localhost:8888/lab?
Eager memory allocation is a problem for large arrays.
“Lazy” operations:
Hold off allocating memory/doing computations for as long as we can…
…and only operate on the minimum amount of in-memory data needed.
Our goal is to plot a slice of a 3D brain image.
Eager strategy:
Our goal is to plot a slice of a 3D brain image on disk.
Lazy strategy:
numpy can do thisA memory map lets us treat a file on disk as if it were in memory.
The operating system loads parts of it from disk into memory only as they’re actually accessed.
numpy can do thisnumpy memory mapsWe would benefit from increased flexibility like
dask has solved this problem for us.
dask arrayAn array of numpy arrays
that also behaves like a lazy numpy array.
dask arrayAn evolution of the numpy array
dask keeps track of lazy operations in a “task graph”
dask arraydask arraydask.array<random, shape=(10000, 10000), dtype=float64, chunksize=(1000, 1000), chunktype=numpy.ndarray>
Lazy evaluation means print(x) doesn’t load array data into memory.
dask arrayLazy evaluation means print(x) and slicing still doesn’t load array data into memory.
dask arraydask.array<getitem, shape=(100, 100), dtype=float64, chunksize=(100, 100), chunktype=numpy.ndarray>
dask array.compute method gives us control over when to actually load into memory.
dask array[[0.03253803 0.2614327 0.64681631 ... 0.49464121 0.46261251 0.17876863]
[0.01697999 0.6643703 0.58008443 ... 0.01420531 0.90376143 0.31415469]
[0.95924724 0.16851846 0.09130982 ... 0.35808623 0.82692309 0.88373208]
...
[0.68139916 0.6499788 0.85691498 ... 0.96517229 0.01258917 0.35944043]
[0.27329427 0.40688852 0.66911353 ... 0.17843952 0.24625911 0.67287817]
[0.359546 0.85340175 0.92695889 ... 0.3839975 0.44808929 0.62934189]]
.compute method gives us control over when to actually load into memory.
dask delayeddelayed function allows us to execute our own functions lazily.
dask delayedDelayed('ndarray-6639df84-fc12-4b55-b394-c2452a27e7d0')
[10713 10070 9443 10340 8764 10698 9925 9931 9548 9109 9852 9778
9391 9987 9454 9959 10309 11093 10239 10865 8978 8706 9623 9754
9861 9302 9413 8719 9348 11095 10199 11380 10790 9714 10914 8905
9777 10032 10765 9924 9883 10885 10268 9401 9282 9391 9267 10121
9658 9377 9686 10151 9805 9766 10398 10098 9550 9668 9545 9832
9912 9328 9486 9309 10835 9481 9830 9937 10057 9875 9994 10111
10204 9341 9334 9370 9709 10305 9290 9037 10736 10070 9729 8456
9310 9280 9567 9878 10244 8613 9541 10346 10905 10115 10486 9773
10633 9783 10230 10062]
delayed function allows us to execute our own functions lazily.
Loading of large array slices with
numpy (eager, via tifffile.imread)numpy.memmap (lazy, via tifffile.memmap)daskshowcasing the increased flexibility of dask.
If you want to run on your own data
notebooks/2_numpy_to_dask.ipynbhttp://localhost:8888/lab?dask arraysSo far, we have only created dask arrays in-memory (lazily). But how do we store them on disk?
zarr has solved this problem for us.
zarr-pythonZarr is not the same as zarr-python.
zarr-python is a Python library to work with Zarr files
Showcases
dask reading a file from a public cloud archivedask processing the data and storing part of it to zarrnotebooks/3_dask_remote_to_zarr_io.ipynbhttp://localhost:8888/lab?numpy eager memory allocation is a bad idea for large arraysnumpy.memmap helps but has limitationsdask is more flexible
delayedzarr
One of the main libraries for interacting with Zarr in python is zarr-python.
Quick demo of using zarr-python to make arrays and groups: notebooks/zarr_python_example.ipynb
The zarr specification dictates how a Zarr file should be structured.
As we saw in the demo, the main components of a Zarr file are:
zarr.json file with basic metadataOne of Zarr’s great strengths is that it can be applied to any kind of array data across many fields.
Their website’s dataset section includes:
Equally, one of Zarr’s great weaknesses is that it is general!
When we use it for a specific kind of data (say biological images), there is no consistent place for related metadata to be stored.
For example:
This makes it harder to share datasets with others, and to process datasets that weren’t created by yourself.
Specific fields can get around this limitation by extending the Zarr specification.
Let’s take a look at some OME-Zarr metadata using ome-zarr-models.
OME-Zarr models is a python package specialised to validating and loading OME-Zarr metadata.
Quick demo of accessing metadata: notebooks/ome-zarr-models_example.ipynb
Part of the OME-Zarr specification adds multiscales metadata.

Viewing a whole slice from a multi-terabyte dataset can still be hundreds of GB!
Image from Vergara et al. 2021 - Whole-body integration of gene expression and single-cell morphology
Specialised viewers dynamically load different resolution levels.
Web examples:
Local examples:
Spec versions can get complicated.
ome-ngff tag on image.scThe ecosystem of tools / packages around OME-Zarr is rapidly evolving.
Don’t be afraid to try different options, and see what works best for your data.
notebooks/creating_ome_zarr.ipynb: creating OME-Zarr step by stepnotebooks/converting_ome_zarr.ipynb: libraries for converting to OME-Zarrnotebooks/visualising_ome_zarr.ipynb: viewing OME-Zarr in naparidaskdaskHow should we coordinate array operations so Python can execute them at the same time?
Two types of concurrency built into Python
import threading
counter = 0
lock = threading.Lock()
def increment(n):
global counter
for _ in range(n):
with lock: # make sure counter doesn't get updated simultaneously
counter += 1
threads = [threading.Thread(target=increment, args=(100_000,)) for _ in range(4)]
with ThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(increment, 100_000) for _ in range(4)]
for f in futures:
f.result()
print(counter)400000
Queueimport threading
import queue
q = queue.Queue()
def worker():
while True:
item = q.get()
print(f'Working on {item}')
print(f'Finished {item}')
q.task_done()
# Turn-on the worker thread.
threading.Thread(target=worker, daemon=True).start()
# Send thirty task requests to the worker.
for item in range(30):
q.put(item)
# Block until all tasks are done.
q.join()
print('All work completed')2222
[3, 7, 11, 15]
36
Python has concurrency built-in.
Use Queue for safe communication.
But writing concurrent code is complex and error-prone!
Dask provides some abstractions so you don’t have to worry about the intricacies.
We can split operations on arrays into two types:
We can split operations on arrays into two types:
Neighbouring elements may live in a different chunk!
dask/numpy arraysSome extra dask/numpy syntax that will come in handy
wherenanmeandask chunksYou can tell dask to apply a function lazily on each chunk
map_block (each chunk has all the info)map_overlap (you need info from neighbouring chunks)Exercise 4 will guide you through a realistic analysis of multiphoton imaging data that uses chunkwise dask operations
Also showing how to choose between multi-threading and multi-processing.
notebooks/4_parallel_processing_with_dask.ipynbhttp://localhost:8888/lab?daskFavour robustness and simplicity over speed
Goal: abstract the array backend
Goal: work with different array backends
map_blocks to apply chunkwise operationsmap_overlap if you need information from neighbouring chunks,
depth parameterdask Arrays operate concurrently using multi-threadingComputers store data as bits.
60 bytes total
First five elements:
0000000000100000 0000000000101011 0000000001000001 0000000001011111 0000000000100101 ...
We can be smart about how to store this data with fewer bits.
60 bytes total
First five elements:
0000000000100000 0000000000101011 0000000001000001 0000000001011111 0000000000100101 ...
We could “encode” something like “assume the first 8 bits are always 0” and halve the file size.
60 bytes total
First five elements:
0000000000100000 0000000000101011 0000000001000001 0000000001011111 0000000000100101 ...
Some compression algorithms preserve all the information.
Others are lossy, e.g. “.jpg” files
Codec stands for encoder/decoder.
If defines how data is written to/read from disk.
Different codecs will work better/worse for different data.
Generally, we expect a smaller size on disk to result in slower read/write.
But compression algorithms can be clever about this too.
Two audio codecs work well on electrophysiology data.
Better compression ratio, slower decompression speed
Buccino et al. (2023)
Zarr provides a number of Codecs that you can specify when writing files.
Compares write speeds and disk size for different compression levels.
notebooks/5_remote_file_compression.ipynbhttp://localhost:8888/lab?There are lots of settings that can be adjusted when writing Zarr and OME-Zarr images:
These settings affect:
Write time
Read time
Size on disk
Number of files
Memory usage for different operations
Choosing settings is a trade-off between these different factors. You have to decide what you want to prioritise for your data.
Benchmarks were made as part of the HEFTIE project:
3 images: heart (335 MB), dense segmentation, sparse segmentation
All sized 806 x 629 x 629
All 16-bit unsigned integer
Heart: HiP-CT scan of a heart from the Human Organ Atlas

Dense: segmented neurons from electron microscopy

Sparse: A few select segmented neurons from electron microscopy

pytest-benchmarkHigher compression level = higher compression ratio = longer write time
Higher compression level doesn’t usually result in longer read times. For many compressors this is a feature of their design, with a large one-off cost of compressing the data, but no slow down when reading it
Larger chunks = lower compression ratio (but only slightly)
Larger chunks = faster write and read times (mainly due to fewer total files)
Heart image
Dense segmentation
Sparse segmentation 
Segmentations compress much more!

Which software library you use affects read / write times. Tensorstore had the best performance.
Some general recommendations:
Use tensorstore library for fastest read/write
blosc-zstd is a good default compressor
Use a high compression level to get the smallest file size (usually means longer write times, but not much effect on read time)
Smaller chunks = smaller overall file size + less memory usage
Larger chunks = faster read/write times + fewer files
It’s a balance! People often use 64x64x64 or 128x128x128
This is different for different data! Worth testing some small samples of your own data with different settings
One of the settings we didn’t benchmark was ‘sharding’:
Let’s experiment with sharding options:
notebooks/sharding.ipynbCase studies from the Neuroinformatics Unit
suite2p, CaImAn)We need flexibility to customise!
Vision: Mix and match like
A project in its very early stages
Demo time!
spikeinterfacephoton-mosaic-pipeline for batch-processing on HPCsnakemake.
Alessandro Felder

Laura Porta

Alessio Buccino

Arielle Leon

Johannes Friedrich

Sean McCulloch

Georg Raiser
Collaboration day ideas? Open an issue.
brainglobe-atlasapiEstablished 2020 with three aims:

Problem - the data analysis tools are fragmented
Claudi et al. (2020)

Claudi et al. (2020)

Claudi et al. (2020)
Atlases are made up of 4 main components:
brainglobe-atlasapi V2.tiff files.obj files.tar.gz file
brainglobe-atlasapi V3brainglobe-atlasapi V3
brainglobe-atlasapi annotations(798, 1320, 800, 1140)
Finding centre coordinates of cells (bright blobs) in large arrays.

Osten and Margrie (2013)
cellfinder input data
cellfinder input data
Finding centre coordinates of cells (bright blobs) in large arrays.
Arrays are ~100GB / channel, stored as 2D tiffs (40 MB each)
A series of 2D and 3D filtering and thresholding operations
Store centres of bright blobs (“cell candidates”)
Use a classification algorithm


cellfinder codebaselazy_imread = delayed(tifffile.imread) # lazy reader
def read_with_dask(path: str | Path) -> da.Array:
...
shape, dtype = get_tiff_meta(filenames[0])
lazy_arrays = [
lazy_imread(fn, is_ome=False)
for fn in get_sorted_file_paths(filenames)
]
dask_arrays = [
da.from_delayed(delayed_reader, shape=shape, dtype=dtype)
for delayed_reader in lazy_arrays
]
stack = da.stack(dask_arrays, axis=0)
return stack@inference_wrapper
def _plane_filter(
process: ProcessWithException,
tile_processor: TileProcessor,
n_threads: int,
buffers: List[Tuple[torch.Tensor, torch.Tensor]],
):
...
while True:
msg = process.get_msg_from_mainthread()
if msg == EOFSignal:
return
# with torch multiprocessing, tensors are shared in memory - so
# just update in place
token, i = msg
tensor, masks = buffers[token]
plane, mask = tile_processor.get_tile_mask(tensor[i : i + 1, :, :])
tensor[i : i + 1, :, :] = plane
masks[i : i + 1, :, :] = mask
# tell the main thread we processed all the planes for this tensor
process.send_msg_to_mainthread(None)def start_dataset_thread(self, num_workers: int) -> None:
# include queue for host thread
ctx = mp.get_context("spawn")
# we use maxsize=0 to prevent potential locking issues. But, we never
# actually request more than one data batch at a time over a queue
queues = [ctx.Queue(maxsize=0) for _ in range(num_workers + 1)]
self._worker_queues = queues
self._dataset_thread = ThreadWithExceptionMPSafe(
target=_read_data_send_cuboids,
args=(self.src_image_data, queues),
pass_self=True,
)
self._dataset_thread.start()dask for lazy loadingpytorch for processing
Cellfinder efficiently processes large arrays.
Cellfinder efficiently processes large arrays.
Collaboration day ideas? Open an issue.
Large arrays do not fit into memory
We can deal with this through
zarr-python and dask are generalist libraries for processing large arrays (in parallel, with compression, anywhere).zarr files, ideally following a community standard (e.g. OME-Zarr).Niko Sirmpilatze
Please provide your honest feedback :)

Sainsbury Wellcome Centre | OSSS | 17-21 August 2026