Skip to content

Commit 8ec8981

Browse files
committed
WIP FormatMultiImage refactor
1 parent 04dff3d commit 8ec8981

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

src/dxtbx/format/FormatMultiImage.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3+
import copy
34
import functools
45
import os
6+
from pathlib import Path
57
from typing import Sequence, Type
68

79
from scitbx.array_family import flex
@@ -340,3 +342,190 @@ def get_imageset(
340342
_add_static_mask_to_iset(format_instance, iset)
341343

342344
return iset
345+
346+
@classmethod
347+
def create_imagesequence(
348+
cls,
349+
filenames: Sequence[str],
350+
single_file_indices: Sequence[int] | None = None,
351+
format_kwargs: dict | None = None,
352+
) -> ImageSequence:
353+
"""Create an ImageSequence by reading the image file"""
354+
return cls.get_imageset(
355+
filenames,
356+
single_file_indices=single_file_indices,
357+
format_kwargs=format_kwargs,
358+
as_sequence=True,
359+
)
360+
361+
@classmethod
362+
def reload_imagesequence(
363+
cls,
364+
filenames: Sequence[str],
365+
single_file_indices: Sequence[int],
366+
beam: model.Beam,
367+
detector: model.Detector,
368+
goniometer: model.Goniometer | None = None,
369+
scan: model.Scan | None = None,
370+
format_kwargs: dict | None = None,
371+
) -> ImageSequence:
372+
"""
373+
Construct an ImageSequence from existing data
374+
"""
375+
# Although this accepts filenames as a sequence, for Liskov substitution
376+
# reasons, since this is a FormatMultiImage we only support _one_ file
377+
# that contains many images in a single dataset, not multiple files.
378+
if len(filenames) == 0:
379+
raise ValueError("Error: Cannot make an ImageSequence out of no files")
380+
elif len(filenames) > 1:
381+
raise ValueError(
382+
"Error: FormatMultiImage.get_imagesequence only supports single multiimage files"
383+
)
384+
385+
# Default empty kwargs
386+
format_kwargs = format_kwargs or {}
387+
# Make filenames absolute
388+
filename = Path(filenames[0]).absolute()
389+
# WIP: Ensure that we can't use this again
390+
del filenames
391+
392+
assert single_file_indices, (
393+
"We must have single file indices to reload ImageSequence"
394+
)
395+
num_images = len(single_file_indices)
396+
397+
# Get the format instance
398+
# assert len(filenames) == 1
399+
# cls._current_filename_ = None
400+
# cls._current_instance_ = None
401+
# format_instance = cls.get_instance(filename, **format_kwargs)
402+
403+
# if num_images is None:
404+
# # As we now have the actual format class we can get the number
405+
# # of images from here. This saves having to create another
406+
# # format class instance in the Reader() constructor
407+
# # NOTE: Having this information breaks internal assumptions in
408+
# # *Lazy classes, so they have to figure this out in
409+
# # their own time.
410+
# num_images = format_instance.get_num_images()
411+
412+
# Get some information from the format class
413+
reader = cls.get_reader()([filename], num_images=num_images, **format_kwargs)
414+
415+
# WIP: This should always be safe if we have the actual format class,
416+
# and the assumption we have in this version is that we do
417+
assert not cls.is_abstract()
418+
# WIP: ... BUT, we don't have a FormatClass instance (and probably
419+
# don't need to have one), so maybe we need to change all these
420+
# existing examples to ClassMethod? Or, check_format=False always
421+
# just set this blank, so maybe we never need this after initial
422+
# load.
423+
vendor = ""
424+
425+
if not single_file_indices:
426+
raise ValueError(
427+
"single_file_indices can not be empty to reload FormatMultiImage ImageSequence"
428+
)
429+
single_file_indices = flex.size_t(single_file_indices)
430+
431+
# Check indices are sequential
432+
if (
433+
max(single_file_indices) - min(single_file_indices)
434+
== len(single_file_indices) - 1
435+
):
436+
raise ValueError("single_file_idices are not sequential")
437+
num_images = len(single_file_indices)
438+
439+
# Check the scan makes sense - we must want to use <= total images
440+
if scan is not None:
441+
assert scan.get_num_images() <= num_images
442+
443+
# Create the masker
444+
format_instance: FormatMultiImage = None
445+
446+
loader = DeferredLoader(
447+
cls,
448+
filename,
449+
format_kwargs=format_kwargs,
450+
gonimeter=goniometer,
451+
)
452+
453+
isetdata = ImageSetData(
454+
reader=reader,
455+
masker=loader.load_dynamic_mask,
456+
vendor=vendor,
457+
params=format_kwargs,
458+
format=cls,
459+
template=filename,
460+
)
461+
462+
# Create the sequence
463+
iset = ImageSequence(
464+
isetdata,
465+
beam=beam,
466+
detector=detector,
467+
goniometer=goniometer,
468+
scan=scan,
469+
indices=single_file_indices,
470+
)
471+
472+
# Handle merging detector static mask... if necessary
473+
assert iset.external_lookup.mask.data.empty(), (
474+
"Deferred static mask loading assumes nothing loaded already"
475+
)
476+
477+
# Set up deferred instantiation and loading of the format class
478+
iset.external_lookup.mask.set_data_generator(loader.load_static_mask)
479+
return iset
480+
481+
# WIP: Things reloading reads involving format_instance
482+
# - [x] _add_static_mask_to_iset(format_instance, iset)
483+
# - Added set_data_generator to ExternalLookupItem
484+
# - [x] masker = format_instance.get_masker(goniometer=goniometer)
485+
# - Masker is now a callable that returns the masker on request
486+
# - [x] vendor = format_instance.get_vendortype()
487+
# - Have just removed when reloading, for now
488+
# - [x] reader = cls.get_reader()(filenames, num_images=num_images, **format_kwargs)
489+
# - Reader now lazily loads rather than eagerly loads
490+
491+
492+
class DeferredLoader:
493+
"""
494+
Single object to hold data related to deferred loading
495+
496+
We don't want to eagerly access the raw image data on disk unless
497+
it's actively requested. This class holds the information to do this,
498+
and instances of it's methods passed into appropriate places so that
499+
they can load on demand.
500+
501+
Also, the objects this is going on are likely to be passed through a
502+
pickly boundary, so needs to be statically defined.
503+
"""
504+
505+
def __init__(
506+
self,
507+
format: Type[Format],
508+
filename: str,
509+
format_kwargs: dict,
510+
gonimeter: model.Goniometer,
511+
):
512+
self.format = format
513+
self.filename = filename
514+
self.format_kwargs = copy.deepcopy(format_kwargs or {})
515+
self.goniometer = gonimeter
516+
517+
def load_static_mask(self) -> ImageBool | None:
518+
print("Deferred loading of static mask")
519+
static_mask = self.format.get_instance(
520+
self.filename, **self.format_kwargs
521+
).get_static_mask()
522+
if static_mask:
523+
return ImageBool(static_mask)
524+
return None
525+
526+
def load_dynamic_mask(self) -> dxtbx.masking.GoniometerShadowMasker | None:
527+
"""Lazy reading of dynamic masking"""
528+
print("Rendering masker")
529+
return self.format.get_instance(self.filename, **self.format_kwargs).get_masker(
530+
goniometer=self.goniometer
531+
)

0 commit comments

Comments
 (0)