Skip to content

Commit

Permalink
Add inheritance (#91)
Browse files Browse the repository at this point in the history
* Add simple experiment base class

* Use pytest's `tmp_path` for IO testing

* Refactor format tests

* Properly separate read/write tests

* Add context manager for experiments

* Refactor experiment tests

* Microscope base class

* Sort alphabethically

* Use microscope base class; add type hints

* Base camera class proposal

* Add mock cameras for testing

* Run ruff

* Use the camera base class

* Add rudimentary camera tests

* Add type hints

* PR feedback; 3.7 compatibility

* Run pre-commit
  • Loading branch information
viljarjf authored Oct 22, 2024
1 parent 6c4ab42 commit 2576993
Show file tree
Hide file tree
Showing 33 changed files with 708 additions and 305 deletions.
10 changes: 6 additions & 4 deletions src/instamatic/TEMController/TEMController.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import time
from collections import namedtuple
from concurrent.futures import ThreadPoolExecutor
from typing import Tuple
from typing import Optional, Tuple

import numpy as np

from instamatic import config
from instamatic.camera import Camera
from instamatic.camera.camera_base import CameraBase
from instamatic.exceptions import TEMControllerError
from instamatic.formats import write_tiff
from instamatic.image_utils import rotate_image
from instamatic.TEMController.microscope_base import MicroscopeBase

from .deflectors import *
from .lenses import *
Expand Down Expand Up @@ -89,7 +91,7 @@ class TEMController:
cam: Camera control object (see instamatic.camera) [optional]
"""

def __init__(self, tem, cam=None):
def __init__(self, tem: MicroscopeBase, cam: Optional[CameraBase] = None):
super().__init__()

self._executor = ThreadPoolExecutor(max_workers=1)
Expand Down Expand Up @@ -122,7 +124,7 @@ def __init__(self, tem, cam=None):

def __repr__(self):
return (f'Mode: {self.tem.getFunctionMode()}\n'
f'High tension: {self.high_tension/1000:.0f} kV\n'
f'High tension: {self.high_tension / 1000:.0f} kV\n'
f'Current density: {self.current_density:.2f} pA/cm2\n'
f'{self.gunshift}\n'
f'{self.guntilt}\n'
Expand Down Expand Up @@ -244,7 +246,7 @@ def run_script(self, script: str, verbose: bool = True) -> None:
t1 = time.perf_counter()

if verbose:
print(f'\nScript finished in {t1-t0:.4f} s')
print(f'\nScript finished in {t1 - t0:.4f} s')

def get_stagematrix(self, binning: int = None, mag: int = None, mode: int = None):
"""Helper function to get the stage matrix from the config file. The
Expand Down
3 changes: 2 additions & 1 deletion src/instamatic/TEMController/fei_microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from instamatic import config
from instamatic.exceptions import FEIValueError, TEMCommunicationError
from instamatic.TEMController.microscope_base import MicroscopeBase

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,7 +46,7 @@ def get_camera_length_mapping():
CameraLengthMapping = get_camera_length_mapping()


class FEIMicroscope:
class FEIMicroscope(MicroscopeBase):
"""Python bindings to the FEI microscope using the COM interface."""

def __init__(self, name='fei'):
Expand Down
3 changes: 2 additions & 1 deletion src/instamatic/TEMController/fei_simu_microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from instamatic import config
from instamatic.exceptions import FEIValueError, TEMCommunicationError
from instamatic.TEMController.microscope_base import MicroscopeBase

logger = logging.getLogger(__name__)

Expand All @@ -17,7 +18,7 @@
MAX = 1.0


class FEISimuMicroscope:
class FEISimuMicroscope(MicroscopeBase):
"""Python bindings to the FEI simulated microscope using the COM
interface."""

Expand Down
3 changes: 2 additions & 1 deletion src/instamatic/TEMController/jeol_microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from instamatic import config
from instamatic.exceptions import JEOLValueError, TEMCommunicationError, TEMValueError
from instamatic.TEMController.microscope_base import MicroscopeBase

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,7 +46,7 @@
# Piezo stage seems to operate on a different level than standard XY


class JeolMicroscope:
class JeolMicroscope(MicroscopeBase):
"""Python bindings to the JEOL microscope using the COM interface."""

def __init__(self, name: str = 'jeol'):
Expand Down
5 changes: 3 additions & 2 deletions src/instamatic/TEMController/microscope.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from instamatic import config
from instamatic.TEMController.microscope_base import MicroscopeBase

default_tem_interface = config.microscope.interface

__all__ = ['Microscope', 'get_tem']


def get_tem(interface: str):
def get_tem(interface: str) -> 'type[MicroscopeBase]':
"""Grab tem class with the specific 'interface'."""
simulate = config.settings.simulate

Expand All @@ -28,7 +29,7 @@ def get_tem(interface: str):
return cls


def Microscope(name: str = None, use_server: bool = False):
def Microscope(name: str = None, use_server: bool = False) -> MicroscopeBase:
"""Generic class to load microscope interface class.
name: str
Expand Down
176 changes: 176 additions & 0 deletions src/instamatic/TEMController/microscope_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from abc import ABC, abstractmethod
from typing import Tuple


class MicroscopeBase(ABC):
@abstractmethod
def getBeamShift(self) -> Tuple[int, int]:
pass

@abstractmethod
def getBeamTilt(self) -> Tuple[int, int]:
pass

@abstractmethod
def getBrightness(self) -> int:
pass

@abstractmethod
def getCondensorLensStigmator(self) -> Tuple[int, int]:
pass

@abstractmethod
def getCurrentDensity(self) -> float:
pass

@abstractmethod
def getDiffFocus(self, confirm_mode: bool) -> int:
pass

@abstractmethod
def getDiffShift(self) -> Tuple[int, int]:
pass

@abstractmethod
def getFunctionMode(self) -> str:
pass

@abstractmethod
def getGunShift(self) -> Tuple[int, int]:
pass

@abstractmethod
def getGunTilt(self) -> Tuple[int, int]:
pass

@abstractmethod
def getHTValue(self) -> float:
pass

@abstractmethod
def getImageShift1(self) -> Tuple[int, int]:
pass

@abstractmethod
def getImageShift2(self) -> Tuple[int, int]:
pass

@abstractmethod
def getIntermediateLensStigmator(self) -> Tuple[int, int]:
pass

@abstractmethod
def getMagnification(self) -> int:
pass

@abstractmethod
def getMagnificationAbsoluteIndex(self) -> int:
pass

@abstractmethod
def getMagnificationIndex(self) -> int:
pass

@abstractmethod
def getMagnificationRanges(self) -> dict:
pass

@abstractmethod
def getObjectiveLensStigmator(self) -> Tuple[int, int]:
pass

@abstractmethod
def getSpotSize(self) -> int:
pass

@abstractmethod
def getStagePosition(self) -> Tuple[int, int, int, int, int]:
pass

@abstractmethod
def isBeamBlanked(self) -> bool:
pass

@abstractmethod
def isStageMoving(self) -> bool:
pass

@abstractmethod
def release_connection(self) -> None:
pass

@abstractmethod
def setBeamBlank(self, mode: bool) -> None:
pass

@abstractmethod
def setBeamShift(self, x: int, y: int) -> None:
pass

@abstractmethod
def setBeamTilt(self, x: int, y: int) -> None:
pass

@abstractmethod
def setBrightness(self, value: int) -> None:
pass

@abstractmethod
def setCondensorLensStigmator(self, x: int, y: int) -> None:
pass

@abstractmethod
def setDiffFocus(self, value: int, confirm_mode: bool) -> None:
pass

@abstractmethod
def setDiffShift(self, x: int, y: int) -> None:
pass

@abstractmethod
def setFunctionMode(self, value: int) -> None:
pass

@abstractmethod
def setGunShift(self, x: int, y: int) -> None:
pass

@abstractmethod
def setGunTilt(self, x: int, y: int) -> None:
pass

@abstractmethod
def setImageShift1(self, x: int, y: int) -> None:
pass

@abstractmethod
def setImageShift2(self, x: int, y: int) -> None:
pass

@abstractmethod
def setIntermediateLensStigmator(self, x: int, y: int) -> None:
pass

@abstractmethod
def setMagnification(self, value: int) -> None:
pass

@abstractmethod
def setMagnificationIndex(self, index: int) -> None:
pass

@abstractmethod
def setObjectiveLensStigmator(self, x: int, y: int) -> None:
pass

@abstractmethod
def setSpotSize(self, value: int) -> None:
pass

@abstractmethod
def setStagePosition(self, x: int, y: int, z: int, a: int, b: int, wait: bool) -> None:
pass

@abstractmethod
def stopStage(self) -> None:
pass
3 changes: 2 additions & 1 deletion src/instamatic/TEMController/simu_microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from instamatic import config
from instamatic.exceptions import TEMValueError
from instamatic.TEMController.microscope_base import MicroscopeBase

NTRLMAPPING = {
'GUN1': 0,
Expand All @@ -30,7 +31,7 @@
MIN = 0


class SimuMicroscope:
class SimuMicroscope(MicroscopeBase):
"""Simulates a microscope connection.
Has the same variables as the real JEOL/FEI equivalents, but does
Expand Down
2 changes: 1 addition & 1 deletion src/instamatic/camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def get_cam(interface: str = None):
elif interface == 'gatansocket':
from instamatic.camera.camera_gatan2 import CameraGatan2 as cam
elif interface in ('timepix', 'pytimepix'):
from instamatic.camera import camera_timepix as cam
from instamatic.camera.camera_timepix import CameraTPX as cam
elif interface in ('emmenu', 'tvips'):
from instamatic.camera.camera_emmenu import CameraEMMENU as cam
elif interface == 'serval':
Expand Down
71 changes: 71 additions & 0 deletions src/instamatic/camera/camera_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from abc import ABC, abstractmethod
from typing import List, Tuple

from numpy import ndarray

from instamatic import config


class CameraBase(ABC):

# Set manually
name: str
streamable: bool

# Set by `load_defaults`
camera_rotation_vs_stage_xy: float
default_binsize: int
default_exposure: float
dimensions: Tuple[int, int]
interface: str
possible_binsizes: List[int]
stretch_amplitude: float
stretch_azimuth: float

@abstractmethod
def __init__(self, name: str):
self.name = name
self.load_defaults()

@abstractmethod
def establish_connection(self):
pass

@abstractmethod
def release_connection(self):
pass

@abstractmethod
def get_image(
self, exposure: float = None, binsize: int = None, **kwargs
) -> ndarray:
pass

def get_movie(
self, n_frames: int, exposure: float = None, binsize: int = None, **kwargs
) -> List[ndarray]:
"""Basic implementation, subclasses should override with appropriate
optimization."""
return [
self.get_image(exposure=exposure, binsize=binsize, **kwargs)
for _ in range(n_frames)
]

def __enter__(self):
self.establish_connection()
return self

def __exit__(self, kind, value, traceback):
self.release_connection()

def get_camera_dimensions(self) -> Tuple[int, int]:
return self.dimensions

def get_name(self) -> str:
return self.name

def load_defaults(self):
if self.name != config.settings.camera:
config.load_camera_config(camera_name=self.name)
for key, val in config.camera.mapping.items():
setattr(self, key, val)
Loading

0 comments on commit 2576993

Please sign in to comment.