Skip to content

Commit 0c3e4f1

Browse files
authored
Merge pull request #791 from henrypinkard/mmpycorexrefactor
Factor out mmpycorex into seperate repo
2 parents ef136c2 + 32a95eb commit 0c3e4f1

15 files changed

+43
-475
lines changed

pycromanager/__init__.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
name = "pycromanager"
22

3-
from pycromanager.acquisition.java_backend_acquisitions import JavaBackendAcquisition, MagellanAcquisition, XYTiledAcquisition, ExploreAcquisition
3+
from pycromanager.acquisition.java_backend_acquisitions import (JavaBackendAcquisition, MagellanAcquisition,
4+
XYTiledAcquisition, ExploreAcquisition)
45
from pycromanager.acquisition.acquisition_superclass import multi_d_acquisition_events
56
from pycromanager.acquisition.acq_constructor import Acquisition
6-
from pycromanager.headless import start_headless, stop_headless
77
from pycromanager.mm_java_classes import Studio, Magellan
8-
from pycromanager.core import Core
9-
from pyjavaz import JavaObject, JavaClass, PullSocket, PushSocket
108
from pycromanager.acquisition.acq_eng_py.main.acq_notification import AcqNotification
11-
from ndstorage import Dataset
9+
10+
from pycromanager.headless import start_headless, stop_headless
11+
from mmpycorex import download_and_install_mm, find_existing_mm_install, Core
12+
1213
from ._version import __version__, version_info

pycromanager/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
version_info = (0, 34, 8)
1+
version_info = (0, 35, 0)
22
__version__ = ".".join(map(str, version_info))

pycromanager/acquisition/acq_constructor.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from pycromanager.headless import _PYMMCORES
21
from pycromanager.acquisition.java_backend_acquisitions import JavaBackendAcquisition
32
from pycromanager.acquisition.python_backend_acquisitions import PythonBackendAcquisition
43
from pycromanager.acquisition.acquisition_superclass import Acquisition as PycromanagerAcquisitionBase
54
from inspect import signature
5+
from mmpycorex import is_pymmcore_active
66

77
# This is a convenience class that automatically selects the appropriate acquisition
88
# type based on backend is running. It is subclassed from the base acquisition class
@@ -29,7 +29,7 @@ def __new__(cls,
2929
dict(signature(Acquisition.__init__).parameters.items())[arg_name].default)
3030
for arg_name in arg_names }
3131

32-
if _PYMMCORES:
32+
if is_pymmcore_active():
3333
# Python backend detected, so create a python backend acquisition
3434
specific_arg_names = [k for k in signature(PythonBackendAcquisition.__init__).parameters.keys() if k != 'self']
3535
for name in specific_arg_names:
File renamed without changes.

pycromanager/acquisition/acquisition_superclass.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55
import copy
66
import types
77
import numpy as np
8-
from typing import Union, List, Iterable
8+
from typing import List, Iterable
99
import warnings
1010
from abc import ABCMeta, abstractmethod
1111
from docstring_inheritance import NumpyDocstringInheritanceMeta
1212
import queue
1313
import weakref
14-
from pycromanager.acq_future import AcqNotification, AcquisitionFuture
15-
import os
14+
from pycromanager.acquisition.acq_future import AcqNotification, AcquisitionFuture
1615
import threading
1716
from inspect import signature
18-
from typing import Generator
1917
from types import GeneratorType
20-
import time
2118

2219
from queue import Queue
2320
from typing import Generator, Dict, Union

