diff --git a/Smartscope/core/db_manipulations.py b/Smartscope/core/db_manipulations.py index e16b6e91..8b0095b0 100755 --- a/Smartscope/core/db_manipulations.py +++ b/Smartscope/core/db_manipulations.py @@ -86,6 +86,13 @@ def update_target_label(model:models.Model,objects_ids:List[str],value:str,metho for obj in new_objs: Classifier(object_id=obj, method_name=method,content_type=content_type, label=value).save() +def update_target_status(model:models.Model,objects_ids:List[str],value:str, *args, **kwargs): + objs = list(model.objects.filter(pk__in=objects_ids)) + with transaction.atomic(): + for obj in objs: + obj.status = value + obj.save() + def set_or_update_refined_finder(object_id, stage_x, stage_y, stage_z): from .models.target_label import Finder @@ -124,7 +131,8 @@ def group_holes_for_BIS(hole_models, max_radius=4, min_group_size=1, queue_all=F f'grouping params, max radius = {max_radius}, min group size = {min_group_size}, queue all = {queue_all}, max iterations = {iterations}, score_weight = {score_weight}') # Extract coordinated for the holes prefetch_related_objects(hole_models, 'finders') - coords = np.array([[list(h.finders.all())[0].stage_x, list(h.finders.all())[0].stage_y] for h in hole_models]) + coords = [] + coords = np.array([h.stage_coords for h in hole_models]) input_number = len(hole_models) # Generate distance matrix cd = cdist(coords, coords) @@ -275,7 +283,7 @@ def add_high_mag(grid, parent): def select_n_squares(parent, n): squares = np.array(parent.squaremodel_set.all().filter(selected=False, status=None).order_by('area')) - squares = [s for s in squares if s.is_good()] + squares = [s for s in squares if s.is_good() and not s.is_out_of_range()] if len(squares) == 0: return split_squares = np.array_split(squares, n) @@ -295,7 +303,7 @@ def select_n_holes(parent, n, is_bis=False): holes = list(parent.holemodel_set.filter( **filter_fields).order_by('dist_from_center')) - holes = [h for h in holes if h.is_good()] + holes = [h for h in holes if h.is_good() and not h.is_out_of_range()] if n <= 0: with transaction.atomic(): @@ -333,13 +341,13 @@ def select_n_areas(parent, n, is_bis=False): if n <= 0: with transaction.atomic(): for t in targets: - if t.is_good() and not t.is_excluded()[0]: + if t.is_good() and not t.is_excluded()[0] and not t.is_out_of_range(): update(t, selected=True, status='queued') return clusters = dict() for t in targets: - if not t.is_good(): + if not t.is_good() or t.is_out_of_range(): continue excluded, label = t.is_excluded() if excluded: diff --git a/Smartscope/core/grid/run_square.py b/Smartscope/core/grid/run_square.py index a4182f6e..6df01f6c 100644 --- a/Smartscope/core/grid/run_square.py +++ b/Smartscope/core/grid/run_square.py @@ -54,7 +54,7 @@ def process_square_image(square, grid, microscope_id): if is_bis: holes = list(HoleModel.display.filter(square_id=square.square_id)) holes = group_holes_for_BIS( - [h for h in holes if h.is_good() and not h.is_excluded()[0]], + [h for h in holes if h.is_good() and not h.is_excluded()[0] and not h.is_out_of_range()], max_radius=grid.params_id.bis_max_distance, min_group_size=grid.params_id.min_bis_group_size ) diff --git a/Smartscope/core/interfaces/microscope.py b/Smartscope/core/interfaces/microscope.py index f9a9d8a2..55e32919 100755 --- a/Smartscope/core/interfaces/microscope.py +++ b/Smartscope/core/interfaces/microscope.py @@ -34,6 +34,8 @@ class AtlasSettings(BaseModel): maxY:int = Field(alias='atlas_max_tiles_Y') spotSize:int = Field(alias='spot_size') c2:float = Field(alias='c2_perc') + atlas_to_search_offset_x:float + atlas_to_search_offset_y:float atlas_c2_aperture: Optional[int] = None class Config: diff --git a/Smartscope/core/interfaces/tfsserialem_interface.py b/Smartscope/core/interfaces/tfsserialem_interface.py index a3f6b912..4dc6cac9 100644 --- a/Smartscope/core/interfaces/tfsserialem_interface.py +++ b/Smartscope/core/interfaces/tfsserialem_interface.py @@ -13,21 +13,35 @@ class Aperture: CONDENSER_3:int=3 OBJECTIVE:int = 2 -def change_aperture_temporarily(function: Callable, aperture:Aperture, aperture_size:Optional[int]): +def change_aperture_temporarily(function: Callable, aperture:Aperture, aperture_size:Optional[int], wait:int=3): def wrapper(*args, **kwargs): inital_aperture_size = int(sem.ReportApertureSize(aperture)) if inital_aperture_size == aperture_size or aperture_size is None: - return function(*args, **kwargs) + return function(*args, **kwargs) + msg = f'Changing condenser aperture {aperture} from {inital_aperture_size} to {aperture_size} and waiting {wait}s.' + sem.Echo(msg) + logger.info(msg) sem.SetApertureSize(aperture,aperture_size) + time.sleep(wait) function(*args, **kwargs) + msg = f'Resetting condenser aperture to {inital_aperture_size}.' + sem.Echo(msg) + logger.info(msg) sem.SetApertureSize(aperture,inital_aperture_size) return wrapper -def remove_objective_aperture(function: Callable): +def remove_objective_aperture(function: Callable, wait:int=3): def wrapper(*args, **kwargs): + msg = 'Removing objective aperture.' + sem.Echo(msg) + logger.info(msg) sem.RemoveAperture(Aperture.OBJECTIVE) function(*args, **kwargs) + msg = f'Reinserting objective aperture and waiting {wait}s.' + sem.Echo(msg) + logger.info(msg) sem.ReInsertAperture(Aperture.OBJECTIVE) + time.sleep(wait) return wrapper class TFSSerialemInterface(SerialemInterface): diff --git a/Smartscope/core/main_commands.py b/Smartscope/core/main_commands.py index 6c7c95b2..121e742a 100755 --- a/Smartscope/core/main_commands.py +++ b/Smartscope/core/main_commands.py @@ -122,13 +122,8 @@ def regroup_bis(grid_id, square_id): holes_for_grouping = [] other_holes = [] for h in filtered_holes: - - # h.bis_group = None - # h.bis_type = None - if h.is_good() and not h.is_excluded()[0]: + if h.is_good() and not h.is_excluded()[0] and not h.is_out_of_range(): holes_for_grouping.append(h) - # else: - # other_holes.append(h) logger.info(f'Filtered holes = {len(filtered_holes)}\nHoles for grouping = {len(holes_for_grouping)}') @@ -194,4 +189,56 @@ def download_testfiles(overwrite=False): p.wait() print('Done.') - +def get_atlas_to_search_offset(detector_name,maximum=0): + if isinstance(maximum, str): + maximum = int(maximum) + detector = Detector.objects.filter(name__contains=detector_name).first() + if detector is None: + print(f'Could not find detector with name {detector_name}') + return + completed_square = SquareModel.just_labels.filter(status='completed',grid_id__session_id__detector_id__pk=detector.pk) + count = completed_square.count() + no_finder = 0 + total_done = 0 + if maximum > 0 and count > maximum: + print(f'Found {count} completed squares, limiting to {maximum}') + count=maximum + else: + print(f'Found {count} completed squares') + + for square in completed_square: + if total_done == count: + break + finders = square.finders.all() + recenter = finders.values_list('stage_x', 'stage_y').filter(method_name='Recentering').first() + original = finders.values_list('stage_x', 'stage_y').filter(method_name='AI square finder').first() + if any([recenter is None, original is None]): + no_finder+=1 + total_done+=1 + print(f'Progress: {total_done/count*100:.0f}%, Skipped: {no_finder}', end='\r') + continue + diff = np.array(original) - np.array(recenter) + if not 'array' in locals(): + array = diff.reshape(1,2) + print(f'Progress: {total_done/count*100:.0f}%, Skipped: {no_finder}', end='\r') + total_done+=1 + continue + total_done+=1 + array = np.append(array, diff.reshape(1,2), axis=0) + print(f'Progress: {total_done/count*100:.0f}%, Skipped: {no_finder}', end='\r') + print(f'Progress: {total_done/count*100:.0f}%, Skipped: {no_finder}') + mean = np.mean(array, axis=0).round(2) + std = np.std(array, axis=0).round(2) + print(f'Calculated offset from {total_done-no_finder} squares:\n\t X: {mean[0]} +/- {std[0]} um\n\t Y: {mean[1]} +/- {std[1]} um \n') + set_values = 'unset' + while set_values not in ['y', 'n']: + set_values = input(f'atlas_to_search_offset_x={mean[0]} um\natlas_to_search_offset_y={mean[1]} um\n\nSet values of for {detector}? (y/n)') + if set_values == 'y': + detector.atlas_to_search_offset_x = mean[0] + detector.atlas_to_search_offset_y = mean[1] + detector.save() + print('Saved') + return + print('Skip setting values') + + \ No newline at end of file diff --git a/Smartscope/core/models/custom_paths.py b/Smartscope/core/models/custom_paths.py new file mode 100644 index 00000000..c305d0a4 --- /dev/null +++ b/Smartscope/core/models/custom_paths.py @@ -0,0 +1,26 @@ +from .base_model import * + +class CustomGroupPath(BaseModel): + group = models.OneToOneField( + Group, + null=True, + on_delete=models.SET_NULL, + to_field='name' + ) + path = models.CharField(max_length=300) + + class Meta(BaseModel.Meta): + db_table = 'customgrouppath' + + def __str__(self): + return f'{self.group.name}: {self.path}' + +class CustomUserPath(BaseModel): + user = models.OneToOneField(User, null=True, on_delete=models.SET_NULL, to_field='username') + path = models.CharField(max_length=300) + + class Meta(BaseModel.Meta): + db_table = 'customuserpath' + + def __str__(self): + return f'{self.user.username}: {self.path}' \ No newline at end of file diff --git a/Smartscope/core/models/screening_session.py b/Smartscope/core/models/screening_session.py index 64033319..8eeede9b 100644 --- a/Smartscope/core/models/screening_session.py +++ b/Smartscope/core/models/screening_session.py @@ -1,21 +1,56 @@ +import logging + from .base_model import * + +from .custom_paths import CustomUserPath, CustomGroupPath from Smartscope.lib.image.smartscope_storage import SmartscopeStorage from Smartscope import __version__ as SmartscopeVersion # from .microscope import Microscope # from .detector import Detector +logger = logging.getLogger(__name__) class ScreeningSessionManager(models.Manager): def get_queryset(self): return super().get_queryset().prefetch_related('microscope_id')\ .prefetch_related('detector_id') + +def root_directories(session): + root_directories = [] + if settings.USE_CUSTOM_PATHS: + # if session.custom_path is not None: + # root_directories.append(session.custom_path.path) + custom_group_path = CustomGroupPath.objects.filter(group=session.group).first() + if custom_group_path is not None: + root_directories.append(custom_group_path.path) + custom_user_path = CustomUserPath.objects.filter(user=session.user).first() + if custom_user_path is not None: + root_directories.append(custom_user_path.path) + if settings.USE_STORAGE: + root_directories.append(settings.AUTOSCREENDIR) + if (groupname:=session.group.name) is not None: + root_directories.append(os.path.join(settings.AUTOSCREENDIR,groupname)) + if settings.USE_LONGTERMSTORAGE: + root_directories.append(settings.AUTOSCREENSTORAGE) + if (groupname:=session.group.name) is not None: + root_directories.append(os.path.join(settings.AUTOSCREENSTORAGE,groupname)) + ###FIX AWS STORAGE + return root_directories + +def find_screening_session(root_directories,directory_name): + for directory in root_directories: + logger.debug(f'Looking for {directory_name} in {directory}') + if os.path.isdir(os.path.join(directory,directory_name)): + return os.path.join(directory,directory_name) + raise FileNotFoundError(f'Could not find {directory_name} in {root_directories}') class ScreeningSession(BaseModel): from .microscope import Microscope from .detector import Detector session = models.CharField(max_length=30) + user = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, to_field='username') group = models.ForeignKey( Group, null=True, @@ -49,28 +84,9 @@ def directory(self): if (directory:=cache.get(cache_key)) is not None: logger.info(f'Session {self} directory from cache.') return directory - - if settings.USE_STORAGE: - cwd = os.path.join(settings.AUTOSCREENDIR, self.working_dir) - if os.path.isdir(cwd): - cache.set(cache_key,cwd,timeout=21600) - return cwd - - if settings.USE_LONGTERMSTORAGE: - cwd_storage = os.path.join(settings.AUTOSCREENSTORAGE, self.working_dir) - if os.path.isdir(cwd_storage): - cache.set(cache_key,cwd_storage,timeout=21600) - return cwd_storage - - if settings.USE_AWS: - storage = SmartscopeStorage() - if storage.dir_exists(self.working_dir): - cache.set(cache_key,self.working_dir,timeout=21600) - return self.working_dir - - if settings.USE_STORAGE: - cache.set(cache_key,cwd,timeout=21600) - return cwd + cwd = find_screening_session(root_directories(self),self.working_directory) + cache.set(cache_key,cwd,timeout=10800) + return cwd @property def stop_file(self): @@ -87,9 +103,9 @@ def currentGrid(self): return self.autoloadergrid_set.all().order_by('position')\ .exclude(status='complete').first() - @property - def storage(self): - return os.path.join(settings.AUTOSCREENSTORAGE, self.working_dir) + # @property + # def storage(self): + # return os.path.join(settings.AUTOSCREENSTORAGE, self.working_dir) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -97,15 +113,18 @@ def __init__(self, *args, **kwargs): if not self.date: self.date = datetime.today().strftime('%Y%m%d') self.session_id = generate_unique_id(extra_inputs=[self.date, self.session]) + + @property + def working_directory(self): + return f'{self.date}_{self.session}' def save(self, *args, **kwargs): self.session = self.session.replace(' ', '_') if not self.version: self.version = SmartscopeVersion - self.working_dir = os.path.join(self.group.name, f'{self.date}_{self.session}') super().save(*args, **kwargs) return self def __str__(self): - return f'{self.date}_{self.session}' + return self.working_directory diff --git a/Smartscope/core/models/session.py b/Smartscope/core/models/session.py index 1e6e6d87..a197b362 100644 --- a/Smartscope/core/models/session.py +++ b/Smartscope/core/models/session.py @@ -34,6 +34,7 @@ from .screening_session import * from .square import * from .target import * +from .change_log import * ''' diff --git a/Smartscope/core/models/signals.py b/Smartscope/core/models/signals.py index d132d495..aacf3c7e 100644 --- a/Smartscope/core/models/signals.py +++ b/Smartscope/core/models/signals.py @@ -23,28 +23,36 @@ @receiver(pre_save, sender=SquareModel) @receiver(pre_save, sender=HoleModel) def pre_update(sender, instance, **kwargs): - if not instance._state.adding: - original = sender.objects.get(pk=instance.pk) - for new, old in zip(get_fields(instance), get_fields(original)): - if new == old: - return instance - col, new_val = new - old_val = old[1] - if col == 'selected': - change = ChangeLog(date=timezone.now(), table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, - column_name=col, initial_value=old_val.encode(), new_value=new_val.encode()) - change.save() - elif col == 'quality': - items = ChangeLog.objects.filter(table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, - column_name=col) - logger.debug([item.__dict__ for item in items]) - change, created = ChangeLog.objects.get_or_create(table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, - column_name=col) - change.date = timezone.now() - change.new_value = new_val.encode() - if created: - change.initial_value = old_val.encode() - change.save() + if instance._state.adding: + return instance + original = sender.objects.get(pk=instance.pk) + for new, old in zip(get_fields(instance), get_fields(original)): + if new == old: + continue + col, new_val = new + old_val = old[1] + if col == 'selected': + change = ChangeLog(date=timezone.now(), table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, + column_name=col, initial_value=old_val.encode(), new_value=new_val.encode()) + change.save() + continue + if col == 'quality': + items = ChangeLog.objects.filter(table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, + column_name=col) + logger.debug([item.__dict__ for item in items]) + change, created = ChangeLog.objects.get_or_create(table_name=instance._meta.db_table, grid_id=instance.grid_id, line_id=instance.pk, + column_name=col) + change.date = timezone.now() + change.new_value = new_val.encode() + if created: + change.initial_value = old_val.encode() + change.save() + continue + if col == 'status': + if old_val == status.SKIPPED and new_val != status.QUEUED: + instance.status = status.SKIPPED + logger.debug(f'Changing status of {instance} from to {status.SKIPPED}') + continue return instance diff --git a/Smartscope/core/models/square.py b/Smartscope/core/models/square.py index 731af14b..db5457f4 100644 --- a/Smartscope/core/models/square.py +++ b/Smartscope/core/models/square.py @@ -17,6 +17,14 @@ def get_queryset(self): .prefetch_related('selectors')\ .prefetch_related('holemodel_set') +class SquareLabelManager(models.Manager): + + def get_queryset(self): + return super().get_queryset().\ + prefetch_related('finders')\ + .prefetch_related('classifiers')\ + .prefetch_related('selectors') + class SquareImageManager(models.Manager): def get_queryset(self): return super().get_queryset()\ @@ -46,6 +54,7 @@ class SquareModel(Target, ExtraPropertyMixin): withholes = SquareImageManager() objects = ImageManager() display = SquareDisplayManager() + just_labels = SquareLabelManager() # aliases @property diff --git a/Smartscope/core/models/target.py b/Smartscope/core/models/target.py index 6025d401..ef3b3e23 100644 --- a/Smartscope/core/models/target.py +++ b/Smartscope/core/models/target.py @@ -82,6 +82,8 @@ def is_good(self): return False return True + def is_out_of_range(self) -> bool: + return not self.finders.first().is_position_within_stage_limits() # def css_color(self, display_type, method): # if method is None: diff --git a/Smartscope/core/models/target_label.py b/Smartscope/core/models/target_label.py index 57ab9aae..aa569ae7 100644 --- a/Smartscope/core/models/target_label.py +++ b/Smartscope/core/models/target_label.py @@ -3,6 +3,7 @@ ''' from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +import math from .base_model import * @@ -32,6 +33,13 @@ class Finder(TargetLabel): class Meta(BaseModel.Meta): db_table = 'finder' + def radius_from_origin(self, offset_x=0, offset_y=0) -> float: + return math.sqrt((self.stage_x + offset_x) ** 2 + (self.stage_y + offset_y) ** 2) + + def is_position_within_stage_limits(self, stage_radius_limit:int = 975, offset_x:float=0, offset_y:float=0) -> bool: + ##NEED TO ADD OFFSETS AND NOT HARDCODE THE LIMIT + return self.radius_from_origin(offset_x=offset_x,offset_y=offset_y) <= stage_radius_limit + class Classifier(TargetLabel): label = models.CharField(max_length=30, null=True) diff --git a/Smartscope/core/pipelines/__init__.py b/Smartscope/core/pipelines/__init__.py index 74bc1bc9..3b6ed089 100644 --- a/Smartscope/core/pipelines/__init__.py +++ b/Smartscope/core/pipelines/__init__.py @@ -4,4 +4,5 @@ from .preprocessing_pipeline_cmd import PreprocessingPipelineCmd from .smartscope_preprocessing_cmd_kwargs import SmartScopePreprocessingCmdKwargs from .smartscope_preprocessing_pipeline import SmartscopePreprocessingPipeline -from .smartscope_preprocessing_pipeline_form import SmartScopePreprocessingPipelineForm \ No newline at end of file +from .smartscope_preprocessing_pipeline_form import SmartScopePreprocessingPipelineForm +from .cryosparc_live import CryoSPARCPipeline diff --git a/Smartscope/core/pipelines/cryosparc_live.py b/Smartscope/core/pipelines/cryosparc_live.py new file mode 100644 index 00000000..1c69e448 --- /dev/null +++ b/Smartscope/core/pipelines/cryosparc_live.py @@ -0,0 +1,146 @@ + +from functools import partial +import time +from typing import Dict + +import multiprocessing +import logging +from pathlib import Path + +from Smartscope.core.db_manipulations import websocket_update +from Smartscope.core.models.grid import AutoloaderGrid +from Smartscope.lib.preprocessing_methods import get_CTFFIN4_data, \ + process_hm_from_average, process_hm_from_frames, processing_worker_wrapper +from Smartscope.core.models.models_actions import update_fields + +from django.db import transaction + +from .preprocessing_pipeline import PreprocessingPipeline +from .smartscope_preprocessing_pipeline_form import SmartScopePreprocessingPipelineForm +from .smartscope_preprocessing_cmd_kwargs import SmartScopePreprocessingCmdKwargs + +from typing import Union +from pathlib import Path +from pydantic import BaseModel, Field, validator + +logger = logging.getLogger(__name__) + +from django import forms +from cryosparc.tools import CryoSPARC + +class CryoSPARCPipelineForm(forms.Form): + cs_address = forms.CharField(label='CryoSPARC URL:',help_text='Web address of CryoSPARC installation (must be accessible from the SmartScope computer).') + cs_port = forms.IntegerField(label='CryoSPARC Port:',help_text='Port of CryoSPARC installation. Defaults to 39000 if not set') + cs_license = forms.CharField(label='CryoSPARC License Key',help_text='CryoSPARC License Key') + cs_email = forms.CharField(label='CryoSPARC User Email',help_text='CryoSPARC User Email Address') + cs_password = forms.CharField(label='CryoSPARC User Password',help_text='CryoSPARC User Password') + cs_project = forms.IntegerField(label='CryoSPARC Project # P',help_text='Enter the project number of the CryoSPARC project you would like to spawn the Live sessions in. Omit the P at the beginning') + cs_worker_processes=forms.IntegerField(label='# of pre-processing workers:',help_text='Number of worker processes to spawn') + cs_preprocessing_lane = forms.CharField(label='Name of pre-processing lane:',help_text='Name of lane to use for CryoSPARC Live preprocessing lane') + frames_directory = forms.CharField(help_text='Absolute path for frame directory relative to CryoSPARC Master instance') + cs_dose=forms.FloatField(label='Dose',help_text='Total dose in e/A2') + cs_apix=forms.FloatField(label='Pixel Size',help_text='Angstroms per pixel') + cs_lanes=forms.CharField(label='Worker Lane',help_text='Name of CryoSPARC Live Worker Lane') + + def __init__(self, *args,**kwargs): + super().__init__(*args, **kwargs) + for visible in self.visible_fields(): + visible.field.widget.attrs['class'] = 'form-control' + visible.field.required = False + +class CryoSPARCCmdKwargs(BaseModel): + cs_address:str = "" + cs_port:int = 39000 + cs_license:str = "" + cs_email:str = "" + cs_password:str = "" + cs_project:int = 9999 + cs_worker_processes:int = 1 + cs_preprocessing_lane:str = "" + frames_directory:str = "" + cs_dose:float = 50.0 + cs_apix:float = 1.0 + cs_lanes:str = "" + + +class CryoSPARCPipeline(PreprocessingPipeline): + verbose_name = 'CryoSPARC Live Pre-Processing Pipeline' + name = 'cryoSPARC' + description = 'Spawn CryoSPARC Live sessions at each grid. Requires a functional CryoSPARC installation.' + to_process_queue = multiprocessing.JoinableQueue() + processed_queue = multiprocessing.Queue() + child_process = [] + to_update = [] + incomplete_processes = [] + cmdkwargs_handler = CryoSPARCCmdKwargs + pipeline_form= CryoSPARCPipelineForm + + def __init__(self, grid: AutoloaderGrid, cmd_data:Dict): + super().__init__(grid=grid) + self.microscope = self.grid.session_id.microscope_id + self.detector = self.grid.session_id.detector_id + self.cmd_data = self.cmdkwargs_handler.parse_obj(cmd_data) + logger.debug(self.cmd_data) + + self.license = self.cmd_data.cs_license + self.host = self.cmd_data.cs_address + self.base_port = self.cmd_data.cs_port + self.email = self.cmd_data.cs_email + self.password = self.cmd_data.cs_password + self.project = 'P' + str(self.cmd_data.cs_project) + self.frames_directory = self.cmd_data.frames_directory + self.dose = self.cmd_data.cs_dose + self.apix = self.cmd_data.cs_apix + self.lane = self.cmd_data.cs_lanes + + self.cs_session = "" + + def start(self): #Abstract Class Function - Required + + #Setup connection to CryoSPARC Instance + cs_instance = CryoSPARC(license=self.license,host=self.host,base_port=self.base_port,email=self.email,password=self.password) + csparc_debug = str(cs_instance.test_connection()) + logger.debug(f'CryoSPARC Connection Test: {csparc_debug}') + + #Need to check here if session already exists. If so, skip creation, if not, create. + + #Create new CryoSPARC Live session + cs_session = cs_instance.rtp.create_new_live_workspace(project_uid=str(self.project), created_by_user_id=str(cs_instance.cli.get_id_by_email(self.email)), title=str(self.grid.session_id)) + return cs_session + + #Setup lanes + cs_instance.rtp.update_compute_configuration(project_uid=str(self.project), session_uid=cs_session, key='phase_one_lane', value=str(self.lane)) + cs_instance.rtp.update_compute_configuration(project_uid=str(self.project), session_uid=cs_session, key='phase_one_gpus', value=2) + cs_instance.rtp.update_compute_configuration(project_uid=str(self.project), session_uid=cs_session, key='phase_two_lane', value=str(self.lane)) + cs_instance.rtp.update_compute_configuration(project_uid=str(self.project), session_uid=cs_session, key='auxiliary_lane', value=str(self.lane)) + + #Setup exposure group + cs_instance.rtp.exposure_group_update_value(project_uid=str(self.project), session_uid=cs_session, exp_group_id=1, name='file_engine_watch_path_abs', value=str(self.frames_directory)) + cs_instance.rtp.exposure_group_update_value(project_uid=str(self.project), session_uid=cs_session, exp_group_id=1, name='file_engine_filter', value='.tif') + cs_instance.rtp.exposure_group_finalize_and_enable(project_uid=str(self.project), session_uid=cs_session, exp_group_id=1) + + #Motion Correction Settings + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='mscope_params', param_name='accel_kv', value=float(self.microscope.voltage)) + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='mscope_params', param_name='cs_mm', value=float(self.microscope.spherical_abberation)) + + ##Need to check for if files have been written here, and get values from .mdoc file. + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='mscope_params', param_name='total_dose_e_per_A2', value=float(self.dose)) + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='mscope_params', param_name='psize_A', value=float(self.apix)) + ##Also need gain controls here (Flip/rotate) + + #Extraction Settings + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='blob_pick', param_name='diameter', value=100) + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='blob_pick', param_name='diameter_max', value=200) + cs_instance.rtp.set_param(project_uid=str(self.project), session_uid=cs_session, param_sec='extraction', param_name='box_size_pix', value=440) + + #Start the session + cs_instance.rtp.start_session(project_uid=str(self.project), session_uid=cs_session, user_id=cs_instance.cli.get_id_by_email(self.email)) + + def stop(self): #Abstract Class Function - Required + #Turn off live session + cs_instance = CryoSPARC(license=self.license,host=self.host,base_port=self.base_port,email=self.email,password=self.password) + cs_instance.rtp.pause_session(project_uid=str(self.project), session_uid=self.cs_session) + + def check_for_update(self, instance): #Abstract Class Function - Required + #Here should probably go some logic that will get the hole and image, check CryoSPARC for existing thumbnail and data, and update the object + pass diff --git a/Smartscope/core/preprocessing_pipelines.py b/Smartscope/core/preprocessing_pipelines.py index 64ce31b0..78e62980 100755 --- a/Smartscope/core/preprocessing_pipelines.py +++ b/Smartscope/core/preprocessing_pipelines.py @@ -9,10 +9,10 @@ logger = logging.getLogger(__name__) -from .pipelines import PreprocessingPipelineCmd, SmartscopePreprocessingPipeline +from .pipelines import PreprocessingPipelineCmd, SmartscopePreprocessingPipeline, CryoSPARCPipeline -PREPROCESSING_PIPELINE_FACTORY = dict(smartscopePipeline=SmartscopePreprocessingPipeline) +PREPROCESSING_PIPELINE_FACTORY = dict(smartscopePipeline=SmartscopePreprocessingPipeline, cryoSPARC=CryoSPARCPipeline) def load_preprocessing_pipeline(file:Path): if file.exists(): diff --git a/Smartscope/core/protocol_commands.py b/Smartscope/core/protocol_commands.py index 9b317538..68b0f59a 100644 --- a/Smartscope/core/protocol_commands.py +++ b/Smartscope/core/protocol_commands.py @@ -40,6 +40,14 @@ def moveStage(scope,params,instance) -> None: stage_x, stage_y, stage_z = finder.stage_x, finder.stage_y, finder.stage_z scope.moveStage(stage_x,stage_y,stage_z) +def moveStageWithAtlasToSearchOffset(scope,params,instance) -> None: + """Moves the stage to the instance position with an offset""" + offset_x = scope.atlas_settings.atlas_to_search_offset_x + offset_y = scope.atlas_settings.atlas_to_search_offset_y + finder = instance.finders.first() + stage_x, stage_y, stage_z = finder.stage_x, finder.stage_y, finder.stage_z + scope.moveStage(stage_x+offset_x,stage_y+offset_y,stage_z) + def eucentricSearch(scope,params,instance): """Calculates eucentricity using the Search preset. @@ -152,6 +160,7 @@ def setFocusPosition(scope,params,instance): realignToSquare=realignToSquare, square=square, moveStage=moveStage, + moveStageWithAtlasToSearchOffset=moveStageWithAtlasToSearchOffset, eucentricSearch=eucentricSearch, eucentricMediumMag=eucentricMediumMag, mediumMagHole=mediumMagHole, diff --git a/Smartscope/core/run_grid.py b/Smartscope/core/run_grid.py index 112d32f8..d0d7ca5d 100644 --- a/Smartscope/core/run_grid.py +++ b/Smartscope/core/run_grid.py @@ -161,12 +161,15 @@ def run_grid( params, hole ) + if hole.status == status.SKIPPED: + continue hole = update(hole, status=status.ACQUIRED, completion_time=timezone.now() ) RunHole.process_hole_image(hole, grid, microscope) - + if hole.status == status.SKIPPED: + continue # process high image scope.focusDrift( params.target_defocus_min, @@ -176,7 +179,9 @@ def run_grid( ) scope.reset_image_shift_values() for hm in hole.targets.exclude(status__in=[status.ACQUIRED,status.COMPLETED]).order_by('hole_id__number'): - hm = update(hm, status=status.STARTED) + hm = update(hm, refresh_from_db=False, status=status.STARTED) + if hm.hole_id.status == status.SKIPPED: + break hm = runAcquisition( scope, protocol.highMag.acquisition, @@ -245,11 +250,11 @@ def run_grid( def get_queue(grid): - square = grid.squaremodel_set.\ - filter(selected=True, status__in=[status.QUEUED, status.STARTED]).\ + square = grid.squaremodel_set.filter(selected=True).\ + exclude(status__in=[status.SKIPPED, status.COMPLETED]).\ order_by('number').first() hole = grid.holemodel_set.filter(selected=True, square_id__status=status.COMPLETED).\ - exclude(status=status.COMPLETED).\ + exclude(status__in=[status.SKIPPED, status.COMPLETED]).\ order_by('square_id__completion_time', 'number').first() return square, hole#[h for h in holes if not h.bisgroup_acquired] diff --git a/Smartscope/core/settings/server_docker.py b/Smartscope/core/settings/server_docker.py index ad331184..f8f6dbc0 100755 --- a/Smartscope/core/settings/server_docker.py +++ b/Smartscope/core/settings/server_docker.py @@ -18,9 +18,9 @@ PROJECT_DIR = os.path.dirname(BASE_DIR) AUTOSCREENDIR = os.getenv('AUTOSCREENDIR') +USE_CUSTOM_PATHS = eval(os.getenv('USE_CUSTOM_PATHS', 'False')) TEMPDIR = os.getenv('TEMPDIR') - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ diff --git a/Smartscope/core/svg_plots.py b/Smartscope/core/svg_plots.py index a3e06334..4889833e 100644 --- a/Smartscope/core/svg_plots.py +++ b/Smartscope/core/svg_plots.py @@ -91,6 +91,9 @@ def drawAtlas(atlas, targets, display_type, method) -> draw.Drawing: if color is not None: sz = floor(sqrt(i.area)) finder = list(i.finders.all())[0] + if not finder.is_position_within_stage_limits(): + color = '#505050' + label = 'Out of range' x = finder.x - sz // 2 y = (finder.y - sz // 2) r = draw.Rectangle(x, y, sz, sz, id=i.pk, stroke_width=floor(d.width / 300), stroke=color, fill=color, fill_opacity=0, label=label, @@ -131,6 +134,9 @@ def drawSquare(square, targets, display_type, method) -> draw.Drawing: color, label, prefix = css_color(i, display_type, method) if color is not None: finder = list(i.finders.all())[0] + if not finder.is_position_within_stage_limits(): + color = '#505050' + label = 'Out of range' x = finder.x y = finder.y c = draw.Circle(x, y, i.radius, id=i.pk, stroke_width=floor(d.width / 250), stroke=color, fill=color, fill_opacity=0, label=label, diff --git a/Smartscope/server/api/migrations/0016_screeningsession_user_and_more.py b/Smartscope/server/api/migrations/0016_screeningsession_user_and_more.py new file mode 100644 index 00000000..fc025d51 --- /dev/null +++ b/Smartscope/server/api/migrations/0016_screeningsession_user_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.2 on 2023-10-20 20:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('API', '0015_detector_atlas_c2_aperture_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='screeningsession', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, to_field='username'), + ), + migrations.AlterField( + model_name='gridcollectionparams', + name='coldfegflash_delay', + field=models.IntegerField(default=-1, help_text='Number of hours between cold FEG flashes. Will only work if the microscope has a cold FEG. Values smaller than 0 will disable the procedure.', verbose_name='ColdFEG Flash Delay'), + ), + migrations.CreateModel( + name='CustomUserPath', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(editable=False, max_length=300)), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, to_field='username')), + ], + options={ + 'db_table': 'customuserpath', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CustomGroupPath', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(editable=False, max_length=300)), + ('group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group', to_field='name')), + ], + options={ + 'db_table': 'customgrouppath', + 'abstract': False, + }, + ), + ] diff --git a/Smartscope/server/api/migrations/0017_alter_customgrouppath_group_and_more.py b/Smartscope/server/api/migrations/0017_alter_customgrouppath_group_and_more.py new file mode 100644 index 00000000..34725101 --- /dev/null +++ b/Smartscope/server/api/migrations/0017_alter_customgrouppath_group_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.2 on 2023-10-20 20:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0012_alter_user_first_name_max_length'), + ('API', '0016_screeningsession_user_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='customgrouppath', + name='group', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group', to_field='name'), + ), + migrations.AlterField( + model_name='customgrouppath', + name='path', + field=models.CharField(max_length=300), + ), + migrations.AlterField( + model_name='customuserpath', + name='path', + field=models.CharField(max_length=300), + ), + migrations.AlterField( + model_name='customuserpath', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, to_field='username'), + ), + ] diff --git a/Smartscope/server/api/templates/mapcard.html b/Smartscope/server/api/templates/mapcard.html index 0a6b692a..a9963c3a 100755 --- a/Smartscope/server/api/templates/mapcard.html +++ b/Smartscope/server/api/templates/mapcard.html @@ -1,61 +1,7 @@ {% load rest_framework %} {% load static %}