pycromanager/acquisition/java_backend_acquisitions.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""
22
The Pycro-manager Acquisiton system
33
"""
4-
import json
54
import logging
65
import warnings
7-
import weakref
86

97
import numpy as np
108
import multiprocessing
119
import threading
1210
from inspect import signature
13-
import time
1411
from pyjavaz import deserialize_array
1512
from pyjavaz import PullSocket, PushSocket, JavaObject, JavaClass
1613
from pyjavaz import DEFAULT_BRIDGE_PORT as DEFAULT_PORT
@@ -19,11 +16,10 @@
1916

2017
from ndstorage import Dataset
2118
import os.path
22-
import queue
2319
from docstring_inheritance import NumpyDocstringInheritanceMeta
2420
from pycromanager.acquisition.acquisition_superclass import Acquisition
2521
import traceback
26-
from pycromanager.acq_future import AcqNotification, AcquisitionFuture
22+
from pycromanager.acquisition.acq_future import AcqNotification
2723
import json
2824

2925
logger = logging.getLogger(__name__)
@@ -337,7 +333,7 @@ def __init__(
337333
import napari
338334
except:
339335
raise Exception('Napari must be installed in order to use this feature')
340-
from pycromanager.napari_util import start_napari_signalling
336+
from pycromanager.acquisition.napari_util import start_napari_signalling
341337
assert isinstance(napari_viewer, napari.Viewer), 'napari_viewer must be an instance of napari.Viewer'
342338
self._napari_viewer = napari_viewer
343339
start_napari_signalling(self._napari_viewer, self.get_dataset())
File renamed without changes.

pycromanager/acquisition/python_backend_acquisitions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pycromanager.acquisition.acq_eng_py.main.AcqEngPy_Acquisition import Acquisition as pymmcore_Acquisition
44
from pycromanager.acquisition.acquisition_superclass import _validate_acq_events, Acquisition
55
from pycromanager.acquisition.acq_eng_py.main.acquisition_event import AcquisitionEvent
6-
from pycromanager.acq_future import AcqNotification
6+
from pycromanager.acquisition.acq_future import AcqNotification
77
import threading
88
from inspect import signature
99
import traceback
@@ -110,7 +110,7 @@ def post_notification(notification):
110110
import napari
111111
except:
112112
raise Exception('Napari must be installed in order to use this feature')
113-
from pycromanager.napari_util import start_napari_signalling
113+
from pycromanager.acquisition.napari_util import start_napari_signalling
114114
assert isinstance(napari_viewer, napari.Viewer), 'napari_viewer must be an instance of napari.Viewer'
115115
self._napari_viewer = napari_viewer
116116
start_napari_signalling(self._napari_viewer, self.get_dataset())

pycromanager/core.py

-15
This file was deleted.

pycromanager/headless.py

+19-186
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,21 @@
1-
import logging
2-
import subprocess
3-
import platform
4-
import atexit
5-
import threading
6-
import types
7-
import os
8-
1+
from mmpycorex import create_core_instance, terminate_core_instances
2+
from mmpycorex import Core
93
from pycromanager.acquisition.acq_eng_py.internal.engine import Engine
10-
from pymmcore import CMMCore
4+
from pyjavaz import DEFAULT_BRIDGE_PORT
5+
import atexit
116
import pymmcore
12-
from pyjavaz import DEFAULT_BRIDGE_PORT, server_terminated
13-
14-
import re
15-
16-
logger = logging.getLogger(__name__)
17-
18-
class TaggedImage:
19-
20-
def __init__(self, tags, pix):
21-
self.tags = tags
22-
self.pix = pix
23-
24-
def _camel_to_snake(name):
25-
"""
26-
Convert camelCase string to snake_case
27-
"""
28-
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
29-
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
30-
31-
def _create_pymmcore_instance():
32-
"""
33-
Make a subclass of CMMCore with two differences:
34-
35-
1. All methods are converted to snake_case
36-
2. add convenience methods to match the MMCoreJ API:
37-
"""
38-
39-
# Create a new dictionary for the class attributes
40-
new_attributes = {}
41-
42-
# Iterate through the original attributes
43-
for attr_name, attr_value in vars(CMMCore).items():
44-
# If it's a dunder method, skip it (we don't want to override these)
45-
if attr_name.startswith("__") and attr_name.endswith("__"):
46-
continue
47-
# If the attribute is callable (i.e., a method), convert its name to snake_case and add it
48-
if callable(attr_value):
49-
new_attr_name = _camel_to_snake(attr_name)
50-
new_attributes[new_attr_name] = attr_value
51-
52-
# Create and return a new class that subclasses the original class and has the new attributes
53-
clz = type(CMMCore.__name__ + "SnakeCase", (CMMCore,), new_attributes)
54-
55-
instance = clz()
56-
57-
def pop_next_tagged_image(self):
58-
md = pymmcore.Metadata()
59-
pix = self.pop_next_image_md(0, 0, md)
60-
tags = {key: md.GetSingleTag(key).GetValue() for key in md.GetKeys()}
61-
return TaggedImage(tags, pix)
62-
63-
def get_tagged_image(core, cam_index, camera, height, width, binning=None, pixel_type=None, roi_x_start=None,
64-
roi_y_start=None):
65-
"""
66-
Different signature than the Java version because of difference in metadata handling in the swig layers
67-
"""
68-
pix = core.get_image()
69-
md = pymmcore.Metadata()
70-
# most of the same tags from pop_next_tagged_image, which may not be the same as the MMCoreJ version of this function
71-
tags = {'Camera': camera, 'Height': height, 'Width': width, 'PixelType': pixel_type,
72-
'CameraChannelIndex': cam_index}
73-
# Could optionally add these for completeness but there might be a performance hit
74-
if binning is not None:
75-
tags['Binning'] = binning
76-
if roi_x_start is not None:
77-
tags['ROI-X-start'] = roi_x_start
78-
if roi_y_start is not None:
79-
tags['ROI-Y-start'] = roi_y_start
80-
81-
return TaggedImage(tags, pix)
82-
83-
instance.get_tagged_image = types.MethodType(get_tagged_image, instance)
84-
instance.pop_next_tagged_image = types.MethodType(pop_next_tagged_image, instance)
85-
86-
# attach TaggedImage class
87-
instance.TaggedImage = TaggedImage
88-
return instance
89-
90-
91-
_JAVA_HEADLESS_SUBPROCESSES = []
92-
_PYMMCORES = []
93-
94-
def stop_headless(debug=False):
95-
96-
for p in _JAVA_HEADLESS_SUBPROCESSES:
97-
port = p.port
98-
if debug:
99-
logger.debug('Stopping headless process with pid {}'.format(p.pid))
100-
p.terminate()
101-
server_terminated(port)
102-
if debug:
103-
logger.debug('Waiting for process with pid {} to terminate'.format(p.pid))
104-
p.wait() # wait for process to terminate
105-
if debug:
106-
logger.debug('Process with pid {} terminated'.format(p.pid))
107-
_JAVA_HEADLESS_SUBPROCESSES.clear()
108-
if debug:
109-
logger.debug('Stopping {} pymmcore instances'.format(len(_PYMMCORES)))
110-
for c in _PYMMCORES:
111-
if debug:
112-
logger.debug('Stopping pymmcore instance')
113-
c.unloadAllDevices()
114-
if debug:
115-
logger.debug('Unloaded all devices')
116-
Engine.get_instance().shutdown()
117-
if debug:
118-
logger.debug('Engine shut down')
119-
_PYMMCORES.clear()
120-
if debug:
121-
logger.debug('Headless stopped')
7+
import types
1228

123-
# make sure any Java processes are cleaned up when Python exits
124-
atexit.register(stop_headless)
1259

12610
def start_headless(
12711
mm_app_path: str, config_file: str=None, java_loc: str=None,
12812
python_backend=False, core_log_path: str='',
12913
buffer_size_mb: int=1024, max_memory_mb: int=2000,
13014
port: int=DEFAULT_BRIDGE_PORT, debug=False):
13115
"""
132-
Start a Java process that contains the neccessary libraries for pycro-manager to run,
133-
so that it can be run independently of the Micro-Manager GUI/application. This calls
134-
will create and initialize MMCore with the configuration file provided.
16+
Start an instance of the Micro-Manager core and acquisition engine in headless mode. This can be
17+
either a Python (i.e. pymmcore) or Java (i.e. MMCoreJ) backend. If a Python backend is used,
18+
the core will be started in the same process.
13519
13620
On windows plaforms, the Java Runtime Environment will be grabbed automatically
13721
as it is installed along with the Micro-Manager application.
@@ -161,68 +45,17 @@ def start_headless(
16145
debug : bool
16246
Print debug messages
16347
"""
164-
48+
create_core_instance(
49+
mm_app_path=mm_app_path, config_file=config_file, java_loc=java_loc,
50+
python_backend=python_backend, core_log_path=core_log_path,
51+
buffer_size_mb=buffer_size_mb, max_memory_mb=max_memory_mb,
52+
port=port, debug=debug)
16553
if python_backend:
166-
mmc = _create_pymmcore_instance()
167-
mmc.set_device_adapter_search_paths([mm_app_path])
168-
if config_file is not None and config_file != "":
169-
mmc.load_system_configuration(config_file)
170-
mmc.set_circular_buffer_memory_footprint(buffer_size_mb)
171-
_PYMMCORES.append(mmc) # Store so it doesn't get garbage collected
172-
Engine(mmc)
173-
else:
174-
classpath = mm_app_path + '/plugins/Micro-Manager/*'
175-
if java_loc is None:
176-
if platform.system() == "Windows":
177-
# windows comes with its own JRE
178-
java_loc = mm_app_path + "/jre/bin/javaw.exe"
179-
else:
180-
java_loc = "java"
181-
if debug:
182-
logger.debug(f'Java location: {java_loc}')
183-
#print classpath
184-
logger.debug(f'Classpath: {classpath}')
185-
# print stuff in the classpath directory
186-
logger.debug('Contents of classpath directory:')
187-
for f in os.listdir(classpath.split('*')[0]):
188-
logger.debug(f)
189-
190-
# This starts Java process and instantiates essential objects (core,
191-
# acquisition engine, ZMQServer)
192-
process = subprocess.Popen(
193-
[
194-
java_loc,
195-
"-classpath",
196-
classpath,
197-
"-Dsun.java2d.dpiaware=false",
198-
f"-Xmx{max_memory_mb}m",
199-
# This is used by MM desktop app but breaks things on MacOS...Don't think its neccessary
200-
# "-XX:MaxDirectMemorySize=1000",
201-
"org.micromanager.remote.HeadlessLauncher",
202-
str(port),
203-
config_file if config_file is not None else '',
204-
str(buffer_size_mb),
205-
core_log_path,
206-
], cwd=mm_app_path, stdout=subprocess.PIPE
207-
)
208-
process.port = port
209-
_JAVA_HEADLESS_SUBPROCESSES.append(process)
210-
211-
started = False
212-
output = True
213-
# Some drivers output various status messages which need to be skipped over to look for the STARTED token.
214-
while output and not started:
215-
output = process.stdout.readline()
216-
started = "STARTED" in output.decode('utf-8')
217-
if not started:
218-
raise Exception('Error starting headless mode')
219-
if debug:
220-
logger.debug('Headless mode started')
221-
def loggerFunction():
222-
while process in _JAVA_HEADLESS_SUBPROCESSES:
223-
line = process.stdout.readline().decode('utf-8')
224-
if line.strip() != '':
225-
logger.debug(line)
226-
threading.Thread(target=loggerFunction).start()
54+
Engine(Core())
22755

56+
def stop_headless(debug=False):
57+
terminate_core_instances(debug=debug)
58+
Engine.get_instance().shutdown()
22859

60+
# make sure any Java processes are cleaned up when Python exits
61+
atexit.register(stop_headless)

0 commit comments

Comments
 (0)